2452 lines
87 KiB
C++
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;
|
|
}
|