Skip to content

Commit

Permalink
Merge pull request musescore#17558 from cbjeukendrup/save_to_cloud/co…
Browse files Browse the repository at this point in the history
…nflict_checking

Use server-side conflict checking when publishing/saving to cloud
  • Loading branch information
RomanPudashkin authored May 16, 2023
2 parents d78daf0 + 76f7ec1 commit ba4ebb8
Show file tree
Hide file tree
Showing 12 changed files with 136 additions and 38 deletions.
2 changes: 2 additions & 0 deletions src/cloud/clouderrors.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ enum class Err {

AccessTokenIsEmpty,
AccountNotActivated,
Conflict,
NetworkError, /// < use cloudNetworkErrorUserDescription to retrieve user-readable description
CouldNotReceiveSourceUrl,
};
Expand All @@ -49,6 +50,7 @@ inline Ret make_ret(Err e)
case Err::UnknownError: return Ret(retCode);
case Err::AccessTokenIsEmpty: return Ret(retCode, "Access token is empty");
case Err::AccountNotActivated: return Ret(retCode, "Account not activated");
case Err::Conflict: return Ret(retCode, "Conflict");
case Err::NetworkError: return Ret(retCode, "Network error");
case Err::CouldNotReceiveSourceUrl: return Ret(retCode, "Could not receive source url");
}
Expand Down
3 changes: 3 additions & 0 deletions src/cloud/cloudtypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ enum class Visibility {

struct ScoreInfo {
int id = 0;
int revisionId = 0;
QString title;
QString description;
Visibility visibility = Visibility::Private;
Expand All @@ -89,6 +90,8 @@ struct ScoreInfo {
bool equals = true;

equals &= (id == another.id);
equals &= (revisionId == another.revisionId);
equals &= (title == another.title);
equals &= (description == another.description);
equals &= (visibility == another.visibility);
equals &= (license == another.license);
Expand Down
2 changes: 1 addition & 1 deletion src/cloud/icloudprojectsservice.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class ICloudProjectsService : MODULE_EXPORT_INTERFACE
virtual ~ICloudProjectsService() = default;

virtual framework::ProgressPtr uploadScore(QIODevice& scoreData, const QString& title, Visibility visibility = Visibility::Private,
const QUrl& sourceUrl = QUrl()) = 0;
const QUrl& sourceUrl = QUrl(), int revisionId = 0) = 0;
virtual framework::ProgressPtr uploadAudio(QIODevice& audioData, const QString& audioFormat, const QUrl& sourceUrl) = 0;

virtual RetVal<ScoreInfo> downloadScoreInfo(const QUrl& sourceUrl) = 0;
Expand Down
24 changes: 20 additions & 4 deletions src/cloud/internal/cloudservice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ static const std::string STATUS_KEY("status");
static constexpr int USER_UNAUTHORIZED_STATUS_CODE = 401;
static constexpr int FORBIDDEN_CODE = 403;
static constexpr int NOT_FOUND_STATUS_CODE = 404;
static constexpr int CONFLICT_STATUS_CODE = 409;

static constexpr int INVALID_SCORE_ID = 0;

Expand Down Expand Up @@ -347,6 +348,7 @@ mu::RetVal<ScoreInfo> CloudService::downloadScoreInfo(int scoreId)
QJsonObject scoreInfo = document.object();

result.val.id = scoreInfo.value("id").toInt();
result.val.revisionId = scoreInfo.value("revision_id").toInt();
result.val.title = scoreInfo.value("title").toString();
result.val.description = scoreInfo.value("description").toString();
result.val.license = scoreInfo.value("license").toString();
Expand Down Expand Up @@ -466,7 +468,8 @@ void CloudService::setAccountInfo(const AccountInfo& info)
m_userAuthorized.set(info.isValid());
}

ProgressPtr CloudService::uploadScore(QIODevice& scoreData, const QString& title, Visibility visibility, const QUrl& sourceUrl)
ProgressPtr CloudService::uploadScore(QIODevice& scoreData, const QString& title, Visibility visibility, const QUrl& sourceUrl,
int revisionId)
{
ProgressPtr progress = std::make_shared<Progress>();

Expand All @@ -477,8 +480,8 @@ ProgressPtr CloudService::uploadScore(QIODevice& scoreData, const QString& title

std::shared_ptr<ValMap> scoreUrlMap = std::make_shared<ValMap>();

auto uploadCallback = [this, manager, &scoreData, title, visibility, sourceUrl, scoreUrlMap]() {
RetVal<ValMap> urlMap = doUploadScore(manager, scoreData, title, visibility, sourceUrl);
auto uploadCallback = [this, manager, &scoreData, title, visibility, sourceUrl, revisionId, scoreUrlMap]() {
RetVal<ValMap> urlMap = doUploadScore(manager, scoreData, title, visibility, sourceUrl, revisionId);
*scoreUrlMap = urlMap.val;

return urlMap.ret;
Expand Down Expand Up @@ -527,6 +530,10 @@ static Ret uploadingRetFromRawUploadingRet(const Ret& rawRet, bool isScoreAlread
return make_ret(cloud::Err::AccountNotActivated);
}

if (code == CONFLICT_STATUS_CODE) {
return make_ret(cloud::Err::Conflict);
}

static const std::map<int, mu::TranslatableString> codes {
{ 400, mu::TranslatableString("cloud", "Invalid request") },
{ 401, mu::TranslatableString("cloud", "Authorization required") },
Expand All @@ -548,7 +555,7 @@ static Ret uploadingRetFromRawUploadingRet(const Ret& rawRet, bool isScoreAlread
}

mu::RetVal<mu::ValMap> CloudService::doUploadScore(INetworkManagerPtr uploadManager, QIODevice& scoreData, const QString& title,
Visibility visibility, const QUrl& sourceUrl)
Visibility visibility, const QUrl& sourceUrl, int revisionId)
{
TRACEFUNC;

Expand Down Expand Up @@ -597,6 +604,13 @@ mu::RetVal<mu::ValMap> CloudService::doUploadScore(INetworkManagerPtr uploadMana
scoreIdPart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"score_id\""));
scoreIdPart.setBody(QString::number(scoreId).toLatin1());
multiPart.append(scoreIdPart);

if (revisionId) {
QHttpPart revisionIdPart;
scoreIdPart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"last_revision_id\""));
scoreIdPart.setBody(QByteArray::number(revisionId));
multiPart.append(scoreIdPart);
}
}

QHttpPart titlePart;
Expand Down Expand Up @@ -635,6 +649,7 @@ mu::RetVal<mu::ValMap> CloudService::doUploadScore(INetworkManagerPtr uploadMana
QJsonObject scoreInfo = QJsonDocument::fromJson(receivedData.data()).object();
QUrl newSourceUrl = QUrl(scoreInfo.value("permalink").toString());
QUrl editUrl = QUrl(scoreInfo.value("edit_url").toString());
int newRevisionId = scoreInfo.value("revision_id").toInt();

if (!newSourceUrl.isValid()) {
result.ret = make_ret(cloud::Err::CouldNotReceiveSourceUrl);
Expand All @@ -643,6 +658,7 @@ mu::RetVal<mu::ValMap> CloudService::doUploadScore(INetworkManagerPtr uploadMana

result.val["sourceUrl"] = Val(newSourceUrl.toString());
result.val["editUrl"] = Val(editUrl.toString());
result.val["revisionId"] = Val(newRevisionId);

return result;
}
Expand Down
4 changes: 2 additions & 2 deletions src/cloud/internal/cloudservice.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ class CloudService : public QObject, public IAuthorizationService, public ICloud
Ret checkCloudIsAvailable() const override;

framework::ProgressPtr uploadScore(QIODevice& scoreData, const QString& title, Visibility visibility = Visibility::Private,
const QUrl& sourceUrl = QUrl()) override;
const QUrl& sourceUrl = QUrl(), int revisionId = 0) override;
framework::ProgressPtr uploadAudio(QIODevice& audioData, const QString& audioFormat, const QUrl& sourceUrl) override;

RetVal<ScoreInfo> downloadScoreInfo(const QUrl& sourceUrl) override;
Expand All @@ -95,7 +95,7 @@ private slots:
RetVal<ScoreInfo> downloadScoreInfo(int scoreId);

mu::RetVal<mu::ValMap> doUploadScore(network::INetworkManagerPtr uploadManager, QIODevice& scoreData, const QString& title,
Visibility visibility, const QUrl& sourceUrl = QUrl());
Visibility visibility, const QUrl& sourceUrl = QUrl(), int revisionId = 0);
Ret doUploadAudio(network::INetworkManagerPtr uploadManager, QIODevice& audioData, const QString& audioFormat, const QUrl& sourceUrl);

using RequestCallback = std::function<Ret()>;
Expand Down
6 changes: 5 additions & 1 deletion src/project/internal/isaveprojectscenario.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ class ISaveProjectScenario : MODULE_EXPORT_INTERFACE

virtual bool warnBeforeSavingToExistingPubliclyVisibleCloudProject() const = 0;

virtual void showCloudSaveError(const Ret& ret, bool publishMode, bool alreadyAttempted) const = 0;
static constexpr int RET_CODE_CONFLICT_RESPONSE_SAVE_AS = 1235;
static constexpr int RET_CODE_CONFLICT_RESPONSE_PUBLISH_AS_NEW_SCORE = 1236;
static constexpr int RET_CODE_CONFLICT_RESPONSE_REPLACE = 1237;

virtual Ret showCloudSaveError(const Ret& ret, const CloudProjectInfo& info, bool publishMode, bool alreadyAttempted) const = 0;
};
}

Expand Down
3 changes: 3 additions & 0 deletions src/project/internal/notationproject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ static const QString COMPOSER_TAG("composer");
static const QString LYRICIST_TAG("lyricist");
static const QString POET_TAG("poet");
static const QString SOURCE_TAG("source");
static const QString SOURCE_REVISION_ID_TAG("sourceRevisionId");
static const QString COPYRIGHT_TAG("copyright");
static const QString TRANSLATOR_TAG("translator");
static const QString ARRANGER_TAG("arranger");
Expand Down Expand Up @@ -426,6 +427,7 @@ const CloudProjectInfo& NotationProject::cloudInfo() const
if (!m_cloudInfo.isValid()) {
m_cloudInfo.name = io::filename(m_path, false).toQString();
m_cloudInfo.sourceUrl = m_masterNotation->masterScore()->metaTags()[SOURCE_TAG].toQString();
m_cloudInfo.revisionId = m_masterNotation->masterScore()->metaTags()[SOURCE_REVISION_ID_TAG].toInt();
}

return m_cloudInfo;
Expand All @@ -435,6 +437,7 @@ void NotationProject::setCloudInfo(const CloudProjectInfo& info)
{
m_cloudInfo = info;
m_masterNotation->masterScore()->setMetaTag(SOURCE_TAG, info.sourceUrl.toString());
m_masterNotation->masterScore()->setMetaTag(SOURCE_REVISION_ID_TAG, String::number(info.revisionId));
}

mu::Ret NotationProject::save(const io::path_t& path, SaveMode saveMode)
Expand Down
44 changes: 38 additions & 6 deletions src/project/internal/projectactionscontroller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -737,7 +737,8 @@ void ProjectActionsController::uploadProject(const CloudProjectInfo& info, const

bool isFirstSave = info.sourceUrl.isEmpty();

m_uploadingProjectProgress = cloudProjectsService()->uploadScore(*projectData, info.name, info.visibility, info.sourceUrl);
m_uploadingProjectProgress = cloudProjectsService()->uploadScore(*projectData, info.name, info.visibility, info.sourceUrl,
info.revisionId);

m_uploadingProjectProgress->started.onNotify(this, [this]() {
showUploadProgressDialog();
Expand All @@ -750,19 +751,20 @@ void ProjectActionsController::uploadProject(const CloudProjectInfo& info, const
}
});

m_uploadingProjectProgress->finished.onReceive(this, [this, project, projectData, audio, openEditUrl, publishMode,
m_uploadingProjectProgress->finished.onReceive(this, [this, project, projectData, info, audio, openEditUrl, publishMode,
isFirstSave](const ProgressResult& res) {
projectData->deleteLater();

if (!res.ret) {
LOGE() << res.ret.toString();
onProjectUploadFailed(res.ret, publishMode);
onProjectUploadFailed(res.ret, info, audio, openEditUrl, publishMode);
return;
}

ValMap urlMap = res.val.toMap();
QString newSourceUrl = urlMap["sourceUrl"].toQString();
QString editUrl = openEditUrl ? urlMap["editUrl"].toQString() : QString();
int newRevisionId = urlMap["revisionId"].toInt();

LOGD() << "Source url received: " << newSourceUrl;

Expand All @@ -773,11 +775,12 @@ void ProjectActionsController::uploadProject(const CloudProjectInfo& info, const
}

CloudProjectInfo info = project->cloudInfo();
if (info.sourceUrl == newSourceUrl) {
if (info.sourceUrl == newSourceUrl && info.revisionId == newRevisionId) {
return;
}

info.sourceUrl = newSourceUrl;
info.revisionId = newRevisionId;
project->setCloudInfo(info);

if (!project->isNewlyCreated()) {
Expand Down Expand Up @@ -852,13 +855,42 @@ void ProjectActionsController::onProjectSuccessfullyUploaded(const QUrl& urlToOp
}
}

void ProjectActionsController::onProjectUploadFailed(const Ret& ret, bool publishMode)
void ProjectActionsController::onProjectUploadFailed(const Ret& ret, const CloudProjectInfo& info, const AudioFile& audio, bool openEditUrl,
bool publishMode)
{
m_isProjectUploading = false;

closeUploadProgressDialog();

saveProjectScenario()->showCloudSaveError(ret, publishMode, true);
Ret userResponse = saveProjectScenario()->showCloudSaveError(ret, info, publishMode, true);
switch (userResponse.code()) {
case ISaveProjectScenario::RET_CODE_CONFLICT_RESPONSE_SAVE_AS: {
saveProject(SaveMode::SaveAs);
break;
}
case ISaveProjectScenario::RET_CODE_CONFLICT_RESPONSE_PUBLISH_AS_NEW_SCORE: {
CloudProjectInfo newInfo = info;
newInfo.sourceUrl = QUrl();
uploadProject(newInfo, audio, openEditUrl, publishMode);
break;
}
case ISaveProjectScenario::RET_CODE_CONFLICT_RESPONSE_REPLACE: {
RetVal<cloud::ScoreInfo> scoreInfo = cloudProjectsService()->downloadScoreInfo(info.sourceUrl);
if (!scoreInfo.ret) {
LOGE() << scoreInfo.ret.toString();
saveProjectScenario()->showCloudSaveError(scoreInfo.ret, info, publishMode, false);
return;
}

int cloudRevisionId = scoreInfo.val.revisionId;
CloudProjectInfo newInfo = info;
newInfo.revisionId = cloudRevisionId;
uploadProject(newInfo, audio, openEditUrl, publishMode);
break;
}
default:
break;
}
}

void ProjectActionsController::warnCloudIsNotAvailable()
Expand Down
2 changes: 1 addition & 1 deletion src/project/internal/projectactionscontroller.h
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ class ProjectActionsController : public IProjectFilesController, public QObject,
void uploadAudio(const AudioFile& audio, const QUrl& sourceUrl, const QUrl& urlToOpen, bool isFirstSave);

void onProjectSuccessfullyUploaded(const QUrl& urlToOpen = QUrl(), bool isFirstSave = true);
void onProjectUploadFailed(const Ret& ret, bool publishMode);
void onProjectUploadFailed(const Ret& ret, const CloudProjectInfo& info, const AudioFile& audio, bool openEditUrl, bool publishMode);

void warnCloudIsNotAvailable();

Expand Down
Loading

0 comments on commit ba4ebb8

Please sign in to comment.