From 425eb155c735a38304d9f9200e9dbf716ef0005d Mon Sep 17 00:00:00 2001 From: Frederic Devernay Date: Thu, 6 Jan 2022 19:16:47 -0800 Subject: [PATCH 1/2] Save backup versions of projects --- CHANGELOG.md | 8 +---- Engine/Project.cpp | 86 +++++++++++++++++++++++++++++++++++++++------ Engine/Settings.cpp | 20 +++++++++++ Engine/Settings.h | 3 ++ 4 files changed, 100 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20eb7294d3..371415d7fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,17 +17,11 @@ ### Changes - Allow creating a node with the same name that was just deleted. #732 +- Natron can now keep up to 32 project backups (see Preferences/General/Save versions). #562 ## Version 2.4.2 -### Known issues - -- Crash when closing a project window on macOS 12+ (Qt4 only). #712 -- Rendering sometimes silently stalls after X frames. #248 -- Some image formats may have issues (PCX, PSB). #602 -- MTS video files are sometimes not read correctly. #186 - ### Changes - Fix OpenFX overlay actions being executed in the wrong order. #711 diff --git a/Engine/Project.cpp b/Engine/Project.cpp index 1e12be46c6..ae7061aabe 100644 --- a/Engine/Project.cpp +++ b/Engine/Project.cpp @@ -53,6 +53,7 @@ GCC_DIAG_UNUSED_LOCAL_TYPEDEFS_ON #include #include #include +#include #include #include #include @@ -500,6 +501,53 @@ fileCopy(const QString & source, return success; } +static QStringList +findBackups(const QString & filePath) +{ + QStringList ret; + if ( QFile::exists(filePath) ) { + ret.append(filePath); + } + // find files matching filePath.~[0-9]+~ + QRegExp rx(QString::fromUtf8("\\.~(\\d+)~$")); + QFileInfo fileInfo(filePath); + QString fileName = fileInfo.fileName(); + QDirIterator it(fileInfo.dir()); + while (it.hasNext()) { + QString filename = it.next(); + QFileInfo file(filename); + + if (file.isDir()) { // Check if it's a dir + continue; + } + + // If the filename contains target string - put it in the hitlist + QString fn = file.fileName(); + if (fn.startsWith(fileName) && rx.lastIndexIn(fn) >= 0) { + ret.append(file.filePath()); + } + } + ret.sort(); + qDebug() << "found backups:" << ret; + + return ret; +} + +// if filePath matches .*\.~[0-9]+~, increment the backup number +// else append .~1~ +static QString +nextBackup(const QString & filePath) +{ + QRegExp rx(QString::fromUtf8("\\.~(\\d+)~$")); + int pos = rx.lastIndexIn(filePath); + if (pos >= 0) { + int i = rx.cap(1).toInt(); + return filePath.left(i) + QString::fromUtf8(".~%1~").arg(i+1); + } else { + return filePath + QString::fromUtf8(".~1~"); + } +} + QString Project::saveProjectInternal(const QString & path, const QString & name, @@ -595,20 +643,38 @@ Project::saveProjectInternal(const QString & path, } } // ofile - if ( QFile::exists(filePath) ) { - QFile::remove(filePath); + // rotate backups + int saveVersions = autoSave ? 0 : appPTR->getCurrentSettings()->saveVersions(); + // find the list of ordered backups (including the file itself if it exists) + QStringList backups = findBackups(filePath); + // remove extra backups + for (int i = backups.size() - 1; i >= saveVersions; --i) { + if ( QFile::exists(backups.last()) ) { + QFile::remove(backups.last()); + } + backups.removeLast(); } - int nAttemps = 0; - - while ( nAttemps < 10 && !fileCopy(tmpFilename, filePath) ) { - ++nAttemps; + // rename existing backups + for (int i = backups.size() - 1; i >= 0; --i) { + QFile::rename(backups.at(i), nextBackup(backups.at(i))); } - if (nAttemps >= 10) { - throw std::runtime_error( "Failed to save to " + filePath.toStdString() ); - } + if (!QFile::rename(tmpFilename, filePath)) { + // QFile::rename() may fail, e.g. if tmpFilename and filePath are not on the same partition + if (!QFile::copy(tmpFilename, filePath)) { + int nAttemps = 0; - QFile::remove(tmpFilename); + while ( nAttemps < 10 && !fileCopy(tmpFilename, filePath) ) { + ++nAttemps; + } + + if (nAttemps >= 10) { + throw std::runtime_error( "Failed to save to " + filePath.toStdString() ); + } + } + + QFile::remove(tmpFilename); + } if (!autoSave && updateProjectProperties) { QString lockFilePath = getLockAbsoluteFilePath(); diff --git a/Engine/Settings.cpp b/Engine/Settings.cpp index 2af9dd6cf6..3f0bff1cff 100644 --- a/Engine/Settings.cpp +++ b/Engine/Settings.cpp @@ -193,6 +193,18 @@ Settings::initializeKnobsGeneral() "Disabling this will no longer save un-saved project.").arg( QString::fromUtf8(NATRON_APPLICATION_NAME) ) ); _generalTab->addKnob(_autoSaveUnSavedProjects); + _saveVersions = AppManager::createKnob( this, tr("Save versions") ); + _saveVersions->setName("saveVersions"); + _saveVersions->disableSlider(); + _saveVersions->setMinimum(0); + _saveVersions->setMaximum(32); + _saveVersions->setHintToolTip( tr("Number of versions created (for backup) when saving newer versions of a file.\n" + "This option keeps saved versions of your file in the same directory, adding " + ".~1~, .~2~, etc., with the number increasing to the number of versions you specify.\n" + "Older files will be named with a higher number. E.g. with the default setting of 2, " + "you will have three versions of your file: *.ntp (last saved), *.ntp.~1~ (second " + "last saved), *.~2~ (third last saved).") ); + _generalTab->addKnob(_saveVersions); _hostName = AppManager::createKnob( this, tr("Appear to plug-ins as") ); _hostName->setName("pluginHostName"); @@ -1447,6 +1459,7 @@ Settings::setDefaultValues() #endif _autoSaveUnSavedProjects->setDefaultValue(true); _autoSaveDelay->setDefaultValue(5, 0); + _saveVersions->setDefaultValue(1); _hostName->setDefaultValue(0); _customHostName->setDefaultValue(NATRON_ORGANIZATION_DOMAIN_TOPLEVEL "." NATRON_ORGANIZATION_DOMAIN_SUB "." NATRON_APPLICATION_NAME); @@ -2877,6 +2890,13 @@ Settings::isAutoSaveEnabledForUnsavedProjects() const return _autoSaveUnSavedProjects->getValue(); } +int +Settings::saveVersions() const +{ + return _saveVersions->getValue(); +} + + bool Settings::isSnapToNodeEnabled() const { diff --git a/Engine/Settings.h b/Engine/Settings.h index 10d1ff9006..58b84c2bb7 100644 --- a/Engine/Settings.h +++ b/Engine/Settings.h @@ -179,6 +179,8 @@ GCC_DIAG_SUGGEST_OVERRIDE_ON bool isAutoSaveEnabledForUnsavedProjects() const; + int saveVersions() const; + bool isSnapToNodeEnabled() const; bool isCheckForUpdatesEnabled() const; @@ -430,6 +432,7 @@ GCC_DIAG_SUGGEST_OVERRIDE_ON #endif KnobBoolPtr _autoSaveUnSavedProjects; KnobIntPtr _autoSaveDelay; + KnobIntPtr _saveVersions; KnobChoicePtr _hostName; KnobStringPtr _customHostName; From eca4de7897c6c6126ba6ac618e4c5f5554ae4cd0 Mon Sep 17 00:00:00 2001 From: Frederic Devernay Date: Fri, 7 Jan 2022 15:12:58 -0800 Subject: [PATCH 2/2] bug fix for backups beyond the first one --- Engine/Project.cpp | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/Engine/Project.cpp b/Engine/Project.cpp index ae7061aabe..df08fe1db4 100644 --- a/Engine/Project.cpp +++ b/Engine/Project.cpp @@ -523,12 +523,11 @@ findBackups(const QString & filePath) // If the filename contains target string - put it in the hitlist QString fn = file.fileName(); - if (fn.startsWith(fileName) && rx.lastIndexIn(fn) >= 0) { + if (fn.startsWith(fileName) && rx.lastIndexIn(fn) == fileName.size()) { ret.append(file.filePath()); } } ret.sort(); - qDebug() << "found backups:" << ret; return ret; } @@ -542,7 +541,7 @@ nextBackup(const QString & filePath) int pos = rx.lastIndexIn(filePath); if (pos >= 0) { int i = rx.cap(1).toInt(); - return filePath.left(i) + QString::fromUtf8(".~%1~").arg(i+1); + return filePath.left(pos) + QString::fromUtf8(".~%1~").arg(i+1); } else { return filePath + QString::fromUtf8(".~1~"); } @@ -643,20 +642,22 @@ Project::saveProjectInternal(const QString & path, } } // ofile - // rotate backups - int saveVersions = autoSave ? 0 : appPTR->getCurrentSettings()->saveVersions(); - // find the list of ordered backups (including the file itself if it exists) - QStringList backups = findBackups(filePath); - // remove extra backups - for (int i = backups.size() - 1; i >= saveVersions; --i) { - if ( QFile::exists(backups.last()) ) { - QFile::remove(backups.last()); + if (!autoSave) { + // rotate backups + int saveVersions = appPTR->getCurrentSettings()->saveVersions(); + // find the list of ordered backups (including the file itself if it exists) + QStringList backups = findBackups(filePath); + // remove extra backups + for (int i = backups.size() - 1; i >= saveVersions; --i) { + if ( QFile::exists(backups.last()) ) { + QFile::remove(backups.last()); + } + backups.removeLast(); + } + // rename existing backups + for (int i = backups.size() - 1; i >= 0; --i) { + QFile::rename(backups.at(i), nextBackup(backups.at(i))); } - backups.removeLast(); - } - // rename existing backups - for (int i = backups.size() - 1; i >= 0; --i) { - QFile::rename(backups.at(i), nextBackup(backups.at(i))); } if (!QFile::rename(tmpFilename, filePath)) {