Audacity 3.2.0
ProjectCloudExtension.cpp
Go to the documentation of this file.
1/* SPDX-License-Identifier: GPL-2.0-or-later */
2/*!********************************************************************
3
4 Audacity: A Digital Audio Editor
5
6 ProjectCloudExtension.h
7
8 Dmitry Vedenko
9
10**********************************************************************/
11
13
14#include <algorithm>
15#include <cstring>
16#include <vector>
17
19#include "CloudSyncDTO.h"
21#include "ServiceConfig.h"
22
23#include "CodeConversions.h"
24
25#include "Project.h"
26
27#include "ProjectFileIO.h"
28#include "ProjectSerializer.h"
29
30#include "BasicUI.h"
31
32#include "MemoryX.h"
33
35
36#include "Track.h"
37
39{
40namespace
41{
44 { return std::make_shared<ProjectCloudExtension>(project); }
45};
46} // namespace
47
49{
51 std::shared_ptr<ProjectUploadOperation> Operation;
52 std::optional<CreateSnapshotResponse> SnapshotResponse;
53
54 int64_t BlocksHandled { 0 };
55 int64_t BlocksTotal { 0 };
56
57 bool ProjectDataUploaded { false };
58 bool ReadyForUpload { false };
59 bool Synced { false };
60};
61
63 private Observer::Publisher<CloudStatusChangedMessage>
64{
65 void Enqueue(CloudStatusChangedMessage message, bool canMerge)
66 {
67 auto lock = std::lock_guard { QueueMutex };
68
69 if (Queue.empty() || !canMerge)
70 Queue.push_back({ message, canMerge });
71 else if (Queue.back().second)
72 Queue.back().first = message;
73 else
74 Queue.push_back({ message, canMerge });
75 }
76
78 {
79 QueueType queue;
80 {
81 auto lock = std::lock_guard { QueueMutex };
82 std::swap(queue, Queue);
83 }
84
85 auto lock = std::lock_guard { ObserverMutex };
86
87 for (const auto& [message, _] : queue)
88 Publish(message);
89 }
90
92 SubscribeSafe(std::function<void(const CloudStatusChangedMessage&)> callback)
93 {
94 auto lock = std::lock_guard { ObserverMutex };
95 return Subscribe(std::move(callback));
96 }
97
98 using QueueType = std::vector<std::pair<CloudStatusChangedMessage, bool>>;
99
100 std::mutex QueueMutex;
102 std::recursive_mutex ObserverMutex;
103};
104
106 : mProject { project }
107 , mProjectPathChangedSubscription { ProjectFileIO::Get(project).Subscribe(
108 [this](auto message)
109 {
112 }) }
113 , mAsyncStateNotifier { std::make_unique<CloudStatusChangedNotifier>() }
114 , mUIStateNotifier { std::make_unique<CloudStatusChangedNotifier>() }
115{
116 if (!ProjectFileIO::Get(project).IsTemporary())
117 UpdateIdFromDatabase();
118}
119
120ProjectCloudExtension::~ProjectCloudExtension() = default;
121
123{
124 return project.AttachedObjects::Get<ProjectCloudExtension&>(key);
125}
126
129{
130 return Get(const_cast<AudacityProject&>(project));
131}
132
133bool ProjectCloudExtension::IsCloudProject() const
134{
135 auto lock = std::lock_guard { mIdentifiersMutex };
136 return !mProjectId.empty();
137}
138
139void ProjectCloudExtension::OnLoad()
140{
141 UpdateIdFromDatabase();
142}
143
144void ProjectCloudExtension::OnSyncStarted()
145{
146 auto element = std::make_shared<UploadQueueElement>();
147
148 element->Data.Tracks = TrackList::Create(nullptr);
149
150 for (auto pTrack : TrackList::Get(mProject))
151 {
152 if (pTrack->GetId() == TrackId {})
153 // Don't copy a pending added track
154 continue;
155 element->Data.Tracks->Add(pTrack->Duplicate());
156 }
157
158 auto lock = std::lock_guard { mUploadQueueMutex };
159 mUploadQueue.push_back(std::move(element));
160}
161
163 std::shared_ptr<ProjectUploadOperation> uploadOperation,
164 int64_t missingBlocksCount, bool needsProjectUpload)
165{
166 auto element = std::make_shared<UploadQueueElement>();
167 element->Operation = uploadOperation;
168 element->BlocksTotal = missingBlocksCount;
169 element->ProjectDataUploaded = !needsProjectUpload;
170
171 {
172 auto lock = std::lock_guard { mUploadQueueMutex };
173 mUploadQueue.push_back(element);
174 }
175
176 uploadOperation->Start();
177 UnsafeUpdateProgress();
178}
179
180void ProjectCloudExtension::OnUploadOperationCreated(
181 std::shared_ptr<ProjectUploadOperation> uploadOperation)
182{
183 auto lock = std::lock_guard { mUploadQueueMutex };
184 assert(mUploadQueue.size() > 0);
185
186 if (mUploadQueue.empty())
187 return;
188
189 mUploadQueue.back()->Operation = uploadOperation;
190 UnsafeUpdateProgress();
191}
192
193void ProjectCloudExtension::OnBlocksHashed(
194 ProjectUploadOperation& uploadOperation)
195{
196 auto lock = std::lock_guard { mUploadQueueMutex };
197 auto element = UnsafeFindUploadQueueElement(uploadOperation);
198
199 if (!element)
200 return;
201
202 element->ReadyForUpload = true;
203 uploadOperation.Start();
204}
205
206void ProjectCloudExtension::OnSnapshotCreated(
207 const ProjectUploadOperation& uploadOperation,
208 const CreateSnapshotResponse& response)
209{
210 auto& cloudDatabase = CloudProjectsDatabase::Get();
211
212 const auto projectFilePath =
213 audacity::ToUTF8(ProjectFileIO::Get(mProject).GetFileName());
214
215 auto previousDbData = cloudDatabase.GetProjectDataForPath(projectFilePath);
216
217 DBProjectData dbData;
218
219 if (previousDbData)
220 dbData = *previousDbData;
221
222 dbData.ProjectId = response.Project.Id;
223 dbData.SnapshotId = response.Snapshot.Id;
224 dbData.LocalPath = projectFilePath;
225 dbData.LastModified = wxDateTime::Now().GetTicks();
226 dbData.LastRead = dbData.LastModified;
227 dbData.SyncStatus = DBProjectData::SyncStatusUploading;
228 dbData.SavesCount++;
229
230 cloudDatabase.UpdateProjectData(dbData);
231 cloudDatabase.SetProjectUserSlug(
232 response.Project.Id, response.Project.Username);
233
234 {
235 auto lock = std::lock_guard { mIdentifiersMutex };
236
237 mProjectId = response.Project.Id;
238 mSnapshotId = response.Snapshot.Id;
239 }
240
241 auto lock = std::lock_guard { mUploadQueueMutex };
242 auto element = UnsafeFindUploadQueueElement(uploadOperation);
243
244 if (!element)
245 return;
246
247 element->BlocksTotal = response.SyncState.MissingBlocks.size();
248 element->SnapshotResponse = response;
249
250 UnsafeUpdateProgress();
251}
252
253void ProjectCloudExtension::OnProjectDataUploaded(
254 const ProjectUploadOperation& uploadOperation)
255{
256 auto lock = std::lock_guard { mUploadQueueMutex };
257 auto element = UnsafeFindUploadQueueElement(uploadOperation);
258
259 if (!element)
260 return;
261
262 element->ProjectDataUploaded = true;
263
264 UnsafeUpdateProgress();
265}
266
267void ProjectCloudExtension::OnBlockUploaded(
268 const ProjectUploadOperation& uploadOperation, std::string_view blockID,
269 bool successful)
270{
271 auto lock = std::lock_guard { mUploadQueueMutex };
272 auto element = UnsafeFindUploadQueueElement(uploadOperation);
273
274 if (!element)
275 return;
276
277 element->BlocksHandled++;
278
279 UnsafeUpdateProgress();
280}
281
282void ProjectCloudExtension::OnSyncCompleted(
283 const ProjectUploadOperation* uploadOperation,
284 std::optional<CloudSyncError> error)
285{
286 auto lock = std::lock_guard { mUploadQueueMutex };
287
288 auto element = uploadOperation != nullptr ?
289 UnsafeFindUploadQueueElement(*uploadOperation) :
290 nullptr;
291
292 if (element != nullptr)
293 {
294 element->Synced = true;
295
296 if (
297 error.has_value() && error->Type == CloudSyncError::Aborted &&
298 element->SnapshotResponse)
299 {
300 auto& cloudDatabase = CloudProjectsDatabase::Get();
301
302 const auto projectFilePath =
303 audacity::ToUTF8(ProjectFileIO::Get(mProject).GetFileName());
304
305 auto previousDbData =
306 cloudDatabase.GetProjectDataForPath(projectFilePath);
307
308 DBProjectData dbData;
309
310 if (previousDbData)
311 dbData = *previousDbData;
312
313 const auto parentId = element->SnapshotResponse->Snapshot.ParentId;
314
315 dbData.SnapshotId = parentId;
316
317 cloudDatabase.UpdateProjectData(dbData);
318
319 {
320 auto lock = std::lock_guard { mIdentifiersMutex };
321 mSnapshotId = parentId;
322 }
323 }
324 }
325
326 // std::all_of returns true if the range is empty
327 if (!std::all_of(
328 mUploadQueue.begin(), mUploadQueue.end(),
329 [](auto& operation) { return operation->Synced; }))
330 {
331 UnsafeUpdateProgress();
332 return;
333 }
334
335 mUploadQueue.clear();
336
337 MarkProjectSynced(!error.has_value());
338
339 Publish(
340 { error.has_value() ? ProjectSyncStatus::Failed :
341 ProjectSyncStatus::Synced,
342 {},
343 error },
344 false);
345
346 if (!IsCloudProject())
347 Publish({ ProjectSyncStatus::Local }, false);
348}
349
350void ProjectCloudExtension::CancelSync()
351{
352 std::vector<std::shared_ptr<UploadQueueElement>> queue;
353
354 {
355 auto lock = std::lock_guard { mUploadQueueMutex };
356 std::swap(queue, mUploadQueue);
357 }
358
359 if (queue.empty())
360 return;
361
362 for (auto& item : queue)
363 {
364 if (!item->Operation)
365 continue;
366
367 item->Operation->Cancel();
368 }
369}
370
371bool ProjectCloudExtension::IsSyncing() const
372{
373 auto lock = std::lock_guard { mStatusMutex };
374
375 return mLastStatus.Status == ProjectSyncStatus::Syncing;
376}
377
378std::string ProjectCloudExtension::GetCloudProjectId() const
379{
380 auto lock = std::lock_guard { mIdentifiersMutex };
381
382 return mProjectId;
383}
384
385std::string ProjectCloudExtension::GetSnapshotId() const
386{
387 auto lock = std::lock_guard { mIdentifiersMutex };
388
389 return mSnapshotId;
390}
391
392void ProjectCloudExtension::OnUpdateSaved(const ProjectSerializer& serializer)
393{
394 auto lock = std::lock_guard { mUploadQueueMutex };
395
396 if (mUploadQueue.empty())
397 return;
398
399 const size_t dictSize = serializer.GetDict().GetSize();
400 const size_t projectSize = serializer.GetData().GetSize();
401
402 std::vector<uint8_t> data;
403 data.resize(projectSize + dictSize + sizeof(uint64_t));
404
405 const uint64_t dictSizeData =
406 IsLittleEndian() ? dictSize : SwapIntBytes(dictSize);
407
408 std::memcpy(data.data(), &dictSizeData, sizeof(uint64_t));
409
410 uint64_t offset = sizeof(dictSize);
411
412 for (const auto [chunkData, size] : serializer.GetDict())
413 {
414 std::memcpy(data.data() + offset, chunkData, size);
415 offset += size;
416 }
417
418 for (const auto [chunkData, size] : serializer.GetData())
419 {
420 std::memcpy(data.data() + offset, chunkData, size);
421 offset += size;
422 }
423
424 mUploadQueue.back()->Data.ProjectSnapshot = std::move(data);
425
426 if (mUploadQueue.back()->Operation)
427 {
428 mUploadQueue.back()->Operation->SetUploadData(mUploadQueue.back()->Data);
429
430 auto dbData = CloudProjectsDatabase::Get().GetProjectData(mProjectId);
431
432 if (dbData)
433 {
434 dbData->LocalPath =
435 audacity::ToUTF8(ProjectFileIO::Get(mProject).GetFileName());
436 CloudProjectsDatabase::Get().UpdateProjectData(*dbData);
437 }
438 }
439 else
440 Publish({ ProjectSyncStatus::Failed }, false);
441}
442
443std::weak_ptr<AudacityProject> ProjectCloudExtension::GetProject() const
444{
445 return mProject.weak_from_this();
446}
447
448int64_t ProjectCloudExtension::GetSavesCount() const
449{
450 auto lock = std::lock_guard { mIdentifiersMutex };
451
452 if (mProjectId.empty())
453 return 0;
454
455 auto& cloudDatabase = CloudProjectsDatabase::Get();
456 auto dbData = cloudDatabase.GetProjectData(mProjectId);
457
458 if (!dbData)
459 return 0;
460
461 return dbData->SavesCount;
462}
463
464Observer::Subscription ProjectCloudExtension::SubscribeStatusChanged(
465 std::function<void(const CloudStatusChangedMessage&)> callback,
466 bool onUIThread)
467{
468 return onUIThread ? mUIStateNotifier->SubscribeSafe(std::move(callback)) :
469 mAsyncStateNotifier->SubscribeSafe(std::move(callback));
470}
471
472void ProjectCloudExtension::UpdateIdFromDatabase()
473{
474 auto& projectFileIO = ProjectFileIO::Get(mProject);
475 auto& cloudDatabase = CloudProjectsDatabase::Get();
476
477 auto projectData = cloudDatabase.GetProjectDataForPath(
478 audacity::ToUTF8(projectFileIO.GetFileName()));
479
480 if (!projectData)
481 return;
482
483 {
484 auto lock = std::lock_guard { mIdentifiersMutex };
485
486 mProjectId = projectData->ProjectId;
487 mSnapshotId = projectData->SnapshotId;
488 }
489
490 Publish(
491 {
492 projectData->SyncStatus ==
493 DBProjectData::SyncStatusType::SyncStatusSynced ?
494 ProjectSyncStatus::Synced :
495 ProjectSyncStatus::Unsynced,
496 },
497 false);
498}
499
500void ProjectCloudExtension::UnsafeUpdateProgress()
501{
502 if (mUploadQueue.empty())
503 return;
504
505 int64_t handledElements = 0;
506 int64_t totalElements = 0;
507
508 for (auto& element : mUploadQueue)
509 {
510 handledElements +=
511 element->BlocksHandled + int(element->ProjectDataUploaded);
512 totalElements += element->BlocksTotal + 1;
513 }
514
515 assert(totalElements > 0);
516
517 Publish(
518 { ProjectSyncStatus::Syncing,
519 double(handledElements) / double(totalElements) },
520 true);
521}
522
523void ProjectCloudExtension::Publish(
524 CloudStatusChangedMessage cloudStatus, bool canMerge)
525{
526 {
527 auto lock = std::lock_guard { mStatusMutex };
528 mLastStatus = cloudStatus;
529 }
530
531 mAsyncStateNotifier->Enqueue(cloudStatus, canMerge);
532 mUIStateNotifier->Enqueue(cloudStatus, canMerge);
533
534 mAsyncStateNotifier->PublishSafe();
535
537 {
538 mUINotificationPending.store(false);
539 mUIStateNotifier->PublishSafe();
540 }
541 else if (!mUINotificationPending.exchange(true))
542 {
544 [this]
545 {
546 if (mUINotificationPending.exchange(false))
547 mUIStateNotifier->PublishSafe();
548 });
549 }
550}
551
552void ProjectCloudExtension::MarkProjectSynced(bool success)
553{
554 auto& cloudDatabase = CloudProjectsDatabase::Get();
555
556 const auto projectFilePath =
557 audacity::ToUTF8(ProjectFileIO::Get(mProject).GetFileName());
558
559 auto previousDbData = cloudDatabase.GetProjectDataForPath(projectFilePath);
560
561 DBProjectData dbData;
562
563 if (previousDbData)
564 dbData = *previousDbData;
565
566 dbData.LastModified = wxDateTime::Now().GetTicks();
567 dbData.LastRead = dbData.LastModified;
568 dbData.SyncStatus = success ? DBProjectData::SyncStatusSynced :
569 DBProjectData::SyncStatusUploading;
570
571 cloudDatabase.UpdateProjectData(dbData);
572}
573
575ProjectCloudExtension::UnsafeFindUploadQueueElement(
576 const ProjectUploadOperation& uploadOperation)
577{
578 auto it = std::find_if(
579 mUploadQueue.begin(), mUploadQueue.end(),
580 [&](const auto& element)
581 { return element->Operation.get() == &uploadOperation; });
582
583 return it != mUploadQueue.end() ? it->get() : nullptr;
584}
585
587ProjectCloudExtension::UnsafeFindUploadQueueElement(
588 const ProjectUploadOperation& uploadOperation) const
589{
590 return const_cast<ProjectCloudExtension*>(this)
591 ->UnsafeFindUploadQueueElement(uploadOperation);
592}
593
594void ProjectCloudExtension::OnProjectPathChanged()
595{
596 bool wasCloudProject = false;
597
598 {
599 auto lock = std::lock_guard { mIdentifiersMutex };
600
601 wasCloudProject = !mProjectId.empty();
602
603 mProjectId.clear();
604 mSnapshotId.clear();
605 }
606
607 // This will set the status to cloud if the project is a cloud project
608 UpdateIdFromDatabase();
609
610 if (mProjectId.empty() && wasCloudProject)
611 Publish({ ProjectSyncStatus::Local }, false);
612}
613
614std::string ProjectCloudExtension::GetCloudProjectPage() const
615{
616 const auto projectId =
617 ProjectCloudExtension::Get(mProject).GetCloudProjectId();
618
619 const auto userSlug =
620 CloudProjectsDatabase::Get().GetProjectUserSlug(projectId);
621
622 return GetServiceConfig().GetProjectPageUrl(userSlug, projectId);
623}
624
625bool ProjectCloudExtension::IsBlockLocked(int64_t blockID) const
626{
627 // Try load the project info first based on the
628 // file path
629 const_cast<ProjectCloudExtension*>(this)->OnLoad();
630
631 const auto projectId = GetCloudProjectId();
632
633 if (projectId.empty())
634 return false;
635
636 return CloudProjectsDatabase::Get().IsProjectBlockLocked(projectId, blockID);
637}
638
639ProjectSyncStatus ProjectCloudExtension::GetCurrentSyncStatus() const
640{
641 auto lock = std::lock_guard {
642 const_cast<ProjectCloudExtension*>(this)->mStatusMutex
643 };
644
645 return mLastStatus.Status;
646}
647
648bool ProjectCloudExtension::IsFirstSyncDialogShown() const
649{
650 auto lock = std::lock_guard { mIdentifiersMutex };
651
652 if (mProjectId.empty())
653 return false;
654
655 return CloudProjectsDatabase::Get().IsFirstSyncDialogShown(mProjectId);
656}
657
658void ProjectCloudExtension::SetFirstSyncDialogShown(bool shown)
659{
660 auto lock = std::lock_guard { mIdentifiersMutex };
661
662 if (mProjectId.empty())
663 return;
664
665 CloudProjectsDatabase::Get().SetFirstSyncDialogShown(mProjectId, shown);
666}
667
668bool CloudStatusChangedMessage::IsSyncing() const noexcept
669{
670 return Status == ProjectSyncStatus::Syncing;
671}
672
673} // namespace audacity::cloud::audiocom::sync
Toolkit-neutral facade for basic user interface services.
Declare functions to perform UTF-8 to std::wstring conversions.
#define _(s)
Definition: Internat.h:73
constexpr IntType SwapIntBytes(IntType value) noexcept
Swap bytes in an integer.
Definition: MemoryX.h:377
bool IsLittleEndian() noexcept
Check that machine is little-endian.
Definition: MemoryX.h:368
@ ProjectFilePathChange
A normal occurrence.
const auto project
declares abstract base class Track, TrackList, and iterators over TrackList
The top-level handle to an Audacity project. It serves as a source of events that other objects can b...
Definition: Project.h:90
Client code makes static instance from a factory of attachments; passes it to Get or Find as a retrie...
Definition: ClientData.h:275
const size_t GetSize() const noexcept
An object that sends messages to an open-ended list of subscribed callbacks.
Definition: Observer.h:108
Subscription Subscribe(Callback callback)
Connect a callback to the Publisher; later-connected are called earlier.
Definition: Observer.h:199
CallbackReturn Publish(const CloudStatusChangedMessage &message)
Send a message to connected callbacks.
Definition: Observer.h:207
A move-only handle representing a connection to a Publisher.
Definition: Observer.h:70
Object associated with a project that manages reading and writing of Audacity project file formats,...
Definition: ProjectFileIO.h:66
static ProjectFileIO & Get(AudacityProject &project)
a class used to (de)serialize the project catalog
const MemoryStream & GetData() const
const MemoryStream & GetDict() const
An in-session identifier of track objects across undo states. It does not persist between sessions.
Definition: Track.h:79
static TrackListHolder Create(AudacityProject *pOwner)
Definition: Track.cpp:330
static TrackList & Get(AudacityProject &project)
Definition: Track.cpp:314
std::string GetProjectPageUrl(std::string_view userId, std::string_view projectId) const
void OnSyncResumed(std::shared_ptr< ProjectUploadOperation > uploadOperation, int64_t missingBlocksCount, bool needsProjectUpload)
This method is called from the UI thread.
void CallAfter(Action action)
Schedule an action to be done later, and in the main thread.
Definition: BasicUI.cpp:214
bool IsUiThread()
Whether the current thread is the UI thread.
Definition: BasicUI.h:399
Services * Get()
Fetch the global instance, or nullptr if none is yet installed.
Definition: BasicUI.cpp:202
void swap(std::unique_ptr< Alg_seq > &a, std::unique_ptr< Alg_seq > &b)
Definition: NoteTrack.cpp:628
const AudacityProject & GetProject(const Track &track)
Definition: TrackArt.cpp:479
const ServiceConfig & GetServiceConfig()
Returns the instance of the ServiceConfig.
std::string ToUTF8(const std::wstring &wstr)
enum audacity::cloud::audiocom::sync::DBProjectData::SyncStatusType SyncStatus
Observer::Subscription SubscribeSafe(std::function< void(const CloudStatusChangedMessage &)> callback)
std::vector< std::pair< CloudStatusChangedMessage, bool > > QueueType