Audacity 3.2.0
LocalProjectSnapshot.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 CloudProjectSnapshot.cpp
7
8 Dmitry Vedenko
9
10**********************************************************************/
12
13#include <algorithm>
14#include <future>
15
16#include "../OAuthService.h"
17#include "../ServiceConfig.h"
18
19#include "BasicUI.h"
20
21#include "BlockHasher.h"
23#include "DataUploader.h"
24#include "MixdownUploader.h"
26
27#include "MemoryX.h"
28#include "Project.h"
29#include "SampleBlock.h"
30#include "Sequence.h"
31#include "Track.h"
32#include "WaveClip.h"
33#include "WaveTrack.h"
34#include "WaveTrackUtilities.h"
35
36#include "IResponse.h"
37#include "NetworkManager.h"
38#include "Request.h"
39
41
42#include "StringUtils.h"
43
44#include "crypto/SHA256.h"
45
47{
49{
51
53
54 std::vector<LockedBlock> Blocks;
55 std::vector<BlockUploadTask> MissingBlocks;
56
57 std::unordered_map<int64_t, size_t> BlockIdToIndex;
58 std::unordered_map<std::string, size_t> BlockHashToIndex;
59
60 std::unique_ptr<BlockHasher> Hasher;
61
62 std::future<void> UpdateCacheFuture;
63 std::vector<std::pair<int64_t, std::string>> NewHashes;
64
65 std::function<void()> OnBlocksLocked;
66
69 std::function<void()> onBlocksLocked)
71 , OnBlocksLocked { std::move(onBlocksLocked) }
72 {
74
76 {
79 }
80
81 Hasher = std::make_unique<BlockHasher>();
82
83 Hasher->ComputeHashes(*this, Blocks, [this] { CollectHashes(); });
84 }
85
87 {
88 }
89
91 {
92 const auto visitor = [this](const SampleBlockPtr &pBlock){
93 const auto id = pBlock->GetBlockID();
94 Blocks.push_back({
95 id, pBlock->GetSampleFormat(), pBlock });
96 BlockIdToIndex[id] = Blocks.size() - 1;
97 };
99 }
100
102 {
103 // TakeResult() will call UpdateHash() for each block
104 // not found in the cache
105 const auto result = Hasher->TakeResult();
106
107 for (auto [id, hash] : result)
108 {
109 auto it = BlockIdToIndex.find(id);
110
111 if (it == BlockIdToIndex.end())
112 {
113 assert(false);
114 continue;
115 }
116
117 while (BlockHashToIndex.find(hash) != BlockHashToIndex.end())
118 {
119 // Hash is used by another block, rehash
120 hash = crypto::sha256(hash);
121 }
122
124 Blocks[it->second].Hash = std::move(hash);
125 }
126
127 // This will potentially block, if the cache is being updated
128 // already
130
131 if (OnBlocksLocked)
133 }
134
136 {
138 return;
139
140 UpdateCacheFuture = std::async(
141 std::launch::async,
142 [this, hashes = std::move(NewHashes)]
143 {
145 Extension.GetCloudProjectId(), hashes);
146 });
147 }
148
149 bool GetHash(int64_t blockId, std::string& hash) const override
150 {
152 return false;
153
154 auto cachedResult = CloudProjectsDatabase::Get().GetBlockHash(
155 Extension.GetCloudProjectId(), blockId);
156
157 if (!cachedResult)
158 return false;
159
160 hash = std::move(*cachedResult);
161
162 return true;
163 }
164
165 void UpdateHash(int64_t blockId, const std::string& hash) override
166 {
167 NewHashes.emplace_back(blockId, hash);
168 }
169
170 void FillMissingBlocks(const std::vector<UploadUrls>& missingBlockUrls)
171 {
172 for (const auto& urls : missingBlockUrls)
173 {
174 auto it = BlockHashToIndex.find(ToUpper(urls.Id));
175
176 if (it == BlockHashToIndex.end())
177 {
178 assert(false);
179 continue;
180 }
181
182 const auto index = it->second;
183
184 MissingBlocks.push_back(BlockUploadTask { urls, Blocks[index] });
185 }
186 }
187};
188
190 Tag, const ServiceConfig& config, const OAuthService& oauthService,
192 : mProjectCloudExtension { extension }
193 , mWeakProject { extension.GetProject() }
194 , mServiceConfig { config }
195 , mOAuthService { oauthService }
196 , mProjectName { std::move(name) }
197 , mUploadMode { mode }
198 , mCancellationContext { concurrency::CancellationContext::Create() }
199{
200}
201
203{
204}
205
207 const ServiceConfig& config, const OAuthService& oauthService,
209{
210 auto project = extension.GetProject().lock();
211
212 if (!project)
213 return {};
214
215 auto snapshot = std::make_shared<LocalProjectSnapshot>(
216 Tag {}, config, oauthService, extension, std::move(name), mode);
217
218 snapshot->mProjectCloudExtension.OnUploadOperationCreated(snapshot);
219
220 snapshot->mProjectBlocksLock = std::make_unique<ProjectBlocksLock>(
222 [weakSnapshot = std::weak_ptr(snapshot)]
223 {
224 auto snapshot = weakSnapshot.lock();
225
226 if (snapshot == nullptr)
227 return;
228
229 auto project = snapshot->GetProject();
230
231 if (project == nullptr)
232 return;
233
234 snapshot->mProjectCloudExtension.OnBlocksHashed(*snapshot);
235 });
236
237 return snapshot->mCreateSnapshotPromise.get_future();
238}
239
241{
242 return mCompleted.load(std::memory_order_acquire);
243}
244
245std::shared_ptr<AudacityProject> LocalProjectSnapshot::GetProject()
246{
247 return mWeakProject.lock();
248}
249
251{
253}
254
256{
257 mProjectDataReady.store(true);
258 mProjectDataPromise.set_value(data);
259}
260
262{
263 mCancelled.store(true, std::memory_order_release);
264
265 mCancellationContext->Cancel();
266
267 if (!mProjectDataReady.load(std::memory_order_acquire))
268 mProjectDataPromise.set_value({});
269
271}
272
274{
275 mCancelled.store(true, std::memory_order_release);
276
277 mCancellationContext->Cancel();
278
279 if (!mProjectDataReady.load(std::memory_order_acquire))
280 mProjectDataPromise.set_value({});
281
283
285}
286
288{
289 if (!mCompleted.exchange(true, std::memory_order_release))
290 mProjectCloudExtension.OnSyncCompleted(this, std::make_optional(error));
291}
292
294{
295 UploadFailed({ DeduceError(uploadResult.Code), uploadResult.Content });
296}
297
299 const MissingBlocksUploadProgress& uploadResult)
300{
302
303 for (const auto& uploadError : uploadResult.UploadErrors)
304 {
305 if (
306 uploadError.Code == SyncResultCode::Success ||
307 uploadError.Code == SyncResultCode::Conflict)
308 continue;
309
310 const auto deducedError = DeduceError(uploadError.Code);
311
312 if (
314 deducedError == CloudSyncError::Network)
315 {
316 errorType = deducedError;
317 }
318 else if (
320 deducedError == CloudSyncError::Cancelled)
321 {
322 errorType = deducedError;
323 break;
324 }
325 }
326
327 UploadFailed({ errorType, {} });
328}
329
331{
332 auto project = mWeakProject.lock();
333
334 if (project == nullptr)
335 {
337 XO("Project was closed before snapshot was created")));
338 return;
339 }
340
341 const bool isCloudProject = mProjectCloudExtension.IsCloudProject();
342 const bool createNew =
343 mUploadMode == UploadMode::CreateNew || !isCloudProject;
344
345 ProjectForm projectForm;
346
347 if (createNew)
348 projectForm.Name = mProjectName;
349 else
351
352 // For empty projects, mProjectBlocksLock will be nullptr at this point
353 if (mProjectBlocksLock != nullptr)
354 {
355 projectForm.Hashes.reserve(mProjectBlocksLock->Blocks.size());
356 std::transform(
357 mProjectBlocksLock->Blocks.begin(), mProjectBlocksLock->Blocks.end(),
358 std::back_inserter(projectForm.Hashes),
359 [](const auto& block) { return block.Hash; });
360 }
361
362 using namespace audacity::network_manager;
363
364 const auto url = createNew ? mServiceConfig.GetCreateProjectUrl() :
367
368 projectForm.Force = !createNew && mUploadMode == UploadMode::ForceOverwrite;
369
370 auto request = Request(url);
371
372 request.setHeader(
374 request.setHeader(
376 // request.setHeader(common_headers::ContentEncoding, "gzip");
377
378 const auto language = mServiceConfig.GetAcceptLanguageValue();
379
380 if (!language.empty())
381 request.setHeader(
383
384 request.setHeader(
386
387 auto serializedForm = Serialize(projectForm);
388
389 auto response = NetworkManager::GetInstance().doPost(
390 request, serializedForm.data(), serializedForm.size());
391
392 response->setRequestFinishedCallback(
393 [this, response, createNew, weakThis = weak_from_this()](auto)
394 {
395 auto strongThis = weakThis.lock();
396 if (!strongThis)
397 return;
398
399 const auto error = response->getError();
400
401 if (error != NetworkError::NoError)
402 {
404
405 mCreateSnapshotPromise.set_value({});
406 return;
407 }
408
409 const auto body = response->readAll<std::string>();
410 auto result = DeserializeCreateSnapshotResponse(body);
411
412 if (!result)
413 {
415 XO("Invalid Response: %s").Format(body).Translation()));
416
417 mCreateSnapshotPromise.set_value({});
418 return;
419 }
420
421 OnSnapshotCreated(*result, createNew);
422 });
423
424 mCancellationContext->OnCancelled(response);
425}
426
428 const CreateSnapshotResponse& response, bool newProject)
429{
430 auto project = mWeakProject.lock();
431
432 if (project == nullptr)
433 {
435 XO("Project was closed before snapshot was created")));
436 return;
437 }
438
439 if (newProject)
440 mProjectBlocksLock->UpdateProjectHashesInCache();
441
442 mProjectBlocksLock->FillMissingBlocks(response.SyncState.MissingBlocks);
443
445
446 {
447 auto lock = std::lock_guard { mCreateSnapshotResponseMutex };
448 mCreateSnapshotResponse = response;
449 }
450
451 mCreateSnapshotPromise.set_value({ response, shared_from_this() });
452
453 auto projectData = mProjectDataPromise.get_future().get();
454
455 if (mCancelled.load(std::memory_order_acquire))
456 return;
457
458 StorePendingSnapshot(response, projectData);
459
462 projectData.ProjectSnapshot,
463 [this, weakThis = weak_from_this()](ResponseResult result)
464 {
465 auto strongThis = weakThis.lock();
466 if (!strongThis)
467 return;
468
469 auto& db = CloudProjectsDatabase::Get();
470
471 const auto projectId = mCreateSnapshotResponse->Project.Id;
472 const auto snapshotId = mCreateSnapshotResponse->Snapshot.Id;
473
474 if (result.Code != SyncResultCode::Success)
475 {
476 db.RemovePendingSnapshot(projectId, snapshotId);
477 db.RemovePendingProjectBlob(projectId, snapshotId);
478 db.RemovePendingProjectBlocks(projectId, snapshotId);
479
480 DataUploadFailed(result);
481 return;
482 }
483
485 db.RemovePendingProjectBlob(projectId, snapshotId);
486
487 if (mProjectBlocksLock->MissingBlocks.empty())
488 {
490 return;
491 }
492
495 mProjectBlocksLock->MissingBlocks,
496 [this, weakThis = weak_from_this()](
497 auto result, auto block, auto uploadResult)
498 {
499 auto strongThis = weakThis.lock();
500 if (!strongThis)
501 return;
502
503 if (!IsUploadRecoverable(uploadResult.Code))
505 mCreateSnapshotResponse->Project.Id, block.Id);
506
508 *this, block.Hash,
509 uploadResult.Code == SyncResultCode::Success);
510
511 const auto completed =
512 result.UploadedBlocks == result.TotalBlocks ||
513 result.FailedBlocks != 0;
514 const bool succeeded = completed && result.FailedBlocks == 0;
515
516 if (!completed)
517 return;
518
519 if (succeeded)
521 else
522 DataUploadFailed(result);
523 });
524 });
525}
526
528 const CreateSnapshotResponse& response, const ProjectUploadData& projectData)
529{
531 { response.Project.Id, response.Snapshot.Id,
533 response.Project.Id, response.Snapshot.Id) });
534
536 { response.Project.Id, response.Snapshot.Id,
539 response.SyncState.FileUrls.FailUrl, projectData.ProjectSnapshot });
540
541 if (mProjectBlocksLock->MissingBlocks.empty())
542 return;
543
544 std::vector<PendingProjectBlockData> pendingBlocks;
545 pendingBlocks.reserve(mProjectBlocksLock->MissingBlocks.size());
546
547 for (const auto& block : mProjectBlocksLock->MissingBlocks)
548 {
549 pendingBlocks.push_back(PendingProjectBlockData {
550 response.Project.Id, response.Snapshot.Id, block.BlockUrls.UploadUrl,
551 block.BlockUrls.SuccessUrl, block.BlockUrls.FailUrl, block.Block.Id,
552 static_cast<int>(block.Block.Format), block.Block.Hash });
553 }
554
556}
557
559{
560 using namespace network_manager;
562 mCreateSnapshotResponse->Project.Id,
563 mCreateSnapshotResponse->Snapshot.Id));
564
565 SetCommonHeaders(request);
566
567 auto response = NetworkManager::GetInstance().doPost(request, nullptr, 0);
568
569 response->setRequestFinishedCallback(
570 [this, response, weakThis = weak_from_this()](auto)
571 {
572 auto strongThis = weakThis.lock();
573 if (!strongThis)
574 return;
575
577 mCreateSnapshotResponse->Project.Id,
578 mCreateSnapshotResponse->Snapshot.Id);
579
580 if (response->getError() != NetworkError::NoError)
581 {
583 return;
584 }
585
586 mCompleted.store(true, std::memory_order_release);
588 });
589
590 mCancellationContext->OnCancelled(response);
591}
592
594{
596 return;
597
598 using namespace network_manager;
599
601 mCreateSnapshotResponse->Project.Id,
602 mCreateSnapshotResponse->Snapshot.Id));
603
604 SetCommonHeaders(request);
605
606 auto response = NetworkManager::GetInstance().doDelete(request);
607
608 response->setRequestFinishedCallback(
609 [this, response, strongThis = shared_from_this()](auto)
610 {
612 mCreateSnapshotResponse->Project.Id,
613 mCreateSnapshotResponse->Snapshot.Id);
614 });
615}
616
617} // namespace audacity::cloud::audiocom::sync
Toolkit-neutral facade for basic user interface services.
std::shared_ptr< SampleBlock > SampleBlockPtr
Definition: CloudSyncDTO.h:25
std::unordered_set< SampleBlockID > SampleBlockIDSet
Definition: CloudSyncDTO.h:27
const TranslatableString name
Definition: Distortion.cpp:76
XO("Cut/Copy/Paste")
Declare an interface for HTTP response.
Declare a class for performing HTTP requests.
Declare a class for constructing HTTP requests.
std::string ToUpper(const std::string &str)
Definition: StringUtils.cpp:46
const auto tracks
const auto project
declares abstract base class Track, TrackList, and iterators over TrackList
int id
The top-level handle to an Audacity project. It serves as a source of events that other objects can b...
Definition: Project.h:90
Abstract base class used in importing a file.
A flat linked list of tracks supporting Add, Remove, Clear, and Contains, serialization of the list o...
Definition: Track.h:850
static TrackList & Get(AudacityProject &project)
Definition: Track.cpp:314
Service responsible for OAuth authentication against the audio.com service.
Definition: OAuthService.h:40
std::string GetAccessToken() const
Return the current access token, if any.
Configuration for the audio.com.
Definition: ServiceConfig.h:23
std::string GetCreateSnapshotUrl(std::string_view projectId) const
std::string GetDeleteSnapshotUrl(std::string_view projectId, std::string_view snapshotId) const
std::string GetAcceptLanguageValue() const
Returns the preferred language.
std::string GetSnapshotSyncUrl(std::string_view projectId, std::string_view snapshotId) const
void UpdateBlockHashes(std::string_view projectId, const std::vector< std::pair< int64_t, std::string > > &hashes)
std::optional< std::string > GetBlockHash(std::string_view projectId, int64_t blockId) const
void AddPendingSnapshot(const PendingSnapshotData &snapshotData)
void UpdateProjectBlockList(std::string_view projectId, const SampleBlockIDSet &blockSet)
void RemovePendingProjectBlock(std::string_view projectId, int64_t blockId)
void AddPendingProjectBlocks(const std::vector< PendingProjectBlockData > &blockData)
void AddPendingProjectBlob(const PendingProjectBlobData &blobData)
void RemovePendingSnapshot(std::string_view projectId, std::string_view snapshotId)
void Upload(CancellationContextPtr cancellationContex, const ServiceConfig &config, const UploadUrls &target, std::vector< uint8_t > data, std::function< void(ResponseResult)> callback, std::function< void(double)> progressCallback={})
void DataUploadFailed(const ResponseResult &uploadResult)
void OnSnapshotCreated(const CreateSnapshotResponse &response, bool newProject)
void StorePendingSnapshot(const CreateSnapshotResponse &response, const ProjectUploadData &data)
std::unique_ptr< ProjectBlocksLock > mProjectBlocksLock
std::optional< CreateSnapshotResponse > mCreateSnapshotResponse
LocalProjectSnapshot(Tag, const ServiceConfig &config, const OAuthService &oauthService, ProjectCloudExtension &extension, std::string name, UploadMode mode)
static Future Create(const ServiceConfig &config, const OAuthService &oauthService, ProjectCloudExtension &extension, std::string name, UploadMode mode)
void SetUploadData(const ProjectUploadData &data) override
std::shared_ptr< MissingBlocksUploader > mMissingBlockUploader
concurrency::CancellationContextPtr mCancellationContext
static std::shared_ptr< MissingBlocksUploader > Create(CancellationContextPtr cancellationContex, const ServiceConfig &serviceConfig, std::vector< BlockUploadTask > uploadTasks, MissingBlocksUploadProgressCallback progress)
void OnSyncCompleted(const ProjectUploadOperation *uploadOperation, std::optional< CloudSyncError > error)
This method is called from any thread.
void OnSnapshotCreated(const ProjectUploadOperation &uploadOperation, const CreateSnapshotResponse &response)
This method is called from the network thread.
void OnProjectDataUploaded(const ProjectUploadOperation &uploadOperation)
This method is called from the network thread.
void OnBlockUploaded(const ProjectUploadOperation &uploadOperation, std::string_view blockID, bool successful)
This method is called from the network thread.
ResponsePtr doPost(const Request &request, const void *data, size_t size)
ResponsePtr doDelete(const Request &request)
WAVE_TRACK_API void VisitBlocks(TrackList &tracks, BlockVisitor visitor, SampleBlockIDSet *pIDs=nullptr)
const AudacityProject & GetProject(const Track &track)
Definition: TrackArt.cpp:479
CloudSyncError::ErrorType DeduceError(SyncResultCode code)
std::string Serialize(const ProjectForm &form)
CloudSyncError MakeClientFailure(const TranslatableString &message)
CLOUD_AUDIOCOM_API CloudSyncError DeduceUploadError(audacity::network_manager::IResponse &response)
std::optional< CreateSnapshotResponse > DeserializeCreateSnapshotResponse(const std::string &data)
bool IsUploadRecoverable(SyncResultCode code)
void SetCommonHeaders(Request &request)
std::string sha256(const T &data)
Definition: SHA256.h:50
STL namespace.
void UpdateHash(int64_t blockId, const std::string &hash) override
bool GetHash(int64_t blockId, std::string &hash) const override
ProjectBlocksLock(ProjectCloudExtension &extension, AudacityProject &project, std::function< void()> onBlocksLocked)
void FillMissingBlocks(const std::vector< UploadUrls > &missingBlockUrls)