Files
2025-12-10 14:38:26 -08:00

2452 lines
87 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 "config.h"
#include <zenxml/xml.h>
#include <zen/file_access.h>
#include <zen/time.h>
#include <zen/process_exec.h>
#include <wx/uilocale.h>
#include "ffs_paths.h"
#include "base_tools.h"
using namespace zen;
using namespace fff; //required for correct overload resolution!
namespace
{
//-------------------------------------------------------------------------------------------------------------------------------
const int XML_FORMAT_GLOBAL_CFG = 28; //2025-09-25
const int XML_FORMAT_SYNC_CFG = 23; //2023-08-24
//-------------------------------------------------------------------------------------------------------------------------------
}
const ExternalApp fff::extCommandFileManager
//"xdg-open %parent_path%" -> not good enough: we need %local_path% for proper MTP/Google Drive handling
{L"Show in file manager", "xdg-open \"$(dirname %local_path%)\""};
//mark for extraction: _("Show in file manager") Linux doesn't use the term "folder"
const ExternalApp fff::extCommandOpenDefault
{L"Open with default application", "xdg-open %local_path%"};
GlobalConfig::GlobalConfig() :
soundFileSyncFinished(appendPath(getResourceDirPath(), Zstr("bell.wav"))),
soundFileAlertPending(appendPath(getResourceDirPath(), Zstr("remind.wav")))
{
}
//################################################################################################################
Zstring fff::getGlobalConfigDefaultPath() { return appendPath(getConfigDirPath(), Zstr("GlobalSettings.xml")); }
Zstring fff::getLogFolderDefaultPath () { return appendPath(getConfigDirPath(), Zstr("Logs")); }
namespace
{
std::vector<Zstring> splitFilterByLines(Zstring filterPhrase)
{
trim(filterPhrase);
if (filterPhrase.empty())
return {};
return splitCpy(filterPhrase, Zstr('\n'), SplitOnEmpty::allow);
}
Zstring mergeFilterLines(const std::vector<Zstring>& filterLines)
{
Zstring out;
for (const Zstring& line : filterLines)
{
out += line;
out += Zstr('\n');
}
return trimCpy(out);
}
}
namespace zen
{
template <> inline
void writeText(const wxLanguage& value, std::string& output)
{
//use description as unique wxLanguage identifier, see localization.cpp
//=> handle changes to wxLanguage enum between wxWidgets versions
const wxString& canonicalName = wxUILocale::GetLanguageCanonicalName(value);
assert(!canonicalName.empty());
if (!canonicalName.empty())
output = utfTo<std::string>(canonicalName);
else
output = utfTo<std::string>(wxUILocale::GetLanguageCanonicalName(wxLANGUAGE_ENGLISH_US));
}
template <> inline
bool readText(const std::string& input, wxLanguage& value)
{
if (const wxLanguageInfo* lngInfo = wxUILocale::FindLanguageInfo(utfTo<wxString>(input)))
{
value = static_cast<wxLanguage>(lngInfo->Language);
return true;
}
return false;
}
template <> inline
void writeText(const ColorTheme& value, std::string& output)
{
switch (value)
{
case ColorTheme::System:
output = "Default";
break;
case ColorTheme::Light:
output = "Light";
break;
case ColorTheme::Dark:
output = "Dark";
break;
}
}
template <> inline
bool readText(const std::string& input, ColorTheme& value)
{
const std::string tmp = trimCpy(input);
if (tmp == "Default")
value = ColorTheme::System;
else if (tmp == "Light")
value = ColorTheme::Light;
else if (tmp == "Dark")
value = ColorTheme::Dark;
else
return false;
return true;
}
template <> inline
void writeText(const CompareVariant& value, std::string& output)
{
switch (value)
{
case CompareVariant::timeSize:
output = "TimeAndSize";
break;
case CompareVariant::content:
output = "Content";
break;
case CompareVariant::size:
output = "Size";
break;
}
}
template <> inline
bool readText(const std::string& input, CompareVariant& value)
{
const std::string tmp = trimCpy(input);
if (tmp == "TimeAndSize")
value = CompareVariant::timeSize;
else if (tmp == "Content")
value = CompareVariant::content;
else if (tmp == "Size")
value = CompareVariant::size;
else
return false;
return true;
}
template <> inline
void writeText(const SyncDirection& value, std::string& output)
{
switch (value)
{
case SyncDirection::left:
output = "left";
break;
case SyncDirection::right:
output = "right";
break;
case SyncDirection::none:
output = "none";
break;
}
}
template <> inline
bool readText(const std::string& input, SyncDirection& value)
{
const std::string tmp = trimCpy(input);
if (tmp == "left")
value = SyncDirection::left;
else if (tmp == "right")
value = SyncDirection::right;
else if (tmp == "none")
value = SyncDirection::none;
else
return false;
return true;
}
template <> inline
void writeText(const BatchErrorHandling& value, std::string& output)
{
switch (value)
{
case BatchErrorHandling::showPopup:
output = "Show";
break;
case BatchErrorHandling::cancel:
output = "Cancel";
break;
}
}
template <> inline
bool readText(const std::string& input, ResultsNotification& value)
{
const std::string tmp = trimCpy(input);
if (tmp == "Always")
value = ResultsNotification::always;
else if (tmp == "ErrorWarning")
value = ResultsNotification::errorWarning;
else if (tmp == "ErrorOnly")
value = ResultsNotification::errorOnly;
else
return false;
return true;
}
template <> inline
void writeText(const ResultsNotification& value, std::string& output)
{
switch (value)
{
case ResultsNotification::always:
output = "Always";
break;
case ResultsNotification::errorWarning:
output = "ErrorWarning";
break;
case ResultsNotification::errorOnly:
output = "ErrorOnly";
break;
}
}
template <> inline
bool readText(const std::string& input, BatchErrorHandling& value)
{
const std::string tmp = trimCpy(input);
if (tmp == "Show")
value = BatchErrorHandling::showPopup;
else if (tmp == "Cancel")
value = BatchErrorHandling::cancel;
else
return false;
return true;
}
template <> inline
void writeText(const PostSyncCondition& value, std::string& output)
{
switch (value)
{
case PostSyncCondition::completion:
output = "Completion";
break;
case PostSyncCondition::errors:
output = "Errors";
break;
case PostSyncCondition::success:
output = "Success";
break;
}
}
template <> inline
bool readText(const std::string& input, PostSyncCondition& value)
{
const std::string tmp = trimCpy(input);
if (tmp == "Completion")
value = PostSyncCondition::completion;
else if (tmp == "Errors")
value = PostSyncCondition::errors;
else if (tmp == "Success")
value = PostSyncCondition::success;
else
return false;
return true;
}
template <> inline
void writeText(const PostBatchAction& value, std::string& output)
{
switch (value)
{
case PostBatchAction::none:
output = "None";
break;
case PostBatchAction::sleep:
output = "Sleep";
break;
case PostBatchAction::shutdown:
output = "Shutdown";
break;
}
}
template <> inline
bool readText(const std::string& input, PostBatchAction& value)
{
const std::string tmp = trimCpy(input);
if (tmp == "None")
value = PostBatchAction::none;
else if (tmp == "Sleep")
value = PostBatchAction::sleep;
else if (tmp == "Shutdown")
value = PostBatchAction::shutdown;
else
return false;
return true;
}
template <> inline
void writeText(const GridIconSize& value, std::string& output)
{
switch (value)
{
case GridIconSize::small:
output = "Small";
break;
case GridIconSize::medium:
output = "Medium";
break;
case GridIconSize::large:
output = "Large";
break;
}
}
template <> inline
bool readText(const std::string& input, GridIconSize& value)
{
const std::string tmp = trimCpy(input);
if (tmp == "Small")
value = GridIconSize::small;
else if (tmp == "Medium")
value = GridIconSize::medium;
else if (tmp == "Large")
value = GridIconSize::large;
else
return false;
return true;
}
template <> inline
void writeText(const DeletionVariant& value, std::string& output)
{
switch (value)
{
case DeletionVariant::permanent:
output = "Permanent";
break;
case DeletionVariant::recycler:
output = "RecycleBin";
break;
case DeletionVariant::versioning:
output = "Versioning";
break;
}
}
template <> inline
bool readText(const std::string& input, DeletionVariant& value)
{
const std::string tmp = trimCpy(input);
if (tmp == "Permanent")
value = DeletionVariant::permanent;
else if (tmp == "RecycleBin")
value = DeletionVariant::recycler;
else if (tmp == "Versioning")
value = DeletionVariant::versioning;
else
return false;
return true;
}
template <> inline
void writeText(const SymLinkHandling& value, std::string& output)
{
switch (value)
{
case SymLinkHandling::exclude:
output = "Exclude";
break;
case SymLinkHandling::asLink:
output = "Direct";
break;
case SymLinkHandling::follow:
output = "Follow";
break;
}
}
template <> inline
bool readText(const std::string& input, SymLinkHandling& value)
{
const std::string tmp = trimCpy(input);
if (tmp == "Exclude")
value = SymLinkHandling::exclude;
else if (tmp == "Direct")
value = SymLinkHandling::asLink;
else if (tmp == "Follow")
value = SymLinkHandling::follow;
else
return false;
return true;
}
template <> inline
void writeText(const GridViewType& value, std::string& output)
{
switch (value)
{
case GridViewType::difference:
output = "Difference";
break;
case GridViewType::action:
output = "Action";
break;
}
}
template <> inline
bool readText(const std::string& input, GridViewType& value)
{
const std::string tmp = trimCpy(input);
if (tmp == "Difference")
value = GridViewType::difference;
else if (tmp == "Action")
value = GridViewType::action;
else
return false;
return true;
}
template <> inline
void writeText(const ColumnTypeRim& value, std::string& output)
{
switch (value)
{
case ColumnTypeRim::path:
output = "Path";
break;
case ColumnTypeRim::size:
output = "Size";
break;
case ColumnTypeRim::date:
output = "Date";
break;
case ColumnTypeRim::extension:
output = "Ext";
break;
}
}
template <> inline
bool readText(const std::string& input, ColumnTypeRim& value)
{
const std::string tmp = trimCpy(input);
if (tmp == "Path")
value = ColumnTypeRim::path;
else if (tmp == "Size")
value = ColumnTypeRim::size;
else if (tmp == "Date")
value = ColumnTypeRim::date;
else if (tmp == "Ext")
value = ColumnTypeRim::extension;
else
return false;
return true;
}
template <> inline
void writeText(const ItemPathFormat& value, std::string& output)
{
switch (value)
{
case ItemPathFormat::name:
output = "Item";
break;
case ItemPathFormat::relative:
output = "Relative";
break;
case ItemPathFormat::full:
output = "Full";
break;
}
}
template <> inline
bool readText(const std::string& input, ItemPathFormat& value)
{
const std::string tmp = trimCpy(input);
if (tmp == "Item")
value = ItemPathFormat::name;
else if (tmp == "Relative")
value = ItemPathFormat::relative;
else if (tmp == "Full")
value = ItemPathFormat::full;
else
return false;
return true;
}
template <> inline
void writeText(const ColumnTypeCfg& value, std::string& output)
{
switch (value)
{
case ColumnTypeCfg::name:
output = "Name";
break;
case ColumnTypeCfg::lastSync:
output = "Last";
break;
case ColumnTypeCfg::lastLog:
output = "Log";
break;
}
}
template <> inline
bool readText(const std::string& input, ColumnTypeCfg& value)
{
const std::string tmp = trimCpy(input);
if (tmp == "Name")
value = ColumnTypeCfg::name;
else if (tmp == "Last")
value = ColumnTypeCfg::lastSync;
else if (tmp == "Log")
value = ColumnTypeCfg::lastLog;
else
return false;
return true;
}
template <> inline
void writeText(const ColumnTypeOverview& value, std::string& output)
{
switch (value)
{
case ColumnTypeOverview::folder:
output = "Tree";
break;
case ColumnTypeOverview::itemCount:
output = "Count";
break;
case ColumnTypeOverview::bytes:
output = "Bytes";
break;
}
}
template <> inline
bool readText(const std::string& input, ColumnTypeOverview& value)
{
const std::string tmp = trimCpy(input);
if (tmp == "Tree")
value = ColumnTypeOverview::folder;
else if (tmp == "Count")
value = ColumnTypeOverview::itemCount;
else if (tmp == "Bytes")
value = ColumnTypeOverview::bytes;
else
return false;
return true;
}
template <> inline
void writeText(const UnitSize& value, std::string& output)
{
switch (value)
{
case UnitSize::none:
output = "None";
break;
case UnitSize::byte:
output = "Byte";
break;
case UnitSize::kb:
output = "KB";
break;
case UnitSize::mb:
output = "MB";
break;
}
}
template <> inline
bool readText(const std::string& input, UnitSize& value)
{
const std::string tmp = trimCpy(input);
if (tmp == "None")
value = UnitSize::none;
else if (tmp == "Byte")
value = UnitSize::byte;
else if (tmp == "KB")
value = UnitSize::kb;
else if (tmp == "MB")
value = UnitSize::mb;
else
return false;
return true;
}
template <> inline
void writeText(const UnitTime& value, std::string& output)
{
switch (value)
{
case UnitTime::none:
output = "None";
break;
case UnitTime::today:
output = "Today";
break;
case UnitTime::thisMonth:
output = "Month";
break;
case UnitTime::thisYear:
output = "Year";
break;
case UnitTime::lastDays:
output = "x-days";
break;
}
}
template <> inline
bool readText(const std::string& input, UnitTime& value)
{
const std::string tmp = trimCpy(input);
if (tmp == "None")
value = UnitTime::none;
else if (tmp == "Today")
value = UnitTime::today;
else if (tmp == "Month")
value = UnitTime::thisMonth;
else if (tmp == "Year")
value = UnitTime::thisYear;
else if (tmp == "x-days")
value = UnitTime::lastDays;
else
return false;
return true;
}
template <> inline
void writeText(const LogFileFormat& value, std::string& output)
{
switch (value)
{
case LogFileFormat::html:
output = "HTML";
break;
case LogFileFormat::text:
output = "Text";
break;
}
}
template <> inline
bool readText(const std::string& input, LogFileFormat& value)
{
const std::string tmp = trimCpy(input);
if (tmp == "HTML")
value = LogFileFormat::html;
else if (tmp == "Text")
value = LogFileFormat::text;
else
return false;
return true;
}
template <> inline
void writeText(const VersioningStyle& value, std::string& output)
{
switch (value)
{
case VersioningStyle::replace:
output = "Replace";
break;
case VersioningStyle::timestampFolder:
output = "TimeStamp-Folder";
break;
case VersioningStyle::timestampFile:
output = "TimeStamp-File";
break;
}
}
template <> inline
bool readText(const std::string& input, VersioningStyle& value)
{
const std::string tmp = trimCpy(input);
if (tmp == "Replace")
value = VersioningStyle::replace;
else if (tmp == "TimeStamp-Folder")
value = VersioningStyle::timestampFolder;
else if (tmp == "TimeStamp-File")
value = VersioningStyle::timestampFile;
else
return false;
return true;
}
template <> inline
void writeStruc(const ColAttributesRim& value, XmlElement& output)
{
output.setAttribute("Type", value.type);
output.setAttribute("Visible", value.visible);
output.setAttribute("Width", value.offset);
output.setAttribute("Stretch", value.stretch);
}
template <> inline
bool readStruc(const XmlElement& input, ColAttributesRim& value)
{
bool success = true;
success = input.getAttribute("Type", value.type) && success;
success = input.getAttribute("Visible", value.visible) && success;
success = input.getAttribute("Width", value.offset) && success; //offset == width if stretch is 0
success = input.getAttribute("Stretch", value.stretch) && success;
return success; //[!] avoid short-circuit evaluation
}
template <> inline
void writeStruc(const ColAttributesCfg& value, XmlElement& output)
{
output.setAttribute("Type", value.type);
output.setAttribute("Visible", value.visible);
output.setAttribute("Width", value.offset);
output.setAttribute("Stretch", value.stretch);
}
template <> inline
bool readStruc(const XmlElement& input, ColAttributesCfg& value)
{
bool success = true;
success = input.getAttribute("Type", value.type) && success;
success = input.getAttribute("Visible", value.visible) && success;
success = input.getAttribute("Width", value.offset) && success; //offset == width if stretch is 0
success = input.getAttribute("Stretch", value.stretch) && success;
return success; //[!] avoid short-circuit evaluation
}
template <> inline
void writeStruc(const ColumnAttribOverview& value, XmlElement& output)
{
output.setAttribute("Type", value.type);
output.setAttribute("Visible", value.visible);
output.setAttribute("Width", value.offset);
output.setAttribute("Stretch", value.stretch);
}
template <> inline
bool readStruc(const XmlElement& input, ColumnAttribOverview& value)
{
bool success = true;
success = input.getAttribute("Type", value.type) && success;
success = input.getAttribute("Visible", value.visible) && success;
success = input.getAttribute("Width", value.offset) && success; //offset == width if stretch is 0
success = input.getAttribute("Stretch", value.stretch) && success;
return success; //[!] avoid short-circuit evaluation
}
template <> inline
void writeStruc(const ExternalApp& value, XmlElement& output)
{
output.setValue(value.cmdLine);
output.setAttribute("Label", value.description);
}
template <> inline
bool readStruc(const XmlElement& input, ExternalApp& value)
{
const bool rv1 = input.getValue(value.cmdLine);
const bool rv2 = input.getAttribute("Label", value.description);
return rv1 && rv2;
}
template <> inline
void writeText(const TaskResult& value, std::string& output)
{
switch (value)
{
case TaskResult::success:
output = "Success";
break;
case TaskResult::warning:
output = "Warning";
break;
case TaskResult::error:
output = "Error";
break;
case TaskResult::cancelled:
output = "Stopped";
break;
}
}
template <> inline
bool readText(const std::string& input, TaskResult& value)
{
const std::string tmp = trimCpy(input);
if (tmp == "Success")
value = TaskResult::success;
else if (tmp == "Warning")
value = TaskResult::warning;
else if (tmp == "Error")
value = TaskResult::error;
else if (tmp == "Stopped")
value = TaskResult::cancelled;
else
return false;
return true;
}
}
namespace
{
Zstring makePortablePath(const Zstring& pathPhrase)
{
const Zstring& pathTrm = trimCpy(pathPhrase);
const Zstring& ffsPath = getInstallDirPath();
if (pathTrm == ffsPath)
return Zstr("%ffs_path%");
if (startsWith(pathTrm, appendSeparator(ffsPath))) //don't allow *partial* component match!
return Zstring(Zstr("%ffs_path%")) + (pathTrm.c_str() + appendSeparator(ffsPath).size() - 1);
return pathPhrase;
}
Zstring resolvePortablePath(const Zstring& portablePathPhrase)
{
const Zstring& pathTrm = trimCpy(portablePathPhrase);
if (startsWith(pathTrm, Zstr("%ffs_path%")))
return appendPath(getInstallDirPath(), afterFirst(pathTrm, FILE_NAME_SEPARATOR, IfNotFoundReturn::none)); //caveat: appendPath() requires relPath!
//TODO: remove parameter migration after some time! 2022-06-14
if (startsWith(pathTrm, Zstr("%ffs_resource%")))
return appendPath(getResourceDirPath(), afterFirst(pathTrm, FILE_NAME_SEPARATOR, IfNotFoundReturn::none));
return portablePathPhrase;
}
std::vector<Zstring> makePortablePath(std::vector<Zstring> pathPhrases)
{
for (Zstring& pathPhrase : pathPhrases)
pathPhrase = makePortablePath(pathPhrase);
return pathPhrases;
}
std::vector<Zstring> resolvePortablePath(std::vector<Zstring> pathPhrases)
{
for (Zstring& pathPhrase : pathPhrases)
pathPhrase = resolvePortablePath(pathPhrase);
return pathPhrases;
}
}
namespace zen
{
template <> inline
bool readStruc(const XmlElement& input, ConfigFileItem& value)
{
bool success = true;
success = input.getAttribute("LastSync", value.lastRunStats.startTime) && success;
success = input.getAttribute("Result", value.lastRunStats.syncResult) && success;
if (input.hasAttribute("CfgPath")) //TODO: remove after migration! 2020-02-09
success = input.getAttribute("CfgPath", value.cfgFilePath) && success; //
else
success = input.getAttribute("Config", value.cfgFilePath) && success;
//FFS portable: use special syntax for config file paths: e.g. "%ffs_drive%\SyncJob.ffs_gui"
value.cfgFilePath = resolvePortablePath(value.cfgFilePath);
Zstring logFilePhrase;
if (input.hasAttribute("LogPath")) //TODO: remove after migration! 2020-02-09
success = input.getAttribute("LogPath", logFilePhrase) && success; //
else
success = input.getAttribute("Log", logFilePhrase) && success;
value.lastRunStats.logFilePath = createAbstractPath(resolvePortablePath(logFilePhrase));
if (!input.hasAttribute("Items")) //TODO: remove after migration! 2023-05-13
;
else
success = input.getAttribute("Items", value.lastRunStats.itemsProcessed) && success;
if (!input.hasAttribute("Bytes")) //TODO: remove after migration! 2023-05-13
;
else
success = input.getAttribute("Bytes", value.lastRunStats.bytesProcessed) && success;
if (!input.hasAttribute("TotalTime")) //TODO: remove after migration! 2023-05-13
;
else
success = input.getAttribute("TotalTime", value.lastRunStats.totalTime) && success;
if (!input.hasAttribute("Errors")) //TODO: remove after migration! 2023-05-13
;
else
success = input.getAttribute("Errors", value.lastRunStats.errors) && success;
if (!input.hasAttribute("Warnings")) //TODO: remove after migration! 2023-05-13
;
else
success = input.getAttribute("Warnings", value.lastRunStats.warnings) && success;
std::string hexColor; //optional XML attribute!
if (input.getAttribute("Color", hexColor) && hexColor.size() == 6)
value.backColor.Set(unhexify(hexColor[0], hexColor[1]),
unhexify(hexColor[2], hexColor[3]),
unhexify(hexColor[4], hexColor[5]));
return success; //[!] avoid short-circuit evaluation
}
template <> inline
void writeStruc(const ConfigFileItem& value, XmlElement& output)
{
output.setAttribute("LastSync", value.lastRunStats.startTime);
output.setAttribute("Result", value.lastRunStats.syncResult);
output.setAttribute("Config", makePortablePath(value.cfgFilePath));
output.setAttribute("Log", makePortablePath(AFS::getInitPathPhrase(value.lastRunStats.logFilePath)));
output.setAttribute("Items", value.lastRunStats.itemsProcessed);
output.setAttribute("Bytes", value.lastRunStats.bytesProcessed);
output.setAttribute("TotalTime", value.lastRunStats.totalTime);
output.setAttribute("Errors", value.lastRunStats.errors);
output.setAttribute("Warnings", value.lastRunStats.warnings);
if (value.backColor.IsOk())
{
assert(value.backColor.Alpha() == wxALPHA_OPAQUE);
const auto [rh, rl] = hexify(value.backColor.Red ());
const auto [gh, gl] = hexify(value.backColor.Green());
const auto [bh, bl] = hexify(value.backColor.Blue ());
output.setAttribute("Color", std::string({rh, rl, gh, gl, bh, bl}));
}
}
}
namespace
{
void readConfig(const XmlIn& in, CompConfig& cmpCfg)
{
in["Variant" ](cmpCfg.compareVar);
in["Symlinks"](cmpCfg.handleSymlinks);
std::wstring timeShiftPhrase;
if (in["IgnoreTimeShift"](timeShiftPhrase))
cmpCfg.ignoreTimeShiftMinutes = fromTimeShiftPhrase(timeShiftPhrase);
}
void readConfig(const XmlIn& in, SyncDirectionConfig& dirCfg, int formatVer)
{
if (formatVer < 21) //TODO: remove if parameter migration after some time! 2023-08-09
{
std::string varName;
in["Variant"](varName);
trim(varName);
if (varName == "TwoWay")
dirCfg = getDefaultSyncCfg(SyncVariant::twoWay);
else if (varName == "Mirror")
{
dirCfg = getDefaultSyncCfg(SyncVariant::mirror);
bool detectMovedFiles = false;
in["DetectMovedFiles"](detectMovedFiles);
if (detectMovedFiles)
{
if (const DirectionByDiff* diffDirs = std::get_if<DirectionByDiff>(&dirCfg.dirs))
dirCfg.dirs = getChangesDirDefault(*diffDirs); //convert to "changes"-based mirror, so that move detection is enabled
else assert(false);
}
}
else if (varName == "Update")
dirCfg.dirs = DirectionByDiff
{
.leftOnly = SyncDirection::right,
.rightOnly = SyncDirection::none,
.leftNewer = SyncDirection::right,
.rightNewer = SyncDirection::none, //note: will be fixed below for CompareVariant::content/size
};
else
{
assert(varName == "Custom");
dirCfg.dirs = DirectionByDiff();
XmlIn inCustDir = in["CustomDirections"];
inCustDir["LeftOnly" ](std::get<DirectionByDiff>(dirCfg.dirs).leftOnly);
inCustDir["RightOnly" ](std::get<DirectionByDiff>(dirCfg.dirs).rightOnly);
inCustDir["LeftNewer" ](std::get<DirectionByDiff>(dirCfg.dirs).leftNewer); //note: will be fixed below for CompareVariant::content/size
inCustDir["RightNewer"](std::get<DirectionByDiff>(dirCfg.dirs).rightNewer); //
}
}
else
{
if (XmlIn inDirs = in["Differences"])
{
dirCfg.dirs = DirectionByDiff();
inDirs.attribute("LeftOnly", std::get<DirectionByDiff>(dirCfg.dirs).leftOnly);
inDirs.attribute("LeftNewer", std::get<DirectionByDiff>(dirCfg.dirs).leftNewer);
inDirs.attribute("RightNewer", std::get<DirectionByDiff>(dirCfg.dirs).rightNewer);
inDirs.attribute("RightOnly", std::get<DirectionByDiff>(dirCfg.dirs).rightOnly);
}
else
{
assert(in["Changes"]);
dirCfg.dirs = DirectionByChange();
XmlIn inDirsL = in["Changes"]["Left"];
inDirsL.attribute("Create", std::get<DirectionByChange>(dirCfg.dirs).left.create);
inDirsL.attribute("Update", std::get<DirectionByChange>(dirCfg.dirs).left.update);
inDirsL.attribute("Delete", std::get<DirectionByChange>(dirCfg.dirs).left.delete_);
XmlIn inDirsR = in["Changes"]["Right"];
inDirsR.attribute("Create", std::get<DirectionByChange>(dirCfg.dirs).right.create);
inDirsR.attribute("Update", std::get<DirectionByChange>(dirCfg.dirs).right.update);
inDirsR.attribute("Delete", std::get<DirectionByChange>(dirCfg.dirs).right.delete_);
}
}
}
void readConfig(const XmlIn& in, SyncConfig& syncCfg, std::map<AfsDevice, size_t>& deviceParallelOps, int formatVer)
{
readConfig(in, syncCfg.directionCfg, formatVer);
in["DeletionPolicy" ](syncCfg.deletionVariant);
in["VersioningFolder"](syncCfg.versioningFolderPhrase);
XmlIn verFolder = in["VersioningFolder"];
size_t parallelOps = 1;
if (verFolder.hasAttribute("Threads")) //*no error* if not available
verFolder.attribute("Threads", parallelOps); //try to get attribute
const size_t parallelOpsPrev = getDeviceParallelOps(deviceParallelOps, syncCfg.versioningFolderPhrase);
/**/ setDeviceParallelOps(deviceParallelOps, syncCfg.versioningFolderPhrase, std::max(parallelOps, parallelOpsPrev));
in["VersioningFolder"].attribute("Style", syncCfg.versioningStyle);
if (syncCfg.versioningStyle != VersioningStyle::replace)
{
if (verFolder.hasAttribute("MaxAge")) //try to get attributes if available => *no error* if not available
verFolder.attribute("MaxAge", syncCfg.versionMaxAgeDays);
if (verFolder.hasAttribute("MinCount"))
verFolder.attribute("MinCount", syncCfg.versionCountMin); // => *no error* if not available
if (verFolder.hasAttribute("MaxCount"))
verFolder.attribute("MaxCount", syncCfg.versionCountMax); //
}
}
void readConfig(const XmlIn& in, FilterConfig& filter /*int formatVer? but which one; Filter is used by GlobalConfig and FfsGuiConfig! :( */)
{
std::vector<Zstring> tmpIn;
if (in["Include"](tmpIn)) //else: keep default value
filter.includeFilter = mergeFilterLines(tmpIn);
std::vector<Zstring> tmpEx;
if (in["Exclude"](tmpEx)) //else: keep default value
filter.excludeFilter = mergeFilterLines(tmpEx);
in["SizeMin"](filter.sizeMin);
in["SizeMin"].attribute("Unit", filter.unitSizeMin);
in["SizeMax"](filter.sizeMax);
in["SizeMax"].attribute("Unit", filter.unitSizeMax);
in["TimeSpan"](filter.timeSpan);
in["TimeSpan"].attribute("Type", filter.unitTimeSpan);
}
void readConfig(const XmlIn& in, LocalPairConfig& lpc, std::map<AfsDevice, size_t>& deviceParallelOps, int formatVer)
{
//read folder pairs
in["Left" ](lpc.folderPathPhraseLeft);
in["Right"](lpc.folderPathPhraseRight);
size_t parallelOpsL = 1;
size_t parallelOpsR = 1;
if (in["Left" ].hasAttribute("Threads")) in["Left" ].attribute("Threads", parallelOpsL); //try to get attributes:
if (in["Right"].hasAttribute("Threads")) in["Right"].attribute("Threads", parallelOpsR); // => *no error* if not available
auto setParallelOps = [&](const Zstring& folderPathPhrase, size_t parallelOps)
{
const size_t parallelOpsPrev = getDeviceParallelOps(deviceParallelOps, folderPathPhrase);
/**/ setDeviceParallelOps(deviceParallelOps, folderPathPhrase, std::max(parallelOps, parallelOpsPrev));
};
setParallelOps(lpc.folderPathPhraseLeft, parallelOpsL);
setParallelOps(lpc.folderPathPhraseRight, parallelOpsR);
//TODO: remove after migration! 2020-04-24
if (formatVer < 16)
{
replaceAsciiNoCase(lpc.folderPathPhraseLeft, Zstr("%weekday%"), Zstr("%WeekDayName%"));
replaceAsciiNoCase(lpc.folderPathPhraseRight, Zstr("%weekday%"), Zstr("%WeekDayName%"));
}
//###########################################################
//alternate comp configuration (optional)
if (XmlIn inLocalCmp = in["Compare"])
{
CompConfig cmpCfg;
readConfig(inLocalCmp, cmpCfg);
lpc.localCmpCfg = cmpCfg;
}
//###########################################################
//alternate sync configuration (optional)
if (XmlIn inLocalSync = in["Synchronize"])
{
SyncConfig syncCfg;
readConfig(inLocalSync, syncCfg, deviceParallelOps, formatVer);
lpc.localSyncCfg = syncCfg;
}
//###########################################################
//alternate filter configuration
if (XmlIn inLocFilter = in["Filter"])
readConfig(inLocFilter, lpc.localFilter);
}
void readConfig(const XmlIn& in, MainConfiguration& mainCfg, int formatVer)
{
readConfig(in["Compare"], mainCfg.cmpCfg);
readConfig(in["Synchronize"], mainCfg.syncCfg, mainCfg.deviceParallelOps, formatVer);
if (formatVer < 20) //TODO: remove if parameter migration after some time! 2023-08-09
if (mainCfg.cmpCfg.compareVar == CompareVariant::content ||
mainCfg.cmpCfg.compareVar == CompareVariant::size)
if (std::string varName;
in["Synchronize"]["Variant"](varName))
{
if (varName == "Update")
std::get<DirectionByDiff>(mainCfg.syncCfg.directionCfg.dirs).rightNewer = SyncDirection::right;
else if (varName == "Custom")
{
SyncDirection different = SyncDirection::none;
in["Synchronize"]["CustomDirections"]["Different"](different);
std::get<DirectionByDiff>(mainCfg.syncCfg.directionCfg.dirs).leftNewer =
std::get<DirectionByDiff>(mainCfg.syncCfg.directionCfg.dirs).rightNewer = different;
}
}
if (formatVer < 23) //TODO: remove if parameter migration after some time! 2023-08-24
{
bool detectMovedFiles = false;
in["Synchronize"]["DetectMovedFiles"](detectMovedFiles);
if (detectMovedFiles)
if (getSyncVariant(mainCfg.syncCfg.directionCfg) == SyncVariant::mirror)
{
if (const DirectionByDiff* diffDirs = std::get_if<DirectionByDiff>(&mainCfg.syncCfg.directionCfg.dirs))
mainCfg.syncCfg.directionCfg.dirs = getChangesDirDefault(*diffDirs); //convert to "changes"-based mirror, so that move detection is enabled
else assert(false);
}
}
readConfig(in["Filter"], mainCfg.globalFilter);
//###########################################################
//read folder pairs
bool firstItem = true;
in["FolderPairs"].visitChildren([&](const XmlIn& inPair)
{
assert(*inPair.getName() == "Pair");
LocalPairConfig lpc;
readConfig(inPair, lpc, mainCfg.deviceParallelOps, formatVer);
if (formatVer < 20) //TODO: remove if parameter migration after some time! 2023-08-09
if (lpc.localSyncCfg)
{
const CompConfig& cmpCfg = lpc.localCmpCfg ? *lpc.localCmpCfg : mainCfg.cmpCfg;
if (cmpCfg.compareVar == CompareVariant::content ||
cmpCfg.compareVar == CompareVariant::size)
if (std::string varName;
inPair["Synchronize"]["Variant"](varName))
{
if (varName == "Update")
std::get<DirectionByDiff>(lpc.localSyncCfg->directionCfg.dirs).rightNewer = SyncDirection::right;
else if (varName == "Custom")
if (inPair["Synchronize"]["CustomDirections"]["Different"])
{
SyncDirection different = SyncDirection::none;
inPair["Synchronize"]["CustomDirections"]["Different"](different);
std::get<DirectionByDiff>(lpc.localSyncCfg->directionCfg.dirs).leftNewer =
std::get<DirectionByDiff>(lpc.localSyncCfg->directionCfg.dirs).rightNewer = different;
}
}
}
if (formatVer < 23) //TODO: remove if parameter migration after some time! 2023-08-24
if (lpc.localSyncCfg)
{
bool detectMovedFiles = false;
inPair["Synchronize"]["DetectMovedFiles"](detectMovedFiles);
if (detectMovedFiles)
if (getSyncVariant(lpc.localSyncCfg->directionCfg) == SyncVariant::mirror)
{
if (const DirectionByDiff* diffDirs = std::get_if<DirectionByDiff>(&lpc.localSyncCfg->directionCfg.dirs))
lpc.localSyncCfg->directionCfg.dirs = getChangesDirDefault(*diffDirs); //convert to "changes"-based mirror, so that move detection is enabled
else assert(false);
}
}
if (firstItem)
{
firstItem = false;
mainCfg.firstPair = lpc;
mainCfg.additionalPairs.clear();
}
else
mainCfg.additionalPairs.push_back(lpc);
});
in["Errors"].attribute("Ignore", mainCfg.ignoreErrors);
in["Errors"].attribute("Retry", mainCfg.autoRetryCount);
in["Errors"].attribute("Delay", mainCfg.autoRetryDelay);
in["PostSyncCommand"](mainCfg.postSyncCommand);
in["PostSyncCommand"].attribute("Condition", mainCfg.postSyncCondition);
in["LogFolder"](mainCfg.altLogFolderPathPhrase);
//TODO: remove after migration! 2020-04-24
if (formatVer < 16)
replaceAsciiNoCase(mainCfg.altLogFolderPathPhrase, Zstr("%weekday%"), Zstr("%WeekDayName%"));
//TODO: remove if parameter migration after some time! 2020-01-30
if (formatVer < 15)
;
else
{
in["EmailNotification"](mainCfg.emailNotifyAddress);
in["EmailNotification"].attribute("Condition", mainCfg.emailNotifyCondition);
}
}
void readConfig(const XmlIn& in, FfsGuiConfig& cfg, int formatVer)
{
if (formatVer < 18) //TODO: remove if parameter migration after some time! 2023-05-15
;
else
in["Notes"](cfg.notes);
readConfig(in, cfg.mainCfg, formatVer);
if (formatVer < 19) //TODO: remove after migration! 2023-06-09
{
XmlIn inGui = in["Gui"];
//TODO: remove after migration! 2020-10-14
if (formatVer < 17)
{
if (inGui["MiddleGridView"])
{
std::string tmp;
inGui["MiddleGridView"](tmp);
if (tmp == "Category")
cfg.gridViewType = GridViewType::difference;
else if (tmp == "Action")
cfg.gridViewType = GridViewType::action;
}
}
else
inGui["GridViewType"](cfg.gridViewType);
}
else
in["GridViewType"](cfg.gridViewType);
}
void readConfig(const XmlIn& in, FfsBatchConfig& cfg, int formatVer)
{
if (formatVer < 19) //TODO: remove after migration! 2023-06-09
readConfig(in, cfg.guiCfg.mainCfg, formatVer);
else
readConfig(in, cfg.guiCfg, formatVer);
XmlIn inBatch = in["Batch"];
inBatch["ProgressDialog"].attribute("Minimized", cfg.batchExCfg.runMinimized);
inBatch["ProgressDialog"].attribute("AutoClose", cfg.batchExCfg.autoCloseSummary);
inBatch["ErrorDialog"](cfg.batchExCfg.batchErrorHandling);
inBatch["PostSyncAction"](cfg.batchExCfg.postBatchAction);
}
void readConfig(const XmlIn& in, GlobalConfig& cfg, int formatVer)
{
assert(cfg.dpiLayouts.empty());
XmlIn in2 = in;
if (in["General"]) //TODO: remove old parameter after migration! 2020-12-03
in2 = in["General"];
//TODO: remove after migration! 2022-04-18
if (in2["Language"].hasAttribute("Name"))
{
std::string lngName;
in2["Language"].attribute("Name", lngName);
if (lngName == "English (US)")
cfg.programLanguage = wxLANGUAGE_ENGLISH_US;
else if (lngName == "Chinese (Simplified)")
cfg.programLanguage = wxLANGUAGE_CHINESE_CHINA;
else if (lngName == "Chinese (Traditional)")
cfg.programLanguage = wxLANGUAGE_CHINESE_TAIWAN;
else if (lngName == "English (U.K.)")
cfg.programLanguage = wxLANGUAGE_ENGLISH_UK;
else if (lngName == "Norwegian (Bokmal)")
cfg.programLanguage = wxLANGUAGE_NORWEGIAN;
else if (lngName == "Portuguese (Brazilian)")
cfg.programLanguage = wxLANGUAGE_PORTUGUESE_BRAZILIAN;
else if (const wxLanguageInfo* lngInfo = wxUILocale::FindLanguageInfo(utfTo<wxString>(lngName)))
cfg.programLanguage = static_cast<wxLanguage>(lngInfo->Language);
}
else
in2["Language"].attribute("Code", cfg.programLanguage);
in2["ColorTheme"].attribute("Appearance", cfg.appColorTheme);
in2["FailSafeFileCopy" ].attribute("Enabled", cfg.failSafeFileCopy);
in2["CopyLockedFiles" ].attribute("Enabled", cfg.copyLockedFiles);
in2["CopyFilePermissions" ].attribute("Enabled", cfg.copyFilePermissions);
in2["FileTimeTolerance" ].attribute("Seconds", cfg.fileTimeTolerance);
in2["RunWithBackgroundPriority"].attribute("Enabled", cfg.runWithBackgroundPriority);
in2["LockDirectoriesDuringSync"].attribute("Enabled", cfg.createLockFile);
in2["VerifyCopiedFiles" ].attribute("Enabled", cfg.verifyFileCopy);
in2["LogFiles" ].attribute("MaxAge", cfg.logfilesMaxAgeDays);
in2["LogFiles" ].attribute("Format", cfg.logFormat);
//TODO: remove old parameter after migration! 2021-03-06
if (formatVer < 21)
{
cfg.dpiLayouts[getDpiScalePercent()].progressDlg.size = wxSize();
in2["ProgressDialog"].attribute("Width", cfg.dpiLayouts[getDpiScalePercent()].progressDlg.size->x);
in2["ProgressDialog"].attribute("Height", cfg.dpiLayouts[getDpiScalePercent()].progressDlg.size->y);
in2["ProgressDialog"].attribute("Maximized", cfg.dpiLayouts[getDpiScalePercent()].progressDlg.isMaximized);
}
in2["ProgressDialog"].attribute("AutoClose", cfg.progressDlgAutoClose);
XmlIn inOpt = in2["OptionalDialogs"];
inOpt["ConfirmStartSync" ].attribute("Show", cfg.confirmDlgs.confirmSyncStart);
inOpt["ConfirmSaveConfig" ].attribute("Show", cfg.confirmDlgs.confirmSaveConfig);
inOpt["ConfirmSwapSides" ].attribute("Show", cfg.confirmDlgs.confirmSwapSides);
if (formatVer < 12) //TODO: remove old parameter after migration! 2019-02-09
inOpt["ConfirmExternalCommandMassInvoke"].attribute("Show", cfg.confirmDlgs.confirmCommandMassInvoke);
else
inOpt["ConfirmCommandMassInvoke"].attribute("Show", cfg.confirmDlgs.confirmCommandMassInvoke);
inOpt["WarnFolderNotExisting" ].attribute("Show", cfg.warnDlgs.warnFolderNotExisting);
inOpt["WarnFoldersDifferInCase" ].attribute("Show", cfg.warnDlgs.warnFoldersDifferInCase);
inOpt["WarnUnresolvedConflicts" ].attribute("Show", cfg.warnDlgs.warnUnresolvedConflicts);
inOpt["WarnNotEnoughDiskSpace" ].attribute("Show", cfg.warnDlgs.warnNotEnoughDiskSpace);
inOpt["WarnSignificantDifference" ].attribute("Show", cfg.warnDlgs.warnSignificantDifference);
inOpt["WarnRecycleBinNotAvailable" ].attribute("Show", cfg.warnDlgs.warnRecyclerMissing);
inOpt["WarnDependentFolderPair" ].attribute("Show", cfg.warnDlgs.warnDependentFolderPair);
inOpt["WarnDependentBaseFolders" ].attribute("Show", cfg.warnDlgs.warnDependentBaseFolders);
inOpt["WarnDirectoryLockFailed" ].attribute("Show", cfg.warnDlgs.warnDirectoryLockFailed);
inOpt["WarnVersioningFolderPartOfSync"].attribute("Show", cfg.warnDlgs.warnVersioningFolderPartOfSync);
//TODO: remove after migration! 2022-08-26
if (formatVer < 25)
cfg.warnDlgs.warnDependentBaseFolders = true; //new semantics! should not be ignored
//TODO: remove after migration! 2021-12-02
if (formatVer < 23)
{
in2["NotificationSound"].attribute("CompareFinished", cfg.soundFileCompareFinished);
in2["NotificationSound"].attribute("SyncFinished", cfg.soundFileSyncFinished);
}
else
{
in2["Sounds"]["CompareFinished"].attribute("Path", cfg.soundFileCompareFinished);
in2["Sounds"]["SyncFinished" ].attribute("Path", cfg.soundFileSyncFinished);
in2["Sounds"]["AlertPending" ].attribute("Path", cfg.soundFileAlertPending);
}
//TODO: remove if parameter migration after some time! 2019-05-29
if (formatVer < 13)
{
if (!cfg.soundFileCompareFinished.empty()) cfg.soundFileCompareFinished = appendPath(getResourceDirPath(), cfg.soundFileCompareFinished);
if (!cfg.soundFileSyncFinished .empty()) cfg.soundFileSyncFinished = appendPath(getResourceDirPath(), cfg.soundFileSyncFinished);
}
else
{
cfg.soundFileCompareFinished = resolvePortablePath(cfg.soundFileCompareFinished);
cfg.soundFileSyncFinished = resolvePortablePath(cfg.soundFileSyncFinished);
cfg.soundFileAlertPending = resolvePortablePath(cfg.soundFileAlertPending);
}
XmlIn inMainWin = in["MainDialog"];
//TODO: remove old parameter after migration! 2020-12-03
if (in["Gui"])
inMainWin = in["Gui"]["MainDialog"];
//TODO: remove old parameter after migration! 2021-03-06
if (formatVer < 21)
{
cfg.dpiLayouts[getDpiScalePercent()].mainDlg.size = wxSize();
inMainWin.attribute("Width", cfg.dpiLayouts[getDpiScalePercent()].mainDlg.size->x);
inMainWin.attribute("Height", cfg.dpiLayouts[getDpiScalePercent()].mainDlg.size->y);
cfg.dpiLayouts[getDpiScalePercent()].mainDlg.pos = wxPoint();
inMainWin.attribute("PosX", cfg.dpiLayouts[getDpiScalePercent()].mainDlg.pos->x);
inMainWin.attribute("PosY", cfg.dpiLayouts[getDpiScalePercent()].mainDlg.pos->y);
inMainWin.attribute("Maximized", cfg.dpiLayouts[getDpiScalePercent()].mainDlg.isMaximized);
}
//###########################################################
inMainWin["SearchPanel"].attribute("CaseSensitive", cfg.mainDlg.textSearchRespectCase);
//###########################################################
XmlIn inConfig = inMainWin["ConfigPanel"];
inConfig.attribute("ScrollPos", cfg.mainDlg.config.topRowPos);
inConfig.attribute("SyncOverdue", cfg.mainDlg.config.syncOverdueDays);
inConfig.attribute("SortByColumn", cfg.mainDlg.config.lastSortColumn);
inConfig.attribute("SortAscending", cfg.mainDlg.config.lastSortAscending);
//TODO: remove old parameter after migration! 2021-03-06
if (formatVer < 21)
inConfig["Columns"](cfg.dpiLayouts[getDpiScalePercent()].configColumnAttribs);
inConfig["Configurations"].attribute("MaxSize", cfg.mainDlg.config.histItemsMax);
inConfig["Configurations"].attribute("LastSelected", cfg.mainDlg.config.lastSelectedFile);
cfg.mainDlg.config.lastSelectedFile = resolvePortablePath(cfg.mainDlg.config.lastSelectedFile);
inConfig["Configurations"](cfg.mainDlg.config.fileHistory);
//TODO: remove after migration! 2019-11-30
if (formatVer < 15)
{
const Zstring lastRunConfigPath = appendPath(getConfigDirPath(), Zstr("LastRun.ffs_gui"));
for (ConfigFileItem& item : cfg.mainDlg.config.fileHistory)
if (equalNativePath(item.cfgFilePath, lastRunConfigPath))
item.backColor = wxColor(0xdd, 0xdd, 0xdd); //light grey from onCfgGridContext()
}
inConfig["LastUsed"](cfg.mainDlg.config.lastUsedFiles);
cfg.mainDlg.config.lastUsedFiles = resolvePortablePath(cfg.mainDlg.config.lastUsedFiles);
//###########################################################
XmlIn inOverview = inMainWin["OverviewPanel"];
inOverview.attribute("ShowPercentage", cfg.mainDlg.overview.showPercentBar);
inOverview.attribute("SortByColumn", cfg.mainDlg.overview.lastSortColumn);
inOverview.attribute("SortAscending", cfg.mainDlg.overview.lastSortAscending);
//TODO: remove old parameter after migration! 2021-03-06
if (formatVer < 21)
inOverview["Columns"](cfg.dpiLayouts[getDpiScalePercent()].overviewColumnAttribs);
XmlIn inFilePanel = inMainWin["FilePanel"];
//TODO: remove after migration! 2020-10-13
if (formatVer < 19)
; //new icon layout => let user re-evaluate settings
else
{
inFilePanel.attribute("ShowIcons", cfg.mainDlg.showIcons);
inFilePanel.attribute("IconSize", cfg.mainDlg.iconSize);
}
inFilePanel.attribute("SashOffset", cfg.mainDlg.sashOffset);
//TODO: remove if parameter migration after some time! 2020-01-30
if (formatVer < 16)
inFilePanel.attribute("MaxFolderPairsShown", cfg.mainDlg.folderPairsVisibleMax);
else
inFilePanel.attribute("FolderPairsMax", cfg.mainDlg.folderPairsVisibleMax);
//TODO: remove old parameter after migration! 2021-03-06
if (formatVer < 21)
{
inFilePanel["ColumnsLeft" ](cfg.dpiLayouts[getDpiScalePercent()].fileColumnAttribsLeft);
inFilePanel["ColumnsRight"](cfg.dpiLayouts[getDpiScalePercent()].fileColumnAttribsRight);
inFilePanel["ColumnsLeft" ].attribute("PathFormat", cfg.mainDlg.itemPathFormatLeftGrid);
inFilePanel["ColumnsRight"].attribute("PathFormat", cfg.mainDlg.itemPathFormatRightGrid);
}
else
{
inFilePanel.attribute("PathFormatLeft", cfg.mainDlg.itemPathFormatLeftGrid);
inFilePanel.attribute("PathFormatRight", cfg.mainDlg.itemPathFormatRightGrid);
}
inFilePanel["FolderHistoryLeft" ](cfg.mainDlg.folderHistoryLeft);
inFilePanel["FolderHistoryRight"](cfg.mainDlg.folderHistoryRight);
cfg.mainDlg.folderHistoryLeft = resolvePortablePath(cfg.mainDlg.folderHistoryLeft);
cfg.mainDlg.folderHistoryRight = resolvePortablePath(cfg.mainDlg.folderHistoryRight);
inFilePanel["FolderHistoryLeft" ].attribute("LastSelected", cfg.mainDlg.folderLastSelectedLeft);
inFilePanel["FolderHistoryRight"].attribute("LastSelected", cfg.mainDlg.folderLastSelectedRight);
cfg.mainDlg.folderLastSelectedLeft = resolvePortablePath(cfg.mainDlg.folderLastSelectedLeft);
cfg.mainDlg.folderLastSelectedRight = resolvePortablePath(cfg.mainDlg.folderLastSelectedRight);
//###########################################################
XmlIn inCopyTo = inMainWin["ManualCopyTo"];
inCopyTo.attribute("KeepRelativePaths", cfg.mainDlg.copyToCfg.keepRelPaths);
inCopyTo.attribute("OverwriteIfExists", cfg.mainDlg.copyToCfg.overwriteIfExists);
XmlIn inCopyToHistory = inCopyTo["FolderHistory"];
inCopyToHistory(cfg.mainDlg.copyToCfg.folderHistory);
inCopyToHistory.attribute("TargetFolder", cfg.mainDlg.copyToCfg.targetFolderPath);
inCopyToHistory.attribute("LastSelected", cfg.mainDlg.copyToCfg.targetFolderLastSelected);
cfg.mainDlg.copyToCfg.folderHistory = resolvePortablePath(cfg.mainDlg.copyToCfg.folderHistory);
cfg.mainDlg.copyToCfg.targetFolderPath = resolvePortablePath(cfg.mainDlg.copyToCfg.targetFolderPath);
cfg.mainDlg.copyToCfg.targetFolderLastSelected = resolvePortablePath(cfg.mainDlg.copyToCfg.targetFolderLastSelected);
//###########################################################
XmlIn inDefFilter = inMainWin["DefaultViewFilter"];
inDefFilter.attribute("Equal", cfg.mainDlg.viewFilterDefault.equal);
inDefFilter.attribute("Conflict", cfg.mainDlg.viewFilterDefault.conflict);
inDefFilter.attribute("Excluded", cfg.mainDlg.viewFilterDefault.excluded);
XmlIn diffView = inDefFilter["Difference"];
//TODO: remove after migration! 2020-10-13
if (formatVer < 19)
diffView = inDefFilter["CategoryView"];
diffView.attribute("LeftOnly", cfg.mainDlg.viewFilterDefault.leftOnly);
diffView.attribute("RightOnly", cfg.mainDlg.viewFilterDefault.rightOnly);
diffView.attribute("LeftNewer", cfg.mainDlg.viewFilterDefault.leftNewer);
diffView.attribute("RightNewer", cfg.mainDlg.viewFilterDefault.rightNewer);
diffView.attribute("Different", cfg.mainDlg.viewFilterDefault.different);
XmlIn actView = inDefFilter["Action"];
//TODO: remove after migration! 2020-10-13
if (formatVer < 19)
actView = inDefFilter["ActionView"];
actView.attribute("CreateLeft", cfg.mainDlg.viewFilterDefault.createLeft);
actView.attribute("CreateRight", cfg.mainDlg.viewFilterDefault.createRight);
actView.attribute("UpdateLeft", cfg.mainDlg.viewFilterDefault.updateLeft);
actView.attribute("UpdateRight", cfg.mainDlg.viewFilterDefault.updateRight);
actView.attribute("DeleteLeft", cfg.mainDlg.viewFilterDefault.deleteLeft);
actView.attribute("DeleteRight", cfg.mainDlg.viewFilterDefault.deleteRight);
actView.attribute("DoNothing", cfg.mainDlg.viewFilterDefault.doNothing);
//TODO: remove old parameter after migration! 2021-03-06
if (formatVer < 21)
inMainWin["Perspective"](cfg.dpiLayouts[getDpiScalePercent()].panelLayout);
//TODO: remove after migration! 2019-11-30
auto splitEditMerge = [](wxString& perspective, wchar_t delim, const std::function<void(wxString& item)>& editItem)
{
std::vector<wxString> v = splitCpy(perspective, delim, SplitOnEmpty::allow);
assert(!v.empty());
perspective.clear();
std::for_each(v.begin(), v.end() - 1, [&](wxString& item)
{
editItem(item);
perspective += item;
perspective += delim;
});
editItem(v.back());
perspective += v.back();
};
//TODO: remove after migration! 2019-11-30
if (formatVer < 15)
{
//set minimal TopPanel height => search and set actual height to 0 and let MainDialog's min-size handling kick in:
std::optional<int> tpDir;
std::optional<int> tpLayer;
std::optional<int> tpRow;
splitEditMerge(cfg.dpiLayouts[getDpiScalePercent()].panelLayout, L'|', [&](wxString& paneCfg)
{
if (contains(paneCfg, L"name=TopPanel"))
splitEditMerge(paneCfg, L';', [&](wxString& paneAttr)
{
if (startsWith(paneAttr, L"dir="))
tpDir = stringTo<int>(afterFirst(paneAttr, L'=', IfNotFoundReturn::none));
else if (startsWith(paneAttr, L"layer="))
tpLayer = stringTo<int>(afterFirst(paneAttr, L'=', IfNotFoundReturn::none));
else if (startsWith(paneAttr, L"row="))
tpRow = stringTo<int>(afterFirst(paneAttr, L'=', IfNotFoundReturn::none));
});
});
if (tpDir && tpLayer && tpRow)
{
const wxString tpSize = L"dock_size(" +
numberTo<wxString>(*tpDir ) + L"," +
numberTo<wxString>(*tpLayer) + L"," +
numberTo<wxString>(*tpRow ) + L")=";
splitEditMerge(cfg.dpiLayouts[getDpiScalePercent()].panelLayout, L'|', [&](wxString& paneCfg)
{
if (startsWith(paneCfg, tpSize))
paneCfg = tpSize + L"0";
});
}
}
//TODO: remove if parameter migration after some time! 2020-01-30
if (formatVer < 16)
;
else if (formatVer < 20) //TODO: remove old parameter after migration! 2020-12-03
in["Gui"]["FolderHistory" ].attribute("MaxSize", cfg.folderHistoryMax);
else
in["FolderHistory" ].attribute("MaxSize", cfg.folderHistoryMax);
if (formatVer < 20) //TODO: remove old parameter after migration! 2020-12-03
{
in["Gui"]["SftpKeyFile"].attribute("LastSelected", cfg.sftpKeyFileLastSelected);
}
else
{
in["SftpKeyFile"].attribute("LastSelected", cfg.sftpKeyFileLastSelected);
cfg.sftpKeyFileLastSelected = resolvePortablePath(cfg.sftpKeyFileLastSelected);
}
if (formatVer < 22) //TODO: remove old parameter after migration! 2021-07-31
{
}
else
readConfig(in["DefaultFilter"], cfg.defaultFilter);
if (formatVer < 20) //TODO: remove old parameter after migration! 2020-12-03
{
in["Gui"]["VersioningFolderHistory"](cfg.versioningFolderHistory);
in["Gui"]["VersioningFolderHistory"].attribute("LastSelected", cfg.versioningFolderLastSelected);
}
else
{
in["VersioningFolderHistory"](cfg.versioningFolderHistory);
in["VersioningFolderHistory"].attribute("LastSelected", cfg.versioningFolderLastSelected);
cfg.versioningFolderLastSelected = resolvePortablePath(cfg.versioningFolderLastSelected);
}
in["LogFolder"](cfg.logFolderPhrase);
cfg.logFolderPhrase = resolvePortablePath(cfg.logFolderPhrase);
if (formatVer < 20) //TODO: remove old parameter after migration! 2020-12-03
{
in["Gui"]["LogFolderHistory"](cfg.logFolderHistory);
in["Gui"]["LogFolderHistory"].attribute("LastSelected", cfg.logFolderLastSelected);
}
else
{
in["LogFolderHistory"](cfg.logFolderHistory);
in["LogFolderHistory"].attribute("LastSelected", cfg.logFolderLastSelected);
cfg.logFolderHistory = resolvePortablePath(cfg.logFolderHistory);
cfg.logFolderLastSelected = resolvePortablePath(cfg.logFolderLastSelected);
}
if (formatVer < 20) //TODO: remove old parameter after migration! 2020-12-03
{
in["Gui"]["EmailHistory"](cfg.emailHistory);
in["Gui"]["EmailHistory"].attribute("MaxSize", cfg.emailHistoryMax);
}
else
{
in["EmailHistory"](cfg.emailHistory);
in["EmailHistory"].attribute("MaxSize", cfg.emailHistoryMax);
}
if (formatVer < 20) //TODO: remove old parameter after migration! 2020-12-03
{
in["Gui"]["CommandHistory"](cfg.commandHistory);
in["Gui"]["CommandHistory"].attribute("MaxSize", cfg.commandHistoryMax);
}
else
{
in["CommandHistory"](cfg.commandHistory);
in["CommandHistory"].attribute("MaxSize", cfg.commandHistoryMax);
}
//TODO: remove if parameter migration after some time! 2020-01-30
if (formatVer < 15)
if (cfg.commandHistoryMax <= 8)
cfg.commandHistoryMax = GlobalConfig().commandHistoryMax;
if (formatVer < 20) //TODO: remove old parameter after migration! 2020-12-03
in["Gui"]["ExternalApps"](cfg.externalApps);
else
in["ExternalApps"](cfg.externalApps);
//TODO: remove after migration! 2019-11-30
if (formatVer < 15)
for (ExternalApp& item : cfg.externalApps)
{
replace(item.cmdLine, Zstr("%folder_path%"), Zstr("%parent_path%"));
replace(item.cmdLine, Zstr("%folder_path2%"), Zstr("%parent_path2%"));
}
//TODO: remove after migration! 2020-06-13
if (formatVer < 18)
for (ExternalApp& item : cfg.externalApps)
{
trim(item.cmdLine);
if (item.cmdLine == "xdg-open \"%parent_path%\"")
item.cmdLine = "xdg-open \"$(dirname %local_path%)\"";
}
//TODO: remove after migration! 2022-04-29
if (formatVer < 24)
for (ExternalApp& item : cfg.externalApps)
if (item.description == L"Browse directory")
item.description = L"Show in file manager";
//TODO: remove after migration! 2025-09-25
if (formatVer < 28)
for (ExternalApp& item : cfg.externalApps)
{
trim(item.cmdLine);
auto removeQuotes = [&](const ZstringView macroName) { replace(item.cmdLine, Zstring() + Zstr('"') + macroName + Zstr('"'), macroName); };
removeQuotes(Zstr("%item_path%"));
removeQuotes(Zstr("%item_path2%"));
removeQuotes(Zstr("%item_paths%"));
removeQuotes(Zstr("%local_path%"));
removeQuotes(Zstr("%local_path2%"));
removeQuotes(Zstr("%local_paths%"));
removeQuotes(Zstr("%item_name%"));
removeQuotes(Zstr("%item_name2%"));
removeQuotes(Zstr("%item_names%"));
removeQuotes(Zstr("%parent_path%"));
removeQuotes(Zstr("%parent_path2%"));
removeQuotes(Zstr("%parent_paths%"));
}
if (formatVer < 20) //TODO: remove old parameter after migration! 2020-12-03
{
in["Gui"]["LastOnlineCheck" ](cfg.lastUpdateCheck);
in["Gui"]["LastOnlineVersion"](cfg.lastOnlineVersion);
}
else
{
in["LastOnlineCheck" ](cfg.lastUpdateCheck);
in["LastOnlineVersion"](cfg.lastOnlineVersion);
}
in["WelcomeDialogVersion"](cfg.welcomeDialogLastVersion);
//cfg.dpiLayouts.clear(); -> NO: honor migration code above!
in["DpiLayouts"].visitChildren([&](const XmlIn& inLayout)
{
assert(*inLayout.getName() == "Layout");
if (std::string scaleTxt;
inLayout.attribute("Scale", scaleTxt))
{
const int scalePercent = stringTo<int>(beforeLast(scaleTxt, '%', IfNotFoundReturn::none));
DpiLayout layout;
//TODO: remove parameter migration after some time! 2023-02-18
if (formatVer < 26)
{
XmlIn inLayoutMain = inLayout["MainDialog"];
layout.mainDlg.size = wxSize();
inLayoutMain.attribute("Width", layout.mainDlg.size->x);
inLayoutMain.attribute("Height", layout.mainDlg.size->y);
layout.mainDlg.pos = wxPoint();
inLayoutMain.attribute("PosX", layout.mainDlg.pos->x);
inLayoutMain.attribute("PosY", layout.mainDlg.pos->y);
inLayoutMain.attribute("Maximized", layout.mainDlg.isMaximized);
inLayoutMain["PanelLayout" ](layout.panelLayout);
inLayoutMain["ConfigPanel" ](layout.configColumnAttribs);
inLayoutMain["OverviewPanel" ](layout.overviewColumnAttribs);
inLayoutMain["FilePanelLeft" ](layout.fileColumnAttribsLeft);
inLayoutMain["FilePanelRight"](layout.fileColumnAttribsRight);
XmlIn inLayoutProgress = inLayout["ProgressDialog"];
layout.progressDlg.size = wxSize();
inLayoutProgress.attribute("Width", layout.progressDlg.size->x);
inLayoutProgress.attribute("Height", layout.progressDlg.size->y);
inLayoutProgress.attribute("Maximized", layout.progressDlg.isMaximized);
}
else
{
XmlIn inLayoutMain = inLayout["MainWindow"];
if (inLayoutMain.hasAttribute("Width") &&
inLayoutMain.hasAttribute("Height"))
{
layout.mainDlg.size = wxSize();
inLayoutMain.attribute("Width", layout.mainDlg.size->x);
inLayoutMain.attribute("Height", layout.mainDlg.size->y);
}
if (inLayoutMain.hasAttribute("PosX") &&
inLayoutMain.hasAttribute("PosY"))
{
layout.mainDlg.pos = wxPoint();
inLayoutMain.attribute("PosX", layout.mainDlg.pos->x);
inLayoutMain.attribute("PosY", layout.mainDlg.pos->y);
}
inLayoutMain.attribute("Maximized", layout.mainDlg.isMaximized);
XmlIn inLayoutProgress = inLayout["ProgressDialog"];
if (inLayoutProgress.hasAttribute("Width") &&
inLayoutProgress.hasAttribute("Height"))
{
layout.progressDlg.size = wxSize();
inLayoutProgress.attribute("Width", layout.progressDlg.size->x);
inLayoutProgress.attribute("Height", layout.progressDlg.size->y);
}
inLayoutProgress.attribute("Maximized", layout.progressDlg.isMaximized);
inLayout["Panels" ](layout.panelLayout);
inLayout["ConfigPanel" ](layout.configColumnAttribs);
inLayout["OverviewPanel" ](layout.overviewColumnAttribs);
inLayout["FilePanelLeft" ](layout.fileColumnAttribsLeft);
inLayout["FilePanelRight"](layout.fileColumnAttribsRight);
}
cfg.dpiLayouts.emplace(scalePercent, std::move(layout));
}
});
}
template <class ConfigType>
std::pair<ConfigType, std::wstring /*warningMsg*/> parseConfig(const XmlDoc& doc, const Zstring& filePath, int currentXmlFormatVer) //noexcept
{
int formatVer = 0;
/*bool success =*/ doc.root().getAttribute("XmlFormat", formatVer);
XmlIn in(doc);
ConfigType cfg;
readConfig(in, cfg, formatVer);
std::wstring warningMsg;
if (const std::wstring& errors = in.getErrors();
!errors.empty())
warningMsg = replaceCpy(_("Configuration file %x is incomplete. The missing elements have been set to their default values."), L"%x", fmtPath(filePath)) + L"\n\n" +
_("The following XML elements could not be read:") + L'\n' + errors;
else //(try to) migrate old configuration if needed
if (formatVer < currentXmlFormatVer)
try
{
fff::writeConfig(cfg, filePath); //throw FileError
}
catch (const FileError& e) { warningMsg = e.toString(); }
return {cfg, warningMsg};
}
template <class ConfigType>
std::pair<ConfigType, std::wstring /*warningMsg*/> readConfig(const Zstring& filePath, const char* expectedCfgType, int currentXmlFormatVer) //throw FileError
{
XmlDoc doc = loadXml(filePath); //throw FileError
const std::string cfgType = [&]
{
if (doc.root().getName() == "FreeFileSync")
{
std::string type;
if (doc.root().getAttribute("XmlType", type))
return type;
}
return std::string();
}();
if (cfgType != expectedCfgType)
throw FileError(replaceCpy(_("File %x does not contain a valid configuration."), L"%x", fmtPath(filePath)));
return parseConfig<ConfigType>(doc, filePath, currentXmlFormatVer);
}
}
std::pair<FfsGuiConfig, std::wstring /*warningMsg*/> fff::readGuiConfig(const Zstring& filePath)
{
return readConfig<FfsGuiConfig>(filePath, "GUI", XML_FORMAT_SYNC_CFG); //throw FileError
}
std::pair<FfsBatchConfig, std::wstring /*warningMsg*/> fff::readBatchConfig(const Zstring& filePath)
{
return readConfig<FfsBatchConfig>(filePath, "BATCH", XML_FORMAT_SYNC_CFG); //throw FileError
}
std::pair<GlobalConfig, std::wstring /*warningMsg*/> fff::readGlobalConfig(const Zstring& filePath)
{
return readConfig<GlobalConfig>(filePath, "GLOBAL", XML_FORMAT_GLOBAL_CFG); //throw FileError
}
std::pair<FfsGuiConfig, std::wstring /*warningMsg*/> fff::readAnyConfig(const std::vector<Zstring>& filePaths) //throw FileError
{
assert(!filePaths.empty());
std::wstring warningMsgAll;
std::vector<FfsGuiConfig> guiCfgs;
for (const Zstring& filePath : filePaths)
if (endsWithAsciiNoCase(filePath, Zstr(".ffs_gui")))
{
const auto& [guiCfg, warningMsg] = readGuiConfig(filePath); //throw FileError
guiCfgs.push_back(guiCfg);
if (!warningMsg.empty())
warningMsgAll += warningMsg + L"\n\n";
}
else if (endsWithAsciiNoCase(filePath, Zstr(".ffs_batch")))
{
const auto& [batchCfg, warningMsg] = readBatchConfig(filePath); //throw FileError
guiCfgs.push_back(batchCfg.guiCfg);
if (!warningMsg.empty())
warningMsgAll += warningMsg + L"\n\n";
}
else
throw FileError(replaceCpy(_("Cannot open file %x."), L"%x", fmtPath(filePath)),
_("Unexpected file extension:") + L' ' + fmtPath(getFileExtension(filePath)) + L'\n' +
_("Expected:") + L" ffs_gui, ffs_batch");
return {merge(guiCfgs), trimCpy(warningMsgAll)};
}
//################################################################################################
namespace
{
void writeConfig(const CompConfig& cmpCfg, XmlOut& out)
{
out["Variant" ](cmpCfg.compareVar);
out["Symlinks"](cmpCfg.handleSymlinks);
out["IgnoreTimeShift"](toTimeShiftPhrase(cmpCfg.ignoreTimeShiftMinutes));
}
void writeConfig(const SyncDirectionConfig& dirCfg, XmlOut& out)
{
if (const DirectionByDiff* diffDirs = std::get_if<DirectionByDiff>(&dirCfg.dirs))
{
XmlOut outDirs = out["Differences"];
outDirs.attribute("LeftOnly", diffDirs->leftOnly);
outDirs.attribute("LeftNewer", diffDirs->leftNewer);
outDirs.attribute("RightNewer", diffDirs->rightNewer);
outDirs.attribute("RightOnly", diffDirs->rightOnly);
}
else
{
const DirectionByChange& changeDirs = std::get<DirectionByChange>(dirCfg.dirs);
XmlOut outDirsL = out["Changes"]["Left"];
outDirsL.attribute("Create", changeDirs.left.create);
outDirsL.attribute("Update", changeDirs.left.update);
outDirsL.attribute("Delete", changeDirs.left.delete_);
XmlOut outDirsR = out["Changes"]["Right"];
outDirsR.attribute("Create", changeDirs.right.create);
outDirsR.attribute("Update", changeDirs.right.update);
outDirsR.attribute("Delete", changeDirs.right.delete_);
}
}
void writeConfig(const SyncConfig& syncCfg, const std::map<AfsDevice, size_t>& deviceParallelOps, XmlOut& out)
{
writeConfig(syncCfg.directionCfg, out);
out["DeletionPolicy" ](syncCfg.deletionVariant);
out["VersioningFolder"](syncCfg.versioningFolderPhrase);
const size_t parallelOps = getDeviceParallelOps(deviceParallelOps, syncCfg.versioningFolderPhrase);
if (parallelOps > 1) out["VersioningFolder"].attribute("Threads", parallelOps);
out["VersioningFolder"].attribute("Style", syncCfg.versioningStyle);
if (syncCfg.versioningStyle != VersioningStyle::replace)
{
if (syncCfg.versionMaxAgeDays > 0) out["VersioningFolder"].attribute("MaxAge", syncCfg.versionMaxAgeDays);
if (syncCfg.versionCountMin > 0) out["VersioningFolder"].attribute("MinCount", syncCfg.versionCountMin);
if (syncCfg.versionCountMax > 0) out["VersioningFolder"].attribute("MaxCount", syncCfg.versionCountMax);
}
}
void writeConfig(const FilterConfig& filter, XmlOut& out)
{
out["Include"](splitFilterByLines(filter.includeFilter));
out["Exclude"](splitFilterByLines(filter.excludeFilter));
out["SizeMin"](filter.sizeMin);
out["SizeMin"].attribute("Unit", filter.unitSizeMin);
out["SizeMax"](filter.sizeMax);
out["SizeMax"].attribute("Unit", filter.unitSizeMax);
out["TimeSpan"](filter.timeSpan);
out["TimeSpan"].attribute("Type", filter.unitTimeSpan);
}
void writeConfig(const LocalPairConfig& lpc, const std::map<AfsDevice, size_t>& deviceParallelOps, XmlOut& out)
{
XmlOut outPair = out.addChild("Pair");
//read folder pairs
outPair["Left" ](lpc.folderPathPhraseLeft);
outPair["Right"](lpc.folderPathPhraseRight);
const size_t parallelOpsL = getDeviceParallelOps(deviceParallelOps, lpc.folderPathPhraseLeft);
const size_t parallelOpsR = getDeviceParallelOps(deviceParallelOps, lpc.folderPathPhraseRight);
if (parallelOpsL > 1) outPair["Left" ].attribute("Threads", parallelOpsL);
if (parallelOpsR > 1) outPair["Right"].attribute("Threads", parallelOpsR);
//avoid "fake" changed configs by only storing "real" parallel-enabled devices in deviceParallelOps
assert(std::all_of(deviceParallelOps.begin(), deviceParallelOps.end(), [](const auto& item) { return item.second > 1; }));
//###########################################################
//alternate comp configuration (optional)
if (lpc.localCmpCfg)
{
XmlOut outLocalCmp = outPair["Compare"];
writeConfig(*lpc.localCmpCfg, outLocalCmp);
}
//###########################################################
//alternate sync configuration (optional)
if (lpc.localSyncCfg)
{
XmlOut outLocalSync = outPair["Synchronize"];
writeConfig(*lpc.localSyncCfg, deviceParallelOps, outLocalSync);
}
//###########################################################
//alternate filter configuration
if (lpc.localFilter != FilterConfig()) //don't spam .ffs_gui file with default filter entries
{
XmlOut outFilter = outPair["Filter"];
writeConfig(lpc.localFilter, outFilter);
}
}
void writeConfig(const MainConfiguration& mainCfg, XmlOut& out)
{
XmlOut outCmp = out["Compare"];
writeConfig(mainCfg.cmpCfg, outCmp);
//###########################################################
XmlOut outSync = out["Synchronize"];
writeConfig(mainCfg.syncCfg, mainCfg.deviceParallelOps, outSync);
//###########################################################
XmlOut outFilter = out["Filter"];
writeConfig(mainCfg.globalFilter, outFilter);
//###########################################################
XmlOut outFp = out["FolderPairs"];
//write folder pairs
writeConfig(mainCfg.firstPair, mainCfg.deviceParallelOps, outFp);
for (const LocalPairConfig& lpc : mainCfg.additionalPairs)
writeConfig(lpc, mainCfg.deviceParallelOps, outFp);
out["Errors"].attribute("Ignore", mainCfg.ignoreErrors);
out["Errors"].attribute("Retry", mainCfg.autoRetryCount);
out["Errors"].attribute("Delay", mainCfg.autoRetryDelay);
out["PostSyncCommand"](mainCfg.postSyncCommand);
out["PostSyncCommand"].attribute("Condition", mainCfg.postSyncCondition);
out["LogFolder"](mainCfg.altLogFolderPathPhrase);
out["EmailNotification"](mainCfg.emailNotifyAddress);
out["EmailNotification"].attribute("Condition", mainCfg.emailNotifyCondition);
}
void writeConfig(const FfsGuiConfig& cfg, XmlOut& out)
{
out["Notes"](cfg.notes);
writeConfig(cfg.mainCfg, out); //write main config
out["GridViewType"](cfg.gridViewType);
}
void writeConfig(const FfsBatchConfig& cfg, XmlOut& out)
{
writeConfig(cfg.guiCfg, out);
XmlOut outBatch = out["Batch"];
outBatch["ProgressDialog"].attribute("Minimized", cfg.batchExCfg.runMinimized);
outBatch["ProgressDialog"].attribute("AutoClose", cfg.batchExCfg.autoCloseSummary);
outBatch["ErrorDialog" ](cfg.batchExCfg.batchErrorHandling);
outBatch["PostSyncAction"](cfg.batchExCfg.postBatchAction);
}
void writeConfig(const GlobalConfig& cfg, XmlOut& out)
{
out["Language"].attribute("Code", cfg.programLanguage);
out["ColorTheme"].attribute("Appearance", cfg.appColorTheme);
out["FailSafeFileCopy" ].attribute("Enabled", cfg.failSafeFileCopy);
out["CopyLockedFiles" ].attribute("Enabled", cfg.copyLockedFiles);
out["CopyFilePermissions" ].attribute("Enabled", cfg.copyFilePermissions);
out["FileTimeTolerance" ].attribute("Seconds", cfg.fileTimeTolerance);
out["RunWithBackgroundPriority"].attribute("Enabled", cfg.runWithBackgroundPriority);
out["LockDirectoriesDuringSync"].attribute("Enabled", cfg.createLockFile);
out["VerifyCopiedFiles" ].attribute("Enabled", cfg.verifyFileCopy);
out["LogFiles" ].attribute("MaxAge", cfg.logfilesMaxAgeDays);
out["LogFiles" ].attribute("Format", cfg.logFormat);
out["ProgressDialog"].attribute("AutoClose", cfg.progressDlgAutoClose);
XmlOut outOpt = out["OptionalDialogs"];
outOpt["ConfirmStartSync" ].attribute("Show", cfg.confirmDlgs.confirmSyncStart);
outOpt["ConfirmSaveConfig" ].attribute("Show", cfg.confirmDlgs.confirmSaveConfig);
outOpt["ConfirmSwapSides" ].attribute("Show", cfg.confirmDlgs.confirmSwapSides);
outOpt["ConfirmCommandMassInvoke" ].attribute("Show", cfg.confirmDlgs.confirmCommandMassInvoke);
outOpt["WarnFolderNotExisting" ].attribute("Show", cfg.warnDlgs.warnFolderNotExisting);
outOpt["WarnFoldersDifferInCase" ].attribute("Show", cfg.warnDlgs.warnFoldersDifferInCase);
outOpt["WarnUnresolvedConflicts" ].attribute("Show", cfg.warnDlgs.warnUnresolvedConflicts);
outOpt["WarnNotEnoughDiskSpace" ].attribute("Show", cfg.warnDlgs.warnNotEnoughDiskSpace);
outOpt["WarnSignificantDifference" ].attribute("Show", cfg.warnDlgs.warnSignificantDifference);
outOpt["WarnRecycleBinNotAvailable" ].attribute("Show", cfg.warnDlgs.warnRecyclerMissing);
outOpt["WarnDependentFolderPair" ].attribute("Show", cfg.warnDlgs.warnDependentFolderPair);
outOpt["WarnDependentBaseFolders" ].attribute("Show", cfg.warnDlgs.warnDependentBaseFolders);
outOpt["WarnDirectoryLockFailed" ].attribute("Show", cfg.warnDlgs.warnDirectoryLockFailed);
outOpt["WarnVersioningFolderPartOfSync"].attribute("Show", cfg.warnDlgs.warnVersioningFolderPartOfSync);
out["Sounds"]["CompareFinished"].attribute("Path", makePortablePath(cfg.soundFileCompareFinished));
out["Sounds"]["SyncFinished" ].attribute("Path", makePortablePath(cfg.soundFileSyncFinished));
out["Sounds"]["AlertPending" ].attribute("Path", makePortablePath(cfg.soundFileAlertPending));
//gui specific global settings (optional)
XmlOut outMainWin = out["MainDialog"];
//###########################################################
outMainWin["SearchPanel"].attribute("CaseSensitive", cfg.mainDlg.textSearchRespectCase);
//###########################################################
XmlOut outConfig = outMainWin["ConfigPanel"];
outConfig.attribute("ScrollPos", cfg.mainDlg.config.topRowPos);
outConfig.attribute("SyncOverdue", cfg.mainDlg.config.syncOverdueDays);
outConfig.attribute("SortByColumn", cfg.mainDlg.config.lastSortColumn);
outConfig.attribute("SortAscending", cfg.mainDlg.config.lastSortAscending);
outConfig["Configurations"].attribute("MaxSize", cfg.mainDlg.config.histItemsMax);
outConfig["Configurations"].attribute("LastSelected", makePortablePath(cfg.mainDlg.config.lastSelectedFile));
outConfig["Configurations"](cfg.mainDlg.config.fileHistory);
outConfig["LastUsed"](makePortablePath(cfg.mainDlg.config.lastUsedFiles));
//###########################################################
XmlOut outOverview = outMainWin["OverviewPanel"];
outOverview.attribute("ShowPercentage", cfg.mainDlg.overview.showPercentBar);
outOverview.attribute("SortByColumn", cfg.mainDlg.overview.lastSortColumn);
outOverview.attribute("SortAscending", cfg.mainDlg.overview.lastSortAscending);
XmlOut outFilePanel = outMainWin["FilePanel"];
outFilePanel.attribute("ShowIcons", cfg.mainDlg.showIcons);
outFilePanel.attribute("IconSize", cfg.mainDlg.iconSize);
outFilePanel.attribute("SashOffset", cfg.mainDlg.sashOffset);
outFilePanel.attribute("FolderPairsMax", cfg.mainDlg.folderPairsVisibleMax);
outFilePanel.attribute("PathFormatLeft", cfg.mainDlg.itemPathFormatLeftGrid);
outFilePanel.attribute("PathFormatRight", cfg.mainDlg.itemPathFormatRightGrid);
outFilePanel["FolderHistoryLeft" ](makePortablePath(cfg.mainDlg.folderHistoryLeft));
outFilePanel["FolderHistoryRight"](makePortablePath(cfg.mainDlg.folderHistoryRight));
outFilePanel["FolderHistoryLeft" ].attribute("LastSelected", makePortablePath(cfg.mainDlg.folderLastSelectedLeft));
outFilePanel["FolderHistoryRight"].attribute("LastSelected", makePortablePath(cfg.mainDlg.folderLastSelectedRight));
//###########################################################
XmlOut outCopyTo = outMainWin["ManualCopyTo"];
outCopyTo.attribute("KeepRelativePaths", cfg.mainDlg.copyToCfg.keepRelPaths);
outCopyTo.attribute("OverwriteIfExists", cfg.mainDlg.copyToCfg.overwriteIfExists);
XmlOut outCopyToHistory = outCopyTo["FolderHistory"];
outCopyToHistory(makePortablePath(cfg.mainDlg.copyToCfg.folderHistory));
outCopyToHistory.attribute("TargetFolder", makePortablePath(cfg.mainDlg.copyToCfg.targetFolderPath));
outCopyToHistory.attribute("LastSelected", makePortablePath(cfg.mainDlg.copyToCfg.targetFolderLastSelected));
//###########################################################
XmlOut outDefFilter = outMainWin["DefaultViewFilter"];
outDefFilter.attribute("Equal", cfg.mainDlg.viewFilterDefault.equal);
outDefFilter.attribute("Conflict", cfg.mainDlg.viewFilterDefault.conflict);
outDefFilter.attribute("Excluded", cfg.mainDlg.viewFilterDefault.excluded);
XmlOut catView = outDefFilter["Difference"];
catView.attribute("LeftOnly", cfg.mainDlg.viewFilterDefault.leftOnly);
catView.attribute("RightOnly", cfg.mainDlg.viewFilterDefault.rightOnly);
catView.attribute("LeftNewer", cfg.mainDlg.viewFilterDefault.leftNewer);
catView.attribute("RightNewer", cfg.mainDlg.viewFilterDefault.rightNewer);
catView.attribute("Different", cfg.mainDlg.viewFilterDefault.different);
XmlOut actView = outDefFilter["Action"];
actView.attribute("CreateLeft", cfg.mainDlg.viewFilterDefault.createLeft);
actView.attribute("CreateRight", cfg.mainDlg.viewFilterDefault.createRight);
actView.attribute("UpdateLeft", cfg.mainDlg.viewFilterDefault.updateLeft);
actView.attribute("UpdateRight", cfg.mainDlg.viewFilterDefault.updateRight);
actView.attribute("DeleteLeft", cfg.mainDlg.viewFilterDefault.deleteLeft);
actView.attribute("DeleteRight", cfg.mainDlg.viewFilterDefault.deleteRight);
actView.attribute("DoNothing", cfg.mainDlg.viewFilterDefault.doNothing);
out["FolderHistory" ].attribute("MaxSize", cfg.folderHistoryMax);
out["SftpKeyFile"].attribute("LastSelected", makePortablePath(cfg.sftpKeyFileLastSelected));
XmlOut outFileFilter = out["DefaultFilter"];
writeConfig(cfg.defaultFilter, outFileFilter);
out["VersioningFolderHistory"](cfg.versioningFolderHistory);
out["VersioningFolderHistory"].attribute("LastSelected", makePortablePath(cfg.versioningFolderLastSelected));
out["LogFolder"](makePortablePath(cfg.logFolderPhrase));
out["LogFolderHistory"](makePortablePath(cfg.logFolderHistory));
out["LogFolderHistory"].attribute("LastSelected", makePortablePath(cfg.logFolderLastSelected));
out["EmailHistory"](cfg.emailHistory);
out["EmailHistory"].attribute("MaxSize", cfg.emailHistoryMax);
out["CommandHistory"](cfg.commandHistory);
out["CommandHistory"].attribute("MaxSize", cfg.commandHistoryMax);
//external applications
out["ExternalApps"](cfg.externalApps);
//last update check
out["LastOnlineCheck" ](cfg.lastUpdateCheck);
out["LastOnlineVersion"](cfg.lastOnlineVersion);
out["WelcomeDialogVersion"](cfg.welcomeDialogLastVersion);
for (const auto& [scalePercent, layout] : cfg.dpiLayouts)
{
XmlOut outLayout = out["DpiLayouts"].addChild("Layout");
outLayout.attribute("Scale", numberTo<std::string>(scalePercent) + '%');
XmlOut outLayoutMain = outLayout["MainWindow"];
if (layout.mainDlg.size)
{
outLayoutMain.attribute("Width", layout.mainDlg.size->x);
outLayoutMain.attribute("Height", layout.mainDlg.size->y);
}
if (layout.mainDlg.pos)
{
outLayoutMain.attribute("PosX", layout.mainDlg.pos->x);
outLayoutMain.attribute("PosY", layout.mainDlg.pos->y);
}
outLayoutMain.attribute("Maximized", layout.mainDlg.isMaximized);
XmlOut outLayoutProgress = outLayout["ProgressDialog"];
if (layout.progressDlg.size)
{
outLayoutProgress.attribute("Width", layout.progressDlg.size->x);
outLayoutProgress.attribute("Height", layout.progressDlg.size->y);
}
outLayoutProgress.attribute("Maximized", layout.progressDlg.isMaximized);
outLayout["Panels" ](layout.panelLayout);
outLayout["ConfigPanel" ](layout.configColumnAttribs);
outLayout["OverviewPanel" ](layout.overviewColumnAttribs);
outLayout["FilePanelLeft" ](layout.fileColumnAttribsLeft);
outLayout["FilePanelRight"](layout.fileColumnAttribsRight);
}
}
template <class ConfigType>
void writeConfig(const ConfigType& cfg, const char* cfgType, int xmlFormatVer, const Zstring& filePath)
{
XmlDoc doc("FreeFileSync");
doc.root().setAttribute("XmlType", cfgType);
doc.root().setAttribute("XmlFormat", xmlFormatVer);
XmlOut out(doc);
writeConfig(cfg, out);
saveXml(doc, filePath); //throw FileError
}
}
void fff::writeConfig(const FfsGuiConfig& cfg, const Zstring& filePath)
{
::writeConfig(cfg, "GUI", XML_FORMAT_SYNC_CFG, filePath); //throw FileError
}
void fff::writeConfig(const FfsBatchConfig& cfg, const Zstring& filePath)
{
::writeConfig(cfg, "BATCH", XML_FORMAT_SYNC_CFG, filePath); //throw FileError
}
void fff::writeConfig(const GlobalConfig& cfg, const Zstring& filePath)
{
::writeConfig(cfg, "GLOBAL", XML_FORMAT_GLOBAL_CFG, filePath); //throw FileError
}
std::wstring fff::extractJobName(const Zstring& cfgFilePath)
{
const Zstring fileName = getItemName(cfgFilePath);
const Zstring jobName = beforeLast(fileName, Zstr('.'), IfNotFoundReturn::all);
return utfTo<std::wstring>(jobName);
}
std::string fff::serializeFilter(const FilterConfig& filterCfg)
{
XmlDoc doc("Filter");
doc.setEncoding("");
XmlOut out(doc);
::writeConfig(filterCfg, out);
return serializeXml(doc); //noexcept
}
std::optional<FilterConfig> fff::parseFilterBuf(const std::string& filterBuf)
{
try
{
XmlDoc doc = parseXml(filterBuf); //throw XmlParsingError
XmlIn in(doc);
FilterConfig filterCfg;
::readConfig(in, filterCfg);
if (in.getErrors().empty())
return filterCfg;
}
catch (XmlParsingError&) {}
return std::nullopt;
}
void fff::saveErrorLog(const ErrorLog& log, const Zstring& filePath) //throw FileError
{
XmlDoc doc("Log");
doc.setEncoding("");
XmlOut out(doc);
for (const LogEntry& e : log)
{
XmlOut outMsg = out.addChild(e.type == MessageType::MSG_TYPE_ERROR ? "Error" : (e.type == MessageType::MSG_TYPE_WARNING ? "Warning" : "Info"));
outMsg.attribute("Time", formatTime(formatIsoDateTimeTag, getLocalTime(e.time)));
outMsg(e.message);
}
saveXml(doc, filePath); //throw FileError
}
ErrorLog fff::loadErrorLog(const Zstring& filePath) //throw FileError
{
XmlDoc doc = loadXml(filePath); //throw FileError
XmlIn in(doc);
ErrorLog log;
in.visitChildren([&](const XmlIn& inMsg)
{
Zstring timeStr;
inMsg.attribute("Time", timeStr);
Zstringc msg;
inMsg(msg);
log.push_back(
{
.time = localToTimeT(parseTime(formatIsoDateTimeTag, timeStr)).first,
.type = *inMsg.getName() == "Error" ? MessageType::MSG_TYPE_ERROR : (*inMsg.getName() == "Warning" ? MessageType::MSG_TYPE_WARNING : MessageType::MSG_TYPE_INFO),
.message = std::move(msg),
});
});
if (const std::wstring& errors = in.getErrors();
!errors.empty())
throw FileError(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(filePath)),
_("The following XML elements could not be read:") + L'\n' + errors);
return log;
}