diff --git a/libraries/lib-cloud-audiocom/CMakeLists.txt b/libraries/lib-cloud-audiocom/CMakeLists.txt index 1ecae6dd44eb..8495891a198d 100644 --- a/libraries/lib-cloud-audiocom/CMakeLists.txt +++ b/libraries/lib-cloud-audiocom/CMakeLists.txt @@ -44,6 +44,8 @@ set( SOURCES sync/ProjectUploadOperation.h sync/RemoteProjectSnapshot.cpp sync/RemoteProjectSnapshot.h + sync/ResumedSnaphotUploadOperation.cpp + sync/ResumedSnaphotUploadOperation.h sync/WavPackCompressor.cpp sync/WavPackCompressor.h ) diff --git a/libraries/lib-cloud-audiocom/CloudSyncService.cpp b/libraries/lib-cloud-audiocom/CloudSyncService.cpp index d3327d452060..3fca95c8c276 100644 --- a/libraries/lib-cloud-audiocom/CloudSyncService.cpp +++ b/libraries/lib-cloud-audiocom/CloudSyncService.cpp @@ -273,8 +273,7 @@ CloudSyncService& CloudSyncService::Get() } CloudSyncService::GetProjectsFuture CloudSyncService::GetProjects( - concurrency::CancellationContextPtr context, int page, - int pageSize, + concurrency::CancellationContextPtr context, int page, int pageSize, std::string searchString) { using namespace audacity::network_manager; @@ -340,6 +339,12 @@ CloudSyncService::SyncFuture CloudSyncService::OpenFromCloud( return mSyncPromise.get_future(); } + if (projectId.empty()) + { + FailSync({ ResponseResultCode::InternalClientError, "Empty projectId" }); + return mSyncPromise.get_future(); + } + if (!callback) mProgressCallback = [](auto...) { return true; }; else @@ -403,8 +408,7 @@ CloudSyncService::SyncFuture CloudSyncService::SyncProject( return; } - auto& projectFileIO = ProjectFileIO::Get(project); - + // Do not perform the snapshot request if the project is up to date if (remoteInfo.HeadSnapshot.Id == projectInfo.SnapshotId) { CompleteSync({ sync::ProjectSyncResult::StatusCode::Succeeded }); @@ -490,15 +494,42 @@ void CloudSyncService::SyncCloudSnapshot( InvisibleTemporaryProject project; ProjectFileIO::Get(project.Project()).LoadProject(wxPath, true); } - else if (HasAutosave(utf8Path)) + else { - if (mode == SyncMode::Normal) + assert(localProjectInfo.has_value()); + assert(mode != SyncMode::ForceNew); + // The project exists on the disk. Depending on how we got here, we might + // different scenarios: + // 1. Local snapshot ID matches the remote snapshot ID. Just complete the + // sync right away. If the project was modified locally, but not saved, + // the user will be prompted about the autosave. + if (localProjectInfo->SnapshotId == snapshotInfo.Id) { - FailSync({ ResponseResultCode::Conflict }); + CompleteSync( + { sync::ProjectSyncResult::StatusCode::Succeeded, {}, utf8Path }); return; } - - DropAutosave(utf8Path); + // 2. Project sync was interrupted. + if ( + mode == SyncMode::Normal && + localProjectInfo->SyncStatus != sync::DBProjectData::SyncStatusSynced) + { + // There is not enough information to decide if the project has + // diverged. Just open it, so the sync can resume. If the project has + // diverged, the user will be prompted to resolve the conflict on the + // next save. + CompleteSync( + { sync::ProjectSyncResult::StatusCode::Succeeded, {}, utf8Path }); + return; + } + // 3. Project was modified locally, but not saved. + if (HasAutosave(utf8Path)) + { + if (mode == SyncMode::Normal) + FailSync({ ResponseResultCode::Conflict }); + else + DropAutosave(utf8Path); + } } mRemoteSnapshot = sync::RemoteProjectSnapshot::Sync( diff --git a/libraries/lib-cloud-audiocom/sync/CloudSyncError.cpp b/libraries/lib-cloud-audiocom/sync/CloudSyncError.cpp index e277a94c5f41..739897515263 100644 --- a/libraries/lib-cloud-audiocom/sync/CloudSyncError.cpp +++ b/libraries/lib-cloud-audiocom/sync/CloudSyncError.cpp @@ -85,4 +85,39 @@ CloudSyncError MakeClientFailure(const char* message) return { CloudSyncError::ClientFailure, message }; } +CloudSyncError::ErrorType DeduceError(ResponseResultCode code) +{ + switch (code) + { + case ResponseResultCode::Success: + return CloudSyncError::None; + case ResponseResultCode::Cancelled: + return CloudSyncError::Cancelled; + case ResponseResultCode::Expired: + return CloudSyncError::DataUploadFailed; + case ResponseResultCode::Conflict: + return CloudSyncError::ProjectVersionConflict; + case ResponseResultCode::ConnectionFailed: + return CloudSyncError::Network; + case ResponseResultCode::PaymentRequired: + return CloudSyncError::ProjectStorageLimitReached; + case ResponseResultCode::TooLarge: + return CloudSyncError::ProjectStorageLimitReached; + case ResponseResultCode::Unauthorized: + return CloudSyncError::Authorization; + case ResponseResultCode::Forbidden: + return CloudSyncError::Authorization; + case ResponseResultCode::NotFound: + return CloudSyncError::ProjectNotFound; + case ResponseResultCode::UnexpectedResponse: + return CloudSyncError::Server; + case ResponseResultCode::InternalClientError: + return CloudSyncError::ClientFailure; + case ResponseResultCode::UnknownError: + return CloudSyncError::DataUploadFailed; + } + + return CloudSyncError::DataUploadFailed; +} + } // namespace audacity::cloud::audiocom::sync diff --git a/libraries/lib-cloud-audiocom/sync/CloudSyncError.h b/libraries/lib-cloud-audiocom/sync/CloudSyncError.h index 2d2a60ce882d..904d280851a1 100644 --- a/libraries/lib-cloud-audiocom/sync/CloudSyncError.h +++ b/libraries/lib-cloud-audiocom/sync/CloudSyncError.h @@ -12,6 +12,8 @@ #include +#include "NetworkUtils.h" + class TranslatableString; namespace audacity::network_manager @@ -54,4 +56,7 @@ CloudSyncError MakeClientFailure(const std::string& message); CLOUD_AUDIOCOM_API CloudSyncError MakeClientFailure(const char* message); +CLOUD_AUDIOCOM_API CloudSyncError::ErrorType +DeduceError(ResponseResultCode code); + } // namespace audacity::cloud::audiocom::sync diff --git a/libraries/lib-cloud-audiocom/sync/CloudSyncUtils.cpp b/libraries/lib-cloud-audiocom/sync/CloudSyncUtils.cpp index 2014e1216be0..f91ba69665f0 100644 --- a/libraries/lib-cloud-audiocom/sync/CloudSyncUtils.cpp +++ b/libraries/lib-cloud-audiocom/sync/CloudSyncUtils.cpp @@ -183,6 +183,9 @@ bool Deserialize(const rapidjson::Value& value, SnapshotInfo& urls) if (!Deserialize(value, "date_updated", tempSnapshot.Updated)) return {}; + if (!Deserialize(value, "date_synced", tempSnapshot.Synced)) + return {}; + if (!Deserialize(value, "file_size", tempSnapshot.FileSize)) return {}; diff --git a/libraries/lib-cloud-audiocom/sync/CloudSyncUtils.h b/libraries/lib-cloud-audiocom/sync/CloudSyncUtils.h index 9a63d584029e..7a3d0500a999 100644 --- a/libraries/lib-cloud-audiocom/sync/CloudSyncUtils.h +++ b/libraries/lib-cloud-audiocom/sync/CloudSyncUtils.h @@ -80,6 +80,7 @@ struct SnapshotInfo final int64_t Created {}; int64_t Updated {}; + int64_t Synced {}; int64_t FileSize; int64_t BlocksSize; diff --git a/libraries/lib-cloud-audiocom/sync/LocalProjectSnapshot.cpp b/libraries/lib-cloud-audiocom/sync/LocalProjectSnapshot.cpp index 756e1e028e16..3305b5c5b72e 100644 --- a/libraries/lib-cloud-audiocom/sync/LocalProjectSnapshot.cpp +++ b/libraries/lib-cloud-audiocom/sync/LocalProjectSnapshot.cpp @@ -291,44 +291,6 @@ void LocalProjectSnapshot::UploadFailed(CloudSyncError error) mProjectCloudExtension.OnSyncCompleted(this, std::make_optional(error)); } -namespace -{ -CloudSyncError::ErrorType DeduceError(ResponseResultCode code) -{ - switch (code) - { - case ResponseResultCode::Success: - return CloudSyncError::None; - case ResponseResultCode::Cancelled: - return CloudSyncError::Cancelled; - case ResponseResultCode::Expired: - return CloudSyncError::DataUploadFailed; - case ResponseResultCode::Conflict: - return CloudSyncError::ProjectVersionConflict; - case ResponseResultCode::ConnectionFailed: - return CloudSyncError::Network; - case ResponseResultCode::PaymentRequired: - return CloudSyncError::ProjectStorageLimitReached; - case ResponseResultCode::TooLarge: - return CloudSyncError::ProjectStorageLimitReached; - case ResponseResultCode::Unauthorized: - return CloudSyncError::Authorization; - case ResponseResultCode::Forbidden: - return CloudSyncError::Authorization; - case ResponseResultCode::NotFound: - return CloudSyncError::ProjectNotFound; - case ResponseResultCode::UnexpectedResponse: - return CloudSyncError::Server; - case ResponseResultCode::InternalClientError: - return CloudSyncError::ClientFailure; - case ResponseResultCode::UnknownError: - return CloudSyncError::DataUploadFailed; - } - - return CloudSyncError::DataUploadFailed; -} -} // namespace - void LocalProjectSnapshot::DataUploadFailed(const ResponseResult& uploadResult) { UploadFailed({ DeduceError(uploadResult.Code), uploadResult.Content }); @@ -515,7 +477,7 @@ void LocalProjectSnapshot::OnSnapshotCreated( if (mProjectBlocksLock->MissingBlocks.empty()) { - MarkSnapshotSynced(0); + MarkSnapshotSynced(); return; } @@ -541,7 +503,7 @@ void LocalProjectSnapshot::OnSnapshotCreated( if (succeeded) { - MarkSnapshotSynced(handledBlocks); + MarkSnapshotSynced(); return; } @@ -583,26 +545,19 @@ void LocalProjectSnapshot::StorePendingSnapshot( CloudProjectsDatabase::Get().AddPendingProjectBlocks(pendingBlocks); } -void LocalProjectSnapshot::MarkSnapshotSynced(int64_t blocksCount) +void LocalProjectSnapshot::MarkSnapshotSynced() { - using namespace audacity::network_manager; + using namespace network_manager; Request request(mServiceConfig.GetSnapshotSyncUrl( mProjectCloudExtension.GetCloudProjectId(), mProjectCloudExtension.GetSnapshotId())); - const auto language = mServiceConfig.GetAcceptLanguageValue(); - - if (!language.empty()) - request.setHeader( - audacity::network_manager::common_headers::AcceptLanguage, language); - - request.setHeader( - common_headers::Authorization, mOAuthService.GetAccessToken()); + SetCommonHeaders(request); auto response = NetworkManager::GetInstance().doPost(request, nullptr, 0); response->setRequestFinishedCallback( - [this, response, blocksCount](auto) + [this, response](auto) { CloudProjectsDatabase::Get().RemovePendingSnapshot( mCreateSnapshotResponse->Project.Id, diff --git a/libraries/lib-cloud-audiocom/sync/LocalProjectSnapshot.h b/libraries/lib-cloud-audiocom/sync/LocalProjectSnapshot.h index 725e7ce7c029..98b06bf5e299 100644 --- a/libraries/lib-cloud-audiocom/sync/LocalProjectSnapshot.h +++ b/libraries/lib-cloud-audiocom/sync/LocalProjectSnapshot.h @@ -84,7 +84,7 @@ class CLOUD_AUDIOCOM_API LocalProjectSnapshot final : OnSnapshotCreated(const CreateSnapshotResponse& response, bool newProject); void StorePendingSnapshot( const CreateSnapshotResponse& response, const ProjectUploadData& data); - void MarkSnapshotSynced(int64_t blocksCount); + void MarkSnapshotSynced(); ProjectCloudExtension& mProjectCloudExtension; diff --git a/libraries/lib-cloud-audiocom/sync/ProjectCloudExtension.cpp b/libraries/lib-cloud-audiocom/sync/ProjectCloudExtension.cpp index c6943eadff64..7309983613f9 100644 --- a/libraries/lib-cloud-audiocom/sync/ProjectCloudExtension.cpp +++ b/libraries/lib-cloud-audiocom/sync/ProjectCloudExtension.cpp @@ -53,7 +53,6 @@ struct ProjectCloudExtension::UploadQueueElement final int64_t BlocksHandled { 0 }; int64_t BlocksTotal { 0 }; - bool SnapshotCreated { false }; bool ProjectDataUploaded { false }; bool ReadyForUpload { false }; bool Synced { false }; @@ -160,6 +159,24 @@ void ProjectCloudExtension::OnSyncStarted() mUploadQueue.push_back(std::move(element)); } +void cloud::audiocom::sync::ProjectCloudExtension::OnSyncResumed( + std::shared_ptr uploadOperation, + int64_t missingBlocksCount, bool needsProjectUpload) +{ + auto element = std::make_shared(); + element->Operation = uploadOperation; + element->BlocksTotal = missingBlocksCount; + element->ProjectDataUploaded = !needsProjectUpload; + + { + auto lock = std::lock_guard { mUploadQueueMutex }; + mUploadQueue.push_back(element); + } + + uploadOperation->Start(UploadMode::Normal); + UnsafeUpdateProgress(); +} + void ProjectCloudExtension::OnUploadOperationCreated( std::shared_ptr uploadOperation) { @@ -234,7 +251,6 @@ void ProjectCloudExtension::OnSnapshotCreated( if (!element) return; - element->SnapshotCreated = true; element->BlocksTotal = response.SyncState.MissingBlocks.size(); UnsafeUpdateProgress(); @@ -275,6 +291,9 @@ void ProjectCloudExtension::OnSyncCompleted( { auto lock = std::lock_guard { mUploadQueueMutex }; + if (mUploadQueue.empty()) + return; + if (uploadOperation == nullptr) { mUploadQueue.pop_back(); diff --git a/libraries/lib-cloud-audiocom/sync/ProjectCloudExtension.h b/libraries/lib-cloud-audiocom/sync/ProjectCloudExtension.h index 36146befd828..dae7df890098 100644 --- a/libraries/lib-cloud-audiocom/sync/ProjectCloudExtension.h +++ b/libraries/lib-cloud-audiocom/sync/ProjectCloudExtension.h @@ -18,15 +18,14 @@ #include #include #include -#include #include #include "ClientData.h" #include "Observer.h" -#include "ProjectUploadOperation.h" #include "CloudSyncError.h" #include "CloudSyncUtils.h" +#include "ProjectUploadOperation.h" class AudacityProject; class ProjectSerializer; @@ -69,6 +68,10 @@ class CLOUD_AUDIOCOM_API ProjectCloudExtension final : public ClientData::Base //! This method is called from the UI thread void OnSyncStarted(); //! This method is called from the UI thread + void OnSyncResumed( + std::shared_ptr uploadOperation, + int64_t missingBlocksCount, bool needsProjectUpload); + //! This method is called from the UI thread void OnUploadOperationCreated( std::shared_ptr uploadOperation); //! This method is called not from the UI thread diff --git a/libraries/lib-cloud-audiocom/sync/ResumedSnaphotUploadOperation.cpp b/libraries/lib-cloud-audiocom/sync/ResumedSnaphotUploadOperation.cpp new file mode 100644 index 000000000000..b2d11e0de8f6 --- /dev/null +++ b/libraries/lib-cloud-audiocom/sync/ResumedSnaphotUploadOperation.cpp @@ -0,0 +1,285 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/*!******************************************************************** + + Audacity: A Digital Audio Editor + + ResumedSnaphotUploadOperation.cpp + + Dmitry Vedenko + +**********************************************************************/ + +#include "ResumedSnaphotUploadOperation.h" + +#include +#include +#include + +#include "concurrency/CancellationContext.h" + +#include "CloudProjectsDatabase.h" +#include "DataUploader.h" +#include "MissingBlocksUploader.h" +#include "ProjectCloudExtension.h" +#include "ProjectUploadOperation.h" +#include "ServiceConfig.h" + +#include "SampleBlock.h" +#include "WaveTrack.h" + +#include "Request.h" +#include "IResponse.h" +#include "NetworkManager.h" + +namespace audacity::cloud::audiocom::sync +{ +namespace +{ +class ResumedSnaphotUploadOperation final : + public ProjectUploadOperation, + public std::enable_shared_from_this +{ + struct Tag + { + }; + +public: + ResumedSnaphotUploadOperation( + Tag, ProjectCloudExtension& projectCloudExtension, + std::string_view snapshotId, std::string_view confirmationUrl) + : mProjectCloudExtension { projectCloudExtension } + , mProjectId { mProjectCloudExtension.GetCloudProjectId() } + , mSnapshotId { snapshotId } + , mConfirmationUrl { confirmationUrl } + , mCancellationContext { concurrency::CancellationContext::Create() } + { + } + + ~ResumedSnaphotUploadOperation() override + { + } + + static void Perform( + ProjectCloudExtension& projectCloudExtension, std::string_view snapshotId, + std::string_view confirmationUrl) + { + auto& cloudProjectsDatabase = CloudProjectsDatabase::Get(); + + auto operation = std::make_shared( + Tag {}, projectCloudExtension, snapshotId, confirmationUrl); + + const auto projectId = projectCloudExtension.GetCloudProjectId(); + + operation->mPendingProjectBlobData = + cloudProjectsDatabase.GetPendingProjectBlob(projectId, snapshotId); + + operation->mPendingProjectBlocks = + cloudProjectsDatabase.GetPendingProjectBlocks(projectId, snapshotId); + + const int64_t totalBlocks = operation->mPendingProjectBlocks.size(); + + projectCloudExtension.OnSyncResumed( + operation, totalBlocks, + operation->mPendingProjectBlobData.has_value()); + } + +private: + void UploadSnapshot() + { + const auto urls = UploadUrls { {}, + mPendingProjectBlobData->UploadUrl, + mPendingProjectBlobData->ConfirmUrl, + mPendingProjectBlobData->FailUrl }; + + DataUploader::Get().Upload( + mCancellationContext, GetServiceConfig(), urls, + mPendingProjectBlobData->BlobData, + [this](auto result) + { + if (result.Code != ResponseResultCode::ConnectionFailed) + CloudProjectsDatabase::Get().RemovePendingProjectBlob( + mProjectId, mSnapshotId); + + if (result.Code == ResponseResultCode::Success) + { + mProjectCloudExtension.OnProjectDataUploaded(*this); + UploadBlocks(); + } + else + FailSync(std::move(result)); + }); + } + + void CompleteSync() + { + mCompleted.store(true); + mProjectCloudExtension.OnSyncCompleted(this, {}); + } + + void FailSync (CloudSyncError error) + { + mCompleted.store(true); + mProjectCloudExtension.OnSyncCompleted(this, error); + } + + void FailSync(ResponseResult result) + { + FailSync(CloudSyncError { DeduceError(result.Code), result.Content }); + } + + void UploadBlocks() + { + if (mPendingProjectBlocks.empty()) + MarkSnapshotSynced(); + + auto project = mProjectCloudExtension.GetProject().lock(); + + if (!project) + { + FailSync({ ResponseResultCode::InternalClientError }); + return; + } + + auto& waveTrackFactory = WaveTrackFactory::Get(*project); + auto& sampleBlockFactory = waveTrackFactory.GetSampleBlockFactory(); + + std::vector blockTasks; + blockTasks.reserve(mPendingProjectBlocks.size()); + + for (const auto& pendingBlock : mPendingProjectBlocks) + { + BlockUploadTask task; + + task.BlockUrls = { {}, + pendingBlock.UploadUrl, + pendingBlock.ConfirmUrl, + pendingBlock.FailUrl }; + + task.Block.Format = + static_cast(pendingBlock.BlockSampleFormat); + task.Block.Hash = pendingBlock.BlockHash; + task.Block.Id = pendingBlock.BlockId; + task.Block.Block = sampleBlockFactory->CreateFromId( + task.Block.Format, pendingBlock.BlockId); + + blockTasks.push_back(std::move(task)); + } + + mMissingBlocksUploader = MissingBlocksUploader::Create( + mCancellationContext, GetServiceConfig(), std::move(blockTasks), + [this]( + const MissingBlocksUploadProgress& progress, + const LockedBlock& block, ResponseResult blockResponseResult) + { + const auto handledBlocks = + progress.UploadedBlocks + progress.FailedBlocks; + + if ( + blockResponseResult.Code != ResponseResultCode::ConnectionFailed) + CloudProjectsDatabase::Get().RemovePendingProjectBlock( + mProjectId, mSnapshotId, block.Id); + + mProjectCloudExtension.OnBlockUploaded( + *this, block.Hash, + blockResponseResult.Code == ResponseResultCode::Success); + + const auto completed = handledBlocks == progress.TotalBlocks; + const bool succeeded = completed && progress.FailedBlocks == 0; + + if (succeeded) + { + MarkSnapshotSynced(); + return; + } + + if (completed && !succeeded) + FailSync(std::move(blockResponseResult)); + }); + } + + void Start(UploadMode mode) override + { + if (mPendingProjectBlobData.has_value()) + UploadSnapshot(); + else + UploadBlocks(); + } + + void MarkSnapshotSynced() + { + using namespace network_manager; + Request request(mConfirmationUrl); + + SetCommonHeaders(request); + + auto response = NetworkManager::GetInstance().doPost(request, nullptr, 0); + + response->setRequestFinishedCallback( + [this, response](auto) + { + CloudProjectsDatabase::Get().RemovePendingSnapshot( + mProjectId, mSnapshotId); + + if (response->getError() != NetworkError::NoError) + { + FailSync(DeduceUploadError(*response)); + return; + } + + CompleteSync(); + }); + + mCancellationContext->OnCancelled(response); + } + + void SetUploadData(const ProjectUploadData& data) override + { + // This method will never be called for resumed operations + } + + bool IsCompleted() const override + { + return mCompleted.load(); + } + + void Cancel() override + { + mCancellationContext->Cancel(); + } + + ProjectCloudExtension& mProjectCloudExtension; + + std::string mProjectId; + std::string mSnapshotId; + std::string mConfirmationUrl; + + concurrency::CancellationContextPtr mCancellationContext; + + std::optional mPendingProjectBlobData; + std::vector mPendingProjectBlocks; + + std::shared_ptr mMissingBlocksUploader; + + std::atomic mCompleted { false }; +}; // class ResumedProjectUploadOperation + +} // namespace + +void ResumeProjectUpload( + ProjectCloudExtension& projectCloudExtension, + std::function onBeforeUploadStarts) +{ + auto& cloudProjectsDatabase = CloudProjectsDatabase::Get(); + + auto pendingSnapshots = cloudProjectsDatabase.GetPendingSnapshots( + projectCloudExtension.GetCloudProjectId()); + + if (!pendingSnapshots.empty() && onBeforeUploadStarts) + onBeforeUploadStarts(); + + for (const auto& snapshot : pendingSnapshots) + ResumedSnaphotUploadOperation::Perform( + projectCloudExtension, snapshot.SnapshotId, snapshot.ConfirmUrl); +} + +} // namespace audacity::cloud::audiocom::sync diff --git a/libraries/lib-cloud-audiocom/sync/ResumedSnaphotUploadOperation.h b/libraries/lib-cloud-audiocom/sync/ResumedSnaphotUploadOperation.h new file mode 100644 index 000000000000..f709506daf55 --- /dev/null +++ b/libraries/lib-cloud-audiocom/sync/ResumedSnaphotUploadOperation.h @@ -0,0 +1,24 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/*!******************************************************************** + + Audacity: A Digital Audio Editor + + ResumedSnaphotUploadOperation.h + + Dmitry Vedenko + +**********************************************************************/ + +#pragma once + +#include + +namespace audacity::cloud::audiocom::sync +{ +class ProjectCloudExtension; + +void CLOUD_AUDIOCOM_API ResumeProjectUpload( + ProjectCloudExtension& projectCloudExtension, + std::function onBeforeUploadStarts); + +} // namespace audacity::cloud::audiocom::sync diff --git a/modules/mod-cloud-audiocom/CloudProjectFileIOExtensions.cpp b/modules/mod-cloud-audiocom/CloudProjectFileIOExtensions.cpp index a2d8d52e990f..c5a76df37cf8 100644 --- a/modules/mod-cloud-audiocom/CloudProjectFileIOExtensions.cpp +++ b/modules/mod-cloud-audiocom/CloudProjectFileIOExtensions.cpp @@ -26,6 +26,7 @@ #include "sync/CloudSyncUtils.h" #include "sync/LocalProjectSnapshot.h" #include "sync/ProjectCloudExtension.h" +#include "sync/ResumedSnaphotUploadOperation.h" #include "BasicUI.h" #include "CodeConversions.h" @@ -52,7 +53,13 @@ class IOExtension final : public ProjectFileIOExtension void OnLoad(AudacityProject& project) override { - ProjectCloudExtension::Get(project).OnLoad(); + auto& projectCloudExtenstion = ProjectCloudExtension::Get(project); + projectCloudExtenstion.OnLoad(); + + if (projectCloudExtenstion.IsCloudProject()) + ResumeProjectUpload( + projectCloudExtenstion, + [&project] { PerformBlockingAuth(&project); }); } OnSaveAction CreateSnapshot(AudacityProject& project, std::string name)