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