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 if(id >= 0)
96 {
97 Blocks.push_back({
98 id, pBlock->GetSampleFormat(), pBlock });
99 BlockIdToIndex[id] = Blocks.size() - 1;
100 }
101 //Do not compute hashes for negative id's, which describe
102 //the length of a silenced sequence. Making an id record in
103 //project blob is enough to restore block contents fully.
104 //VS: Older versions of Audacity encoded them as a regular
105 //blocks, but due to wrong sanity checks in `WavPackCompressor`
106 //attempt to load them could fail. The check above will purge
107 //such blocks if they are present in old project.
108 };
110 }
111
113 {
114 // TakeResult() will call UpdateHash() for each block
115 // not found in the cache
116 const auto result = Hasher->TakeResult();
117
118 for (auto [id, hash] : result)
119 {
120 auto it = BlockIdToIndex.find(id);
121
122 if (it == BlockIdToIndex.end())
123 {
124 assert(false);
125 continue;
126 }
127
128 while (BlockHashToIndex.find(hash) != BlockHashToIndex.end())
129 {
130 // Hash is used by another block, rehash
131 hash = crypto::sha256(hash);
132 }
133
135 Blocks[it->second].Hash = std::move(hash);
136 }
137
138 // This will potentially block, if the cache is being updated
139 // already
141
142 if (OnBlocksLocked)
144 }
145
147 {
149 return;
150
151 UpdateCacheFuture = std::async(
152 std::launch::async,
153 [this, hashes = std::move(NewHashes)]
154 {
156 Extension.GetCloudProjectId(), hashes);
157 });
158 }
159
160 bool GetHash(int64_t blockId, std::string& hash) const override
161 {
163 return false;
164
165 auto cachedResult = CloudProjectsDatabase::Get().GetBlockHash(
166 Extension.GetCloudProjectId(), blockId);
167
168 if (!cachedResult)
169 return false;
170
171 hash = std::move(*cachedResult);
172
173 return true;
174 }
175
176 void UpdateHash(int64_t blockId, const std::string& hash) override
177 {
178 NewHashes.emplace_back(blockId, hash);
179 }
180
181 void FillMissingBlocks(const std::vector<UploadUrls>& missingBlockUrls)
182 {
183 for (const auto& urls : missingBlockUrls)
184 {
185 auto it = BlockHashToIndex.find(ToUpper(urls.Id));
186
187 if (it == BlockHashToIndex.end())
188 {
189 assert(false);
190 continue;
191 }
192
193 const auto index = it->second;
194
195 MissingBlocks.push_back(BlockUploadTask { urls, Blocks[index] });
196 }
197 }
198};
199
201 Tag, const ServiceConfig& config, const OAuthService& oauthService,
203 AudiocomTrace trace)
204 : mProjectCloudExtension { extension }
205 , mWeakProject { extension.GetProject() }
206 , mServiceConfig { config }
207 , mOAuthService { oauthService }
208 , mAudiocomTrace { trace }
209 , mProjectName { std::move(name) }
210 , mUploadMode { mode }
211 , mCancellationContext { concurrency::CancellationContext::Create() }
212{
213}
214
216{
217}
218
220 const ServiceConfig& config, const OAuthService& oauthService,
222 AudiocomTrace trace)
223{
224 auto project = extension.GetProject().lock();
225
226 if (!project)
227 return {};
228
229 auto snapshot = std::make_shared<LocalProjectSnapshot>(
230 Tag {}, config, oauthService, extension, std::move(name), mode, trace);
231
232 snapshot->mProjectCloudExtension.OnUploadOperationCreated(snapshot);
233
234 snapshot->mProjectBlocksLock = std::make_unique<ProjectBlocksLock>(
236 [weakSnapshot = std::weak_ptr(snapshot)]
237 {
238 auto snapshot = weakSnapshot.lock();
239
240 if (snapshot == nullptr)
241 return;
242
243 auto project = snapshot->GetProject();
244
245 if (project == nullptr)
246 return;
247
248 snapshot->mProjectCloudExtension.OnBlocksHashed(*snapshot);
249 });
250
251 return snapshot->mCreateSnapshotPromise.get_future();
252}
253
255{
256 return mCompleted.load(std::memory_order_acquire);
257}
258
259std::shared_ptr<AudacityProject> LocalProjectSnapshot::GetProject()
260{
261 return mWeakProject.lock();
262}
263
265{
267}
268
270{
271 mProjectDataReady.store(true);
272 mProjectDataPromise.set_value(data);
273}
274
276{
277 mCancelled.store(true, std::memory_order_release);
278
279 mCancellationContext->Cancel();
280
281 if (!mProjectDataReady.load(std::memory_order_acquire))
282 mProjectDataPromise.set_value({});
283
285}
286
288{
289 mCancelled.store(true, std::memory_order_release);
290
291 mCancellationContext->Cancel();
292
293 if (!mProjectDataReady.load(std::memory_order_acquire))
294 mProjectDataPromise.set_value({});
295
297
299}
300
302{
303 if (!mCompleted.exchange(true, std::memory_order_release))
305 this, std::make_optional(error), mAudiocomTrace);
306}
307
309{
310 UploadFailed({ DeduceError(uploadResult.Code), uploadResult.Content });
311}
312
314 const MissingBlocksUploadProgress& uploadResult)
315{
317
318 for (const auto& uploadError : uploadResult.UploadErrors)
319 {
320 if (
321 uploadError.Code == SyncResultCode::Success ||
322 uploadError.Code == SyncResultCode::Conflict)
323 continue;
324
325 const auto deducedError = DeduceError(uploadError.Code);
326
327 if (
329 deducedError == CloudSyncError::Network)
330 {
331 errorType = deducedError;
332 }
333 else if (
335 deducedError == CloudSyncError::Cancelled)
336 {
337 errorType = deducedError;
338 break;
339 }
340 }
341
342 UploadFailed({ errorType, {} });
343}
344
346{
347 auto project = mWeakProject.lock();
348
349 if (project == nullptr)
350 {
352 XO("Project was closed before snapshot was created")));
353 return;
354 }
355
356 const bool isCloudProject = mProjectCloudExtension.IsCloudProject();
357 const bool createNew =
358 mUploadMode == UploadMode::CreateNew || !isCloudProject;
359
360 ProjectForm projectForm;
361
362 if (createNew)
363 projectForm.Name = mProjectName;
364 else
366
367 // For empty projects, mProjectBlocksLock will be nullptr at this point
368 if (mProjectBlocksLock != nullptr)
369 {
370 projectForm.Hashes.reserve(mProjectBlocksLock->Blocks.size());
371 std::transform(
372 mProjectBlocksLock->Blocks.begin(), mProjectBlocksLock->Blocks.end(),
373 std::back_inserter(projectForm.Hashes),
374 [](const auto& block) { return block.Hash; });
375 }
376
377 using namespace audacity::network_manager;
378
379 const auto url = createNew ? mServiceConfig.GetCreateProjectUrl() :
382
383 projectForm.Force = !createNew && mUploadMode == UploadMode::ForceOverwrite;
384
385 auto request = Request(url);
386
387 request.setHeader(
389 request.setHeader(
391 // request.setHeader(common_headers::ContentEncoding, "gzip");
392
393 const auto language = mServiceConfig.GetAcceptLanguageValue();
394
395 if (!language.empty())
396 request.setHeader(
398
399 request.setHeader(
401
402 auto serializedForm = Serialize(projectForm);
403
404 auto response = NetworkManager::GetInstance().doPost(
405 request, serializedForm.data(), serializedForm.size());
406
407 response->setRequestFinishedCallback(
408 [this, response, createNew, weakThis = weak_from_this()](auto)
409 {
410 auto strongThis = weakThis.lock();
411 if (!strongThis)
412 return;
413
414 const auto error = response->getError();
415
416 if (error != NetworkError::NoError)
417 {
419
420 mCreateSnapshotPromise.set_value({});
421 return;
422 }
423
424 const auto body = response->readAll<std::string>();
425 auto result = DeserializeCreateSnapshotResponse(body);
426
427 if (!result)
428 {
430 XO("Invalid Response: %s").Format(body).Translation()));
431
432 mCreateSnapshotPromise.set_value({});
433 return;
434 }
435
436 OnSnapshotCreated(*result, createNew);
437 });
438
439 mCancellationContext->OnCancelled(response);
440}
441
443 const CreateSnapshotResponse& response, bool newProject)
444{
445 auto project = mWeakProject.lock();
446
447 if (project == nullptr)
448 {
450 XO("Project was closed before snapshot was created")));
451 return;
452 }
453
454 if (newProject)
455 mProjectBlocksLock->UpdateProjectHashesInCache();
456
457 mProjectBlocksLock->FillMissingBlocks(response.SyncState.MissingBlocks);
458
460
461 {
462 auto lock = std::lock_guard { mCreateSnapshotResponseMutex };
463 mCreateSnapshotResponse = response;
464 }
465
466 mCreateSnapshotPromise.set_value({ response, shared_from_this() });
467
468 auto projectData = mProjectDataPromise.get_future().get();
469
470 if (mCancelled.load(std::memory_order_acquire))
471 return;
472
473 StorePendingSnapshot(response, projectData);
474
477 projectData.ProjectSnapshot,
478 [this, weakThis = weak_from_this()](ResponseResult result)
479 {
480 auto strongThis = weakThis.lock();
481 if (!strongThis)
482 return;
483
484 auto& db = CloudProjectsDatabase::Get();
485
486 const auto projectId = mCreateSnapshotResponse->Project.Id;
487 const auto snapshotId = mCreateSnapshotResponse->Snapshot.Id;
488
489 if (result.Code != SyncResultCode::Success)
490 {
491 db.RemovePendingSnapshot(projectId, snapshotId);
492 db.RemovePendingProjectBlob(projectId, snapshotId);
493 db.RemovePendingProjectBlocks(projectId, snapshotId);
494
495 DataUploadFailed(result);
496 return;
497 }
498
500 db.RemovePendingProjectBlob(projectId, snapshotId);
501
502 if (mProjectBlocksLock->MissingBlocks.empty())
503 {
505 return;
506 }
507
510 mProjectBlocksLock->MissingBlocks,
511 [this, weakThis = weak_from_this()](
512 auto result, auto block, auto uploadResult)
513 {
514 auto strongThis = weakThis.lock();
515 if (!strongThis)
516 return;
517
518 if (!IsUploadRecoverable(uploadResult.Code))
520 mCreateSnapshotResponse->Project.Id, block.Id);
521
523 *this, block.Hash,
524 uploadResult.Code == SyncResultCode::Success);
525
526 const auto completed =
527 result.UploadedBlocks == result.TotalBlocks ||
528 result.FailedBlocks != 0;
529 const bool succeeded = completed && result.FailedBlocks == 0;
530
531 if (!completed)
532 return;
533
534 if (succeeded)
536 else
537 DataUploadFailed(result);
538 });
539 });
540}
541
543 const CreateSnapshotResponse& response, const ProjectUploadData& projectData)
544{
546 { response.Project.Id, response.Snapshot.Id,
548 response.Project.Id, response.Snapshot.Id) });
549
551 { response.Project.Id, response.Snapshot.Id,
554 response.SyncState.FileUrls.FailUrl, projectData.ProjectSnapshot });
555
556 if (mProjectBlocksLock->MissingBlocks.empty())
557 return;
558
559 std::vector<PendingProjectBlockData> pendingBlocks;
560 pendingBlocks.reserve(mProjectBlocksLock->MissingBlocks.size());
561
562 for (const auto& block : mProjectBlocksLock->MissingBlocks)
563 {
564 pendingBlocks.push_back(PendingProjectBlockData {
565 response.Project.Id, response.Snapshot.Id, block.BlockUrls.UploadUrl,
566 block.BlockUrls.SuccessUrl, block.BlockUrls.FailUrl, block.Block.Id,
567 static_cast<int>(block.Block.Format), block.Block.Hash });
568 }
569
571}
572
574{
575 using namespace network_manager;
577 mCreateSnapshotResponse->Project.Id,
578 mCreateSnapshotResponse->Snapshot.Id));
579
580 SetCommonHeaders(request);
581
582 auto response = NetworkManager::GetInstance().doPost(request, nullptr, 0);
583
584 response->setRequestFinishedCallback(
585 [this, response, weakThis = weak_from_this()](auto)
586 {
587 auto strongThis = weakThis.lock();
588 if (!strongThis)
589 return;
590
592 mCreateSnapshotResponse->Project.Id,
593 mCreateSnapshotResponse->Snapshot.Id);
594
595 if (response->getError() != NetworkError::NoError)
596 {
598 return;
599 }
600
601 mCompleted.store(true, std::memory_order_release);
603 });
604
605 mCancellationContext->OnCancelled(response);
606}
607
609{
611 return;
612
613 using namespace network_manager;
614
616 mCreateSnapshotResponse->Project.Id,
617 mCreateSnapshotResponse->Snapshot.Id));
618
619 SetCommonHeaders(request);
620
621 auto response = NetworkManager::GetInstance().doDelete(request);
622
623 response->setRequestFinishedCallback(
624 [this, response, strongThis = shared_from_this()](auto)
625 {
627 mCreateSnapshotResponse->Project.Id,
628 mCreateSnapshotResponse->Snapshot.Id);
629 });
630}
631
632} // 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
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
wxString name
Definition: TagsEditor.cpp:166
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)