// ************************************************************************** // * This file is part of the FreeFileSync project. It is distributed under * // * GNU General Public License: http://www.gnu.org/licenses/gpl.html * // * Copyright (C) Zenju (zenju AT gmx DOT de) - All Rights Reserved * // ************************************************************************** #include "algorithm.h" #include #include #include #include #include #include #include #include "lib/resources.h" #include "lib/norm_filter.h" #include "lib/db_file.h" #include "lib/cmp_filetime.h" #include "lib/norm_filter.h" #include "process_callback.h" //for UI_UPDATE_INTERVAL using namespace zen; using namespace std::rel_ops; void zen::swapGrids(const MainConfiguration& config, FolderComparison& folderCmp) { std::for_each(begin(folderCmp), end(folderCmp), [](BaseDirPair& baseObj) { baseObj.flip(); }); redetermineSyncDirection(config, folderCmp, [](const std::wstring&) {}); } //---------------------------------------------------------------------------------------------- namespace { class Redetermine { public: static void execute(const DirectionSet& dirCfgIn, HierarchyObject& hierObj) { Redetermine(dirCfgIn).recurse(hierObj); } private: Redetermine(const DirectionSet& dirCfgIn) : dirCfg(dirCfgIn) {} void recurse(HierarchyObject& hierObj) const { for (FilePair& fileObj : hierObj.refSubFiles()) processFile(fileObj); for (SymlinkPair& linkObj : hierObj.refSubLinks()) processLink(linkObj); for (DirPair& dirObj : hierObj.refSubDirs()) processDir(dirObj); } void processFile(FilePair& fileObj) const { const CompareFilesResult cat = fileObj.getCategory(); //##################### schedule old temporary files for deletion #################### if (cat == FILE_LEFT_SIDE_ONLY && endsWith(fileObj.getShortName(), TEMP_FILE_ENDING)) return fileObj.setSyncDir(SyncDirection::LEFT); else if (cat == FILE_RIGHT_SIDE_ONLY && endsWith(fileObj.getShortName(), TEMP_FILE_ENDING)) return fileObj.setSyncDir(SyncDirection::RIGHT); //#################################################################################### switch (cat) { case FILE_LEFT_SIDE_ONLY: fileObj.setSyncDir(dirCfg.exLeftSideOnly); break; case FILE_RIGHT_SIDE_ONLY: fileObj.setSyncDir(dirCfg.exRightSideOnly); break; case FILE_RIGHT_NEWER: fileObj.setSyncDir(dirCfg.rightNewer); break; case FILE_LEFT_NEWER: fileObj.setSyncDir(dirCfg.leftNewer); break; case FILE_DIFFERENT: fileObj.setSyncDir(dirCfg.different); break; case FILE_CONFLICT: case FILE_DIFFERENT_METADATA: //use setting from "conflict/cannot categorize" if (dirCfg.conflict == SyncDirection::NONE) fileObj.setSyncDirConflict(fileObj.getCatExtraDescription()); //take over category conflict else fileObj.setSyncDir(dirCfg.conflict); break; case FILE_EQUAL: fileObj.setSyncDir(SyncDirection::NONE); break; } } void processLink(SymlinkPair& linkObj) const { switch (linkObj.getLinkCategory()) { case SYMLINK_LEFT_SIDE_ONLY: linkObj.setSyncDir(dirCfg.exLeftSideOnly); break; case SYMLINK_RIGHT_SIDE_ONLY: linkObj.setSyncDir(dirCfg.exRightSideOnly); break; case SYMLINK_LEFT_NEWER: linkObj.setSyncDir(dirCfg.leftNewer); break; case SYMLINK_RIGHT_NEWER: linkObj.setSyncDir(dirCfg.rightNewer); break; case SYMLINK_CONFLICT: case SYMLINK_DIFFERENT_METADATA: //use setting from "conflict/cannot categorize" if (dirCfg.conflict == SyncDirection::NONE) linkObj.setSyncDirConflict(linkObj.getCatExtraDescription()); //take over category conflict else linkObj.setSyncDir(dirCfg.conflict); break; case SYMLINK_DIFFERENT: linkObj.setSyncDir(dirCfg.different); break; case SYMLINK_EQUAL: linkObj.setSyncDir(SyncDirection::NONE); break; } } void processDir(DirPair& dirObj) const { const CompareDirResult cat = dirObj.getDirCategory(); //########### schedule abandoned temporary recycle bin directory for deletion ########## if (cat == DIR_LEFT_SIDE_ONLY && endsWith(dirObj.getShortName(), TEMP_FILE_ENDING)) return setSyncDirectionRec(SyncDirection::LEFT, dirObj); // else if (cat == DIR_RIGHT_SIDE_ONLY && endsWith(dirObj.getShortName(), TEMP_FILE_ENDING)) return setSyncDirectionRec(SyncDirection::RIGHT, dirObj); //don't recurse below! //####################################################################################### switch (cat) { case DIR_LEFT_SIDE_ONLY: dirObj.setSyncDir(dirCfg.exLeftSideOnly); break; case DIR_RIGHT_SIDE_ONLY: dirObj.setSyncDir(dirCfg.exRightSideOnly); break; case DIR_EQUAL: dirObj.setSyncDir(SyncDirection::NONE); break; case DIR_DIFFERENT_METADATA: //use setting from "conflict/cannot categorize" if (dirCfg.conflict == SyncDirection::NONE) dirObj.setSyncDirConflict(dirObj.getCatExtraDescription()); //take over category conflict else dirObj.setSyncDir(dirCfg.conflict); break; } recurse(dirObj); } const DirectionSet dirCfg; }; //--------------------------------------------------------------------------------------------------------------- //test if non-equal items exist in scanned data bool allItemsCategoryEqual(const HierarchyObject& hierObj) { return std::all_of(hierObj.refSubFiles().begin(), hierObj.refSubFiles().end(), [](const FilePair& fileObj) { return fileObj.getCategory() == FILE_EQUAL; })&& //files std::all_of(hierObj.refSubLinks().begin(), hierObj.refSubLinks().end(), [](const SymlinkPair& linkObj) { return linkObj.getLinkCategory() == SYMLINK_EQUAL; })&& //symlinks std::all_of(hierObj.refSubDirs(). begin(), hierObj.refSubDirs().end(), [](const DirPair& dirObj) { return dirObj.getDirCategory() == DIR_EQUAL && allItemsCategoryEqual(dirObj); //short circuit-behavior! }); //directories } } bool zen::allElementsEqual(const FolderComparison& folderCmp) { return std::all_of(begin(folderCmp), end(folderCmp), [](const BaseDirPair& baseObj) { return allItemsCategoryEqual(baseObj); }); } //--------------------------------------------------------------------------------------------------------------- namespace { template inline const InSyncDescrFile& getDescriptor(const InSyncFile& dbFile) { return dbFile.left; } template <> inline const InSyncDescrFile& getDescriptor(const InSyncFile& dbFile) { return dbFile.right; } //check whether database entry and current item match: *irrespective* of current comparison settings template inline bool isEqual(const FilePair& fileObj, const InSyncDir::FileList::value_type* dbFile) { if (fileObj.isEmpty()) return !dbFile; else if (!dbFile) return false; const Zstring& shortNameDb = dbFile->first; const InSyncDescrFile& descrDb = getDescriptor(dbFile->second); return fileObj.getShortName() == shortNameDb && //detect changes in case (windows) //respect 2 second FAT/FAT32 precision! copying a file to a FAT32 drive changes it's modification date by up to 2 seconds sameFileTime(fileObj.getLastWriteTime(), descrDb.lastWriteTimeRaw, 2) && fileObj.getFileSize() == dbFile->second.fileSize; //note: we do *not* consider FileId here, but are only interested in *visual* changes. Consider user moving data to some other medium, this is not a change! } //check whether database entry is in sync considering *current* comparison settings inline bool stillInSync(const InSyncFile& dbFile, CompareVariant compareVar, size_t fileTimeTolerance) { switch (compareVar) { case CMP_BY_TIME_SIZE: if (dbFile.cmpVar == CMP_BY_CONTENT) return true; //special rule: this is already "good enough" for CMP_BY_TIME_SIZE! return //case-sensitive short name match is a database invariant! CmpFileTime::getResult(dbFile.left.lastWriteTimeRaw, dbFile.right.lastWriteTimeRaw, fileTimeTolerance) == CmpFileTime::TIME_EQUAL; //dbFile.left.fileSize == dbFile.right.fileSize; case CMP_BY_CONTENT: //case-sensitive short name match is a database invariant! return dbFile.cmpVar == CMP_BY_CONTENT; //in contrast to comparison, we don't care about modification time here! } assert(false); return false; } //-------------------------------------------------------------------- template inline const InSyncDescrLink& getDescriptor(const InSyncSymlink& dbLink) { return dbLink.left; } template <> inline const InSyncDescrLink& getDescriptor(const InSyncSymlink& dbLink) { return dbLink.right; } //check whether database entry and current item match: *irrespective* of current comparison settings template inline bool isEqual(const SymlinkPair& linkObj, const InSyncDir::LinkList::value_type* dbLink) { if (linkObj.isEmpty()) return !dbLink; else if (!dbLink) return false; const Zstring& shortNameDb = dbLink->first; const InSyncDescrLink& descrDb = getDescriptor(dbLink->second); return linkObj.getShortName() == shortNameDb && //respect 2 second FAT/FAT32 precision! copying a file to a FAT32 drive changes its modification date by up to 2 seconds sameFileTime(linkObj.getLastWriteTime(), descrDb.lastWriteTimeRaw, 2); } //check whether database entry is in sync considering *current* comparison settings inline bool stillInSync(const InSyncSymlink& dbLink, CompareVariant compareVar, size_t fileTimeTolerance) { switch (compareVar) { case CMP_BY_TIME_SIZE: if (dbLink.cmpVar == CMP_BY_CONTENT) return true; //special rule: this is already "good enough" for CMP_BY_TIME_SIZE! return //case-sensitive short name match is a database invariant! CmpFileTime::getResult(dbLink.left.lastWriteTimeRaw, dbLink.right.lastWriteTimeRaw, fileTimeTolerance) == CmpFileTime::TIME_EQUAL; case CMP_BY_CONTENT: //case-sensitive short name match is a database invariant! return dbLink.cmpVar == CMP_BY_CONTENT; //in contrast to comparison, we don't care about modification time here! } assert(false); return false; } //-------------------------------------------------------------------- //check whether database entry and current item match: *irrespective* of current comparison settings template inline bool isEqual(const DirPair& dirObj, const InSyncDir::DirList::value_type* dbDir) { if (dirObj.isEmpty()) return !dbDir || dbDir->second.status == InSyncDir::DIR_STATUS_STRAW_MAN; else if (!dbDir || dbDir->second.status == InSyncDir::DIR_STATUS_STRAW_MAN) return false; const Zstring& shortNameDb = dbDir->first; return dirObj.getShortName() == shortNameDb; } inline bool stillInSync(const InSyncDir& dbDir) { //case-sensitive short name match is a database invariant! //InSyncDir::DIR_STATUS_STRAW_MAN considered return true; } //---------------------------------------------------------------------------------------------- class DetectMovedFiles { public: static void execute(BaseDirPair& baseDirectory, const InSyncDir& dbContainer) { DetectMovedFiles(baseDirectory, dbContainer); } private: DetectMovedFiles(BaseDirPair& baseDirectory, const InSyncDir& dbContainer) : cmpVar(baseDirectory.getCompVariant()), fileTimeTolerance(baseDirectory.getFileTimeTolerance()) { recurse(baseDirectory); if (!exLeftOnly.empty() && !exRightOnly.empty()) detectFilePairs(dbContainer); } void recurse(HierarchyObject& hierObj) { for (FilePair& fileObj : hierObj.refSubFiles()) { const CompareFilesResult cat = fileObj.getCategory(); if (cat == FILE_LEFT_SIDE_ONLY) { if (fileObj.getFileId() != FileId()) { auto rv = exLeftOnly.insert(std::make_pair(fileObj.getFileId(), &fileObj)); assert(rv.second); if (!rv.second) //duplicate file ID! rv.first->second = nullptr; } } else if (cat == FILE_RIGHT_SIDE_ONLY) { if (fileObj.getFileId() != FileId()) { auto rv = exRightOnly.insert(std::make_pair(fileObj.getFileId(), &fileObj)); assert(rv.second); if (!rv.second) //duplicate file ID! rv.first->second = nullptr; } } } for (DirPair& dirObj : hierObj.refSubDirs()) recurse(dirObj); } void detectFilePairs(const InSyncDir& container) const { for (auto& dbFile : container.files) findAndSetMovePair(dbFile.second); for (auto& dbDir : container.dirs) detectFilePairs(dbDir.second); } static bool sameSizeAndDateLeft(const FilePair& fsObj, const InSyncFile& dbEntry) { return fsObj.getFileSize() == dbEntry.fileSize && sameFileTime(fsObj.getLastWriteTime(), dbEntry.left.lastWriteTimeRaw, 2); //respect 2 second FAT/FAT32 precision! //PS: *never* allow 2 sec tolerance as container predicate!! // => no strict weak ordering relation! reason: no transitivity of equivalence! } static bool sameSizeAndDateRight(const FilePair& fsObj, const InSyncFile& dbEntry) { return fsObj.getFileSize() == dbEntry.fileSize && sameFileTime(fsObj.getLastWriteTime(), dbEntry.right.lastWriteTimeRaw, 2); } void findAndSetMovePair(const InSyncFile& dbEntry) const { const FileId idLeft = dbEntry.left .fileId; const FileId idRight = dbEntry.right.fileId; if (idLeft != FileId() && idRight != FileId() && stillInSync(dbEntry, cmpVar, fileTimeTolerance)) { auto itL = exLeftOnly.find(idLeft); if (itL != exLeftOnly.end()) if (FilePair* fileLeftOnly = itL->second) //= nullptr, if duplicate ID! if (sameSizeAndDateLeft(*fileLeftOnly, dbEntry)) { auto itR = exRightOnly.find(idRight); if (itR != exRightOnly.end()) if (FilePair* fileRightOnly = itR->second) //= nullptr, if duplicate ID! if (sameSizeAndDateRight(*fileRightOnly, dbEntry)) if (fileLeftOnly ->getMoveRef() == nullptr && //the db may contain duplicate file ids on left or right side: e.g. consider aliasing through symlinks fileRightOnly->getMoveRef() == nullptr) //=> should not be a problem (same id, size, date => alias!) but don't let a row participate in two move pairs! { fileLeftOnly ->setMoveRef(fileRightOnly->getId()); //found a pair, mark it! fileRightOnly->setMoveRef(fileLeftOnly ->getId()); // } } } } const CompareVariant cmpVar; const size_t fileTimeTolerance; std::map exLeftOnly; //FilePair* == nullptr for duplicate ids! => consider aliasing through symlinks! std::map exRightOnly; //=> avoid ambiguity for mixtures of files/symlinks on one side and allow 1-1 mapping only! /* detect renamed files: X -> |_| Create right |_| -> Y Delete right is detected as: Rename Y to X on right Algorithm: ---------- DB-file left <--- (name, size, date) ---> DB-file right | | | (file ID, size, date) | (file ID, size, date) \|/ \|/ file left only file right only FAT caveat: File Ids are generally not stable when file is either moved or renamed! => 1. Move/rename operations on FAT cannot be detected reliably. => 2. database generally contains wrong file ID on FAT after renaming from .ffs_tmp files => correct file Ids in database only after next sync => 3. even exFAT screws up (but less than FAT) and changes IDs after file move. Did they learn nothing from the past? Possible refinement ------------------- If the file ID is wrong (FAT) or not available, we could at least allow direct association by name, instead of breaking the chain completely: support NTFS -> FAT */ }; //---------------------------------------------------------------------------------------------- class RedetermineTwoWay { public: static void execute(BaseDirPair& baseDirectory, const InSyncDir& dbContainer) { RedetermineTwoWay(baseDirectory, dbContainer); } private: RedetermineTwoWay(BaseDirPair& baseDirectory, const InSyncDir& dbContainer) : txtBothSidesChanged(_("Both sides have changed since last synchronization.")), txtNoSideChanged(_("Cannot determine sync-direction:") + L" \n" + _("No change since last synchronization.")), txtDbNotInSync(_("Cannot determine sync-direction:") + L" \n" + _("The database entry is not in sync considering current settings.")), cmpVar(baseDirectory.getCompVariant()), fileTimeTolerance(baseDirectory.getFileTimeTolerance()) { //-> considering filter not relevant: //if narrowing filter: all ok; if widening filter (if file ex on both sides -> conflict, fine; if file ex. on one side: copy to other side: fine) recurse(baseDirectory, &dbContainer); } void recurse(HierarchyObject& hierObj, const InSyncDir* dbContainer) const { for (FilePair& fileObj : hierObj.refSubFiles()) processFile(fileObj, dbContainer); for (SymlinkPair& linkObj : hierObj.refSubLinks()) processSymlink(linkObj, dbContainer); for (DirPair& dirObj : hierObj.refSubDirs()) processDir(dirObj, dbContainer); } void processFile(FilePair& fileObj, const InSyncDir* dbContainer) const { const CompareFilesResult cat = fileObj.getCategory(); if (cat == FILE_EQUAL) return; //##################### schedule old temporary files for deletion #################### if (cat == FILE_LEFT_SIDE_ONLY && endsWith(fileObj.getShortName(), TEMP_FILE_ENDING)) return fileObj.setSyncDir(SyncDirection::LEFT); else if (cat == FILE_RIGHT_SIDE_ONLY && endsWith(fileObj.getShortName(), TEMP_FILE_ENDING)) return fileObj.setSyncDir(SyncDirection::RIGHT); //#################################################################################### //try to find corresponding database entry const InSyncDir::FileList::value_type* dbEntry = nullptr; if (dbContainer) { auto it = dbContainer->files.find(fileObj.getObjShortName()); if (it != dbContainer->files.end()) dbEntry = &*it; } //evaluation const bool changeOnLeft = !isEqual(fileObj, dbEntry); const bool changeOnRight = !isEqual(fileObj, dbEntry); if (changeOnLeft != changeOnRight) { //if database entry not in sync according to current settings! -> do not set direction based on async status! if (dbEntry && !stillInSync(dbEntry->second, cmpVar, fileTimeTolerance)) fileObj.setSyncDirConflict(txtDbNotInSync); else fileObj.setSyncDir(changeOnLeft ? SyncDirection::RIGHT : SyncDirection::LEFT); } else { if (changeOnLeft) fileObj.setSyncDirConflict(txtBothSidesChanged); else fileObj.setSyncDirConflict(txtNoSideChanged); } } void processSymlink(SymlinkPair& linkObj, const InSyncDir* dbContainer) const { const CompareSymlinkResult cat = linkObj.getLinkCategory(); if (cat == SYMLINK_EQUAL) return; //try to find corresponding database entry const InSyncDir::LinkList::value_type* dbEntry = nullptr; if (dbContainer) { auto it = dbContainer->symlinks.find(linkObj.getObjShortName()); if (it != dbContainer->symlinks.end()) dbEntry = &*it; } //evaluation const bool changeOnLeft = !isEqual(linkObj, dbEntry); const bool changeOnRight = !isEqual(linkObj, dbEntry); if (changeOnLeft != changeOnRight) { //if database entry not in sync according to current settings! -> do not set direction based on async status! if (dbEntry && !stillInSync(dbEntry->second, cmpVar, fileTimeTolerance)) linkObj.setSyncDirConflict(txtDbNotInSync); else linkObj.setSyncDir(changeOnLeft ? SyncDirection::RIGHT : SyncDirection::LEFT); } else { if (changeOnLeft) linkObj.setSyncDirConflict(txtBothSidesChanged); else linkObj.setSyncDirConflict(txtNoSideChanged); } } void processDir(DirPair& dirObj, const InSyncDir* dbContainer) const { const CompareDirResult cat = dirObj.getDirCategory(); //########### schedule abandoned temporary recycle bin directory for deletion ########## if (cat == DIR_LEFT_SIDE_ONLY && endsWith(dirObj.getShortName(), TEMP_FILE_ENDING)) return setSyncDirectionRec(SyncDirection::LEFT, dirObj); // else if (cat == DIR_RIGHT_SIDE_ONLY && endsWith(dirObj.getShortName(), TEMP_FILE_ENDING)) return setSyncDirectionRec(SyncDirection::RIGHT, dirObj); //don't recurse below! //####################################################################################### //try to find corresponding database entry const InSyncDir::DirList::value_type* dbEntry = nullptr; if (dbContainer) { auto it = dbContainer->dirs.find(dirObj.getObjShortName()); if (it != dbContainer->dirs.end()) dbEntry = &*it; } if (cat != DIR_EQUAL) { //evaluation const bool changeOnLeft = !isEqual(dirObj, dbEntry); const bool changeOnRight = !isEqual(dirObj, dbEntry); if (changeOnLeft != changeOnRight) { //if database entry not in sync according to current settings! -> do not set direction based on async status! if (dbEntry && !stillInSync(dbEntry->second)) dirObj.setSyncDirConflict(txtDbNotInSync); else dirObj.setSyncDir(changeOnLeft ? SyncDirection::RIGHT : SyncDirection::LEFT); } else { if (changeOnLeft) dirObj.setSyncDirConflict(txtBothSidesChanged); else dirObj.setSyncDirConflict(txtNoSideChanged); } } recurse(dirObj, dbEntry ? &dbEntry->second : nullptr); } const std::wstring txtBothSidesChanged; const std::wstring txtNoSideChanged; const std::wstring txtDbNotInSync; const CompareVariant cmpVar; const size_t fileTimeTolerance; }; } //--------------------------------------------------------------------------------------------------------------- std::vector zen::extractDirectionCfg(const MainConfiguration& mainCfg) { //merge first and additional pairs std::vector allPairs; allPairs.push_back(mainCfg.firstPair); allPairs.insert(allPairs.end(), mainCfg.additionalPairs.begin(), //add additional pairs mainCfg.additionalPairs.end()); std::vector output; std::for_each(allPairs.begin(), allPairs.end(), [&](const FolderPairEnh& fp) { output.push_back(fp.altSyncConfig.get() ? fp.altSyncConfig->directionCfg : mainCfg.syncCfg.directionCfg); }); return output; } void zen::redetermineSyncDirection(const DirectionConfig& dirCfg, BaseDirPair& baseDirectory, std::function reportWarning) { //try to load sync-database files std::shared_ptr lastSyncState; if (dirCfg.var == DirectionConfig::TWOWAY || detectMovedFilesEnabled(dirCfg)) try { if (allItemsCategoryEqual(baseDirectory)) return; //nothing to do: abort and don't even try to open db files lastSyncState = loadLastSynchronousState(baseDirectory); //throw FileError, FileErrorDatabaseNotExisting } catch (FileErrorDatabaseNotExisting&) {} //let's ignore this error, there's no value in reporting it other than confuse users catch (FileError& error) //e.g. incompatible database version { reportWarning(error.toString() + (dirCfg.var == DirectionConfig::TWOWAY ? L" \n\n" + _("Setting default synchronization directions: Old files will be overwritten with newer files.") : std::wstring())); } //set sync directions if (dirCfg.var == DirectionConfig::TWOWAY) { if (lastSyncState) RedetermineTwoWay::execute(baseDirectory, *lastSyncState); else //default fallback Redetermine::execute(getTwoWayUpdateSet(), baseDirectory); } else Redetermine::execute(extractDirections(dirCfg), baseDirectory); //detect renamed files if (lastSyncState) DetectMovedFiles::execute(baseDirectory, *lastSyncState); } void zen::redetermineSyncDirection(const MainConfiguration& mainCfg, FolderComparison& folderCmp, std::function reportWarning) { if (folderCmp.empty()) return; std::vector directCfgs = extractDirectionCfg(mainCfg); if (folderCmp.size() != directCfgs.size()) throw std::logic_error("Programming Error: Contract violation! " + std::string(__FILE__) + ":" + numberTo(__LINE__)); for (auto it = folderCmp.begin(); it != folderCmp.end(); ++it) { const DirectionConfig& cfg = directCfgs[it - folderCmp.begin()]; redetermineSyncDirection(cfg, **it, reportWarning); } } //--------------------------------------------------------------------------------------------------------------- struct SetNewDirection { static void execute(FilePair& fileObj, SyncDirection newDirection) { if (fileObj.getCategory() != FILE_EQUAL) fileObj.setSyncDir(newDirection); } static void execute(SymlinkPair& linkObj, SyncDirection newDirection) { if (linkObj.getLinkCategory() != SYMLINK_EQUAL) linkObj.setSyncDir(newDirection); } static void execute(DirPair& dirObj, SyncDirection newDirection) { if (dirObj.getDirCategory() != DIR_EQUAL) dirObj.setSyncDir(newDirection); //recurse: for (FilePair& fileObj : dirObj.refSubFiles()) execute(fileObj, newDirection); for (SymlinkPair& linkObj : dirObj.refSubLinks()) execute(linkObj, newDirection); for (DirPair& dirObj2 : dirObj.refSubDirs()) execute(dirObj2, newDirection); } }; void zen::setSyncDirectionRec(SyncDirection newDirection, FileSystemObject& fsObj) { //process subdirectories also! struct Recurse: public FSObjectVisitor { Recurse(SyncDirection newDir) : newDir_(newDir) {} virtual void visit(const FilePair& fileObj) { SetNewDirection::execute(const_cast(fileObj), newDir_); //phyiscal object is not const in this method anyway } virtual void visit(const SymlinkPair& linkObj) { SetNewDirection::execute(const_cast(linkObj), newDir_); // } virtual void visit(const DirPair& dirObj) { SetNewDirection::execute(const_cast(dirObj), newDir_); // } private: SyncDirection newDir_; } setDirVisitor(newDirection); fsObj.accept(setDirVisitor); } //--------------- functions related to filtering ------------------------------------------------------------------------------------ namespace { template void inOrExcludeAllRows(zen::HierarchyObject& hierObj) { for (FilePair& fileObj : hierObj.refSubFiles()) fileObj.setActive(include); for (SymlinkPair& linkObj : hierObj.refSubLinks()) linkObj.setActive(include); for (DirPair& dirObj : hierObj.refSubDirs()) { dirObj.setActive(include); inOrExcludeAllRows(dirObj); //recurse } } } void zen::setActiveStatus(bool newStatus, zen::FolderComparison& folderCmp) { if (newStatus) std::for_each(begin(folderCmp), end(folderCmp), [](BaseDirPair& baseDirObj) { inOrExcludeAllRows(baseDirObj); }); //include all rows else std::for_each(begin(folderCmp), end(folderCmp), [](BaseDirPair& baseDirObj) { inOrExcludeAllRows(baseDirObj); }); //exclude all rows } void zen::setActiveStatus(bool newStatus, zen::FileSystemObject& fsObj) { fsObj.setActive(newStatus); //process subdirectories also! struct Recurse: public FSObjectVisitor { Recurse(bool newStat) : newStatus_(newStat) {} virtual void visit(const FilePair& fileObj) {} virtual void visit(const SymlinkPair& linkObj) {} virtual void visit(const DirPair& dirObj) { if (newStatus_) inOrExcludeAllRows(const_cast(dirObj)); //object is not physically const here anyway else inOrExcludeAllRows(const_cast(dirObj)); // } private: const bool newStatus_; } recurse(newStatus); fsObj.accept(recurse); } namespace { enum FilterStrategy { STRATEGY_SET, STRATEGY_AND, STRATEGY_OR }; template struct Eval; template <> struct Eval //process all elements { template bool process(const T& obj) const { return true; } }; template <> struct Eval { template bool process(const T& obj) const { return obj.isActive(); } }; template <> struct Eval { template bool process(const T& obj) const { return !obj.isActive(); } }; template class ApplyHardFilter { public: static void execute(HierarchyObject& hierObj, const HardFilter& filterProcIn) { ApplyHardFilter(hierObj, filterProcIn); } private: ApplyHardFilter(HierarchyObject& hierObj, const HardFilter& filterProcIn) : filterProc(filterProcIn) { recurse(hierObj); } void recurse(HierarchyObject& hierObj) const { for (FilePair& fileObj : hierObj.refSubFiles()) processFile(fileObj); for (SymlinkPair& linkObj : hierObj.refSubLinks()) processLink(linkObj); for (DirPair& dirObj : hierObj.refSubDirs()) processDir(dirObj); }; void processFile(FilePair& fileObj) const { if (Eval().process(fileObj)) fileObj.setActive(filterProc.passFileFilter(fileObj.getObjRelativeName())); } void processLink(SymlinkPair& linkObj) const { if (Eval().process(linkObj)) linkObj.setActive(filterProc.passFileFilter(linkObj.getObjRelativeName())); } void processDir(DirPair& dirObj) const { bool subObjMightMatch = true; const bool filterPassed = filterProc.passDirFilter(dirObj.getObjRelativeName(), &subObjMightMatch); if (Eval().process(dirObj)) dirObj.setActive(filterPassed); if (!subObjMightMatch) //use same logic like directory traversing here: evaluate filter in subdirs only if objects could match { inOrExcludeAllRows(dirObj); //exclude all files dirs in subfolders return; } recurse(dirObj); } const HardFilter& filterProc; }; template <> class ApplyHardFilter; //usage of InOrExcludeAllRows doesn't allow for strategy "or" template class ApplySoftFilter //falsify only! -> can run directly after "hard/base filter" { public: static void execute(HierarchyObject& hierObj, const SoftFilter& timeSizeFilter) { ApplySoftFilter(hierObj, timeSizeFilter); } private: ApplySoftFilter(HierarchyObject& hierObj, const SoftFilter& timeSizeFilter) : timeSizeFilter_(timeSizeFilter) { recurse(hierObj); } void recurse(zen::HierarchyObject& hierObj) const { for (FilePair& fileObj : hierObj.refSubFiles()) processFile(fileObj); for (SymlinkPair& linkObj : hierObj.refSubLinks()) processLink(linkObj); for (DirPair& dirObj : hierObj.refSubDirs()) processDir(dirObj); }; void processFile(FilePair& fileObj) const { if (Eval().process(fileObj)) { if (fileObj.isEmpty()) fileObj.setActive(matchSize(fileObj) && matchTime(fileObj)); else if (fileObj.isEmpty()) fileObj.setActive(matchSize(fileObj) && matchTime(fileObj)); else { //the only case with partially unclear semantics: //file and time filters may match or not match on each side, leaving a total of 16 combinations for both sides! /* ST S T - ST := match size and time --------- S := match size only ST |X|X|X|X| T := match time only ------------ - := no match S |X|O|?|O| ------------ X := include row T |X|?|O|O| O := exclude row ------------ ? := unclear - |X|O|O|O| ------------ */ //let's set ? := O fileObj.setActive((matchSize(fileObj) && matchTime(fileObj)) || (matchSize(fileObj) && matchTime(fileObj))); } } } void processLink(SymlinkPair& linkObj) const { if (Eval().process(linkObj)) { if (linkObj.isEmpty()) linkObj.setActive(matchTime(linkObj)); else if (linkObj.isEmpty()) linkObj.setActive(matchTime(linkObj)); else linkObj.setActive(matchTime(linkObj) || matchTime (linkObj)); } } void processDir(DirPair& dirObj) const { if (Eval().process(dirObj)) dirObj.setActive(timeSizeFilter_.matchFolder()); //if date filter is active we deactivate all folders: effectively gets rid of empty folders! recurse(dirObj); } template bool matchTime(const T& obj) const { return timeSizeFilter_.matchTime(obj.template getLastWriteTime()); } template bool matchSize(const T& obj) const { return timeSizeFilter_.matchSize(obj.template getFileSize()); } const SoftFilter timeSizeFilter_; }; } void zen::addHardFiltering(BaseDirPair& baseDirObj, const Zstring& excludeFilter) { ApplyHardFilter::execute(baseDirObj, NameFilter(FilterConfig().includeFilter, excludeFilter)); } void zen::addSoftFiltering(BaseDirPair& baseDirObj, const SoftFilter& timeSizeFilter) { if (!timeSizeFilter.isNull()) //since we use STRATEGY_AND, we may skip a "null" filter ApplySoftFilter::execute(baseDirObj, timeSizeFilter); } void zen::applyFiltering(FolderComparison& folderCmp, const MainConfiguration& mainCfg) { if (folderCmp.empty()) return; else if (folderCmp.size() != mainCfg.additionalPairs.size() + 1) throw std::logic_error("Programming Error: Contract violation! " + std::string(__FILE__) + ":" + numberTo(__LINE__)); //merge first and additional pairs std::vector allPairs; allPairs.push_back(mainCfg.firstPair); allPairs.insert(allPairs.end(), mainCfg.additionalPairs.begin(), //add additional pairs mainCfg.additionalPairs.end()); for (auto it = allPairs.begin(); it != allPairs.end(); ++it) { BaseDirPair& baseDirectory = *folderCmp[it - allPairs.begin()]; const NormalizedFilter normFilter = normalizeFilters(mainCfg.globalFilter, it->localFilter); //"set" hard filter ApplyHardFilter::execute(baseDirectory, *normFilter.nameFilter); //"and" soft filter addSoftFiltering(baseDirectory, normFilter.timeSizeFilter); } } class FilterByTimeSpan { public: static void execute(HierarchyObject& hierObj, const Int64& timeFrom, const Int64& timeTo) { FilterByTimeSpan(hierObj, timeFrom, timeTo); } private: FilterByTimeSpan(HierarchyObject& hierObj, const Int64& timeFrom, const Int64& timeTo) : timeFrom_(timeFrom), timeTo_(timeTo) { recurse(hierObj); } void recurse(HierarchyObject& hierObj) const { for (FilePair& fileObj : hierObj.refSubFiles()) processFile(fileObj); for (SymlinkPair& linkObj : hierObj.refSubLinks()) processLink(linkObj); for (DirPair& dirObj : hierObj.refSubDirs()) processDir(dirObj); }; void processFile(FilePair& fileObj) const { if (fileObj.isEmpty()) fileObj.setActive(matchTime(fileObj)); else if (fileObj.isEmpty()) fileObj.setActive(matchTime(fileObj)); else fileObj.setActive(matchTime(fileObj) || matchTime(fileObj)); } void processLink(SymlinkPair& linkObj) const { if (linkObj.isEmpty()) linkObj.setActive(matchTime(linkObj)); else if (linkObj.isEmpty()) linkObj.setActive(matchTime(linkObj)); else linkObj.setActive(matchTime(linkObj) || matchTime (linkObj)); } void processDir(DirPair& dirObj) const { dirObj.setActive(false); recurse(dirObj); } template bool matchTime(const T& obj) const { return timeFrom_ <= obj.template getLastWriteTime() && obj.template getLastWriteTime() <= timeTo_; } const Int64 timeFrom_; const Int64 timeTo_; }; void zen::applyTimeSpanFilter(FolderComparison& folderCmp, const Int64& timeFrom, const Int64& timeTo) { std::for_each(begin(folderCmp), end(folderCmp), [&](BaseDirPair& baseDirObj) { FilterByTimeSpan::execute(baseDirObj, timeFrom, timeTo); }); } //############################################################################################################ std::pair zen::deleteFromGridAndHDPreview(const std::vector& selectionLeft, const std::vector& selectionRight, bool deleteOnBothSides) { //don't use wxString here, it's linear allocation strategy would bring perf down to a crawl; Zstring: exponential growth! Zstring fileList; int totalDelCount = 0; if (deleteOnBothSides) { //mix selected rows from left and right (without changing order) std::vector selection; { hash_set objectsUsed; std::copy_if(selectionLeft .begin(), selectionLeft .end(), std::back_inserter(selection), [&](FileSystemObject* fsObj) { return objectsUsed.insert(fsObj).second; }); std::copy_if(selectionRight.begin(), selectionRight.end(), std::back_inserter(selection), [&](FileSystemObject* fsObj) { return objectsUsed.insert(fsObj).second; }); } std::for_each(selection.begin(), selection.end(), [&](const FileSystemObject* fsObj) { if (!fsObj->isEmpty()) { fileList += fsObj->getFullName() + Zstr('\n'); ++totalDelCount; } if (!fsObj->isEmpty()) { fileList += fsObj->getFullName() + Zstr('\n'); ++totalDelCount; } fileList += Zstr('\n'); }); } else //delete selected files only { std::for_each(selectionLeft.begin(), selectionLeft.end(), [&](const FileSystemObject* fsObj) { if (!fsObj->isEmpty()) { fileList += fsObj->getFullName() + Zstr('\n'); ++totalDelCount; } }); std::for_each(selectionRight.begin(), selectionRight.end(), [&](const FileSystemObject* fsObj) { if (!fsObj->isEmpty()) { fileList += fsObj->getFullName() + Zstr('\n'); ++totalDelCount; } }); } return std::make_pair(fileList, totalDelCount); } namespace { template inline bool tryReportingError(Function cmd, DeleteFilesHandler& handler) //return "true" on success, "false" if error was ignored { for (;;) try { cmd(); //throw FileError return true; } catch (FileError& error) { switch (handler.reportError(error.toString())) //may throw! { case DeleteFilesHandler::IGNORE_ERROR: return false; case DeleteFilesHandler::RETRY: break; //continue with loop default: assert(false); break; } } } #ifdef ZEN_WIN //recycleBinStatus() blocks seriously if recycle bin is really full and drive is slow StatusRecycler recycleBinStatusUpdating(const Zstring& dirname, DeleteFilesHandler& callback) { const std::wstring msg = replaceCpy(_("Checking recycle bin availability for folder %x..."), L"%x", fmtFileName(dirname), false); auto ft = async([=] { return recycleBinStatus(dirname); }); while (!ft.timed_wait(boost::posix_time::milliseconds(UI_UPDATE_INTERVAL / 2))) callback.reportStatus(msg); //may throw! return ft.get(); } #endif template void categorize(const std::set& rowsIn, std::vector& deletePermanent, std::vector& deleteRecyler, bool useRecycleBin, std::map& hasRecyclerBuffer, DeleteFilesHandler& callback) { auto hasRecycler = [&](const FileSystemObject& fsObj) -> bool { #ifdef ZEN_WIN const Zstring& baseDirPf = fsObj.root().getBaseDirPf(); auto it = hasRecyclerBuffer.find(baseDirPf); if (it != hasRecyclerBuffer.end()) return it->second; return hasRecyclerBuffer.insert(std::make_pair(baseDirPf, recycleBinStatusUpdating(baseDirPf, callback) == STATUS_REC_EXISTS)).first->second; #elif defined ZEN_LINUX || defined ZEN_MAC return true; #endif }; for (auto it = rowsIn.begin(); it != rowsIn.end(); ++it) if (!(*it)->isEmpty()) { if (useRecycleBin && hasRecycler(**it)) //Windows' ::SHFileOperation() will delete permanently anyway, but we have a superior deletion routine deleteRecyler.push_back(*it); else deletePermanent.push_back(*it); } } template struct ItemDeleter : public FSObjectVisitor //throw FileError, but nothrow constructor!!! { ItemDeleter(bool useRecycleBin, DeleteFilesHandler& handler) : handler_(handler), useRecycleBin_(useRecycleBin), remCallback(*this) { if (useRecycleBin_) { txtRemovingFile = _("Moving file %x to the recycle bin" ); txtRemovingDirectory = _("Moving folder %x to the recycle bin" ); txtRemovingSymlink = _("Moving symbolic link %x to the recycle bin"); } else { txtRemovingFile = _("Deleting file %x" ); txtRemovingDirectory = _("Deleting folder %x" ); txtRemovingSymlink = _("Deleting symbolic link %x"); } } virtual void visit(const FilePair& fileObj) { notifyFileDeletion(fileObj.getFullName()); if (useRecycleBin_) zen::recycleOrDelete(fileObj.getFullName()); //throw FileError else zen::removeFile(fileObj.getFullName()); //throw FileError } virtual void visit(const SymlinkPair& linkObj) { notifySymlinkDeletion(linkObj.getFullName()); if (useRecycleBin_) zen::recycleOrDelete(linkObj.getFullName()); //throw FileError else { if (dirExists(linkObj.getFullName())) //dir symlink zen::removeDirectory(linkObj.getFullName()); //throw FileError else //file symlink, broken symlink zen::removeFile(linkObj.getFullName()); //throw FileError } } virtual void visit(const DirPair& dirObj) { notifyDirectoryDeletion(dirObj.getFullName()); //notfied twice! see RemoveCallbackImpl -> no big deal if (useRecycleBin_) zen::recycleOrDelete(dirObj.getFullName()); //throw FileError else zen::removeDirectory(dirObj.getFullName(), &remCallback); //throw FileError } private: struct RemoveCallbackImpl : public zen::CallbackRemoveDir { RemoveCallbackImpl(ItemDeleter& itemDeleter) : itemDeleter_(itemDeleter) {} virtual void onBeforeFileDeletion(const Zstring& filename) { itemDeleter_.notifyFileDeletion (filename); } virtual void onBeforeDirDeletion (const Zstring& dirname) { itemDeleter_.notifyDirectoryDeletion(dirname ); } private: ItemDeleter& itemDeleter_; }; void notifyFileDeletion (const Zstring& objName) { notifyItemDeletion(txtRemovingFile , objName); } void notifyDirectoryDeletion(const Zstring& objName) { notifyItemDeletion(txtRemovingDirectory, objName); } void notifySymlinkDeletion (const Zstring& objName) { notifyItemDeletion(txtRemovingSymlink , objName); } void notifyItemDeletion(const std::wstring& statusText, const Zstring& objName) { handler_.reportStatus(replaceCpy(statusText, L"%x", fmtFileName(objName))); } DeleteFilesHandler& handler_; const bool useRecycleBin_; RemoveCallbackImpl remCallback; std::wstring txtRemovingFile; std::wstring txtRemovingDirectory; std::wstring txtRemovingSymlink; }; template void deleteFromGridAndHDOneSide(std::vector& ptrList, bool useRecycleBin, DeleteFilesHandler& handler) { ItemDeleter deleter(useRecycleBin, handler); for (auto it = ptrList.begin(); it != ptrList.end(); ++it) //VS 2010 bug prevents replacing this by std::for_each + lamba { FileSystemObject& fsObj = **it; //all pointers are required(!) to be bound if (!fsObj.isEmpty()) //element may be implicitly deleted, e.g. if parent folder was deleted first tryReportingError([&] { fsObj.accept(deleter); //throw FileError fsObj.removeObject(); //if directory: removes recursively! }, handler); } } } void zen::deleteFromGridAndHD(const std::vector& rowsToDeleteOnLeft, //refresh GUI grid after deletion to remove invalid rows const std::vector& rowsToDeleteOnRight, //all pointers need to be bound! FolderComparison& folderCmp, //attention: rows will be physically deleted! const std::vector& directCfgs, bool deleteOnBothSides, bool useRecycleBin, DeleteFilesHandler& statusHandler, bool& warningRecyclerMissing) { if (folderCmp.empty()) return; else if (folderCmp.size() != directCfgs.size()) throw std::logic_error("Programming Error: Contract violation! " + std::string(__FILE__) + ":" + numberTo(__LINE__)); //build up mapping from base directory to corresponding direction config hash_map baseDirCfgs; for (auto it = folderCmp.begin(); it != folderCmp.end(); ++it) baseDirCfgs[&** it] = directCfgs[it - folderCmp.begin()]; std::set deleteLeft (rowsToDeleteOnLeft .begin(), rowsToDeleteOnLeft .end()); std::set deleteRight(rowsToDeleteOnRight.begin(), rowsToDeleteOnRight.end()); if (deleteOnBothSides) { deleteLeft.insert(deleteRight.begin(), deleteRight.end()); deleteRight = deleteLeft; } set_remove_if(deleteLeft, [](const FileSystemObject* fsObj) { return fsObj->isEmpty(); }); //still needed? set_remove_if(deleteRight, [](const FileSystemObject* fsObj) { return fsObj->isEmpty(); }); // //ensure cleanup: redetermination of sync-directions and removal of invalid rows auto updateDirection = [&]() { //update sync direction: we cannot do a full redetermination since the user may already have entered manual changes std::set deletedTotal = deleteLeft; deletedTotal.insert(deleteRight.begin(), deleteRight.end()); for (auto it = deletedTotal.begin(); it != deletedTotal.end(); ++it) { FileSystemObject& fsObj = **it; //all pointers are required(!) to be bound if (fsObj.isEmpty() != fsObj.isEmpty()) //make sure objects exists on one side only { auto cfgIter = baseDirCfgs.find(&fsObj.root()); if (cfgIter != baseDirCfgs.end()) { SyncDirection newDir = SyncDirection::NONE; if (cfgIter->second.var == DirectionConfig::TWOWAY) newDir = fsObj.isEmpty() ? SyncDirection::RIGHT : SyncDirection::LEFT; else { const DirectionSet& dirCfg = extractDirections(cfgIter->second); newDir = fsObj.isEmpty() ? dirCfg.exRightSideOnly : dirCfg.exLeftSideOnly; } setSyncDirectionRec(newDir, fsObj); //set new direction (recursively) } else assert(!"this should not happen!"); } } //last step: cleanup empty rows: this one invalidates all pointers! std::for_each(begin(folderCmp), end(folderCmp), BaseDirPair::removeEmpty); }; ZEN_ON_SCOPE_EXIT(updateDirection()); //MSVC: assert is a macro and it doesn't play nice with ZEN_ON_SCOPE_EXIT, surprise... wasn't there something about macros being "evil"? //categorize rows into permanent deletion and recycle bin std::vector deletePermanentLeft; std::vector deletePermanentRight; std::vector deleteRecylerLeft; std::vector deleteRecylerRight; std::map hasRecyclerBuffer; categorize(deleteLeft, deletePermanentLeft, deleteRecylerLeft, useRecycleBin, hasRecyclerBuffer, statusHandler); categorize(deleteRight, deletePermanentRight, deleteRecylerRight, useRecycleBin, hasRecyclerBuffer, statusHandler); //windows: check if recycle bin really exists; if not, Windows will silently delete, which is wrong if (useRecycleBin && std::any_of(hasRecyclerBuffer.begin(), hasRecyclerBuffer.end(), [](std::pair item) { return !item.second; })) { std::wstring msg = _("The recycle bin is not available for the following folders. Files will be deleted permanently instead:") + L"\n"; for (auto it = hasRecyclerBuffer.begin(); it != hasRecyclerBuffer.end(); ++it) if (!it->second) msg += std::wstring(L"\n") + it->first; statusHandler.reportWarning(msg, warningRecyclerMissing); } deleteFromGridAndHDOneSide(deleteRecylerLeft, true, statusHandler); deleteFromGridAndHDOneSide(deletePermanentLeft, false, statusHandler); deleteFromGridAndHDOneSide(deleteRecylerRight, true, statusHandler); deleteFromGridAndHDOneSide(deletePermanentRight, false, statusHandler); }