616 lines
28 KiB
C++
616 lines
28 KiB
C++
// *****************************************************************************
|
|
// * This file is part of the FreeFileSync project. It is distributed under *
|
|
// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 *
|
|
// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved *
|
|
// *****************************************************************************
|
|
|
|
#include "versioning.h"
|
|
#include "parallel_scan.h"
|
|
#include "status_handler_impl.h"
|
|
#include "dir_exist_async.h"
|
|
|
|
using namespace zen;
|
|
using namespace fff;
|
|
|
|
|
|
namespace
|
|
{
|
|
inline
|
|
Zstring getDotExtension(const Zstring& filePath) //including "." if extension is existing, returns empty string otherwise
|
|
{
|
|
//const Zstring& extension = getFileExtension(filePath);
|
|
//return extension.empty() ? extension : Zstr('.') + extension;
|
|
|
|
auto it = findLast(filePath.begin(), filePath.end(), FILE_NAME_SEPARATOR);
|
|
if (it == filePath.end())
|
|
it = filePath.begin();
|
|
else
|
|
++it;
|
|
|
|
return Zstring(findLast(it, filePath.end(), Zstr('.')), filePath.end());
|
|
}
|
|
}
|
|
|
|
|
|
//e.g. "Sample.txt 2012-05-15 131513.txt"
|
|
//or "Sample 2012-05-15 131513"
|
|
std::pair<time_t, Zstring> fff::impl::parseVersionedFileName(const Zstring& fileName)
|
|
{
|
|
const auto ext = makeStringView(findLast(fileName.begin(), fileName.end(), Zstr('.')), fileName.end());
|
|
|
|
if (fileName.size() < 2 * ext.length() + 18)
|
|
return {};
|
|
|
|
const auto itExt1 = fileName.end() - (2 * ext.length() + 18);
|
|
if (!equalString(ext, makeStringView(itExt1, ext.length())))
|
|
return {};
|
|
|
|
const auto itTs = itExt1 + ext.length();
|
|
const TimeComp tc = parseTime(Zstr(" %Y-%m-%d %H%M%S"), makeStringView(itTs, 18)); //returns TimeComp() on error
|
|
|
|
const auto [localTime, timeValid] = localToTimeT(tc);
|
|
if (!timeValid)
|
|
return {};
|
|
|
|
Zstring fileNameOrig(fileName.begin(), itTs);
|
|
if (fileNameOrig.empty())
|
|
return {};
|
|
|
|
return {localTime, std::move(fileNameOrig)};
|
|
}
|
|
|
|
|
|
//e.g. "2012-05-15 131513"
|
|
time_t fff::impl::parseVersionedFolderName(const Zstring& folderName)
|
|
{
|
|
const TimeComp tc = parseTime(Zstr("%Y-%m-%d %H%M%S"), folderName); //returns TimeComp() on error
|
|
|
|
const auto [localTime, timeValid] = localToTimeT(tc);
|
|
if (!timeValid)
|
|
return 0;
|
|
|
|
return localTime;
|
|
}
|
|
|
|
|
|
AbstractPath FileVersioner::generateVersionedPath(const Zstring& relativePath) const
|
|
{
|
|
assert(isValidRelPath(relativePath));
|
|
assert(!relativePath.empty());
|
|
|
|
Zstring versionedRelPath;
|
|
switch (versioningStyle_)
|
|
{
|
|
case VersioningStyle::replace:
|
|
versionedRelPath = relativePath;
|
|
break;
|
|
case VersioningStyle::timestampFolder:
|
|
versionedRelPath = timeStamp_ + FILE_NAME_SEPARATOR + relativePath;
|
|
break;
|
|
case VersioningStyle::timestampFile: //assemble time-stamped version name
|
|
versionedRelPath = relativePath + Zstr(' ') + timeStamp_ + getDotExtension(relativePath);
|
|
assert(impl::parseVersionedFileName(getItemName(versionedRelPath)) ==
|
|
std::pair(syncStartTime_, getItemName(relativePath)));
|
|
(void)syncStartTime_; //clang: -Wunused-private-field
|
|
break;
|
|
}
|
|
return AFS::appendRelPath(versioningFolderPath_, versionedRelPath);
|
|
}
|
|
|
|
|
|
namespace
|
|
{
|
|
/* move source to target across volumes:
|
|
- source is expected to exist
|
|
- if target already exists, it is overwritten, unless it is of a different type, e.g. a directory!
|
|
- target parent directories are created if missing */
|
|
template <class Function>
|
|
void moveExistingItemToVersioning(const AbstractPath& sourcePath, const AbstractPath& targetPath, //throw FileError
|
|
Function copyNewItemPlain /*throw FileError*/)
|
|
{
|
|
//start deleting existing target as required by copyFileTransactional()/moveAndRenameItem():
|
|
//best amortized performance if "already existing" is the most common case
|
|
std::exception_ptr deletionError;
|
|
try { AFS::removeFilePlain(targetPath); /*throw FileError*/ }
|
|
catch (FileError&) { deletionError = std::current_exception(); } //probably "not existing" error, defer evaluation
|
|
//overwrite AFS::ItemType::folder with FILE? => highly dubious, do not allow
|
|
|
|
auto fixTargetPathIssues = [&](const FileError& prevEx) //throw FileError
|
|
{
|
|
bool alreadyExisting = false;
|
|
try
|
|
{
|
|
AFS::getItemType(targetPath); //throw FileError
|
|
alreadyExisting = true;
|
|
}
|
|
catch (FileError&) {} //=> not yet existing (=> fine, no path issue) or access error:
|
|
//- let's pretend it doesn't happen :> if it does, worst case: the retry fails with (useless) already existing error
|
|
//- AFS::itemExists()? too expensive, considering that "already existing" is the most common case
|
|
|
|
if (alreadyExisting)
|
|
{
|
|
if (deletionError)
|
|
std::rethrow_exception(deletionError);
|
|
throw prevEx; //yes, slicing, but not relevant here
|
|
}
|
|
|
|
//parent folder missing => create + retry
|
|
//parent folder existing => maybe created shortly after move attempt by parallel thread! => retry
|
|
if (const std::optional<AbstractPath> targetParentPath = AFS::getParentPath(targetPath))
|
|
AFS::createFolderIfMissingRecursion(*targetParentPath); //throw FileError
|
|
};
|
|
|
|
try //first try to move directly without copying
|
|
{
|
|
//already existing: undefined behavior! (e.g. fail/overwrite)
|
|
AFS::moveAndRenameItem(sourcePath, targetPath); //throw FileError, ErrorMoveUnsupported
|
|
//great, we get away cheaply!
|
|
}
|
|
catch (ErrorMoveUnsupported&)
|
|
{
|
|
try
|
|
{
|
|
copyNewItemPlain(); //throw FileError
|
|
}
|
|
catch (const FileError& e)
|
|
{
|
|
fixTargetPathIssues(e); //throw FileError
|
|
|
|
//retry:
|
|
copyNewItemPlain(); //throw FileError
|
|
}
|
|
//[!] remove source file AFTER handling target path errors!
|
|
AFS::removeFilePlain(sourcePath); //throw FileError
|
|
}
|
|
catch (const FileError& e)
|
|
{
|
|
fixTargetPathIssues(e); //throw FileError
|
|
|
|
try //retry
|
|
{
|
|
//already existing: undefined behavior! (e.g. fail/overwrite)
|
|
AFS::moveAndRenameItem(sourcePath, targetPath); //throw FileError, ErrorMoveUnsupported
|
|
}
|
|
catch (ErrorMoveUnsupported&)
|
|
{
|
|
copyNewItemPlain(); //throw FileError
|
|
AFS::removeFilePlain(sourcePath); //throw FileError
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void FileVersioner::checkPathConflict(const AbstractPath& itemPath, const Zstring& relativePath) const //throw FileError
|
|
{
|
|
if (std::optional<PathDependency> pd = getPathDependency(itemPath, versioningFolderPath_))
|
|
{
|
|
assert(pd->itemPathParent == versioningFolderPath_); //otherwise: what the fuck!?
|
|
//user ignored warning about versioning folder being part of sync =>
|
|
//prevent files from being moved to versioning recursively:
|
|
throw FileError(trimCpy(replaceCpy(replaceCpy(_("Cannot move %x to %y."),
|
|
L"%x", L'\n' + fmtPath(AFS::getDisplayPath(itemPath))),
|
|
L"%y", L'\n' + fmtPath(AFS::getDisplayPath(generateVersionedPath(relativePath))))),
|
|
_("Item already located in the versioning folder."));
|
|
}
|
|
}
|
|
|
|
|
|
void FileVersioner::revisionFile(const FileDescriptor& fileDescr, const Zstring& relativePath, const IoCallback& notifyUnbufferedIO /*throw X*/) const //throw FileError, X
|
|
{
|
|
checkPathConflict(fileDescr.path, relativePath); //throw FileError
|
|
|
|
if (const std::optional<AFS::ItemType> type = AFS::getItemTypeIfExists(fileDescr.path)) //throw FileError
|
|
{
|
|
assert(*type != AFS::ItemType::symlink);
|
|
|
|
if (*type == AFS::ItemType::symlink)
|
|
revisionSymlinkImpl(fileDescr.path, relativePath, nullptr /*onBeforeMove*/); //throw FileError
|
|
else
|
|
revisionFileImpl(fileDescr, relativePath, nullptr /*onBeforeMove*/, notifyUnbufferedIO); //throw FileError, X
|
|
}
|
|
//else -> missing source item is not an error => check BEFORE deleting target
|
|
}
|
|
|
|
|
|
void FileVersioner::revisionFileImpl(const FileDescriptor& fileDescr, const Zstring& relativePath, //throw FileError, X
|
|
const std::function<void(const std::wstring& displayPathFrom, const std::wstring& displayPathTo)>& onBeforeMove,
|
|
const IoCallback& notifyUnbufferedIO /*throw X*/) const
|
|
{
|
|
const AbstractPath& filePath = fileDescr.path;
|
|
|
|
const AbstractPath targetPath = generateVersionedPath(relativePath);
|
|
const AFS::StreamAttributes fileAttr{fileDescr.attr.modTime, fileDescr.attr.fileSize, fileDescr.attr.filePrint};
|
|
|
|
if (onBeforeMove)
|
|
onBeforeMove(AFS::getDisplayPath(filePath), AFS::getDisplayPath(targetPath));
|
|
|
|
moveExistingItemToVersioning(filePath, targetPath, [&] //throw FileError
|
|
{
|
|
//already existing: undefined behavior! (e.g. fail/overwrite/auto-rename)
|
|
//=> not expected, but possible if target deletion failed
|
|
//already existing + no onDeleteTargetFile: undefined behavior! (e.g. fail/overwrite/auto-rename)
|
|
/*const AFS::FileCopyResult result =*/ AFS::copyFileTransactional(filePath, fileAttr, targetPath, //throw FileError, ErrorFileLocked, X
|
|
false, //copyFilePermissions
|
|
false, //transactionalCopy: not needed for versioning! partial copy will be overwritten next time
|
|
nullptr /*onDeleteTargetFile*/, notifyUnbufferedIO);
|
|
//result.errorModTime? => irrelevant for versioning!
|
|
});
|
|
}
|
|
|
|
|
|
void FileVersioner::revisionSymlink(const AbstractPath& linkPath, const Zstring& relativePath) const //throw FileError
|
|
{
|
|
checkPathConflict(linkPath, relativePath); //throw FileError
|
|
|
|
if (AFS::itemExists(linkPath)) //throw FileError
|
|
revisionSymlinkImpl(linkPath, relativePath, nullptr /*onBeforeMove*/); //throw FileError
|
|
//else -> missing source item is not an error => check BEFORE deleting target
|
|
}
|
|
|
|
|
|
void FileVersioner::revisionSymlinkImpl(const AbstractPath& linkPath, const Zstring& relativePath, //throw FileError
|
|
const std::function<void(const std::wstring& displayPathFrom, const std::wstring& displayPathTo)>& onBeforeMove) const
|
|
{
|
|
|
|
const AbstractPath targetPath = generateVersionedPath(relativePath);
|
|
|
|
if (onBeforeMove)
|
|
onBeforeMove(AFS::getDisplayPath(linkPath), AFS::getDisplayPath(targetPath));
|
|
|
|
moveExistingItemToVersioning(linkPath, targetPath, [&] { AFS::copySymlink(linkPath, targetPath, false /*copy filesystem permissions*/); }); //throw FileError
|
|
}
|
|
|
|
|
|
void FileVersioner::revisionFolder(const AbstractPath& folderPath, const Zstring& relativePath, //throw FileError, X
|
|
const std::function<void(const std::wstring& displayPathFrom, const std::wstring& displayPathTo)>& onBeforeFileMove /*throw X*/,
|
|
const std::function<void(const std::wstring& displayPathFrom, const std::wstring& displayPathTo)>& onBeforeFolderMove /*throw X*/,
|
|
const IoCallback& notifyUnbufferedIO /*throw X*/) const
|
|
{
|
|
checkPathConflict(folderPath, relativePath); //throw FileError
|
|
|
|
//no error situation if directory is not existing! manual deletion relies on it!
|
|
if (const std::optional<AFS::ItemType> type = AFS::getItemTypeIfExists(folderPath)) //throw FileError
|
|
{
|
|
assert(*type != AFS::ItemType::symlink);
|
|
|
|
if (*type == AFS::ItemType::symlink) //on Linux there is just one type of symlink, and since we do revision file symlinks, we should revision dir symlinks as well!
|
|
revisionSymlinkImpl(folderPath, relativePath, onBeforeFileMove); //throw FileError
|
|
else
|
|
revisionFolderImpl(folderPath, relativePath, onBeforeFileMove, onBeforeFolderMove, notifyUnbufferedIO); //throw FileError, X
|
|
}
|
|
else //even if the folder does not exist anymore, significant I/O work was done => report
|
|
if (onBeforeFolderMove) onBeforeFolderMove(AFS::getDisplayPath(folderPath), AFS::getDisplayPath(AFS::appendRelPath(versioningFolderPath_, relativePath)));
|
|
}
|
|
|
|
|
|
void FileVersioner::revisionFolderImpl(const AbstractPath& folderPath, const Zstring& relPath, //throw FileError, X
|
|
const std::function<void(const std::wstring& displayPathFrom, const std::wstring& displayPathTo)>& onBeforeFileMove,
|
|
const std::function<void(const std::wstring& displayPathFrom, const std::wstring& displayPathTo)>& onBeforeFolderMove,
|
|
const IoCallback& notifyUnbufferedIO /*throw X*/) const
|
|
{
|
|
|
|
//create target directories only when needed in moveFileToVersioning(): avoid empty directories!
|
|
std::vector<AFS::FolderInfo> folders;
|
|
{
|
|
std::vector<AFS::FileInfo> files;
|
|
std::vector<AFS::SymlinkInfo> symlinks;
|
|
|
|
AFS::traverseFolder(folderPath, //throw FileError
|
|
[&](const AFS::FileInfo& fi) { files .push_back(fi); assert(!files.back().isFollowedSymlink); },
|
|
[&](const AFS::FolderInfo& fi) { folders .push_back(fi); },
|
|
[&](const AFS::SymlinkInfo& si) { symlinks.push_back(si); });
|
|
|
|
for (const AFS::FileInfo& fileInfo : files)
|
|
{
|
|
const FileDescriptor fileDescr
|
|
{
|
|
.path = AFS::appendRelPath(folderPath, fileInfo.itemName),
|
|
.attr = {fileInfo.modTime, fileInfo.fileSize, fileInfo.filePrint, false /*isFollowedSymlink*/},
|
|
};
|
|
|
|
revisionFileImpl(fileDescr, appendPath(relPath, fileInfo.itemName), onBeforeFileMove, notifyUnbufferedIO); //throw FileError, X
|
|
}
|
|
|
|
for (const AFS::SymlinkInfo& linkInfo : symlinks)
|
|
revisionSymlinkImpl(AFS::appendRelPath(folderPath, linkInfo.itemName),
|
|
appendPath(relPath, linkInfo.itemName), onBeforeFileMove); //throw FileError
|
|
}
|
|
|
|
//move folders recursively
|
|
for (const AFS::FolderInfo& folderInfo : folders)
|
|
revisionFolderImpl(AFS::appendRelPath(folderPath, folderInfo.itemName), //throw FileError, X
|
|
appendPath(relPath, folderInfo.itemName),
|
|
onBeforeFileMove, onBeforeFolderMove, notifyUnbufferedIO);
|
|
//delete source
|
|
if (onBeforeFolderMove)
|
|
onBeforeFolderMove(AFS::getDisplayPath(folderPath), AFS::getDisplayPath(AFS::appendRelPath(versioningFolderPath_, relPath)));
|
|
|
|
AFS::removeFolderPlain(folderPath); //throw FileError
|
|
}
|
|
|
|
//###########################################################################################
|
|
|
|
namespace
|
|
{
|
|
struct VersionInfo
|
|
{
|
|
time_t versionTime = 0;
|
|
AbstractPath filePath;
|
|
bool isSymlink = false;
|
|
};
|
|
using VersionInfoMap = std::unordered_map<Zstring, std::vector<VersionInfo>>; //relPathOrig => <version infos>
|
|
|
|
//subfolder\Sample.txt 2012-05-15 131513.txt => subfolder\Sample.txt version:2012-05-15 131513
|
|
//2012-05-15 131513\subfolder\Sample.txt => " "
|
|
|
|
void findFileVersions(VersionInfoMap& versions,
|
|
const FolderContainer& folderCont,
|
|
const AbstractPath& parentFolderPath,
|
|
const Zstring& relPathOrigParent,
|
|
const time_t* versionTimeParent)
|
|
{
|
|
auto addVersion = [&](const Zstring& fileName, const Zstring& fileNameOrig, time_t versionTime, bool isSymlink)
|
|
{
|
|
const Zstring& relPathOrig = appendPath(relPathOrigParent, fileNameOrig);
|
|
const AbstractPath& filePath = AFS::appendRelPath(parentFolderPath, fileName);
|
|
|
|
versions[relPathOrig].push_back(VersionInfo{versionTime, filePath, isSymlink});
|
|
};
|
|
|
|
auto extractFileVersion = [&](const Zstring& fileName, bool isSymlink)
|
|
{
|
|
if (versionTimeParent) //VersioningStyle::timestampFolder
|
|
addVersion(fileName, fileName, *versionTimeParent, isSymlink);
|
|
else
|
|
{
|
|
const std::pair<time_t, Zstring> vfn = fff::impl::parseVersionedFileName(fileName);
|
|
if (vfn.first != 0) //VersioningStyle::timestampFile
|
|
addVersion(fileName, vfn.second, vfn.first, isSymlink);
|
|
}
|
|
};
|
|
|
|
for (const auto& [fileName, attr] : folderCont.files)
|
|
extractFileVersion(fileName, false /*isSymlink*/);
|
|
|
|
for (const auto& [linkName, attr] : folderCont.symlinks)
|
|
extractFileVersion(linkName, true /*isSymlink*/);
|
|
|
|
for (const auto& [folderName, attrAndSub] : folderCont.folders)
|
|
{
|
|
if (relPathOrigParent.empty() && !versionTimeParent) //VersioningStyle::timestampFolder?
|
|
{
|
|
assert(!versionTimeParent);
|
|
const time_t versionTime = fff::impl::parseVersionedFolderName(folderName);
|
|
if (versionTime != 0)
|
|
{
|
|
findFileVersions(versions, attrAndSub.second,
|
|
AFS::appendRelPath(parentFolderPath, folderName),
|
|
Zstring(), //[!] skip time-stamped folder
|
|
&versionTime);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
findFileVersions(versions, attrAndSub.second,
|
|
AFS::appendRelPath(parentFolderPath, folderName),
|
|
appendPath(relPathOrigParent, folderName),
|
|
versionTimeParent);
|
|
}
|
|
}
|
|
|
|
|
|
void getFolderItemCount(std::map<AbstractPath, size_t>& folderItemCount, const FolderContainer& folderCont, const AbstractPath& parentFolderPath)
|
|
{
|
|
size_t& itemCount = folderItemCount[parentFolderPath];
|
|
itemCount = std::max(itemCount, folderCont.files.size() + folderCont.symlinks.size() + folderCont.folders.size());
|
|
//theoretically possible that the same folder is found in one case with items, in another case empty (due to an error)
|
|
//e.g. "subfolder" for versioning folders c:\folder and c:\folder\subfolder
|
|
|
|
for (const auto& [folderName, attrAndSub] : folderCont.folders)
|
|
getFolderItemCount(folderItemCount, attrAndSub.second, AFS::appendRelPath(parentFolderPath, folderName));
|
|
}
|
|
}
|
|
|
|
|
|
std::weak_ordering fff::operator<=>(const VersioningLimitFolder& lhs, const VersioningLimitFolder& rhs)
|
|
{
|
|
if (const std::weak_ordering cmp = std::tie(lhs.versioningFolderPath, lhs.versionMaxAgeDays) <=>
|
|
std::tie(rhs.versioningFolderPath, rhs.versionMaxAgeDays);
|
|
cmp != std::weak_ordering::equivalent)
|
|
return cmp;
|
|
|
|
if (lhs.versionMaxAgeDays > 0)
|
|
if (lhs.versionCountMin != rhs.versionCountMin)
|
|
return lhs.versionCountMin <=> rhs.versionCountMin;
|
|
|
|
return lhs.versionCountMax <=> rhs.versionCountMax;
|
|
}
|
|
|
|
|
|
void fff::applyVersioningLimit(const std::set<VersioningLimitFolder>& folderLimits,
|
|
PhaseCallback& callback /*throw X*/) //throw X
|
|
{
|
|
//--------- determine existing folder paths for traversal ---------
|
|
std::set<DirectoryKey> foldersToRead;
|
|
std::set<VersioningLimitFolder> folderLimitsTmp;
|
|
{
|
|
std::set<AbstractPath> pathsToCheck;
|
|
|
|
for (const VersioningLimitFolder& vlf : folderLimits)
|
|
if (vlf.versionMaxAgeDays > 0 || vlf.versionCountMax > 0) //only analyze versioning folders when needed!
|
|
{
|
|
pathsToCheck.insert(vlf.versioningFolderPath);
|
|
folderLimitsTmp.insert(vlf);
|
|
}
|
|
|
|
//what if versioning folder paths differ only in case? => perf pessimization, but already checked, see fff::synchronize()
|
|
|
|
//we don't want to show an error if version path does not yet exist!
|
|
tryReportingError([&]
|
|
{
|
|
const FolderStatus status = getFolderStatusParallel(pathsToCheck,
|
|
false /*authenticateAccess*/, nullptr /*requestPassword*/, callback); //throw X
|
|
foldersToRead.clear();
|
|
for (const AbstractPath& folderPath : status.existing)
|
|
foldersToRead.insert(DirectoryKey({folderPath, makeSharedRef<NullFilter>(), SymLinkHandling::asLink}));
|
|
|
|
if (!status.failedChecks.empty())
|
|
{
|
|
std::wstring msg = _("Cannot find the following folders:") + L'\n';
|
|
|
|
for (const auto& [folderPath, error] : status.failedChecks)
|
|
msg += L'\n' + AFS::getDisplayPath(folderPath);
|
|
|
|
msg += L"\n___________________________________________";
|
|
for (const auto& [folderPath, error] : status.failedChecks)
|
|
msg += L"\n\n" + replaceCpy(error.toString(), L"\n\n", L'\n');
|
|
|
|
throw FileError(msg);
|
|
}
|
|
}, callback); //throw X
|
|
}
|
|
|
|
//--------- traverse all versioning folders ---------
|
|
const std::wstring textScanning = _("Searching for old file versions:") + L' ';
|
|
|
|
auto onStatusUpdate = [&](const std::wstring& statusLine, int itemsTotal)
|
|
{
|
|
callback.updateStatus(textScanning + statusLine); //throw X
|
|
};
|
|
|
|
const std::map<DirectoryKey, DirectoryValue> folderBuf = parallelFolderScan(foldersToRead,
|
|
[&](const PhaseCallback::ErrorInfo& errorInfo) { return callback.reportError(errorInfo); } /*throw X*/,
|
|
onStatusUpdate /*throw X*/, UI_UPDATE_INTERVAL / 2); //every ~25 ms
|
|
|
|
//--------- group versions per (original) relative path ---------
|
|
std::map<AbstractPath, VersionInfoMap> versionDetails; //versioningFolderPath => <version details>
|
|
std::map<AbstractPath, size_t> folderItemCount; //<folder path> => <item count> for determination of empty folders
|
|
|
|
for (const auto& [folderKey, folderVal] : folderBuf)
|
|
{
|
|
const AbstractPath versioningFolderPath = folderKey.folderPath;
|
|
|
|
assert(!versionDetails.contains(versioningFolderPath));
|
|
|
|
findFileVersions(versionDetails[versioningFolderPath],
|
|
folderVal.folderCont,
|
|
versioningFolderPath,
|
|
Zstring() /*relPathOrigParent*/,
|
|
nullptr /*versionTimeParent*/);
|
|
|
|
//determine item count per folder for later detection and removal of empty folders:
|
|
getFolderItemCount(folderItemCount, folderVal.folderCont, versioningFolderPath);
|
|
|
|
//make sure the versioning folder is never found empty and is not deleted:
|
|
++folderItemCount[versioningFolderPath];
|
|
|
|
//similarly, failed folder traversal should not make folders look empty:
|
|
for (const auto& [relPath, errorMsg] : folderVal.failedFolderReads) ++folderItemCount[AFS::appendRelPath(versioningFolderPath, relPath)];
|
|
for (const auto& [relPath, errorMsg] : folderVal.failedItemReads ) ++folderItemCount[AFS::appendRelPath(versioningFolderPath, beforeLast(relPath, FILE_NAME_SEPARATOR, IfNotFoundReturn::none))];
|
|
}
|
|
|
|
//--------- calculate excess file versions ---------
|
|
std::map<AbstractPath, bool /*isSymlink*/> itemsToDelete;
|
|
|
|
const time_t lastMidnightTime = []
|
|
{
|
|
TimeComp tc = getLocalTime(); //returns TimeComp() on error
|
|
tc.second = 0;
|
|
tc.minute = 0;
|
|
tc.hour = 0;
|
|
return localToTimeT(tc).first; //0 on error => swallow => no versions trimmed by versionMaxAgeDays
|
|
}();
|
|
|
|
for (const VersioningLimitFolder& vlf : folderLimitsTmp)
|
|
{
|
|
auto it = versionDetails.find(vlf.versioningFolderPath);
|
|
if (it != versionDetails.end())
|
|
for (auto& [versioningFolderPath, versions] : it->second)
|
|
{
|
|
size_t versionsToKeep = versions.size();
|
|
if (vlf.versionMaxAgeDays > 0)
|
|
{
|
|
const time_t cutOffTime = lastMidnightTime - static_cast<time_t>(vlf.versionMaxAgeDays) * 24 * 3600;
|
|
|
|
versionsToKeep = std::count_if(versions.begin(), versions.end(), [cutOffTime](const VersionInfo& vi) { return vi.versionTime >= cutOffTime; });
|
|
|
|
if (vlf.versionCountMin > 0)
|
|
versionsToKeep = std::max<size_t>(versionsToKeep, vlf.versionCountMin);
|
|
}
|
|
if (vlf.versionCountMax > 0)
|
|
versionsToKeep = std::min<size_t>(versionsToKeep, vlf.versionCountMax);
|
|
|
|
if (versions.size() > versionsToKeep)
|
|
{
|
|
std::nth_element(versions.begin(), versions.end() - versionsToKeep, versions.end(),
|
|
[](const VersionInfo& lhs, const VersionInfo& rhs) { return lhs.versionTime < rhs.versionTime; });
|
|
//oldest versions sorted to the front
|
|
|
|
std::for_each(versions.begin(), versions.end() - versionsToKeep, [&](const VersionInfo& vi)
|
|
{
|
|
itemsToDelete.emplace(vi.filePath, vi.isSymlink);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
//--------- remove excess file versions ---------
|
|
Protected<std::map<AbstractPath, size_t>&> protFolderItemCount(folderItemCount);
|
|
const std::wstring txtRemoving = _("Removing old file versions:") + L' ';
|
|
const std::wstring txtDeletingFolder = _("Deleting folder %x");
|
|
|
|
std::function<void(const AbstractPath& folderPath, AsyncCallback& acb)> deleteEmptyFolderTask;
|
|
deleteEmptyFolderTask = [&txtDeletingFolder, &protFolderItemCount, &deleteEmptyFolderTask](const AbstractPath& folderPath, AsyncCallback& acb) //throw ThreadStopRequest
|
|
{
|
|
const std::wstring errMsg = tryReportingError([&] //throw ThreadStopRequest
|
|
{
|
|
acb.updateStatus(replaceCpy(txtDeletingFolder, L"%x", fmtPath(AFS::getDisplayPath(folderPath)))); //throw ThreadStopRequest
|
|
AFS::removeEmptyFolderIfExists(folderPath); //throw FileError
|
|
}, acb);
|
|
|
|
if (errMsg.empty())
|
|
if (const std::optional<AbstractPath> parentPath = AFS::getParentPath(folderPath))
|
|
{
|
|
bool deleteParent = false;
|
|
protFolderItemCount.access([&](auto& folderItemCount2) { deleteParent = --folderItemCount2[*parentPath] == 0; });
|
|
if (deleteParent) //we're done here anyway => no need to schedule parent deletion in a separate task!
|
|
deleteEmptyFolderTask(*parentPath, acb); //throw ThreadStopRequest
|
|
}
|
|
};
|
|
|
|
std::vector<std::pair<AbstractPath, ParallelWorkItem>> parallelWorkload;
|
|
|
|
for (const auto& [folderPath, itemCount] : folderItemCount)
|
|
if (itemCount == 0)
|
|
parallelWorkload.emplace_back(folderPath, [&deleteEmptyFolderTask](ParallelContext& ctx)
|
|
{
|
|
deleteEmptyFolderTask(ctx.itemPath, ctx.acb); //throw ThreadStopRequest
|
|
});
|
|
|
|
for (const auto& [itemPath, isSymlink] : itemsToDelete)
|
|
parallelWorkload.emplace_back(itemPath, [isSymlink /*clang bug*/= isSymlink, &txtRemoving, &protFolderItemCount, &deleteEmptyFolderTask](ParallelContext& ctx) //throw ThreadStopRequest
|
|
{
|
|
const std::wstring errMsg = tryReportingError([&] //throw ThreadStopRequest
|
|
{
|
|
reportInfo(txtRemoving + AFS::getDisplayPath(ctx.itemPath), ctx.acb); //throw ThreadStopRequest
|
|
if (isSymlink)
|
|
AFS::removeSymlinkIfExists(ctx.itemPath); //throw FileError
|
|
else
|
|
AFS::removeFileIfExists(ctx.itemPath); //throw FileError
|
|
}, ctx.acb);
|
|
|
|
if (errMsg.empty())
|
|
if (const std::optional<AbstractPath> parentPath = AFS::getParentPath(ctx.itemPath))
|
|
{
|
|
bool deleteParent = false;
|
|
protFolderItemCount.access([&](auto& folderItemCount2) { deleteParent = --folderItemCount2[*parentPath] == 0; });
|
|
if (deleteParent)
|
|
deleteEmptyFolderTask(*parentPath, ctx.acb); //throw ThreadStopRequest
|
|
}
|
|
});
|
|
|
|
massParallelExecute(parallelWorkload,
|
|
Zstr("Versioning Limit"), callback /*throw X*/); //throw X
|
|
}
|