Audacity 3.2.0
CloudSyncService.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 CloudSyncService.cpp
7
8 Dmitry Vedenko
9
10**********************************************************************/
11#include "CloudSyncService.h"
12
13#include <algorithm>
14#include <cassert>
15#include <chrono>
16#include <mutex>
17#include <string>
18
20
22#include "sync/CloudSyncDTO.h"
24
25#include "CodeConversions.h"
26
27#include "OAuthService.h"
28#include "ServiceConfig.h"
29
30#include "MemoryX.h"
31
32#include "Project.h"
33#include "ProjectFileIO.h"
34
35#include "BasicUI.h"
36#include "FileNames.h"
37
38#include "IResponse.h"
39#include "NetworkManager.h"
40#include "Request.h"
41
43{
44namespace
45{
46std::mutex& GetResponsesMutex()
47{
48 static std::mutex mutex;
49 return mutex;
50}
51
52std::vector<std::shared_ptr<audacity::network_manager::IResponse>>&
54{
55 static std::vector<std::shared_ptr<audacity::network_manager::IResponse>>
56 requests;
57
58 return requests;
59}
60
62{
63 std::lock_guard lock { GetResponsesMutex() };
64
65 auto& requests = GetPendingRequests();
66
67 requests.erase(
68 std::remove_if(
69 requests.begin(), requests.end(),
70 [request](const auto& r) { return r.get() == request; }),
71 requests.end());
72}
73
75 OAuthService& oAuthService, std::string url,
76 std::function<void(ResponseResult)> dataCallback)
77{
78 assert(oAuthService.HasAccessToken());
79
80 if (!oAuthService.HasAccessToken())
81 {
82 dataCallback({ SyncResultCode::Unauthorized });
83 return;
84 }
85
86 using namespace audacity::network_manager;
87
88 auto request = Request(std::move(url));
89
90 request.setHeader(
92
93 request.setHeader(
95
96 SetCommonHeaders(request);
97
98 auto response = NetworkManager::GetInstance().doGet(request);
99
100 response->setRequestFinishedCallback(
101 [dataCallback = std::move(dataCallback)](auto response)
102 {
104 [dataCallback = std::move(dataCallback), response]
105 {
106 auto removeRequest =
107 finally([response] { RemovePendingRequest(response); });
108
109 dataCallback(GetResponseResult(*response, true));
110 });
111 });
112
113 std::lock_guard lock { GetResponsesMutex() };
114 GetPendingRequests().emplace_back(std::move(response));
115}
116
118 OAuthService& oAuthService, const ServiceConfig& serviceConfig,
119 std::string projectId,
120 std::function<void(sync::ProjectInfo, ResponseResult)> callback)
121{
122 assert(callback);
123
125 oAuthService, serviceConfig.GetProjectInfoUrl(projectId),
126 [callback = std::move(callback)](ResponseResult result)
127 {
128 if (result.Code != SyncResultCode::Success)
129 {
130 callback(sync::ProjectInfo {}, std::move(result));
131 return;
132 }
133
134 auto projectInfo = sync::DeserializeProjectInfo(result.Content);
135
136 if (!projectInfo)
137 {
138 callback(
140 std::move(result.Content) });
141 return;
142 }
143
144 callback(std::move(*projectInfo), std::move(result));
145 });
146}
147
149 OAuthService& oAuthService, const ServiceConfig& serviceConfig,
150 std::string projectId, std::string snapshotId,
151 std::function<void(sync::SnapshotInfo, ResponseResult result)> callback)
152{
153 assert(callback);
154
156 oAuthService, serviceConfig.GetSnapshotInfoUrl(projectId, snapshotId),
157 [callback = std::move(callback)](ResponseResult result)
158 {
159 if (result.Code != SyncResultCode::Success)
160 {
161 callback({}, std::move(result));
162 return;
163 }
164
165 auto snapshotInfo = sync::DeserializeSnapshotInfo(result.Content);
166
167 if (!snapshotInfo)
168 {
169 callback(
171 std::move(result.Content) });
172 return;
173 }
174
175 callback(std::move(*snapshotInfo), std::move(result));
176 });
177}
178
180 OAuthService& oAuthService, const ServiceConfig& serviceConfig,
181 std::string projectId, std::string snapshotId,
182 std::function<
184 callback)
185{
186 assert(callback);
187
189 oAuthService, serviceConfig, projectId,
190 [callback = std::move(callback), projectId,
191 snapshotId = std::move(snapshotId), &oAuthService,
192 &serviceConfig](sync::ProjectInfo projectInfo, ResponseResult result)
193 {
194 if (result.Code != SyncResultCode::Success)
195 {
196 callback({}, {}, std::move(result));
197 return;
198 }
199
200 const auto id = !snapshotId.empty() ?
201 snapshotId :
202 (projectInfo.HeadSnapshot.Synced > 0 ?
203 projectInfo.HeadSnapshot.Id :
204 projectInfo.LastSyncedSnapshotId);
205
206 if (id.empty())
207 {
208 callback(
209 std::move(projectInfo), sync::SnapshotInfo {},
210 { SyncResultCode::Success });
211 return;
212 }
213
215 oAuthService, serviceConfig, projectId, id,
216 [callback = std::move(callback),
217 projectInfo = std::move(projectInfo)](
218 sync::SnapshotInfo snapshotInfo, ResponseResult result)
219 {
220 callback(
221 std::move(projectInfo), std::move(snapshotInfo),
222 std::move(result));
223 });
224 });
225}
226
227bool HasAutosave(const std::string& path)
228{
229 auto connection = sqlite::Connection::Open(path, sqlite::OpenMode::ReadOnly);
230
231 if (!connection)
232 return false;
233
234 if (!connection->CheckTableExists("autosave"))
235 return false;
236
237 auto statement =
238 connection->CreateStatement("SELECT COUNT(1) FROM autosave");
239
240 if (!statement)
241 return false;
242
243 auto result = statement->Prepare().Run();
244
245 if (!result.IsOk())
246 return false;
247
248 for (const auto& row : result)
249 {
250 if (row.GetOr(0, 0) > 0)
251 return true;
252 }
253
254 return false;
255}
256
257bool DropAutosave(const std::string& path)
258{
259 auto connection =
260 sqlite::Connection::Open(path, sqlite::OpenMode::ReadWrite);
261
262 if (!connection)
263 return false;
264
265 if (!connection->CheckTableExists("autosave"))
266 return false;
267
268 auto statement = connection->CreateStatement("DELETE FROM autosave");
269
270 if (!statement)
271 return false;
272
273 auto result = statement->Prepare().Run();
274
275 return result.IsOk();
276}
277
278} // namespace
279
281{
282 static CloudSyncService service;
283 return service;
284}
285
286CloudSyncService::GetProjectsFuture CloudSyncService::GetProjects(
287 concurrency::CancellationContextPtr context, int page, int pageSize,
288 std::string_view searchString)
289{
290 using namespace audacity::network_manager;
291
292 auto promise = std::make_shared<GetProjectsPromise>();
293
294 auto& serviceConfig = GetServiceConfig();
295 auto& oAuthService = GetOAuthService();
296
297 auto request =
298 Request(serviceConfig.GetProjectsUrl(page, pageSize, searchString));
299
300 request.setHeader(
302 request.setHeader(
304
305 SetCommonHeaders(request);
306
307 auto response = NetworkManager::GetInstance().doGet(request);
308
309 context->OnCancelled(response);
310
311 response->setRequestFinishedCallback(
312 [promise, response](auto)
313 {
314 auto responseResult = GetResponseResult(*response, true);
315
316 if (responseResult.Code != SyncResultCode::Success)
317 {
318 promise->set_value(responseResult);
319 return;
320 }
321
322 auto projects =
323 sync::DeserializePaginatedProjectsResponse(responseResult.Content);
324
325 if (!projects)
326 {
327 promise->set_value(
328 ResponseResult { SyncResultCode::UnexpectedResponse,
329 std::move(responseResult.Content) });
330 return;
331 }
332
333 promise->set_value(std::move(*projects));
334 });
335
336 return promise->get_future();
337}
338
339CloudSyncService::SyncFuture CloudSyncService::OpenFromCloud(
340 std::string projectId, std::string snapshotId, SyncMode mode,
341 sync::ProgressCallback callback)
342{
344
345 // Reset promise
346 mSyncPromise = {};
347
348 if (mSyncInProcess.exchange(true))
349 {
350 CompleteSync({ sync::ProjectSyncResult::StatusCode::Blocked, {}, {} });
351 return mSyncPromise.get_future();
352 }
353
354 if (projectId.empty())
355 {
356 FailSync({ SyncResultCode::InternalClientError, "Empty projectId" });
357 return mSyncPromise.get_future();
358 }
359
360 if (!callback)
361 mProgressCallback = [](auto...) { return true; };
362 else
363 mProgressCallback = std::move(callback);
364
366 GetOAuthService(), GetServiceConfig(), projectId, snapshotId,
367 [this, mode](
368 sync::ProjectInfo projectInfo, sync::SnapshotInfo snapshotInfo,
369 ResponseResult result)
370 {
371 if (result.Code != SyncResultCode::Success)
372 FailSync(std::move(result));
373 else
374 SyncCloudSnapshot(projectInfo, snapshotInfo, mode);
375 });
376
377 return mSyncPromise.get_future();
378}
379
380CloudSyncService::SyncFuture CloudSyncService::SyncProject(
381 AudacityProject& project, const std::string& path, bool forceSync,
382 sync::ProgressCallback callback)
383{
385
386 // Reset promise
387 mSyncPromise = {};
388
389 if (mSyncInProcess.exchange(true))
390 {
391 CompleteSync({ sync::ProjectSyncResult::StatusCode::Blocked });
392 return mSyncPromise.get_future();
393 }
394
395 if (!callback)
396 mProgressCallback = [](auto...) { return true; };
397 else
398 mProgressCallback = std::move(callback);
399
400 auto& cloudDatabase = sync::CloudProjectsDatabase::Get();
401
402 auto projectInfo = cloudDatabase.GetProjectDataForPath(path);
403
404 if (!projectInfo)
405 {
406 // We assume, that the project is local
407 CompleteSync(path);
408 return mSyncPromise.get_future();
409 }
410
412 GetOAuthService(), GetServiceConfig(), projectInfo->ProjectId,
413 [projectInfo = *projectInfo, &project, path,
414 mode = forceSync ? SyncMode::ForceOverwrite : SyncMode::Normal,
415 this](sync::ProjectInfo remoteInfo, ResponseResult result)
416 {
417 if (result.Code != SyncResultCode::Success)
418 {
419 FailSync(std::move(result));
420 return;
421 }
422
423 // Do not perform the snapshot request if the project is up to date
424 if (remoteInfo.HeadSnapshot.Id == projectInfo.SnapshotId)
425 {
426 CompleteSync({ sync::ProjectSyncResult::StatusCode::Succeeded });
427 return;
428 }
429
430 const auto snapshotId = remoteInfo.HeadSnapshot.Synced > 0 ?
431 remoteInfo.HeadSnapshot.Id :
432 remoteInfo.LastSyncedSnapshotId;
433
435 GetOAuthService(), GetServiceConfig(), remoteInfo.Id, snapshotId,
436 [this, &project, mode,
437 remoteInfo](sync::SnapshotInfo snapshotInfo, ResponseResult result)
438 {
439 if (result.Code != SyncResultCode::Success)
440 FailSync(std::move(result));
441 else
442 SyncCloudSnapshot(remoteInfo, snapshotInfo, mode);
443 });
444 });
445
446 return mSyncPromise.get_future();
447}
448
449bool CloudSyncService::IsCloudProject(const std::string& path)
450{
451 auto& cloudDatabase = sync::CloudProjectsDatabase::Get();
452 auto projectInfo = cloudDatabase.GetProjectDataForPath(path);
453
454 return projectInfo.has_value();
455}
456
458CloudSyncService::GetProjectState(const std::string& projectId)
459{
460 auto& cloudDatabase = sync::CloudProjectsDatabase::Get();
461
462 auto projectInfo = cloudDatabase.GetProjectData(projectId);
463
464 if (!projectInfo)
465 return ProjectState::NotAvaliable;
466
467 if (!wxFileExists(ToWXString(projectInfo->LocalPath)))
468 return ProjectState::NotAvaliable;
469
470 if (projectInfo->SyncStatus == sync::DBProjectData::SyncStatusSynced)
471 return ProjectState::FullySynced;
472
473 return ProjectState::PendingSync;
474}
475
477CloudSyncService::GetHeadSnapshotID(std::string_view projectId)
478{
480
481 auto promise = std::make_shared<GetHeadSnapshotIDPromise>();
482
484 GetOAuthService(), GetServiceConfig(), std::string(projectId),
485 [promise](sync::ProjectInfo projectInfo, ResponseResult result)
486 {
487 if (result.Code != SyncResultCode::Success)
488 {
489 promise->set_value(
490 sync::GetHeadSnapshotIDResult { std::move(result) });
491 }
492 else
493 promise->set_value({ projectInfo.HeadSnapshot.Id });
494 });
495
496 return promise->get_future();
497}
498
499void CloudSyncService::FailSync(ResponseResult responseResult)
500{
501 CompleteSync(
502 sync::ProjectSyncResult { sync::ProjectSyncResult::StatusCode::Failed,
503 std::move(responseResult),
504 {} });
505}
506
507void CloudSyncService::CompleteSync(std::string path)
508{
509 CompleteSync(sync::ProjectSyncResult {
510 sync::ProjectSyncResult::StatusCode::Succeeded, {}, std::move(path) });
511}
512
513void CloudSyncService::CompleteSync(sync::ProjectSyncResult result)
514{
515 if (mRemoteSnapshot)
516 {
517 result.Stats = mRemoteSnapshot->GetTransferStats();
518 ReportUploadStats(mRemoteSnapshot->GetProjectId(), result.Stats);
519 }
520
521 mSyncPromise.set_value(std::move(result));
522 mRemoteSnapshot.reset();
523 mSyncInProcess.store(false);
524}
525
526void CloudSyncService::SyncCloudSnapshot(
527 const sync::ProjectInfo& projectInfo, const sync::SnapshotInfo& snapshotInfo,
528 SyncMode mode)
529{
530 // Get the project location
531 auto localProjectInfo =
532 sync::CloudProjectsDatabase::Get().GetProjectData(projectInfo.Id);
533
534 const auto createNew = !localProjectInfo || mode == SyncMode::ForceNew;
535
536 const auto wxPath = createNew ?
539 audacity::ToWXString(projectInfo.Name)) :
540 audacity::ToWXString(localProjectInfo->LocalPath);
541
542 const auto utf8Path = audacity::ToUTF8(wxPath);
543
544 const auto fileExists = wxFileExists(wxPath);
545
546 if (!fileExists)
547 {
548 if (snapshotInfo.Id.empty())
549 {
550 FailSync({ SyncResultCode::SyncImpossible });
551 return;
552 }
553
554 const auto dir = CloudProjectsSavePath.Read();
555 FileNames::MkDir(dir);
556
558 ProjectFileIO::Get(project.Project()).LoadProject(wxPath, true);
559 }
560 else
561 {
562 assert(localProjectInfo.has_value());
563 assert(mode != SyncMode::ForceNew);
564 // The project exists on the disk. Depending on how we got here, we might
565 // different scenarios:
566 // 1. Local snapshot ID matches the remote snapshot ID. Just complete the
567 // sync right away. If the project was modified locally, but not saved,
568 // the user will be prompted about the autosave.
569 if (
570 localProjectInfo->SnapshotId == snapshotInfo.Id &&
571 localProjectInfo->SyncStatus != sync::DBProjectData::SyncStatusDownloading)
572 {
573 CompleteSync(
574 { sync::ProjectSyncResult::StatusCode::Succeeded, {}, utf8Path });
575 return;
576 }
577 // 2. Project sync was interrupted.
578 if (
579 mode == SyncMode::Normal &&
580 localProjectInfo->SyncStatus == sync::DBProjectData::SyncStatusUploading)
581 {
582 // There is not enough information to decide if the project has
583 // diverged. Just open it, so the sync can resume. If the project has
584 // diverged, the user will be prompted to resolve the conflict on the
585 // next save.
586 CompleteSync(
587 { sync::ProjectSyncResult::StatusCode::Succeeded, {}, utf8Path });
588 return;
589 }
590 // 3. Project was modified locally, but not saved.
591 if (HasAutosave(utf8Path))
592 {
593 if (mode == SyncMode::Normal)
594 {
595 FailSync({ SyncResultCode::Conflict });
596 return;
597 }
598 else
599 DropAutosave(utf8Path);
600 }
601 }
602
603 if (snapshotInfo.Id.empty())
604 {
605 FailSync({ SyncResultCode::SyncImpossible });
606 return;
607 }
608
609 mRemoteSnapshot = sync::RemoteProjectSnapshot::Sync(
610 projectInfo, snapshotInfo, utf8Path,
611 [this, createNew, path = utf8Path, projectId = projectInfo.Id,
612 snapshotId = snapshotInfo.Id](sync::RemoteProjectSnapshotState state)
613 {
614 UpdateDowloadProgress(
615 (state.ProjectDownloaded + state.BlocksDownloaded) /
616 (state.BlocksTotal + 1.0));
617
618 if (state.IsComplete())
619 {
620 const bool success = state.Result.Code == SyncResultCode::Success;
621
622 CompleteSync({ success ?
623 sync::ProjectSyncResult::StatusCode::Succeeded :
624 sync::ProjectSyncResult::StatusCode::Failed,
625 std::move(state.Result), std::move(path) });
626 }
627 },
628 mode == SyncMode::ForceNew);
629}
630
631void CloudSyncService::UpdateDowloadProgress(double downloadProgress)
632{
633 mDownloadProgress.store(downloadProgress);
634
635 if (mProgressUpdateQueued.exchange(true))
636 return;
637
639 [this]
640 {
641 auto remoteSnapshot = mRemoteSnapshot;
642
643 if (remoteSnapshot && !mProgressCallback(mDownloadProgress.load()))
644 remoteSnapshot->Cancel();
645
646 mProgressUpdateQueued.store(false);
647 });
648}
649
650void CloudSyncService::ReportUploadStats(
651 std::string_view projectId, const TransferStats& stats)
652{
653 sync::NetworkStats networkStats;
654
655 networkStats.Bytes = stats.BytesTransferred;
656 networkStats.Blocks = stats.BlocksTransferred;
657 networkStats.Files = stats.ProjectFilesTransferred;
658 networkStats.Mixes = 0;
659 networkStats.IsDownload = true;
660
661 using namespace audacity::network_manager;
662
663 auto request = Request(GetServiceConfig().GetNetworkStatsUrl(projectId));
664
665 request.setHeader(
667
668 SetCommonHeaders(request);
669
670 auto body = Serialize(networkStats);
671
672 auto response =
673 NetworkManager::GetInstance().doPost(request, body.data(), body.size());
674 // Keep response alive
675 response->setRequestFinishedCallback([response](auto) {});
676}
677
678} // namespace audacity::cloud::audiocom
Toolkit-neutral facade for basic user interface services.
#define ASSERT_MAIN_THREAD()
Definition: BasicUI.h:406
Declare functions to perform UTF-8 to std::wstring conversions.
Declare an interface for HTTP response.
Declare a class for performing HTTP requests.
Declare a class for constructing HTTP requests.
const auto project
The top-level handle to an Audacity project. It serves as a source of events that other objects can b...
Definition: Project.h:90
Makes a temporary project that doesn't display on the screen.
std::optional< TentativeConnection > LoadProject(const FilePath &fileName, bool ignoreAutosave)
static ProjectFileIO & Get(AudacityProject &project)
bool Read(T *pVar) const
overload of Read returning a boolean that is true if the value was previously defined *‍/
Definition: Prefs.h:207
std::future< sync::GetProjectsResult > GetProjectsFuture
std::future< sync::GetHeadSnapshotIDResult > GetHeadSnapshotIDFuture
std::future< sync::ProjectSyncResult > SyncFuture
Service responsible for OAuth authentication against the audio.com service.
Definition: OAuthService.h:40
bool HasAccessToken() const
Indicates, that service has a valid access token, i. e. that the user is authorized.
Configuration for the audio.com.
Definition: ServiceConfig.h:23
std::string GetSnapshotInfoUrl(std::string_view projectId, std::string_view snapshotId) const
std::string GetProjectInfoUrl(std::string_view projectId) const
Interface, that provides access to the data from the HTTP response.
Definition: IResponse.h:113
ResponsePtr doGet(const Request &request)
void CallAfter(Action action)
Schedule an action to be done later, and in the main thread.
Definition: BasicUI.cpp:214
Services * Get()
Fetch the global instance, or nullptr if none is yet installed.
Definition: BasicUI.cpp:202
FILES_API wxString MkDir(const wxString &Str)
void Sync(const wxString &string)
Definition: Journal.cpp:321
FrameStatistics & GetInstance() noexcept
std::vector< std::shared_ptr< audacity::network_manager::IResponse > > & GetPendingRequests()
void PerformProjectGetRequest(OAuthService &oAuthService, std::string url, std::function< void(ResponseResult)> dataCallback)
void RemovePendingRequest(audacity::network_manager::IResponse *request)
void GetProjectInfo(OAuthService &oAuthService, const ServiceConfig &serviceConfig, std::string projectId, std::function< void(sync::ProjectInfo, ResponseResult)> callback)
void GetSnapshotInfo(OAuthService &oAuthService, const ServiceConfig &serviceConfig, std::string projectId, std::string snapshotId, std::function< void(sync::ProjectInfo, sync::SnapshotInfo, ResponseResult result)> callback)
std::optional< ProjectInfo > DeserializeProjectInfo(const std::string &data)
wxString MakeSafeProjectPath(const wxString &rootDir, const wxString &projectName)
std::string Serialize(const ProjectForm &form)
std::optional< PaginatedProjectsResponse > DeserializePaginatedProjectsResponse(const std::string &data)
std::optional< SnapshotInfo > DeserializeSnapshotInfo(const std::string &data)
std::function< bool(double)> ProgressCallback
void SetCommonHeaders(Request &request)
OAuthService & GetOAuthService()
Returns the instance of the OAuthService.
ResponseResult GetResponseResult(IResponse &response, bool readBody)
const ServiceConfig & GetServiceConfig()
Returns the instance of the ServiceConfig.
std::shared_ptr< CancellationContext > CancellationContextPtr
std::string ToUTF8(const std::wstring &wstr)
wxString ToWXString(const std::string &str)