From 7d37d8dfa9edf32dabf1c4be1521d6dea93d08c6 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Sat, 14 Jun 2025 16:10:24 -0500 Subject: [PATCH] App: Extract BackupPolicy into its own file No code changes except to add AppExport to BackupPolicy --- src/App/BackupPolicy.cpp | 340 +++++++++++++++++++++++++++++++++++++++ src/App/BackupPolicy.h | 68 ++++++++ src/App/CMakeLists.txt | 2 + src/App/Document.cpp | 315 +----------------------------------- 4 files changed, 411 insertions(+), 314 deletions(-) create mode 100644 src/App/BackupPolicy.cpp create mode 100644 src/App/BackupPolicy.h diff --git a/src/App/BackupPolicy.cpp b/src/App/BackupPolicy.cpp new file mode 100644 index 0000000000..ded6ca83f0 --- /dev/null +++ b/src/App/BackupPolicy.cpp @@ -0,0 +1,340 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +/**************************************************************************** + * Copyright (c) 2020 Werner Mayer * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + +#include "PreCompiled.h" + +#ifndef _PreComp_ +#include +#include +#include +#endif + +#include +#include +#include +#include +#include +#include +#include + +#include "BackupPolicy.h" + +using namespace App; + +void BackupPolicy::setPolicy(const Policy p) +{ + policy = p; +} +void BackupPolicy::setNumberOfFiles(const int count) +{ + numberOfFiles = count; +} +void BackupPolicy::useBackupExtension(const bool on) +{ + useFCBakExtension = on; +} +void BackupPolicy::setDateFormat(const std::string& fmt) +{ + saveBackupDateFormat = fmt; +} +void BackupPolicy::apply(const std::string& sourcename, const std::string& targetname) +{ + switch (policy) { + case Standard: + applyStandard(sourcename, targetname); + break; + case TimeStamp: + applyTimeStamp(sourcename, targetname); + break; + } +} + +void BackupPolicy::applyStandard(const std::string& sourcename, const std::string& targetname) const +{ + // if saving the project data succeeded rename to the actual file name + if (Base::FileInfo fi(targetname); fi.exists()) { + if (numberOfFiles > 0) { + int nSuff = 0; + std::string fn = fi.fileName(); + Base::FileInfo di(fi.dirPath()); + std::vector backup; + std::vector files = di.getDirectoryContent(); + for (const Base::FileInfo& it : files) { + if (std::string file = it.fileName(); file.substr(0, fn.length()) == fn) { + // starts with the same file name + std::string suf(file.substr(fn.length())); + if (!suf.empty()) { + std::string::size_type nPos = suf.find_first_not_of("0123456789"); + if (nPos == std::string::npos) { + // store all backup files + backup.push_back(it); + nSuff = + std::max(nSuff, static_cast(std::atol(suf.c_str()))); + } + } + } + } + + if (!backup.empty() && static_cast(backup.size()) >= numberOfFiles) { + // delete the oldest backup file we found + Base::FileInfo del = backup.front(); + for (const Base::FileInfo& it : backup) { + if (it.lastModified() < del.lastModified()) { + del = it; + } + } + + del.deleteFile(); + fn = del.filePath(); + } + else { + // create a new backup file + std::stringstream str; + str << fi.filePath() << (nSuff + 1); + fn = str.str(); + } + + if (!fi.renameFile(fn.c_str())) { + Base::Console().warning("Cannot rename project file to backup file\n"); + } + } + else { + fi.deleteFile(); + } + } + + if (Base::FileInfo tmp(sourcename); !tmp.renameFile(targetname.c_str())) { + throw Base::FileException("Cannot rename tmp save file to project file", + Base::FileInfo(targetname)); + } +} + +void BackupPolicy::applyTimeStamp(const std::string& sourcename, const std::string& 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()) { + if (numberOfFiles > 0) { + // replace . by - in format to avoid . between base name and extension + boost::replace_all(saveBackupDateFormat, ".", "-"); + { + // Remove all extra backups + std::string filename = fi.fileName(); + Base::FileInfo di(fi.dirPath()); + std::vector backup; + std::vector files = di.getDirectoryContent(); + for (const Base::FileInfo& it : files) { + 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(), + static_cast(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, filename) && (file.length() > filename.length()) + && checkDigits(file.substr(filename.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() && static_cast(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 (Base::FileInfo& it : backup) { + 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 ext2 = 1; + if (useFCBakExtension) { + std::stringstream str; + Base::TimeInfo ti = fi.lastModified(); + time_t s = ti.getTime_t(); + 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.empty()) || (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")) { + fn = fn + "-"; + } + else { + done = true; + } + } + + if (!done) { + while (ext2 < numberOfFiles + 10) { + if (renameFileNoErase(fi, fn + std::to_string(ext2) + ".FCBak")) { + break; + } + ext2++; + } + } + } + else { + // changed but simpler and solves also the delay sometimes introduced by + // google drive + while (ext2 < 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(ext2))) { + break; + } + ext2++; + } + } + + if (ext2 >= 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); + } + } + } + else { + try { + fi.deleteFile(); + } + catch (...) { + Base::Console().warning("Cannot remove backup file: %s\n", + fi.fileName().c_str()); + backupManagementError = true; + } + } + } + + Base::FileInfo tmp(sourcename); + if (!tmp.renameFile(targetname.c_str())) { + throw Base::FileException( + "Save interrupted: Cannot rename temporary file to project file", + tmp); + } + + if (backupManagementError) { + throw Base::FileException( + "Warning: Save complete, but error while managing backup history.", + fi); + } +} + +bool BackupPolicy::fileComparisonByDate(const Base::FileInfo& i, const Base::FileInfo& j) +{ + return (i.lastModified() > j.lastModified()); +} + +bool BackupPolicy::startsWith(const std::string& st1, const std::string& st2) const +{ + return st1.substr(0, st2.length()) == st2; +} + +bool BackupPolicy::checkValidString(const std::string& cmpl, const boost::regex& e) const +{ + boost::smatch what; + const bool res = boost::regex_search(cmpl, what, e); + return res; +} + +bool BackupPolicy::checkValidComplement(const std::string& file, + const std::string& pbn, + const std::string& ext) const +{ + const std::string cmpl = + file.substr(pbn.length(), file.length() - pbn.length() - ext.length() - 1); + const boost::regex e(R"(^[^.]*$)"); + return checkValidString(cmpl, e); +} + +bool BackupPolicy::checkDigits(const std::string& cmpl) const +{ + const boost::regex e(R"(^[0-9]*$)"); + return checkValidString(cmpl, e); +} + +bool BackupPolicy::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 + const Base::FileInfo nf(newName); + if (!nf.exists()) { + return fi.renameFile(newName.c_str()); + } + return false; +} diff --git a/src/App/BackupPolicy.h b/src/App/BackupPolicy.h new file mode 100644 index 0000000000..d403b88171 --- /dev/null +++ b/src/App/BackupPolicy.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +/**************************************************************************** + * Copyright (c) 2020 Werner Mayer * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + +#include "FCGlobal.h" + +#include +#include +#include +#include + +namespace App +{ +// Helper class to handle different backup policies: originally a private class in Document.cpp, +// and extracted for public access to enable direct testing since the logic involved is quite +// complex. +class AppExport BackupPolicy +{ +public: + enum Policy + { + Standard, + TimeStamp + }; + void setPolicy(const Policy p); + void setNumberOfFiles(const int count); + void useBackupExtension(const bool on); + void setDateFormat(const std::string& fmt); + void apply(const std::string& sourcename, const std::string& targetname); + +private: + void applyStandard(const std::string& sourcename, const std::string& targetname) const; + void applyTimeStamp(const std::string& sourcename, const std::string& targetname); + static bool fileComparisonByDate(const Base::FileInfo& i, const Base::FileInfo& j); + bool startsWith(const std::string& st1, const std::string& st2) const; + bool checkValidString(const std::string& cmpl, const boost::regex& e) const; + bool checkValidComplement(const std::string& file, + const std::string& pbn, + const std::string& ext) const; + bool checkDigits(const std::string& cmpl) const; + bool renameFileNoErase(Base::FileInfo fi, const std::string& newName); + +private: + Policy policy {Standard}; + int numberOfFiles {1}; + bool useFCBakExtension {true}; + std::string saveBackupDateFormat {"%Y%m%d-%H%M%S"}; +}; +} // namespace App \ No newline at end of file diff --git a/src/App/CMakeLists.txt b/src/App/CMakeLists.txt index 6230c42755..416740dcb3 100644 --- a/src/App/CMakeLists.txt +++ b/src/App/CMakeLists.txt @@ -133,6 +133,7 @@ SOURCE_GROUP("Pyi" FILES ${FreeCADApp_Pyi_SRCS}) # The document stuff SET(Document_CPP_SRCS Annotation.cpp + BackupPolicy.cpp Document.cpp DocumentObject.cpp Extension.cpp @@ -187,6 +188,7 @@ SET(Document_CPP_SRCS SET(Document_HPP_SRCS Annotation.h + BackupPolicy.h Document.h DocumentObject.h Extension.h diff --git a/src/App/Document.cpp b/src/App/Document.cpp index 57458dd9fd..d1cf886f92 100644 --- a/src/App/Document.cpp +++ b/src/App/Document.cpp @@ -70,6 +70,7 @@ #include "private/DocumentP.h" #include "Application.h" #include "AutoTransaction.h" +#include "BackupPolicy.h" #include "ExpressionParser.h" #include "GeoFeature.h" #include "License.h" @@ -1724,320 +1725,6 @@ bool Document::save() return false; } -namespace App -{ -// Helper class to handle different backup policies -class BackupPolicy -{ -public: - enum Policy - { - Standard, - TimeStamp - }; - BackupPolicy() - {} - ~BackupPolicy() = default; - void setPolicy(const Policy p) - { - policy = p; - } - void setNumberOfFiles(const int count) - { - numberOfFiles = count; - } - void useBackupExtension(const 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: - applyStandard(sourcename, targetname); - break; - case TimeStamp: - applyTimeStamp(sourcename, targetname); - break; - } - } - -private: - void applyStandard(const std::string& sourcename, const std::string& targetname) const - { - // if saving the project data succeeded rename to the actual file name - if (Base::FileInfo fi(targetname); fi.exists()) { - if (numberOfFiles > 0) { - int nSuff = 0; - std::string fn = fi.fileName(); - Base::FileInfo di(fi.dirPath()); - std::vector backup; - std::vector files = di.getDirectoryContent(); - for (const Base::FileInfo& it : files) { - if (std::string file = it.fileName(); file.substr(0, fn.length()) == fn) { - // starts with the same file name - std::string suf(file.substr(fn.length())); - if (!suf.empty()) { - std::string::size_type nPos = suf.find_first_not_of("0123456789"); - if (nPos == std::string::npos) { - // store all backup files - backup.push_back(it); - nSuff = - std::max(nSuff, static_cast(std::atol(suf.c_str()))); - } - } - } - } - - if (!backup.empty() && static_cast(backup.size()) >= numberOfFiles) { - // delete the oldest backup file we found - Base::FileInfo del = backup.front(); - for (const Base::FileInfo& it : backup) { - if (it.lastModified() < del.lastModified()) { - del = it; - } - } - - del.deleteFile(); - fn = del.filePath(); - } - else { - // create a new backup file - std::stringstream str; - str << fi.filePath() << (nSuff + 1); - fn = str.str(); - } - - if (!fi.renameFile(fn.c_str())) { - Base::Console().warning("Cannot rename project file to backup file\n"); - } - } - else { - fi.deleteFile(); - } - } - - if (Base::FileInfo tmp(sourcename); !tmp.renameFile(targetname.c_str())) { - throw Base::FileException("Cannot rename tmp save file to project file", - Base::FileInfo(targetname)); - } - } - void applyTimeStamp(const std::string& sourcename, const std::string& 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()) { - if (numberOfFiles > 0) { - // replace . by - in format to avoid . between base name and extension - boost::replace_all(saveBackupDateFormat, ".", "-"); - { - // Remove all extra backups - std::string filename = fi.fileName(); - Base::FileInfo di(fi.dirPath()); - std::vector backup; - std::vector files = di.getDirectoryContent(); - for (const Base::FileInfo& it : files) { - 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(), - static_cast(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, filename) && (file.length() > filename.length()) - && checkDigits(file.substr(filename.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() && static_cast(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 (Base::FileInfo& it : backup) { - 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 ext2 = 1; - if (useFCBakExtension) { - std::stringstream str; - Base::TimeInfo ti = fi.lastModified(); - time_t s = ti.getTime_t(); - 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.empty()) || (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")) { - fn = fn + "-"; - } - else { - done = true; - } - } - - if (!done) { - while (ext2 < numberOfFiles + 10) { - if (renameFileNoErase(fi, fn + std::to_string(ext2) + ".FCBak")) { - break; - } - ext2++; - } - } - } - else { - // changed but simpler and solves also the delay sometimes introduced by - // google drive - while (ext2 < 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(ext2))) { - break; - } - ext2++; - } - } - - if (ext2 >= 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); - } - } - } - else { - try { - fi.deleteFile(); - } - catch (...) { - Base::Console().warning("Cannot remove backup file: %s\n", - fi.fileName().c_str()); - backupManagementError = true; - } - } - } - - Base::FileInfo tmp(sourcename); - if (!tmp.renameFile(targetname.c_str())) { - throw Base::FileException( - "Save interrupted: Cannot rename temporary file to project file", - tmp); - } - - 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; - const 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 - { - const std::string cmpl = - file.substr(pbn.length(), file.length() - pbn.length() - ext.length() - 1); - const boost::regex e(R"(^[^.]*$)"); - return checkValidString(cmpl, e); - } - bool checkDigits(const std::string& cmpl) const - { - 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 - const Base::FileInfo nf(newName); - if (!nf.exists()) { - return fi.renameFile(newName.c_str()); - } - return false; - } - -private: - Policy policy {Standard}; - int numberOfFiles {1}; - bool useFCBakExtension {true}; - std::string saveBackupDateFormat {"%Y%m%d-%H%M%S"}; -}; -} // namespace App - bool Document::saveToFile(const char* filename) const { signalStartSave(*this, filename);