// 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 #include #include #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(); std::tm local_tm {}; #if defined(_WIN32) localtime_s(&local_tm, &s); // Windows #else localtime_r(&s, &local_tm); // POSIX #endif constexpr size_t bufferLength = 128; std::array buffer {}; if (size_t bytes = std::strftime(buffer.data(), bufferLength, saveBackupDateFormat.c_str(), &local_tm); bytes == 0) { // An error here is typically that we over-ran the maximum buffer length ( // which should be a *very* unusual condition). Base::Console().error("Failed to create valid backup file name from format string:\n"); Base::Console().error(saveBackupDateFormat.c_str()); const auto knownGoodFormat {"%Y-%m-%d_%H-%M-%S"}; std::strftime(buffer.data(), bufferLength, knownGoodFormat, &local_tm); } str << bn << buffer.data(); 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; }