diff --git a/src/App/Document.cpp b/src/App/Document.cpp index e762ee8a69..64e3cb7a41 100644 --- a/src/App/Document.cpp +++ b/src/App/Document.cpp @@ -2263,6 +2263,8 @@ public: BackupPolicy() { policy = Standard; numberOfFiles = 1; + useFCBakExtension = false; + saveBackupDateFormat = "%Y%m%d-%H%M%S"; } ~BackupPolicy() { } @@ -2272,6 +2274,12 @@ public: void setNumberOfFiles(int count) { numberOfFiles = count; } + void useBackupExtension(bool on) { + useFCBakExtension = on; + } + void setDateFormat(const std::string& fmt) { + saveBackupDateFormat = fmt; + } void apply(const std::string& sourcename, const std::string& targetname) { switch (policy) { case Standard: @@ -2341,16 +2349,191 @@ private: Base::Console().Warning("Cannot rename file from '%s' to '%s'\n", sourcename.c_str(), targetname.c_str()); } - } void applyTimeStamp(const std::string& sourcename, const std::string& targetname) { - (void)sourcename; - (void)targetname; + Base::FileInfo fi(targetname); + + std::string fn = sourcename; + std::string ext = fi.extension(); + std::string bn; // full path with no extension but with "." + std::string pbn; // base name of the project + "." + if (!ext.empty()) { + bn = fi.filePath().substr(0, fi.filePath().length() - ext.length()); + pbn = fi.fileName().substr(0, fi.fileName().length() - ext.length()); + } + else { + bn = fi.filePath() + "."; + pbn = fi.fileName() + "."; + } + + bool backupManagementError = false; // Note error and report at the end + if (fi.exists()) { + // replace . by - in format to avoid . between base name and extension + boost::replace_all(saveBackupDateFormat, ".", "-"); + { + // Remove all extra backups + std::string fn = fi.fileName(); + Base::FileInfo di(fi.dirPath()); + std::vector backup; + std::vector files = di.getDirectoryContent(); + for (std::vector::iterator it = files.begin(); it != files.end(); ++it) { + if (it->isFile()) { + std::string file = it->fileName(); + std::string fext = it->extension(); + std::string fextUp = fext; + std::transform(fextUp.begin(), fextUp.end(), fextUp.begin(),(int (*)(int))toupper); + // re-enforcing identification of the backup file + + + // old case : the name starts with the full name of the project and follows with numbers + if ((startsWith(file, fn) && + (file.length() > fn.length()) && + checkDigits(file.substr(fn.length()))) || + // .FCBak case : The bame starts with the base name of the project + "." + // + complement with no "." + ".FCBak" + ((fextUp == "FCBAK") && startsWith(file, pbn) && + (checkValidComplement(file, pbn, fext)))) { + backup.push_back(*it); + } + } + } + + if (!backup.empty() && (int)backup.size() >= numberOfFiles) { + std::sort (backup.begin(), backup.end(), fileComparisonByDate); + // delete the oldest backup file we found + // Base::FileInfo del = backup.front(); + int nb = 0; + for (std::vector::iterator it = backup.begin(); it != backup.end(); ++it) { + nb++; + if (nb >= numberOfFiles) { + try { + if (!it->deleteFile()) { + backupManagementError = true; + Base::Console().Warning("Cannot remove backup file : %s\n", it->fileName().c_str()); + } + } + catch (...) { + backupManagementError = true; + Base::Console().Warning("Cannot remove backup file : %s\n", it->fileName().c_str()); + } + } + } + + } + } //end remove backup + + // create a new backup file + { + int ext = 1; + if (useFCBakExtension) { + std::stringstream str; + Base::TimeInfo ti = fi.lastModified(); + time_t s =ti.getSeconds(); + struct tm * timeinfo = localtime(& s); + char buffer[100]; + + strftime(buffer,sizeof(buffer),saveBackupDateFormat.c_str(),timeinfo); + str << bn << buffer ; + + fn = str.str(); + bool done = false; + + if ((fn == "") || (fn[fn.length()-1] == ' ') || (fn[fn.length()-1] == '-')) { + if (fn[fn.length()-1] == ' ') { + fn = fn.substr(0,fn.length()-1); + } + } + else { + if (renameFileNoErase(fi, fn+".FCBak") == false) { + fn = fn + "-"; + } + else { + done = true; + } + } + + if (!done) { + while (ext < numberOfFiles + 10) { + if (renameFileNoErase(fi, fn+std::to_string(ext)+".FCBak")) + break; + ext++; + } + } + } + else { + // changed but simpler and solves also the delay sometimes introduced by google drive + while (ext < numberOfFiles + 10) { + // linux just replace the file if exists, and then the existence is to be tested before rename + if (renameFileNoErase(fi, fi.filePath()+std::to_string(ext))) + break; + ext++; + } + } + + if (ext >= numberOfFiles + 10) { + Base::Console().Error("File not saved: Cannot rename project file to backup file\n"); + //throw Base::FileException("File not saved: Cannot rename project file to backup file", fi); + } + } + } + + Base::FileInfo tmp(sourcename); + if (tmp.renameFile(targetname.c_str()) == false) { + Base::Console().Error("Save interrupted: Cannot rename file from '%s' to '%s'\n", + sourcename.c_str(), targetname.c_str()); + //throw Base::FileException("Save interrupted: Cannot rename temporary file to project file", tmp); + } + + if (numberOfFiles <= 0) { + try { + fi.deleteFile(); + } + catch (...) { + Base::Console().Warning("Cannot remove backup file: %s\n", fi.fileName().c_str()); + backupManagementError = true; + } + } + + if (backupManagementError) { + throw Base::FileException("Warning: Save complete, but error while managing backup history.", fi); + } + } + static bool fileComparisonByDate(const Base::FileInfo& i, + const Base::FileInfo& j) { + return (i.lastModified()>j.lastModified()); + } + bool startsWith(const std::string& st1, + const std::string& st2) const { + return st1.substr(0,st2.length()) == st2; + } + bool checkValidString (const std::string& cmpl, const boost::regex& e) const { + boost::smatch what; + bool res = boost::regex_search (cmpl,what,e); + return res; + } + bool checkValidComplement(const std::string& file, const std::string& pbn, const std::string& ext) const { + std::string cmpl = file.substr(pbn.length(),file.length()- pbn.length() - ext.length()-1); + boost::regex e (R"(^[^.]*$)"); + return checkValidString(cmpl,e); + } + bool checkDigits (const std::string& cmpl) const { + boost::regex e (R"(^[0-9]*$)"); + return checkValidString(cmpl,e); + } + bool renameFileNoErase(Base::FileInfo fi, const std::string& newName) { + // linux just replaces the file if it exists, so the existence is to be tested before rename + Base::FileInfo nf(newName); + if (!nf.exists()) { + return fi.renameFile(newName.c_str()); + } + return false; } private: Policy policy; int numberOfFiles; + bool useFCBakExtension; + std::string saveBackupDateFormat; }; } @@ -2419,9 +2602,20 @@ bool Document::saveToFile(const char* filename) const if (!backup) { count_bak = -1; } + bool useFCBakExtension = App::GetApplication().GetParameterGroupByPath + ("User parameter:BaseApp/Preferences/Document")->GetBool("UseFCBakExtension",false); + std::string saveBackupDateFormat = App::GetApplication().GetParameterGroupByPath + ("User parameter:BaseApp/Preferences/Document")->GetASCII("SaveBackupDateFormat","%Y%m%d-%H%M%S"); BackupPolicy policy; - policy.setPolicy(BackupPolicy::Standard); + if (useFCBakExtension) { + policy.setPolicy(BackupPolicy::TimeStamp); + policy.useBackupExtension(useFCBakExtension); + policy.setDateFormat(saveBackupDateFormat); + } + else { + policy.setPolicy(BackupPolicy::Standard); + } policy.setNumberOfFiles(count_bak); policy.apply(fn, filename); } diff --git a/src/Base/FileInfo.cpp b/src/Base/FileInfo.cpp index 9ac01920b2..1e26aa051b 100644 --- a/src/Base/FileInfo.cpp +++ b/src/Base/FileInfo.cpp @@ -482,6 +482,9 @@ bool FileInfo::renameFile(const char* NewName) int code = errno; std::clog << "Error in renameFile: " << strerror(code) << " (" << code << ")" << std::endl; } + else { + FileName = NewName; + } return res; } diff --git a/src/Gui/DlgSettingsDocument.ui b/src/Gui/DlgSettingsDocument.ui index 129007a979..1c60f6702f 100644 --- a/src/Gui/DlgSettingsDocument.ui +++ b/src/Gui/DlgSettingsDocument.ui @@ -7,7 +7,7 @@ 0 0 607 - 720 + 820 @@ -33,7 +33,16 @@ General - + + 9 + + + 9 + + + 9 + + 9 @@ -44,7 +53,16 @@ 6 - + + 0 + + + 0 + + + 0 + + 0 @@ -78,7 +96,16 @@ 6 - + + 0 + + + 0 + + + 0 + + 0 @@ -334,7 +361,16 @@ automatically run a file recovery when it is started. 6 - + + 0 + + + 0 + + + 0 + + 0 @@ -387,6 +423,58 @@ automatically run a file recovery when it is started. + + + + 6 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Use date and FCBak extension + + + UseFCBakExtension + + + Document + + + + + + + Date format + + + + + + + %Y%m%d-%H%M%S + + + SaveBackupDateFormat + + + Document + + + + + @@ -667,12 +755,12 @@ You can also use the form: John Doe <john@doe.com> setEnabled(bool) - 106 - 325 + 128 + 486 - 479 - 326 + 584 + 488 @@ -683,12 +771,44 @@ You can also use the form: John Doe <john@doe.com> setEnabled(bool) - 196 - 253 + 218 + 385 - 275 - 254 + 582 + 387 + + + + + prefSaveBackupFiles + toggled(bool) + prefSaveBackupExtension + setEnabled(bool) + + + 258 + 474 + + + 182 + 519 + + + + + prefSaveBackupFiles + toggled(bool) + prefSaveBackupDateFormat + setEnabled(bool) + + + 304 + 477 + + + 380 + 499 diff --git a/src/Gui/DlgSettingsDocumentImp.cpp b/src/Gui/DlgSettingsDocumentImp.cpp index 98f329b00c..4a463f8df4 100644 --- a/src/Gui/DlgSettingsDocumentImp.cpp +++ b/src/Gui/DlgSettingsDocumentImp.cpp @@ -46,6 +46,12 @@ DlgSettingsDocumentImp::DlgSettingsDocumentImp( QWidget* parent ) ui->prefSaveTransaction->hide(); ui->prefDiscardTransaction->hide(); + QString tip = QString::fromLatin1("

%1

" + "

%2: %Y%m%d-%H%M%S

" + "

%3: C++ strftime" + "

").arg(tr("The format of the date to use."), tr("Default"), tr("Format")); + ui->prefSaveBackupDateFormat->setToolTip(tip); + ui->prefCountBackupFiles->setMaximum(INT_MAX); ui->prefCompression->setMinimum(Z_NO_COMPRESSION); ui->prefCompression->setMaximum(Z_BEST_COMPRESSION); @@ -74,6 +80,8 @@ void DlgSettingsDocumentImp::saveSettings() ui->prefAddLogo->onSave(); ui->prefSaveBackupFiles->onSave(); ui->prefCountBackupFiles->onSave(); + ui->prefSaveBackupExtension->onSave(); + ui->prefSaveBackupDateFormat->onSave(); ui->prefDuplicateLabel->onSave(); ui->prefPartialLoading->onSave(); ui->prefLicenseType->onSave(); @@ -105,6 +113,8 @@ void DlgSettingsDocumentImp::loadSettings() ui->prefAddLogo->onRestore(); ui->prefSaveBackupFiles->onRestore(); ui->prefCountBackupFiles->onRestore(); + ui->prefSaveBackupExtension->onRestore(); + ui->prefSaveBackupDateFormat->onRestore(); ui->prefDuplicateLabel->onRestore(); ui->prefPartialLoading->onRestore(); ui->prefLicenseType->onRestore();