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 "ExportUtils.h"
28#include "MemoryX.h"
29#include "Project.h"
30#include "SampleBlock.h"
31#include "Sequence.h"
32#include "Track.h"
33#include "WaveClip.h"
34#include "WaveTrack.h"
35#include "WaveTrackUtilities.h"
36
37#include "IResponse.h"
38#include "NetworkManager.h"
39#include "Request.h"
40
42
43#include "StringUtils.h"
44
45#include "crypto/SHA256.h"
46
48{
50{
52
54
55 std::vector<LockedBlock> Blocks;
56 std::vector<BlockUploadTask> MissingBlocks;
57
58 std::unordered_map<int64_t, size_t> BlockIdToIndex;
59 std::unordered_map<std::string, size_t> BlockHashToIndex;
60
61 std::unique_ptr<BlockHasher> Hasher;
62
63 std::future<void> UpdateCacheFuture;
64 std::vector<std::pair<int64_t, std::string>> NewHashes;
65
66 std::function<void()> OnBlocksLocked;
67
70 std::function<void()> onBlocksLocked)
72 , OnBlocksLocked { std::move(onBlocksLocked) }
73 {
75
77 {
80 }
81
82 Hasher = std::make_unique<BlockHasher>();
83
84 Hasher->ComputeHashes(*this, Blocks, [this] { CollectHashes(); });
85 }
86
88 {
89 }
90
92 {
93 const auto visitor = [this](const SampleBlockPtr &pBlock){
94 const auto id = pBlock->GetBlockID();
95 Blocks.push_back({
96 id, pBlock->GetSampleFormat(), pBlock });
97 BlockIdToIndex[id] = Blocks.size() - 1;
98 };
100 }
101
103 {
104 // TakeResult() will call UpdateHash() for each block
105 // not found in the cache
106 const auto result = Hasher->TakeResult();
107
108 for (auto [id, hash] : result)
109 {
110 auto it = BlockIdToIndex.find(id);
111
112 if (it == BlockIdToIndex.end())
113 {
114 assert(false);
115 continue;
116 }
117
118 while (BlockHashToIndex.find(hash) != BlockHashToIndex.end())
119 {
120 // Hash is used by another block, rehash
121 hash = crypto::sha256(hash);
122 }
123
125 Blocks[it->second].Hash = std::move(hash);
126 }
127
128 // This will potentially block, if the cache is being updated
129 // already
131
132 if (OnBlocksLocked)
134 }
135
137 {
139 return;
140
141 UpdateCacheFuture = std::async(
142 std::launch::async,
143 [this, hashes = std::move(NewHashes)]
144 {
146 Extension.GetCloudProjectId(), hashes);
147 });
148 }
149
150 bool GetHash(int64_t blockId, std::string& hash) const override
151 {
153 return false;
154
155 auto cachedResult = CloudProjectsDatabase::Get().GetBlockHash(
156 Extension.GetCloudProjectId(), blockId);
157
158 if (!cachedResult)
159 return false;
160
161 hash = std::move(*cachedResult);
162
163 return true;
164 }
165
166 void UpdateHash(int64_t blockId, const std::string& hash) override
167 {
168 NewHashes.emplace_back(blockId, hash);
169 }
170
171 void FillMissingBlocks(const std::vector<UploadUrls>& missingBlockUrls)
172 {
173 for (const auto& urls : missingBlockUrls)
174 {
175 auto it = BlockHashToIndex.find(ToUpper(urls.Id));
176
177 if (it == BlockHashToIndex.end())
178 {
179 assert(false);
180 continue;
181 }
182
183 const auto index = it->second;
184
185 MissingBlocks.push_back(BlockUploadTask { urls, Blocks[index] });
186 }
187 }
188};
189
191 Tag, const ServiceConfig& config, const OAuthService& oauthService,
193 AudiocomTrace trace)
194 : mProjectCloudExtension { extension }
195 , mWeakProject { extension.GetProject() }
196 , mServiceConfig { config }
197 , mOAuthService { oauthService }
198 , mAudiocomTrace { trace }
199 , mProjectName { std::move(name) }
200 , mUploadMode { mode }
201 , mCancellationContext { concurrency::CancellationContext::Create() }
202{
203}
204
206{
207}
208
210 const ServiceConfig& config, const OAuthService& oauthService,
212 AudiocomTrace trace)
213{
214 auto project = extension.GetProject().lock();
215
216 if (!project)
217 return {};
218
219 auto snapshot = std::make_shared<LocalProjectSnapshot>(
220 Tag {}, config, oauthService, extension, std::move(name), mode, trace);
221
222 snapshot->mProjectCloudExtension.OnUploadOperationCreated(snapshot);
223
224 snapshot->mProjectBlocksLock = std::make_unique<ProjectBlocksLock>(
226 [weakSnapshot = std::weak_ptr(snapshot)]
227 {
228 auto snapshot = weakSnapshot.lock();
229
230 if (snapshot == nullptr)
231 return;
232
233 auto project = snapshot->GetProject();
234
235 if (project == nullptr)
236 return;
237
238 snapshot->mProjectCloudExtension.OnBlocksHashed(*snapshot);
239 });
240
241 return snapshot->mCreateSnapshotPromise.get_future();
242}
243
245{
246 return mCompleted.load(std::memory_order_acquire);
247}
248
249std::shared_ptr<AudacityProject> LocalProjectSnapshot::GetProject()
250{
251 return mWeakProject.lock();
252}
253
255{
257}
258
260{
261 mProjectDataReady.store(true);
262 mProjectDataPromise.set_value(data);
263}
264
266{
267 mCancelled.store(true, std::memory_order_release);
268
269 mCancellationContext->Cancel();
270
271 if (!mProjectDataReady.load(std::memory_order_acquire))
272 mProjectDataPromise.set_value({});
273
275}
276
278{
279 mCancelled.store(true, std::memory_order_release);
280
281 mCancellationContext->Cancel();
282
283 if (!mProjectDataReady.load(std::memory_order_acquire))
284 mProjectDataPromise.set_value({});
285
287
289}
290
292{
293 if (!mCompleted.exchange(true, std::memory_order_release))
295 this, std::make_optional(error), mAudiocomTrace);
296}
297
299{
300 UploadFailed({ DeduceError(uploadResult.Code), uploadResult.Content });
301}
302
304 const MissingBlocksUploadProgress& uploadResult)
305{
307
308 for (const auto& uploadError : uploadResult.UploadErrors)
309 {
310 if (
311 uploadError.Code == SyncResultCode::Success ||
312 uploadError.Code == SyncResultCode::Conflict)
313 continue;
314
315 const auto deducedError = DeduceError(uploadError.Code);
316
317 if (
319 deducedError == CloudSyncError::Network)
320 {
321 errorType = deducedError;
322 }
323 else if (
325 deducedError == CloudSyncError::Cancelled)
326 {
327 errorType = deducedError;
328 break;
329 }
330 }
331
332 UploadFailed({ errorType, {} });
333}
334
336{
337 auto project = mWeakProject.lock();
338
339 if (project == nullptr)
340 {
342 XO("Project was closed before snapshot was created")));
343 return;
344 }
345
346 const bool isCloudProject = mProjectCloudExtension.IsCloudProject();
347 const bool createNew =
348 mUploadMode == UploadMode::CreateNew || !isCloudProject;
349
350 ProjectForm projectForm;
351
352 if (createNew)
353 projectForm.Name = mProjectName;
354 else
356
357 // For empty projects, mProjectBlocksLock will be nullptr at this point
358 if (mProjectBlocksLock != nullptr)
359 {
360 projectForm.Hashes.reserve(mProjectBlocksLock->Blocks.size());
361 std::transform(
362 mProjectBlocksLock->Blocks.begin(), mProjectBlocksLock->Blocks.end(),
363 std::back_inserter(projectForm.Hashes),
364 [](const auto& block) { return block.Hash; });
365 }
366
367 using namespace audacity::network_manager;
368
369 const auto url = createNew ? mServiceConfig.GetCreateProjectUrl() :
372
373 projectForm.Force = !createNew && mUploadMode == UploadMode::ForceOverwrite;
374
375 auto request = Request(url);
376
377 request.setHeader(
379 request.setHeader(
381 // request.setHeader(common_headers::ContentEncoding, "gzip");
382
383 const auto language = mServiceConfig.GetAcceptLanguageValue();
384
385 if (!language.empty())
386 request.setHeader(
388
389 request.setHeader(
391
392 auto serializedForm = Serialize(projectForm);
393
394 auto response = NetworkManager::GetInstance().doPost(
395 request, serializedForm.data(), serializedForm.size());
396
397 response->setRequestFinishedCallback(
398 [this, response, createNew, weakThis = weak_from_this()](auto)
399 {
400 auto strongThis = weakThis.lock();
401 if (!strongThis)
402 return;
403
404 const auto error = response->getError();
405
406 if (error != NetworkError::NoError)
407 {
409
410 mCreateSnapshotPromise.set_value({});
411 return;
412 }
413
414 const auto body = response->readAll<std::string>();
415 auto result = DeserializeCreateSnapshotResponse(body);
416
417 if (!result)
418 {
420 XO("Invalid Response: %s").Format(body).Translation()));
421
422 mCreateSnapshotPromise.set_value({});
423 return;
424 }
425
426 OnSnapshotCreated(*result, createNew);
427 });
428
429 mCancellationContext->OnCancelled(response);
430}
431
433 const CreateSnapshotResponse& response, bool newProject)
434{
435 auto project = mWeakProject.lock();
436
437 if (project == nullptr)
438 {
440 XO("Project was closed before snapshot was created")));
441 return;
442 }
443
444 if (newProject)
445 mProjectBlocksLock->UpdateProjectHashesInCache();
446
447 mProjectBlocksLock->FillMissingBlocks(response.SyncState.MissingBlocks);
448
450
451 {
452 auto lock = std::lock_guard { mCreateSnapshotResponseMutex };
453 mCreateSnapshotResponse = response;
454 }
455
456 mCreateSnapshotPromise.set_value({ response, shared_from_this() });
457
458 auto projectData = mProjectDataPromise.get_future().get();
459
460 if (mCancelled.load(std::memory_order_acquire))
461 return;
462
463 StorePendingSnapshot(response, projectData);
464
467 projectData.ProjectSnapshot,
468 [this, weakThis = weak_from_this()](ResponseResult result)
469 {
470 auto strongThis = weakThis.lock();
471 if (!strongThis)
472 return;
473
474 auto& db = CloudProjectsDatabase::Get();
475
476 const auto projectId = mCreateSnapshotResponse->Project.Id;
477 const auto snapshotId = mCreateSnapshotResponse->Snapshot.Id;
478
479 if (result.Code != SyncResultCode::Success)
480 {
481 db.RemovePendingSnapshot(projectId, snapshotId);
482 db.RemovePendingProjectBlob(projectId, snapshotId);
483 db.RemovePendingProjectBlocks(projectId, snapshotId);
484
485 DataUploadFailed(result);
486 return;
487 }
488
490 db.RemovePendingProjectBlob(projectId, snapshotId);
491
492 if (mProjectBlocksLock->MissingBlocks.empty())
493 {
495 return;
496 }
497
500 mProjectBlocksLock->MissingBlocks,
501 [this, weakThis = weak_from_this()](
502 auto result, auto block, auto uploadResult)
503 {
504 auto strongThis = weakThis.lock();
505 if (!strongThis)
506 return;
507
508 if (!IsUploadRecoverable(uploadResult.Code))
510 mCreateSnapshotResponse->Project.Id, block.Id);
511
513 *this, block.Hash,
514 uploadResult.Code == SyncResultCode::Success);
515
516 const auto completed =
517 result.UploadedBlocks == result.TotalBlocks ||
518 result.FailedBlocks != 0;
519 const bool succeeded = completed && result.FailedBlocks == 0;
520
521 if (!completed)
522 return;
523
524 if (succeeded)
526 else
527 DataUploadFailed(result);
528 });
529 });
530}
531
533 const CreateSnapshotResponse& response, const ProjectUploadData& projectData)
534{
536 { response.Project.Id, response.Snapshot.Id,
538 response.Project.Id, response.Snapshot.Id) });
539
541 { response.Project.Id, response.Snapshot.Id,
544 response.SyncState.FileUrls.FailUrl, projectData.ProjectSnapshot });
545
546 if (mProjectBlocksLock->MissingBlocks.empty())
547 return;
548
549 std::vector<PendingProjectBlockData> pendingBlocks;
550 pendingBlocks.reserve(mProjectBlocksLock->MissingBlocks.size());
551
552 for (const auto& block : mProjectBlocksLock->MissingBlocks)
553 {
554 pendingBlocks.push_back(PendingProjectBlockData {
555 response.Project.Id, response.Snapshot.Id, block.BlockUrls.UploadUrl,
556 block.BlockUrls.SuccessUrl, block.BlockUrls.FailUrl, block.Block.Id,
557 static_cast<int>(block.Block.Format), block.Block.Hash });
558 }
559
561}
562
564{
565 using namespace network_manager;
567 mCreateSnapshotResponse->Project.Id,
568 mCreateSnapshotResponse->Snapshot.Id));
569
570 SetCommonHeaders(request);
571
572 auto response = NetworkManager::GetInstance().doPost(request, nullptr, 0);
573
574 response->setRequestFinishedCallback(
575 [this, response, weakThis = weak_from_this()](auto)
576 {
577 auto strongThis = weakThis.lock();
578 if (!strongThis)
579 return;
580
582 mCreateSnapshotResponse->Project.Id,
583 mCreateSnapshotResponse->Snapshot.Id);
584
585 if (response->getError() != NetworkError::NoError)
586 {
588 return;
589 }
590
591 mCompleted.store(true, std::memory_order_release);
593 });
594
595 mCancellationContext->OnCancelled(response);
596}
597
599{
601 return;
602
603 using namespace network_manager;
604
606 mCreateSnapshotResponse->Project.Id,
607 mCreateSnapshotResponse->Snapshot.Id));
608
609 SetCommonHeaders(request);
610
611 auto response = NetworkManager::GetInstance().doDelete(request);
612
613 response->setRequestFinishedCallback(
614 [this, response, strongThis = shared_from_this()](auto)
615 {
617 mCreateSnapshotResponse->Project.Id,
618 mCreateSnapshotResponse->Snapshot.Id);
619 });
620}
621
622} // 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
AudiocomTrace
Definition: ExportUtils.h:27
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:43
std::string GetAccessToken() const
Return the current access token, if any.
Configuration for the audio.com.
Definition: ServiceConfig.h:25
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={})
LocalProjectSnapshot(Tag, const ServiceConfig &config, const OAuthService &oauthService, ProjectCloudExtension &extension, std::string name, UploadMode mode, AudiocomTrace trace)
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
static Future Create(const ServiceConfig &config, const OAuthService &oauthService, ProjectCloudExtension &extension, std::string name, UploadMode mode, AudiocomTrace trace)
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 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.
void OnSyncCompleted(const ProjectUploadOperation *uploadOperation, std::optional< CloudSyncError > error, AudiocomTrace trace)
This method is called from any 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)