Audacity 3.2.0
ResumedSnaphotUploadOperation.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 ResumedSnaphotUploadOperation.cpp
7
8 Dmitry Vedenko
9
10**********************************************************************/
11
13
14#include <algorithm>
15#include <memory>
16#include <string>
17#include <string_view>
18
20
22#include "DataUploader.h"
26#include "ServiceConfig.h"
27
28#include "ExportUtils.h"
29#include "SampleBlock.h"
30#include "WaveTrack.h"
31
32#include "CodeConversions.h"
33#include "DateTimeConversions.h"
34#include "FromChars.h"
35#include "IResponse.h"
36#include "NetworkManager.h"
37#include "Request.h"
38#include "UriParser.h"
39
41{
42namespace
43{
44
45bool IsUrlExpired(const std::string& url)
46{
47 const auto parsedUri = ParseUri(url);
48 const auto parsedQuery = ParseUriQuery(parsedUri.Query);
49
50 const auto amzDateIt = parsedQuery.find("X-Amz-Date");
51
52 if (amzDateIt == parsedQuery.end())
53 return false;
54
55 SystemTime time;
56
57 if (!ParseISO8601Date(std::string(amzDateIt->second), &time))
58 return false;
59
60 const auto amzExpiresIt = parsedQuery.find("X-Amz-Expires");
61
62 if (amzExpiresIt == parsedQuery.end())
63 return false;
64
65 int64_t expiresSeconds;
66
67 auto expiresParseResult = FromChars(
68 amzExpiresIt->second.data(),
69 amzExpiresIt->second.data() + amzExpiresIt->second.size(),
70 expiresSeconds);
71
72 if (expiresParseResult.ec != std::errc {})
73 return false;
74
75 return (time + std::chrono::seconds { expiresSeconds }) <
76 std::chrono::system_clock::now();
77}
78
81 public std::enable_shared_from_this<ResumedSnaphotUploadOperation>
82{
83 struct Tag
84 {
85 };
86
87public:
89 Tag, ProjectCloudExtension& projectCloudExtension,
90 std::string_view snapshotId, std::string_view confirmationUrl)
91 : mProjectCloudExtension { projectCloudExtension }
92 , mProjectId { mProjectCloudExtension.GetCloudProjectId() }
93 , mSnapshotId { snapshotId }
94 , mConfirmationUrl { confirmationUrl }
95 , mCancellationContext { concurrency::CancellationContext::Create() }
96 {
97 }
98
100 {
101 }
102
103 static void Perform(
104 ProjectCloudExtension& projectCloudExtension, std::string_view snapshotId,
105 std::string_view confirmationUrl)
106 {
107 auto& cloudProjectsDatabase = CloudProjectsDatabase::Get();
108
109 auto operation = std::make_shared<ResumedSnaphotUploadOperation>(
110 Tag {}, projectCloudExtension, snapshotId, confirmationUrl);
111
112 const auto projectId = projectCloudExtension.GetCloudProjectId();
113
114 operation->mPendingProjectBlobData =
115 cloudProjectsDatabase.GetPendingProjectBlob(projectId, snapshotId);
116
117 operation->mPendingProjectBlocks =
118 cloudProjectsDatabase.GetPendingProjectBlocks(projectId, snapshotId);
119
120 const int64_t totalBlocks = operation->mPendingProjectBlocks.size();
121
122 if (operation->mPendingProjectBlobData)
123 {
124 operation->mHasExpiredUrls =
125 IsUrlExpired(operation->mPendingProjectBlobData->UploadUrl);
126 }
127
128 for (const auto& block : operation->mPendingProjectBlocks)
129 {
130 if (operation->mHasExpiredUrls)
131 break;
132
133 operation->mHasExpiredUrls = IsUrlExpired(block.UploadUrl);
134 }
135
136 projectCloudExtension.OnSyncResumed(
137 operation, totalBlocks,
138 operation->mPendingProjectBlobData.has_value());
139 }
140
141private:
143 {
144 const auto urls = UploadUrls { {},
145 mPendingProjectBlobData->UploadUrl,
146 mPendingProjectBlobData->ConfirmUrl,
147 mPendingProjectBlobData->FailUrl };
148
150 mCancellationContext, GetServiceConfig(), urls,
151 mPendingProjectBlobData->BlobData,
152 [this, weakThis = weak_from_this()](auto result)
153 {
154 auto strongThis = weakThis.lock();
155 if (!strongThis)
156 return;
157
158 if (!IsUploadRecoverable(result.Code))
160 mProjectId, mSnapshotId);
161
162 if (result.Code == SyncResultCode::Success)
163 {
164 mProjectCloudExtension.OnProjectDataUploaded(*this);
165 UploadBlocks();
166 }
167 else
168 FailSync(std::move(result));
169 });
170 }
171
173 {
174 if (!mCompleted.exchange(true))
175 mProjectCloudExtension.OnSyncCompleted(
177 }
178
180 {
181 if (!mCompleted.exchange(true))
182 mProjectCloudExtension.OnSyncCompleted(
184 }
185
187 {
188 FailSync(CloudSyncError { DeduceError(result.Code), result.Content });
189 }
190
192 {
193 if (mPendingProjectBlocks.empty())
194 MarkSnapshotSynced();
195
196 auto project = mProjectCloudExtension.GetProject().lock();
197
198 if (!project)
199 {
201 return;
202 }
203
204 auto& waveTrackFactory = WaveTrackFactory::Get(*project);
205 auto& sampleBlockFactory = waveTrackFactory.GetSampleBlockFactory();
206
207 std::vector<BlockUploadTask> blockTasks;
208 blockTasks.reserve(mPendingProjectBlocks.size());
209
210 for (const auto& pendingBlock : mPendingProjectBlocks)
211 {
212 BlockUploadTask task;
213
214 task.BlockUrls = { {},
215 pendingBlock.UploadUrl,
216 pendingBlock.ConfirmUrl,
217 pendingBlock.FailUrl };
218
219 task.Block.Format =
220 static_cast<sampleFormat>(pendingBlock.BlockSampleFormat);
221 task.Block.Hash = pendingBlock.BlockHash;
222 task.Block.Id = pendingBlock.BlockId;
223 try
224 {
225 task.Block.Block = sampleBlockFactory->CreateFromId(
226 task.Block.Format, pendingBlock.BlockId);
227
228 blockTasks.push_back(std::move(task));
229 }
230 catch (const FileException& e)
231 {
232 // We have failed to resume the upload, local data is missing
234 mProjectId, mSnapshotId);
235 FailSync(
237 ToUTF8(
238 XO("Local project data was removed before the sync has completed")
239 .Translation()) });
240 return;
241 }
242 }
243
244 mMissingBlocksUploader = MissingBlocksUploader::Create(
245 mCancellationContext, GetServiceConfig(), std::move(blockTasks),
246 [this, weakThis = weak_from_this()](
247 const MissingBlocksUploadProgress& progress,
248 const LockedBlock& block, ResponseResult blockResponseResult)
249 {
250 auto strongThis = weakThis.lock();
251 if (!strongThis)
252 return;
253
254 if (
255 blockResponseResult.Code != SyncResultCode::ConnectionFailed &&
256 blockResponseResult.Code != SyncResultCode::Cancelled)
258 mProjectId, block.Id);
259
260 mProjectCloudExtension.OnBlockUploaded(
261 *this, block.Hash,
262 blockResponseResult.Code == SyncResultCode::Success);
263
264 const auto completed =
265 progress.UploadedBlocks == progress.TotalBlocks ||
266 progress.FailedBlocks != 0;
267
268 const bool succeeded = completed && progress.FailedBlocks == 0;
269
270 if (!completed)
271 return;
272
273 if (succeeded)
274 MarkSnapshotSynced();
275 else
276 FailSync(std::move(blockResponseResult));
277 });
278 }
279
280 void Start() override
281 {
282 if (mHasExpiredUrls)
283 RefreshUrls();
284 else if (mPendingProjectBlobData.has_value())
285 UploadSnapshot();
286 else
287 UploadBlocks();
288 }
289
291 {
292 using namespace network_manager;
294 mProjectId, mSnapshotId) };
295
296 SetCommonHeaders(request);
297
298 auto response = NetworkManager::GetInstance().doGet(request);
299
300 response->setRequestFinishedCallback(
301 [this, response, weakThis = weak_from_this()](auto)
302 {
303 auto strongThis = weakThis.lock();
304 if (!strongThis)
305 return;
306
307 if (response->getError() != NetworkError::NoError)
308 {
309 FailSync(DeduceUploadError(*response));
310 return;
311 }
312
313 auto syncState =
314 DeserializeProjectSyncState(response->readAll<std::string>());
315
316 if (!syncState)
317 {
318 FailSync(
319 MakeClientFailure(XO("Failed to deserialize the response")));
320 return;
321 }
322
323 UpdateUrls(*syncState);
324
325 if (mPendingProjectBlobData.has_value())
326 UploadSnapshot();
327 else
328 UploadBlocks();
329 });
330 }
331
332 void UpdateUrls(const ProjectSyncState& syncState)
333 {
334 if (mPendingProjectBlobData.has_value())
335 {
336 if (syncState.FileUrls.UploadUrl.empty())
337 {
338 mPendingProjectBlobData = {};
340 mProjectId, mSnapshotId);
341 }
342 else
343 {
344 mPendingProjectBlobData->UploadUrl = syncState.FileUrls.UploadUrl;
345 mPendingProjectBlobData->ConfirmUrl = syncState.FileUrls.SuccessUrl;
346 mPendingProjectBlobData->FailUrl = syncState.FileUrls.SuccessUrl;
347 }
348 }
349
350 std::unordered_map<std::string, UploadUrls> urlsLookup;
351 for (const auto& urls : syncState.MissingBlocks)
352 urlsLookup.emplace(urls.Id, urls);
353
354 for (auto& block : mPendingProjectBlocks)
355 {
356 auto it = urlsLookup.find(block.BlockHash);
357
358 if (it == urlsLookup.end())
359 {
361 mProjectId, block.BlockId);
362
363 continue;
364 }
365
366 block.UploadUrl = urlsLookup[block.BlockHash].UploadUrl;
367 block.ConfirmUrl = urlsLookup[block.BlockHash].SuccessUrl;
368 block.FailUrl = urlsLookup[block.BlockHash].FailUrl;
369 }
370
371 mPendingProjectBlocks.erase(
372 std::remove_if(
373 mPendingProjectBlocks.begin(), mPendingProjectBlocks.end(),
374 [&urlsLookup](auto& block)
375 { return urlsLookup.find(block.BlockHash) == urlsLookup.end(); }),
376 mPendingProjectBlocks.end());
377 }
378
380 {
381 using namespace network_manager;
382 Request request { mConfirmationUrl };
383
384 SetCommonHeaders(request);
385
386 auto response = NetworkManager::GetInstance().doPost(request, nullptr, 0);
387
388 response->setRequestFinishedCallback(
389 [this, response, weakThis = weak_from_this()](auto)
390 {
391 auto strongThis = weakThis.lock();
392 if (!strongThis)
393 return;
394
396 mProjectId, mSnapshotId);
397
398 if (response->getError() != NetworkError::NoError)
399 {
400 FailSync(DeduceUploadError(*response));
401 return;
402 }
403
404 CompleteSync();
405 });
406
407 mCancellationContext->OnCancelled(response);
408 }
409
410 void SetUploadData(const ProjectUploadData& data) override
411 {
412 // This method will never be called for resumed operations
413 }
414
415 bool IsCompleted() const override
416 {
417 return mCompleted.load();
418 }
419
420 void Cancel() override
421 {
422 mCancellationContext->Cancel();
424 }
425
427
428 std::string mProjectId;
429 std::string mSnapshotId;
430 std::string mConfirmationUrl;
431
433
434 std::optional<PendingProjectBlobData> mPendingProjectBlobData;
435 std::vector<PendingProjectBlockData> mPendingProjectBlocks;
436
437 std::shared_ptr<MissingBlocksUploader> mMissingBlocksUploader;
438
439 std::atomic<bool> mCompleted { false };
440
441 bool mHasExpiredUrls { false };
442}; // class ResumedProjectUploadOperation
443
444} // namespace
445
447 ProjectCloudExtension& projectCloudExtension,
448 std::function<void(AudiocomTrace)> onBeforeUploadStarts)
449{
450 auto& cloudProjectsDatabase = CloudProjectsDatabase::Get();
451
452 auto pendingSnapshots = cloudProjectsDatabase.GetPendingSnapshots(
453 projectCloudExtension.GetCloudProjectId());
454
455 if (!pendingSnapshots.empty() && onBeforeUploadStarts)
457
458 for (const auto& snapshot : pendingSnapshots)
459 ResumedSnaphotUploadOperation::Perform(
460 projectCloudExtension, snapshot.SnapshotId, snapshot.ConfirmUrl);
461}
462
463} // namespace audacity::cloud::audiocom::sync
Declare functions to perform UTF-8 to std::wstring conversions.
Declare functions to work with date and time string representations.
AudiocomTrace
Definition: ExportUtils.h:27
@ ProjectOpenedAndUploadResumed
FromCharsResult FromChars(const char *buffer, const char *last, float &value) noexcept
Parse a string into a single precision floating point value, always uses the dot as decimal.
Definition: FromChars.cpp:153
Declare functions to convert numeric types to string representation.
XO("Cut/Copy/Paste")
Declare an interface for HTTP response.
Declare a class for performing HTTP requests.
Declare a class for constructing HTTP requests.
sampleFormat
The ordering of these values with operator < agrees with the order of increasing bit width.
Definition: SampleFormat.h:30
const auto project
QueryFields ParseUriQuery(std::string_view query, std::string_view delimiter) noexcept
Parses URI query and returns QueryFields structure with parsed fields.
Definition: UriParser.cpp:59
UriFields ParseUri(std::string_view uri) noexcept
Definition: UriParser.cpp:9
Thrown for failure of file or database operations in deeply nested places.
Definition: FileException.h:19
static WaveTrackFactory & Get(AudacityProject &project)
Definition: WaveTrack.cpp:3358
std::string GetSnapshotSyncUrl(std::string_view projectId, std::string_view snapshotId) const
void RemovePendingProjectBlock(std::string_view projectId, int64_t blockId)
void RemovePendingSnapshot(std::string_view projectId, std::string_view snapshotId)
void RemovePendingProjectBlob(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={})
static std::shared_ptr< MissingBlocksUploader > Create(CancellationContextPtr cancellationContex, const ServiceConfig &serviceConfig, std::vector< BlockUploadTask > uploadTasks, MissingBlocksUploadProgressCallback progress)
void OnSyncResumed(std::shared_ptr< ProjectUploadOperation > uploadOperation, int64_t missingBlocksCount, bool needsProjectUpload)
This method is called from the UI thread.
static void Perform(ProjectCloudExtension &projectCloudExtension, std::string_view snapshotId, std::string_view confirmationUrl)
ResumedSnaphotUploadOperation(Tag, ProjectCloudExtension &projectCloudExtension, std::string_view snapshotId, std::string_view confirmationUrl)
ResponsePtr doPost(const Request &request, const void *data, size_t size)
ResponsePtr doGet(const Request &request)
CloudSyncError::ErrorType DeduceError(SyncResultCode code)
std::optional< ProjectSyncState > DeserializeProjectSyncState(const std::string &data)
void ResumeProjectUpload(ProjectCloudExtension &projectCloudExtension, std::function< void(AudiocomTrace)> onBeforeUploadStarts)
CloudSyncError MakeClientFailure(const TranslatableString &message)
CLOUD_AUDIOCOM_API CloudSyncError DeduceUploadError(audacity::network_manager::IResponse &response)
bool IsUploadRecoverable(SyncResultCode code)
void SetCommonHeaders(Request &request)
const ServiceConfig & GetServiceConfig()
Returns the instance of the ServiceConfig.
std::shared_ptr< CancellationContext > CancellationContextPtr
std::string ToUTF8(const std::wstring &wstr)
bool ParseISO8601Date(const std::string &dateString, SystemTime *time)
std::chrono::system_clock::time_point SystemTime