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..df08fe1db4 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,52 @@ 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) == fileName.size()) { + ret.append(file.filePath()); + } + } + ret.sort(); + + 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(pos) + QString::fromUtf8(".~%1~").arg(i+1); + } else { + return filePath + QString::fromUtf8(".~1~"); + } +} + QString Project::saveProjectInternal(const QString & path, const QString & name, @@ -595,20 +642,40 @@ Project::saveProjectInternal(const QString & path, } } // ofile - if ( QFile::exists(filePath) ) { - QFile::remove(filePath); + 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))); + } } - int nAttemps = 0; - while ( nAttemps < 10 && !fileCopy(tmpFilename, filePath) ) { - ++nAttemps; - } + 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; - if (nAttemps >= 10) { - throw std::runtime_error( "Failed to save to " + filePath.toStdString() ); - } + while ( nAttemps < 10 && !fileCopy(tmpFilename, filePath) ) { + ++nAttemps; + } + + if (nAttemps >= 10) { + throw std::runtime_error( "Failed to save to " + filePath.toStdString() ); + } + } - QFile::remove(tmpFilename); + 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;