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#include "ExportUtils.h"
32
33#include "MemoryX.h"
34
36
37#include "Track.h"
38
40{
41namespace
42{
45 { return std::make_shared<ProjectCloudExtension>(project); }
46};
47} // namespace
48
50{
52 std::shared_ptr<ProjectUploadOperation> Operation;
53 std::optional<CreateSnapshotResponse> SnapshotResponse;
54
55 int64_t BlocksHandled { 0 };
56 int64_t BlocksTotal { 0 };
57
58 bool ProjectDataUploaded { false };
59 bool ReadyForUpload { false };
60 bool Synced { false };
61};
62
64 private Observer::Publisher<CloudStatusChangedMessage>
65{
66 void Enqueue(CloudStatusChangedMessage message, bool canMerge)
67 {
68 auto lock = std::lock_guard { QueueMutex };
69
70 if (Queue.empty() || !canMerge)
71 Queue.push_back({ message, canMerge });
72 else if (Queue.back().second)
73 Queue.back().first = message;
74 else
75 Queue.push_back({ message, canMerge });
76 }
77
79 {
80 QueueType queue;
81 {
82 auto lock = std::lock_guard { QueueMutex };
83 std::swap(queue, Queue);
84 }
85
86 auto lock = std::lock_guard { ObserverMutex };
87
88 for (const auto& [message, _] : queue)
89 Publish(message);
90 }
91
93 SubscribeSafe(std::function<void(const CloudStatusChangedMessage&)> callback)
94 {
95 auto lock = std::lock_guard { ObserverMutex };
96 return Subscribe(std::move(callback));
97 }
98
99 using QueueType = std::vector<std::pair<CloudStatusChangedMessage, bool>>;
100
101 std::mutex QueueMutex;
103 std::recursive_mutex ObserverMutex;
104};
105
107 : mProject { project }
108 , mProjectPathChangedSubscription { ProjectFileIO::Get(project).Subscribe(
109 [this](auto message)
110 {
113 }) }
114 , mAsyncStateNotifier { std::make_unique<CloudStatusChangedNotifier>() }
115 , mUIStateNotifier { std::make_unique<CloudStatusChangedNotifier>() }
116{
117 if (!ProjectFileIO::Get(project).IsTemporary())
118 UpdateIdFromDatabase();
119}
120
121ProjectCloudExtension::~ProjectCloudExtension() = default;
122
124{
125 return project.AttachedObjects::Get<ProjectCloudExtension&>(key);
126}
127
130{
131 return Get(const_cast<AudacityProject&>(project));
132}
133
134bool ProjectCloudExtension::IsCloudProject() const
135{
136 auto lock = std::lock_guard { mIdentifiersMutex };
137 return !mProjectId.empty();
138}
139
140void ProjectCloudExtension::OnLoad()
141{
142 UpdateIdFromDatabase();
143}
144
145void ProjectCloudExtension::OnSyncStarted()
146{
147 auto element = std::make_shared<UploadQueueElement>();
148
149 element->Data.Tracks = TrackList::Create(nullptr);
150
151 for (auto pTrack : TrackList::Get(mProject))
152 {
153 if (pTrack->GetId() == TrackId {})
154 // Don't copy a pending added track
155 continue;
156 element->Data.Tracks->Add(pTrack->Duplicate());
157 }
158
159 auto lock = std::lock_guard { mUploadQueueMutex };
160 mUploadQueue.push_back(std::move(element));
161}
162
164 std::shared_ptr<ProjectUploadOperation> uploadOperation,
165 int64_t missingBlocksCount, bool needsProjectUpload)
166{
167 auto element = std::make_shared<UploadQueueElement>();
168 element->Operation = uploadOperation;
169 element->BlocksTotal = missingBlocksCount;
170 element->ProjectDataUploaded = !needsProjectUpload;
171
172 {
173 auto lock = std::lock_guard { mUploadQueueMutex };
174 mUploadQueue.push_back(element);
175 }
176
177 uploadOperation->Start();
178 UnsafeUpdateProgress();
179}
180
181void ProjectCloudExtension::OnUploadOperationCreated(
182 std::shared_ptr<ProjectUploadOperation> uploadOperation)
183{
184 auto lock = std::lock_guard { mUploadQueueMutex };
185 assert(mUploadQueue.size() > 0);
186
187 if (mUploadQueue.empty())
188 return;
189
190 mUploadQueue.back()->Operation = uploadOperation;
191 UnsafeUpdateProgress();
192}
193
194void ProjectCloudExtension::OnBlocksHashed(
195 ProjectUploadOperation& uploadOperation)
196{
197 auto lock = std::lock_guard { mUploadQueueMutex };
198 auto element = UnsafeFindUploadQueueElement(uploadOperation);
199
200 if (!element)
201 return;
202
203 element->ReadyForUpload = true;
204 uploadOperation.Start();
205}
206
207void ProjectCloudExtension::OnSnapshotCreated(
208 const ProjectUploadOperation& uploadOperation,
209 const CreateSnapshotResponse& response)
210{
211 auto& cloudDatabase = CloudProjectsDatabase::Get();
212
213 const auto projectFilePath =
214 audacity::ToUTF8(ProjectFileIO::Get(mProject).GetFileName());
215
216 auto previousDbData = cloudDatabase.GetProjectDataForPath(projectFilePath);
217
218 DBProjectData dbData;
219
220 if (previousDbData)
221 dbData = *previousDbData;
222
223 dbData.ProjectId = response.Project.Id;
224 dbData.SnapshotId = response.Snapshot.Id;
225 dbData.LocalPath = projectFilePath;
226 dbData.LastModified = wxDateTime::Now().GetTicks();
227 dbData.LastRead = dbData.LastModified;
228 dbData.SyncStatus = DBProjectData::SyncStatusUploading;
229 dbData.SavesCount++;
230
231 cloudDatabase.UpdateProjectData(dbData);
232 cloudDatabase.SetProjectUserSlug(
233 response.Project.Id, response.Project.Username);
234
235 {
236 auto lock = std::lock_guard { mIdentifiersMutex };
237
238 mProjectId = response.Project.Id;
239 mSnapshotId = response.Snapshot.Id;
240 }
241
242 auto lock = std::lock_guard { mUploadQueueMutex };
243 auto element = UnsafeFindUploadQueueElement(uploadOperation);
244
245 if (!element)
246 return;
247
248 element->BlocksTotal = response.SyncState.MissingBlocks.size();
249 element->SnapshotResponse = response;
250
251 UnsafeUpdateProgress();
252}
253
254void ProjectCloudExtension::OnProjectDataUploaded(
255 const ProjectUploadOperation& uploadOperation)
256{
257 auto lock = std::lock_guard { mUploadQueueMutex };
258 auto element = UnsafeFindUploadQueueElement(uploadOperation);
259
260 if (!element)
261 return;
262
263 element->ProjectDataUploaded = true;
264
265 UnsafeUpdateProgress();
266}
267
268void ProjectCloudExtension::OnBlockUploaded(
269 const ProjectUploadOperation& uploadOperation, std::string_view blockID,
270 bool successful)
271{
272 auto lock = std::lock_guard { mUploadQueueMutex };
273 auto element = UnsafeFindUploadQueueElement(uploadOperation);
274
275 if (!element)
276 return;
277
278 element->BlocksHandled++;
279
280 UnsafeUpdateProgress();
281}
282
283void ProjectCloudExtension::OnSyncCompleted(
284 const ProjectUploadOperation* uploadOperation,
285 std::optional<CloudSyncError> error, AudiocomTrace trace)
286{
287 auto lock = std::lock_guard { mUploadQueueMutex };
288
289 auto element = uploadOperation != nullptr ?
290 UnsafeFindUploadQueueElement(*uploadOperation) :
291 nullptr;
292
293 if (element != nullptr)
294 {
295 element->Synced = true;
296
297 if (
298 error.has_value() && error->Type == CloudSyncError::Aborted &&
299 element->SnapshotResponse)
300 {
301 auto& cloudDatabase = CloudProjectsDatabase::Get();
302
303 const auto projectFilePath =
304 audacity::ToUTF8(ProjectFileIO::Get(mProject).GetFileName());
305
306 auto previousDbData =
307 cloudDatabase.GetProjectDataForPath(projectFilePath);
308
309 DBProjectData dbData;
310
311 if (previousDbData)
312 dbData = *previousDbData;
313
314 const auto parentId = element->SnapshotResponse->Snapshot.ParentId;
315
316 dbData.SnapshotId = parentId;
317
318 cloudDatabase.UpdateProjectData(dbData);
319
320 {
321 auto lock = std::lock_guard { mIdentifiersMutex };
322 mSnapshotId = parentId;
323 }
324 }
325 }
326
327 // std::all_of returns true if the range is empty
328 if (!std::all_of(
329 mUploadQueue.begin(), mUploadQueue.end(),
330 [](auto& operation) { return operation->Synced; }))
331 {
332 UnsafeUpdateProgress();
333 return;
334 }
335
336 mUploadQueue.clear();
337
338 MarkProjectSynced(!error.has_value());
339
340 Publish(
341 { trace,
342 error.has_value() ? ProjectSyncStatus::Failed :
343 ProjectSyncStatus::Synced,
344 {},
345 error },
346 false);
347
348 if (!IsCloudProject())
349 Publish(
350 { AudiocomTrace::ignore, // All right
351 ProjectSyncStatus::Local },
352 false);
353}
354
355void ProjectCloudExtension::CancelSync()
356{
357 std::vector<std::shared_ptr<UploadQueueElement>> queue;
358
359 {
360 auto lock = std::lock_guard { mUploadQueueMutex };
361 std::swap(queue, mUploadQueue);
362 }
363
364 if (queue.empty())
365 return;
366
367 for (auto& item : queue)
368 {
369 if (!item->Operation)
370 continue;
371
372 item->Operation->Cancel();
373 }
374}
375
376bool ProjectCloudExtension::IsSyncing() const
377{
378 auto lock = std::lock_guard { mStatusMutex };
379
380 return mLastStatus.Status == ProjectSyncStatus::Syncing;
381}
382
383std::string ProjectCloudExtension::GetCloudProjectId() const
384{
385 auto lock = std::lock_guard { mIdentifiersMutex };
386
387 return mProjectId;
388}
389
390std::string ProjectCloudExtension::GetSnapshotId() const
391{
392 auto lock = std::lock_guard { mIdentifiersMutex };
393
394 return mSnapshotId;
395}
396
397void ProjectCloudExtension::OnUpdateSaved(const ProjectSerializer& serializer)
398{
399 auto lock = std::lock_guard { mUploadQueueMutex };
400
401 if (mUploadQueue.empty())
402 return;
403
404 const size_t dictSize = serializer.GetDict().GetSize();
405 const size_t projectSize = serializer.GetData().GetSize();
406
407 std::vector<uint8_t> data;
408 data.resize(projectSize + dictSize + sizeof(uint64_t));
409
410 const uint64_t dictSizeData =
411 IsLittleEndian() ? dictSize : SwapIntBytes(dictSize);
412
413 std::memcpy(data.data(), &dictSizeData, sizeof(uint64_t));
414
415 uint64_t offset = sizeof(dictSize);
416
417 for (const auto [chunkData, size] : serializer.GetDict())
418 {
419 std::memcpy(data.data() + offset, chunkData, size);
420 offset += size;
421 }
422
423 for (const auto [chunkData, size] : serializer.GetData())
424 {
425 std::memcpy(data.data() + offset, chunkData, size);
426 offset += size;
427 }
428
429 mUploadQueue.back()->Data.ProjectSnapshot = std::move(data);
430
431 if (mUploadQueue.back()->Operation)
432 {
433 mUploadQueue.back()->Operation->SetUploadData(mUploadQueue.back()->Data);
434
435 auto dbData = CloudProjectsDatabase::Get().GetProjectData(mProjectId);
436
437 if (dbData)
438 {
439 dbData->LocalPath =
440 audacity::ToUTF8(ProjectFileIO::Get(mProject).GetFileName());
441 CloudProjectsDatabase::Get().UpdateProjectData(*dbData);
442 }
443 }
444 else
445 Publish(
446 { AudiocomTrace::ignore, // TODO Is this correct ?
447 ProjectSyncStatus::Failed },
448 false);
449}
450
451std::weak_ptr<AudacityProject> ProjectCloudExtension::GetProject() const
452{
453 return mProject.weak_from_this();
454}
455
456int64_t ProjectCloudExtension::GetSavesCount() const
457{
458 auto lock = std::lock_guard { mIdentifiersMutex };
459
460 if (mProjectId.empty())
461 return 0;
462
463 auto& cloudDatabase = CloudProjectsDatabase::Get();
464 auto dbData = cloudDatabase.GetProjectData(mProjectId);
465
466 if (!dbData)
467 return 0;
468
469 return dbData->SavesCount;
470}
471
472Observer::Subscription ProjectCloudExtension::SubscribeStatusChanged(
473 std::function<void(const CloudStatusChangedMessage&)> callback,
474 bool onUIThread)
475{
476 return onUIThread ? mUIStateNotifier->SubscribeSafe(std::move(callback)) :
477 mAsyncStateNotifier->SubscribeSafe(std::move(callback));
478}
479
480void ProjectCloudExtension::UpdateIdFromDatabase()
481{
482 auto& projectFileIO = ProjectFileIO::Get(mProject);
483 auto& cloudDatabase = CloudProjectsDatabase::Get();
484
485 auto projectData = cloudDatabase.GetProjectDataForPath(
486 audacity::ToUTF8(projectFileIO.GetFileName()));
487
488 if (!projectData)
489 return;
490
491 {
492 auto lock = std::lock_guard { mIdentifiersMutex };
493
494 mProjectId = projectData->ProjectId;
495 mSnapshotId = projectData->SnapshotId;
496 }
497
498 Publish(
499 {
500 AudiocomTrace::ignore, // OK here because we're not publishing an
501 // error
502 projectData->SyncStatus ==
503 DBProjectData::SyncStatusType::SyncStatusSynced ?
504 ProjectSyncStatus::Synced :
505 ProjectSyncStatus::Unsynced,
506 },
507 false);
508}
509
510void ProjectCloudExtension::UnsafeUpdateProgress()
511{
512 if (mUploadQueue.empty())
513 return;
514
515 int64_t handledElements = 0;
516 int64_t totalElements = 0;
517
518 for (auto& element : mUploadQueue)
519 {
520 handledElements +=
521 element->BlocksHandled + int(element->ProjectDataUploaded);
522 totalElements += element->BlocksTotal + 1;
523 }
524
525 assert(totalElements > 0);
526
527 Publish(
528 { AudiocomTrace::ignore, // All right
529 ProjectSyncStatus::Syncing,
530 double(handledElements) / double(totalElements) },
531 true);
532}
533
534void ProjectCloudExtension::Publish(
535 CloudStatusChangedMessage cloudStatus, bool canMerge)
536{
537 {
538 auto lock = std::lock_guard { mStatusMutex };
539 mLastStatus = cloudStatus;
540 }
541
542 mAsyncStateNotifier->Enqueue(cloudStatus, canMerge);
543 mUIStateNotifier->Enqueue(cloudStatus, canMerge);
544
545 mAsyncStateNotifier->PublishSafe();
546
548 {
549 mUINotificationPending.store(false);
550 mUIStateNotifier->PublishSafe();
551 }
552 else if (!mUINotificationPending.exchange(true))
553 {
555 [this]
556 {
557 if (mUINotificationPending.exchange(false))
558 mUIStateNotifier->PublishSafe();
559 });
560 }
561}
562
563void ProjectCloudExtension::MarkProjectSynced(bool success)
564{
565 auto& cloudDatabase = CloudProjectsDatabase::Get();
566
567 const auto projectFilePath =
568 audacity::ToUTF8(ProjectFileIO::Get(mProject).GetFileName());
569
570 auto previousDbData = cloudDatabase.GetProjectDataForPath(projectFilePath);
571
572 DBProjectData dbData;
573
574 if (previousDbData)
575 dbData = *previousDbData;
576
577 dbData.LastModified = wxDateTime::Now().GetTicks();
578 dbData.LastRead = dbData.LastModified;
579 dbData.SyncStatus = success ? DBProjectData::SyncStatusSynced :
580 DBProjectData::SyncStatusUploading;
581
582 cloudDatabase.UpdateProjectData(dbData);
583}
584
586ProjectCloudExtension::UnsafeFindUploadQueueElement(
587 const ProjectUploadOperation& uploadOperation)
588{
589 auto it = std::find_if(
590 mUploadQueue.begin(), mUploadQueue.end(),
591 [&](const auto& element)
592 { return element->Operation.get() == &uploadOperation; });
593
594 return it != mUploadQueue.end() ? it->get() : nullptr;
595}
596
598ProjectCloudExtension::UnsafeFindUploadQueueElement(
599 const ProjectUploadOperation& uploadOperation) const
600{
601 return const_cast<ProjectCloudExtension*>(this)
602 ->UnsafeFindUploadQueueElement(uploadOperation);
603}
604
605void ProjectCloudExtension::OnProjectPathChanged()
606{
607 bool wasCloudProject = false;
608
609 {
610 auto lock = std::lock_guard { mIdentifiersMutex };
611
612 wasCloudProject = !mProjectId.empty();
613
614 mProjectId.clear();
615 mSnapshotId.clear();
616 }
617
618 // This will set the status to cloud if the project is a cloud project
619 UpdateIdFromDatabase();
620
621 if (mProjectId.empty() && wasCloudProject)
622 Publish({ AudiocomTrace::ignore, ProjectSyncStatus::Local }, false);
623}
624
625std::string
626ProjectCloudExtension::GetCloudProjectPage(AudiocomTrace trace) const
627{
628 const auto projectId =
629 ProjectCloudExtension::Get(mProject).GetCloudProjectId();
630
631 const auto userSlug =
632 CloudProjectsDatabase::Get().GetProjectUserSlug(projectId);
633
634 return GetServiceConfig().GetProjectPageUrl(userSlug, projectId, trace);
635}
636
637bool ProjectCloudExtension::IsBlockLocked(int64_t blockID) const
638{
639 // Try load the project info first based on the
640 // file path
641 const_cast<ProjectCloudExtension*>(this)->OnLoad();
642
643 const auto projectId = GetCloudProjectId();
644
645 if (projectId.empty())
646 return false;
647
648 return CloudProjectsDatabase::Get().IsProjectBlockLocked(projectId, blockID);
649}
650
651ProjectSyncStatus ProjectCloudExtension::GetCurrentSyncStatus() const
652{
653 auto lock = std::lock_guard {
654 const_cast<ProjectCloudExtension*>(this)->mStatusMutex
655 };
656
657 return mLastStatus.Status;
658}
659
660bool ProjectCloudExtension::IsFirstSyncDialogShown() const
661{
662 auto lock = std::lock_guard { mIdentifiersMutex };
663
664 if (mProjectId.empty())
665 return false;
666
667 return CloudProjectsDatabase::Get().IsFirstSyncDialogShown(mProjectId);
668}
669
670void ProjectCloudExtension::SetFirstSyncDialogShown(bool shown)
671{
672 auto lock = std::lock_guard { mIdentifiersMutex };
673
674 if (mProjectId.empty())
675 return;
676
677 CloudProjectsDatabase::Get().SetFirstSyncDialogShown(mProjectId, shown);
678}
679
680bool CloudStatusChangedMessage::IsSyncing() const noexcept
681{
682 return Status == ProjectSyncStatus::Syncing;
683}
684
685} // namespace audacity::cloud::audiocom::sync
Toolkit-neutral facade for basic user interface services.
Declare functions to perform UTF-8 to std::wstring conversions.
AudiocomTrace
Definition: ExportUtils.h:27
#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, AudiocomTrace) 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:407
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