summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Wilhelm <shieldwed@outlook.com>2020-02-23 22:12:27 +0000
committerDaniel Wilhelm <shieldwed@outlook.com>2020-02-23 22:12:27 +0000
commit450f803dd75f831f8ee14072fe0eb664bbe518df (patch)
treeb3e831d44df50348a20f3541b6062f7fbab6ff3d
parentMerge branch '10.19' into 'master' (diff)
parentremove upstream deleted files (diff)
downloadFreeFileSync-10.20.tar.gz
FreeFileSync-10.20.tar.bz2
FreeFileSync-10.20.zip
Merge branch '10.20' into 'master'10.20
add upstream 10.20 See merge request opensource-tracking/FreeFileSync!17
-rwxr-xr-xBugs.txt16
-rwxr-xr-xChangelog.txt22
-rw-r--r--FreeFileSync/Build/Resources/Icons.zipbin391482 -> 389295 bytes
-rw-r--r--FreeFileSync/Build/Resources/Languages.zipbin506524 -> 522133 bytes
-rwxr-xr-xFreeFileSync/Build/Resources/cacert.pem38
-rw-r--r--[-rwxr-xr-x]FreeFileSync/Source/Makefile2
-rw-r--r--FreeFileSync/Source/RealTimeSync/application.cpp2
-rw-r--r--FreeFileSync/Source/RealTimeSync/tray_menu.cpp4
-rw-r--r--FreeFileSync/Source/afs/abstract.cpp2
-rw-r--r--FreeFileSync/Source/afs/concrete.cpp4
-rw-r--r--FreeFileSync/Source/afs/ftp.cpp115
-rw-r--r--FreeFileSync/Source/afs/ftp.h3
-rw-r--r--FreeFileSync/Source/afs/gdrive.cpp618
-rw-r--r--FreeFileSync/Source/afs/init_curl_libssh2.cpp2
-rw-r--r--FreeFileSync/Source/afs/sftp.cpp139
-rw-r--r--FreeFileSync/Source/afs/sftp.h20
-rw-r--r--FreeFileSync/Source/base/application.cpp20
-rw-r--r--FreeFileSync/Source/base/config.cpp141
-rw-r--r--FreeFileSync/Source/base/config.h13
-rw-r--r--FreeFileSync/Source/base/db_file.cpp137
-rw-r--r--FreeFileSync/Source/base/dir_exist_async.h10
-rw-r--r--FreeFileSync/Source/base/dir_lock.cpp122
-rw-r--r--FreeFileSync/Source/base/file_hierarchy.cpp4
-rw-r--r--FreeFileSync/Source/base/file_hierarchy.h2
-rw-r--r--FreeFileSync/Source/base/icon_loader.h2
-rw-r--r--FreeFileSync/Source/base/log_file.cpp582
-rw-r--r--FreeFileSync/Source/base/log_file.h20
-rw-r--r--FreeFileSync/Source/base/return_codes.h4
-rw-r--r--FreeFileSync/Source/base/status_handler.h4
-rw-r--r--FreeFileSync/Source/base/structures.cpp6
-rw-r--r--FreeFileSync/Source/base/structures.h41
-rw-r--r--FreeFileSync/Source/base/synchronization.cpp2
-rw-r--r--FreeFileSync/Source/base/versioning.cpp4
-rw-r--r--FreeFileSync/Source/ui/batch_config.cpp4
-rw-r--r--FreeFileSync/Source/ui/batch_status_handler.cpp150
-rw-r--r--FreeFileSync/Source/ui/batch_status_handler.h10
-rw-r--r--FreeFileSync/Source/ui/cfg_grid.cpp6
-rw-r--r--FreeFileSync/Source/ui/command_box.cpp12
-rw-r--r--FreeFileSync/Source/ui/command_box.h2
-rw-r--r--FreeFileSync/Source/ui/file_grid.cpp8
-rw-r--r--FreeFileSync/Source/ui/folder_history_box.cpp4
-rw-r--r--FreeFileSync/Source/ui/folder_history_box.h30
-rw-r--r--FreeFileSync/Source/ui/folder_pair.h46
-rw-r--r--FreeFileSync/Source/ui/folder_selector.cpp2
-rw-r--r--FreeFileSync/Source/ui/gui_generated.cpp154
-rw-r--r--FreeFileSync/Source/ui/gui_generated.h25
-rw-r--r--FreeFileSync/Source/ui/gui_status_handler.cpp118
-rw-r--r--FreeFileSync/Source/ui/gui_status_handler.h14
-rw-r--r--FreeFileSync/Source/ui/main_dlg.cpp155
-rw-r--r--FreeFileSync/Source/ui/main_dlg.h4
-rw-r--r--FreeFileSync/Source/ui/progress_indicator.cpp60
-rw-r--r--FreeFileSync/Source/ui/progress_indicator.h4
-rw-r--r--FreeFileSync/Source/ui/search_grid.cpp4
-rw-r--r--FreeFileSync/Source/ui/small_dlgs.cpp106
-rw-r--r--FreeFileSync/Source/ui/small_dlgs.h3
-rw-r--r--FreeFileSync/Source/ui/sync_cfg.cpp333
-rw-r--r--FreeFileSync/Source/ui/sync_cfg.h15
-rw-r--r--FreeFileSync/Source/ui/version_check.cpp8
-rw-r--r--FreeFileSync/Source/version/version.h2
-rw-r--r--libcurl/curl_wrap.h (renamed from FreeFileSync/Source/afs/libcurl/curl_wrap.h)28
-rw-r--r--libcurl/rest.cpp185
-rw-r--r--libcurl/rest.h52
-rw-r--r--libssh2/libssh2_wrap.h (renamed from FreeFileSync/Source/afs/libssh2/libssh2_wrap.h)0
-rw-r--r--wx+/image_resources.cpp16
-rw-r--r--wx+/image_tools.h12
-rw-r--r--zen/error_log.h47
-rw-r--r--zen/http.cpp165
-rw-r--r--zen/http.h13
-rw-r--r--zen/json.h32
-rw-r--r--zen/serialize.h18
-rw-r--r--zen/shell_execute.h48
-rw-r--r--zen/shutdown.cpp4
-rw-r--r--zen/string_tools.h23
-rw-r--r--zen/sys_error.h3
-rw-r--r--zen/system.cpp84
-rw-r--r--zen/system.h33
-rw-r--r--zen/zlib_wrap.cpp20
-rw-r--r--zen/zlib_wrap.h12
-rw-r--r--zen/zstring.cpp4
79 files changed, 2597 insertions, 1582 deletions
diff --git a/Bugs.txt b/Bugs.txt
index ad75f52f..fe61dc97 100755
--- a/Bugs.txt
+++ b/Bugs.txt
@@ -1,6 +1,6 @@
When manually compiling FreeFileSync, you should also fix the following bugs in its library dependencies.
FreeFileSync generally uses the latest library versions and works with upstream to get the bugs fixed
-that affect FreeFileSync. Therefore it is not recommended to compile against older library versions than
+that affect FreeFileSync. Therefore it is NOT RECOMMENDED TO COMPILE AGAINST OLDER library versions than
the ones mentioned below. The remaining issues that are yet to be fixed are listed in the following:
@@ -64,17 +64,11 @@ move the following constants from src/sftp.h to include/libssh2_sftp.h:
#define MAX_SFTP_READ_SIZE 30000
__________________________________________________________________________________________________________
-src/comp.c
-https://github.com/libssh2/libssh2/pull/418
+src/transport.c
+https://github.com/libssh2/libssh2/pull/443
-_libssh2_comp_methods(LIBSSH2_SESSION* session)
-{
-+ #undef compress
- if (session->flag.compress)
- return comp_methods;
- else
- return no_comp_methods;
-}
+- if (encrypted && compressed)
++ if (encrypted && compressed && session->local.comp_abstract)
__________________________________________________________________________________________________________
src/openssl.cpp
diff --git a/Changelog.txt b/Changelog.txt
index c4b991e7..a4103889 100755
--- a/Changelog.txt
+++ b/Changelog.txt
@@ -1,3 +1,23 @@
+FreeFileSync 10.20 [2020-02-14]
+-------------------------------
+Send email notifications after sync
+Generate log files in HTML format
+Detect sync database consistency errors
+Start log file with preview of first 50 errors/warnings
+Mitigate lock file data corruption
+Print Windows error codes in hexadecimal
+Fixed missing MTP and network links in folder picker (Linux)
+Display versioning and log folder path history
+Display and log all config names for merged configurations
+Run post-sync command synchronously and log exit code
+Fixed crash on Bitvise SFTP servers with zlib delayed compression
+Show actual time out used in failure message
+Show detailed error message when failing to test sound files
+Fixed timeout for long-running FTP uploads by sending keep-alives
+Use Donation Edition on unlimited number of virtual machines
+Ignore accidental clicks in empty space of configuration panel
+
+
FreeFileSync 10.19 [2019-12-27]
-------------------------------
Unified rendering of disabled grid layouts
@@ -599,7 +619,7 @@ Run SFTP tasks directly on worker threads without helper thread overhead
FreeFileSync 8.4 [2016-08-12]
-----------------------------
Mark temporary copies created by %local_path% read-only
-Fixed crash when accessing Bitvise SFTP Servers
+Fixed crash when accessing Bitvise SFTP servers
Support nanosecond-precision file time copying (Linux)
Start maximized instead of in full screen mode (OS X)
Fixed crash while setting privileges during shutdown
diff --git a/FreeFileSync/Build/Resources/Icons.zip b/FreeFileSync/Build/Resources/Icons.zip
index a02a0759..79a4dd7f 100644
--- a/FreeFileSync/Build/Resources/Icons.zip
+++ b/FreeFileSync/Build/Resources/Icons.zip
Binary files differ
diff --git a/FreeFileSync/Build/Resources/Languages.zip b/FreeFileSync/Build/Resources/Languages.zip
index a6c2362b..81e8e960 100644
--- a/FreeFileSync/Build/Resources/Languages.zip
+++ b/FreeFileSync/Build/Resources/Languages.zip
Binary files differ
diff --git a/FreeFileSync/Build/Resources/cacert.pem b/FreeFileSync/Build/Resources/cacert.pem
index edc5090c..651694e8 100755
--- a/FreeFileSync/Build/Resources/cacert.pem
+++ b/FreeFileSync/Build/Resources/cacert.pem
@@ -1,7 +1,7 @@
##
## Bundle of CA Root Certificates
##
-## Certificate data from Mozilla as of: Wed Oct 16 03:12:09 2019 GMT
+## Certificate data from Mozilla as of: Wed Jan 1 04:12:10 2020 GMT
##
## This is a bundle of X.509 certificates of public Certificate Authorities
## (CA). These were automatically extracted from Mozilla's root certificates
@@ -14,7 +14,7 @@
## Just configure this file as the SSLCACertificateFile.
##
## Conversion done with mk-ca-bundle.pl version 1.27.
-## SHA256: c979c6f35714a0fedb17d9e5ba37adecbbc91a8faf4186b4e23d6f9ca44fd6cb
+## SHA256: f3bdcd74612952da8476a9d4147f50b29ad0710b7dd95b4c8690500209986d70
##
@@ -3430,3 +3430,37 @@ hcErulWuBurQB7Lcq9CClnXO0lD+mefPL5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB
60PZ2Pierc+xYw5F9KBaLJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fq
dBb9HxEGmpv0
-----END CERTIFICATE-----
+
+Entrust Root Certification Authority - G4
+=========================================
+-----BEGIN CERTIFICATE-----
+MIIGSzCCBDOgAwIBAgIRANm1Q3+vqTkPAAAAAFVlrVgwDQYJKoZIhvcNAQELBQAwgb4xCzAJBgNV
+BAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3Qu
+bmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNSBFbnRydXN0LCBJbmMuIC0gZm9yIGF1
+dGhvcml6ZWQgdXNlIG9ubHkxMjAwBgNVBAMTKUVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1
+dGhvcml0eSAtIEc0MB4XDTE1MDUyNzExMTExNloXDTM3MTIyNzExNDExNlowgb4xCzAJBgNVBAYT
+AlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0
+L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNSBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhv
+cml6ZWQgdXNlIG9ubHkxMjAwBgNVBAMTKUVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhv
+cml0eSAtIEc0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsewsQu7i0TD/pZJH4i3D
+umSXbcr3DbVZwbPLqGgZ2K+EbTBwXX7zLtJTmeH+H17ZSK9dE43b/2MzTdMAArzE+NEGCJR5WIoV
+3imz/f3ET+iq4qA7ec2/a0My3dl0ELn39GjUu9CH1apLiipvKgS1sqbHoHrmSKvS0VnM1n4j5pds
+8ELl3FFLFUHtSUrJ3hCX1nbB76W1NhSXNdh4IjVS70O92yfbYVaCNNzLiGAMC1rlLAHGVK/XqsEQ
+e9IFWrhAnoanw5CGAlZSCXqc0ieCU0plUmr1POeo8pyvi73TDtTUXm6Hnmo9RR3RXRv06QqsYJn7
+ibT/mCzPfB3pAqoEmh643IhuJbNsZvc8kPNXwbMv9W3y+8qh+CmdRouzavbmZwe+LGcKKh9asj5X
+xNMhIWNlUpEbsZmOeX7m640A2Vqq6nPopIICR5b+W45UYaPrL0swsIsjdXJ8ITzI9vF01Bx7owVV
+7rtNOzK+mndmnqxpkCIHH2E6lr7lmk/MBTwoWdPBDFSoWWG9yHJM6Nyfh3+9nEg2XpWjDrk4JFX8
+dWbrAuMINClKxuMrLzOg2qOGpRKX/YAr2hRC45K9PvJdXmd0LhyIRyk0X+IyqJwlN4y6mACXi0mW
+Hv0liqzc2thddG5msP9E36EYxr5ILzeUePiVSj9/E15dWf10hkNjc0kCAwEAAaNCMEAwDwYDVR0T
+AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJ84xFYjwznooHFs6FRM5Og6sb9n
+MA0GCSqGSIb3DQEBCwUAA4ICAQAS5UKme4sPDORGpbZgQIeMJX6tuGguW8ZAdjwD+MlZ9POrYs4Q
+jbRaZIxowLByQzTSGwv2LFPSypBLhmb8qoMi9IsabyZIrHZ3CL/FmFz0Jomee8O5ZDIBf9PD3Vht
+7LGrhFV0d4QEJ1JrhkzO3bll/9bGXp+aEJlLdWr+aumXIOTkdnrG0CSqkM0gkLpHZPt/B7NTeLUK
+YvJzQ85BK4FqLoUWlFPUa19yIqtRLULVAJyZv967lDtX/Zr1hstWO1uIAeV8KEsD+UmDfLJ/fOPt
+jqF/YFOOVZ1QNBIPt5d7bIdKROf1beyAN/BYGW5KaHbwH5Lk6rWS02FREAutp9lfx1/cH6NcjKF+
+m7ee01ZvZl4HliDtC3T7Zk6LERXpgUl+b7DUUH8i119lAg2m9IUe2K4GS0qn0jFmwvjO5QimpAKW
+RGhXxNUzzxkvFMSUHHuk2fCfDrGA4tGeEWSpiBE6doLlYsKA2KSD7ZPvfC+QsDJMlhVoSFLUmQjA
+JOgc47OlIQ6SwJAfzyBfyjs4x7dtOvPmRLgOMWuIjnDrnBdSqEGULoe256YSxXXfW8AKbnuk5F6G
++TaU33fD6Q3AOfF5u0aOq0NZJ7cguyPpVkAh7DE9ZapD8j3fcEThuk0mEDuYn/PIjhs4ViFqUZPT
+kcpG2om3PVODLAgfi49T3f+sHw==
+-----END CERTIFICATE-----
diff --git a/FreeFileSync/Source/Makefile b/FreeFileSync/Source/Makefile
index d3c1209a..87352ea4 100755..100644
--- a/FreeFileSync/Source/Makefile
+++ b/FreeFileSync/Source/Makefile
@@ -78,6 +78,7 @@ CPP_FILES+=ui/taskbar.cpp
CPP_FILES+=ui/tray_icon.cpp
CPP_FILES+=ui/triple_splitter.cpp
CPP_FILES+=ui/version_check.cpp
+CPP_FILES+=../../libcurl/rest.cpp
CPP_FILES+=../../zen/recycler.cpp
CPP_FILES+=../../zen/file_access.cpp
CPP_FILES+=../../zen/file_io.cpp
@@ -89,6 +90,7 @@ CPP_FILES+=../../zen/legacy_compiler.cpp
CPP_FILES+=../../zen/open_ssl.cpp
CPP_FILES+=../../zen/process_priority.cpp
CPP_FILES+=../../zen/shutdown.cpp
+CPP_FILES+=../../zen/system.cpp
CPP_FILES+=../../zen/thread.cpp
CPP_FILES+=../../zen/zlib_wrap.cpp
CPP_FILES+=../../wx+/file_drop.cpp
diff --git a/FreeFileSync/Source/RealTimeSync/application.cpp b/FreeFileSync/Source/RealTimeSync/application.cpp
index db28c580..7f81883b 100644
--- a/FreeFileSync/Source/RealTimeSync/application.cpp
+++ b/FreeFileSync/Source/RealTimeSync/application.cpp
@@ -46,8 +46,6 @@ bool Application::OnInit()
::gtk_rc_parse((fff::getResourceDirPf() + "Gtk2Styles.rc").c_str());
//fix hang on Ubuntu 19.10 (see FFS's application.cpp)
- if (::setenv("GIO_USE_VFS", "local", true /*overwrite*/) != 0)
- std::cerr << utfTo<std::string>(formatSystemError(L"setenv(GIO_USE_VFS)", errno)) << "\n";
g_vfs_get_default(); //returns unowned GVfs*
#elif GTK_MAJOR_VERSION == 3
diff --git a/FreeFileSync/Source/RealTimeSync/tray_menu.cpp b/FreeFileSync/Source/RealTimeSync/tray_menu.cpp
index 3b50e9c6..a4c1a91a 100644
--- a/FreeFileSync/Source/RealTimeSync/tray_menu.cpp
+++ b/FreeFileSync/Source/RealTimeSync/tray_menu.cpp
@@ -105,7 +105,7 @@ private:
void OnErrorFlashIcon(wxEvent& event)
{
iconFlashStatusLast_ = !iconFlashStatusLast_;
- setTrayIcon(iconFlashStatusLast_ ? trayBmp_ : greyScale(trayBmp_), _("Error"));
+ setTrayIcon(greyScaleIfDisabled(trayBmp_, iconFlashStatusLast_), _("Error"));
}
void setTrayIcon(const wxBitmap& bmp, const wxString& statusTxt)
@@ -274,7 +274,7 @@ rts::AbortReason rts::runFolderMonitor(const XmlRealConfig& config, const wxStri
auto cmdLineExp = fff::expandMacros(cmdLine);
try
{
- shellExecute(cmdLineExp, ExecutionType::SYNC, config.hideConsoleWindow); //throw FileError
+ shellExecute(cmdLineExp, ExecutionType::sync, config.hideConsoleWindow); //throw FileError
}
catch (const FileError& e)
{
diff --git a/FreeFileSync/Source/afs/abstract.cpp b/FreeFileSync/Source/afs/abstract.cpp
index 0b3c85d5..9454f68b 100644
--- a/FreeFileSync/Source/afs/abstract.cpp
+++ b/FreeFileSync/Source/afs/abstract.cpp
@@ -193,7 +193,7 @@ AFS::FileCopyResult AFS::copyFileTransactional(const AbstractPath& apSource, con
if (transactionalCopy && !hasNativeTransactionalCopy(apTarget))
{
- std::optional<AbstractPath> parentPath = AFS::getParentPath(apTarget);
+ const std::optional<AbstractPath> parentPath = AFS::getParentPath(apTarget);
if (!parentPath)
throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(AFS::getDisplayPath(apTarget))), L"Path is device root.");
const Zstring fileName = AFS::getItemName(apTarget);
diff --git a/FreeFileSync/Source/afs/concrete.cpp b/FreeFileSync/Source/afs/concrete.cpp
index b792c350..700351dd 100644
--- a/FreeFileSync/Source/afs/concrete.cpp
+++ b/FreeFileSync/Source/afs/concrete.cpp
@@ -15,6 +15,8 @@ using namespace fff;
void fff::initAfs(const AfsConfig& cfg)
{
+ ftpInit();
+ sftpInit();
googleDriveInit(appendSeparator(cfg.configDirPathPf) + Zstr("GoogleDrive"),
appendSeparator(cfg.resourceDirPathPf) + Zstr("cacert.pem"));
}
@@ -23,6 +25,8 @@ void fff::initAfs(const AfsConfig& cfg)
void fff::teardownAfs()
{
googleDriveTeardown();
+ sftpTeardown();
+ ftpTeardown();
}
diff --git a/FreeFileSync/Source/afs/ftp.cpp b/FreeFileSync/Source/afs/ftp.cpp
index 0380df48..e502936a 100644
--- a/FreeFileSync/Source/afs/ftp.cpp
+++ b/FreeFileSync/Source/afs/ftp.cpp
@@ -9,7 +9,7 @@
#include <zen/sys_error.h>
#include <zen/globals.h>
#include <zen/time.h>
-#include "libcurl/curl_wrap.h" //DON'T include <curl/curl.h> directly!
+#include <libcurl/curl_wrap.h> //DON'T include <curl/curl.h> directly!
#include "init_curl_libssh2.h"
#include "ftp_common.h"
#include "abstract_impl.h"
@@ -307,23 +307,9 @@ public:
::curl_easy_cleanup(easyHandle_);
}
- //const FtpLoginInfo& getSessionId() const { return sessionId_; }
-
- struct Option
- {
- template <class T>
- Option(CURLoption o, T val) : option(o), value(static_cast<uint64_t>(val)) { static_assert(sizeof(val) <= sizeof(value)); }
-
- template <class T>
- Option(CURLoption o, T* val) : option(o), value(reinterpret_cast<uint64_t>(val)) { static_assert(sizeof(val) <= sizeof(value)); }
-
- CURLoption option = CURLOPT_LASTENTRY;
- uint64_t value = 0;
- };
-
//returns server response (header data)
std::string perform(const AfsPath& afsPath, bool isDir, curl_ftpmethod pathMethod,
- const std::vector<Option>& extraOptions, bool requiresUtf8, int timeoutSec) //throw SysError
+ const std::vector<CurlOption>& extraOptions, bool requiresUtf8, int timeoutSec) //throw SysError
{
if (requiresUtf8) //avoid endless recursion
sessionEnableUtf8(timeoutSec); //throw SysError
@@ -337,12 +323,12 @@ public:
else
::curl_easy_reset(easyHandle_);
- std::vector<Option> options;
+ std::vector<CurlOption> options;
- curlErrorBuf_[0] = '\0';
- options.emplace_back(CURLOPT_ERRORBUFFER, curlErrorBuf_);
+ char curlErrorBuf[CURL_ERROR_SIZE] = {};
+ options.emplace_back(CURLOPT_ERRORBUFFER, curlErrorBuf);
- headerData_.clear();
+ std::string headerData;
using CbType = size_t (*)(const char* buffer, size_t size, size_t nitems, void* callbackData);
CbType onHeaderReceived = [](const char* buffer, size_t size, size_t nitems, void* callbackData)
{
@@ -350,7 +336,7 @@ public:
output.append(buffer, size * nitems);
return size * nitems;
};
- options.emplace_back(CURLOPT_HEADERDATA, &headerData_);
+ options.emplace_back(CURLOPT_HEADERDATA, &headerData);
options.emplace_back(CURLOPT_HEADERFUNCTION, onHeaderReceived);
//lifetime: keep alive until after curl_easy_setopt() below
@@ -384,10 +370,13 @@ public:
options.emplace_back(CURLOPT_LOW_SPEED_LIMIT, 1L); //[bytes], can't use "0" which means "inactive", so use some low number
//unlike CURLOPT_TIMEOUT, this one is NOT a limit on the total transfer time
- options.emplace_back(CURLOPT_FTP_RESPONSE_TIMEOUT, timeoutSec);
+ options.emplace_back(CURLOPT_FTP_RESPONSE_TIMEOUT, timeoutSec); //== alias of CURLOPT_SERVER_RESPONSE_TIMEOUT
//CURLOPT_ACCEPTTIMEOUT_MS? => only relevant for "active" FTP connections
+ //long-running file uploads require us to send keep-alives for the TCP control connection: https://freefilesync.org/forum/viewtopic.php?t=6928
+ options.emplace_back(CURLOPT_TCP_KEEPALIVE, 1L);
+
//Use share interface? https://curl.haxx.se/libcurl/c/libcurl-share.html
//perf test, 4 and 8 parallel threads:
@@ -482,13 +471,7 @@ public:
append(options, extraOptions);
- for (const Option& opt : options)
- {
- const CURLcode rc = ::curl_easy_setopt(easyHandle_, opt.option, opt.value);
- if (rc != CURLE_OK)
- throw SysError(formatSystemError(L"curl_easy_setopt " + numberTo<std::wstring>(static_cast<int>(opt.option)),
- formatCurlStatusCode(rc), utfTo<std::wstring>(::curl_easy_strerror(rc))));
- }
+ applyCurlOptions(easyHandle_, options); //throw SysError
//=======================================================================================================
const CURLcode rcPerf = ::curl_easy_perform(easyHandle_);
@@ -502,10 +485,30 @@ public:
//=======================================================================================================
if (rcPerf != CURLE_OK)
- throw SysError(formatLastCurlError(L"curl_easy_perform", rcPerf, ftpStatusCode));
+ {
+ std::wstring errorMsg = trimCpy(utfTo<std::wstring>(curlErrorBuf)); //optional
+
+ if (rcPerf != CURLE_RECV_ERROR)
+ {
+ const std::vector<std::string> headerLines = splitFtpResponse(headerData);
+ if (!headerLines.empty())
+ errorMsg += (errorMsg.empty() ? L"" : L"\n") + trimCpy(utfTo<std::wstring>(headerLines.back())); //that *should* be the servers error response
+ }
+ else //failed to get server response
+ errorMsg += (errorMsg.empty() ? L"" : L"\n") + formatFtpStatusCode(ftpStatusCode);
+#if 0
+ //utfTo<std::wstring>(::curl_easy_strerror(ec)) is uninteresting
+ //use CURLINFO_OS_ERRNO ?? https://curl.haxx.se/libcurl/c/CURLINFO_OS_ERRNO.html
+ long nativeErrorCode = 0;
+ if (::curl_easy_getinfo(easyHandle_, CURLINFO_OS_ERRNO, &nativeErrorCode) == CURLE_OK)
+ if (nativeErrorCode != 0)
+ errorMsg += (errorMsg.empty() ? L"" : L"\n") + std::wstring(L"Native error code: ") + numberTo<std::wstring>(nativeErrorCode);
+#endif
+ throw SysError(formatSystemError(L"curl_easy_perform", formatCurlStatusCode(rcPerf), errorMsg));
+ }
lastSuccessfulUseTime_ = std::chrono::steady_clock::now();
- return headerData_;
+ return headerData;
}
//returns server response (header data)
@@ -767,43 +770,15 @@ private:
return output;
}
- std::wstring formatLastCurlError(const std::wstring& functionName, CURLcode ec, long ftpStatusCode) const
- {
- std::wstring errorMsg;
-
- if (curlErrorBuf_[0] != 0)
- errorMsg = trimCpy(utfTo<std::wstring>(curlErrorBuf_));
-
- if (ec != CURLE_RECV_ERROR)
- {
- const std::vector<std::string> headerLines = splitFtpResponse(headerData_);
- if (!headerLines.empty())
- errorMsg += (errorMsg.empty() ? L"" : L"\n") + trimCpy(utfTo<std::wstring>(headerLines.back())); //that *should* be the servers error response
- }
- else //failed to get server response
- errorMsg += (errorMsg.empty() ? L"" : L"\n") + formatFtpStatusCode(ftpStatusCode);
-#if 0
- //utfTo<std::wstring>(::curl_easy_strerror(ec)) is uninteresting
- //use CURLINFO_OS_ERRNO ?? https://curl.haxx.se/libcurl/c/CURLINFO_OS_ERRNO.html
- long nativeErrorCode = 0;
- if (::curl_easy_getinfo(easyHandle_, CURLINFO_OS_ERRNO, &nativeErrorCode) == CURLE_OK)
- if (nativeErrorCode != 0)
- errorMsg += (errorMsg.empty() ? L"" : L"\n") + std::wstring(L"Native error code: ") + numberTo<std::wstring>(nativeErrorCode);
-#endif
- return formatSystemError(functionName, formatCurlStatusCode(ec), errorMsg);
- }
-
const FtpSessionId sessionId_;
CURL* easyHandle_ = nullptr;
- char curlErrorBuf_[CURL_ERROR_SIZE] = {};
- std::string headerData_;
curl_socket_t utf8EnabledSocket_ = 0;
std::optional<Features> featureCache_;
std::optional<AfsPath> homePathCached_;
- std::shared_ptr<UniCounterCookie> libsshCurlUnifiedInitCookie_;
+ const std::shared_ptr<UniCounterCookie> libsshCurlUnifiedInitCookie_;
std::chrono::steady_clock::time_point lastSuccessfulUseTime_;
};
@@ -917,9 +892,9 @@ private:
};
//--------------------------------------------------------------------------------------
-UniInitializer globalStartupInitFtp(*globalFtpSessionCount.get()); //static ordering: place *before* SftpSessionManager instance!
+UniInitializer globalStartupInitFtp(*globalFtpSessionCount.get());
-Global<FtpSessionManager> globalFtpSessionManager(std::make_unique<FtpSessionManager>());
+Global<FtpSessionManager> globalFtpSessionManager; //caveat: life time must be subset of static UniInitializer!
//--------------------------------------------------------------------------------------
void accessFtpSession(const FtpLoginInfo& login, const std::function<void(FtpSession& session)>& useFtpSession /*throw X*/) //throw SysError, X
@@ -962,7 +937,7 @@ public:
{
accessFtpSession(login, [&](FtpSession& session) //throw SysError
{
- std::vector<FtpSession::Option> options =
+ std::vector<CurlOption> options =
{
{ CURLOPT_WRITEDATA, &rawListing },
{ CURLOPT_WRITEFUNCTION, onBytesReceived },
@@ -2167,6 +2142,20 @@ Zstring concatenateFtpFolderPathPhrase(const FtpLoginInfo& login, const AfsPath&
}
+void fff::ftpInit()
+{
+ assert(!globalFtpSessionManager.get());
+ globalFtpSessionManager.set(std::make_unique<FtpSessionManager>());
+}
+
+
+void fff::ftpTeardown()
+{
+ assert(globalFtpSessionManager.get());
+ globalFtpSessionManager.set(nullptr);
+}
+
+
AfsPath fff::getFtpHomePath(const FtpLoginInfo& login) //throw FileError
{
try
diff --git a/FreeFileSync/Source/afs/ftp.h b/FreeFileSync/Source/afs/ftp.h
index b2f7c7ce..12f8fd1a 100644
--- a/FreeFileSync/Source/afs/ftp.h
+++ b/FreeFileSync/Source/afs/ftp.h
@@ -17,6 +17,9 @@ AbstractPath createItemPathFtp(const Zstring& itemPathPhrase); //noexcept
//-------------------------------------------------------
+void ftpInit();
+void ftpTeardown();
+
struct FtpLoginInfo
{
Zstring server;
diff --git a/FreeFileSync/Source/afs/gdrive.cpp b/FreeFileSync/Source/afs/gdrive.cpp
index 073e916c..8c92eb47 100644
--- a/FreeFileSync/Source/afs/gdrive.cpp
+++ b/FreeFileSync/Source/afs/gdrive.cpp
@@ -6,23 +6,23 @@
#include "gdrive.h"
#include <variant>
-#include <unordered_map>
-#include <unordered_set>
-#include <zen/base64.h>
+#include <unordered_set> //needed by clang
+#include <unordered_map> //
+#include <libcurl/rest.h>
#include <zen/basic_math.h>
-#include <zen/file_traverser.h>
-#include <zen/shell_execute.h>
-#include <zen/http.h>
-#include <zen/zlib_wrap.h>
+#include <zen/base64.h>
#include <zen/crc.h>
-#include <zen/json.h>
-#include <zen/time.h>
#include <zen/file_access.h>
+#include <zen/file_io.h>
+#include <zen/file_traverser.h>
#include <zen/guid.h>
+#include <zen/http.h>
+#include <zen/json.h>
+#include <zen/shell_execute.h>
#include <zen/socket.h>
-#include <zen/file_io.h>
+#include <zen/time.h>
+#include <zen/zlib_wrap.h>
#include "abstract_impl.h"
-#include "libcurl/curl_wrap.h" //DON'T include <curl/curl.h> directly!
#include "init_curl_libssh2.h"
#include "../base/resolve_path.h"
@@ -67,13 +67,15 @@ const int GDRIVE_STREAM_BUFFER_SIZE = 512 * 1024; //unit: [byte]
const Zchar googleDrivePrefix[] = Zstr("gdrive:");
const char googleFolderMimeType[] = "application/vnd.google-apps.folder";
-const char DB_FORMAT_DESCR[] = "FreeFileSync: Google Drive Database";
-const int DB_FORMAT_VER = 2; //2019-12-05
+const char DB_FILE_DESCR[] = "FreeFileSync: Google Drive Database";
+const int DB_FILE_VERSION = 2; //2019-12-05
std::string getGoogleDriveClientId () { return ""; } // => replace with live credentials
std::string getGoogleDriveClientSecret() { return ""; } //
+
+
struct HttpSessionId
{
/*explicit*/ HttpSessionId(const Zstring& serverName) :
@@ -98,7 +100,7 @@ Zstring concatenateGoogleFolderPathPhrase(const GdrivePath& gdrivePath) //noexce
}
-//e.g.: gdrive:/zenju@gmx.net/folder/file.txt
+//e.g.: gdrive:/john@gmail.com/folder/file.txt
std::wstring getGoogleDisplayPath(const GdrivePath& gdrivePath)
{
return utfTo<std::wstring>(concatenateGoogleFolderPathPhrase(gdrivePath)); //noexcept
@@ -128,7 +130,7 @@ std::wstring formatGoogleErrorRaw(const std::string& serverResponse)
//the inner message is generally more descriptive!
else if (const JsonValue* errors = getChildFromJsonObject(*error, "errors"))
if (errors->type == JsonValue::Type::array && !errors->arrayVal.empty())
- if (const JsonValue* message = getChildFromJsonObject(*errors->arrayVal[0], "message"))
+ if (const JsonValue* message = getChildFromJsonObject(errors->arrayVal[0], "message"))
if (message->type == JsonValue::Type::string)
return utfTo<std::wstring>(message->primVal);
}
@@ -142,234 +144,9 @@ std::wstring formatGoogleErrorRaw(const std::string& serverResponse)
//----------------------------------------------------------------------------------------------------------------
Global<UniSessionCounter> httpSessionCount(createUniSessionCounter());
-
-
-class HttpSession
-{
-public:
- HttpSession(const HttpSessionId& sessionId, const Zstring& caCertFilePath) : //throw SysError
- sessionId_(sessionId),
- caCertFilePath_(utfTo<std::string>(caCertFilePath)),
- libsshCurlUnifiedInitCookie_(getLibsshCurlUnifiedInitCookie(httpSessionCount)), //throw SysError
- lastSuccessfulUseTime_(std::chrono::steady_clock::now()) {}
-
- ~HttpSession()
- {
- if (easyHandle_)
- ::curl_easy_cleanup(easyHandle_);
- }
-
- struct Option
- {
- template <class T>
- Option(CURLoption o, T val) : option(o), value(static_cast<uint64_t>(val)) { static_assert(sizeof(val) <= sizeof(value)); }
-
- template <class T>
- Option(CURLoption o, T* val) : option(o), value(reinterpret_cast<uint64_t>(val)) { static_assert(sizeof(val) <= sizeof(value)); }
-
- CURLoption option = CURLOPT_LASTENTRY;
- uint64_t value = 0;
- };
-
- struct HttpResult
- {
- int statusCode = 0;
- //std::string contentType;
- };
- HttpResult perform(const std::string& serverRelPath,
- const std::vector<std::string>& extraHeaders, const std::vector<Option>& extraOptions, //throw SysError
- const std::function<void (const void* buffer, size_t bytesToWrite)>& writeResponse /*throw X*/, //optional
- const std::function<size_t( void* buffer, size_t bytesToRead )>& readRequest /*throw X*/) //
- {
- if (!easyHandle_)
- {
- easyHandle_ = ::curl_easy_init();
- if (!easyHandle_)
- throw SysError(formatSystemError(L"curl_easy_init", formatCurlStatusCode(CURLE_OUT_OF_MEMORY), std::wstring()));
- }
- else
- ::curl_easy_reset(easyHandle_);
-
-
- std::vector<Option> options;
-
- curlErrorBuf_[0] = '\0';
- options.emplace_back(CURLOPT_ERRORBUFFER, curlErrorBuf_);
-
- options.emplace_back(CURLOPT_USERAGENT, "FreeFileSync"); //default value; may be overwritten by caller
-
- //lifetime: keep alive until after curl_easy_setopt() below
- std::string curlPath = "https://" + utfTo<std::string>(sessionId_.server) + serverRelPath;
- options.emplace_back(CURLOPT_URL, curlPath.c_str());
-
- options.emplace_back(CURLOPT_NOSIGNAL, 1L); //thread-safety: https://curl.haxx.se/libcurl/c/threadsafe.html
-
- options.emplace_back(CURLOPT_CONNECTTIMEOUT, std::chrono::seconds(HTTP_SESSION_ACCESS_TIME_OUT).count());
-
- //CURLOPT_TIMEOUT: "Since this puts a hard limit for how long time a request is allowed to take, it has limited use in dynamic use cases with varying transfer times."
- options.emplace_back(CURLOPT_LOW_SPEED_TIME, std::chrono::seconds(HTTP_SESSION_ACCESS_TIME_OUT).count());
- options.emplace_back(CURLOPT_LOW_SPEED_LIMIT, 1L); //[bytes], can't use "0" which means "inactive", so use some low number
-
-
- //libcurl forwards this char-string to OpenSSL as is, which - thank god - accepts UTF8
- options.emplace_back(CURLOPT_CAINFO, caCertFilePath_.c_str()); //hopefully latest version from https://curl.haxx.se/docs/caextract.html
- //CURLOPT_SSL_VERIFYPEER => already active by default
- //CURLOPT_SSL_VERIFYHOST =>
-
- //---------------------------------------------------
- std::exception_ptr userCallbackException;
-
- auto onBytesReceived = [&](const void* buffer, size_t len)
- {
- try
- {
- writeResponse(buffer, len); //throw X
- return len;
- }
- catch (...)
- {
- userCallbackException = std::current_exception();
- return len + 1; //signal error condition => CURLE_WRITE_ERROR
- }
- };
- using ReadCbType = decltype(onBytesReceived);
- using ReadCbWrapperType = size_t (*)(const void* buffer, size_t size, size_t nitems, ReadCbType* callbackData); //needed for cdecl function pointer cast
- ReadCbWrapperType onBytesReceivedWrapper = [](const void* buffer, size_t size, size_t nitems, ReadCbType* callbackData)
- {
- return (*callbackData)(buffer, size * nitems); //free this poor little C-API from its shackles and redirect to a proper lambda
- };
- //---------------------------------------------------
- auto getBytesToSend = [&](void* buffer, size_t len) -> size_t
- {
- try
- {
- //libcurl calls back until 0 bytes are returned (Posix read() semantics), or,
- //if CURLOPT_INFILESIZE_LARGE was set, after exactly this amount of bytes
- const size_t bytesRead = readRequest(buffer, len);//throw X; return "bytesToRead" bytes unless end of stream!
- return bytesRead;
- }
- catch (...)
- {
- userCallbackException = std::current_exception();
- return CURL_READFUNC_ABORT; //signal error condition => CURLE_ABORTED_BY_CALLBACK
- }
- };
- using WriteCbType = decltype(getBytesToSend);
- using WriteCbWrapperType = size_t (*)(void* buffer, size_t size, size_t nitems, WriteCbType* callbackData);
- WriteCbWrapperType getBytesToSendWrapper = [](void* buffer, size_t size, size_t nitems, WriteCbType* callbackData)
- {
- return (*callbackData)(buffer, size * nitems); //free this poor little C-API from its shackles and redirect to a proper lambda
- };
- //---------------------------------------------------
- if (writeResponse)
- {
- options.emplace_back(CURLOPT_WRITEDATA, &onBytesReceived);
- options.emplace_back(CURLOPT_WRITEFUNCTION, onBytesReceivedWrapper);
- }
- if (readRequest)
- {
- if (std::all_of(extraOptions.begin(), extraOptions.end(), [](const Option& o) { return o.option != CURLOPT_POST; }))
- options.emplace_back(CURLOPT_UPLOAD, 1L); //issues HTTP PUT
- options.emplace_back(CURLOPT_READDATA, &getBytesToSend);
- options.emplace_back(CURLOPT_READFUNCTION, getBytesToSendWrapper);
- }
-
- if (std::any_of(extraOptions.begin(), extraOptions.end(), [](const Option& o) { return o.option == CURLOPT_WRITEFUNCTION || o.option == CURLOPT_READFUNCTION; }))
- throw std::logic_error("Contract violation! " + std::string(__FILE__) + ":" + numberTo<std::string>(__LINE__)); //Option already used here!
-
- if (readRequest && std::any_of(extraOptions.begin(), extraOptions.end(), [](const Option& o) { return o.option == CURLOPT_POSTFIELDS; }))
- throw std::logic_error("Contract violation! " + std::string(__FILE__) + ":" + numberTo<std::string>(__LINE__)); //Contradicting options: CURLOPT_READFUNCTION, CURLOPT_POSTFIELDS
-
- //---------------------------------------------------
- curl_slist* headers = nullptr; //"libcurl will not copy the entire list so you must keep it!"
- ZEN_ON_SCOPE_EXIT(::curl_slist_free_all(headers));
-
- for (const std::string& headerLine : extraHeaders)
- headers = ::curl_slist_append(headers, headerLine.c_str());
-
- //WTF!!! 1 sec delay when server doesn't support "Expect: 100-continue!! https://stackoverflow.com/questions/49670008/how-to-disable-expect-100-continue-in-libcurl
- headers = ::curl_slist_append(headers, "Expect:"); //guess, what: www.googleapis.com doesn't support it! e.g. gdriveUploadFile()
-
- if (headers)
- options.emplace_back(CURLOPT_HTTPHEADER, headers);
- //---------------------------------------------------
-
- append(options, extraOptions);
-
- for (const Option& opt : options)
- {
- const CURLcode rc = ::curl_easy_setopt(easyHandle_, opt.option, opt.value);
- if (rc != CURLE_OK)
- throw SysError(formatSystemError(L"curl_easy_setopt " + numberTo<std::wstring>(static_cast<int>(opt.option)),
- formatCurlStatusCode(rc), utfTo<std::wstring>(::curl_easy_strerror(rc))));
- }
-
- //=======================================================================================================
- const CURLcode rcPerf = ::curl_easy_perform(easyHandle_);
- //WTF: curl_easy_perform() considers FTP response codes 4XX, 5XX as failure, but for HTTP response codes 4XX are considered success!! CONSISTENCY, people!!!
- //=> at least libcurl is aware: CURLOPT_FAILONERROR: "request failure on HTTP response >= 400"; default: "0, do not fail on error"
- //https://curl.haxx.se/docs/faq.html#curl_doesn_t_return_error_for_HT
- //=> Curiously Google also screws up in their REST API design and returns HTTP 4XX status for domain-level errors!
- //=> let caller handle HTTP status to work around this mess!
-
- if (userCallbackException)
- std::rethrow_exception(userCallbackException); //throw X
- //=======================================================================================================
-
- long httpStatusCode = 0; //optional
- /*const CURLcode rc = */ ::curl_easy_getinfo(easyHandle_, CURLINFO_RESPONSE_CODE, &httpStatusCode);
-
- if (rcPerf != CURLE_OK)
- throw SysError(formatLastCurlError(L"curl_easy_perform", rcPerf, httpStatusCode));
-
- lastSuccessfulUseTime_ = std::chrono::steady_clock::now();
- return { static_cast<int>(httpStatusCode) /*, contentType ? contentType : ""*/ };
- }
-
- //------------------------------------------------------------------------------------------------------------
-
- bool isHealthy() const
- {
- return numeric::dist(std::chrono::steady_clock::now(), lastSuccessfulUseTime_) <= HTTP_SESSION_MAX_IDLE_TIME;
- }
-
- const HttpSessionId& getSessionId() const { return sessionId_; }
-
-private:
- HttpSession (const HttpSession&) = delete;
- HttpSession& operator=(const HttpSession&) = delete;
-
- std::wstring formatLastCurlError(const std::wstring& functionName, CURLcode ec, int httpStatusCode /*optional*/) const
- {
- std::wstring errorMsg;
-
- if (curlErrorBuf_[0] != 0)
- errorMsg = trimCpy(utfTo<std::wstring>(curlErrorBuf_));
-
- if (httpStatusCode != 0) //optional
- errorMsg += (errorMsg.empty() ? L"" : L"\n") + formatHttpStatusCode(httpStatusCode);
-#if 0
- //utfTo<std::wstring>(::curl_easy_strerror(ec)) is uninteresting
- //use CURLINFO_OS_ERRNO ?? https://curl.haxx.se/libcurl/c/CURLINFO_OS_ERRNO.html
- long nativeErrorCode = 0;
- if (::curl_easy_getinfo(easyHandle_, CURLINFO_OS_ERRNO, &nativeErrorCode) == CURLE_OK)
- if (nativeErrorCode != 0)
- errorMsg += (errorMsg.empty() ? L"" : L"\n") + std::wstring(L"Native error code: ") + numberTo<std::wstring>(nativeErrorCode);
-#endif
- return formatSystemError(functionName, formatCurlStatusCode(ec), errorMsg);
- }
-
- const HttpSessionId sessionId_;
- const std::string caCertFilePath_;
- CURL* easyHandle_ = nullptr;
- char curlErrorBuf_[CURL_ERROR_SIZE] = {};
-
- std::shared_ptr<UniCounterCookie> libsshCurlUnifiedInitCookie_;
- std::chrono::steady_clock::time_point lastSuccessfulUseTime_;
-};
+UniInitializer startupInitHttp(*httpSessionCount.get());
//----------------------------------------------------------------------------------------------------------------
-//----------------------------------------------------------------------------------------------------------------
class HttpSessionManager //reuse (healthy) HTTP sessions globally
{
@@ -387,13 +164,11 @@ public:
sessionCleaner_.join();
}
- using IdleHttpSessions = std::vector<std::unique_ptr<HttpSession>>;
-
void access(const HttpSessionId& login, const std::function<void(HttpSession& session)>& useHttpSession /*throw X*/) //throw SysError, X
{
Protected<HttpSessionManager::IdleHttpSessions>& sessionStore = getSessionStore(login);
- std::unique_ptr<HttpSession> httpSession;
+ std::unique_ptr<HttpInitSession> httpSession;
sessionStore.access([&](HttpSessionManager::IdleHttpSessions& sessions)
{
@@ -407,19 +182,32 @@ public:
//create new HTTP session outside the lock: 1. don't block other threads 2. non-atomic regarding "sessionStore"! => one session too many is not a problem!
if (!httpSession)
- httpSession = std::make_unique<HttpSession>(login, caCertFilePath_); //throw SysError
+ httpSession = std::make_unique<HttpInitSession>(getLibsshCurlUnifiedInitCookie(httpSessionCount), login.server, caCertFilePath_); //throw SysError
ZEN_ON_SCOPE_EXIT(
- if (httpSession->isHealthy()) //thread that created the "!isHealthy()" session is responsible for clean up (avoid hitting server connection limits!)
+ if (isHealthy(httpSession->session)) //thread that created the "!isHealthy()" session is responsible for clean up (avoid hitting server connection limits!)
sessionStore.access([&](HttpSessionManager::IdleHttpSessions& sessions) { sessions.push_back(std::move(httpSession)); }); );
- useHttpSession(*httpSession); //throw X
+ useHttpSession(httpSession->session); //throw X
}
private:
HttpSessionManager (const HttpSessionManager&) = delete;
HttpSessionManager& operator=(const HttpSessionManager&) = delete;
+ //associate session counting (for initialization/teardown)
+ struct HttpInitSession
+ {
+ HttpInitSession(std::shared_ptr<UniCounterCookie> cook, const Zstring& server, const Zstring& caCertFilePath) :
+ cookie(std::move(cook)), session(server, caCertFilePath, HTTP_SESSION_ACCESS_TIME_OUT) {}
+
+ std::shared_ptr<UniCounterCookie> cookie;
+ HttpSession session; //life time must be subset of UniCounterCookie
+ };
+ static bool isHealthy(const HttpSession& s) { return numeric::dist(std::chrono::steady_clock::now(), s.getLastUseTime()) <= HTTP_SESSION_MAX_IDLE_TIME; }
+
+ using IdleHttpSessions = std::vector<std::unique_ptr<HttpInitSession>>;
+
Protected<IdleHttpSessions>& getSessionStore(const HttpSessionId& login)
{
//single global session store per login; life-time bound to globalInstance => never remove a sessionStore!!!
@@ -460,8 +248,8 @@ private:
for (bool done = false; !done;)
sessionStore->access([&](IdleHttpSessions& sessions)
{
- for (std::unique_ptr<HttpSession>& sshSession : sessions)
- if (!sshSession->isHealthy()) //!isHealthy() sessions are destroyed after use => in this context this means they have been idle for too long
+ for (std::unique_ptr<HttpInitSession>& sshSession : sessions)
+ if (!isHealthy(sshSession->session)) //!isHealthy() sessions are destroyed after use => in this context this means they have been idle for too long
{
sshSession.swap(sessions.back());
/**/ sessions.pop_back(); //run ~HttpSession *inside* the lock! => avoid hitting server limits!
@@ -481,35 +269,32 @@ private:
};
//--------------------------------------------------------------------------------------
-UniInitializer startupInitHttp(*httpSessionCount.get()); //static ordering: place *before* HttpSessionManager instance!
-
-Global<HttpSessionManager> httpSessionManager;
+Global<HttpSessionManager> globalHttpSessionManager; //caveat: life time must be subset of static UniInitializer!
//--------------------------------------------------------------------------------------
//===========================================================================================================================
//try to get a grip on this crazy REST API: - parameters are passed via query string, header, or body, using GET, POST, PUT, PATCH, DELETE, ... it's a dice roll
-HttpSession::HttpResult googleHttpsRequest(const std::string& serverRelPath, //throw SysError
- const std::vector<std::string>& extraHeaders,
- const std::vector<HttpSession::Option>& extraOptions,
- const std::function<void (const void* buffer, size_t bytesToWrite)>& writeResponse /*throw X*/, //optional
- const std::function<size_t( void* buffer, size_t bytesToRead )>& readRequest /*throw X*/) //optional; returning 0 signals EOF
+HttpSession::Result googleHttpsRequest(const std::string& serverRelPath, //throw SysError
+ const std::vector<std::string>& extraHeaders,
+ const std::vector<CurlOption>& extraOptions,
+ const std::function<void (const void* buffer, size_t bytesToWrite)>& writeResponse /*throw X*/, //optional
+ const std::function<size_t( void* buffer, size_t bytesToRead )>& readRequest /*throw X*/) //optional; returning 0 signals EOF
{
- const std::shared_ptr<HttpSessionManager> mgr = httpSessionManager.get();
+ const std::shared_ptr<HttpSessionManager> mgr = globalHttpSessionManager.get();
if (!mgr)
throw SysError(L"googleHttpsRequest() function call not allowed during init/shutdown.");
- HttpSession::HttpResult httpResult;
+ HttpSession::Result httpResult;
mgr->access(HttpSessionId(GOOGLE_REST_API_SERVER), [&](HttpSession& session) //throw SysError
{
- std::vector<HttpSession::Option> options =
+ std::vector<CurlOption> options =
{
//https://developers.google.com/drive/api/v3/performance
- //"In order to receive a gzip-encoded response you must do two things: Set an Accept-Encoding header, and modify your user agent to contain the string gzip."
- { CURLOPT_ACCEPT_ENCODING, "gzip" },
- { CURLOPT_USERAGENT, "FreeFileSync (gzip)" },
+ //"In order to receive a gzip-encoded response you must do two things: Set an Accept-Encoding header, ["gzip" automatically set by HttpSession]
+ { CURLOPT_USERAGENT, "FreeFileSync (gzip)" }, // and modify your user agent to contain the string gzip."
};
append(options, extraOptions);
@@ -552,14 +337,15 @@ GoogleUserInfo getUserInfo(const std::string& accessToken) //throw SysError
}
-const char* htmlMessageTemplate = R""(<!doctype html>
+const char htmlMessageTemplate[] = R"(<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TITLE_PLACEHOLDER</title>
- <style type="text/css">
+ <style>
* {
- font-family: "Helvetica Neue", "Segoe UI", Segoe, Helvetica, Arial, "Lucida Grande", sans-serif;
+ font-family: -apple-system, 'Segoe UI', arial, Tahoma, Helvetica, sans-serif;
text-align: center;
background-color: #eee; }
h1 {
@@ -572,11 +358,11 @@ const char* htmlMessageTemplate = R""(<!doctype html>
</style>
</head>
<body>
- <h1><img src="https://freefilesync.org/images/FreeFileSync.png" style="vertical-align:middle; height: 50px;" alt=""> TITLE_PLACEHOLDER</h1>
+ <h1><img src="https://freefilesync.org/images/FreeFileSync.png" style="vertical-align:middle; height:50px;" alt=""> TITLE_PLACEHOLDER</h1>
<div class="descr">MESSAGE_PLACEHOLDER</div>
</body>
</html>
-)"";
+)";
struct GoogleAuthCode
{
@@ -690,12 +476,12 @@ GoogleAccessInfo authorizeAccessToGoogleDrive(const Zstring& googleLoginHint, co
addr.ss_family != AF_INET6)
throw SysError(L"getsockname: unknown protocol family (" + numberTo<std::wstring>(addr.ss_family) + L")");
- const int port = ntohs(reinterpret_cast<const sockaddr_in&>(addr).sin_port);
- //the socket is not bound to a specific local IP => inet_ntoa(reinterpret_cast<const sockaddr_in&>(addr).sin_addr) == "0.0.0.0"
- const std::string redirectUrl = "http://127.0.0.1:" + numberTo<std::string>(port);
+const int port = ntohs(reinterpret_cast<const sockaddr_in&>(addr).sin_port);
+//the socket is not bound to a specific local IP => inet_ntoa(reinterpret_cast<const sockaddr_in&>(addr).sin_addr) == "0.0.0.0"
+const std::string redirectUrl = "http://127.0.0.1:" + numberTo<std::string>(port);
- if (::listen(socket, SOMAXCONN) != 0)
- THROW_LAST_SYS_ERROR_WSA(L"listen");
+if (::listen(socket, SOMAXCONN) != 0)
+ THROW_LAST_SYS_ERROR_WSA(L"listen");
//"A code_verifier is a high-entropy cryptographic random string using the unreserved characters:"
@@ -708,132 +494,132 @@ GoogleAccessInfo authorizeAccessToGoogleDrive(const Zstring& googleLoginHint, co
//authenticate Google Drive via browser: https://developers.google.com/identity/protocols/OAuth2InstalledApp#step-2-send-a-request-to-googles-oauth-20-server
const std::string oauthUrl = "https://accounts.google.com/o/oauth2/v2/auth?" + xWwwFormUrlEncode(
+{
+ { "client_id", getGoogleDriveClientId() },
+ { "redirect_uri", redirectUrl },
+ { "response_type", "code" },
+ { "scope", "https://www.googleapis.com/auth/drive" },
+ { "code_challenge", codeChallenge },
+ { "code_challenge_method", "plain" },
+ { "login_hint", utfTo<std::string>(googleLoginHint) },
+});
+try
+{
+ openWithDefaultApplication(utfTo<Zstring>(oauthUrl)); //throw FileError
+}
+catch (const FileError& e) { throw SysError(e.toString()); } //errors should be further enriched by context info => SysError
+
+//process incoming HTTP requests
+for (;;)
+{
+for (;;) //::accept() blocks forever if no client connects (e.g. user just closes the browser window!) => wait for incoming traffic with a time-out via ::select()
{
- { "client_id", getGoogleDriveClientId() },
- { "redirect_uri", redirectUrl },
- { "response_type", "code" },
- { "scope", "https://www.googleapis.com/auth/drive" },
- { "code_challenge", codeChallenge },
- { "code_challenge_method", "plain" },
- { "login_hint", utfTo<std::string>(googleLoginHint) },
- });
- try
- {
- openWithDefaultApplication(utfTo<Zstring>(oauthUrl)); //throw FileError
+ if (updateGui) updateGui(); //throw X
+
+ fd_set rfd = {};
+ FD_ZERO(&rfd);
+ FD_SET(socket, &rfd);
+ fd_set* readfds = &rfd;
+
+ struct ::timeval tv = {};
+ tv.tv_usec = static_cast<long>(100 /*ms*/) * 1000;
+
+ //WSAPoll broken, even ::poll() on OS X? https://daniel.haxx.se/blog/2012/10/10/wsapoll-is-broken/
+ //perf: no significant difference compared to ::WSAPoll()
+ const int rc = ::select(socket + 1, readfds, nullptr /*writefds*/, nullptr /*errorfds*/, &tv);
+ if (rc < 0)
+ THROW_LAST_SYS_ERROR_WSA(L"select");
+ if (rc != 0)
+ break;
+ //else: time-out!
}
- catch (const FileError& e) { throw SysError(e.toString()); } //errors should be further enriched by context info => SysError
+ //potential race! if the connection is gone right after ::select() and before ::accept(), latter will hang
+ const SocketType clientSocket = ::accept(socket, //SOCKET s,
+ nullptr, //sockaddr *addr,
+ nullptr); //int *addrlen
+ if (clientSocket == invalidSocket)
+ THROW_LAST_SYS_ERROR_WSA(L"accept");
- //process incoming HTTP requests
+ //receive first line of HTTP request
+ std::string reqLine;
for (;;)
{
- for (;;) //::accept() blocks forever if no client connects (e.g. user just closes the browser window!) => wait for incoming traffic with a time-out via ::select()
- {
- if (updateGui) updateGui(); //throw X
-
- fd_set rfd = {};
- FD_ZERO(&rfd);
- FD_SET(socket, &rfd);
- fd_set* readfds = &rfd;
+ const size_t blockSize = 64 * 1024;
+ reqLine.resize(reqLine.size() + blockSize);
+ const size_t bytesReceived = tryReadSocket(clientSocket, &*(reqLine.end() - blockSize), blockSize); //throw SysError
+ reqLine.resize(reqLine.size() - blockSize + bytesReceived); //caveat: unsigned arithmetics
- struct ::timeval tv = {};
- tv.tv_usec = static_cast<long>(100 /*ms*/) * 1000;
-
- //WSAPoll broken, even ::poll() on OS X? https://daniel.haxx.se/blog/2012/10/10/wsapoll-is-broken/
- //perf: no significant difference compared to ::WSAPoll()
- const int rc = ::select(socket + 1, readfds, nullptr /*writefds*/, nullptr /*errorfds*/, &tv);
- if (rc < 0)
- THROW_LAST_SYS_ERROR_WSA(L"select");
- if (rc != 0)
- break;
- //else: time-out!
- }
- //potential race! if the connection is gone right after ::select() and before ::accept(), latter will hang
- const SocketType clientSocket = ::accept(socket, //SOCKET s,
- nullptr, //sockaddr *addr,
- nullptr); //int *addrlen
- if (clientSocket == invalidSocket)
- THROW_LAST_SYS_ERROR_WSA(L"accept");
-
- //receive first line of HTTP request
- std::string reqLine;
- for (;;)
+ if (contains(reqLine, "\r\n"))
{
- const size_t blockSize = 64 * 1024;
- reqLine.resize(reqLine.size() + blockSize);
- const size_t bytesReceived = tryReadSocket(clientSocket, &*(reqLine.end() - blockSize), blockSize); //throw SysError
- reqLine.resize(reqLine.size() - blockSize + bytesReceived); //caveat: unsigned arithmetics
-
- if (contains(reqLine, "\r\n"))
- {
- reqLine = beforeFirst(reqLine, "\r\n", IF_MISSING_RETURN_NONE);
- break;
- }
- if (bytesReceived == 0 || reqLine.size() >= 100000 /*bogus line length*/)
- break;
+ reqLine = beforeFirst(reqLine, "\r\n", IF_MISSING_RETURN_NONE);
+ break;
}
+ if (bytesReceived == 0 || reqLine.size() >= 100000 /*bogus line length*/)
+ break;
+ }
- //get OAuth2.0 authorization result from Google, either:
- std::string code;
- std::string error;
+ //get OAuth2.0 authorization result from Google, either:
+ std::string code;
+ std::string error;
- //parse header; e.g.: GET http://127.0.0.1:62054/?code=4/ZgBRsB9k68sFzc1Pz1q0__Kh17QK1oOmetySrGiSliXt6hZtTLUlYzm70uElNTH9vt1OqUMzJVeFfplMsYsn4uI HTTP/1.1
- const std::vector<std::string> statusItems = split(reqLine, ' ', SplitType::ALLOW_EMPTY); //Method SP Request-URI SP HTTP-Version CRLF
+ //parse header; e.g.: GET http://127.0.0.1:62054/?code=4/ZgBRsB9k68sFzc1Pz1q0__Kh17QK1oOmetySrGiSliXt6hZtTLUlYzm70uElNTH9vt1OqUMzJVeFfplMsYsn4uI HTTP/1.1
+ const std::vector<std::string> statusItems = split(reqLine, ' ', SplitType::ALLOW_EMPTY); //Method SP Request-URI SP HTTP-Version CRLF
- if (statusItems.size() == 3 && statusItems[0] == "GET" && startsWith(statusItems[2], "HTTP/"))
- {
- for (const auto& [name, value] : xWwwFormUrlDecode(afterFirst(statusItems[1], "?", IF_MISSING_RETURN_NONE)))
- if (name == "code")
- code = value;
- else if (name == "error")
- error = value; //e.g. "access_denied" => no more detailed error info available :(
- } //"add explicit braces to avoid dangling else [-Wdangling-else]"
+ if (statusItems.size() == 3 && statusItems[0] == "GET" && startsWith(statusItems[2], "HTTP/"))
+ {
+ for (const auto& [name, value] : xWwwFormUrlDecode(afterFirst(statusItems[1], "?", IF_MISSING_RETURN_NONE)))
+ if (name == "code")
+ code = value;
+ else if (name == "error")
+ error = value; //e.g. "access_denied" => no more detailed error info available :(
+ } //"add explicit braces to avoid dangling else [-Wdangling-else]"
- std::optional<std::variant<GoogleAccessInfo, SysError>> authResult;
+ std::optional<std::variant<GoogleAccessInfo, SysError>> authResult;
- //send HTTP response; https://www.w3.org/Protocols/HTTP/1.0/spec.html#Request-Line
- std::string httpResponse;
- if (code.empty() && error.empty()) //parsing error or unrelated HTTP request
- httpResponse = "HTTP/1.0 400 Bad Request" "\r\n" "\r\n" "400 Bad Request\n" + reqLine;
- else
+ //send HTTP response; https://www.w3.org/Protocols/HTTP/1.0/spec.html#Request-Line
+ std::string httpResponse;
+ if (code.empty() && error.empty()) //parsing error or unrelated HTTP request
+ httpResponse = "HTTP/1.0 400 Bad Request" "\r\n" "\r\n" "400 Bad Request\n" + reqLine;
+ else
+ {
+ std::string htmlMsg = htmlMessageTemplate;
+ try
{
- std::string htmlMsg = htmlMessageTemplate;
- try
- {
- if (!error.empty())
- throw SysError(replaceCpy(_("Error Code %x"), L"%x", + L"\"" + utfTo<std::wstring>(error) + L"\""));
-
- //do as many login-related tasks as possible while we have the browser as an error output device!
- //see AFS::connectNetworkFolder() => errors will be lost after time out in dir_exist_async.h!
- authResult = googleDriveExchangeAuthCode({ code, redirectUrl, codeChallenge }); //throw SysError
- replace(htmlMsg, "TITLE_PLACEHOLDER", utfTo<std::string>(_("Authentication completed.")));
- replace(htmlMsg, "MESSAGE_PLACEHOLDER", utfTo<std::string>(_("You may close this page now and continue with FreeFileSync.")));
- }
- catch (const SysError& e)
- {
- authResult = e;
- replace(htmlMsg, "TITLE_PLACEHOLDER", utfTo<std::string>(_("Authentication failed.")));
- replace(htmlMsg, "MESSAGE_PLACEHOLDER", utfTo<std::string>(replaceCpy(_("Unable to connect to %x."), L"%x", L"Google Drive") + L"\n\n" + e.toString()));
- }
- httpResponse = "HTTP/1.0 200 OK" "\r\n"
- "Content-Type: text/html" "\r\n"
- "Content-Length: " + numberTo<std::string>(strLength(htmlMsg)) + "\r\n"
- "\r\n" + htmlMsg;
+ if (!error.empty())
+ throw SysError(replaceCpy(_("Error Code %x"), L"%x", + L"\"" + utfTo<std::wstring>(error) + L"\""));
+
+ //do as many login-related tasks as possible while we have the browser as an error output device!
+ //see AFS::connectNetworkFolder() => errors will be lost after time out in dir_exist_async.h!
+ authResult = googleDriveExchangeAuthCode({ code, redirectUrl, codeChallenge }); //throw SysError
+ replace(htmlMsg, "TITLE_PLACEHOLDER", utfTo<std::string>(_("Authentication completed.")));
+ replace(htmlMsg, "MESSAGE_PLACEHOLDER", utfTo<std::string>(_("You may close this page now and continue with FreeFileSync.")));
}
+ catch (const SysError& e)
+ {
+ authResult = e;
+ replace(htmlMsg, "TITLE_PLACEHOLDER", utfTo<std::string>(_("Authentication failed.")));
+ replace(htmlMsg, "MESSAGE_PLACEHOLDER", utfTo<std::string>(replaceCpy(_("Unable to connect to %x."), L"%x", L"Google Drive") + L"\n\n" + e.toString()));
+ }
+ httpResponse = "HTTP/1.0 200 OK" "\r\n"
+ "Content-Type: text/html" "\r\n"
+ "Content-Length: " + numberTo<std::string>(strLength(htmlMsg)) + "\r\n"
+ "\r\n" + htmlMsg;
+ }
- for (size_t bytesToSend = httpResponse.size(); bytesToSend > 0;)
- bytesToSend -= tryWriteSocket(clientSocket, &*(httpResponse.end() - bytesToSend), bytesToSend); //throw SysError
+ for (size_t bytesToSend = httpResponse.size(); bytesToSend > 0;)
+ bytesToSend -= tryWriteSocket(clientSocket, &*(httpResponse.end() - bytesToSend), bytesToSend); //throw SysError
- shutdownSocketSend(clientSocket); //throw SysError
- //---------------------------------------------------------------
+ shutdownSocketSend(clientSocket); //throw SysError
+ //---------------------------------------------------------------
- if (authResult)
- {
- if (const SysError* e = std::get_if<SysError>(&*authResult))
- throw *e;
- return std::get<GoogleAccessInfo>(*authResult);
- }
+ if (authResult)
+ {
+ if (const SysError* e = std::get_if<SysError>(&*authResult))
+ throw *e;
+ return std::get<GoogleAccessInfo>(*authResult);
}
}
+}
GoogleAccessToken refreshAccessToGoogleDrive(const std::string& refreshToken) //throw SysError
@@ -867,11 +653,11 @@ GoogleAccessToken refreshAccessToGoogleDrive(const std::string& refreshToken) //
void revokeAccessToGoogleDrive(const std::string& accessToken, const Zstring& googleUserEmail) //throw SysError
{
//https://developers.google.com/identity/protocols/OAuth2InstalledApp#tokenrevoke
- const std::shared_ptr<HttpSessionManager> mgr = httpSessionManager.get();
+ const std::shared_ptr<HttpSessionManager> mgr = globalHttpSessionManager.get();
if (!mgr)
throw SysError(L"revokeAccessToGoogleDrive() Function call not allowed during process init/shutdown.");
- HttpSession::HttpResult httpResult;
+ HttpSession::Result httpResult;
std::string response;
mgr->access(HttpSessionId(Zstr("accounts.google.com")), [&](HttpSession& session) //throw SysError
@@ -977,13 +763,13 @@ std::vector<GoogleFileItem> readFolderContent(const std::string& folderId, const
for (const auto& childVal : files->arrayVal)
{
- const std::optional<std::string> itemId = getPrimitiveFromJsonObject(*childVal, "id");
- const std::optional<std::string> itemName = getPrimitiveFromJsonObject(*childVal, "name");
- const std::optional<std::string> mimeType = getPrimitiveFromJsonObject(*childVal, "mimeType");
- const std::optional<std::string> shared = getPrimitiveFromJsonObject(*childVal, "shared");
- const std::optional<std::string> size = getPrimitiveFromJsonObject(*childVal, "size");
- const std::optional<std::string> modifiedTime = getPrimitiveFromJsonObject(*childVal, "modifiedTime");
- const JsonValue* parents = getChildFromJsonObject (*childVal, "parents");
+ const std::optional<std::string> itemId = getPrimitiveFromJsonObject(childVal, "id");
+ const std::optional<std::string> itemName = getPrimitiveFromJsonObject(childVal, "name");
+ const std::optional<std::string> mimeType = getPrimitiveFromJsonObject(childVal, "mimeType");
+ const std::optional<std::string> shared = getPrimitiveFromJsonObject(childVal, "shared");
+ const std::optional<std::string> size = getPrimitiveFromJsonObject(childVal, "size");
+ const std::optional<std::string> modifiedTime = getPrimitiveFromJsonObject(childVal, "modifiedTime");
+ const JsonValue* parents = getChildFromJsonObject (childVal, "parents");
if (!itemId || !itemName || !mimeType || !modifiedTime || !parents)
throw SysError(formatGoogleErrorRaw(response));
@@ -1010,9 +796,9 @@ std::vector<GoogleFileItem> readFolderContent(const std::string& folderId, const
std::vector<std::string> parentIds;
for (const auto& parentVal : parents->arrayVal)
{
- if (parentVal->type != JsonValue::Type::string)
+ if (parentVal.type != JsonValue::Type::string)
throw SysError(formatGoogleErrorRaw(response));
- parentIds.push_back(parentVal->primVal);
+ parentIds.push_back(parentVal.primVal);
}
assert(std::find(parentIds.begin(), parentIds.end(), folderId) != parentIds.end());
@@ -1071,10 +857,10 @@ ChangesDelta getChangesDelta(const std::string& startPageToken, const std::strin
for (const auto& childVal : changes->arrayVal)
{
- const std::optional<std::string> kind = getPrimitiveFromJsonObject(*childVal, "kind");
- const std::optional<std::string> removed = getPrimitiveFromJsonObject(*childVal, "removed");
- const std::optional<std::string> itemId = getPrimitiveFromJsonObject(*childVal, "fileId");
- const JsonValue* file = getChildFromJsonObject (*childVal, "file");
+ const std::optional<std::string> kind = getPrimitiveFromJsonObject(childVal, "kind");
+ const std::optional<std::string> removed = getPrimitiveFromJsonObject(childVal, "removed");
+ const std::optional<std::string> itemId = getPrimitiveFromJsonObject(childVal, "fileId");
+ const JsonValue* file = getChildFromJsonObject (childVal, "file");
if (!kind || *kind != "drive#change" || !removed || !itemId)
throw SysError(formatGoogleErrorRaw(response));
@@ -1120,9 +906,9 @@ ChangesDelta getChangesDelta(const std::string& startPageToken, const std::strin
for (const auto& parentVal : parents->arrayVal)
{
- if (parentVal->type != JsonValue::Type::string)
+ if (parentVal.type != JsonValue::Type::string)
throw SysError(formatGoogleErrorRaw(response));
- itemDetails.parentIds.push_back(parentVal->primVal);
+ itemDetails.parentIds.push_back(parentVal.primVal);
}
changeItem.details = std::move(itemDetails);
}
@@ -1164,7 +950,7 @@ void gdriveDeleteItem(const std::string& itemId, const std::string& accessToken)
{
//https://developers.google.com/drive/api/v3/reference/files/delete
std::string response;
- const HttpSession::HttpResult httpResult = googleHttpsRequest("/drive/v3/files/" + itemId, { "Authorization: Bearer " + accessToken }, //throw SysError
+ const HttpSession::Result httpResult = googleHttpsRequest("/drive/v3/files/" + itemId, { "Authorization: Bearer " + accessToken }, //throw SysError
{ { CURLOPT_CUSTOMREQUEST, "DELETE" } },
[&](const void* buffer, size_t bytesToWrite) { response.append(static_cast<const char*>(buffer), bytesToWrite); }, nullptr /*readRequest*/);
@@ -1201,7 +987,7 @@ void gdriveUnlinkParent(const std::string& itemId, const std::string& parentFold
if (parents) //when last parent is removed (=> Google deletes item permanently), Google does NOT return the parents array (not even an empty one!)
if (parents->type != JsonValue::Type::array ||
std::any_of(parents->arrayVal.begin(), parents->arrayVal.end(),
- [&](const std::unique_ptr<JsonValue>& jval) { return jval->type == JsonValue::Type::string && jval->primVal == parentFolderId; }))
+ [&](const JsonValue& jval) { return jval.type == JsonValue::Type::string && jval.primVal == parentFolderId; }))
throw SysError(L"gdriveUnlinkParent: Google Drive internal failure"); //user should never see this...
}
@@ -1298,7 +1084,7 @@ void gdriveMoveAndRenameItem(const std::string& itemId, const std::string& paren
throw SysError(formatGoogleErrorRaw(response));
if (!std::any_of(parents->arrayVal.begin(), parents->arrayVal.end(),
- [&](const std::unique_ptr<JsonValue>& jval) { return jval->type == JsonValue::Type::string && jval->primVal == parentIdTo; }))
+ [&](const JsonValue& jval) { return jval.type == JsonValue::Type::string && jval.primVal == parentIdTo; }))
throw SysError(L"gdriveMoveAndRenameItem: Google Drive internal failure"); //user should never see this...
}
@@ -1336,7 +1122,7 @@ void gdriveDownloadFile(const std::string& itemId, const std::function<void(cons
{
//https://developers.google.com/drive/api/v3/manage-downloads
std::string response;
- const HttpSession::HttpResult httpResult = googleHttpsRequest("/drive/v3/files/" + itemId + "?alt=media", //throw SysError, X
+ const HttpSession::Result httpResult = googleHttpsRequest("/drive/v3/files/" + itemId + "?alt=media", //throw SysError, X
{ "Authorization: Bearer " + accessToken }, {} /*extraOptions*/,
[&](const void* buffer, size_t bytesToWrite)
{
@@ -1427,7 +1213,7 @@ TODO:
gzip-compress HTTP request body!
std::string response;
- const HttpSession::HttpResult httpResult = googleHttpsRequest("/upload/drive/v3/files?uploadType=multipart", //throw SysError, X
+ const HttpSession::Result httpResult = googleHttpsRequest("/upload/drive/v3/files?uploadType=multipart", //throw SysError, X
{
"Authorization: Bearer " + accessToken,
"Content-Type: multipart/related; boundary=" + boundaryString,
@@ -1496,7 +1282,7 @@ std::string /*itemId*/ gdriveUploadFile(const Zstring& fileName, const std::stri
};
std::string response;
- const HttpSession::HttpResult httpResult = googleHttpsRequest("/upload/drive/v3/files?uploadType=resumable", //throw SysError
+ const HttpSession::Result httpResult = googleHttpsRequest("/upload/drive/v3/files?uploadType=resumable", //throw SysError
{ "Authorization: Bearer " + accessToken, "Content-Type: application/json; charset=UTF-8" },
{ { CURLOPT_POSTFIELDS, postBuf.c_str() }, { CURLOPT_HEADERDATA, &onBytesReceived }, { CURLOPT_HEADERFUNCTION, onBytesReceivedWrapper } },
[&](const void* buffer, size_t bytesToWrite) { response.append(static_cast<const char*>(buffer), bytesToWrite); }, nullptr /*readRequest*/);
@@ -2208,8 +1994,8 @@ private:
{
MemoryStreamOut<ByteArray> streamOut;
- writeArray(streamOut, DB_FORMAT_DESCR, sizeof(DB_FORMAT_DESCR));
- writeNumber<int32_t>(streamOut, DB_FORMAT_VER);
+ writeArray(streamOut, DB_FILE_DESCR, sizeof(DB_FILE_DESCR));
+ writeNumber<int32_t>(streamOut, DB_FILE_VERSION);
userSession.accessBuf.ref().serialize(streamOut);
userSession.fileState.ref().serialize(streamOut);
@@ -2234,25 +2020,25 @@ private:
}
catch (const SysError& e) { throw FileError(_("Database file is corrupted:") + L" " + fmtPath(dbFilePath), e.toString()); }
- MemoryStreamIn<ByteArray> streamIn(rawStream);
+ MemoryStreamIn streamIn(rawStream);
try
{
//-------- file format header --------
- char tmp[sizeof(DB_FORMAT_DESCR)] = {};
+ char tmp[sizeof(DB_FILE_DESCR)] = {};
readArray(streamIn, &tmp, sizeof(tmp)); //throw UnexpectedEndOfStreamError
- if (!std::equal(std::begin(tmp), std::end(tmp), std::begin(DB_FORMAT_DESCR)))
+ if (!std::equal(std::begin(tmp), std::end(tmp), std::begin(DB_FILE_DESCR)))
throw FileError(replaceCpy(_("Database file %x is incompatible."), L"%x", fmtPath(dbFilePath)));
- const int dbVersion = readNumber<int32_t>(streamIn);
+ const int version = readNumber<int32_t>(streamIn);
//TODO: remove migration code at some time! 2019-12-05
- if (dbVersion != 1 &&
- dbVersion != DB_FORMAT_VER)
+ if (version != 1 &&
+ version != DB_FILE_VERSION)
throw FileError(replaceCpy(_("Database file %x is incompatible."), L"%x", fmtPath(dbFilePath)));
auto accessBuf = makeSharedRef<GoogleAccessBuffer>(streamIn); //throw UnexpectedEndOfStreamError
- auto fileState = makeSharedRef<GoogleFileState >(streamIn, accessBuf.ref(), dbVersion); //throw UnexpectedEndOfStreamError
+ auto fileState = makeSharedRef<GoogleFileState >(streamIn, accessBuf.ref(), version); //throw UnexpectedEndOfStreamError
return { accessBuf, fileState };
}
catch (UnexpectedEndOfStreamError&) { throw FileError(_("Database file is corrupted:") + L" " + fmtPath(dbFilePath), L"Unexpected end of stream."); }
@@ -2982,8 +2768,8 @@ private:
void fff::googleDriveInit(const Zstring& configDirPath, const Zstring& caCertFilePath)
{
- assert(!httpSessionManager.get());
- httpSessionManager.set(std::make_unique<HttpSessionManager>(caCertFilePath));
+ assert(!globalHttpSessionManager.get());
+ globalHttpSessionManager.set(std::make_unique<HttpSessionManager>(caCertFilePath));
assert(!globalGoogleSessions.get());
globalGoogleSessions.set(std::make_unique<GooglePersistentSessions>(configDirPath));
@@ -3002,8 +2788,8 @@ void fff::googleDriveTeardown()
assert(globalGoogleSessions.get());
globalGoogleSessions.set(nullptr);
- assert(httpSessionManager.get());
- httpSessionManager.set(nullptr);
+ assert(globalHttpSessionManager.get());
+ globalHttpSessionManager.set(nullptr);
}
@@ -3052,7 +2838,7 @@ Zstring fff::condenseToGoogleFolderPathPhrase(const Zstring& userEmail, const Zs
}
-//e.g.: gdrive:/zenju@gmx.net/folder/file.txt
+//e.g.: gdrive:/john@gmail.com/folder/file.txt
GdrivePath fff::getResolvedGooglePath(const Zstring& folderPathPhrase) //noexcept
{
Zstring path = folderPathPhrase;
diff --git a/FreeFileSync/Source/afs/init_curl_libssh2.cpp b/FreeFileSync/Source/afs/init_curl_libssh2.cpp
index 76556b43..57cbfa95 100644
--- a/FreeFileSync/Source/afs/init_curl_libssh2.cpp
+++ b/FreeFileSync/Source/afs/init_curl_libssh2.cpp
@@ -8,9 +8,9 @@
#include <cassert>
#include <zen/thread.h>
#include <zen/file_error.h>
+#include <libcurl/curl_wrap.h> //DON'T include <curl/curl.h> directly!
#include <zen/open_ssl.h>
#include "libssh2/libssh2_wrap.h" //DON'T include <libssh2_sftp.h> directly!
-#include "libcurl/curl_wrap.h" //DON'T include <curl/curl.h> directly!
using namespace zen;
diff --git a/FreeFileSync/Source/afs/sftp.cpp b/FreeFileSync/Source/afs/sftp.cpp
index 1fcedc43..d6279ebd 100644
--- a/FreeFileSync/Source/afs/sftp.cpp
+++ b/FreeFileSync/Source/afs/sftp.cpp
@@ -12,7 +12,7 @@
#include <zen/basic_math.h>
#include <zen/socket.h>
#include <zen/open_ssl.h>
-#include "libssh2/libssh2_wrap.h" //DON'T include <libssh2_sftp.h> directly!
+#include <libssh2/libssh2_wrap.h> //DON'T include <libssh2_sftp.h> directly!
#include "init_curl_libssh2.h"
#include "ftp_common.h"
#include "abstract_impl.h"
@@ -55,6 +55,14 @@ const std::chrono::seconds SFTP_SESSION_MAX_IDLE_TIME (20);
const std::chrono::seconds SFTP_SESSION_CLEANUP_INTERVAL (4); //facilitate default of 5-seconds delay for error retry
const std::chrono::seconds SFTP_CHANNEL_LIMIT_DETECTION_TIME_OUT(30);
+//rw- r-- r-- [0644] default permissions for newly created files
+const long SFTP_DEFAULT_PERMISSION_FILE = LIBSSH2_SFTP_S_IRUSR | LIBSSH2_SFTP_S_IWUSR | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IROTH;
+
+//rw- r-- r-- [0755] default permissions for newly created folders
+const long SFTP_DEFAULT_PERMISSION_FOLDER = LIBSSH2_SFTP_S_IRUSR | LIBSSH2_SFTP_S_IWUSR | LIBSSH2_SFTP_S_IXUSR |
+ LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IXGRP |
+ LIBSSH2_SFTP_S_IROTH | LIBSSH2_SFTP_S_IXOTH;
+
//attention: if operation fails due to time out, e.g. file copy, the cleanup code may hang, too => total delay = 2 x time out interval
const size_t SFTP_OPTIMAL_BLOCK_SIZE_READ = 4 * MAX_SFTP_READ_SIZE; //https://github.com/libssh2/libssh2/issues/90
@@ -105,11 +113,10 @@ struct SshSessionId
Zstring server;
int port = 0;
Zstring username;
- SftpAuthType authType = SftpAuthType::PASSWORD;
+ SftpAuthType authType = SftpAuthType::password;
Zstring password;
Zstring privateKeyFilePath;
- //traverserChannelsPerConnection => irrelevant for session equality
- //timeoutSec
+ //timeoutSec, traverserChannelsPerConnection => irrelevant for session equality
};
@@ -132,17 +139,17 @@ bool operator<(const SshSessionId& lhs, const SshSessionId& rhs)
switch (lhs.authType)
{
- case SftpAuthType::PASSWORD:
+ case SftpAuthType::password:
return compareString(lhs.password, rhs.password) < 0; //case sensitive!
- case SftpAuthType::KEY_FILE:
+ case SftpAuthType::keyFile:
rv = compareString(lhs.password, rhs.password); //case sensitive!
if (rv != 0)
return rv < 0;
return compareString(lhs.privateKeyFilePath, rhs.privateKeyFilePath) < 0; //case sensitive!
- case SftpAuthType::AGENT:
+ case SftpAuthType::agent:
return false;
}
assert(false);
@@ -168,24 +175,6 @@ std::wstring getSftpDisplayPath(const Zstring& serverName, const AfsPath& afsPat
//===========================================================================================================================
-std::wstring formatLastSshError(const std::wstring& functionName, LIBSSH2_SESSION* sshSession, LIBSSH2_SFTP* sftpChannel /*optional*/)
-{
- char* lastErrorMsg = nullptr; //owned by "sshSession"
- const int sshStatusCode = ::libssh2_session_last_error(sshSession, &lastErrorMsg, nullptr, false /*want_buf*/);
- assert(lastErrorMsg);
-
- std::wstring errorMsg;
- if (lastErrorMsg)
- errorMsg = trimCpy(utfTo<std::wstring>(lastErrorMsg));
-
- if (sftpChannel && sshStatusCode == LIBSSH2_ERROR_SFTP_PROTOCOL)
- errorMsg += (errorMsg.empty() ? L"" : L" - ") + formatSftpStatusCode(::libssh2_sftp_last_error(sftpChannel));
-
- return formatSystemError(functionName, formatSshStatusCode(sshStatusCode), errorMsg);
-}
-
-//===========================================================================================================================
-
class FatalSshError //=> consider SshSession corrupted and stop use ASAP! same conceptual level like SysError
{
public:
@@ -231,7 +220,7 @@ public:
if (::libssh2_session_handshake(sshSession_, socket_->get()) != 0)
- throw SysError(formatLastSshError(L"libssh2_session_handshake", sshSession_, nullptr));
+ throw SysError(formatLastSshError(L"libssh2_session_handshake", nullptr));
//evaluate fingerprint = libssh2_hostkey_hash(sshSession_, LIBSSH2_HOSTKEY_HASH_SHA1) ???
@@ -242,7 +231,7 @@ public:
if (!authList)
{
if (::libssh2_userauth_authenticated(sshSession_) == 0)
- throw SysError(formatLastSshError(L"libssh2_userauth_list", sshSession_, nullptr));
+ throw SysError(formatLastSshError(L"libssh2_userauth_list", nullptr));
//else: SSH_USERAUTH_NONE has authenticated successfully => we're already done
}
else
@@ -263,12 +252,12 @@ public:
switch (sessionId_.authType)
{
- case SftpAuthType::PASSWORD:
+ case SftpAuthType::password:
{
if (supportAuthPassword)
{
if (::libssh2_userauth_password(sshSession_, usernameUtf8, passwordUtf8) != 0)
- throw SysError(formatLastSshError(L"libssh2_userauth_password", sshSession_, nullptr));
+ throw SysError(formatLastSshError(L"libssh2_userauth_password", nullptr));
}
else if (supportAuthInteractive) //some servers, e.g. web.sourceforge.net, support "keyboard-interactive", but not "password"
{
@@ -308,7 +297,7 @@ public:
ZEN_ON_SCOPE_EXIT(*::libssh2_session_abstract(sshSession_) = nullptr);
if (::libssh2_userauth_keyboard_interactive(sshSession_, usernameUtf8, authCallbackWrapper) != 0)
- throw SysError(formatLastSshError(L"libssh2_userauth_keyboard_interactive", sshSession_, nullptr) +
+ throw SysError(formatLastSshError(L"libssh2_userauth_keyboard_interactive", nullptr) +
(unexpectedPrompts.empty() ? L"" : L"\nUnexpected prompts: " + unexpectedPrompts));
}
else
@@ -317,7 +306,7 @@ public:
}
break;
- case SftpAuthType::KEY_FILE:
+ case SftpAuthType::keyFile:
{
if (!supportAuthKeyfile)
throw SysError(replaceCpy(_("The server does not support authentication via %x."), L"%x", L"\"key file\"") +
@@ -380,24 +369,24 @@ public:
replaceCpy<std::wstring>(L"%x is not an OpenSSH or PuTTY private key file.", L"%x",
fmtPath(sessionId_.privateKeyFilePath) + L" [" + invalidKeyFormat + L"]"));
- throw SysError(formatLastSshError(L"libssh2_userauth_publickey_frommemory", sshSession_, nullptr));
+ throw SysError(formatLastSshError(L"libssh2_userauth_publickey_frommemory", nullptr));
}
}
break;
- case SftpAuthType::AGENT:
+ case SftpAuthType::agent:
{
LIBSSH2_AGENT* sshAgent = ::libssh2_agent_init(sshSession_);
if (!sshAgent)
- throw SysError(formatLastSshError(L"libssh2_agent_init", sshSession_, nullptr));
+ throw SysError(formatLastSshError(L"libssh2_agent_init", nullptr));
ZEN_ON_SCOPE_EXIT(::libssh2_agent_free(sshAgent));
if (::libssh2_agent_connect(sshAgent) != 0)
- throw SysError(formatLastSshError(L"libssh2_agent_connect", sshSession_, nullptr));
+ throw SysError(formatLastSshError(L"libssh2_agent_connect", nullptr));
ZEN_ON_SCOPE_EXIT(::libssh2_agent_disconnect(sshAgent));
if (::libssh2_agent_list_identities(sshAgent) != 0)
- throw SysError(formatLastSshError(L"libssh2_agent_list_identities", sshSession_, nullptr));
+ throw SysError(formatLastSshError(L"libssh2_agent_list_identities", nullptr));
for (libssh2_agent_publickey* prev = nullptr;;)
{
@@ -408,7 +397,7 @@ public:
else if (rc == 1) //no more public keys
throw SysError(L"SSH agent contains no matching public key.");
else
- throw SysError(formatLastSshError(L"libssh2_agent_get_identity", sshSession_, nullptr));
+ throw SysError(formatLastSshError(L"libssh2_agent_get_identity", nullptr));
if (::libssh2_agent_userauth(sshAgent, usernameUtf8.c_str(), identity) == 0)
break; //authentication successful
@@ -508,7 +497,7 @@ public:
nbInfo.commandPending = false;
if (rc < 0)
- throw SysError(formatLastSshError(functionName, sshSession_, sftpChannel));
+ throw SysError(formatLastSshError(functionName, sftpChannel));
lastSuccessfulUseTime_ = std::chrono::steady_clock::now();
return true;
@@ -672,6 +661,22 @@ private:
}
}
+ std::wstring formatLastSshError(const std::wstring& functionName, LIBSSH2_SFTP* sftpChannel /*optional*/) const
+ {
+ char* lastErrorMsg = nullptr; //owned by "sshSession"
+ const int sshStatusCode = ::libssh2_session_last_error(sshSession_, &lastErrorMsg, nullptr, false /*want_buf*/);
+ assert(lastErrorMsg);
+
+ std::wstring errorMsg;
+ if (lastErrorMsg)
+ errorMsg = trimCpy(utfTo<std::wstring>(lastErrorMsg));
+
+ if (sftpChannel && sshStatusCode == LIBSSH2_ERROR_SFTP_PROTOCOL)
+ errorMsg += (errorMsg.empty() ? L"" : L" - ") + formatSftpStatusCode(::libssh2_sftp_last_error(sftpChannel));
+
+ return formatSystemError(functionName, formatSshStatusCode(sshStatusCode), errorMsg);
+ }
+
struct SftpNonBlockInfo
{
bool commandPending = false;
@@ -695,7 +700,7 @@ private:
SftpNonBlockInfo nbInfo_; //for SSH session, e.g. libssh2_sftp_init()
const SshSessionId sessionId_;
- std::shared_ptr<UniCounterCookie> libsshCurlUnifiedInitCookie_;
+ const std::shared_ptr<UniCounterCookie> libsshCurlUnifiedInitCookie_;
std::chrono::steady_clock::time_point lastSuccessfulUseTime_;
};
@@ -964,9 +969,9 @@ private:
};
//--------------------------------------------------------------------------------------
-UniInitializer globalStartupInitSftp(*globalSftpSessionCount.get()); //static ordering: place *before* SftpSessionManager instance!
+UniInitializer globalStartupInitSftp(*globalSftpSessionCount.get());
-Global<SftpSessionManager> globalSftpSessionManager(std::make_unique<SftpSessionManager>());
+Global<SftpSessionManager> globalSftpSessionManager; //caveat: life time must be subset of static UniInitializer!
//--------------------------------------------------------------------------------------
@@ -1344,9 +1349,7 @@ struct OutputStreamSftp : public AbstractFileSystem::OutputStreamImpl
{
fileHandle_ = ::libssh2_sftp_open(sd.sftpChannel, getLibssh2Path(filePath),
LIBSSH2_FXF_WRITE | LIBSSH2_FXF_CREAT | LIBSSH2_FXF_EXCL,
- LIBSSH2_SFTP_S_IRUSR | LIBSSH2_SFTP_S_IWUSR | //
- LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | //0666
- LIBSSH2_SFTP_S_IROTH | LIBSSH2_SFTP_S_IWOTH); //
+ SFTP_DEFAULT_PERMISSION_FILE); //note: server may also apply umask! (e.g. 0022 for ffs.org)
if (!fileHandle_)
return std::min(::libssh2_session_last_errno(sd.sshSession), LIBSSH2_ERROR_SOCKET_NONE);
return LIBSSH2_ERROR_NONE;
@@ -1590,8 +1593,8 @@ private:
runSftpCommand(login_, L"libssh2_sftp_mkdir", //throw SysError
[&](const SshSession::Details& sd) //noexcept!
{
- return ::libssh2_sftp_mkdir(sd.sftpChannel, getLibssh2Path(afsPath), LIBSSH2_SFTP_DEFAULT_MODE);
- //default for newly created directories: 0777 (LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG | LIBSSH2_SFTP_S_IRWXO)
+ return ::libssh2_sftp_mkdir(sd.sftpChannel, getLibssh2Path(afsPath), SFTP_DEFAULT_PERMISSION_FOLDER);
+ //less explicit variant: return ::libssh2_sftp_mkdir(sd.sftpChannel, getLibssh2Path(afsPath), LIBSSH2_SFTP_DEFAULT_MODE);
});
}
catch (const SysError& e) //libssh2_sftp_mkdir reports generic LIBSSH2_FX_FAILURE if existing
@@ -1848,28 +1851,30 @@ Zstring concatenateSftpFolderPathPhrase(const SftpLoginInfo& login, const AfsPat
if (login.port > 0)
port = Zstr(":") + numberTo<Zstring>(login.port);
- Zstring options;
- if (login.traverserChannelsPerConnection > 1)
- options += Zstr("|chan=") + numberTo<Zstring>(login.traverserChannelsPerConnection);
+ const SftpLoginInfo loginDefault;
- if (login.timeoutSec != SftpLoginInfo().timeoutSec)
+ Zstring options;
+ if (login.timeoutSec != loginDefault.timeoutSec)
options += Zstr("|timeout=") + numberTo<Zstring>(login.timeoutSec);
+ if (login.traverserChannelsPerConnection != loginDefault.traverserChannelsPerConnection)
+ options += Zstr("|chan=") + numberTo<Zstring>(login.traverserChannelsPerConnection);
+
switch (login.authType)
{
- case SftpAuthType::PASSWORD:
+ case SftpAuthType::password:
break;
- case SftpAuthType::KEY_FILE:
+ case SftpAuthType::keyFile:
options += Zstr("|keyfile=") + login.privateKeyFilePath;
break;
- case SftpAuthType::AGENT:
+ case SftpAuthType::agent:
options += Zstr("|agent");
break;
}
- if (login.authType != SftpAuthType::AGENT)
+ if (login.authType != SftpAuthType::agent)
if (!login.password.empty()) //password always last => visually truncated by folder input field
options += Zstr("|pass64=") + encodePasswordBase64(login.password);
@@ -1878,6 +1883,20 @@ Zstring concatenateSftpFolderPathPhrase(const SftpLoginInfo& login, const AfsPat
}
+void fff::sftpInit()
+{
+ assert(!globalSftpSessionManager.get());
+ globalSftpSessionManager.set(std::make_unique<SftpSessionManager>());
+}
+
+
+void fff::sftpTeardown()
+{
+ assert(globalSftpSessionManager.get());
+ globalSftpSessionManager.set(nullptr);
+}
+
+
AfsPath fff::getSftpHomePath(const SftpLoginInfo& login) //throw FileError
{
return SftpFileSystem(login).getHomePath(); //throw FileError
@@ -1893,8 +1912,8 @@ Zstring fff::condenseToSftpFolderPathPhrase(const SftpLoginInfo& login, const Zs
trim(loginTmp.username);
trim(loginTmp.privateKeyFilePath);
+ loginTmp.timeoutSec = std::max(1, loginTmp.timeoutSec);
loginTmp.traverserChannelsPerConnection = std::max(1, loginTmp.traverserChannelsPerConnection);
- loginTmp.timeoutSec = std::max(1, loginTmp.timeoutSec);
if (startsWithAsciiNoCase(loginTmp.server, Zstr("http:" )) ||
startsWithAsciiNoCase(loginTmp.server, Zstr("https:")) ||
@@ -1974,17 +1993,17 @@ SftpPathInfo fff::getResolvedSftpPath(const Zstring& folderPathPhrase) //noexcep
if (!options.empty())
{
for (const Zstring& optPhrase : split(options, Zstr("|"), SplitType::SKIP_EMPTY))
- if (startsWith(optPhrase, Zstr("chan=")))
- login.traverserChannelsPerConnection = stringTo<int>(afterFirst(optPhrase, Zstr("="), IF_MISSING_RETURN_NONE));
- else if (startsWith(optPhrase, Zstr("timeout=")))
+ if (startsWith(optPhrase, Zstr("timeout=")))
login.timeoutSec = stringTo<int>(afterFirst(optPhrase, Zstr("="), IF_MISSING_RETURN_NONE));
+ else if (startsWith(optPhrase, Zstr("chan=")))
+ login.traverserChannelsPerConnection = stringTo<int>(afterFirst(optPhrase, Zstr("="), IF_MISSING_RETURN_NONE));
else if (startsWith(optPhrase, Zstr("keyfile=")))
{
- login.authType = SftpAuthType::KEY_FILE;
+ login.authType = SftpAuthType::keyFile;
login.privateKeyFilePath = afterFirst(optPhrase, Zstr("="), IF_MISSING_RETURN_NONE);
}
else if (optPhrase == Zstr("agent"))
- login.authType = SftpAuthType::AGENT;
+ login.authType = SftpAuthType::agent;
else if (startsWith(optPhrase, Zstr("pass64=")))
login.password = decodePasswordBase64(afterFirst(optPhrase, Zstr("="), IF_MISSING_RETURN_NONE));
else
diff --git a/FreeFileSync/Source/afs/sftp.h b/FreeFileSync/Source/afs/sftp.h
index dbd73556..bdfcda6f 100644
--- a/FreeFileSync/Source/afs/sftp.h
+++ b/FreeFileSync/Source/afs/sftp.h
@@ -16,13 +16,16 @@ bool acceptsItemPathPhraseSftp(const Zstring& itemPathPhrase); //noexcept
AbstractPath createItemPathSftp(const Zstring& itemPathPhrase); //noexcept
//-------------------------------------------------------
-
enum class SftpAuthType
{
- PASSWORD,
- KEY_FILE,
- AGENT,
+ password,
+ keyFile,
+ agent,
};
+//-------------------------------------------------------
+
+void sftpInit();
+void sftpTeardown();
struct SftpLoginInfo
{
@@ -30,15 +33,16 @@ struct SftpLoginInfo
int port = 0; // > 0 if set
Zstring username;
- SftpAuthType authType = SftpAuthType::PASSWORD;
- Zstring password; //authType == PASSWORD or KEY_FILE
- Zstring privateKeyFilePath; //authType == KEY_FILE: use PEM-encoded private key (protected by password) for authentication
+ SftpAuthType authType = SftpAuthType::password;
+ Zstring password; //authType == password or keyFile
+ Zstring privateKeyFilePath; //authType == keyFile: use PEM-encoded private key (protected by password) for authentication
//other settings not specific to SFTP session:
+ int timeoutSec = 15; //valid range: [1, inf)
int traverserChannelsPerConnection = 1; //valid range: [1, inf)
- int timeoutSec = 15; //
};
+
struct SftpPathInfo
{
SftpLoginInfo login;
diff --git a/FreeFileSync/Source/base/application.cpp b/FreeFileSync/Source/base/application.cpp
index d66b08ff..657eac13 100644
--- a/FreeFileSync/Source/base/application.cpp
+++ b/FreeFileSync/Source/base/application.cpp
@@ -63,9 +63,11 @@ bool Application::OnInit()
//hang on Ubuntu 19.10 (GLib 2.62) caused by ibus initialization: https://freefilesync.org/forum/viewtopic.php?t=6704
//=> work around 1: bonus: avoid needless DBus calls: https://developer.gnome.org/gio/stable/running-gio-apps.html
- if (::setenv("GIO_USE_VFS", "local", true /*overwrite*/) != 0)
- std::cerr << utfTo<std::string>(formatSystemError(L"setenv(GIO_USE_VFS)", errno)) << "\n";
- //=> work around 2: setting GIO_USE_VFS seems to suffice, but this one also does it:
+ // drawback: missing MTP and network links in folder picker: https://freefilesync.org/forum/viewtopic.php?t=6871
+ //if (::setenv("GIO_USE_VFS", "local", true /*overwrite*/) != 0)
+ // std::cerr << utfTo<std::string>(formatSystemError(L"setenv(GIO_USE_VFS)", errno)) << "\n";
+ //
+ //=> work around 2:
g_vfs_get_default(); //returns unowned GVfs*
//no such issue on GTK3!
@@ -560,8 +562,6 @@ void runBatchMode(const Zstring& globalConfigFilePath, const XmlBatchConfig& bat
batchCfg.batchExCfg.batchErrorHandling,
batchCfg.mainCfg.automaticRetryCount,
batchCfg.mainCfg.automaticRetryDelay,
- batchCfg.mainCfg.postSyncCommand,
- batchCfg.mainCfg.postSyncCondition,
batchCfg.batchExCfg.postSyncAction);
try
{
@@ -595,22 +595,24 @@ void runBatchMode(const Zstring& globalConfigFilePath, const XmlBatchConfig& bat
}
catch (AbortProcess&) {} //exit used by statusHandler
- BatchStatusHandler::Result r = statusHandler.reportFinalStatus(batchCfg.mainCfg.altLogFolderPathPhrase, globalCfg.logfilesMaxAgeDays, logFilePathsToKeep); //noexcept
+ BatchStatusHandler::Result r = statusHandler.reportResults(batchCfg.mainCfg.postSyncCommand, batchCfg.mainCfg.postSyncCondition,
+ batchCfg.mainCfg.altLogFolderPathPhrase, globalCfg.logfilesMaxAgeDays, logFilePathsToKeep,
+ batchCfg.mainCfg.emailNotifyAddress, batchCfg.mainCfg.emailNotifyCondition); //noexcept
//----------------------------------------------------------------------
- raiseReturnCode(returnCode, mapToReturnCode(r.finalStatus));
+ raiseReturnCode(returnCode, mapToReturnCode(r.resultStatus));
//update last sync stats for the selected cfg file
for (ConfigFileItem& cfi : globalCfg.gui.mainDlg.cfgFileHistory)
if (equalNativePath(cfi.cfgFilePath, cfgFilePath))
{
- if (r.finalStatus != SyncResult::aborted)
+ if (r.resultStatus != SyncResult::aborted)
cfi.lastSyncTime = std::chrono::system_clock::to_time_t(syncStartTime);
assert(!AFS::isNullPath(r.logFilePath));
if (!AFS::isNullPath(r.logFilePath))
{
cfi.logFilePath = r.logFilePath;
- cfi.logResult = r.finalStatus;
+ cfi.logResult = r.resultStatus;
}
break;
}
diff --git a/FreeFileSync/Source/base/config.cpp b/FreeFileSync/Source/base/config.cpp
index 552b377f..0a170bbb 100644
--- a/FreeFileSync/Source/base/config.cpp
+++ b/FreeFileSync/Source/base/config.cpp
@@ -21,8 +21,8 @@ using namespace fff; //functionally needed for correct overload resolution!!!
namespace
{
//-------------------------------------------------------------------------------------------------------------------------------
-const int XML_FORMAT_GLOBAL_CFG = 15; //2019-11-30
-const int XML_FORMAT_SYNC_CFG = 14; //2018-08-13
+const int XML_FORMAT_GLOBAL_CFG = 16; //2020-01-30
+const int XML_FORMAT_SYNC_CFG = 15; //2020-01-30
//-------------------------------------------------------------------------------------------------------------------------------
}
@@ -235,6 +235,40 @@ void writeText(const BatchErrorHandling& value, std::string& output)
}
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);
@@ -925,15 +959,21 @@ bool readStruc(const XmlElement& input, ConfigFileItem& value)
const bool rv1 = in.attribute("Result", value.logResult);
- //FFS portable: use special syntax for config file paths: e.g. "FFS:\SyncJob.ffs_gui"
Zstring cfgPathRaw;
- const bool rv2 = in.attribute("CfgPath", cfgPathRaw);
+ bool rv2 = in.attribute("Config", cfgPathRaw);
+ if (!rv2) //TODO: remove after migration! 2020-02-09
+ rv2 = in.attribute("CfgPath", cfgPathRaw); //
+
+ //FFS portable: use special syntax for config file paths: e.g. "FFS:\SyncJob.ffs_gui"
if (rv2) value.cfgFilePath = resolveFreeFileSyncDriveMacro(cfgPathRaw);
const bool rv3 = in.attribute("LastSync", value.lastSyncTime);
Zstring logPathPhrase;
- const bool rv4 = in.attribute("LogPath", logPathPhrase);
+ bool rv4 = in.attribute("Log", logPathPhrase);
+ if (!rv4) //TODO: remove after migration! 2020-02-09
+ rv4 = in.attribute("LogPath", logPathPhrase); //
+
if (rv4) value.logFilePath = createAbstractPath(resolveFreeFileSyncDriveMacro(logPathPhrase));
std::string hexColor; //optional XML attribute!
@@ -950,13 +990,13 @@ void writeStruc(const ConfigFileItem& value, XmlElement& output)
{
XmlOut out(output);
out.attribute("Result", value.logResult);
- out.attribute("CfgPath", substituteFreeFileSyncDriveLetter(value.cfgFilePath));
+ out.attribute("Config", substituteFreeFileSyncDriveLetter(value.cfgFilePath));
out.attribute("LastSync", value.lastSyncTime);
if (std::optional<Zstring> nativePath = AFS::getNativeItemPath(value.logFilePath))
- out.attribute("LogPath", substituteFreeFileSyncDriveLetter(*nativePath));
+ out.attribute("Log", substituteFreeFileSyncDriveLetter(*nativePath));
else
- out.attribute("LogPath", AFS::getInitPathPhrase(value.logFilePath));
+ out.attribute("Log", AFS::getInitPathPhrase(value.logFilePath));
if (value.backColor.IsOk())
{
@@ -1261,7 +1301,7 @@ void readConfig(const XmlIn& in, MainConfiguration& mainCfg, int formatVer)
//TODO: remove if parameter migration after some time! 2017-10-24
if (formatVer < 8)
- inMain["OnCompletion"](mainCfg.postSyncCommand);
+ ;
else
//TODO: remove if parameter migration after some time! 2018-02-24
if (formatVer < 10)
@@ -1273,19 +1313,28 @@ void readConfig(const XmlIn& in, MainConfiguration& mainCfg, int formatVer)
inMain["Errors"].attribute("Delay", mainCfg.automaticRetryDelay);
}
+ //TODO: remove if parameter migration after some time! 2017-10-24
+ if (formatVer < 8)
+ inMain["OnCompletion"](mainCfg.postSyncCommand);
+ else
+ {
+ inMain["PostSyncCommand"](mainCfg.postSyncCommand);
+ inMain["PostSyncCommand"].attribute("Condition", mainCfg.postSyncCondition);
+ }
+
//TODO: remove if parameter migration after some time! 2018-08-13
if (formatVer < 14)
; //path will be extracted from BatchExclusiveConfig
else
inMain["LogFolder"](mainCfg.altLogFolderPathPhrase);
- //TODO: remove if parameter migration after some time! 2017-10-24
- if (formatVer < 8)
- inMain["OnCompletion"](mainCfg.postSyncCommand);
+ //TODO: remove if parameter migration after some time! 2020-01-30
+ if (formatVer < 15)
+ ;
else
{
- inMain["PostSyncCommand"](mainCfg.postSyncCommand);
- inMain["PostSyncCommand"].attribute("Condition", mainCfg.postSyncCondition);
+ inMain["EmailNotification"](mainCfg.emailNotifyAddress);
+ inMain["EmailNotification"].attribute("Condition", mainCfg.emailNotifyCondition);
}
}
@@ -1519,13 +1568,7 @@ void readConfig(const XmlIn& in, XmlGlobalSettings& cfg, int formatVer)
//TODO: remove if parameter migration after some time! 2018-09-09
if (formatVer < 11)
- inWnd["FolderPairsVisible" ].attribute("Max", cfg.gui.mainDlg.maxFolderPairsVisible);
-
- //TODO: remove if parameter migration after some time! 2018-09-09
- if (formatVer < 11)
- ;
- else
- inWnd["FolderHistory" ].attribute("MaxSize", cfg.gui.mainDlg.folderHistItemsMax);
+ inWnd["FolderPairsVisible" ].attribute("Max", cfg.gui.mainDlg.folderPairsVisibleMax);
//###########################################################
@@ -1619,12 +1662,11 @@ void readConfig(const XmlIn& in, XmlGlobalSettings& cfg, int formatVer)
//TODO: remove if parameter migration after some time! 2018-09-09
if (formatVer < 11)
;
+ //TODO: remove if parameter migration after some time! 2020-01-30
+ else if (formatVer < 16)
+ inFileGrid.attribute("MaxFolderPairsShown", cfg.gui.mainDlg.folderPairsVisibleMax);
else
- inFileGrid.attribute("MaxFolderPairsShown", cfg.gui.mainDlg.maxFolderPairsVisible);
-
- //TODO: remove if parameter migration after some time! 2018-09-09
- if (formatVer < 11)
- inFileGrid.attribute("HistoryMaxSize", cfg.gui.mainDlg.folderHistItemsMax);
+ inFileGrid.attribute("FolderPairsMax", cfg.gui.mainDlg.folderPairsVisibleMax);
inFileGrid["ColumnsLeft"].attribute("PathFormat", cfg.gui.mainDlg.itemPathFormatLeftGrid);
inFileGrid["ColumnsLeft"](cfg.gui.mainDlg.columnAttribLeft);
@@ -1641,14 +1683,8 @@ void readConfig(const XmlIn& in, XmlGlobalSettings& cfg, int formatVer)
{
inGui["FolderHistoryLeft" ](cfg.gui.mainDlg.folderHistoryLeft);
inGui["FolderHistoryRight"](cfg.gui.mainDlg.folderHistoryRight);
- inGui["FolderHistoryLeft"].attribute("MaxSize", cfg.gui.mainDlg.folderHistItemsMax);
}
- //TODO: remove if parameter migration after some time! 2018-09-09
- if (formatVer < 11)
- if (cfg.gui.mainDlg.folderHistItemsMax == 15) //default value was too small
- cfg.gui.mainDlg.folderHistItemsMax = XmlGlobalSettings().gui.mainDlg.folderHistItemsMax;
-
//###########################################################
XmlIn inCopyTo = inWnd["ManualCopyTo"];
inCopyTo.attribute("KeepRelativePaths", cfg.gui.mainDlg.copyToCfg.keepRelPaths);
@@ -1745,18 +1781,35 @@ void readConfig(const XmlIn& in, XmlGlobalSettings& cfg, int formatVer)
if (formatVer < 4)
cfg.gui.mainDlg.cfgHistItemsMax = std::max<size_t>(cfg.gui.mainDlg.cfgHistItemsMax, 100);
+ //TODO: remove if parameter migration after some time! 2020-01-30
+ if (formatVer < 16)
+ ;
+ else
+ inGui["FolderHistory" ].attribute("MaxSize", cfg.gui.folderHistoryMax);
+
+ inGui["VersioningFolderHistory"](cfg.gui.versioningFolderHistory);
+ inGui["LogFolderHistory" ](cfg.gui.logFolderHistory);
+
+ inGui["EmailHistory"](cfg.gui.emailHistory);
+ inGui["EmailHistory"].attribute("MaxSize", cfg.gui.emailHistoryMax);
+
//TODO: remove if clause after migration! 2017-10-24
if (formatVer < 5)
{
inGui["OnCompletionHistory"](cfg.gui.commandHistory);
- inGui["OnCompletionHistory"].attribute("MaxSize", cfg.gui.commandHistItemsMax);
+ inGui["OnCompletionHistory"].attribute("MaxSize", cfg.gui.commandHistoryMax);
}
else
{
inGui["CommandHistory"](cfg.gui.commandHistory);
- inGui["CommandHistory"].attribute("MaxSize", cfg.gui.commandHistItemsMax);
+ inGui["CommandHistory"].attribute("MaxSize", cfg.gui.commandHistoryMax);
}
+ //TODO: remove if parameter migration after some time! 2020-01-30
+ if (formatVer < 15)
+ if (cfg.gui.commandHistoryMax <= 8)
+ cfg.gui.commandHistoryMax = XmlGlobalSettings().gui.commandHistoryMax;
+
//external applications
//TODO: remove old parameter after migration! 2016-05-28
if (inGui["ExternalApplications"])
@@ -2099,10 +2152,13 @@ void writeConfig(const MainConfiguration& mainCfg, XmlOut& out)
outMain["Errors"].attribute("Retry", mainCfg.automaticRetryCount);
outMain["Errors"].attribute("Delay", mainCfg.automaticRetryDelay);
- outMain["LogFolder"](mainCfg.altLogFolderPathPhrase);
-
outMain["PostSyncCommand"](mainCfg.postSyncCommand);
outMain["PostSyncCommand"].attribute("Condition", mainCfg.postSyncCondition);
+
+ outMain["LogFolder"](mainCfg.altLogFolderPathPhrase);
+
+ outMain["EmailNotification"](mainCfg.emailNotifyAddress);
+ outMain["EmailNotification"].attribute("Condition", mainCfg.emailNotifyCondition);
}
@@ -2183,7 +2239,6 @@ void writeConfig(const XmlGlobalSettings& cfg, XmlOut& out)
//###########################################################
outWnd["SearchPanel" ].attribute("CaseSensitive", cfg.gui.mainDlg.textSearchRespectCase);
- outWnd["FolderHistory"].attribute("MaxSize", cfg.gui.mainDlg.folderHistItemsMax);
//###########################################################
XmlOut outConfig = outWnd["ConfigPanel"];
@@ -2218,7 +2273,7 @@ void writeConfig(const XmlGlobalSettings& cfg, XmlOut& out)
outFileGrid.attribute("ShowIcons", cfg.gui.mainDlg.showIcons);
outFileGrid.attribute("IconSize", cfg.gui.mainDlg.iconSize);
outFileGrid.attribute("SashOffset", cfg.gui.mainDlg.sashOffset);
- outFileGrid.attribute("MaxFolderPairsShown", cfg.gui.mainDlg.maxFolderPairsVisible);
+ outFileGrid.attribute("FolderPairsMax", cfg.gui.mainDlg.folderPairsVisibleMax);
outFileGrid["ColumnsLeft"].attribute("PathFormat", cfg.gui.mainDlg.itemPathFormatLeftGrid);
outFileGrid["ColumnsLeft"](cfg.gui.mainDlg.columnAttribLeft);
@@ -2245,8 +2300,16 @@ void writeConfig(const XmlGlobalSettings& cfg, XmlOut& out)
outGui["DefaultExclusionFilter"](splitFilterByLines(cfg.gui.defaultExclusionFilter));
+ outGui["FolderHistory" ].attribute("MaxSize", cfg.gui.folderHistoryMax);
+
+ outGui["VersioningFolderHistory"](cfg.gui.versioningFolderHistory);
+ outGui["LogFolderHistory" ](cfg.gui.logFolderHistory);
+
+ outGui["EmailHistory"](cfg.gui.emailHistory);
+ outGui["EmailHistory"].attribute("MaxSize", cfg.gui.emailHistoryMax);
+
outGui["CommandHistory"](cfg.gui.commandHistory);
- outGui["CommandHistory"].attribute("MaxSize", cfg.gui.commandHistItemsMax);
+ outGui["CommandHistory"].attribute("MaxSize", cfg.gui.commandHistoryMax);
//external applications
outGui["ExternalApps"](cfg.gui.externalApps);
diff --git a/FreeFileSync/Source/base/config.h b/FreeFileSync/Source/base/config.h
index 14ea2565..dcafb207 100644
--- a/FreeFileSync/Source/base/config.h
+++ b/FreeFileSync/Source/base/config.h
@@ -198,7 +198,7 @@ struct XmlGlobalSettings
bool isMaximized = false;
bool textSearchRespectCase = false; //good default for Linux, too!
- int maxFolderPairsVisible = 6;
+ int folderPairsVisibleMax = 6;
size_t cfgGridTopRowPos = 0;
int cfgGridSyncOverdueDays = 7;
@@ -214,8 +214,6 @@ struct XmlGlobalSettings
bool treeGridLastSortAscending = getDefaultSortDirection(treeGridLastSortColumnDefault); //
std::vector<ColAttributesTree> treeGridColumnAttribs = getTreeGridDefaultColAttribs();
- size_t folderHistItemsMax = 20;
-
struct
{
bool keepRelPaths = false;
@@ -242,9 +240,16 @@ struct XmlGlobalSettings
Zstring defaultExclusionFilter = Zstr("/.Trash-*/") Zstr("\n")
Zstr("/.recycle/");
+ size_t folderHistoryMax = 20;
+
+ std::vector<Zstring> versioningFolderHistory;
+ std::vector<Zstring> logFolderHistory;
+
+ std::vector<Zstring> emailHistory;
+ size_t emailHistoryMax = 10;
std::vector<Zstring> commandHistory;
- size_t commandHistItemsMax = 8;
+ size_t commandHistoryMax = 10;
std::vector<ExternalApp> externalApps
{
diff --git a/FreeFileSync/Source/base/db_file.cpp b/FreeFileSync/Source/base/db_file.cpp
index aba2947f..94a6afc4 100644
--- a/FreeFileSync/Source/base/db_file.cpp
+++ b/FreeFileSync/Source/base/db_file.cpp
@@ -20,9 +20,9 @@ using namespace fff;
namespace
{
//-------------------------------------------------------------------------------------------------------------------------------
-const char FILE_FORMAT_DESCR[] = "FreeFileSync";
-const int DB_FORMAT_CONTAINER = 10; //since 2017-02-01
-const int DB_FORMAT_STREAM = 3; //
+const char DB_FILE_DESCR[] = "FreeFileSync";
+const int DB_FILE_VERSION = 11; //2020-02-07
+const int DB_STREAM_VERSION = 3; //2017-02-01
//-------------------------------------------------------------------------------------------------------------------------------
DEFINE_NEW_FILE_ERROR(FileErrorDatabaseNotExisting)
@@ -62,105 +62,123 @@ AbstractPath getDatabaseFilePath(const BaseFolderPair& baseFolder)
void saveStreams(const DbStreams& streamList, const AbstractPath& dbPath, const IOCallback& notifyUnbufferedIO /*throw X*/) //throw FileError, X
{
- const std::unique_ptr<AFS::OutputStream> fileStreamOut = AFS::getOutputStream(dbPath, //throw FileError
- std::nullopt /*streamSize*/,
- std::nullopt /*modTime*/,
- notifyUnbufferedIO /*throw X*/);
+ MemoryStreamOut<std::string> memStreamOut;
+
//write FreeFileSync file identifier
- writeArray(*fileStreamOut, FILE_FORMAT_DESCR, sizeof(FILE_FORMAT_DESCR)); //throw FileError, X
+ writeArray(memStreamOut, DB_FILE_DESCR, sizeof(DB_FILE_DESCR));
//save file format version
- writeNumber<int32_t>(*fileStreamOut, DB_FORMAT_CONTAINER); //throw FileError, X
+ writeNumber<int32_t>(memStreamOut, DB_FILE_VERSION);
//write stream list
- writeNumber(*fileStreamOut, static_cast<uint32_t>(streamList.size())); //throw FileError, X
+ writeNumber(memStreamOut, static_cast<uint32_t>(streamList.size()));
for (const auto& [sessionID, sessionData] : streamList)
{
- writeContainer<std::string>(*fileStreamOut, sessionID); //throw FileError, X
+ writeContainer<std::string>(memStreamOut, sessionID);
- writeNumber <int8_t >(*fileStreamOut, sessionData.isLeadStream); //
- writeContainer<ByteArray>(*fileStreamOut, sessionData.rawStream); //
+ writeNumber <int8_t >(memStreamOut, sessionData.isLeadStream);
+ writeContainer<ByteArray>(memStreamOut, sessionData.rawStream);
}
- //commit and close stream:
- fileStreamOut->finalize(); //throw FileError, X
+ writeNumber<uint32_t>(memStreamOut, getCrc32(memStreamOut.ref()));
+ //------------------------------------------------------------------------------------------------------------------------
+
+ const std::unique_ptr<AFS::OutputStream> fileStreamOut = AFS::getOutputStream(dbPath, //throw FileError
+ memStreamOut.ref().size(),
+ std::nullopt /*modTime*/,
+ notifyUnbufferedIO /*throw X*/);
+ fileStreamOut->write(memStreamOut.ref().c_str(), memStreamOut.ref().size()); //throw FileError, X
+ fileStreamOut->finalize(); //throw FileError, X
}
DbStreams loadStreams(const AbstractPath& dbPath, const IOCallback& notifyUnbufferedIO /*throw X*/) //throw FileError, FileErrorDatabaseNotExisting, X
{
+ std::string byteStream;
try
{
const std::unique_ptr<AFS::InputStream> fileStreamIn = AFS::getInputStream(dbPath, notifyUnbufferedIO); //throw FileError, ErrorFileLocked
+ byteStream = bufferedLoad<std::string>(*fileStreamIn); //throw FileError, ErrorFileLocked, X
+ }
+ catch (FileError&)
+ {
+ bool dbNotYetExisting = false;
+ try { dbNotYetExisting = !AFS::itemStillExists(dbPath); /*throw FileError*/ }
+ catch (FileError&) {} //previous exception is more relevant
- //read FreeFileSync file identifier
- char formatDescr[sizeof(FILE_FORMAT_DESCR)] = {};
- readArray(*fileStreamIn, formatDescr, sizeof(formatDescr)); //throw FileError, ErrorFileLocked, X, UnexpectedEndOfStreamError
+ if (dbNotYetExisting) //throw FileError
+ throw FileErrorDatabaseNotExisting(_("Initial synchronization:") + L" \n" +
+ replaceCpy(_("Database file %x does not yet exist."), L"%x", fmtPath(AFS::getDisplayPath(dbPath))));
+ else
+ throw;
+ }
+ //------------------------------------------------------------------------------------------------------------------------
+ try
+ {
+ MemoryStreamIn memStreamIn(byteStream);
- if (!std::equal(FILE_FORMAT_DESCR, FILE_FORMAT_DESCR + sizeof(FILE_FORMAT_DESCR), formatDescr))
- throw FileError(replaceCpy(_("Database file %x is incompatible."), L"%x", fmtPath(AFS::getDisplayPath(dbPath))));
+ char formatDescr[sizeof(DB_FILE_DESCR)] = {};
+ readArray(memStreamIn, formatDescr, sizeof(formatDescr)); //throw UnexpectedEndOfStreamError
- const int version = readNumber<int32_t>(*fileStreamIn); //throw FileError, ErrorFileLocked, X, UnexpectedEndOfStreamError
+ if (!std::equal(DB_FILE_DESCR, DB_FILE_DESCR + sizeof(DB_FILE_DESCR), formatDescr))
+ throw FileError(replaceCpy(_("Database file %x is incompatible."), L"%x", fmtPath(AFS::getDisplayPath(dbPath))));
- //TODO: remove migration code at some time! 2017-02-01
- if (version != 9 &&
- version != DB_FORMAT_CONTAINER)
+ const int version = readNumber<int32_t>(memStreamIn); //throw UnexpectedEndOfStreamError
+ if (version != 9 && //TODO: remove migration code at some time! v9 used until 2017-02-01
+ version != 10 && //TODO: remove migration code at some time! v10 used until 2020-02-07
+ version != DB_FILE_VERSION)
throw FileError(replaceCpy(_("Database file %x is incompatible."), L"%x", fmtPath(AFS::getDisplayPath(dbPath))));
+ if (version == 9 || //TODO: remove migration code at some time! v9 used until 2017-02-01
+ version == 10) //TODO: remove migration code at some time! v10 used until 2020-02-07
+ ;
+ else //catch data corruption ASAP + don't rely on std::bad_alloc for consistency checking
+ // => only "partially" useful for container/stream metadata since the streams data is zlib-compressed
+ {
+ assert(byteStream.size() >= sizeof(uint32_t)); //obviously in this context!
+ MemoryStreamOut<std::string> crcStreamOut;
+ writeNumber<uint32_t>(crcStreamOut, getCrc32({ byteStream.begin(), byteStream.end() - sizeof(uint32_t) }));
+
+ if (!endsWith(byteStream, crcStreamOut.ref()))
+ throw FileError(_("Database file is corrupted:") + L" " + fmtPath(AFS::getDisplayPath(dbPath)), L"Invalid checksum.");
+ }
+
DbStreams output;
//read stream list
- size_t streamCount = readNumber<uint32_t>(*fileStreamIn); //throw FileError, ErrorFileLocked, X, UnexpectedEndOfStreamError
+ size_t streamCount = readNumber<uint32_t>(memStreamIn); //throw UnexpectedEndOfStreamError
while (streamCount-- != 0)
{
- std::string sessionID = readContainer<std::string>(*fileStreamIn); //throw FileError, ErrorFileLocked, X, UnexpectedEndOfStreamError
+ std::string sessionID = readContainer<std::string>(memStreamIn); //throw UnexpectedEndOfStreamError
SessionData sessionData = {};
- //TODO: remove migration code at some time! 2017-02-01
- if (version == 9)
+ if (version == 9) //TODO: remove migration code at some time! v9 used until 2017-02-01
{
- sessionData.rawStream = readContainer<ByteArray>(*fileStreamIn); //throw FileError, ErrorFileLocked, X, UnexpectedEndOfStreamError
+ sessionData.rawStream = readContainer<ByteArray>(memStreamIn); //throw UnexpectedEndOfStreamError
- MemoryStreamIn<ByteArray> streamIn(sessionData.rawStream);
+ MemoryStreamIn streamIn(sessionData.rawStream);
const int streamVersion = readNumber<int32_t>(streamIn); //throw UnexpectedEndOfStreamError
if (streamVersion != 2) //don't throw here due to old stream formats
continue;
- sessionData.isLeadStream = readNumber<int8_t>(streamIn) != 0; //throw FileError, X, UnexpectedEndOfStreamError
+ sessionData.isLeadStream = readNumber<int8_t>(streamIn) != 0; //throw UnexpectedEndOfStreamError
}
else
{
- sessionData.isLeadStream = readNumber <int8_t >(*fileStreamIn) != 0; //throw FileError, ErrorFileLocked, X, UnexpectedEndOfStreamError
- sessionData.rawStream = readContainer<ByteArray>(*fileStreamIn); //
+ sessionData.isLeadStream = readNumber <int8_t >(memStreamIn) != 0; //throw UnexpectedEndOfStreamError
+ sessionData.rawStream = readContainer<ByteArray>(memStreamIn); //
}
output[sessionID] = std::move(sessionData);
}
return output;
}
- catch (FileError&)
- {
- bool dbNotYetExisting = false;
- try { dbNotYetExisting = !AFS::itemStillExists(dbPath); /*throw FileError*/ }
- catch (FileError&) {} //previous exception is more relevant
-
- if (dbNotYetExisting) //throw FileError
- throw FileErrorDatabaseNotExisting(_("Initial synchronization:") + L" \n" +
- replaceCpy(_("Database file %x does not yet exist."), L"%x", fmtPath(AFS::getDisplayPath(dbPath))));
- else
- throw;
- }
catch (UnexpectedEndOfStreamError&)
{
throw FileError(_("Database file is corrupted:") + L" " + fmtPath(AFS::getDisplayPath(dbPath)), L"Unexpected end of stream.");
}
- catch (const std::bad_alloc& e) //still required?
- {
- throw FileError(_("Database file is corrupted:") + L" " + fmtPath(AFS::getDisplayPath(dbPath)),
- _("Out of memory.") + L" " + utfTo<std::wstring>(e.what()));
- }
}
//#######################################################################################################################################
@@ -177,8 +195,8 @@ public:
MemoryStreamOut<ByteArray> outL;
MemoryStreamOut<ByteArray> outR;
//save format version
- writeNumber<int32_t>(outL, DB_FORMAT_STREAM);
- writeNumber<int32_t>(outR, DB_FORMAT_STREAM);
+ writeNumber<int32_t>(outL, DB_STREAM_VERSION);
+ writeNumber<int32_t>(outR, DB_STREAM_VERSION);
auto compStream = [&](const ByteArray& stream) -> ByteArray //throw FileError
{
@@ -313,8 +331,8 @@ public:
try
{
- MemoryStreamIn<ByteArray> streamInL(streamL);
- MemoryStreamIn<ByteArray> streamInR(streamR);
+ MemoryStreamIn streamInL(streamL);
+ MemoryStreamIn streamInR(streamR);
const int streamVersion = readNumber<int32_t>(streamInL); //throw UnexpectedEndOfStreamError
const int streamVersionR = readNumber<int32_t>(streamInR); //
@@ -324,7 +342,7 @@ public:
//TODO: remove migration code at some time! 2017-02-01
if (streamVersion != 2 &&
- streamVersion != DB_FORMAT_STREAM)
+ streamVersion != DB_STREAM_VERSION)
throw FileError(replaceCpy(_("Database file %x is incompatible."), L"%x", fmtPath(displayFilePathL)), L"Unknown stream format");
//TODO: remove migration code at some time! 2017-02-01
@@ -373,7 +391,7 @@ public:
if (sizePart1 > 0) readArray(streamInPart1, &*buf.begin(), sizePart1); //throw UnexpectedEndOfStreamError
if (sizePart2 > 0) readArray(streamInPart2, &*buf.begin() + sizePart1, sizePart2); //
- MemoryStreamIn<ByteArray> streamIn(buf);
+ MemoryStreamIn streamIn(buf);
const ByteArray bufText = readContainer<ByteArray>(streamIn); //
const ByteArray bufSmallNum = readContainer<ByteArray>(streamIn); //throw UnexpectedEndOfStreamError
const ByteArray bufBigNum = readContainer<ByteArray>(streamIn); //
@@ -390,15 +408,10 @@ public:
return output;
}
}
- catch (const UnexpectedEndOfStreamError&)
+ catch (UnexpectedEndOfStreamError&)
{
throw FileError(_("Database file is corrupted:") + L"\n" + fmtPath(displayFilePathL) + L"\n" + fmtPath(displayFilePathR), L"Unexpected end of stream.");
}
- catch (const std::bad_alloc& e)
- {
- throw FileError(_("Database file is corrupted:") + L"\n" + fmtPath(displayFilePathL) + L"\n" + fmtPath(displayFilePathR),
- _("Out of memory.") + L" " + utfTo<std::wstring>(e.what()));
- }
}
private:
diff --git a/FreeFileSync/Source/base/dir_exist_async.h b/FreeFileSync/Source/base/dir_exist_async.h
index 30ed2858..74572919 100644
--- a/FreeFileSync/Source/base/dir_exist_async.h
+++ b/FreeFileSync/Source/base/dir_exist_async.h
@@ -91,15 +91,19 @@ FolderStatus getFolderStatusNonBlocking(const std::set<AbstractPath>& folderPath
procCallback.updateStatus(replaceCpy(_("Searching for folder %x..."), L"%x", displayPathFmt)); //throw X
- const int deviceTimeOut = AFS::getAccessTimeout(folderPath); //0 if no timeout in force
- const auto timeoutTime = startTime + std::chrono::seconds(deviceTimeOut > 0 ? deviceTimeOut : DEFAULT_FOLDER_ACCESS_TIME_OUT_SEC);
+ int deviceTimeOutSec = AFS::getAccessTimeout(folderPath); //0 if no timeout in force
+ if (deviceTimeOutSec <= 0)
+ deviceTimeOutSec = DEFAULT_FOLDER_ACCESS_TIME_OUT_SEC;
+
+ const auto timeoutTime = startTime + std::chrono::seconds(deviceTimeOutSec);
while (std::chrono::steady_clock::now() < timeoutTime &&
future.wait_for(UI_UPDATE_INTERVAL / 2) != std::future_status::ready)
procCallback.requestUiUpdate(); //throw X
if (!isReady(future))
- output.failedChecks.emplace(folderPath, FileError(replaceCpy(_("Timeout while searching for folder %x."), L"%x", displayPathFmt)));
+ output.failedChecks.emplace(folderPath, FileError(replaceCpy(_("Timeout while searching for folder %x."), L"%x", displayPathFmt) +
+ L" [" + _P("1 sec", "%x sec", deviceTimeOutSec) + L"]"));
else
try
{
diff --git a/FreeFileSync/Source/base/dir_lock.cpp b/FreeFileSync/Source/base/dir_lock.cpp
index f32e5832..ab76158f 100644
--- a/FreeFileSync/Source/base/dir_lock.cpp
+++ b/FreeFileSync/Source/base/dir_lock.cpp
@@ -6,18 +6,19 @@
#include "dir_lock.h"
#include <map>
#include <memory>
+#include <zen/crc.h>
#include <zen/sys_error.h>
#include <zen/thread.h>
#include <zen/scope_guard.h>
#include <zen/guid.h>
#include <zen/file_access.h>
#include <zen/file_io.h>
+#include <zen/system.h>
#include <fcntl.h> //open()
- #include <sys/stat.h> //
- #include <unistd.h> //getsid()
+ //#include <sys/stat.h> //
+ #include <unistd.h> //close()
#include <signal.h> //kill()
- #include <pwd.h> //getpwuid_r()
using namespace zen;
using namespace fff;
@@ -29,8 +30,8 @@ const std::chrono::seconds EMIT_LIFE_SIGN_INTERVAL (5); //show life sign;
const std::chrono::seconds POLL_LIFE_SIGN_INTERVAL (4); //poll for life sign;
const std::chrono::seconds DETECT_ABANDONED_INTERVAL(30); //assume abandoned lock;
-const char LOCK_FORMAT_DESCR[] = "FreeFileSync";
-const int LOCK_FORMAT_VER = 2; //lock file format version
+const char LOCK_FILE_DESCR[] = "FreeFileSync";
+const int LOCK_FILE_VERSION = 3; //2020-02-07
const int ABANDONED_LOCK_LEVEL_MAX = 10;
}
@@ -105,8 +106,6 @@ private:
};
-
-
using ProcessId = pid_t;
using SessionId = pid_t;
@@ -142,34 +141,21 @@ LockInformation getLockInfoFromCurrentProcess() //throw FileError
{
LockInformation lockInfo = {};
lockInfo.lockId = generateGUID();
+ lockInfo.userId = utfTo<std::string>(getUserName()); //throw FileError
- //wxGetFullHostName() is a performance killer and can hang for some users, so don't touch!
-
- lockInfo.processId = ::getpid(); //never fails
+ const std::string osName = "Linux";
+ //wxGetFullHostName() is a performance killer and can hang for some users, so don't touch!
std::vector<char> buffer(10000);
if (::gethostname(&buffer[0], buffer.size()) != 0)
THROW_LAST_FILE_ERROR(_("Cannot get process information."), L"gethostname");
- lockInfo.computerName = "Linux."; //distinguish linux/windows lock files
- lockInfo.computerName += &buffer[0];
+ lockInfo.computerName = osName + " " + &buffer[0] + ".";
if (::getdomainname(&buffer[0], buffer.size()) != 0)
THROW_LAST_FILE_ERROR(_("Cannot get process information."), L"getdomainname");
- lockInfo.computerName += ".";
lockInfo.computerName += &buffer[0];
- const uid_t userIdNo = ::getuid(); //never fails
-
- //the id alone is not very distinctive, e.g. often 1000 on Ubuntu => add name
- buffer.resize(std::max<long>(buffer.size(), ::sysconf(_SC_GETPW_R_SIZE_MAX))); //::sysconf may return long(-1)
- struct passwd buffer2 = {};
- struct passwd* pwsEntry = nullptr;
- if (::getpwuid_r(userIdNo, &buffer2, &buffer[0], buffer.size(), &pwsEntry) != 0) //getlogin() is deprecated and not working on Ubuntu at all!!!
- THROW_LAST_FILE_ERROR(_("Cannot get process information."), L"getpwuid_r");
- if (!pwsEntry)
- throw FileError(_("Cannot get process information."), L"no login found"); //should not happen?
-
- lockInfo.userId = numberTo<std::string>(userIdNo) + "(" + pwsEntry->pw_name + ")"; //follow Linux naming convention "1000(zenju)"
+ lockInfo.processId = ::getpid(); //never fails
std::optional<SessionId> sessionIdTmp = getSessionId(lockInfo.processId); //throw FileError
if (!sessionIdTmp)
@@ -180,54 +166,77 @@ LockInformation getLockInfoFromCurrentProcess() //throw FileError
}
-LockInformation unserialize(MemoryStreamIn<ByteArray>& stream) //throw UnexpectedEndOfStreamError
+std::string serialize(const LockInformation& lockInfo)
{
- //-------- file format header --------
- char tmp[sizeof(LOCK_FORMAT_DESCR)] = {};
- readArray(stream, &tmp, sizeof(tmp)); //throw UnexpectedEndOfStreamError
+ MemoryStreamOut<std::string> streamOut;
+ writeArray(streamOut, LOCK_FILE_DESCR, sizeof(LOCK_FILE_DESCR));
+ writeNumber<int32_t>(streamOut, LOCK_FILE_VERSION);
- const int lockFileVersion = readNumber<int32_t>(stream); //throw UnexpectedEndOfStreamError
+ static_assert(sizeof(lockInfo.processId) <= sizeof(uint64_t)); //ensure cross-platform compatibility!
+ static_assert(sizeof(lockInfo.sessionId) <= sizeof(uint64_t)); //
- if (!std::equal(std::begin(tmp), std::end(tmp), std::begin(LOCK_FORMAT_DESCR)) ||
- lockFileVersion != LOCK_FORMAT_VER)
- throw UnexpectedEndOfStreamError(); //well, not really...!?
+ writeContainer(streamOut, lockInfo.lockId);
+ writeContainer(streamOut, lockInfo.computerName);
+ writeContainer(streamOut, lockInfo.userId);
+ writeNumber<uint64_t>(streamOut, lockInfo.sessionId);
+ writeNumber<uint64_t>(streamOut, lockInfo.processId);
- LockInformation lockInfo = {};
- lockInfo.lockId = readContainer<std::string>(stream); //
- lockInfo.computerName = readContainer<std::string>(stream); //UnexpectedEndOfStreamError
- lockInfo.userId = readContainer<std::string>(stream); //
- lockInfo.sessionId = static_cast<SessionId>(readNumber<uint64_t>(stream)); //[!] conversion
- lockInfo.processId = static_cast<ProcessId>(readNumber<uint64_t>(stream)); //[!] conversion
- return lockInfo;
+ writeNumber<uint32_t>(streamOut, getCrc32(streamOut.ref()));
+ writeArray(streamOut, "x", 1); //sentinel: mark logical end with a non-whitespace character
+ return streamOut.ref();
}
-void serialize(const LockInformation& lockInfo, MemoryStreamOut<ByteArray>& stream)
+LockInformation unserialize(const std::string& byteStream) //throw UnexpectedEndOfStreamError
{
- writeArray(stream, LOCK_FORMAT_DESCR, sizeof(LOCK_FORMAT_DESCR));
- writeNumber<int32_t>(stream, LOCK_FORMAT_VER);
+ MemoryStreamIn streamIn(byteStream);
- static_assert(sizeof(lockInfo.processId) <= sizeof(uint64_t)); //ensure cross-platform compatibility!
- static_assert(sizeof(lockInfo.sessionId) <= sizeof(uint64_t)); //
+ char formatDescr[sizeof(LOCK_FILE_DESCR)] = {};
+ readArray(streamIn, &formatDescr, sizeof(formatDescr)); //throw UnexpectedEndOfStreamError
+
+ if (!std::equal(std::begin(formatDescr), std::end(formatDescr), std::begin(LOCK_FILE_DESCR)))
+ throw UnexpectedEndOfStreamError(); //well, not really...!?
+
+ const int version = readNumber<int32_t>(streamIn); //throw UnexpectedEndOfStreamError
+ if (version != 2 && //TODO: remove migration code at some time! v2 used until 2020-02-07
+ version != LOCK_FILE_VERSION)
+ throw UnexpectedEndOfStreamError(); //well, not really...!?
- writeContainer(stream, lockInfo.lockId);
- writeContainer(stream, lockInfo.computerName);
- writeContainer(stream, lockInfo.userId);
- writeNumber<uint64_t>(stream, lockInfo.sessionId);
- writeNumber<uint64_t>(stream, lockInfo.processId);
+ if (version == 2) //TODO: remove migration code at some time! v2 used until 2020-02-07
+ ;
+ else //catch data corruption ASAP + don't rely on std::bad_alloc for consistency checking
+ {
+ std::string byteStreamTrm = trimCpy(byteStream, false, true); //get rid of space chars
+ assert(byteStreamTrm.size() >= sizeof(uint32_t) + sizeof('x')); //obviously in this context!
+ byteStreamTrm.pop_back();
+
+ MemoryStreamOut<std::string> crcStreamOut;
+ writeNumber<uint32_t>(crcStreamOut, getCrc32({ byteStreamTrm.begin(), byteStreamTrm.end() - sizeof(uint32_t) }));
+
+ if (!endsWith(byteStreamTrm, crcStreamOut.ref()))
+ throw UnexpectedEndOfStreamError(); //well, not really...!?
+ }
+
+ LockInformation lockInfo = {};
+ lockInfo.lockId = readContainer<std::string>(streamIn); //
+ lockInfo.computerName = readContainer<std::string>(streamIn); //UnexpectedEndOfStreamError
+ lockInfo.userId = readContainer<std::string>(streamIn); //
+ lockInfo.sessionId = static_cast<SessionId>(readNumber<uint64_t>(streamIn)); //[!] conversion
+ lockInfo.processId = static_cast<ProcessId>(readNumber<uint64_t>(streamIn)); //[!] conversion
+ return lockInfo;
}
LockInformation retrieveLockInfo(const Zstring& lockFilePath) //throw FileError
{
- MemoryStreamIn<ByteArray> memStreamIn(loadBinContainer<ByteArray>(lockFilePath, nullptr /*notifyUnbufferedIO*/)); //throw FileError
+ const std::string byteStream = loadBinContainer<std::string>(lockFilePath, nullptr /*notifyUnbufferedIO*/); //throw FileError
try
{
- return unserialize(memStreamIn); //throw UnexpectedEndOfStreamError
+ return unserialize(byteStream); //throw UnexpectedEndOfStreamError
}
catch (UnexpectedEndOfStreamError&)
{
- throw FileError(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(lockFilePath)), L"Unexpected end of stream.");
+ throw FileError(replaceCpy(_("Consistency check failed for %x."), L"%x", fmtPath(lockFilePath)), L"Unexpected end of stream.");
}
}
@@ -403,11 +412,10 @@ bool tryLock(const Zstring& lockFilePath) //throw FileError
FileOutput fileOut(hFile, lockFilePath, nullptr /*notifyUnbufferedIO*/); //pass handle ownership
//write housekeeping info: user, process info, lock GUID
- MemoryStreamOut<ByteArray> streamOut;
- serialize(getLockInfoFromCurrentProcess(), streamOut); //throw FileError
+ const std::string byteStream = serialize(getLockInfoFromCurrentProcess()); //throw FileError
- fileOut.write(&*streamOut.ref().begin(), streamOut.ref().size()); //throw FileError, (X)
- fileOut.finalize(); //
+ fileOut.write(byteStream.c_str(), byteStream.size()); //throw FileError, (X)
+ fileOut.finalize(); //
return true;
}
}
diff --git a/FreeFileSync/Source/base/file_hierarchy.cpp b/FreeFileSync/Source/base/file_hierarchy.cpp
index 40087637..1f10a793 100644
--- a/FreeFileSync/Source/base/file_hierarchy.cpp
+++ b/FreeFileSync/Source/base/file_hierarchy.cpp
@@ -20,8 +20,8 @@ std::wstring fff::getShortDisplayNameForFolderPair(const AbstractPath& itemPathL
AbstractPath tmpPathR = itemPathR;
for (;;)
{
- std::optional<AbstractPath> parentPathL = AFS::getParentPath(tmpPathL);
- std::optional<AbstractPath> parentPathR = AFS::getParentPath(tmpPathR);
+ const std::optional<AbstractPath> parentPathL = AFS::getParentPath(tmpPathL);
+ const std::optional<AbstractPath> parentPathR = AFS::getParentPath(tmpPathR);
if (!parentPathL || !parentPathR)
break;
diff --git a/FreeFileSync/Source/base/file_hierarchy.h b/FreeFileSync/Source/base/file_hierarchy.h
index 4ab6a6f7..c717e41f 100644
--- a/FreeFileSync/Source/base/file_hierarchy.h
+++ b/FreeFileSync/Source/base/file_hierarchy.h
@@ -985,7 +985,7 @@ FolderPair& ContainerObject::addSubFolder<RIGHT_SIDE>(const Zstring& itemName, c
inline
FilePair& ContainerObject::addSubFile(const Zstring& itemNameL,
const FileAttributes& left, //file exists on both sides
- CompareFileResult defaultCmpResult,
+ CompareFileResult defaultCmpResult,
const Zstring& itemNameR,
const FileAttributes& right)
{
diff --git a/FreeFileSync/Source/base/icon_loader.h b/FreeFileSync/Source/base/icon_loader.h
index 8f6b31b9..0f4e49ec 100644
--- a/FreeFileSync/Source/base/icon_loader.h
+++ b/FreeFileSync/Source/base/icon_loader.h
@@ -14,7 +14,7 @@
namespace fff
{
//=> all functions are safe to call from multiple threads!
-//!!!Note: init COM + system image list before loading icons!!!
+//COM needs to be initialized before calling any of these functions! CoInitializeEx/CoUninitialize
//=> don't call from WM_PAINT handler! https://blogs.msdn.microsoft.com/yvesdolc/2009/08/06/do-you-receive-wm_paint-when-waiting-for-a-com-call-to-return/
//return null icon on failure:
diff --git a/FreeFileSync/Source/base/log_file.cpp b/FreeFileSync/Source/base/log_file.cpp
index 39e8f0a7..ac866e9a 100644
--- a/FreeFileSync/Source/base/log_file.cpp
+++ b/FreeFileSync/Source/base/log_file.cpp
@@ -6,6 +6,8 @@
#include "log_file.h"
#include <zen/file_io.h>
+#include <zen/http.h>
+#include <zen/system.h>
#include <wx/datetime.h>
#include "ffs_paths.h"
#include "../afs/concrete.h"
@@ -17,157 +19,412 @@ using AFS = AbstractFileSystem;
namespace
{
-std::wstring generateLogHeader(const ProcessSummary& s, const ErrorLog& log, const std::wstring& finalStatusMsg)
-{
- //assemble summary box
- std::vector<std::wstring> summary;
+const int LOG_FAIL_PREVIEW_MAX = 25;
+const int SEPARATION_LINE_LEN = 40;
- const std::wstring tabSpace(4, L' '); //4, the one true space count for tabs
- const TimeComp tc = getLocalTime(std::chrono::system_clock::to_time_t(s.startTime)); //returns empty string on failure
+std::string generateLogHeaderTxt(const ProcessSummary& s, const ErrorLog& log, int logFailsPreviewMax)
+{
+ const std::string tabSpace(4, ' '); //4, the one true space count for tabs
- std::wstring headerLine = formatTime<std::wstring>(FORMAT_DATE, tc) + L" [" + formatTime<std::wstring>(FORMAT_TIME, tc) + L"]";
- if (!s.jobName.empty())
- headerLine += L" | " + s.jobName;
+ std::string headerLine;
+ for (const std::wstring& jobName : s.jobNames)
+ headerLine += (headerLine.empty() ? "" : " + ") + utfTo<std::string>(jobName);
- summary.push_back(headerLine);
- summary.push_back(L"");
- summary.push_back(tabSpace + finalStatusMsg);
+ if (!headerLine.empty())
+ headerLine += " ";
+ const TimeComp tc = getLocalTime(std::chrono::system_clock::to_time_t(s.startTime)); //returns empty string on failure
+ headerLine += formatTime<std::string>(FORMAT_DATE, tc) + " [" + formatTime<std::string>(FORMAT_TIME, tc) + ']';
+
+ //assemble summary box
+ std::vector<std::string> summary;
+ summary.emplace_back();
+ summary.push_back(tabSpace + utfTo<std::string>(getResultsStatusLabel(s.resultStatus)));
+ summary.emplace_back();
const int errorCount = log.getItemCount(MSG_TYPE_ERROR | MSG_TYPE_FATAL_ERROR);
const int warningCount = log.getItemCount(MSG_TYPE_WARNING);
- if (errorCount > 0) summary.push_back(tabSpace + _("Errors:") + L" " + formatNumber(errorCount));
- if (warningCount > 0) summary.push_back(tabSpace + _("Warnings:") + L" " + formatNumber(warningCount));
-
+ if (errorCount > 0) summary.push_back(tabSpace + utfTo<std::string>(_("Errors:") + L" " + formatNumber(errorCount)));
+ if (warningCount > 0) summary.push_back(tabSpace + utfTo<std::string>(_("Warnings:") + L" " + formatNumber(warningCount)));
- std::wstring itemsProc = tabSpace + _("Items processed:") + L" " + formatNumber(s.statsProcessed.items); //show always, even if 0!
- itemsProc += L" (" + formatFilesizeShort(s.statsProcessed.bytes) + L")";
- summary.push_back(itemsProc);
+ summary.push_back(tabSpace + utfTo<std::string>(_("Items processed:") + L" " + formatNumber(s.statsProcessed.items) + //show always, even if 0!
+ L" (" + formatFilesizeShort(s.statsProcessed.bytes) + L')'));
if ((s.statsTotal.items < 0 && s.statsTotal.bytes < 0) || //no total items/bytes: e.g. for pure folder comparison
s.statsProcessed == s.statsTotal) //...if everything was processed successfully
;
else
- summary.push_back(tabSpace + _("Items remaining:") +
- L" " + formatNumber (s.statsTotal.items - s.statsProcessed.items) +
- L" (" + formatFilesizeShort(s.statsTotal.bytes - s.statsProcessed.bytes) + L")");
+ summary.push_back(tabSpace + utfTo<std::string>(_("Items remaining:") +
+ L" " + formatNumber (s.statsTotal.items - s.statsProcessed.items) +
+ L" (" + formatFilesizeShort(s.statsTotal.bytes - s.statsProcessed.bytes) + L')'));
const int64_t totalTimeSec = std::chrono::duration_cast<std::chrono::seconds>(s.totalTime).count();
- summary.push_back(tabSpace + _("Total time:") + L" " + copyStringTo<std::wstring>(wxTimeSpan::Seconds(totalTimeSec).Format()));
+ summary.push_back(tabSpace + utfTo<std::string>(_("Total time:")) + " " + utfTo<std::string>(wxTimeSpan::Seconds(totalTimeSec).Format()));
- //calculate max width, this considers UTF-16, not Unicode code points...but maybe good idea? those 2-byte-UTF16 chars are usually wider than fixed-width chars anyway!
- size_t sepLineLen = 0;
- for (const std::wstring& str : summary) sepLineLen = std::max(sepLineLen, str.size());
+ size_t sepLineLen = 0; //calculate max width (considering Unicode!)
+ for (const std::string& str : summary) sepLineLen = std::max(sepLineLen, unicodeLength(str));
- std::wstring output(sepLineLen + 1, L'_');
- output += L'\n';
+ std::string output = headerLine + '\n';
+ output += std::string(sepLineLen + 1, '_') + '\n';
- for (const std::wstring& str : summary) { output += L'|'; output += str; output += L'\n'; }
+ for (const std::string& str : summary)
+ output += '|' + str + '\n';
- output += L'|';
- output.append(sepLineLen, L'_');
- output += L'\n';
+ output += '|' + std::string(sepLineLen, '_') + "\n\n";
+ //------------ warnings/errors preview ----------------
+ const int logFailTotal = errorCount + warningCount;
+ if (logFailTotal > 0)
+ {
+ output += '\n' + utfTo<std::string>(_("Errors and warnings:")) + '\n';
+ output += std::string(SEPARATION_LINE_LEN, '_') + '\n';
+
+ int previewCount = 0;
+ for (const LogEntry& entry : log)
+ if (entry.type & (MSG_TYPE_WARNING | MSG_TYPE_ERROR | MSG_TYPE_FATAL_ERROR))
+ {
+ output += utfTo<std::string>(formatMessage(entry));
+ if (++previewCount >= logFailsPreviewMax)
+ break;
+ }
+ if (logFailTotal > previewCount)
+ output += " [...] " + utfTo<std::string>(replaceCpy(_P("Showing %y of 1 row", "Showing %y of %x rows", logFailTotal), //%x used as plural form placeholder!
+ L"%y", formatNumber(previewCount))) + '\n';
+ output += std::string(SEPARATION_LINE_LEN, '_') + "\n\n\n";
+ }
return output;
}
-void streamToLogFile(const ProcessSummary& summary, //throw FileError
- const ErrorLog& log,
- const std::wstring& finalStatusLabel,
- AFS::OutputStream& streamOut)
+std::string generateLogFooterTxt(const std::wstring& logFilePath, int logItemsTotal, int logItemsPreviewMax) //throw FileError
{
- auto fmtForTxtFile = [needLbReplace = !equalString(LINE_BREAK, '\n')](const std::wstring& str)
- {
- std::string utfStr = utfTo<std::string>(str);
- if (needLbReplace)
- replace(utfStr, '\n', LINE_BREAK);
- return utfStr;
- };
+ const ComputerModel cm = getComputerModel(); //throw FileError
- std::string buffer = fmtForTxtFile(generateLogHeader(summary, log, finalStatusLabel)); //don't replace line break any earlier
+ std::string output;
+ if (logItemsTotal > logItemsPreviewMax)
+ output += " [...] " + utfTo<std::string>(replaceCpy(_P("Showing %y of 1 row", "Showing %y of %x rows", logItemsTotal), //%x used as plural form placeholder!
+ L"%y", formatNumber(logItemsPreviewMax))) + '\n';
- streamOut.write(&buffer[0], buffer.size()); //throw FileError, X
- buffer.clear(); //flush out header if entry.empty()
+ return output += '\n' + utfTo<std::string>(getOsDescription() + /*throw FileError*/ +
+ L" [" + getUserName() /*throw FileError*/ + L"] - " + cm.model + L" - " + cm.vendor + L'\n' +
+ std::wstring(SEPARATION_LINE_LEN, L'_') + L'\n' +
+ _("Log file") + L": " + logFilePath) + '\n';
+}
- buffer += LINE_BREAK;
- for (const LogEntry& entry : log)
- {
- buffer += fmtForTxtFile(formatMessage(entry));
- buffer += LINE_BREAK;
+std::string htmlTxtImpl(std::string&& str)
+{
+ trim(str);
+ std::string msg = htmlSpecialChars(str);
+ if (!contains(msg, '\n'))
+ return msg;
+
+ std::string msgFmt;
+ for (auto it = msg.begin(); it != msg.end(); )
+ if (*it == '\n')
+ {
+ msgFmt += "<br>\n";
+ ++it;
- //write log items in blocks instead of creating one big string: memory allocation might fail; think 1 million entries!
- streamOut.write(&buffer[0], buffer.size()); //throw FileError, X
- buffer.clear();
+ //skip duplicate newlines
+ for (; it != msg.end() && *it == L'\n'; ++it)
+ ;
+
+ //preserve leading spaces
+ for (; it != msg.end() && *it == L' '; ++it)
+ msgFmt += "&nbsp;";
+ }
+ else
+ msgFmt += *it++;
+
+ return msgFmt;
+}
+
+std::string htmlTxt(const std::wstring& str) { return htmlTxtImpl(utfTo<std::string>(str)); }
+std::string htmlTxt(const wchar_t* str) { return htmlTxtImpl(utfTo<std::string>(str)); }
+
+
+//Astyle screws up royally with the following raw string literals!
+//*INDENT-OFF*
+std::string formatMessageHtml(const LogEntry& entry)
+{
+ const std::string typeLabel = htmlTxt(getMessageTypeLabel(entry.type));
+ const char* typeImage = nullptr;
+ switch (entry.type)
+ {
+ case MSG_TYPE_INFO: typeImage = "msg-info.png"; break;
+ case MSG_TYPE_WARNING: typeImage = "msg-warning.png"; break;
+ case MSG_TYPE_ERROR:
+ case MSG_TYPE_FATAL_ERROR: typeImage = "msg-error.png"; break;
}
+
+ return R"( <tr>
+ <td valign="top">)" + formatTime<std::string>(FORMAT_TIME, getLocalTime(entry.time)) + R"(</td>
+ <td valign="top"><img src="https://freefilesync.org/images/log/)" + typeImage + R"(" width="16" height="16" alt=")" + typeLabel + R"(:" title=")" + typeLabel + R"("></td>
+ <td>)" + htmlTxt(entry.message.c_str()) + R"(</td>
+ </tr>
+)";
}
-const int TIME_STAMP_LENGTH = 21;
-const Zchar STATUS_BEGIN_TOKEN[] = Zstr(" [");
-const Zchar STATUS_END_TOKEN = Zstr(']');
+std::wstring generateLogTitle(const ProcessSummary& s)
+{
+ std::wstring jobNamesFmt;
+ for (const std::wstring& jobName : s.jobNames)
+ jobNamesFmt += (jobNamesFmt.empty() ? L"" : L" + ") + jobName;
+
+ std::wstring title = L"[FreeFileSync] ";
-//"Backup FreeFileSync 2013-09-15 015052.123.log" ->
-//"Backup FreeFileSync 2013-09-15 015052.123 [Error].log"
-AbstractPath saveNewLogFile(const ProcessSummary& summary, //throw FileError
- const ErrorLog& log,
- const AbstractPath& logFolderPath,
- const std::function<void(const std::wstring& msg)>& notifyStatus /*throw X*/)
+ if (!jobNamesFmt.empty())
+ title += jobNamesFmt + L' ';
+
+ switch (s.resultStatus)
+ {
+ case SyncResult::finishedSuccess: title += utfTo<std::wstring>("\xe2\x9c\x94\xef\xb8\x8f"); break;
+ case SyncResult::finishedWarning: title += utfTo<std::wstring>("\xe2\x9a\xa0"); break;
+ case SyncResult::finishedError:
+ case SyncResult::aborted: title += utfTo<std::wstring>("\xe2\x9d\x8c"); break;
+ }
+ return title;
+}
+
+
+std::string generateLogHeaderHtml(const ProcessSummary& s, const ErrorLog& log, int logFailsPreviewMax)
{
- //const std::string colon = "\xcb\xb8"; //="modifier letter raised colon" => regular colon is forbidden in file names on Windows and OS X
- //=> too many issues, most notably cmd.exe is not Unicode-aware: https://freefilesync.org/forum/viewtopic.php?t=1679
+ std::string output = R"(<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>)" + htmlTxt(generateLogTitle(s)) + R"(</title>
+ <style>
+)" + /*caveat: non-inline CSS is often ignored by email clients!*/ R"(
+ .summary-table td:nth-child(1) { padding-right: 10px; }
+ .summary-table td:nth-child(2) { padding-right: 5px; }
+ .summary-table img { display: block; }
+
+ .log-items img { display: block; }
+ .log-items td { padding-bottom: 0.1em; }
+ .log-items td:nth-child(1) { padding-right: 10px; }
+ .log-items td:nth-child(2) { padding-right: 10px; }
+ </style>
+</head>
+<body style="font-family: -apple-system, 'Segoe UI', arial, Tahoma, Helvetica, sans-serif;">
+)";
+
+ std::string jobNamesFmt;
+ for (const std::wstring& jobName : s.jobNames)
+ jobNamesFmt += (jobNamesFmt.empty() ? "" : " + ") + htmlTxt(jobName);
- //assemble logfile name
- const TimeComp tc = getLocalTime(std::chrono::system_clock::to_time_t(summary.startTime));
- if (tc == TimeComp())
- throw FileError(L"Failed to determine current time: " + numberTo<std::wstring>(summary.startTime.time_since_epoch().count()));
+ const TimeComp tc = getLocalTime(std::chrono::system_clock::to_time_t(s.startTime)); //returns empty string on failure
+ output += R"( <div><span style="font-weight:600; color:gray;">)" + jobNamesFmt + R"(</span> &nbsp;<span style="white-space:nowrap">)" +
+ formatTime<std::string>(FORMAT_DATE, tc) + " &nbsp;" + formatTime<std::string>(FORMAT_TIME, tc) + "</span></div>\n";
- const auto timeMs = std::chrono::duration_cast<std::chrono::milliseconds>(summary.startTime.time_since_epoch()).count() % 1000;
- assert(std::chrono::duration_cast<std::chrono::seconds>(summary.startTime.time_since_epoch()).count() == std::chrono::system_clock::to_time_t(summary.startTime));
+ std::string resultsStatusImage;
+ switch (s.resultStatus)
+ {
+ case SyncResult::finishedSuccess: resultsStatusImage = "result-succes.png"; break;
+ case SyncResult::finishedWarning: resultsStatusImage = "result-warning.png"; break;
+ case SyncResult::finishedError:
+ case SyncResult::aborted: resultsStatusImage = "result-error.png"; break;
+ }
+ output += R"(
+ <div style="margin:10px 0; display:inline-block; border-radius:7px; background:#f8f8f8; box-shadow:1px 1px 4px #888; overflow:hidden;">
+ <div style="background-color:white; border-bottom:1px solid #AAA; font-size:larger; padding:10px;">
+ <img src="https://freefilesync.org/images/log/)" + resultsStatusImage + R"(" width="32" height="32" alt="" style="vertical-align:middle;">
+ <span style="font-weight:600; vertical-align:middle;">)" + htmlTxt(getResultsStatusLabel(s.resultStatus)) + R"(</span>
+ </div>
+ <table role="presentation" class="summary-table" style="border-spacing:0; margin-left:10px; padding:5px 10px;">)";
- Zstring logFileName;
+ const int errorCount = log.getItemCount(MSG_TYPE_ERROR | MSG_TYPE_FATAL_ERROR);
+ const int warningCount = log.getItemCount(MSG_TYPE_WARNING);
- if (!summary.jobName.empty())
- logFileName += utfTo<Zstring>(summary.jobName) + Zstr(' ');
+ if (errorCount > 0)
+ output += R"(
+ <tr>
+ <td>)" + htmlTxt(_("Errors:")) + R"(</td>
+ <td><img src="https://freefilesync.org/images/log/msg-error.png" width="24" height="24" alt=""></td>
+ <td><span style="font-weight:600;">)" + htmlTxt(formatNumber(errorCount)) + R"(</span></td>
+ </tr>)";
+
+ if (warningCount > 0)
+ output += R"(
+ <tr>
+ <td>)" + htmlTxt(_("Warnings:")) + R"(</td>
+ <td><img src="https://freefilesync.org/images/log/msg-warning.png" width="24" height="24" alt=""></td>
+ <td><span style="font-weight:600;">)" + htmlTxt(formatNumber(warningCount)) + R"(</span></td>
+ </tr>)";
+
+ output += R"(
+ <tr>
+ <td>)" + htmlTxt(_("Items processed:")) + R"(</td>
+ <td><img src="https://freefilesync.org/images/log/file.png" width="24" height="24" alt=""></td>
+ <td><span style="font-weight:600;">)" + htmlTxt(formatNumber(s.statsProcessed.items)) + "</span> (" +
+ htmlTxt(formatFilesizeShort(s.statsProcessed.bytes)) + R"()</td>
+ </tr>)";
- logFileName += formatTime<Zstring>(Zstr("%Y-%m-%d %H%M%S"), tc) +
- Zstr(".") + printNumber<Zstring>(Zstr("%03d"), static_cast<int>(timeMs)); //[ms] should yield a fairly unique name
- static_assert(TIME_STAMP_LENGTH == 21);
+ if ((s.statsTotal.items < 0 && s.statsTotal.bytes < 0) || //no total items/bytes: e.g. for pure folder comparison
+ s.statsProcessed == s.statsTotal) //...if everything was processed successfully
+ ;
+ else
+ output += R"(
+ <tr>
+ <td>)" + htmlTxt(_("Items remaining:")) + R"(</td>
+ <td></td>
+ <td><span style="font-weight:600;">)" + htmlTxt(formatNumber(s.statsTotal.items - s.statsProcessed.items)) + "</span> (" +
+ htmlTxt(formatFilesizeShort(s.statsTotal.bytes - s.statsProcessed.bytes)) + R"()</td>
+ </tr>)";
- const std::wstring failStatus = [&]
+ const int64_t totalTimeSec = std::chrono::duration_cast<std::chrono::seconds>(s.totalTime).count();
+ output += R"(
+ <tr>
+ <td>)" + htmlTxt(_("Total time:")) + R"(</td>
+ <td><img src="https://freefilesync.org/images/log/clock.png" width="24" height="24" alt=""></td>
+ <td><span style="font-weight: 600;">)" + htmlTxt(wxTimeSpan::Seconds(totalTimeSec).Format()) + R"(</span></td>
+ </tr>
+ </table>
+ </div>
+)";
+
+ //------------ warnings/errors preview ----------------
+ const int logFailTotal = errorCount + warningCount;
+ if (logFailTotal > 0)
{
- switch (summary.finalStatus)
- {
- case SyncResult::finishedSuccess:
- break;
- case SyncResult::finishedWarning:
- return _("Warning");
- case SyncResult::finishedError:
- return _("Error");
- case SyncResult::aborted:
- return _("Stopped");
- }
- return std::wstring();
- }();
+ output += R"(
+ <div style="font-weight:600; font-size: large;">)" + htmlTxt(_("Errors and warnings:")) + R"(</div>
+ <div style="border-bottom: 1px solid #AAA; margin: 5px 0;"></div>
+ <table class="log-items" style="line-height:1em; border-spacing:0;">
+)";
+ int previewCount = 0;
+ for (const LogEntry& entry : log)
+ if (entry.type & (MSG_TYPE_WARNING | MSG_TYPE_ERROR | MSG_TYPE_FATAL_ERROR))
+ {
+ output += formatMessageHtml(entry);
+ if (++previewCount >= logFailsPreviewMax)
+ break;
+ }
+ output += R"( </table>
+)";
+ if (logFailTotal > previewCount)
+ output += R"( <div><span style="font-weight:600; padding:0 10px;">[&hellip;]</span>)" +
+ htmlTxt(replaceCpy(_P("Showing %y of 1 row", "Showing %y of %x rows", logFailTotal), //%x used as plural form placeholder!
+ L"%y", formatNumber(previewCount))) + "</div>\n";
+
+ output += R"( <div style="border-bottom: 1px solid #AAA; margin: 5px 0;"></div><br>
+)";
+ }
- if (!failStatus.empty())
- logFileName += STATUS_BEGIN_TOKEN + utfTo<Zstring>(failStatus) + STATUS_END_TOKEN;
- logFileName += Zstr(".log");
+ output += R"(
+ <table class="log-items" style="line-height:1em; border-spacing:0;">
+)";
+ return output;
+}
- const AbstractPath logFilePath = AFS::appendRelPath(logFolderPath, logFileName);
+std::string generateLogFooterHtml(const std::wstring& logFilePath, int logItemsTotal, int logItemsPreviewMax) //throw FileError
+{
+ const std::string osImage = "os-linux.png";
+ const ComputerModel cm = getComputerModel(); //throw FileError
+
+ std::string output = R"( </table>
+)";
+
+ if (logItemsTotal > logItemsPreviewMax)
+ output += R"( <div><span style="font-weight:600; padding:0 10px;">[&hellip;]</span>)" +
+ htmlTxt(replaceCpy(_P("Showing %y of 1 row", "Showing %y of %x rows", logItemsTotal), //%x used as plural form placeholder!
+ L"%y", formatNumber(logItemsPreviewMax))) + "</div>\n";
+
+ return output += R"( <br>
+
+ <div>
+ <img src="https://freefilesync.org/images/log/)" + osImage + R"(" width="24" height="24" alt="" style="vertical-align:middle;">
+ <span style="vertical-align:middle;">)" + htmlTxt(getOsDescription()) + /*throw FileError*/ +
+ " [" + htmlTxt(getUserName()) /*throw FileError*/ + "] &ndash; " + htmlTxt(cm.model) + " &ndash; " + htmlTxt(cm.vendor) + R"(</span>
+ </div>
+ <div style="border-bottom:1px solid #AAA; margin:5px 0;"></div>
+ <div>
+ <img src="https://freefilesync.org/images/log/log.png" width="24" height="24" alt=")" + htmlTxt(_("Log file")) + R"(:" title=")" + htmlTxt(_("Log file")) + R"(" style="vertical-align:middle;">
+ <span style="font-family: Consolas,'Courier New',Courier,monospace; vertical-align:middle;">)" + htmlTxt(logFilePath) + R"(</span>
+ </div>
+</body>
+</html>
+)";
+}
- //-----------------------------------------------------------------------
- try //create logfile folder if required
+//-> Astyle fucks up! => no INDENT-ON
+
+
+void streamToLogFile(const ProcessSummary& summary, //throw FileError
+ const ErrorLog& log,
+ AFS::OutputStream& streamOut,
+ const AbstractPath& logFilePath)
+{
+#if 0
+ auto fmtForTxtFile = [needLbReplace = !equalString(LINE_BREAK, '\n')](const std::string& str)
{
- AFS::createFolderIfMissingRecursion(logFolderPath); //throw FileError
+ if (needLbReplace)
+ return replaceCpy(str, '\n', LINE_BREAK);
+ return str;
+ };
+
+ std::string buffer = fmtForTxtFile(generateLogHeaderTxt(summary, log, LOG_FAIL_PREVIEW_MAX)); //don't replace line break any earlier
+
+ //write log items in blocks instead of creating one big string: memory allocation might fail; think 1 million entries!
+ for (const LogEntry& entry : log)
+ {
+ buffer += fmtForTxtFile(utfTo<std::string>(formatMessage(entry)));
+
+ streamOut.write(&buffer[0], buffer.size()); //throw FileError, X
+ buffer.clear();
}
- catch (const FileError& e) //add context info regarding log file!
+
+ const int logItemsTotal = log.end() - log.begin();
+ const int logItemsPreviewMax = std::numeric_limits<int>::max();
+
+ buffer += fmtForTxtFile(generateLogFooterTxt(AFS::getDisplayPath(logFilePath), logItemsTotal, logItemsPreviewMax)); //throw FileError
+
+ //don't forget to flush:
+ streamOut.write(&buffer[0], buffer.size()); //throw FileError, X
+
+#else
+ std::string buffer = generateLogHeaderHtml(summary, log, LOG_FAIL_PREVIEW_MAX);
+
+ //write log items in blocks instead of creating one big string: memory allocation might fail; think 1 million entries!
+ for (const LogEntry& entry : log)
{
- throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(AFS::getDisplayPath(logFilePath))), e.toString());
+ buffer += formatMessageHtml(entry);
+
+ streamOut.write(&buffer[0], buffer.size()); //throw FileError, X
+ buffer.clear();
}
+
+ const int logItemsTotal = log.end() - log.begin();
+ const int logItemsPreviewMax = std::numeric_limits<int>::max();
+
+ buffer += generateLogFooterHtml(AFS::getDisplayPath(logFilePath), logItemsTotal, logItemsPreviewMax); //throw FileError
+
+ //don't forget to flush:
+ streamOut.write(&buffer[0], buffer.size()); //throw FileError, X
+#endif
+}
+
+
+void saveNewLogFile(const AbstractPath& logFilePath, //throw FileError, X
+ const ProcessSummary& summary,
+ const ErrorLog& log,
+ const std::function<void(const std::wstring& msg)>& notifyStatus /*throw X*/)
+{
+ //create logfile folder if required
+ if (const std::optional<AbstractPath> parentPath = AFS::getParentPath(logFilePath))
+ try
+ {
+ AFS::createFolderIfMissingRecursion(*parentPath); //throw FileError
+ }
+ catch (const FileError& e) //add context info regarding log file!
+ {
+ throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(AFS::getDisplayPath(logFilePath))), e.toString());
+ }
//-----------------------------------------------------------------------
auto notifyUnbufferedIO = [notifyStatus,
@@ -179,21 +436,22 @@ AbstractPath saveNewLogFile(const ProcessSummary& summary, //throw FileError
notifyStatus(msg_ + L" (" + formatFilesizeShort(bytesWritten_ += bytesDelta) + L")"); //throw X
};
- const std::wstring& finalStatusLabel = getFinalStatusLabel(summary.finalStatus);
-
std::unique_ptr<AFS::OutputStream> logFileStream = AFS::getOutputStream(logFilePath, std::nullopt /*streamSize*/, std::nullopt /*modTime*/, notifyUnbufferedIO); //throw FileError
- streamToLogFile(summary, log, finalStatusLabel, *logFileStream); //throw FileError, X
- logFileStream->finalize(); //throw FileError, X
-
- return logFilePath;
+ streamToLogFile(summary, log, *logFileStream, logFilePath); //throw FileError, X
+ logFileStream->finalize(); //throw FileError, X
}
+const int TIME_STAMP_LENGTH = 21;
+const Zchar STATUS_BEGIN_TOKEN[] = Zstr(" [");
+const Zchar STATUS_END_TOKEN = Zstr(']');
+
+
struct LogFileInfo
{
AbstractPath filePath;
time_t timeStamp;
- std::wstring jobName; //may be empty
+ std::wstring jobNames; //may be empty
};
std::vector<LogFileInfo> getLogFiles(const AbstractPath& logFolderPath) //throw FileError
{
@@ -201,14 +459,16 @@ std::vector<LogFileInfo> getLogFiles(const AbstractPath& logFolderPath) //throw
AFS::traverseFolderFlat(logFolderPath, [&](const AFS::FileInfo& fi) //throw FileError
{
- //"Backup FreeFileSync 2013-09-15 015052.123.log"
+ //"Backup FreeFileSync 2013-09-15 015052.123.html"
+ //"Jobname1 + Jobname2 2013-09-15 015052.123.log"
//"2013-09-15 015052.123 [Error].log"
static_assert(TIME_STAMP_LENGTH == 21);
- if (endsWith(fi.itemName, Zstr(".log"))) //case-sensitive: e.g. ".LOG" is not from FFS, right?
+ if (endsWith(fi.itemName, Zstr(".log")) || //case-sensitive: e.g. ".LOG" is not from FFS, right?
+ endsWith(fi.itemName, Zstr(".html")))
{
auto tsBegin = fi.itemName.begin();
- auto tsEnd = fi.itemName.end() - 4;
+ auto tsEnd = tsBegin + fi.itemName.rfind('.');
if (tsBegin != tsEnd && tsEnd[-1] == STATUS_END_TOKEN)
tsEnd = searchLast(tsBegin, tsEnd,
@@ -225,14 +485,14 @@ std::vector<LogFileInfo> getLogFiles(const AbstractPath& logFolderPath) //throw
const time_t t = localToTimeT(tc); //returns -1 on error
if (t != -1)
{
- Zstring jobName(fi.itemName.begin(), tsBegin);
- if (!jobName.empty())
+ Zstring jobNames(fi.itemName.begin(), tsBegin);
+ if (!jobNames.empty())
{
- assert(jobName.size() >= 2 && jobName.end()[-1] == Zstr(' '));
- jobName.pop_back();
+ assert(jobNames.size() >= 2 && endsWith(jobNames, Zstr(' ')));
+ jobNames.pop_back();
}
- logfiles.push_back({ AFS::appendRelPath(logFolderPath, fi.itemName), t, utfTo<std::wstring>(jobName) });
+ logfiles.push_back({ AFS::appendRelPath(logFolderPath, fi.itemName), t, utfTo<std::wstring>(jobNames) });
}
}
}
@@ -244,14 +504,16 @@ std::vector<LogFileInfo> getLogFiles(const AbstractPath& logFolderPath) //throw
}
-void limitLogfileCount(const AbstractPath& logFolderPath, //throw FileError
+void limitLogfileCount(const AbstractPath& logFolderPath, //throw FileError, X
int logfilesMaxAgeDays, //<= 0 := no limit
const std::set<AbstractPath>& logFilePathsToKeep,
- const std::function<void(const std::wstring& msg)>& notifyStatus)
+ const std::function<void(const std::wstring& msg)>& notifyStatus /*throw X*/)
{
if (logfilesMaxAgeDays > 0)
{
- if (notifyStatus) notifyStatus(_("Cleaning up log files:") + L" " + fmtPath(AFS::getDisplayPath(logFolderPath)));
+ const std::wstring statusPrefix = _("Cleaning up log files:") + L" [" + _P("1 day", "%x days", logfilesMaxAgeDays) + L"] ";
+
+ if (notifyStatus) notifyStatus(statusPrefix + fmtPath(AFS::getDisplayPath(logFolderPath))); //throw X
std::vector<LogFileInfo> logFiles = getLogFiles(logFolderPath); //throw FileError
@@ -272,7 +534,7 @@ void limitLogfileCount(const AbstractPath& logFolderPath, //throw FileError
!contains(logFilePathsToKeep, lfi.filePath)) //don't trim latest log files corresponding to last used config files!
//nitpicker's corner: what about path differences due to case? e.g. user-overriden log file path changed in case
{
- if (notifyStatus) notifyStatus(_("Cleaning up log files:") + L" " + fmtPath(AFS::getDisplayPath(lfi.filePath)));
+ if (notifyStatus) notifyStatus(statusPrefix + fmtPath(AFS::getDisplayPath(lfi.filePath))); //throw X
try
{
AFS::removeFilePlain(lfi.filePath); //throw FileError
@@ -290,33 +552,97 @@ void limitLogfileCount(const AbstractPath& logFolderPath, //throw FileError
Zstring fff::getDefaultLogFolderPath() { return getConfigDirPathPf() + Zstr("Logs") ; }
-AbstractPath fff::saveLogFile(const ProcessSummary& summary, //throw FileError
- const ErrorLog& log,
- const Zstring& altLogFolderPathPhrase, //optional
- int logfilesMaxAgeDays,
- const std::set<AbstractPath>& logFilePathsToKeep,
- const std::function<void(const std::wstring& msg)>& notifyStatus /*throw X*/)
+//"Backup FreeFileSync 2013-09-15 015052.123.html"
+//"Backup FreeFileSync 2013-09-15 015052.123 [Error].html"
+AbstractPath fff::generateLogFilePath(const ProcessSummary& summary, const Zstring& altLogFolderPathPhrase /*optional*/)
{
+ //const std::string colon = "\xcb\xb8"; //="modifier letter raised colon" => regular colon is forbidden in file names on Windows and OS X
+ //=> too many issues, most notably cmd.exe is not Unicode-aware: https://freefilesync.org/forum/viewtopic.php?t=1679
+
+ //assemble logfile name
+ const TimeComp tc = getLocalTime(std::chrono::system_clock::to_time_t(summary.startTime));
+ if (tc == TimeComp())
+ throw FileError(L"Failed to determine current time: " + numberTo<std::wstring>(summary.startTime.time_since_epoch().count()));
+
+ const auto timeMs = std::chrono::duration_cast<std::chrono::milliseconds>(summary.startTime.time_since_epoch()).count() % 1000;
+ assert(std::chrono::duration_cast<std::chrono::seconds>(summary.startTime.time_since_epoch()).count() == std::chrono::system_clock::to_time_t(summary.startTime));
+
+ Zstring logFileName;
+ if (!summary.jobNames.empty())
+ {
+ for (const std::wstring& jobName : summary.jobNames)
+ logFileName += utfTo<Zstring>(jobName) + Zstr(" + ");
+ logFileName.resize(logFileName.size() - 2);
+ }
+
+ logFileName += formatTime<Zstring>(Zstr("%Y-%m-%d %H%M%S"), tc) +
+ Zstr(".") + printNumber<Zstring>(Zstr("%03d"), static_cast<int>(timeMs)); //[ms] should yield a fairly unique name
+ static_assert(TIME_STAMP_LENGTH == 21);
+
+ const std::wstring failStatus = [&]
+ {
+ switch (summary.resultStatus)
+ {
+ case SyncResult::finishedSuccess: break;
+ case SyncResult::finishedWarning: return _("Warning");
+ case SyncResult::finishedError: return _("Error");
+ case SyncResult::aborted: return _("Stopped");
+ }
+ return std::wstring();
+ }();
+
+ if (!failStatus.empty())
+ logFileName += STATUS_BEGIN_TOKEN + utfTo<Zstring>(failStatus) + STATUS_END_TOKEN;
+ logFileName += Zstr(".html");
+
+
AbstractPath logFolderPath = createAbstractPath(altLogFolderPathPhrase);
if (AFS::isNullPath(logFolderPath))
logFolderPath = createAbstractPath(getDefaultLogFolderPath());
- AbstractPath logFilePath = getNullPath();
+ return AFS::appendRelPath(logFolderPath, logFileName);
+}
+
+
+void fff::saveLogFile(const AbstractPath& logFilePath, //throw FileError, X
+ const ProcessSummary& summary,
+ const ErrorLog& log,
+ int logfilesMaxAgeDays,
+ const std::set<AbstractPath>& logFilePathsToKeep,
+ const std::function<void(const std::wstring& msg)>& notifyStatus /*throw X*/)
+{
std::exception_ptr firstError;
try
{
- logFilePath = saveNewLogFile(summary, log, logFolderPath, notifyStatus); //throw FileError, X
+ saveNewLogFile(logFilePath, summary, log, notifyStatus); //throw FileError, X
}
catch (const FileError&) { if (!firstError) firstError = std::current_exception(); };
try
{
- limitLogfileCount(logFolderPath, logfilesMaxAgeDays, logFilePathsToKeep, notifyStatus); //throw FileError, X
+ const std::optional<AbstractPath> logFolderPath = AFS::getParentPath(logFilePath);
+ assert(logFolderPath);
+ if (logFolderPath) //else: logFilePath == device root; not possible with generateLogFilePath()
+ limitLogfileCount(*logFolderPath, logfilesMaxAgeDays, logFilePathsToKeep, notifyStatus); //throw FileError, X
}
catch (const FileError&) { if (!firstError) firstError = std::current_exception(); };
if (firstError) //late failure!
std::rethrow_exception(firstError);
+}
+
+
- return logFilePath;
+
+void fff::sendLogAsEmail(const Zstring& email, //throw FileError, X
+ const ProcessSummary& summary,
+ const ErrorLog& log,
+ const AbstractPath& logFilePath,
+ const std::function<void(const std::wstring& msg)>& notifyStatus /*throw X*/)
+{
+ try
+ {
+ throw SysError(_("Requires FreeFileSync Donation Edition"));
+ }
+ catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot send notification email to %x."), L"%x", utfTo<std::wstring>(email)), e.toString()); }
}
diff --git a/FreeFileSync/Source/base/log_file.h b/FreeFileSync/Source/base/log_file.h
index 52cc7780..3a3a481c 100644
--- a/FreeFileSync/Source/base/log_file.h
+++ b/FreeFileSync/Source/base/log_file.h
@@ -19,12 +19,20 @@ namespace fff
Zstring getDefaultLogFolderPath();
-AbstractPath saveLogFile(const ProcessSummary& summary, //throw FileError
- const zen::ErrorLog& log,
- const Zstring& altLogFolderPathPhrase, //optional
- int logfilesMaxAgeDays,
- const std::set<AbstractPath>& logFilePathsToKeep,
- const std::function<void(const std::wstring& msg)>& notifyStatus /*throw X*/);
+AbstractPath generateLogFilePath(const ProcessSummary& summary, const Zstring& altLogFolderPathPhrase /*optional*/);
+
+void saveLogFile(const AbstractPath& logFilePath, //throw FileError, X
+ const ProcessSummary& summary,
+ const zen::ErrorLog& log,
+ int logfilesMaxAgeDays,
+ const std::set<AbstractPath>& logFilePathsToKeep,
+ const std::function<void(const std::wstring& msg)>& notifyStatus /*throw X*/);
+
+void sendLogAsEmail(const Zstring& email, //throw FileError, X
+ const ProcessSummary& summary,
+ const zen::ErrorLog& log,
+ const AbstractPath& logFilePath,
+ const std::function<void(const std::wstring& msg)>& notifyStatus /*throw X*/);
}
#endif //GENERATE_LOGFILE_H_931726432167489732164
diff --git a/FreeFileSync/Source/base/return_codes.h b/FreeFileSync/Source/base/return_codes.h
index a275a1a8..0fe72022 100644
--- a/FreeFileSync/Source/base/return_codes.h
+++ b/FreeFileSync/Source/base/return_codes.h
@@ -59,9 +59,9 @@ FfsReturnCode mapToReturnCode(SyncResult syncStatus)
inline
-std::wstring getFinalStatusLabel(SyncResult finalStatus)
+std::wstring getResultsStatusLabel(SyncResult resultStatus)
{
- switch (finalStatus)
+ switch (resultStatus)
{
case SyncResult::finishedSuccess:
return _("Completed successfully");
diff --git a/FreeFileSync/Source/base/status_handler.h b/FreeFileSync/Source/base/status_handler.h
index e91a2d1a..339f9ebe 100644
--- a/FreeFileSync/Source/base/status_handler.h
+++ b/FreeFileSync/Source/base/status_handler.h
@@ -72,8 +72,8 @@ struct Statistics
struct ProcessSummary
{
std::chrono::system_clock::time_point startTime;
- SyncResult finalStatus = SyncResult::aborted;
- std::wstring jobName; //may be empty
+ SyncResult resultStatus = SyncResult::aborted;
+ std::vector<std::wstring> jobNames; //may be empty
ProgressStats statsProcessed;
ProgressStats statsTotal;
std::chrono::milliseconds totalTime{};
diff --git a/FreeFileSync/Source/base/structures.cpp b/FreeFileSync/Source/base/structures.cpp
index 18448ece..cd275ac5 100644
--- a/FreeFileSync/Source/base/structures.cpp
+++ b/FreeFileSync/Source/base/structures.cpp
@@ -594,7 +594,9 @@ MainConfiguration fff::merge(const std::vector<MainConfiguration>& mainCfgs)
break;
}
- //cfgOut.postSyncCommand = mainCfgs[0].postSyncCommand; -> better leave at default ... !?
- //cfgOut.postSyncCondition = mainCfgs[0].postSyncCondition; ->
+ //cfgOut.postSyncCommand = -> better leave at default ... !?
+ //cfgOut.postSyncCondition = ->
+ //cfgOut.emailNotifyAddress = -> better leave at default ... !?
+ //cfgOut.emailNotifyCondition = ->
return cfgOut;
}
diff --git a/FreeFileSync/Source/base/structures.h b/FreeFileSync/Source/base/structures.h
index 56c4cbaf..5ee9708a 100644
--- a/FreeFileSync/Source/base/structures.h
+++ b/FreeFileSync/Source/base/structures.h
@@ -371,7 +371,7 @@ struct LocalPairConfig //enhanced folder pairs with (optional) alternate configu
std::optional<CompConfig> localCmpCfg;
std::optional<SyncConfig> localSyncCfg;
- FilterConfig localFilter;
+ FilterConfig localFilter;
};
@@ -387,6 +387,14 @@ bool operator==(const LocalPairConfig& lhs, const LocalPairConfig& rhs)
inline bool operator!=(const LocalPairConfig& lhs, const LocalPairConfig& rhs) { return !(lhs == rhs); }
+enum class ResultsNotification
+{
+ always,
+ errorWarning,
+ errorOnly,
+};
+
+
enum class PostSyncCondition
{
COMPLETION,
@@ -410,10 +418,13 @@ struct MainConfiguration
size_t automaticRetryCount = 0;
std::chrono::seconds automaticRetryDelay{5};
- Zstring altLogFolderPathPhrase; //fill to use different log file folder (other than the default %appdata%\FreeFileSync\Logs)
-
Zstring postSyncCommand; //user-defined command line
PostSyncCondition postSyncCondition = PostSyncCondition::COMPLETION;
+
+ Zstring altLogFolderPathPhrase; //fill to use different log file folder (other than the default %appdata%\FreeFileSync\Logs)
+
+ Zstring emailNotifyAddress; //optional
+ ResultsNotification emailNotifyCondition = ResultsNotification::always;
};
std::wstring getCompVariantName(const MainConfiguration& mainCfg);
@@ -427,18 +438,20 @@ void setDeviceParallelOps( std::map<AfsDevice, size_t>& deviceParallelOps
inline
bool operator==(const MainConfiguration& lhs, const MainConfiguration& rhs)
{
- return lhs.cmpCfg == rhs.cmpCfg &&
- lhs.syncCfg == rhs.syncCfg &&
- lhs.globalFilter == rhs.globalFilter &&
- lhs.firstPair == rhs.firstPair &&
- lhs.additionalPairs == rhs.additionalPairs &&
- lhs.deviceParallelOps == rhs.deviceParallelOps &&
- lhs.ignoreErrors == rhs.ignoreErrors &&
- lhs.automaticRetryCount == rhs.automaticRetryCount &&
- lhs.automaticRetryDelay == rhs.automaticRetryDelay &&
+ return lhs.cmpCfg == rhs.cmpCfg &&
+ lhs.syncCfg == rhs.syncCfg &&
+ lhs.globalFilter == rhs.globalFilter &&
+ lhs.firstPair == rhs.firstPair &&
+ lhs.additionalPairs == rhs.additionalPairs &&
+ lhs.deviceParallelOps == rhs.deviceParallelOps &&
+ lhs.ignoreErrors == rhs.ignoreErrors &&
+ lhs.automaticRetryCount == rhs.automaticRetryCount &&
+ lhs.automaticRetryDelay == rhs.automaticRetryDelay &&
+ lhs.postSyncCommand == rhs.postSyncCommand &&
+ lhs.postSyncCondition == rhs.postSyncCondition &&
lhs.altLogFolderPathPhrase == rhs.altLogFolderPathPhrase &&
- lhs.postSyncCommand == rhs.postSyncCommand &&
- lhs.postSyncCondition == rhs.postSyncCondition;
+ lhs.emailNotifyAddress == rhs.emailNotifyAddress &&
+ lhs.emailNotifyCondition == rhs.emailNotifyCondition;
}
diff --git a/FreeFileSync/Source/base/synchronization.cpp b/FreeFileSync/Source/base/synchronization.cpp
index 3533573b..097e3cf0 100644
--- a/FreeFileSync/Source/base/synchronization.cpp
+++ b/FreeFileSync/Source/base/synchronization.cpp
@@ -2083,7 +2083,7 @@ bool createBaseFolder(BaseFolderPair& baseFolder, bool copyFilePermissions, Phas
{
if (baseFolder.isAvailable<sideSrc>()) //copy file permissions
{
- if (std::optional<AbstractPath> parentPath = AFS::getParentPath(baseFolderPath))
+ if (const std::optional<AbstractPath> parentPath = AFS::getParentPath(baseFolderPath))
AFS::createFolderIfMissingRecursion(*parentPath); //throw FileError
AFS::copyNewFolder(baseFolder.getAbstractPath<sideSrc>(), baseFolderPath, copyFilePermissions); //throw FileError
diff --git a/FreeFileSync/Source/base/versioning.cpp b/FreeFileSync/Source/base/versioning.cpp
index 13125c74..c2b95f8d 100644
--- a/FreeFileSync/Source/base/versioning.cpp
+++ b/FreeFileSync/Source/base/versioning.cpp
@@ -551,7 +551,7 @@ void fff::applyVersioningLimit(const std::set<VersioningLimitFolder>& folderLimi
}, acb);
if (errMsg.empty())
- if (std::optional<AbstractPath> parentPath = AFS::getParentPath(folderPath))
+ if (const std::optional<AbstractPath> parentPath = AFS::getParentPath(folderPath))
{
bool deleteParent = false;
folderItemCountShared.access([&](auto& folderItemCount2) { deleteParent = --folderItemCount2[*parentPath] == 0; });
@@ -582,7 +582,7 @@ void fff::applyVersioningLimit(const std::set<VersioningLimitFolder>& folderLimi
}, ctx.acb);
if (errMsg.empty())
- if (std::optional<AbstractPath> parentPath = AFS::getParentPath(ctx.itemPath))
+ if (const std::optional<AbstractPath> parentPath = AFS::getParentPath(ctx.itemPath))
{
bool deleteParent = false;
folderItemCountShared.access([&](auto& folderItemCount2) { deleteParent = --folderItemCount2[*parentPath] == 0; });
diff --git a/FreeFileSync/Source/ui/batch_config.cpp b/FreeFileSync/Source/ui/batch_config.cpp
index 8240eed9..b158a8ca 100644
--- a/FreeFileSync/Source/ui/batch_config.cpp
+++ b/FreeFileSync/Source/ui/batch_config.cpp
@@ -97,12 +97,12 @@ void BatchDialog::updateGui() //re-evaluate gui after config changes
{
const BatchDialogConfig dlgCfg = getConfig(); //resolve parameter ownership: some on GUI controls, others member variables
- m_bitmapIgnoreErrors->SetBitmap(dlgCfg.ignoreErrors ? getResourceImage(L"error_ignore_active") : greyScale(getResourceImage(L"error_ignore_inactive")));
+ m_bitmapIgnoreErrors->SetBitmap(greyScaleIfDisabled(getResourceImage(L"error_ignore_active"), dlgCfg.ignoreErrors));
m_radioBtnErrorDialogShow ->Enable(!dlgCfg.ignoreErrors);
m_radioBtnErrorDialogCancel->Enable(!dlgCfg.ignoreErrors);
- m_bitmapMinimizeToTray->SetBitmap(dlgCfg.batchExCfg.runMinimized ? getResourceImage(L"minimize_to_tray") : greyScale(getResourceImage(L"minimize_to_tray")));
+ m_bitmapMinimizeToTray->SetBitmap(greyScaleIfDisabled(getResourceImage(L"minimize_to_tray"), dlgCfg.batchExCfg.runMinimized));
}
diff --git a/FreeFileSync/Source/ui/batch_status_handler.cpp b/FreeFileSync/Source/ui/batch_status_handler.cpp
index 37a00989..28f1833c 100644
--- a/FreeFileSync/Source/ui/batch_status_handler.cpp
+++ b/FreeFileSync/Source/ui/batch_status_handler.cpp
@@ -26,14 +26,12 @@ BatchStatusHandler::BatchStatusHandler(bool showProgress,
BatchErrorHandling batchErrorHandling,
size_t automaticRetryCount,
std::chrono::seconds automaticRetryDelay,
- const Zstring& postSyncCommand,
- PostSyncCondition postSyncCondition,
PostSyncAction postSyncAction) :
batchErrorHandling_(batchErrorHandling),
automaticRetryCount_(automaticRetryCount),
automaticRetryDelay_(automaticRetryDelay),
progressDlg_(SyncProgressDialog::create([this] { userRequestAbort(); }, *this, nullptr /*parentWindow*/, showProgress, autoCloseDialog,
-startTime, jobName, soundFileSyncComplete, ignoreErrors, automaticRetryCount, [&]
+startTime, { jobName }, soundFileSyncComplete, ignoreErrors, automaticRetryCount, [&]
{
switch (postSyncAction)
{
@@ -48,9 +46,7 @@ startTime, jobName, soundFileSyncComplete, ignoreErrors, automaticRetryCount, [&
return PostSyncAction2::none;
}())),
jobName_(jobName),
- startTime_(startTime),
- postSyncCommand_(postSyncCommand),
- postSyncCondition_(postSyncCondition)
+ startTime_(startTime)
{
//ATTENTION: "progressDlg_" is an unmanaged resource!!! However, at this point we already consider construction complete! =>
//ZEN_ON_SCOPE_FAIL( cleanup(); ); //destructor call would lead to member double clean-up!!!
@@ -59,19 +55,21 @@ jobName_(jobName),
BatchStatusHandler::~BatchStatusHandler()
{
- if (progressDlg_) //reportFinalStatus() was not called!
+ if (progressDlg_) //reportResults() was not called!
std::abort();
}
-BatchStatusHandler::Result BatchStatusHandler::reportFinalStatus(const Zstring& altLogFolderPathPhrase, int logfilesMaxAgeDays, const std::set<AbstractPath>& logFilePathsToKeep) //noexcept!!
+BatchStatusHandler::Result BatchStatusHandler::reportResults(const Zstring& postSyncCommand, PostSyncCondition postSyncCondition,
+ const Zstring& altLogFolderPathPhrase, int logfilesMaxAgeDays, const std::set<AbstractPath>& logFilePathsToKeep,
+ const Zstring& emailNotifyAddress, ResultsNotification emailNotifyCondition) //noexcept!!
{
const auto totalTime = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now() - startTime_);
progressDlg_->timerSetStatus(false /*active*/); //keep correct summary window stats considering count down timer, system sleep
//determine post-sync status irrespective of further errors during tear-down
- const SyncResult finalStatus = [&]
+ const SyncResult resultStatus = [&]
{
if (getAbortStatus())
{
@@ -88,98 +86,97 @@ BatchStatusHandler::Result BatchStatusHandler::reportFinalStatus(const Zstring&
return SyncResult::finishedSuccess;
}();
- assert(finalStatus == SyncResult::aborted || currentPhase() == ProcessPhase::synchronizing);
+ assert(resultStatus == SyncResult::aborted || currentPhase() == ProcessPhase::synchronizing);
const ProcessSummary summary
{
- startTime_, finalStatus, jobName_,
+ startTime_, resultStatus, { jobName_ },
getStatsCurrent(),
getStatsTotal (),
totalTime
};
- //post sync command
- Zstring commandLine = [&]
+ const AbstractPath logFilePath = generateLogFilePath(summary, altLogFolderPathPhrase);
+ //e.g. %AppData%\FreeFileSync\Logs\Backup FreeFileSync 2013-09-15 015052.123 [Error].log
+
+ if (const Zstring cmdLine = trimCpy(postSyncCommand);
+ !cmdLine.empty())
{
if (getAbortStatus() && *getAbortStatus() == AbortTrigger::user)
; //user cancelled => don't run post sync command!
- else
- switch (postSyncCondition_)
+ else if (postSyncCondition == PostSyncCondition::COMPLETION ||
+ (postSyncCondition == PostSyncCondition::ERRORS) == (resultStatus == SyncResult::aborted ||
+ resultStatus == SyncResult::finishedError))
+ try
{
- case PostSyncCondition::COMPLETION:
- return postSyncCommand_;
- case PostSyncCondition::ERRORS:
- if (finalStatus == SyncResult::aborted ||
- finalStatus == SyncResult::finishedError)
- return postSyncCommand_;
- break;
- case PostSyncCondition::SUCCESS:
- if (finalStatus == SyncResult::finishedWarning ||
- finalStatus == SyncResult::finishedSuccess)
- return postSyncCommand_;
- break;
+ ////----------------------------------------------------------------------
+ //::wxSetEnv(L"logfile_path", AFS::getDisplayPath(logFilePath));
+ ////----------------------------------------------------------------------
+ const Zstring cmdLineExp = expandMacros(cmdLine);
+ const int exitCode = shellExecute(cmdLineExp, ExecutionType::sync, false /*hideConsole*/); //throw FileError
+ errorLog_.logMsg(_("Executing command:") + L" " + utfTo<std::wstring>(cmdLineExp) + L" [" + replaceCpy(_("Exit Code %x"), L"%x", numberTo<std::wstring>(exitCode)) + L']',
+ exitCode == 0 ? MSG_TYPE_INFO : MSG_TYPE_ERROR);
}
- return Zstring();
- }();
- trim(commandLine);
+ catch (const FileError& e) { errorLog_.logMsg(e.toString(), MSG_TYPE_ERROR); }
+ }
+
+ //---------------------------- save log file ------------------------------
+ auto notifyStatusNoThrow = [&](const std::wstring& msg) { try { updateStatus(msg); /*throw AbortProcess*/ } catch (AbortProcess&) {} };
- if (!commandLine.empty())
- errorLog_.logMsg(_("Executing command:") + L" " + utfTo<std::wstring>(commandLine), MSG_TYPE_INFO);
+ if (const Zstring notifyEmail = trimCpy(emailNotifyAddress);
+ !notifyEmail.empty())
+ {
+ if (getAbortStatus() && *getAbortStatus() == AbortTrigger::user)
+ ; //user cancelled => don't send email notification!
+ else if (emailNotifyCondition == ResultsNotification::always ||
+ (emailNotifyCondition == ResultsNotification::errorWarning && (resultStatus == SyncResult::aborted ||
+ resultStatus == SyncResult::finishedError ||
+ resultStatus == SyncResult::finishedWarning)) ||
+ (emailNotifyCondition == ResultsNotification::errorOnly && (resultStatus == SyncResult::aborted ||
+ resultStatus == SyncResult::finishedError)))
+ try
+ {
+ sendLogAsEmail(notifyEmail, summary, errorLog_, logFilePath, notifyStatusNoThrow); //throw FileError
+ }
+ catch (const FileError& e) { errorLog_.logMsg(e.toString(), MSG_TYPE_ERROR); }
+ }
- //----------------- always save log under %appdata%\FreeFileSync\Logs ------------------------
- //create not before destruction: 1. avoid issues with FFS trying to sync open log file 2. simplify transactional retry on failure 3. include status in log file name without rename
- // 4. failure to write to particular stream must not be retried!
- AbstractPath logFilePath = getNullPath();
- try
+ try //create not before destruction: 1. avoid issues with FFS trying to sync open log file 2. include status in log file name without extra rename
{
//do NOT use tryReportingError()! saving log files should not be cancellable!
- auto notifyStatusNoThrow = [&](const std::wstring& msg) { try { updateStatus(msg); /*throw AbortProcess*/ } catch (...) {} };
- logFilePath = saveLogFile(summary, errorLog_, altLogFolderPathPhrase, logfilesMaxAgeDays, logFilePathsToKeep, notifyStatusNoThrow); //throw FileError
+ saveLogFile(logFilePath, summary, errorLog_, logfilesMaxAgeDays, logFilePathsToKeep, notifyStatusNoThrow); //throw FileError
}
catch (const FileError& e) { errorLog_.logMsg(e.toString(), MSG_TYPE_ERROR); }
- //execute post sync command *after* writing log files, so that user can refer to the log via the command!
- if (!commandLine.empty())
- try
+ //----------------- post sync action ------------------------
+ auto mayRunAfterCountDown = [&](const std::wstring& operationName)
+ {
+ auto notifyStatusThrowOnCancel = [&](const std::wstring& msg)
{
- //----------------------------------------------------------------------
- ::wxSetEnv(L"logfile_path", AFS::getDisplayPath(logFilePath));
- //----------------------------------------------------------------------
- //use ExecutionType::ASYNC until there is a reason not to: https://freefilesync.org/forum/viewtopic.php?t=31
- shellExecute(expandMacros(commandLine), ExecutionType::ASYNC, false/*hideConsole*/); //throw FileError
- }
- catch (const FileError& e) { errorLog_.logMsg(e.toString(), MSG_TYPE_ERROR); }
+ try { updateStatus(msg); /*throw AbortProcess*/ }
+ catch (AbortProcess&)
+ {
+ if (getAbortStatus() && *getAbortStatus() == AbortTrigger::user)
+ throw;
+ }
+ };
+
+ if (progressDlg_->getWindowIfVisible())
+ try
+ {
+ delayAndCountDown(operationName, std::chrono::seconds(5), notifyStatusThrowOnCancel); //throw AbortProcess
+ }
+ catch (AbortProcess&) { return false; }
+
+ return true;
+ };
- //post sync action
bool autoClose = false;
FinalRequest finalRequest = FinalRequest::none;
if (getAbortStatus() && *getAbortStatus() == AbortTrigger::user)
- ; //user cancelled => don't run post sync command!
+ ; //user cancelled => don't run post sync action!
else
- {
- auto mayRunAfterCountDown = [&](const std::wstring& operationName)
- {
- auto notifyStatusThrowOnCancel = [&](const std::wstring& msg)
- {
- try { updateStatus(msg); /*throw AbortProcess*/ }
- catch (...)
- {
- if (getAbortStatus() && *getAbortStatus() == AbortTrigger::user)
- throw;
- }
- };
-
- if (progressDlg_->getWindowIfVisible())
- try
- {
- delayAndCountDown(operationName, std::chrono::seconds(5), notifyStatusThrowOnCancel); //throw AbortProcess
- }
- catch (...) { return false; }
-
- return true;
- };
-
switch (progressDlg_->getOptionPostSyncAction())
{
case PostSyncAction2::none:
@@ -205,7 +202,6 @@ BatchStatusHandler::Result BatchStatusHandler::reportFinalStatus(const Zstring&
}
break;
}
- }
if (switchToGuiRequested_) //-> avoid recursive yield() calls, thous switch not before ending batch mode
{
autoClose = true;
@@ -216,10 +212,10 @@ BatchStatusHandler::Result BatchStatusHandler::reportFinalStatus(const Zstring&
progressDlg_->destroy(autoClose,
true /*restoreParentFrame: n/a here*/,
- finalStatus, errorLogFinal);
+ resultStatus, errorLogFinal);
progressDlg_ = nullptr;
- return { finalStatus, finalRequest, logFilePath };
+ return { resultStatus, finalRequest, logFilePath };
}
diff --git a/FreeFileSync/Source/ui/batch_status_handler.h b/FreeFileSync/Source/ui/batch_status_handler.h
index 357a9d48..b56a4aed 100644
--- a/FreeFileSync/Source/ui/batch_status_handler.h
+++ b/FreeFileSync/Source/ui/batch_status_handler.h
@@ -29,8 +29,6 @@ public:
BatchErrorHandling batchErrorHandling,
size_t automaticRetryCount,
std::chrono::seconds automaticRetryDelay,
- const Zstring& postSyncCommand,
- PostSyncCondition postSyncCondition,
PostSyncAction postSyncAction); //noexcept!!
~BatchStatusHandler();
@@ -51,11 +49,13 @@ public:
};
struct Result
{
- SyncResult finalStatus;
+ SyncResult resultStatus;
FinalRequest finalRequest;
AbstractPath logFilePath;
};
- Result reportFinalStatus(const Zstring& altLogFolderPathPhrase, int logfilesMaxAgeDays, const std::set<AbstractPath>& logFilePathsToKeep); //noexcept!!
+ Result reportResults(const Zstring& postSyncCommand, PostSyncCondition postSyncCondition,
+ const Zstring& altLogFolderPathPhrase, int logfilesMaxAgeDays, const std::set<AbstractPath>& logFilePathsToKeep,
+ const Zstring& emailNotifyAddress, ResultsNotification emailNotifyCondition); //noexcept!!
private:
bool switchToGuiRequested_ = false;
@@ -66,8 +66,6 @@ private:
SyncProgressDialog* progressDlg_; //managed to have the same lifetime as this handler!
const std::wstring jobName_;
const std::chrono::system_clock::time_point startTime_;
- const Zstring postSyncCommand_;
- const PostSyncCondition postSyncCondition_;
};
}
diff --git a/FreeFileSync/Source/ui/cfg_grid.cpp b/FreeFileSync/Source/ui/cfg_grid.cpp
index b15a3302..7ae27b42 100644
--- a/FreeFileSync/Source/ui/cfg_grid.cpp
+++ b/FreeFileSync/Source/ui/cfg_grid.cpp
@@ -327,7 +327,7 @@ private:
case ColumnTypeCfg::lastLog:
if (!item->isLastRunCfg &&
!AFS::isNullPath(item->cfgItem.logFilePath))
- return getFinalStatusLabel(item->cfgItem.logResult);
+ return getResultsStatusLabel(item->cfgItem.logResult);
break;
}
return std::wstring();
@@ -433,7 +433,7 @@ private:
switch (item->cfgItem.logResult)
{
case SyncResult::finishedSuccess:
- return getResourceImage(L"msg_finished_sicon");
+ return getResourceImage(L"msg_success_sicon");
case SyncResult::finishedWarning:
return getResourceImage(L"msg_warning_sicon");
case SyncResult::finishedError:
@@ -570,7 +570,7 @@ private:
if (!item->isLastRunCfg &&
!AFS::isNullPath(item->cfgItem.logFilePath))
- return getFinalStatusLabel(item->cfgItem.logResult) + SPACED_DASH + AFS::getDisplayPath(item->cfgItem.logFilePath);
+ return getResultsStatusLabel(item->cfgItem.logResult) + SPACED_DASH + AFS::getDisplayPath(item->cfgItem.logFilePath);
break;
}
return std::wstring();
diff --git a/FreeFileSync/Source/ui/command_box.cpp b/FreeFileSync/Source/ui/command_box.cpp
index 7dc2607d..91061f08 100644
--- a/FreeFileSync/Source/ui/command_box.cpp
+++ b/FreeFileSync/Source/ui/command_box.cpp
@@ -22,15 +22,6 @@ inline
wxString getSeparationLine() { return std::wstring(50, EM_DASH); } //no space between dashes!
-std::vector<std::pair<wxString, Zstring>> getDefaultCommands() //(description/command) pairs
-{
- return
- {
- //{_("System: Sleep"), Zstr("rundll32.exe powrprof.dll,SetSuspendState Sleep")},
- };
-}
-
-
const wxEventType EVENT_VALIDATE_USER_SELECTION = wxNewEventType();
}
@@ -45,8 +36,7 @@ CommandBox::CommandBox(wxWindow* parent,
long style,
const wxValidator& validator,
const wxString& name) :
- wxComboBox(parent, id, value, pos, size, n, choices, style, validator, name),
- defaultCommands_(getDefaultCommands())
+ wxComboBox(parent, id, value, pos, size, n, choices, style, validator, name)
{
//####################################
/*#*/ SetMinSize({fastFromDIP(150), -1}); //# workaround yet another wxWidgets bug: default minimum size is much too large for a wxComboBox
diff --git a/FreeFileSync/Source/ui/command_box.h b/FreeFileSync/Source/ui/command_box.h
index 5cc152fb..99715f84 100644
--- a/FreeFileSync/Source/ui/command_box.h
+++ b/FreeFileSync/Source/ui/command_box.h
@@ -53,7 +53,7 @@ private:
std::vector<Zstring> history_;
size_t historyMax_ = 0;
- const std::vector<std::pair<wxString, Zstring>> defaultCommands_;
+ const std::vector<std::pair<wxString, Zstring>> defaultCommands_; //(description/command) pairs
};
}
diff --git a/FreeFileSync/Source/ui/file_grid.cpp b/FreeFileSync/Source/ui/file_grid.cpp
index bc7b7037..12ff49f8 100644
--- a/FreeFileSync/Source/ui/file_grid.cpp
+++ b/FreeFileSync/Source/ui/file_grid.cpp
@@ -1096,15 +1096,11 @@ private:
break;
case ColumnTypeCenter::CMP_CATEGORY:
- colIcon = getResourceImage(L"compare_sicon");
- if (highlightSyncAction_)
- colIcon = greyScale(colIcon);
+ colIcon = greyScaleIfDisabled(getResourceImage(L"compare_sicon"), !highlightSyncAction_);
break;
case ColumnTypeCenter::SYNC_ACTION:
- colIcon = getResourceImage(L"file_sync_sicon");
- if (!highlightSyncAction_)
- colIcon = greyScale(colIcon);
+ colIcon = greyScaleIfDisabled(getResourceImage(L"file_sync_sicon"), highlightSyncAction_);
break;
}
diff --git a/FreeFileSync/Source/ui/folder_history_box.cpp b/FreeFileSync/Source/ui/folder_history_box.cpp
index 8109757a..e9249a6c 100644
--- a/FreeFileSync/Source/ui/folder_history_box.cpp
+++ b/FreeFileSync/Source/ui/folder_history_box.cpp
@@ -74,7 +74,7 @@ void FolderHistoryBox::setValueAndUpdateList(const wxString& folderPathPhrase)
std::sort(tmp.begin(), tmp.end(), LessNaturalSort() /*even on Linux*/);
if (!dirList.empty() && !tmp.empty())
- dirList.push_back(FolderHistory::separationLine());
+ dirList.push_back(HistoryList::separationLine());
for (const Zstring& str : tmp)
dirList.push_back(utfTo<wxString>(str));
@@ -88,7 +88,7 @@ void FolderHistoryBox::setValueAndUpdateList(const wxString& folderPathPhrase)
if (std::find(dirList.begin(), dirList.end(), folderPathPhrase) == dirList.end())
dirList.insert(dirList.begin(), folderPathPhrase);
- warn_static("do something about wxComboBox::Append() perf")
+ warn_static("do something about wxComboBox::Append() perf + also apply for CommandBox::setValueAndUpdateList()")
//this->Clear(); -> NO! emits yet another wxEVT_COMMAND_TEXT_UPDATED!!!
wxItemContainer::Clear(); //suffices to clear the selection items only!
diff --git a/FreeFileSync/Source/ui/folder_history_box.h b/FreeFileSync/Source/ui/folder_history_box.h
index 9db2f062..f2e0e076 100644
--- a/FreeFileSync/Source/ui/folder_history_box.h
+++ b/FreeFileSync/Source/ui/folder_history_box.h
@@ -16,18 +16,12 @@
namespace fff
{
-class FolderHistory //combobox with history function + functionality to delete items (DEL)
+class HistoryList
{
public:
- FolderHistory() {}
-
- FolderHistory(const std::vector<Zstring>& folderPathPhrases, size_t maxSize) :
+ HistoryList(const std::vector<Zstring>& folderPathPhrases, size_t maxSize) :
maxSize_(maxSize),
- folderPathPhrases_(folderPathPhrases)
- {
- if (folderPathPhrases_.size() > maxSize_) //keep maximal size of history list
- folderPathPhrases_.resize(maxSize_);
- }
+ folderPathPhrases_(folderPathPhrases) { truncate(); }
const std::vector<Zstring>& getList() const { return folderPathPhrases_; }
@@ -44,19 +38,24 @@ public:
std::erase_if(folderPathPhrases_, [&](const Zstring& item) { return equalNoCase(item, nameTmp); });
folderPathPhrases_.insert(folderPathPhrases_.begin(), nameTmp);
-
- if (folderPathPhrases_.size() > maxSize_) //keep maximal size of history list
- folderPathPhrases_.resize(maxSize_);
+ truncate();
}
void delItem(const Zstring& folderPathPhrase) { std::erase_if(folderPathPhrases_, [&](const Zstring& item) { return equalNoCase(item, folderPathPhrase); }); }
private:
- size_t maxSize_ = 0;
+ void truncate()
+ {
+ if (folderPathPhrases_.size() > maxSize_) //keep maximal size of history list
+ folderPathPhrases_.resize(maxSize_);
+ }
+
+ const size_t maxSize_ = 0;
std::vector<Zstring> folderPathPhrases_;
};
+//combobox with history function + functionality to delete items (DEL)
class FolderHistoryBox : public wxComboBox
{
public:
@@ -71,7 +70,8 @@ public:
const wxValidator& validator = wxDefaultValidator,
const wxString& name = wxComboBoxNameStr);
- void init(zen::SharedRef<FolderHistory>& sharedHistory) { sharedHistory_ = sharedHistory.ptr(); }
+ void setHistory(std::shared_ptr<HistoryList> sharedHistory) { sharedHistory_ = std::move(sharedHistory); }
+ std::shared_ptr<HistoryList> getHistory() { return sharedHistory_; }
void setValue(const wxString& folderPathPhrase)
{
@@ -85,7 +85,7 @@ private:
void OnRequireHistoryUpdate(wxEvent& event);
void setValueAndUpdateList(const wxString& folderPathPhrase);
- std::shared_ptr<FolderHistory> sharedHistory_;
+ std::shared_ptr<HistoryList> sharedHistory_;
};
}
diff --git a/FreeFileSync/Source/ui/folder_pair.h b/FreeFileSync/Source/ui/folder_pair.h
index 7a90521c..419fc751 100644
--- a/FreeFileSync/Source/ui/folder_pair.h
+++ b/FreeFileSync/Source/ui/folder_pair.h
@@ -57,38 +57,20 @@ private:
{
using namespace zen;
- if (localCmpCfg_)
- {
- setImage(*basicPanel_.m_bpButtonLocalCompCfg, imgCmp_);
- basicPanel_.m_bpButtonLocalCompCfg->SetToolTip(_("Local comparison settings") + L" (" + getVariantName(localCmpCfg_->compareVar) + L")");
- }
- else
- {
- setImage(*basicPanel_.m_bpButtonLocalCompCfg, greyScale(imgCmp_));
- basicPanel_.m_bpButtonLocalCompCfg->SetToolTip(_("Local comparison settings"));
- }
-
- if (localSyncCfg_)
- {
- setImage(*basicPanel_.m_bpButtonLocalSyncCfg, imgSync_);
- basicPanel_.m_bpButtonLocalSyncCfg->SetToolTip(_("Local synchronization settings") + L" (" + getVariantName(localSyncCfg_->directionCfg.var) + L")");
- }
- else
- {
- setImage(*basicPanel_.m_bpButtonLocalSyncCfg, greyScale(imgSync_));
- basicPanel_.m_bpButtonLocalSyncCfg->SetToolTip(_("Local synchronization settings"));
- }
-
- if (!isNullFilter(localFilter_))
- {
- setImage(*basicPanel_.m_bpButtonLocalFilter, imgFilter_);
- basicPanel_.m_bpButtonLocalFilter->SetToolTip(_("Local filter") + L" (" + _("Active") + L")");
- }
- else
- {
- setImage(*basicPanel_.m_bpButtonLocalFilter, greyScale(imgFilter_));
- basicPanel_.m_bpButtonLocalFilter->SetToolTip(_("Local filter") + L" (" + _("None") + L")");
- }
+ setImage(*basicPanel_.m_bpButtonLocalCompCfg, greyScaleIfDisabled(imgCmp_, !!localCmpCfg_));
+ basicPanel_.m_bpButtonLocalCompCfg->SetToolTip(localCmpCfg_ ?
+ _("Local comparison settings") + L" (" + getVariantName(localCmpCfg_->compareVar) + L")" :
+ _("Local comparison settings"));
+
+ setImage(*basicPanel_.m_bpButtonLocalSyncCfg, greyScaleIfDisabled(imgSync_, !!localSyncCfg_));
+ basicPanel_.m_bpButtonLocalSyncCfg->SetToolTip(localSyncCfg_ ?
+ _("Local synchronization settings") + L" (" + getVariantName(localSyncCfg_->directionCfg.var) + L")" :
+ _("Local synchronization settings"));
+
+ setImage(*basicPanel_.m_bpButtonLocalFilter, greyScaleIfDisabled(imgFilter_, !isNullFilter(localFilter_)));
+ basicPanel_.m_bpButtonLocalFilter->SetToolTip(!isNullFilter(localFilter_) ?
+ _("Local filter") + L" (" + _("Active") + L")" :
+ _("Local filter") + L" (" + _("None") + L")");
}
void OnLocalCompCfgContext(wxCommandEvent& event)
diff --git a/FreeFileSync/Source/ui/folder_selector.cpp b/FreeFileSync/Source/ui/folder_selector.cpp
index 379cf5b2..9afdfe3c 100644
--- a/FreeFileSync/Source/ui/folder_selector.cpp
+++ b/FreeFileSync/Source/ui/folder_selector.cpp
@@ -147,7 +147,7 @@ void FolderSelector::onItemPathDropped(FileDropEvent& event)
try
{
if (AFS::getItemType(itemPath) == AFS::ItemType::FILE) //throw FileError
- if (std::optional<AbstractPath> parentPath = AFS::getParentPath(itemPath))
+ if (const std::optional<AbstractPath> parentPath = AFS::getParentPath(itemPath))
return AFS::getInitPathPhrase(*parentPath);
}
catch (FileError&) {} //e.g. good for inactive mapped network shares, not so nice for C:\pagefile.sys
diff --git a/FreeFileSync/Source/ui/gui_generated.cpp b/FreeFileSync/Source/ui/gui_generated.cpp
index 7c9dbb81..80dd34d5 100644
--- a/FreeFileSync/Source/ui/gui_generated.cpp
+++ b/FreeFileSync/Source/ui/gui_generated.cpp
@@ -1413,7 +1413,7 @@ ConfigDlgGenerated::ConfigDlgGenerated( wxWindow* parent, wxWindowID id, const w
wxBoxSizer* bSizer176;
bSizer176 = new wxBoxSizer( wxVERTICAL );
- m_radioBtnSymlinksFollow = new wxRadioButton( m_panelComparisonSettings, wxID_ANY, _("&Follow"), wxDefaultPosition, wxDefaultSize, 0 );
+ m_radioBtnSymlinksFollow = new wxRadioButton( m_panelComparisonSettings, wxID_ANY, _("&Follow"), wxDefaultPosition, wxDefaultSize, wxRB_GROUP );
m_radioBtnSymlinksFollow->SetValue( true );
bSizer176->Add( m_radioBtnSymlinksFollow, 0, wxEXPAND|wxBOTTOM|wxRIGHT|wxLEFT, 5 );
@@ -1548,7 +1548,7 @@ ConfigDlgGenerated::ConfigDlgGenerated( wxWindow* parent, wxWindowID id, const w
m_staticTextPerfDeRequired = new wxStaticText( m_panelComparisonSettings, wxID_ANY, _("Requires FreeFileSync Donation Edition"), wxDefaultPosition, wxDefaultSize, 0 );
m_staticTextPerfDeRequired->Wrap( -1 );
- bSizerPerformance->Add( m_staticTextPerfDeRequired, 0, wxALL, 5 );
+ bSizerPerformance->Add( m_staticTextPerfDeRequired, 0, wxALL|wxALIGN_CENTER_HORIZONTAL, 5 );
m_staticlinePerfDeRequired = new wxStaticLine( m_panelComparisonSettings, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL );
bSizerPerformance->Add( m_staticlinePerfDeRequired, 0, wxEXPAND, 5 );
@@ -2257,6 +2257,64 @@ ConfigDlgGenerated::ConfigDlgGenerated( wxWindow* parent, wxWindowID id, const w
bSizerSyncMisc = new wxBoxSizer( wxHORIZONTAL );
+ wxBoxSizer* bSizer292;
+ bSizer292 = new wxBoxSizer( wxVERTICAL );
+
+ wxBoxSizer* bSizer287;
+ bSizer287 = new wxBoxSizer( wxHORIZONTAL );
+
+ wxBoxSizer* bSizer290;
+ bSizer290 = new wxBoxSizer( wxVERTICAL );
+
+ wxBoxSizer* bSizer291;
+ bSizer291 = new wxBoxSizer( wxHORIZONTAL );
+
+ m_bitmapEmail = new wxStaticBitmap( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 );
+ bSizer291->Add( m_bitmapEmail, 0, wxRIGHT|wxALIGN_CENTER_VERTICAL, 5 );
+
+ m_checkBoxSendEmail = new wxCheckBox( m_panelSyncSettings, wxID_ANY, _("Send email notification:"), wxDefaultPosition, wxDefaultSize, 0 );
+ bSizer291->Add( m_checkBoxSendEmail, 0, wxALIGN_CENTER_VERTICAL, 5 );
+
+
+ bSizer290->Add( bSizer291, 0, 0, 5 );
+
+ m_comboBoxEmail = new fff::CommandBox( m_panelSyncSettings, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0, NULL, 0 );
+ bSizer290->Add( m_comboBoxEmail, 0, wxEXPAND|wxTOP, 5 );
+
+
+ bSizer287->Add( bSizer290, 1, 0, 5 );
+
+ wxBoxSizer* bSizer289;
+ bSizer289 = new wxBoxSizer( wxVERTICAL );
+
+ m_bpButtonEmailAlways = new wxBitmapButton( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, wxBU_AUTODRAW|0 );
+ bSizer289->Add( m_bpButtonEmailAlways, 0, wxLEFT, 5 );
+
+ m_bpButtonEmailErrorWarning = new wxBitmapButton( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, wxBU_AUTODRAW|0 );
+ bSizer289->Add( m_bpButtonEmailErrorWarning, 0, wxLEFT, 5 );
+
+ m_bpButtonEmailErrorOnly = new wxBitmapButton( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, wxBU_AUTODRAW|0 );
+ bSizer289->Add( m_bpButtonEmailErrorOnly, 0, wxLEFT, 5 );
+
+
+ bSizer287->Add( bSizer289, 0, 0, 5 );
+
+
+ bSizer292->Add( bSizer287, 0, wxEXPAND, 5 );
+
+ m_staticTextPerfDeRequired2 = new wxStaticText( m_panelSyncSettings, wxID_ANY, _("Requires FreeFileSync Donation Edition"), wxDefaultPosition, wxDefaultSize, 0 );
+ m_staticTextPerfDeRequired2->Wrap( -1 );
+ bSizer292->Add( m_staticTextPerfDeRequired2, 0, wxALIGN_CENTER_HORIZONTAL|wxTOP, 10 );
+
+
+ bSizerSyncMisc->Add( bSizer292, 0, wxEXPAND|wxALL, 10 );
+
+ m_staticline57 = new wxStaticLine( m_panelSyncSettings, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL );
+ bSizerSyncMisc->Add( m_staticline57, 0, wxEXPAND, 5 );
+
+ wxBoxSizer* bSizer293;
+ bSizer293 = new wxBoxSizer( wxVERTICAL );
+
wxBoxSizer* bSizer2372;
bSizer2372 = new wxBoxSizer( wxHORIZONTAL );
@@ -2272,8 +2330,9 @@ ConfigDlgGenerated::ConfigDlgGenerated( wxWindow* parent, wxWindowID id, const w
m_bitmapLogFile = new wxStaticBitmap( m_panelLogfile, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 );
bSizer279->Add( m_bitmapLogFile, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 );
- m_checkBoxSaveLog = new wxCheckBox( m_panelLogfile, wxID_ANY, _("&Override default log path:"), wxDefaultPosition, wxDefaultSize, 0 );
- bSizer279->Add( m_checkBoxSaveLog, 1, wxALIGN_CENTER_VERTICAL|wxALL, 5 );
+ m_checkBoxOverrideLogPath = new wxCheckBox( m_panelLogfile, wxID_ANY, _("&Override default log path:"), wxDefaultPosition, wxDefaultSize, 0 );
+ m_checkBoxOverrideLogPath->SetValue(true);
+ bSizer279->Add( m_checkBoxOverrideLogPath, 1, wxALIGN_CENTER_VERTICAL|wxALL, 5 );
m_buttonSelectLogFolder = new wxButton( m_panelLogfile, wxID_ANY, _("Browse"), wxDefaultPosition, wxDefaultSize, 0 );
m_buttonSelectLogFolder->SetToolTip( _("Select a folder") );
@@ -2298,37 +2357,31 @@ ConfigDlgGenerated::ConfigDlgGenerated( wxWindow* parent, wxWindowID id, const w
bSizer2372->Add( m_panelLogfile, 1, 0, 5 );
- bSizerSyncMisc->Add( bSizer2372, 1, wxALL, 10 );
+ bSizer293->Add( bSizer2372, 0, wxALL|wxEXPAND, 10 );
- m_staticline57 = new wxStaticLine( m_panelSyncSettings, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL );
- bSizerSyncMisc->Add( m_staticline57, 0, wxEXPAND, 5 );
+ m_staticline80 = new wxStaticLine( m_panelSyncSettings, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL );
+ bSizer293->Add( m_staticline80, 0, wxEXPAND, 5 );
wxBoxSizer* bSizer247;
- bSizer247 = new wxBoxSizer( wxVERTICAL );
-
- wxBoxSizer* bSizer251;
- bSizer251 = new wxBoxSizer( wxHORIZONTAL );
+ bSizer247 = new wxBoxSizer( wxHORIZONTAL );
m_staticTextPostSync = new wxStaticText( m_panelSyncSettings, wxID_ANY, _("Run a command:"), wxDefaultPosition, wxDefaultSize, 0 );
m_staticTextPostSync->Wrap( -1 );
- bSizer251->Add( m_staticTextPostSync, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 );
-
-
- bSizer251->Add( 0, 0, 1, 0, 5 );
+ bSizer247->Add( m_staticTextPostSync, 0, wxRIGHT|wxALIGN_CENTER_VERTICAL, 5 );
wxArrayString m_choicePostSyncConditionChoices;
m_choicePostSyncCondition = new wxChoice( m_panelSyncSettings, wxID_ANY, wxDefaultPosition, wxDefaultSize, m_choicePostSyncConditionChoices, 0 );
m_choicePostSyncCondition->SetSelection( 0 );
- bSizer251->Add( m_choicePostSyncCondition, 0, wxALIGN_CENTER_VERTICAL, 5 );
+ bSizer247->Add( m_choicePostSyncCondition, 0, wxALIGN_CENTER_VERTICAL, 5 );
+ m_comboBoxPostSyncCommand = new fff::CommandBox( m_panelSyncSettings, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0, NULL, 0 );
+ bSizer247->Add( m_comboBoxPostSyncCommand, 1, wxLEFT|wxALIGN_CENTER_VERTICAL, 5 );
- bSizer247->Add( bSizer251, 0, wxEXPAND, 5 );
- m_comboBoxPostSyncCommand = new fff::CommandBox( m_panelSyncSettings, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0, NULL, 0 );
- bSizer247->Add( m_comboBoxPostSyncCommand, 0, wxTOP|wxEXPAND, 5 );
+ bSizer293->Add( bSizer247, 0, wxALL|wxEXPAND, 10 );
- bSizerSyncMisc->Add( bSizer247, 0, wxALL, 10 );
+ bSizerSyncMisc->Add( bSizer293, 1, 0, 5 );
bSizer232->Add( bSizerSyncMisc, 1, wxEXPAND, 5 );
@@ -2421,7 +2474,11 @@ ConfigDlgGenerated::ConfigDlgGenerated( wxWindow* parent, wxWindowID id, const w
m_checkBoxVersionMaxDays->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::OnToggleVersioningLimit ), NULL, this );
m_checkBoxVersionCountMin->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::OnToggleVersioningLimit ), NULL, this );
m_checkBoxVersionCountMax->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::OnToggleVersioningLimit ), NULL, this );
- m_checkBoxSaveLog->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::OnToggleSaveLogfile ), NULL, this );
+ m_checkBoxSendEmail->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::OnToggleMiscOption ), NULL, this );
+ m_bpButtonEmailAlways->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::OnEmailAlways ), NULL, this );
+ m_bpButtonEmailErrorWarning->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::OnEmailErrorWarning ), NULL, this );
+ m_bpButtonEmailErrorOnly->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::OnEmailErrorOnly ), NULL, this );
+ m_checkBoxOverrideLogPath->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::OnToggleMiscOption ), NULL, this );
m_buttonOkay->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::OnOkay ), NULL, this );
m_buttonCancel->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::OnCancel ), NULL, this );
}
@@ -2738,23 +2795,37 @@ CloudSetupDlgGenerated::CloudSetupDlgGenerated( wxWindow* parent, wxWindowID id,
wxBoxSizer* bSizer270;
bSizer270 = new wxBoxSizer( wxHORIZONTAL );
- wxBoxSizer* bSizer275;
- bSizer275 = new wxBoxSizer( wxHORIZONTAL );
-
m_bitmapServerDir = new wxStaticBitmap( m_panel41, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 );
- bSizer275->Add( m_bitmapServerDir, 0, wxTOP|wxBOTTOM|wxLEFT|wxALIGN_CENTER_VERTICAL, 5 );
+ bSizer270->Add( m_bitmapServerDir, 0, wxTOP|wxBOTTOM|wxLEFT|wxALIGN_CENTER_VERTICAL, 5 );
m_staticText1232 = new wxStaticText( m_panel41, wxID_ANY, _("Directory on server:"), wxDefaultPosition, wxDefaultSize, 0 );
m_staticText1232->Wrap( -1 );
- bSizer275->Add( m_staticText1232, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 );
+ bSizer270->Add( m_staticText1232, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 );
- bSizer270->Add( bSizer275, 1, wxALIGN_BOTTOM|wxTOP|wxRIGHT, 5 );
+ bSizer269->Add( bSizer270, 0, wxTOP|wxRIGHT|wxLEFT, 5 );
- bSizerAccessTimeout = new wxBoxSizer( wxHORIZONTAL );
+ wxBoxSizer* bSizer217;
+ bSizer217 = new wxBoxSizer( wxHORIZONTAL );
+
+ m_textCtrlServerPath = new wxTextCtrl( m_panel41, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0 );
+ bSizer217->Add( m_textCtrlServerPath, 1, wxALIGN_CENTER_VERTICAL|wxLEFT, 5 );
+
+ m_buttonSelectFolder = new wxButton( m_panel41, wxID_ANY, _("Browse"), wxDefaultPosition, wxDefaultSize, 0 );
+ m_buttonSelectFolder->SetToolTip( _("Select a folder") );
+
+ bSizer217->Add( m_buttonSelectFolder, 0, wxRIGHT|wxEXPAND, 5 );
- m_staticline72 = new wxStaticLine( m_panel41, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL );
- bSizerAccessTimeout->Add( m_staticline72, 0, wxEXPAND, 5 );
+
+ bSizer269->Add( bSizer217, 0, wxRIGHT|wxLEFT|wxEXPAND, 5 );
+
+ wxBoxSizer* bSizer298;
+ bSizer298 = new wxBoxSizer( wxHORIZONTAL );
+
+
+ bSizer298->Add( 0, 10, 0, 0, 5 );
+
+ bSizerAccessTimeout = new wxBoxSizer( wxHORIZONTAL );
wxBoxSizer* bSizer273;
bSizer273 = new wxBoxSizer( wxHORIZONTAL );
@@ -2769,25 +2840,14 @@ CloudSetupDlgGenerated::CloudSetupDlgGenerated( wxWindow* parent, wxWindowID id,
bSizerAccessTimeout->Add( bSizer273, 0, wxALL, 5 );
+ m_staticline72 = new wxStaticLine( m_panel41, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL );
+ bSizerAccessTimeout->Add( m_staticline72, 0, wxEXPAND, 5 );
- bSizer270->Add( bSizerAccessTimeout, 0, 0, 5 );
-
-
- bSizer269->Add( bSizer270, 0, wxEXPAND|wxLEFT, 5 );
-
- wxBoxSizer* bSizer217;
- bSizer217 = new wxBoxSizer( wxHORIZONTAL );
-
- m_textCtrlServerPath = new wxTextCtrl( m_panel41, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0 );
- bSizer217->Add( m_textCtrlServerPath, 1, wxALIGN_CENTER_VERTICAL|wxBOTTOM|wxLEFT, 5 );
-
- m_buttonSelectFolder = new wxButton( m_panel41, wxID_ANY, _("Browse"), wxDefaultPosition, wxDefaultSize, 0 );
- m_buttonSelectFolder->SetToolTip( _("Select a folder") );
- bSizer217->Add( m_buttonSelectFolder, 0, wxBOTTOM|wxRIGHT|wxEXPAND, 5 );
+ bSizer298->Add( bSizerAccessTimeout, 0, wxALIGN_CENTER_VERTICAL, 5 );
- bSizer269->Add( bSizer217, 0, wxEXPAND|wxBOTTOM|wxRIGHT|wxLEFT, 5 );
+ bSizer269->Add( bSizer298, 0, 0, 5 );
bSizer185->Add( bSizer269, 0, wxEXPAND, 5 );
@@ -4743,7 +4803,7 @@ AboutDlgGenerated::AboutDlgGenerated( wxWindow* parent, wxWindowID id, const wxS
bSizer174 = new wxBoxSizer( wxHORIZONTAL );
m_bitmapLogoLeft = new wxStaticBitmap( m_panel41, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 );
- bSizer174->Add( m_bitmapLogoLeft, 0, 0, 5 );
+ bSizer174->Add( m_bitmapLogoLeft, 0, wxBOTTOM, 5 );
m_staticline81 = new wxStaticLine( m_panel41, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL );
bSizer174->Add( m_staticline81, 0, wxEXPAND, 5 );
@@ -4899,8 +4959,6 @@ AboutDlgGenerated::AboutDlgGenerated( wxWindow* parent, wxWindowID id, const wxS
bSizer290->Add( 0, 5, 0, 0, 5 );
m_bpButtonEmail = new wxBitmapButton( m_panel41, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, wxBU_AUTODRAW|0 );
- m_bpButtonEmail->SetToolTip( _("mailto:zenju@freefilesync.org") );
-
bSizer290->Add( m_bpButtonEmail, 1, wxEXPAND|wxBOTTOM|wxRIGHT, 5 );
diff --git a/FreeFileSync/Source/ui/gui_generated.h b/FreeFileSync/Source/ui/gui_generated.h
index 2a966144..761dfb15 100644
--- a/FreeFileSync/Source/ui/gui_generated.h
+++ b/FreeFileSync/Source/ui/gui_generated.h
@@ -466,11 +466,19 @@ protected:
wxSpinCtrl* m_spinCtrlVersionCountMax;
wxStaticLine* m_staticline582;
wxBoxSizer* bSizerSyncMisc;
+ wxStaticBitmap* m_bitmapEmail;
+ wxCheckBox* m_checkBoxSendEmail;
+ fff::CommandBox* m_comboBoxEmail;
+ wxBitmapButton* m_bpButtonEmailAlways;
+ wxBitmapButton* m_bpButtonEmailErrorWarning;
+ wxBitmapButton* m_bpButtonEmailErrorOnly;
+ wxStaticText* m_staticTextPerfDeRequired2;
+ wxStaticLine* m_staticline57;
wxPanel* m_panelLogfile;
wxStaticBitmap* m_bitmapLogFile;
- wxCheckBox* m_checkBoxSaveLog;
+ wxCheckBox* m_checkBoxOverrideLogPath;
wxButton* m_buttonSelectLogFolder;
- wxStaticLine* m_staticline57;
+ wxStaticLine* m_staticline80;
wxStaticText* m_staticTextPostSync;
fff::CommandBox* m_comboBoxPostSyncCommand;
wxBoxSizer* bSizerStdButtons;
@@ -520,7 +528,10 @@ protected:
virtual void OnHelpVersioning( wxHyperlinkEvent& event ) { event.Skip(); }
virtual void OnChanegVersioningStyle( wxCommandEvent& event ) { event.Skip(); }
virtual void OnToggleVersioningLimit( wxCommandEvent& event ) { event.Skip(); }
- virtual void OnToggleSaveLogfile( wxCommandEvent& event ) { event.Skip(); }
+ virtual void OnToggleMiscOption( wxCommandEvent& event ) { event.Skip(); }
+ virtual void OnEmailAlways( wxCommandEvent& event ) { event.Skip(); }
+ virtual void OnEmailErrorWarning( wxCommandEvent& event ) { event.Skip(); }
+ virtual void OnEmailErrorOnly( wxCommandEvent& event ) { event.Skip(); }
virtual void OnOkay( wxCommandEvent& event ) { event.Skip(); }
virtual void OnCancel( wxCommandEvent& event ) { event.Skip(); }
@@ -600,12 +611,12 @@ protected:
wxStaticLine* m_staticline581;
wxStaticBitmap* m_bitmapServerDir;
wxStaticText* m_staticText1232;
+ wxTextCtrl* m_textCtrlServerPath;
+ wxButton* m_buttonSelectFolder;
wxBoxSizer* bSizerAccessTimeout;
- wxStaticLine* m_staticline72;
wxStaticText* m_staticTextTimeout;
wxSpinCtrl* m_spinCtrlTimeout;
- wxTextCtrl* m_textCtrlServerPath;
- wxButton* m_buttonSelectFolder;
+ wxStaticLine* m_staticline72;
wxBoxSizer* bSizer255;
wxStaticLine* m_staticline571;
wxStaticBitmap* m_bitmapPerf;
@@ -648,7 +659,7 @@ protected:
public:
- CloudSetupDlgGenerated( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = _("Access Online Storage"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize( -1, -1 ), long style = wxDEFAULT_DIALOG_STYLE|wxMAXIMIZE_BOX|wxRESIZE_BORDER );
+ CloudSetupDlgGenerated( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = _("Access Online Storage"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize( -1, -1 ), long style = wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER );
~CloudSetupDlgGenerated();
};
diff --git a/FreeFileSync/Source/ui/gui_status_handler.cpp b/FreeFileSync/Source/ui/gui_status_handler.cpp
index 6b1504b0..8adf1f89 100644
--- a/FreeFileSync/Source/ui/gui_status_handler.cpp
+++ b/FreeFileSync/Source/ui/gui_status_handler.cpp
@@ -131,17 +131,17 @@ StatusHandlerTemporaryPanel::~StatusHandlerTemporaryPanel()
mainDlg_.compareStatus_->teardown();
- if (!errorLog_.empty()) //reportFinalStatus() was not called!
+ if (!errorLog_.empty()) //reportResults() was not called!
std::abort();
}
-StatusHandlerTemporaryPanel::Result StatusHandlerTemporaryPanel::reportFinalStatus() //noexcept!!
+StatusHandlerTemporaryPanel::Result StatusHandlerTemporaryPanel::reportResults() //noexcept!!
{
const auto totalTime = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now() - startTime_);
//determine post-sync status irrespective of further errors during tear-down
- const SyncResult finalStatus = [&]
+ const SyncResult resultStatus = [&]
{
if (getAbortStatus())
{
@@ -158,7 +158,7 @@ StatusHandlerTemporaryPanel::Result StatusHandlerTemporaryPanel::reportFinalStat
const ProcessSummary summary
{
- startTime_, finalStatus, {} /*jobName*/,
+ startTime_, resultStatus, {} /*jobName*/,
getStatsCurrent(),
getStatsTotal (),
totalTime
@@ -334,37 +334,35 @@ StatusHandlerFloatingDialog::StatusHandlerFloatingDialog(wxFrame* parentDlg,
bool ignoreErrors,
size_t automaticRetryCount,
std::chrono::seconds automaticRetryDelay,
- const std::wstring& jobName,
+ const std::vector<std::wstring>& jobNames,
const Zstring& soundFileSyncComplete,
- const Zstring& postSyncCommand,
- PostSyncCondition postSyncCondition,
bool& autoCloseDialog) :
progressDlg_(SyncProgressDialog::create([this] { userRequestAbort(); }, *this, parentDlg, true /*showProgress*/, autoCloseDialog,
-startTime, jobName, soundFileSyncComplete, ignoreErrors, automaticRetryCount, PostSyncAction2::none)),
+startTime, jobNames, soundFileSyncComplete, ignoreErrors, automaticRetryCount, PostSyncAction2::none)),
automaticRetryCount_(automaticRetryCount),
automaticRetryDelay_(automaticRetryDelay),
- jobName_(jobName),
+ jobNames_(jobNames),
startTime_(startTime),
- postSyncCommand_(postSyncCommand),
- postSyncCondition_(postSyncCondition),
autoCloseDialogOut_(autoCloseDialog) {}
StatusHandlerFloatingDialog::~StatusHandlerFloatingDialog()
{
- if (progressDlg_) //reportFinalStatus() was not called!
+ if (progressDlg_) //reportResults() was not called!
std::abort();
}
-StatusHandlerFloatingDialog::Result StatusHandlerFloatingDialog::reportFinalStatus(const Zstring& altLogFolderPathPhrase, int logfilesMaxAgeDays, const std::set<AbstractPath>& logFilePathsToKeep)
+StatusHandlerFloatingDialog::Result StatusHandlerFloatingDialog::reportResults(const Zstring& postSyncCommand, PostSyncCondition postSyncCondition,
+ const Zstring& altLogFolderPathPhrase, int logfilesMaxAgeDays, const std::set<AbstractPath>& logFilePathsToKeep,
+ const Zstring& emailNotifyAddress, ResultsNotification emailNotifyCondition)
{
const auto totalTime = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now() - startTime_);
progressDlg_->timerSetStatus(false /*active*/); //keep correct summary window stats considering count down timer, system sleep
//determine post-sync status irrespective of further errors during tear-down
- const SyncResult finalStatus = [&]
+ const SyncResult resultStatus = [&]
{
if (getAbortStatus())
{
@@ -381,72 +379,75 @@ StatusHandlerFloatingDialog::Result StatusHandlerFloatingDialog::reportFinalStat
return SyncResult::finishedSuccess;
}();
- assert(finalStatus == SyncResult::aborted || currentPhase() == ProcessPhase::synchronizing);
+ assert(resultStatus == SyncResult::aborted || currentPhase() == ProcessPhase::synchronizing);
const ProcessSummary summary
{
- startTime_, finalStatus, jobName_,
+ startTime_, resultStatus, jobNames_,
getStatsCurrent(),
getStatsTotal (),
totalTime
};
- //post sync command
- Zstring commandLine = [&]
+ const AbstractPath logFilePath = generateLogFilePath(summary, altLogFolderPathPhrase);
+ //e.g. %AppData%\FreeFileSync\Logs\Backup FreeFileSync 2013-09-15 015052.123 [Error].log
+
+ if (const Zstring cmdLine = trimCpy(postSyncCommand);
+ !cmdLine.empty())
{
if (getAbortStatus() && *getAbortStatus() == AbortTrigger::user)
; //user cancelled => don't run post sync command!
- else
- switch (postSyncCondition_)
+ else if (postSyncCondition == PostSyncCondition::COMPLETION ||
+ (postSyncCondition == PostSyncCondition::ERRORS) == (resultStatus == SyncResult::aborted ||
+ resultStatus == SyncResult::finishedError))
+ try
{
- case PostSyncCondition::COMPLETION:
- return postSyncCommand_;
- case PostSyncCondition::ERRORS:
- if (finalStatus == SyncResult::aborted ||
- finalStatus == SyncResult::finishedError)
- return postSyncCommand_;
- break;
- case PostSyncCondition::SUCCESS:
- if (finalStatus == SyncResult::finishedWarning ||
- finalStatus == SyncResult::finishedSuccess)
- return postSyncCommand_;
- break;
+ ////----------------------------------------------------------------------
+ //::wxSetEnv(L"logfile_path", AFS::getDisplayPath(logFilePath));
+ ////----------------------------------------------------------------------
+ const Zstring cmdLineExp = expandMacros(cmdLine);
+ const int exitCode = shellExecute(cmdLineExp, ExecutionType::sync, false /*hideConsole*/); //throw FileError
+ errorLog_.logMsg(_("Executing command:") + L" " + utfTo<std::wstring>(cmdLineExp) + L" [" + replaceCpy(_("Exit Code %x"), L"%x", numberTo<std::wstring>(exitCode)) + L']',
+ exitCode == 0 ? MSG_TYPE_INFO : MSG_TYPE_ERROR);
}
- return Zstring();
- }();
- trim(commandLine);
+ catch (const FileError& e) { errorLog_.logMsg(e.toString(), MSG_TYPE_ERROR); }
+ }
- if (!commandLine.empty())
- errorLog_.logMsg(_("Executing command:") + L" " + utfTo<std::wstring>(commandLine), MSG_TYPE_INFO);
+ //---------------------------- save log file ------------------------------
+ auto notifyStatusNoThrow = [&](const std::wstring& msg) { try { updateStatus(msg); /*throw AbortProcess*/ } catch (AbortProcess&) {} };
- //----------------- always save log under %appdata%\FreeFileSync\Logs ------------------------
- AbstractPath logFilePath = getNullPath();
- try
+ if (const Zstring notifyEmail = trimCpy(emailNotifyAddress);
+ !notifyEmail.empty())
+ {
+ if (getAbortStatus() && *getAbortStatus() == AbortTrigger::user)
+ ; //user cancelled => don't send email notification!
+ else if (emailNotifyCondition == ResultsNotification::always ||
+ (emailNotifyCondition == ResultsNotification::errorWarning && (resultStatus == SyncResult::aborted ||
+ resultStatus == SyncResult::finishedError ||
+ resultStatus == SyncResult::finishedWarning)) ||
+ (emailNotifyCondition == ResultsNotification::errorOnly && (resultStatus == SyncResult::aborted ||
+ resultStatus == SyncResult::finishedError)))
+ try
+ {
+ sendLogAsEmail(notifyEmail, summary, errorLog_, logFilePath, notifyStatusNoThrow); //throw FileError
+ }
+ catch (const FileError& e) { errorLog_.logMsg(e.toString(), MSG_TYPE_ERROR); }
+ }
+
+ try //create not before destruction: 1. avoid issues with FFS trying to sync open log file 2. include status in log file name without extra rename
{
//do NOT use tryReportingError()! saving log files should not be cancellable!
- auto notifyStatusNoThrow = [&](const std::wstring& msg) { try { updateStatus(msg); /*throw AbortProcess*/ } catch (...) {} };
- logFilePath = saveLogFile(summary, errorLog_, altLogFolderPathPhrase, logfilesMaxAgeDays, logFilePathsToKeep, notifyStatusNoThrow); //throw FileError
+ saveLogFile(logFilePath, summary, errorLog_, logfilesMaxAgeDays, logFilePathsToKeep, notifyStatusNoThrow); //throw FileError
}
catch (const FileError& e) { errorLog_.logMsg(e.toString(), MSG_TYPE_ERROR); }
- //execute post sync command *after* writing log files, so that user can refer to the log via the command!
- if (!commandLine.empty())
- try
- {
- //----------------------------------------------------------------------
- ::wxSetEnv(L"logfile_path", AFS::getDisplayPath(logFilePath));
- //----------------------------------------------------------------------
- //use ExecutionType::ASYNC until there is reason not to: https://freefilesync.org/forum/viewtopic.php?t=31
- shellExecute(expandMacros(commandLine), ExecutionType::ASYNC, false/*hideConsole*/); //throw FileError
- }
- catch (const FileError& e) { errorLog_.logMsg(e.toString(), MSG_TYPE_ERROR); }
-
+ //----------------- post sync action ------------------------
auto mayRunAfterCountDown = [&](const std::wstring& operationName)
{
auto notifyStatusThrowOnCancel = [&](const std::wstring& msg)
{
try { updateStatus(msg); /*throw AbortProcess*/ }
- catch (...)
+ catch (AbortProcess&)
{
if (getAbortStatus() && *getAbortStatus() == AbortTrigger::user)
throw;
@@ -458,17 +459,16 @@ StatusHandlerFloatingDialog::Result StatusHandlerFloatingDialog::reportFinalStat
{
delayAndCountDown(operationName, std::chrono::seconds(5), notifyStatusThrowOnCancel); //throw AbortProcess
}
- catch (...) { return false; }
+ catch (AbortProcess&) { return false; }
return true;
};
- //post sync action
bool autoClose = false;
FinalRequest finalRequest = FinalRequest::none;
if (getAbortStatus() && *getAbortStatus() == AbortTrigger::user)
- ; //user cancelled => don't run post sync command!
+ ; //user cancelled => don't run post sync action!
else
switch (progressDlg_->getOptionPostSyncAction())
{
@@ -502,7 +502,7 @@ StatusHandlerFloatingDialog::Result StatusHandlerFloatingDialog::reportFinalStat
autoCloseDialogOut_ = //output parameter owned by SyncProgressDialog (evaluate *after* user closed the results dialog)
progressDlg_->destroy(autoClose,
finalRequest == FinalRequest::none /*restoreParentFrame*/,
- finalStatus, errorLogFinal).autoCloseDialog;
+ resultStatus, errorLogFinal).autoCloseDialog;
progressDlg_ = nullptr;
return { summary, errorLogFinal, finalRequest, logFilePath };
diff --git a/FreeFileSync/Source/ui/gui_status_handler.h b/FreeFileSync/Source/ui/gui_status_handler.h
index 0a5e96b9..82903078 100644
--- a/FreeFileSync/Source/ui/gui_status_handler.h
+++ b/FreeFileSync/Source/ui/gui_status_handler.h
@@ -38,7 +38,7 @@ public:
ProcessSummary summary;
std::shared_ptr<const zen::ErrorLog> errorLog;
};
- Result reportFinalStatus(); //noexcept!!
+ Result reportResults(); //noexcept!!
private:
void OnKeyPressed(wxKeyEvent& event);
@@ -64,10 +64,8 @@ public:
bool ignoreErrors,
size_t automaticRetryCount,
std::chrono::seconds automaticRetryDelay,
- const std::wstring& jobName,
+ const std::vector<std::wstring>& jobNames,
const Zstring& soundFileSyncComplete,
- const Zstring& postSyncCommand,
- PostSyncCondition postSyncCondition,
bool& autoCloseDialog); //noexcept!
~StatusHandlerFloatingDialog();
@@ -93,17 +91,17 @@ public:
FinalRequest finalRequest;
AbstractPath logFilePath;
};
- Result reportFinalStatus(const Zstring& altLogFolderPathPhrase, int logfilesMaxAgeDays, const std::set<AbstractPath>& logFilePathsToKeep); //noexcept!!
+ Result reportResults(const Zstring& postSyncCommand, PostSyncCondition postSyncCondition,
+ const Zstring& altLogFolderPathPhrase, int logfilesMaxAgeDays, const std::set<AbstractPath>& logFilePathsToKeep,
+ const Zstring& emailNotifyAddress, ResultsNotification emailNotifyCondition); //noexcept!!
private:
SyncProgressDialog* progressDlg_; //managed to have the same lifetime as this handler!
zen::ErrorLog errorLog_;
const size_t automaticRetryCount_;
const std::chrono::seconds automaticRetryDelay_;
- const std::wstring jobName_;
+ const std::vector<std::wstring> jobNames_;
const std::chrono::system_clock::time_point startTime_;
- const Zstring postSyncCommand_;
- const PostSyncCondition postSyncCondition_;
bool& autoCloseDialogOut_; //owned by SyncProgressDialog
};
}
diff --git a/FreeFileSync/Source/ui/main_dlg.cpp b/FreeFileSync/Source/ui/main_dlg.cpp
index 5fbaea53..d6fb64d4 100644
--- a/FreeFileSync/Source/ui/main_dlg.cpp
+++ b/FreeFileSync/Source/ui/main_dlg.cpp
@@ -379,11 +379,10 @@ MainDialog::MainDialog(const Zstring& globalConfigFilePath,
const XmlGlobalSettings& globalSettings,
bool startComparison) :
MainDialogGenerated(nullptr),
- globalConfigFilePath_(globalConfigFilePath)
+ globalConfigFilePath_(globalConfigFilePath),
+ folderHistoryLeft_ (std::make_shared<HistoryList>(globalSettings.gui.mainDlg.folderHistoryLeft, globalSettings.gui.folderHistoryMax)),
+ folderHistoryRight_(std::make_shared<HistoryList>(globalSettings.gui.mainDlg.folderHistoryRight, globalSettings.gui.folderHistoryMax))
{
- m_folderPathLeft ->init(folderHistoryLeft_ );
- m_folderPathRight->init(folderHistoryRight_);
-
//setup sash: detach + reparent:
m_splitterMain->SetSizer(nullptr); //alas wxFormbuilder doesn't allow us to have child windows without a sizer, so we have to remove it here
m_splitterMain->setupWindows(m_gridMainL, m_gridMainC, m_gridMainR);
@@ -999,8 +998,8 @@ void MainDialog::setGlobalCfgOnInit(const XmlGlobalSettings& globalSettings)
//--------------------------------------------------------------------------------
//load list of last used folders
- folderHistoryLeft_ .ref() = FolderHistory(globalSettings.gui.mainDlg.folderHistoryLeft, globalSettings.gui.mainDlg.folderHistItemsMax);
- folderHistoryRight_.ref() = FolderHistory(globalSettings.gui.mainDlg.folderHistoryRight, globalSettings.gui.mainDlg.folderHistItemsMax);
+ m_folderPathLeft ->setHistory(folderHistoryLeft_);
+ m_folderPathRight->setHistory(folderHistoryRight_);
//show/hide file icons
filegrid::setupIcons(*m_gridMainL, *m_gridMainC, *m_gridMainR, globalSettings.gui.mainDlg.showIcons, convert(globalSettings.gui.mainDlg.iconSize));
@@ -1104,8 +1103,8 @@ XmlGlobalSettings MainDialog::getGlobalCfgBeforeExit()
globalSettings.gui.mainDlg.lastUsedConfigFiles = activeConfigFiles_;
//write list of last used folders
- globalSettings.gui.mainDlg.folderHistoryLeft = folderHistoryLeft_ .ref().getList();
- globalSettings.gui.mainDlg.folderHistoryRight = folderHistoryRight_.ref().getList();
+ globalSettings.gui.mainDlg.folderHistoryLeft = folderHistoryLeft_ ->getList();
+ globalSettings.gui.mainDlg.folderHistoryRight = folderHistoryRight_->getList();
globalSettings.gui.mainDlg.textSearchRespectCase = m_checkBoxMatchCase->GetValue();
@@ -1336,8 +1335,7 @@ void MainDialog::copyToAlternateFolder(const std::vector<FileSystemObject*>& sel
if (showCopyToDialog(this,
selectionLeft, selectionRight,
globalCfg_.gui.mainDlg.copyToCfg.lastUsedPath,
- globalCfg_.gui.mainDlg.copyToCfg.folderHistory,
- globalCfg_.gui.mainDlg.folderHistItemsMax,
+ globalCfg_.gui.mainDlg.copyToCfg.folderHistory, globalCfg_.gui.folderHistoryMax,
globalCfg_.gui.mainDlg.copyToCfg.keepRelPaths,
globalCfg_.gui.mainDlg.copyToCfg.overwriteIfExists) != ReturnSmallDlg::BUTTON_OKAY)
return;
@@ -1365,7 +1363,7 @@ void MainDialog::copyToAlternateFolder(const std::vector<FileSystemObject*>& sel
}
catch (AbortProcess&) {}
- const StatusHandlerTemporaryPanel::Result r = statusHandler.reportFinalStatus(); //noexcept
+ const StatusHandlerTemporaryPanel::Result r = statusHandler.reportResults(); //noexcept
setLastOperationLog(r.summary, r.errorLog);
@@ -1407,7 +1405,7 @@ void MainDialog::deleteSelectedFiles(const std::vector<FileSystemObject*>& selec
}
catch (AbortProcess&) {}
- const StatusHandlerTemporaryPanel::Result r = statusHandler.reportFinalStatus(); //noexcept
+ const StatusHandlerTemporaryPanel::Result r = statusHandler.reportResults(); //noexcept
setLastOperationLog(r.summary, r.errorLog);
@@ -1511,7 +1509,7 @@ void invokeCommandLine(const Zstring& commandLinePhrase, //throw FileError
replace(command, Zstr("%parent_path%"), folderPath);
replace(command, Zstr("%parent_path2%"), folderPath2);
- shellExecute(command, selection.size() > EXT_APP_MASS_INVOKE_THRESHOLD ? ExecutionType::SYNC : ExecutionType::ASYNC, false/*hideConsole*/); //throw FileError
+ shellExecute(command, selection.size() > EXT_APP_MASS_INVOKE_THRESHOLD ? ExecutionType::sync : ExecutionType::async, false/*hideConsole*/); //throw FileError
}
}
}
@@ -1618,11 +1616,11 @@ void MainDialog::openExternalApplication(const Zstring& commandLinePhrase, bool
}
catch (AbortProcess&) {}
- const StatusHandlerTemporaryPanel::Result r = statusHandler.reportFinalStatus(); //noexcept
+ const StatusHandlerTemporaryPanel::Result r = statusHandler.reportResults(); //noexcept
setLastOperationLog(r.summary, r.errorLog);
- if (r.summary.finalStatus == SyncResult::aborted)
+ if (r.summary.resultStatus == SyncResult::aborted)
return;
//updateGui(); -> not needed
@@ -2831,6 +2829,12 @@ void MainDialog::updateUnsavedCfgStatus()
{
const Zstring activeCfgFilePath = activeConfigFiles_.size() == 1 && !equalNativePath(activeConfigFiles_[0], lastRunConfigPath_) ? activeConfigFiles_[0] : Zstring();
+ std::vector<std::wstring> jobNames;
+ for (const Zstring& cfgFilePath : activeConfigFiles_)
+ jobNames.push_back(equalNativePath(cfgFilePath, lastRunConfigPath_) ?
+ L"[" + _("Last session") + L"]" :
+ extractJobName(cfgFilePath));
+
const bool haveUnsavedCfg = lastSavedCfg_ != getConfig();
//update save config button
@@ -2857,8 +2861,9 @@ void MainDialog::updateUnsavedCfgStatus()
title += utfTo<wxString>(activeCfgFilePath);
else if (activeConfigFiles_.size() > 1)
{
- title += extractJobName(activeConfigFiles_[0]);
- std::for_each(activeConfigFiles_.begin() + 1, activeConfigFiles_.end(), [&](const Zstring& filePath) { title += SPACED_DASH + extractJobName(filePath); });
+ title += jobNames[0];
+ std::for_each(jobNames.begin() + 1, jobNames.end(), [&](const std::wstring& jobName)
+ { title += L" + " + jobName; });
}
else
{
@@ -3141,7 +3146,8 @@ void MainDialog::onCfgGridSelection(GridSelectEvent& event)
else
assert(false);
- if (!loadConfiguration(filePaths))
+ if (filePaths.empty() || //ignore accidental clicks in empty space of configuration panel
+ !loadConfiguration(filePaths))
//user changed m_gridCfgHistory selection so it's this method's responsibility to synchronize with activeConfigFiles:
//- if user cancelled saving old config
//- there's an error loading new config
@@ -3579,10 +3585,11 @@ void MainDialog::showConfigDialog(SyncConfigPanel panelToShow, int localPairInde
globalPairCfg.miscCfg.ignoreErrors = currentCfg_.mainCfg.ignoreErrors;
globalPairCfg.miscCfg.automaticRetryCount = currentCfg_.mainCfg.automaticRetryCount;
globalPairCfg.miscCfg.automaticRetryDelay = currentCfg_.mainCfg.automaticRetryDelay;
- globalPairCfg.miscCfg.altLogFolderPathPhrase = currentCfg_.mainCfg.altLogFolderPathPhrase;
globalPairCfg.miscCfg.postSyncCommand = currentCfg_.mainCfg.postSyncCommand;
globalPairCfg.miscCfg.postSyncCondition = currentCfg_.mainCfg.postSyncCondition;
- globalPairCfg.miscCfg.commandHistory = globalCfg_.gui.commandHistory;
+ globalPairCfg.miscCfg.altLogFolderPathPhrase = currentCfg_.mainCfg.altLogFolderPathPhrase;
+ globalPairCfg.miscCfg.emailNotifyAddress = currentCfg_.mainCfg.emailNotifyAddress;
+ globalPairCfg.miscCfg.emailNotifyCondition = currentCfg_.mainCfg.emailNotifyCondition;
//don't recalculate value but consider current screen status!!!
//e.g. it's possible that the first folder pair local config is shown with all config initial if user just removed local config via mouse context menu!
@@ -3609,7 +3616,11 @@ void MainDialog::showConfigDialog(SyncConfigPanel panelToShow, int localPairInde
showMultipleCfgs,
globalPairCfg,
localCfgs,
- globalCfg_.gui.commandHistItemsMax) != ReturnSyncConfig::BUTTON_OKAY)
+ globalCfg_.gui.versioningFolderHistory,
+ globalCfg_.gui.logFolderHistory,
+ globalCfg_.gui.folderHistoryMax,
+ globalCfg_.gui.emailHistory, globalCfg_.gui.emailHistoryMax,
+ globalCfg_.gui.commandHistory, globalCfg_.gui.commandHistoryMax) != ReturnSyncConfig::BUTTON_OKAY)
return;
assert(localCfgs.size() == localPairCfgOld.size());
@@ -3622,10 +3633,11 @@ void MainDialog::showConfigDialog(SyncConfigPanel panelToShow, int localPairInde
currentCfg_.mainCfg.ignoreErrors = globalPairCfg.miscCfg.ignoreErrors;
currentCfg_.mainCfg.automaticRetryCount = globalPairCfg.miscCfg.automaticRetryCount;
currentCfg_.mainCfg.automaticRetryDelay = globalPairCfg.miscCfg.automaticRetryDelay;
- currentCfg_.mainCfg.altLogFolderPathPhrase = globalPairCfg.miscCfg.altLogFolderPathPhrase;
currentCfg_.mainCfg.postSyncCommand = globalPairCfg.miscCfg.postSyncCommand;
currentCfg_.mainCfg.postSyncCondition = globalPairCfg.miscCfg.postSyncCondition;
- globalCfg_.gui.commandHistory = globalPairCfg.miscCfg.commandHistory;
+ currentCfg_.mainCfg.altLogFolderPathPhrase = globalPairCfg.miscCfg.altLogFolderPathPhrase;
+ currentCfg_.mainCfg.emailNotifyAddress = globalPairCfg.miscCfg.emailNotifyAddress;
+ currentCfg_.mainCfg.emailNotifyCondition = globalPairCfg.miscCfg.emailNotifyCondition;
firstFolderPair_->setValues(localCfgs[0]);
@@ -3660,15 +3672,15 @@ void MainDialog::showConfigDialog(SyncConfigPanel panelToShow, int localPairInde
return false;
}();
- //const bool miscConfigChanged = globalPairCfg.miscCfg.deviceParallelOps != globalPairCfgOld.miscCfg.deviceParallelOps ||
- // globalPairCfg.miscCfg.ignoreErrors != globalPairCfgOld.miscCfg.ignoreErrors ||
- // globalPairCfg.miscCfg.automaticRetryCount != globalPairCfgOld.miscCfg.automaticRetryCount ||
- // globalPairCfg.miscCfg.automaticRetryDelay != globalPairCfgOld.miscCfg.automaticRetryDelay ||
+ //const bool miscConfigChanged = globalPairCfg.miscCfg.deviceParallelOps != globalPairCfgOld.miscCfg.deviceParallelOps ||
+ // globalPairCfg.miscCfg.ignoreErrors != globalPairCfgOld.miscCfg.ignoreErrors ||
+ // globalPairCfg.miscCfg.automaticRetryCount != globalPairCfgOld.miscCfg.automaticRetryCount ||
+ // globalPairCfg.miscCfg.automaticRetryDelay != globalPairCfgOld.miscCfg.automaticRetryDelay ||
+ // globalPairCfg.miscCfg.postSyncCommand != globalPairCfgOld.miscCfg.postSyncCommand ||
+ // globalPairCfg.miscCfg.postSyncCondition != globalPairCfgOld.miscCfg.postSyncCondition ||
// globalPairCfg.miscCfg.altLogFolderPathPhrase != globalPairCfgOld.miscCfg.altLogFolderPathPhrase ||
- // globalPairCfg.miscCfg.postSyncCommand != globalPairCfgOld.miscCfg.postSyncCommand ||
- // globalPairCfg.miscCfg.postSyncCondition != globalPairCfgOld.miscCfg.postSyncCondition;
- ///**/ //globalPairCfg.miscCfg.commandHistory != globalPairCfgOld.miscCfg.commandHistory;
- //------------------------------------------------------------------------------------
+ // globalPairCfg.miscCfg.emailNotifyAddress != globalPairCfgOld.miscCfg.emailNotifyAddress ||
+ // globalPairCfg.miscCfg.emailNotifyCondition != globalPairCfgOld.miscCfg.emailNotifyCondition;
if (cmpConfigChanged)
applyCompareConfig(globalPairCfg.cmpCfg.compareVar != globalPairCfgOld.cmpCfg.compareVar /*setDefaultViewType*/);
@@ -3799,18 +3811,9 @@ void MainDialog::OnViewTypeContext(wxEvent& event)
void MainDialog::updateGlobalFilterButton()
{
//global filter: test for Null-filter
- std::wstring status;
- if (!isNullFilter(currentCfg_.mainCfg.globalFilter))
- {
- setImage(*m_bpButtonFilter, getResourceImage(L"cfg_filter"));
- status = _("Active");
- }
- else
- {
- setImage(*m_bpButtonFilter, greyScale(getResourceImage(L"cfg_filter")));
- status = _("None");
- }
+ setImage(*m_bpButtonFilter, greyScaleIfDisabled(getResourceImage(L"cfg_filter"), !isNullFilter(currentCfg_.mainCfg.globalFilter)));
+ const std::wstring status = !isNullFilter(currentCfg_.mainCfg.globalFilter) ? _("Active") : _("None");
m_bpButtonFilter->SetToolTip(_("Filter") + L" (F7) (" + status + L")");
m_bpButtonFilterContext->SetToolTip(m_bpButtonFilter->GetToolTipText());
}
@@ -3863,12 +3866,12 @@ void MainDialog::OnCompare(wxCommandEvent& event)
}
catch (AbortProcess&) {}
- const StatusHandlerTemporaryPanel::Result r = statusHandler.reportFinalStatus(); //noexcept
+ const StatusHandlerTemporaryPanel::Result r = statusHandler.reportResults(); //noexcept
//---------------------------------------------------------------------------
setLastOperationLog(r.summary, r.errorLog);
- if (r.summary.finalStatus == SyncResult::aborted)
+ if (r.summary.resultStatus == SyncResult::aborted)
return updateGui(); //refresh grid in ANY case! (also on abort)
@@ -3898,8 +3901,8 @@ void MainDialog::OnCompare(wxCommandEvent& event)
//remember folder history (except when cancelled by user)
for (const FolderPairCfg& fpCfg : fpCfgList)
{
- folderHistoryLeft_ .ref().addItem(fpCfg.folderPathPhraseLeft_);
- folderHistoryRight_.ref().addItem(fpCfg.folderPathPhraseRight_);
+ folderHistoryLeft_ ->addItem(fpCfg.folderPathPhraseLeft_);
+ folderHistoryRight_->addItem(fpCfg.folderPathPhraseRight_);
}
assert(m_buttonCompare->GetId() != wxID_ANY);
@@ -3907,7 +3910,7 @@ void MainDialog::OnCompare(wxCommandEvent& event)
fp.setFocus(m_buttonSync);
//mark selected cfg files as "in sync" when there is nothing to do: https://freefilesync.org/forum/viewtopic.php?t=4991
- if (r.summary.finalStatus == SyncResult::finishedSuccess)
+ if (r.summary.resultStatus == SyncResult::finishedSuccess)
{
const SyncStatistics st(folderCmp_);
if (st.createCount() +
@@ -3915,7 +3918,7 @@ void MainDialog::OnCompare(wxCommandEvent& event)
st.deleteCount() == 0)
{
flashStatusInformation(_("All files are in sync"));
- updateConfigLastRunStats(std::chrono::system_clock::to_time_t(startTime), r.summary.finalStatus, getNullPath() /*logFilePath*/);
+ updateConfigLastRunStats(std::chrono::system_clock::to_time_t(startTime), r.summary.resultStatus, getNullPath() /*logFilePath*/);
}
}
}
@@ -3968,11 +3971,7 @@ void MainDialog::updateStatistics()
txtControl.SetFont(fnt);
txtControl.SetLabel(valueAsString);
-
- if (isZeroValue)
- bmpControl.SetBitmap(greyScale(mirrorIfRtl(getResourceImage(bmpName))));
- else
- bmpControl.SetBitmap(mirrorIfRtl(getResourceImage(bmpName)));
+ bmpControl.SetBitmap(greyScaleIfDisabled(mirrorIfRtl(getResourceImage(bmpName)), !isZeroValue));
}
};
@@ -4048,25 +4047,28 @@ void MainDialog::OnStartSync(wxCommandEvent& event)
for (const ConfigFileItem& item : cfggrid::getDataView(*m_gridCfgHistory).get())
logFilePathsToKeep.insert(item.logFilePath);
- const Zstring activeCfgFilePath = activeConfigFiles_.size() == 1 && !equalNativePath(activeConfigFiles_[0], lastRunConfigPath_) ? activeConfigFiles_[0] : Zstring();
const std::chrono::system_clock::time_point syncStartTime = std::chrono::system_clock::now();
+ std::vector<std::wstring> jobNames;
+ for (const Zstring& cfgFilePath : activeConfigFiles_)
+ jobNames.push_back(equalNativePath(cfgFilePath, lastRunConfigPath_) ?
+ L"[" + _("Last session") + L"]" :
+ extractJobName(cfgFilePath));
+
using FinalRequest = StatusHandlerFloatingDialog::FinalRequest;
FinalRequest finalRequest = FinalRequest::none;
{
disableAllElements(false /*enableAbort*/); //StatusHandlerFloatingDialog will internally process Window messages, so avoid unexpected callbacks!
ZEN_ON_SCOPE_EXIT(enableAllElements());
- //run this->enableAllElements() BEFORE "exitRequest" buf AFTER StatusHandlerFloatingDialog::reportFinalStatus()
+ //run this->enableAllElements() BEFORE "exitRequest" buf AFTER StatusHandlerFloatingDialog::reportResults()
//class handling status updates and error messages
StatusHandlerFloatingDialog statusHandler(this, syncStartTime,
guiCfg.mainCfg.ignoreErrors,
guiCfg.mainCfg.automaticRetryCount,
guiCfg.mainCfg.automaticRetryDelay,
- extractJobName(activeCfgFilePath),
+ jobNames,
globalCfg_.soundFileSyncFinished,
- guiCfg.mainCfg.postSyncCommand,
- guiCfg.mainCfg.postSyncCondition,
globalCfg_.autoCloseProgressDialog);
try
{
@@ -4109,13 +4111,15 @@ void MainDialog::OnStartSync(wxCommandEvent& event)
}
catch (AbortProcess&) {}
- StatusHandlerFloatingDialog::Result r = statusHandler.reportFinalStatus(guiCfg.mainCfg.altLogFolderPathPhrase, globalCfg_.logfilesMaxAgeDays, logFilePathsToKeep); //noexcept
+ StatusHandlerFloatingDialog::Result r = statusHandler.reportResults(guiCfg.mainCfg.postSyncCommand, guiCfg.mainCfg.postSyncCondition,
+ guiCfg.mainCfg.altLogFolderPathPhrase, globalCfg_.logfilesMaxAgeDays, logFilePathsToKeep,
+ guiCfg.mainCfg.emailNotifyAddress, guiCfg.mainCfg.emailNotifyCondition); //noexcept
//---------------------------------------------------------------------------
setLastOperationLog(r.summary, r.errorLog.ptr());
//update last sync stats for the selected cfg files
- updateConfigLastRunStats(std::chrono::system_clock::to_time_t(syncStartTime), r.summary.finalStatus, r.logFilePath);
+ updateConfigLastRunStats(std::chrono::system_clock::to_time_t(syncStartTime), r.summary.resultStatus, r.logFilePath);
//remove empty rows: just a beautification, invalid rows shouldn't cause issues
filegrid::getDataView(*m_gridMainC).removeInvalidRows();
@@ -4292,7 +4296,7 @@ void MainDialog::startSyncForSelecction(const std::vector<FileSystemObject*>& se
}
catch (AbortProcess&) {}
- const StatusHandlerTemporaryPanel::Result r = statusHandler.reportFinalStatus(); //noexcept
+ const StatusHandlerTemporaryPanel::Result r = statusHandler.reportResults(); //noexcept
setLastOperationLog(r.summary, r.errorLog);
} //run updateGui() *after* reverting our temporary exclusions
@@ -4318,16 +4322,15 @@ void MainDialog::setLastOperationLog(const ProcessSummary& summary, const std::s
{
const wxBitmap statusImage = [&]
{
- switch (summary.finalStatus)
+ switch (summary.resultStatus)
{
case SyncResult::finishedSuccess:
- return getResourceImage(L"status_finished_success");
+ return getResourceImage(L"result_success");
case SyncResult::finishedWarning:
- return getResourceImage(L"status_finished_warnings");
+ return getResourceImage(L"result_warning");
case SyncResult::finishedError:
- return getResourceImage(L"status_finished_errors");
case SyncResult::aborted:
- return getResourceImage(L"status_aborted");
+ return getResourceImage(L"result_error");
}
assert(false);
return wxNullBitmap;
@@ -4335,7 +4338,7 @@ void MainDialog::setLastOperationLog(const ProcessSummary& summary, const std::s
const wxImage statusOverlayImage = [&]
{
- switch (summary.finalStatus)
+ switch (summary.resultStatus)
{
case SyncResult::finishedSuccess:
break;
@@ -4349,7 +4352,7 @@ void MainDialog::setLastOperationLog(const ProcessSummary& summary, const std::s
}();
m_bitmapLogStatus->SetBitmap(statusImage);
- m_staticTextLogStatus->SetLabel(getFinalStatusLabel(summary.finalStatus));
+ m_staticTextLogStatus->SetLabel(getResultsStatusLabel(summary.resultStatus));
m_staticTextItemsProcessed->SetLabel(formatNumber(summary.statsProcessed.items));
@@ -4595,7 +4598,7 @@ void MainDialog::OnSwapSides(wxCommandEvent& event)
}
catch (AbortProcess&) {}
- const StatusHandlerTemporaryPanel::Result r = statusHandler.reportFinalStatus(); //noexcept
+ const StatusHandlerTemporaryPanel::Result r = statusHandler.reportResults(); //noexcept
setLastOperationLog(r.summary, r.errorLog);
}
@@ -4817,7 +4820,7 @@ void MainDialog::setStatusBarFileStats(FileView::FileStats statsLeft,
if (filegrid::getDataView(*m_gridMainC).rowsTotal() > 0)
{
statusCenterNew = _P("Showing %y of 1 row", "Showing %y of %x rows", filegrid::getDataView(*m_gridMainC).rowsTotal());
- replace(statusCenterNew, L"%y", formatNumber(filegrid::getDataView(*m_gridMainC).rowsOnView())); //%x is already used as plural form placeholder!
+ replace(statusCenterNew, L"%y", formatNumber(filegrid::getDataView(*m_gridMainC).rowsOnView())); //%x used as plural form placeholder!
}
//fill middle text (considering flashStatusInformation())
@@ -4863,7 +4866,7 @@ void MainDialog::applySyncDirections()
}
catch (AbortProcess&) {}
- const StatusHandlerTemporaryPanel::Result r = statusHandler.reportFinalStatus(); //noexcept
+ const StatusHandlerTemporaryPanel::Result r = statusHandler.reportResults(); //noexcept
setLastOperationLog(r.summary, r.errorLog);
}
@@ -5158,7 +5161,7 @@ void MainDialog::updateGuiForFolderPair()
m_panelDirectoryPairs->ClientToWindowSize(m_panelTopCenter->GetSize()).y); //
const int addPairHeight = !additionalFolderPairs_.empty() ? additionalFolderPairs_[0]->GetSize().y : 0;
- const double addPairCountMax = std::max(globalCfg_.gui.mainDlg.maxFolderPairsVisible - 1 + 0.5, 1.5);
+ const double addPairCountMax = std::max(globalCfg_.gui.mainDlg.folderPairsVisibleMax - 1 + 0.5, 1.5);
const double addPairCountMin = std::min<double>(1.5, additionalFolderPairs_.size()); //add 0.5 to indicate additional folders
const double addPairCountOpt = std::min<double>(addPairCountMax, additionalFolderPairs_.size()); //
@@ -5199,7 +5202,7 @@ void MainDialog::recalcMaxFolderPairsVisible()
if (numeric::dist(addPairCountCurrent, *addPairCountLast_) > 0.4) //=> presumely changed by user!
{
- globalCfg_.gui.mainDlg.maxFolderPairsVisible = numeric::round(addPairCountCurrent) + 1;
+ globalCfg_.gui.mainDlg.folderPairsVisibleMax = numeric::round(addPairCountCurrent) + 1;
}
}
}
@@ -5223,9 +5226,9 @@ void MainDialog::insertAddFolderPair(const std::vector<LocalPairConfig>& newPair
{
newPair = new FolderPairPanel(m_scrolledWindowFolderPairs, *this);
- //init dropdown history
- newPair->m_folderPathLeft ->init(folderHistoryLeft_ );
- newPair->m_folderPathRight->init(folderHistoryRight_);
+ //setHistory dropdown history
+ newPair->m_folderPathLeft ->setHistory(folderHistoryLeft_ );
+ newPair->m_folderPathRight->setHistory(folderHistoryRight_);
newPair->m_bpButtonFolderPairOptions->SetBitmapLabel(getResourceImage(L"button_arrow_down"));
diff --git a/FreeFileSync/Source/ui/main_dlg.h b/FreeFileSync/Source/ui/main_dlg.h
index 06145bc7..a18a3223 100644
--- a/FreeFileSync/Source/ui/main_dlg.h
+++ b/FreeFileSync/Source/ui/main_dlg.h
@@ -346,8 +346,8 @@ private:
//regenerate view filter button labels only when necessary:
std::unordered_map<const zen::ToggleButton*, int /*itemCount*/> buttonLabelItemCount_;
- zen::SharedRef<FolderHistory> folderHistoryLeft_ = zen::makeSharedRef<FolderHistory>(); //shared by all wxComboBox dropdown controls
- zen::SharedRef<FolderHistory> folderHistoryRight_ = zen::makeSharedRef<FolderHistory>(); //
+ const std::shared_ptr<HistoryList> folderHistoryLeft_; //shared by all wxComboBox dropdown controls
+ const std::shared_ptr<HistoryList> folderHistoryRight_; //
zen::AsyncGuiQueue guiQueue_; //schedule and run long-running tasks asynchronously, but process results on GUI queue
diff --git a/FreeFileSync/Source/ui/progress_indicator.cpp b/FreeFileSync/Source/ui/progress_indicator.cpp
index 7f2ac3df..8cb37ee5 100644
--- a/FreeFileSync/Source/ui/progress_indicator.cpp
+++ b/FreeFileSync/Source/ui/progress_indicator.cpp
@@ -648,13 +648,13 @@ public:
bool showProgress,
bool autoCloseDialog,
const std::chrono::system_clock::time_point& syncStartTime,
- const wxString& jobName,
+ const std::vector<std::wstring>& jobNames,
const Zstring& soundFileSyncComplete,
bool ignoreErrors,
size_t automaticRetryCount,
PostSyncAction2 postSyncAction);
- Result destroy(bool autoClose, bool restoreParentFrame, SyncResult finalStatus, const SharedRef<const zen::ErrorLog>& log) override;
+ Result destroy(bool autoClose, bool restoreParentFrame, SyncResult resultStatus, const SharedRef<const zen::ErrorLog>& log) override;
wxWindow* getWindowIfVisible() override { return this->IsShown() ? this : nullptr; }
//workaround OS X bug: if "this" is used as parent window for a modal dialog then this dialog will erroneously un-hide its parent!
@@ -692,7 +692,7 @@ private:
void OnMinimizeToTray(wxCommandEvent& event) { minimizeToTray(); }
//void OnToggleIgnoreErrors(wxCommandEvent& event) { updateStaticGui(); }
- void showSummary(SyncResult finalStatus, const SharedRef<const ErrorLog>& log);
+ void showSummary(SyncResult resultStatus, const SharedRef<const ErrorLog>& log);
void minimizeToTray();
void resumeFromSystray();
@@ -751,7 +751,7 @@ SyncProgressDialogImpl<TopLevelDialog>::SyncProgressDialogImpl(long style, //wxF
bool showProgress,
bool autoCloseDialog,
const std::chrono::system_clock::time_point& syncStartTime,
- const wxString& jobName,
+ const std::vector<std::wstring>& jobNames,
const Zstring& soundFileSyncComplete,
bool ignoreErrors,
size_t automaticRetryCount,
@@ -759,11 +759,22 @@ SyncProgressDialogImpl<TopLevelDialog>::SyncProgressDialogImpl(long style, //wxF
TopLevelDialog(parentFrame, wxID_ANY, wxString(), wxDefaultPosition, wxDefaultSize, style), //title is overwritten anyway in setExternalStatus()
pnl_(*new SyncProgressPanelGenerated(this)), //ownership passed to "this"
syncStartTime_(syncStartTime),
- jobName_ (jobName),
- soundFileSyncComplete_(soundFileSyncComplete),
- parentFrame_(parentFrame),
- userRequestAbort_(userRequestAbort),
- syncStat_(&syncStat)
+ jobName_([&]
+{
+ std::wstring tmp;
+ if (!jobNames.empty())
+ {
+ tmp = jobNames[0];
+ std::for_each(jobNames.begin() + 1, jobNames.end(), [&](const std::wstring& jobName)
+ { tmp += L" + " + jobName; });
+ }
+ return tmp;
+}
+()),
+soundFileSyncComplete_(soundFileSyncComplete),
+parentFrame_(parentFrame),
+userRequestAbort_(userRequestAbort),
+syncStat_(&syncStat)
{
static_assert(std::is_same_v<TopLevelDialog, wxFrame > ||
std::is_same_v<TopLevelDialog, wxDialog>);
@@ -1210,7 +1221,7 @@ void SyncProgressDialogImpl<TopLevelDialog>::updateStaticGui() //depends on "syn
return getResourceImage(L"status_pause");
if (syncStat_->getAbortStatus())
- return getResourceImage(L"status_aborted");
+ return getResourceImage(L"result_error");
switch (syncStat_->currentPhase())
{
@@ -1255,7 +1266,7 @@ void SyncProgressDialogImpl<TopLevelDialog>::updateStaticGui() //depends on "syn
template <class TopLevelDialog>
-void SyncProgressDialogImpl<TopLevelDialog>::showSummary(SyncResult finalStatus, const SharedRef<const ErrorLog>& log)
+void SyncProgressDialogImpl<TopLevelDialog>::showSummary(SyncResult resultStatus, const SharedRef<const ErrorLog>& log)
{
assert(syncStat_);
//at the LATEST(!) to prevent access to currentStatusHandler
@@ -1315,28 +1326,27 @@ void SyncProgressDialogImpl<TopLevelDialog>::showSummary(SyncResult finalStatus,
const wxBitmap statusImage = [&]
{
- switch (finalStatus)
+ switch (resultStatus)
{
case SyncResult::finishedSuccess:
- return getResourceImage(L"status_finished_success");
+ return getResourceImage(L"result_success");
case SyncResult::finishedWarning:
- return getResourceImage(L"status_finished_warnings");
+ return getResourceImage(L"result_warning");
case SyncResult::finishedError:
- return getResourceImage(L"status_finished_errors");
case SyncResult::aborted:
- return getResourceImage(L"status_aborted");
+ return getResourceImage(L"result_error");
}
assert(false);
return wxNullBitmap;
}();
pnl_.m_bitmapStatus->SetBitmap(statusImage);
- pnl_.m_staticTextPhase->SetLabel(getFinalStatusLabel(finalStatus));
+ pnl_.m_staticTextPhase->SetLabel(getResultsStatusLabel(resultStatus));
//pnl_.m_bitmapStatus->SetToolTip(); -> redundant
//show status on Windows 7 taskbar
if (taskbar_.get())
- switch (finalStatus)
+ switch (resultStatus)
{
case SyncResult::finishedSuccess:
case SyncResult::finishedWarning:
@@ -1350,7 +1360,7 @@ void SyncProgressDialogImpl<TopLevelDialog>::showSummary(SyncResult finalStatus,
}
//----------------------------------
- setExternalStatus(getFinalStatusLabel(finalStatus), wxString());
+ setExternalStatus(getResultsStatusLabel(resultStatus), wxString());
//this->EnableCloseButton(true);
@@ -1430,7 +1440,7 @@ void SyncProgressDialogImpl<TopLevelDialog>::showSummary(SyncResult finalStatus,
pnl_.m_panelTimeStats->Layout();
//play (optional) sound notification after sync has completed -> only play when waiting on results dialog, seems to be pointless otherwise!
- switch (finalStatus)
+ switch (resultStatus)
{
case SyncResult::aborted:
break;
@@ -1456,7 +1466,7 @@ void SyncProgressDialogImpl<TopLevelDialog>::showSummary(SyncResult finalStatus,
template <class TopLevelDialog>
-auto SyncProgressDialogImpl<TopLevelDialog>::destroy(bool autoClose, bool restoreParentFrame, SyncResult finalStatus, const SharedRef<const ErrorLog>& log) -> Result
+auto SyncProgressDialogImpl<TopLevelDialog>::destroy(bool autoClose, bool restoreParentFrame, SyncResult resultStatus, const SharedRef<const ErrorLog>& log) -> Result
{
if (autoClose)
{
@@ -1468,7 +1478,7 @@ auto SyncProgressDialogImpl<TopLevelDialog>::destroy(bool autoClose, bool restor
}
else
{
- showSummary(finalStatus, log);
+ showSummary(resultStatus, log);
//wait until user closes the dialog by pressing "okay"
while (!okayPressed_)
@@ -1634,7 +1644,7 @@ SyncProgressDialog* SyncProgressDialog::create(const std::function<void()>& user
bool showProgress,
bool autoCloseDialog,
const std::chrono::system_clock::time_point& syncStartTime,
- const wxString& jobName,
+ const std::vector<std::wstring>& jobNames,
const Zstring& soundFileSyncComplete,
bool ignoreErrors,
size_t automaticRetryCount,
@@ -1645,12 +1655,12 @@ SyncProgressDialog* SyncProgressDialog::create(const std::function<void()>& user
//due to usual "wxBugs", wxDialog on OS X does not float on its parent; wxFrame OTOH does => hack!
//https://groups.google.com/forum/#!topic/wx-users/J5SjjLaBOQE
return new SyncProgressDialogImpl<wxDialog>(wxDEFAULT_DIALOG_STYLE | wxMAXIMIZE_BOX | wxMINIMIZE_BOX | wxRESIZE_BORDER, [&](wxDialog& progDlg) { return parentWindow; },
- userRequestAbort, syncStat, parentWindow, showProgress, autoCloseDialog, syncStartTime, jobName, soundFileSyncComplete, ignoreErrors, automaticRetryCount, postSyncAction);
+ userRequestAbort, syncStat, parentWindow, showProgress, autoCloseDialog, syncStartTime, jobNames, soundFileSyncComplete, ignoreErrors, automaticRetryCount, postSyncAction);
}
else //FFS batch job
{
auto dlg = new SyncProgressDialogImpl<wxFrame>(wxDEFAULT_FRAME_STYLE, [](wxFrame& progDlg) { return &progDlg; },
- userRequestAbort, syncStat, parentWindow, showProgress, autoCloseDialog, syncStartTime, jobName, soundFileSyncComplete, ignoreErrors, automaticRetryCount, postSyncAction);
+ userRequestAbort, syncStat, parentWindow, showProgress, autoCloseDialog, syncStartTime, jobNames, soundFileSyncComplete, ignoreErrors, automaticRetryCount, postSyncAction);
//only top level windows should have an icon:
dlg->SetIcon(getFfsIcon());
diff --git a/FreeFileSync/Source/ui/progress_indicator.h b/FreeFileSync/Source/ui/progress_indicator.h
index af8a079b..5289f5e9 100644
--- a/FreeFileSync/Source/ui/progress_indicator.h
+++ b/FreeFileSync/Source/ui/progress_indicator.h
@@ -63,13 +63,13 @@ struct SyncProgressDialog
bool showProgress,
bool autoCloseDialog,
const std::chrono::system_clock::time_point& syncStartTime,
- const wxString& jobName,
+ const std::vector<std::wstring>& jobNames,
const Zstring& soundFileSyncComplete,
bool ignoreErrors,
size_t automaticRetryCount,
PostSyncAction2 postSyncAction);
struct Result { bool autoCloseDialog; };
- virtual Result destroy(bool autoClose, bool restoreParentFrame, SyncResult finalStatus, const zen::SharedRef<const zen::ErrorLog>& log) = 0;
+ virtual Result destroy(bool autoClose, bool restoreParentFrame, SyncResult resultStatus, const zen::SharedRef<const zen::ErrorLog>& log) = 0;
//---------------------------------------------------------------------------
virtual wxWindow* getWindowIfVisible() = 0; //may be nullptr; don't abuse, use as parent for modal dialogs only!
diff --git a/FreeFileSync/Source/ui/search_grid.cpp b/FreeFileSync/Source/ui/search_grid.cpp
index 5a4bcabb..86ea8bb6 100644
--- a/FreeFileSync/Source/ui/search_grid.cpp
+++ b/FreeFileSync/Source/ui/search_grid.cpp
@@ -26,7 +26,7 @@ public:
MatchFound(const std::wstring& textToFind) : textToFind_(getUnicodeNormalFormWide(textToFind)) {}
bool operator()(const std::wstring& phrase) const
{
- if (isAsciiString(phrase.c_str())) //perf: save Zstring conversion for getUnicodeNormalFormWide() when not needed
+ if (isAsciiString(phrase)) //perf: save Zstring conversion for getUnicodeNormalFormWide() when not needed
return contains(phrase, textToFind_);
else
return contains(getUnicodeNormalFormWide(phrase), textToFind_);
@@ -44,7 +44,7 @@ public:
MatchFound(const std::wstring& textToFind) : textToFind_(makeUpperCopyWide(textToFind)) {}
bool operator()(std::wstring&& phrase) const
{
- if (isAsciiString(phrase.c_str())) //perf: save Zstring conversion for makeUpperCopyWide() when not needed
+ if (isAsciiString(phrase)) //perf: save Zstring conversion for makeUpperCopyWide() when not needed
{
for (wchar_t& c : phrase) c = asciiToUpper(c);
return contains(phrase, textToFind_);
diff --git a/FreeFileSync/Source/ui/small_dlgs.cpp b/FreeFileSync/Source/ui/small_dlgs.cpp
index 867711ae..d7bcebe9 100644
--- a/FreeFileSync/Source/ui/small_dlgs.cpp
+++ b/FreeFileSync/Source/ui/small_dlgs.cpp
@@ -11,6 +11,8 @@
#include <zen/build_info.h>
#include <zen/stl_tools.h>
#include <zen/shell_execute.h>
+#include <zen/file_io.h>
+#include <zen/http.h>
#include <wx/wupdlock.h>
#include <wx/filedlg.h>
#include <wx/clipbrd.h>
@@ -62,7 +64,7 @@ private:
void OnDonate(wxCommandEvent& event) override { wxLaunchDefaultBrowser(L"https://freefilesync.org/donate.php"); }
void OnOpenHomepage(wxCommandEvent& event) override { wxLaunchDefaultBrowser(L"https://freefilesync.org/"); }
void OnOpenForum (wxCommandEvent& event) override { wxLaunchDefaultBrowser(L"https://freefilesync.org/forum/"); }
- void OnSendEmail (wxCommandEvent& event) override { wxLaunchDefaultBrowser(L"mailto:zenju@freefilesync.org"); }
+ void OnSendEmail (wxCommandEvent& event) override { wxLaunchDefaultBrowser(L"mailto:zenju@" L"freefilesync.org"); }
void OnShowGpl (wxCommandEvent& event) override { wxLaunchDefaultBrowser(L"https://www.gnu.org/licenses/gpl-3.0"); }
void onLocalKeyEvent(wxKeyEvent& event);
@@ -109,13 +111,14 @@ AboutDlg::AboutDlg(wxWindow* parent) : AboutDlgGenerated(parent)
}
//------------------------------------
- wxImage forumImage = stackImages(getResourceImage(L"forum").ConvertToImage(),
+ wxImage forumImage = stackImages(getResourceImage(L"ffs_forum").ConvertToImage(),
createImageFromText(L"FreeFileSync Forum", *wxNORMAL_FONT, m_bpButtonForum->GetForegroundColour()),
ImageStackLayout::vertical, ImageStackAlignment::center, fastFromDIP(5));
m_bpButtonForum->SetBitmapLabel(wxBitmap(forumImage));
- setBitmapTextLabel(*m_bpButtonHomepage, getResourceImage(L"homepage").ConvertToImage(), L"FreeFileSync.org");
- setBitmapTextLabel(*m_bpButtonEmail, getResourceImage(L"email" ).ConvertToImage(), L"zenju@freefilesync.org");
+ setBitmapTextLabel(*m_bpButtonHomepage, getResourceImage(L"ffs_homepage").ConvertToImage(), L"FreeFileSync.org");
+ setBitmapTextLabel(*m_bpButtonEmail, getResourceImage(L"ffs_email" ).ConvertToImage(), L"zenju@" L"freefilesync.org");
+ m_bpButtonEmail->SetToolTip(L"mailto:zenju@" L"freefilesync.org");
//------------------------------------
m_bpButtonGpl->SetBitmapLabel(getResourceImage(L"gpl"));
@@ -196,9 +199,9 @@ private:
void OnConnectionSftp (wxCommandEvent& event) override { type_ = CloudType::sftp; updateGui(); }
void OnConnectionFtp (wxCommandEvent& event) override { type_ = CloudType::ftp; updateGui(); }
- void OnAuthPassword(wxCommandEvent& event) override { sftpAuthType_ = SftpAuthType::PASSWORD; updateGui(); }
- void OnAuthKeyfile (wxCommandEvent& event) override { sftpAuthType_ = SftpAuthType::KEY_FILE; updateGui(); }
- void OnAuthAgent (wxCommandEvent& event) override { sftpAuthType_ = SftpAuthType::AGENT; updateGui(); }
+ void OnAuthPassword(wxCommandEvent& event) override { sftpAuthType_ = SftpAuthType::password; updateGui(); }
+ void OnAuthKeyfile (wxCommandEvent& event) override { sftpAuthType_ = SftpAuthType::keyFile; updateGui(); }
+ void OnAuthAgent (wxCommandEvent& event) override { sftpAuthType_ = SftpAuthType::agent; updateGui(); }
void OnSelectKeyfile(wxCommandEvent& event) override;
@@ -220,7 +223,9 @@ private:
};
CloudType type_ = CloudType::gdrive;
- SftpAuthType sftpAuthType_ = SftpAuthType::PASSWORD;
+ const SftpLoginInfo sftpDefault_;
+
+ SftpAuthType sftpAuthType_ = sftpDefault_.authType;
AsyncGuiQueue guiQueue_;
@@ -272,6 +277,7 @@ CloudSetupDlg::CloudSetupDlg(wxWindow* parent, Zstring& folderPathPhrase, size_t
//use spacer to keep dialog height stable, no matter if key file options are visible
bSizerAuthInner->Add(0, m_panelAuth->GetSize().GetHeight());
+ //---------------------------------------------------------
wxArrayString googleUsers;
try
{
@@ -292,9 +298,10 @@ CloudSetupDlg::CloudSetupDlg(wxWindow* parent, Zstring& folderPathPhrase, size_t
m_staticTextGdriveUser->SetLabel(googleUsers[0]);
}
- m_spinCtrlTimeout->SetValue(FtpLoginInfo().timeoutSec);
- assert(FtpLoginInfo().timeoutSec == SftpLoginInfo().timeoutSec); //make sure the default values are in sync
+ m_spinCtrlTimeout->SetValue(sftpDefault_.timeoutSec);
+ assert(sftpDefault_.timeoutSec == FtpLoginInfo().timeoutSec); //make sure the default values are in sync
+ //---------------------------------------------------------
if (acceptsItemPathPhraseGdrive(folderPathPhrase))
{
type_ = CloudType::gdrive;
@@ -324,8 +331,8 @@ CloudSetupDlg::CloudSetupDlg(wxWindow* parent, Zstring& folderPathPhrase, size_t
m_textCtrlPasswordHidden->ChangeValue(utfTo<wxString>(pi.login.password));
m_textCtrlKeyfilePath ->ChangeValue(utfTo<wxString>(pi.login.privateKeyFilePath));
m_textCtrlServerPath ->ChangeValue(utfTo<wxString>(FILE_NAME_SEPARATOR + pi.afsPath.value));
+ m_spinCtrlTimeout ->SetValue(pi.login.timeoutSec);
m_spinCtrlChannelCountSftp->SetValue(pi.login.traverserChannelsPerConnection);
- m_spinCtrlTimeout ->SetValue(pi.login.timeoutSec);
}
else if (acceptsItemPathPhraseFtp(folderPathPhrase))
{
@@ -356,6 +363,7 @@ CloudSetupDlg::CloudSetupDlg(wxWindow* parent, Zstring& folderPathPhrase, size_t
}
else
m_staticTextConnectionCountDescr->SetLabel(_("Recommended range:") + L" [1" + EN_DASH + L"10]"); //no spaces!
+ //---------------------------------------------------------
//set up default view for dialog size calculation
bSizerGdrive->Show(false);
@@ -487,7 +495,7 @@ void CloudSetupDlg::onKeyFileDropped(FileDropEvent& event)
{
m_textCtrlKeyfilePath->ChangeValue(utfTo<wxString>(itemPaths[0]));
- sftpAuthType_ = SftpAuthType::KEY_FILE;
+ sftpAuthType_ = SftpAuthType::keyFile;
updateGui();
}
}
@@ -495,7 +503,7 @@ void CloudSetupDlg::onKeyFileDropped(FileDropEvent& event)
void CloudSetupDlg::OnSelectKeyfile(wxCommandEvent& event)
{
- assert (type_ == CloudType::sftp && sftpAuthType_ == SftpAuthType::KEY_FILE);
+ assert (type_ == CloudType::sftp && sftpAuthType_ == SftpAuthType::keyFile);
wxFileDialog filePicker(this,
wxString(), //message
beforeLast(m_textCtrlKeyfilePath->GetValue(), utfTo<wxString>(FILE_NAME_SEPARATOR), IF_MISSING_RETURN_NONE), //default folder
@@ -523,11 +531,11 @@ void CloudSetupDlg::updateGui()
bSizerFtpEncrypt->Show(type_ == CloudType:: ftp);
bSizerSftpAuth ->Show(type_ == CloudType::sftp);
- m_staticTextKeyfile->Show(type_ == CloudType::sftp && sftpAuthType_ == SftpAuthType::KEY_FILE);
- bSizerKeyFile ->Show(type_ == CloudType::sftp && sftpAuthType_ == SftpAuthType::KEY_FILE);
+ m_staticTextKeyfile->Show(type_ == CloudType::sftp && sftpAuthType_ == SftpAuthType::keyFile);
+ bSizerKeyFile ->Show(type_ == CloudType::sftp && sftpAuthType_ == SftpAuthType::keyFile);
- m_staticTextPassword->Show(type_ == CloudType::ftp || (type_ == CloudType::sftp && sftpAuthType_ != SftpAuthType::AGENT));
- bSizerPassword ->Show(type_ == CloudType::ftp || (type_ == CloudType::sftp && sftpAuthType_ != SftpAuthType::AGENT));
+ m_staticTextPassword->Show(type_ == CloudType::ftp || (type_ == CloudType::sftp && sftpAuthType_ != SftpAuthType::agent));
+ bSizerPassword ->Show(type_ == CloudType::ftp || (type_ == CloudType::sftp && sftpAuthType_ != SftpAuthType::agent));
if (m_staticTextPassword->IsShown())
{
m_textCtrlPasswordVisible->Show( m_checkBoxShowPassword->GetValue());
@@ -549,15 +557,15 @@ void CloudSetupDlg::updateGui()
switch (sftpAuthType_) //*not* owned by GUI controls
{
- case SftpAuthType::PASSWORD:
+ case SftpAuthType::password:
m_radioBtnPassword->SetValue(true);
m_staticTextPassword->SetLabel(_("Password:"));
break;
- case SftpAuthType::KEY_FILE:
+ case SftpAuthType::keyFile:
m_radioBtnKeyfile->SetValue(true);
m_staticTextPassword->SetLabel(_("Key passphrase:"));
break;
- case SftpAuthType::AGENT:
+ case SftpAuthType::agent:
m_radioBtnAgent->SetValue(true);
break;
}
@@ -594,8 +602,8 @@ Zstring CloudSetupDlg::getFolderPathPhrase() const
login.authType = sftpAuthType_;
login.privateKeyFilePath = utfTo<Zstring>(m_textCtrlKeyfilePath->GetValue());
login.password = utfTo<Zstring>((m_checkBoxShowPassword->GetValue() ? m_textCtrlPasswordVisible : m_textCtrlPasswordHidden)->GetValue());
- login.traverserChannelsPerConnection = m_spinCtrlChannelCountSftp->GetValue();
login.timeoutSec = m_spinCtrlTimeout->GetValue();
+ login.traverserChannelsPerConnection = m_spinCtrlChannelCountSftp->GetValue();
auto serverPath = utfTo<Zstring>(m_textCtrlServerPath->GetValue());
//clean up (messy) user input:
@@ -648,7 +656,7 @@ void CloudSetupDlg::OnBrowseCloudFolder(wxCommandEvent& event)
void CloudSetupDlg::OnOkay(wxCommandEvent& event)
{
//------- parameter validation (BEFORE writing output!) -------
- if (type_ == CloudType::sftp && sftpAuthType_ == SftpAuthType::KEY_FILE)
+ if (type_ == CloudType::sftp && sftpAuthType_ == SftpAuthType::keyFile)
if (trimCpy(m_textCtrlKeyfilePath->GetValue()).empty())
{
showNotificationDialog(this, DialogInfoType::info, PopupDialogCfg().setMainInstructions(_("Please enter a file path.")));
@@ -682,7 +690,7 @@ public:
std::span<const FileSystemObject* const> rowsOnLeft,
std::span<const FileSystemObject* const> rowsOnRight,
Zstring& lastUsedPath,
- SharedRef<FolderHistory>& folderHistory,
+ std::vector<Zstring>& folderHistory, size_t folderHistoryMax,
bool& keepRelPaths,
bool& overwriteIfExists);
@@ -694,12 +702,12 @@ private:
void onLocalKeyEvent(wxKeyEvent& event);
std::unique_ptr<FolderSelector> targetFolder; //always bound
- SharedRef<FolderHistory> folderHistory_;
//output-only parameters:
Zstring& lastUsedPathOut_;
bool& keepRelPathsOut_;
bool& overwriteIfExistsOut_;
+ std::vector<Zstring>& folderHistoryOut_;
};
@@ -707,14 +715,14 @@ CopyToDialog::CopyToDialog(wxWindow* parent,
std::span<const FileSystemObject* const> rowsOnLeft,
std::span<const FileSystemObject* const> rowsOnRight,
Zstring& lastUsedPath,
- SharedRef<FolderHistory>& folderHistory,
+ std::vector<Zstring>& folderHistory, size_t folderHistoryMax,
bool& keepRelPaths,
bool& overwriteIfExists) :
CopyToDlgGenerated(parent),
- folderHistory_(folderHistory),
lastUsedPathOut_(lastUsedPath),
keepRelPathsOut_(keepRelPaths),
- overwriteIfExistsOut_(overwriteIfExists)
+ overwriteIfExistsOut_(overwriteIfExists),
+ folderHistoryOut_(folderHistory)
{
setStandardButtonLayout(*bSizerStdButtons, StdButtons().setAffirmative(m_buttonOK).setCancel(m_buttonCancel));
@@ -728,7 +736,7 @@ CopyToDialog::CopyToDialog(wxWindow* parent,
[](const Zstring& folderPathPhrase) { return 1; } /*getDeviceParallelOps*/,
nullptr /*setDeviceParallelOps*/);
- m_targetFolderPath->init(folderHistory_);
+ m_targetFolderPath->setHistory(std::make_shared<HistoryList>(folderHistory, folderHistoryMax));
m_textCtrlFileList->SetMinSize({fastFromDIP(500), fastFromDIP(200)});
@@ -782,13 +790,13 @@ void CopyToDialog::OnOK(wxCommandEvent& event)
m_targetFolderPath->SetFocus();
return;
}
+ m_targetFolderPath->getHistory()->addItem(targetFolder->getPath());
//-------------------------------------------------------------
lastUsedPathOut_ = targetFolder->getPath();
keepRelPathsOut_ = m_checkBoxKeepRelPath->GetValue();
overwriteIfExistsOut_ = m_checkBoxOverwriteIfExists->GetValue();
-
- folderHistory_.ref().addItem(lastUsedPathOut_);
+ folderHistoryOut_ = m_targetFolderPath->getHistory()->getList();
EndModal(ReturnSmallDlg::BUTTON_OKAY);
}
@@ -798,19 +806,12 @@ ReturnSmallDlg::ButtonPressed fff::showCopyToDialog(wxWindow* parent,
std::span<const FileSystemObject* const> rowsOnLeft,
std::span<const FileSystemObject* const> rowsOnRight,
Zstring& lastUsedPath,
- std::vector<Zstring>& folderPathHistory,
- size_t historySizeMax,
+ std::vector<Zstring>& folderHistory, size_t folderHistoryMax,
bool& keepRelPaths,
bool& overwriteIfExists)
{
-
- auto folderHistory = makeSharedRef<FolderHistory>(folderPathHistory, historySizeMax);
-
- CopyToDialog dlg(parent, rowsOnLeft, rowsOnRight, lastUsedPath, folderHistory, keepRelPaths, overwriteIfExists);
- const auto rc = static_cast<ReturnSmallDlg::ButtonPressed>(dlg.ShowModal());
-
- folderPathHistory = folderHistory.ref().getList(); //unconditionally write path history: support manual item deletion + cancel
- return rc;
+ CopyToDialog dlg(parent, rowsOnLeft, rowsOnRight, lastUsedPath, folderHistory, folderHistoryMax, keepRelPaths, overwriteIfExists);
+ return static_cast<ReturnSmallDlg::ButtonPressed>(dlg.ShowModal());
}
//########################################################################################
@@ -989,10 +990,7 @@ SyncConfirmationDlg::SyncConfirmationDlg(wxWindow* parent,
setText(txtControl, valueAsString);
- if (isZeroValue)
- bmpControl.SetBitmap(greyScale(mirrorIfRtl(getResourceImage(bmpName))));
- else
- bmpControl.SetBitmap(mirrorIfRtl(getResourceImage(bmpName)));
+ bmpControl.SetBitmap(greyScaleIfDisabled(mirrorIfRtl(getResourceImage(bmpName)), !isZeroValue));
};
auto setIntValue = [&setValue](wxStaticText& txtControl, int value, wxStaticBitmap& bmpControl, const wchar_t* bmpName)
@@ -1070,8 +1068,9 @@ private:
void OnChangeSoundFilePath(wxCommandEvent& event) override { updateGui(); }
- void OnPlayCompareDone(wxCommandEvent& event) override { wxSound::Play(trimCpy(m_textCtrlSoundPathCompareDone->GetValue()), wxSOUND_ASYNC); }
- void OnPlaySyncDone (wxCommandEvent& event) override { wxSound::Play(trimCpy(m_textCtrlSoundPathSyncDone ->GetValue()), wxSOUND_ASYNC); }
+ void OnPlayCompareDone(wxCommandEvent& event) override { playSoundWithDiagnostics(trimCpy(m_textCtrlSoundPathCompareDone->GetValue())); }
+ void OnPlaySyncDone (wxCommandEvent& event) override { playSoundWithDiagnostics(trimCpy(m_textCtrlSoundPathSyncDone ->GetValue())); }
+ void playSoundWithDiagnostics(const wxString& filePath);
void onResize(wxSizeEvent& event);
void updateGui();
@@ -1225,6 +1224,21 @@ void OptionsDlg::selectSound(wxTextCtrl& txtCtrl)
}
+void OptionsDlg::playSoundWithDiagnostics(const wxString& filePath)
+{
+ try
+ {
+ //wxSOUND_ASYNC: NO failure indication (on Windows)!
+ //wxSound::Play(..., wxSOUND_SYNC) can return false, but does not provide details!
+ //=> check file access manually first:
+ /*std::string stream = */ loadBinContainer<std::string>(utfTo<Zstring>(filePath), nullptr /*notifyUnbufferedIO*/); //throw FileError
+
+ /*bool success = */ wxSound::Play(filePath, wxSOUND_ASYNC);
+ }
+ catch (const FileError& e) { showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); }
+}
+
+
void OptionsDlg::OnDefault(wxCommandEvent& event)
{
m_checkBoxFailSafe ->SetValue(defaultCfg_.failSafeFileCopy);
diff --git a/FreeFileSync/Source/ui/small_dlgs.h b/FreeFileSync/Source/ui/small_dlgs.h
index a37cf51b..ace76582 100644
--- a/FreeFileSync/Source/ui/small_dlgs.h
+++ b/FreeFileSync/Source/ui/small_dlgs.h
@@ -31,8 +31,7 @@ ReturnSmallDlg::ButtonPressed showCopyToDialog(wxWindow* parent,
std::span<const FileSystemObject* const> rowsOnLeft,
std::span<const FileSystemObject* const> rowsOnRight,
Zstring& lastUsedPath,
- std::vector<Zstring>& folderPathHistory,
- size_t historySizeMax,
+ std::vector<Zstring>& folderPathHistory, size_t folderPathHistoryMax,
bool& keepRelPaths,
bool& overwriteIfExists);
diff --git a/FreeFileSync/Source/ui/sync_cfg.cpp b/FreeFileSync/Source/ui/sync_cfg.cpp
index a4ce9809..243fd315 100644
--- a/FreeFileSync/Source/ui/sync_cfg.cpp
+++ b/FreeFileSync/Source/ui/sync_cfg.cpp
@@ -6,6 +6,7 @@
#include "sync_cfg.h"
#include <memory>
+#include <zen/http.h>
#include <wx/wupdlock.h>
#include <wx/valtext.h>
#include <wx+/rtl.h>
@@ -45,7 +46,11 @@ public:
int localPairIndexToShow, bool showMultipleCfgs,
GlobalPairConfig& globalPairCfg,
std::vector<LocalPairConfig>& localPairConfig,
- size_t commandHistItemsMax);
+ std::vector<Zstring>& versioningFolderHistory,
+ std::vector<Zstring>& logFolderHistory,
+ size_t folderHistoryMax,
+ std::vector<Zstring>& emailHistory, size_t emailHistoryMax,
+ std::vector<Zstring>& commandHistory, size_t commandHistoryMax);
private:
void OnOkay (wxCommandEvent& event) override;
@@ -118,7 +123,6 @@ private:
void OnToggleDetectMovedFiles (wxCommandEvent& event) override { directionCfg_.detectMovedFiles = !directionCfg_.detectMovedFiles; updateSyncGui(); } //parameter NOT owned by checkbox!
void OnChanegVersioningStyle (wxCommandEvent& event) override { updateSyncGui(); }
void OnToggleVersioningLimit (wxCommandEvent& event) override { updateSyncGui(); }
- void OnToggleSaveLogfile (wxCommandEvent& event) override { updateMiscGui(); }
void OnSyncTwoWayDouble(wxMouseEvent& event) override;
void OnSyncMirrorDouble(wxMouseEvent& event) override;
@@ -132,12 +136,17 @@ private:
void OnDifferent (wxCommandEvent& event) override;
void OnConflict (wxCommandEvent& event) override;
+ void OnHelpDetectMovedFiles(wxHyperlinkEvent& event) override { displayHelpEntry(L"synchronization-settings", this); }
+ void OnHelpVersioning (wxHyperlinkEvent& event) override { displayHelpEntry(L"versioning", this); }
+
void OnDeletionPermanent (wxCommandEvent& event) override { handleDeletion_ = DeletionPolicy::permanent; updateSyncGui(); }
void OnDeletionRecycler (wxCommandEvent& event) override { handleDeletion_ = DeletionPolicy::recycler; updateSyncGui(); }
void OnDeletionVersioning (wxCommandEvent& event) override { handleDeletion_ = DeletionPolicy::versioning; updateSyncGui(); }
- void OnHelpDetectMovedFiles(wxHyperlinkEvent& event) override { displayHelpEntry(L"synchronization-settings", this); }
- void OnHelpVersioning (wxHyperlinkEvent& event) override { displayHelpEntry(L"versioning", this); }
+ void OnToggleMiscOption (wxCommandEvent& event) override { updateMiscGui(); }
+ void OnEmailAlways (wxCommandEvent& event) override { emailNotifyCondition_ = ResultsNotification::always; updateMiscGui(); }
+ void OnEmailErrorWarning(wxCommandEvent& event) override { emailNotifyCondition_ = ResultsNotification::errorWarning; updateMiscGui(); }
+ void OnEmailErrorOnly (wxCommandEvent& event) override { emailNotifyCondition_ = ResultsNotification::errorOnly; updateMiscGui(); }
std::optional<SyncConfig> getSyncConfig() const;
void setSyncConfig(const SyncConfig* syncCfg);
@@ -154,10 +163,12 @@ private:
FolderSelector versioningFolder_;
EnumDescrList<VersioningStyle> enumVersioningStyle_;
- FolderSelector logfileDir_;
+ ResultsNotification emailNotifyCondition_ = ResultsNotification::always;
EnumDescrList<PostSyncCondition> enumPostSyncCondition_;
+ FolderSelector logfileDir_;
+
//-----------------------------------------------------
MiscSyncConfig getMiscSyncOptions() const;
@@ -168,11 +179,15 @@ private:
//-----------------------------------------------------
void selectFolderPairConfig(int newPairIndexToShow);
- bool unselectFolderPairConfig(); //returns false on error: shows message box!
+ bool unselectFolderPairConfig(bool validateParams); //returns false on error: shows message box!
//output-only parameters
GlobalPairConfig& globalPairCfgOut_;
std::vector<LocalPairConfig>& localPairCfgOut_;
+ std::vector<Zstring>& versioningFolderHistoryOut_;
+ std::vector<Zstring>& logFolderHistoryOut_;
+ std::vector<Zstring>& emailHistoryOut_;
+ std::vector<Zstring>& commandHistoryOut_;
//working copy of ALL config parameters: only one folder pair is selected at a time!
GlobalPairConfig globalPairCfg_;
@@ -181,10 +196,8 @@ private:
int selectedPairIndexToShow_ = EMPTY_PAIR_INDEX_SELECTED;
static const int EMPTY_PAIR_INDEX_SELECTED = -2;
+ const bool enableExtraFeatures_;
const bool showMultipleCfgs_;
- const bool perfPanelActive_;
-
- const size_t commandHistItemsMax_;
};
//#################################################################################################################
@@ -228,7 +241,11 @@ ConfigDialog::ConfigDialog(wxWindow* parent,
int localPairIndexToShow, bool showMultipleCfgs,
GlobalPairConfig& globalPairCfg,
std::vector<LocalPairConfig>& localPairConfig,
- size_t commandHistItemsMax) :
+ std::vector<Zstring>& versioningFolderHistory,
+ std::vector<Zstring>& logFolderHistory,
+ size_t folderHistoryMax,
+ std::vector<Zstring>& emailHistory, size_t emailHistoryMax,
+ std::vector<Zstring>& commandHistory, size_t commandHistoryMax) :
ConfigDlgGenerated(parent),
getDeviceParallelOps_([this](const Zstring& folderPathPhrase)
@@ -260,11 +277,14 @@ logfileDir_(this, *m_panelLogfile, *m_buttonSelectLogFolder, *m_bpButtonSelectAl
globalPairCfgOut_(globalPairCfg),
localPairCfgOut_(localPairConfig),
+versioningFolderHistoryOut_(versioningFolderHistory),
+logFolderHistoryOut_(logFolderHistory),
+emailHistoryOut_(emailHistory),
+commandHistoryOut_(commandHistory),
globalPairCfg_(globalPairCfg),
localPairCfg_(localPairConfig),
-showMultipleCfgs_(showMultipleCfgs),
-perfPanelActive_(false),
-commandHistItemsMax_(commandHistItemsMax)
+enableExtraFeatures_(false),
+showMultipleCfgs_(showMultipleCfgs)
{
setStandardButtonLayout(*bSizerStdButtons, StdButtons().setAffirmative(m_buttonOkay).setCancel(m_buttonCancel));
@@ -307,8 +327,8 @@ commandHistItemsMax_(commandHistItemsMax)
m_staticTextCompVarDescription->SetMinSize({fastFromDIP(CFG_DESCRIPTION_WIDTH_DIP), -1});
m_scrolledWindowPerf->SetMinSize({fastFromDIP(220), -1});
- m_bitmapPerf->SetBitmap(perfPanelActive_ ? getResourceImage(L"speed") : greyScale(getResourceImage(L"speed")));
- m_panelPerfHeader ->Enable(perfPanelActive_);
+ m_bitmapPerf->SetBitmap(greyScaleIfDisabled(getResourceImage(L"speed"), enableExtraFeatures_));
+ m_panelPerfHeader->Enable(enableExtraFeatures_);
m_spinCtrlAutoRetryCount->SetMinSize({fastFromDIP(60), -1}); //Hack: set size (why does wxWindow::Size() not work?)
m_spinCtrlAutoRetryDelay->SetMinSize({fastFromDIP(60), -1}); //
@@ -380,7 +400,20 @@ commandHistItemsMax_(commandHistItemsMax)
m_spinCtrlVersionCountMin->SetMinSize({fastFromDIP(60), -1}); //Hack: set size (why does wxWindow::Size() not work?)
m_spinCtrlVersionCountMax->SetMinSize({fastFromDIP(60), -1}); //
- m_staticTextPostSync->SetMinSize({fastFromDIP(180), -1});
+ m_versioningFolderPath->setHistory(std::make_shared<HistoryList>(versioningFolderHistory, folderHistoryMax));
+
+ m_logFolderPath->setHistory(std::make_shared<HistoryList>(logFolderHistory, folderHistoryMax));
+
+ m_comboBoxEmail->SetHint(/*_("Example:") + */ L"john.doe@example.com");
+ m_comboBoxEmail->setHistory(emailHistory, emailHistoryMax);
+
+ m_checkBoxSendEmail ->Enable(enableExtraFeatures_);
+ m_comboBoxEmail ->Enable(enableExtraFeatures_);
+ m_bpButtonEmailAlways ->Enable(enableExtraFeatures_);
+ m_bpButtonEmailErrorWarning ->Enable(enableExtraFeatures_);
+ m_bpButtonEmailErrorOnly ->Enable(enableExtraFeatures_);
+
+ //m_staticTextPostSync->SetMinSize({fastFromDIP(180), -1});
enumPostSyncCondition_.
add(PostSyncCondition::COMPLETION, _("On completion:")).
@@ -389,7 +422,7 @@ commandHistItemsMax_(commandHistItemsMax)
m_comboBoxPostSyncCommand->SetHint(_("Example:") + L" systemctl poweroff");
-
+ m_comboBoxPostSyncCommand->setHistory(commandHistory, commandHistoryMax);
//-----------------------------------------------------
//enable dialog-specific key events
@@ -416,13 +449,13 @@ commandHistItemsMax_(commandHistItemsMax)
//temporarily set main config as reference for window height calculations:
globalPairCfg_ = GlobalPairConfig();
- globalPairCfg_.syncCfg.directionCfg.var = DirectionConfig::MIRROR; //
- globalPairCfg_.syncCfg.handleDeletion = DeletionPolicy::versioning; //
- globalPairCfg_.syncCfg.versioningFolderPhrase = Zstr("dummy"); //set tentatively for sync dir height calculation below
+ globalPairCfg_.syncCfg.directionCfg.var = DirectionConfig::MIRROR; //
+ globalPairCfg_.syncCfg.handleDeletion = DeletionPolicy::versioning; //
+ globalPairCfg_.syncCfg.versioningFolderPhrase = Zstr("dummy"); //set tentatively for sync dir height calculation below
globalPairCfg_.syncCfg.versioningStyle = VersioningStyle::timestampFile; //
- globalPairCfg_.syncCfg.versionMaxAgeDays = 30; //
- globalPairCfg_.miscCfg.altLogFolderPathPhrase = Zstr("dummy"); //
-
+ globalPairCfg_.syncCfg.versionMaxAgeDays = 30; //
+ globalPairCfg_.miscCfg.altLogFolderPathPhrase = Zstr("dummy"); //
+ globalPairCfg_.miscCfg.emailNotifyAddress = Zstr("dummy"); //
selectFolderPairConfig(-1);
@@ -434,7 +467,7 @@ commandHistItemsMax_(commandHistItemsMax)
bSizerSyncDirHolder ->SetMinSize(-1, bSizerSyncDirections ->GetSize().y);
bSizerVersioningHolder->SetMinSize(-1, bSizerVersioningHolder->GetSize().y);
- unselectFolderPairConfig();
+ unselectFolderPairConfig(false /*validateParams*/);
globalPairCfg_ = globalPairCfg; //restore proper value
//set actual sync config
@@ -527,7 +560,7 @@ void ConfigDialog::OnSelectFolderPair(wxCommandEvent& event)
//m_listBoxFolderPair has no parameter ownership! => selectedPairIndexToShow has!
- if (!unselectFolderPairConfig())
+ if (!unselectFolderPairConfig(true /*validateParams*/))
{
//restore old selection:
m_listBoxFolderPair->SetSelection(selectedPairIndexToShow_ + 1);
@@ -615,14 +648,6 @@ void ConfigDialog::updateCompGui()
m_notebook->SetPageImage(static_cast<size_t>(SyncConfigPanel::COMPARISON),
static_cast<int>(compOptionsEnabled ? ConfigTypeImage::COMPARISON : ConfigTypeImage::COMPARISON_GREY));
- auto setBitmap = [&](wxStaticBitmap& bmpCtrl, const wxBitmap& bmp)
- {
- if (compOptionsEnabled) //help wxWidgets a little to render inactive config state (needed on Windows, NOT on Linux!)
- bmpCtrl.SetBitmap(bmp);
- else
- bmpCtrl.SetBitmap(greyScale(bmp));
- };
-
//update toggle buttons -> they have no parameter-ownership at all!
m_toggleBtnByTimeSize->SetValue(false);
m_toggleBtnBySize ->SetValue(false);
@@ -645,13 +670,14 @@ void ConfigDialog::updateCompGui()
switch (localCmpVar_) //unconditionally update image, including "local options off"
{
case CompareVariant::timeSize:
- setBitmap(*m_bitmapCompVariant, getResourceImage(L"cmp_file_time"));
+ //help wxWidgets a little to render inactive config state (needed on Windows, NOT on Linux!)
+ m_bitmapCompVariant->SetBitmap(greyScaleIfDisabled(getResourceImage(L"cmp_file_time"), compOptionsEnabled));
break;
case CompareVariant::content:
- setBitmap(*m_bitmapCompVariant, getResourceImage(L"cmp_file_content"));
+ m_bitmapCompVariant->SetBitmap(greyScaleIfDisabled(getResourceImage(L"cmp_file_content"), compOptionsEnabled));
break;
case CompareVariant::size:
- setBitmap(*m_bitmapCompVariant, getResourceImage(L"cmp_file_size"));
+ m_bitmapCompVariant->SetBitmap(greyScaleIfDisabled(getResourceImage(L"cmp_file_size"), compOptionsEnabled));
break;
}
@@ -720,17 +746,10 @@ void ConfigDialog::updateFilterGui()
m_notebook->SetPageImage(static_cast<size_t>(SyncConfigPanel::FILTER),
static_cast<int>(!isNullFilter(activeCfg) ? ConfigTypeImage::FILTER: ConfigTypeImage::FILTER_GREY));
- auto setStatusBitmap = [&](wxStaticBitmap& staticBmp, const wxString& bmpName, bool active)
- {
- if (active)
- staticBmp.SetBitmap(getResourceImage(bmpName));
- else
- staticBmp.SetBitmap(greyScale(getResourceImage(bmpName)));
- };
- setStatusBitmap(*m_bitmapInclude, L"filter_include", !NameFilter::isNull(activeCfg.includeFilter, FilterConfig().excludeFilter));
- setStatusBitmap(*m_bitmapExclude, L"filter_exclude", !NameFilter::isNull(FilterConfig().includeFilter, activeCfg.excludeFilter));
- setStatusBitmap(*m_bitmapFilterDate, L"cmp_file_time", activeCfg.unitTimeSpan != UnitTime::NONE);
- setStatusBitmap(*m_bitmapFilterSize, L"cmp_file_size", activeCfg.unitSizeMin != UnitSize::NONE || activeCfg.unitSizeMax != UnitSize::NONE);
+ m_bitmapInclude ->SetBitmap(greyScaleIfDisabled(getResourceImage(L"filter_include"), !NameFilter::isNull(activeCfg.includeFilter, FilterConfig().excludeFilter)));
+ m_bitmapExclude ->SetBitmap(greyScaleIfDisabled(getResourceImage(L"filter_exclude"), !NameFilter::isNull(FilterConfig().includeFilter, activeCfg.excludeFilter)));
+ m_bitmapFilterDate->SetBitmap(greyScaleIfDisabled(getResourceImage(L"cmp_file_time"), activeCfg.unitTimeSpan != UnitTime::NONE));
+ m_bitmapFilterSize->SetBitmap(greyScaleIfDisabled(getResourceImage(L"cmp_file_size"), activeCfg.unitSizeMin != UnitSize::NONE || activeCfg.unitSizeMax != UnitSize::NONE));
m_spinCtrlTimespan->Enable(activeCfg.unitTimeSpan == UnitTime::LAST_X_DAYS);
m_spinCtrlMinSize ->Enable(activeCfg.unitSizeMin != UnitSize::NONE);
@@ -1007,20 +1026,12 @@ void ConfigDialog::updateSyncGui()
m_checkBoxDetectMove->Enable(detectMovedFilesSelectable(directionCfg_));
m_checkBoxDetectMove->SetValue(detectMovedFilesEnabled(directionCfg_)); //parameter NOT owned by checkbox!
- auto setBitmap = [&](wxStaticBitmap& bmpCtrl, const wxBitmap& bmp)
- {
- if (syncOptionsEnabled) //help wxWidgets a little to render inactive config state (needed on Windows, NOT on Linux!)
- bmpCtrl.SetBitmap(bmp);
- else
- bmpCtrl.SetBitmap(greyScale(bmp));
- };
-
//display only relevant sync options
bSizerDatabase ->Show(directionCfg_.var == DirectionConfig::TWO_WAY);
bSizerSyncDirections->Show(directionCfg_.var != DirectionConfig::TWO_WAY);
if (directionCfg_.var == DirectionConfig::TWO_WAY)
- setBitmap(*m_bitmapDatabase, getResourceImage(L"database"));
+ m_bitmapDatabase->SetBitmap(greyScaleIfDisabled(getResourceImage(L"database"), syncOptionsEnabled));
else
{
const CompareVariant activeCmpVar = m_checkBoxUseLocalCmpOptions->GetValue() ? localCmpVar_ : globalPairCfg_.cmpCfg.compareVar;
@@ -1082,15 +1093,15 @@ void ConfigDialog::updateSyncGui()
switch (handleDeletion_) //unconditionally update image, including "local options off"
{
case DeletionPolicy::recycler:
- setBitmap(*m_bitmapDeletionType, getResourceImage(L"delete_recycler"));
+ m_bitmapDeletionType->SetBitmap(greyScaleIfDisabled(getResourceImage(L"delete_recycler"), syncOptionsEnabled));
setText(*m_staticTextDeletionTypeDescription, _("Retain deleted and overwritten files in the recycle bin"));
break;
case DeletionPolicy::permanent:
- setBitmap(*m_bitmapDeletionType, getResourceImage(L"delete_permanently"));
+ m_bitmapDeletionType->SetBitmap(greyScaleIfDisabled(getResourceImage(L"delete_permanently"), syncOptionsEnabled));
setText(*m_staticTextDeletionTypeDescription, _("Delete and overwrite files permanently"));
break;
case DeletionPolicy::versioning:
- setBitmap(*m_bitmapVersioning, getResourceImage(L"delete_versioning"));
+ m_bitmapVersioning->SetBitmap(greyScaleIfDisabled(getResourceImage(L"delete_versioning"), syncOptionsEnabled));
break;
}
//m_staticTextDeletionTypeDescription->Wrap(fastFromDIP(200)); //needs to be reapplied after SetLabel()
@@ -1153,6 +1164,7 @@ void ConfigDialog::updateSyncGui()
}
m_panelSyncSettings->Layout();
+
//Refresh(); //removes a few artifacts when toggling display of versioning folder
}
@@ -1179,13 +1191,20 @@ MiscSyncConfig ConfigDialog::getMiscSyncOptions() const
miscCfg.automaticRetryCount = m_checkBoxAutoRetry ->GetValue() ? m_spinCtrlAutoRetryCount->GetValue() : 0;
miscCfg.automaticRetryDelay = std::chrono::seconds(m_spinCtrlAutoRetryDelay->GetValue());
//----------------------------------------------------------------------------
- miscCfg.altLogFolderPathPhrase = m_checkBoxSaveLog->GetValue() ? utfTo<Zstring>(logfileDir_.getPath()) : Zstring();
-
miscCfg.postSyncCommand = m_comboBoxPostSyncCommand->getValue();
- miscCfg.postSyncCondition = getEnumVal(enumPostSyncCondition_, *m_choicePostSyncCondition),
- miscCfg.commandHistory = m_comboBoxPostSyncCommand->getHistory();
+ miscCfg.postSyncCondition = getEnumVal(enumPostSyncCondition_, *m_choicePostSyncCondition);
+ //----------------------------------------------------------------------------
+ Zstring altLogPathPhrase = logfileDir_.getPath();
+ if (altLogPathPhrase.empty())
+ altLogPathPhrase = Zstr(" "); //trigger error message on dialog close
+ miscCfg.altLogFolderPathPhrase = m_checkBoxOverrideLogPath->GetValue() ? altLogPathPhrase : Zstring();
+ //----------------------------------------------------------------------------
+ Zstring emailAddress = m_comboBoxEmail->getValue();
+ if (emailAddress.empty())
+ emailAddress = Zstr(" "); //trigger error message on dialog close
+ miscCfg.emailNotifyAddress = m_checkBoxSendEmail->GetValue() ? emailAddress : Zstring();
+ miscCfg.emailNotifyCondition = emailNotifyCondition_;
//----------------------------------------------------------------------------
-
return miscCfg;
}
@@ -1206,11 +1225,11 @@ void ConfigDialog::setMiscSyncOptions(const MiscSyncConfig& miscCfg)
{
wxSpinCtrl* spinCtrlParallelOps = new wxSpinCtrl(m_scrolledWindowPerf, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 1, 2000000000, 1);
spinCtrlParallelOps->SetMinSize({fastFromDIP(60), -1}); //Hack: set size (why does wxWindow::Size() not work?)
- spinCtrlParallelOps->Enable(perfPanelActive_);
+ spinCtrlParallelOps->Enable(enableExtraFeatures_);
fgSizerPerf->Add(spinCtrlParallelOps, 0, wxALIGN_CENTER_VERTICAL);
wxStaticText* staticTextDevice = new wxStaticText(m_scrolledWindowPerf, wxID_ANY, wxEmptyString);
- staticTextDevice->Enable(perfPanelActive_);
+ staticTextDevice->Enable(enableExtraFeatures_);
fgSizerPerf->Add(staticTextDevice, 0, wxALIGN_CENTER_VERTICAL);
}
else
@@ -1228,7 +1247,7 @@ void ConfigDialog::setMiscSyncOptions(const MiscSyncConfig& miscCfg)
staticTextDevice->SetLabel(AFS::getDisplayPath(AbstractPath(afsDevice, AfsPath())));
++i;
}
- m_staticTextPerfParallelOps->Enable(perfPanelActive_ && !devicesForEdit_.empty());
+ m_staticTextPerfParallelOps->Enable(enableExtraFeatures_ && !devicesForEdit_.empty());
m_panelComparisonSettings->Layout(); //*after* setting text labels
@@ -1238,15 +1257,17 @@ void ConfigDialog::setMiscSyncOptions(const MiscSyncConfig& miscCfg)
m_spinCtrlAutoRetryCount->SetValue(std::max<size_t>(miscCfg.automaticRetryCount, 0));
m_spinCtrlAutoRetryDelay->SetValue(miscCfg.automaticRetryDelay.count());
//----------------------------------------------------------------------------
- m_checkBoxSaveLog->SetValue(!trimCpy(miscCfg.altLogFolderPathPhrase).empty());
- logfileDir_.setPath(m_checkBoxSaveLog->GetValue() ? miscCfg.altLogFolderPathPhrase : getDefaultLogFolderPath());
- //can't use logfileDir_.setBackgroundText(): no text shown when control is disabled!
-
m_comboBoxPostSyncCommand->setValue(miscCfg.postSyncCommand);
- setEnumVal(enumPostSyncCondition_, *m_choicePostSyncCondition, miscCfg.postSyncCondition),
- m_comboBoxPostSyncCommand->setHistory(miscCfg.commandHistory, commandHistItemsMax_);
+ setEnumVal(enumPostSyncCondition_, *m_choicePostSyncCondition, miscCfg.postSyncCondition);
+ //----------------------------------------------------------------------------
+ m_checkBoxOverrideLogPath->SetValue(!trimCpy(miscCfg.altLogFolderPathPhrase).empty());
+ logfileDir_.setPath(m_checkBoxOverrideLogPath->GetValue() ? miscCfg.altLogFolderPathPhrase : getDefaultLogFolderPath());
+ //can't use logfileDir_.setBackgroundText(): no text shown when control is disabled!
+ //----------------------------------------------------------------------------
+ m_checkBoxSendEmail->SetValue(!trimCpy(miscCfg.emailNotifyAddress).empty());
+ m_comboBoxEmail->setValue(miscCfg.emailNotifyAddress);
+ emailNotifyCondition_ = miscCfg.emailNotifyCondition;
//----------------------------------------------------------------------------
-
updateMiscGui();
}
@@ -1255,20 +1276,65 @@ void ConfigDialog::updateMiscGui()
{
const MiscSyncConfig miscCfg = getMiscSyncOptions();
- m_bitmapIgnoreErrors->SetBitmap(miscCfg.ignoreErrors ? getResourceImage(L"error_ignore_active") : greyScale(getResourceImage(L"error_ignore_inactive")));
- m_bitmapRetryErrors ->SetBitmap(miscCfg.automaticRetryCount > 0 ? getResourceImage(L"error_retry") : greyScale(getResourceImage(L"error_retry")));
+ m_bitmapIgnoreErrors->SetBitmap(greyScaleIfDisabled(getResourceImage(L"error_ignore_active"), miscCfg.ignoreErrors));
+ m_bitmapRetryErrors ->SetBitmap(greyScaleIfDisabled(getResourceImage(L"error_retry"), miscCfg.automaticRetryCount > 0 ));
fgSizerAutoRetry->Show(miscCfg.automaticRetryCount > 0);
m_panelComparisonSettings->Layout(); //showing "retry count" can affect bSizerPerformance!
//----------------------------------------------------------------------------
+ const bool sendEmailEnabled = m_checkBoxSendEmail->GetValue();
+ m_bitmapEmail->SetBitmap(shrinkImage(greyScaleIfDisabled(getResourceImage(L"email"), sendEmailEnabled).ConvertToImage(), fastFromDIP(24)));
+ m_comboBoxEmail->Show(sendEmailEnabled);
+
+ auto updateButton = [successIcon = getResourceImage(L"msg_success_sicon").ConvertToImage(),
+ warningIcon = getResourceImage(L"msg_warning_sicon").ConvertToImage(),
+ errorIcon = getResourceImage(L"msg_error_sicon" ).ConvertToImage(),
+ sendEmailEnabled, this] (wxBitmapButton& button, ResultsNotification notifyCondition)
+ {
+ button.Show(sendEmailEnabled);
+ if (sendEmailEnabled)
+ {
+ wxString tooltip = _("Error");
+ wxImage label = errorIcon;
- m_bitmapLogFile->SetBitmap(shrinkImage(getResourceImage(L"log_file").ConvertToImage(), fastFromDIP(20)));
- m_logFolderPath ->Enable(m_checkBoxSaveLog->GetValue()); //
- m_buttonSelectLogFolder ->Show(m_checkBoxSaveLog->GetValue()); //enabled status is *not* directly dependent from resolved config! (but transitively)
- m_bpButtonSelectAltLogFolder->Show(m_checkBoxSaveLog->GetValue()); //
+ if (notifyCondition == ResultsNotification::always ||
+ notifyCondition == ResultsNotification::errorWarning)
+ {
+ tooltip += (L" | ") + _("Warning");
+ label = stackImages(label, warningIcon, ImageStackLayout::horizontal, ImageStackAlignment::center);
+ }
+ else
+ label.Resize({label.GetWidth() + warningIcon.GetWidth(), label.GetHeight()}, {});
+
+ if (notifyCondition == ResultsNotification::always)
+ {
+ tooltip += (L" | ") + _("Success");
+ label = stackImages(label, successIcon, ImageStackLayout::horizontal, ImageStackAlignment::center);
+ }
+ else
+ label.Resize({label.GetWidth() + successIcon.GetWidth(), label.GetHeight()}, {});
+
+ button.SetToolTip(tooltip);
+ button.SetBitmapLabel(notifyCondition == emailNotifyCondition_ && sendEmailEnabled ? label : greyScale(label));
+ button.SetBitmapDisabled(greyScale(label)); //fix wxWidgets' all-too-clever multi-state!
+ //=> the disabled bitmap is generated during first SetBitmapLabel() call but never updated again by wxWidgets!
+ }
+ };
+ updateButton(*m_bpButtonEmailAlways, ResultsNotification::always);
+ updateButton(*m_bpButtonEmailErrorWarning, ResultsNotification::errorWarning);
+ updateButton(*m_bpButtonEmailErrorOnly, ResultsNotification::errorOnly);
+
+ m_staticTextPerfDeRequired2->Show(!enableExtraFeatures_); //required after each bSizerSyncMisc->Show()
+
+ //----------------------------------------------------------------------------
+ m_bitmapLogFile->SetBitmap(shrinkImage(greyScaleIfDisabled(getResourceImage(L"log_file"), m_checkBoxOverrideLogPath->GetValue()).ConvertToImage(), fastFromDIP(20)));
+ m_logFolderPath ->Enable(m_checkBoxOverrideLogPath->GetValue()); //
+ m_buttonSelectLogFolder ->Show(m_checkBoxOverrideLogPath->GetValue()); //enabled status can't be derived from resolved config!
+ m_bpButtonSelectAltLogFolder->Show(m_checkBoxOverrideLogPath->GetValue()); //
m_panelSyncSettings->Layout(); //after showing/hiding m_buttonSelectLogFolder
+ m_panelLogfile->Refresh(); //removes a few artifacts when toggling email notifications
}
@@ -1300,8 +1366,8 @@ void ConfigDialog::selectFolderPairConfig(int newPairIndexToShow)
bSizerCompMisc ->Show(mainConfigSelected);
bSizerSyncMisc ->Show(mainConfigSelected);
- if (mainConfigSelected) m_staticTextPerfDeRequired->Show(!perfPanelActive_); //keep after bSizerPerformance->Show()
- if (mainConfigSelected) m_staticlinePerfDeRequired->Show(!perfPanelActive_); //
+ if (mainConfigSelected) m_staticTextPerfDeRequired->Show(!enableExtraFeatures_); //keep after bSizerPerformance->Show()
+ if (mainConfigSelected) m_staticlinePerfDeRequired->Show(!enableExtraFeatures_); //
m_panelCompSettingsTab ->Layout(); //fix comp panel glitch on Win 7 125% font size + perf panel
m_panelFilterSettingsTab->Layout();
@@ -1345,53 +1411,82 @@ void ConfigDialog::selectFolderPairConfig(int newPairIndexToShow)
}
-bool ConfigDialog::unselectFolderPairConfig()
+bool ConfigDialog::unselectFolderPairConfig(bool validateParams)
{
assert(selectedPairIndexToShow_ == -1 || makeUnsigned(selectedPairIndexToShow_) < localPairCfg_.size());
std::optional<CompConfig> compCfg = getCompConfig();
std::optional<SyncConfig> syncCfg = getSyncConfig();
- FilterConfig filterCfg = getFilterConfig();
-
- //------- parameter validation (BEFORE writing output!) -------
+ FilterConfig filterCfg = getFilterConfig();
- //parameter correction: include filter must not be empty!
- if (trimCpy(filterCfg.includeFilter).empty())
- filterCfg.includeFilter = FilterConfig().includeFilter; //no need to show error message, just correct user input
+ std::optional<MiscSyncConfig> miscCfg;
+ if (selectedPairIndexToShow_ < 0)
+ miscCfg = getMiscSyncOptions();
- if (syncCfg && syncCfg->handleDeletion == DeletionPolicy::versioning)
+ //------- parameter validation (BEFORE writing output!) -------
+ if (validateParams)
{
- if (AFS::isNullPath(createAbstractPath(syncCfg->versioningFolderPhrase)))
+ //parameter correction: include filter must not be empty!
+ if (trimCpy(filterCfg.includeFilter).empty())
+ filterCfg.includeFilter = FilterConfig().includeFilter; //no need to show error message, just correct user input
+
+ if (syncCfg && syncCfg->handleDeletion == DeletionPolicy::versioning)
{
- m_notebook->ChangeSelection(static_cast<size_t>(SyncConfigPanel::SYNC));
- showNotificationDialog(this, DialogInfoType::info, PopupDialogCfg().setMainInstructions(_("Please enter a target folder for versioning.")));
- //don't show error icon to follow "Windows' encouraging tone"
- m_versioningFolderPath->SetFocus();
- return false;
+ if (AFS::isNullPath(createAbstractPath(syncCfg->versioningFolderPhrase)))
+ {
+ m_notebook->ChangeSelection(static_cast<size_t>(SyncConfigPanel::SYNC));
+ showNotificationDialog(this, DialogInfoType::info, PopupDialogCfg().setMainInstructions(_("Please enter a target folder for versioning.")));
+ //don't show error icon to follow "Windows' encouraging tone"
+ m_versioningFolderPath->SetFocus();
+ return false;
+ }
+ m_versioningFolderPath->getHistory()->addItem(syncCfg->versioningFolderPhrase);
+
+ if (syncCfg->versioningStyle != VersioningStyle::replace &&
+ syncCfg->versionMaxAgeDays > 0 &&
+ syncCfg->versionCountMin > 0 &&
+ syncCfg->versionCountMax > 0 &&
+ syncCfg->versionCountMin >= syncCfg->versionCountMax)
+ {
+ m_notebook->ChangeSelection(static_cast<size_t>(SyncConfigPanel::SYNC));
+ showNotificationDialog(this, DialogInfoType::info, PopupDialogCfg().setMainInstructions(_("Minimum version count must be smaller than maximum count.")));
+ m_spinCtrlVersionCountMin->SetFocus();
+ return false;
+ }
}
- if (syncCfg->versioningStyle != VersioningStyle::replace &&
- syncCfg->versionMaxAgeDays > 0 &&
- syncCfg->versionCountMin > 0 &&
- syncCfg->versionCountMax > 0 &&
- syncCfg->versionCountMin >= syncCfg->versionCountMax)
+ if (selectedPairIndexToShow_ < 0)
{
- m_notebook->ChangeSelection(static_cast<size_t>(SyncConfigPanel::SYNC));
- showNotificationDialog(this, DialogInfoType::info, PopupDialogCfg().setMainInstructions(_("Minimum version count must be smaller than maximum count.")));
- m_spinCtrlVersionCountMin->SetFocus();
- return false;
+ if (!miscCfg->altLogFolderPathPhrase.empty() &&
+ trimCpy(miscCfg->altLogFolderPathPhrase).empty())
+ {
+ m_notebook->ChangeSelection(static_cast<size_t>(SyncConfigPanel::SYNC));
+ showNotificationDialog(this, DialogInfoType::info, PopupDialogCfg().setMainInstructions(_("Please enter a folder path.")));
+ m_logFolderPath->SetFocus();
+ return false;
+ }
+ m_logFolderPath->getHistory()->addItem(miscCfg->altLogFolderPathPhrase);
+
+ if (!miscCfg->emailNotifyAddress.empty() &&
+ !isValidEmail(trimCpy(miscCfg->emailNotifyAddress)))
+ {
+ m_notebook->ChangeSelection(static_cast<size_t>(SyncConfigPanel::SYNC));
+ showNotificationDialog(this, DialogInfoType::info, PopupDialogCfg().setMainInstructions(_("Please enter a valid email address.")));
+ m_comboBoxEmail->SetFocus();
+ return false;
+ }
+ m_comboBoxEmail ->addItemHistory();
+ m_comboBoxPostSyncCommand->addItemHistory();
}
}
//-------------------------------------------------------------
- m_comboBoxPostSyncCommand->addItemHistory(); //commit current "on completion" history item
-
if (selectedPairIndexToShow_ < 0)
{
globalPairCfg_.cmpCfg = *compCfg;
globalPairCfg_.syncCfg = *syncCfg;
globalPairCfg_.filter = filterCfg;
- globalPairCfg_.miscCfg = getMiscSyncOptions();
+ globalPairCfg_.miscCfg = *miscCfg;
}
else
{
@@ -1408,12 +1503,18 @@ bool ConfigDialog::unselectFolderPairConfig()
void ConfigDialog::OnOkay(wxCommandEvent& event)
{
- if (!unselectFolderPairConfig())
+ if (!unselectFolderPairConfig(true /*validateParams*/))
return;
globalPairCfgOut_ = globalPairCfg_;
localPairCfgOut_ = localPairCfg_;
+ versioningFolderHistoryOut_ = m_versioningFolderPath->getHistory()->getList();
+ logFolderHistoryOut_ = m_logFolderPath ->getHistory()->getList();
+
+ commandHistoryOut_ = m_comboBoxPostSyncCommand->getHistory();
+ emailHistoryOut_ = m_comboBoxEmail->getHistory();
+
EndModal(ReturnSyncConfig::BUTTON_OKAY);
}
}
@@ -1427,7 +1528,11 @@ ReturnSyncConfig::ButtonPressed fff::showSyncConfigDlg(wxWindow* parent,
GlobalPairConfig& globalPairCfg,
std::vector<LocalPairConfig>& localPairConfig,
- size_t commandHistItemsMax)
+ std::vector<Zstring>& versioningFolderHistory,
+ std::vector<Zstring>& logFolderHistory,
+ size_t folderHistoryMax,
+ std::vector<Zstring>& emailHistory, size_t emailHistoryMax,
+ std::vector<Zstring>& commandHistory, size_t commandHistoryMax)
{
ConfigDialog syncDlg(parent,
@@ -1435,6 +1540,12 @@ ReturnSyncConfig::ButtonPressed fff::showSyncConfigDlg(wxWindow* parent,
localPairIndexToShow, showMultipleCfgs,
globalPairCfg,
localPairConfig,
- commandHistItemsMax);
+ versioningFolderHistory,
+ logFolderHistory,
+ folderHistoryMax,
+ emailHistory,
+ emailHistoryMax,
+ commandHistory,
+ commandHistoryMax);
return static_cast<ReturnSyncConfig::ButtonPressed>(syncDlg.ShowModal());
}
diff --git a/FreeFileSync/Source/ui/sync_cfg.h b/FreeFileSync/Source/ui/sync_cfg.h
index 34c4acf2..568b1461 100644
--- a/FreeFileSync/Source/ui/sync_cfg.h
+++ b/FreeFileSync/Source/ui/sync_cfg.h
@@ -35,10 +35,14 @@ struct MiscSyncConfig
bool ignoreErrors = false;
size_t automaticRetryCount = 0;
std::chrono::seconds automaticRetryDelay{0};
- Zstring altLogFolderPathPhrase;
+
Zstring postSyncCommand;
PostSyncCondition postSyncCondition = PostSyncCondition::COMPLETION;
- std::vector<Zstring> commandHistory;
+
+ Zstring altLogFolderPathPhrase;
+
+ Zstring emailNotifyAddress;
+ ResultsNotification emailNotifyCondition = ResultsNotification::always;
};
struct GlobalPairConfig
@@ -57,8 +61,11 @@ ReturnSyncConfig::ButtonPressed showSyncConfigDlg(wxWindow* parent,
GlobalPairConfig& globalPairCfg,
std::vector<LocalPairConfig>& localPairConfig,
-
- size_t commandHistoryMax);
+ std::vector<Zstring>& versioningFolderHistory,
+ std::vector<Zstring>& logFolderHistory,
+ size_t folderHistoryMax,
+ std::vector<Zstring>& emailHistory, size_t emailHistoryMax,
+ std::vector<Zstring>& commandHistory, size_t commandHistoryMax);
}
#endif //SYNC_CFG_H_31289470134253425
diff --git a/FreeFileSync/Source/ui/version_check.cpp b/FreeFileSync/Source/ui/version_check.cpp
index 8728c136..49a1c321 100644
--- a/FreeFileSync/Source/ui/version_check.cpp
+++ b/FreeFileSync/Source/ui/version_check.cpp
@@ -124,7 +124,7 @@ std::vector<std::pair<std::string, std::string>> geHttpPostParameters()
const wxLinuxDistributionInfo distribInfo = wxGetLinuxDistributionInfo();
assert(contains(distribInfo.Release, L'.'));
- std::vector<wxString> digits = split<wxString>(distribInfo.Release, L'.', SplitType::ALLOW_EMPTY); //e.g. "15.04"
+ std::vector<wxString> digits = split<wxString>(distribInfo.Release, L'.', SplitType::ALLOW_EMPTY); //e.g. "7.7.1908"
digits.resize(2);
//distribInfo.Id //e.g. "Ubuntu"
@@ -182,9 +182,9 @@ void showUpdateAvailableDialog(wxWindow* parent, const std::string& onlineVersio
std::string getOnlineVersion(const std::vector<std::pair<std::string, std::string>>& postParams) //throw SysError
{
- const std::string buffer = sendHttpPost(Zstr("https://api.freefilesync.org/latest_version"), postParams,
- ffsUpdateCheckUserAgent, nullptr /*caCertFilePath*/, nullptr /*notifyUnbufferedIO*/).readAll(); //throw SysError
- return trimCpy(buffer);
+ const std::string response = sendHttpPost(Zstr("https://api.freefilesync.org/latest_version"), postParams,
+ ffsUpdateCheckUserAgent, nullptr /*caCertFilePath*/, nullptr /*notifyUnbufferedIO*/).readAll(); //throw SysError
+ return trimCpy(response);
}
diff --git a/FreeFileSync/Source/version/version.h b/FreeFileSync/Source/version/version.h
index af47b93d..933b9f4c 100644
--- a/FreeFileSync/Source/version/version.h
+++ b/FreeFileSync/Source/version/version.h
@@ -3,7 +3,7 @@
namespace fff
{
-const char ffsVersion[] = "10.19"; //internal linkage!
+const char ffsVersion[] = "10.20"; //internal linkage!
const char FFS_VERSION_SEPARATOR = '.';
}
diff --git a/FreeFileSync/Source/afs/libcurl/curl_wrap.h b/libcurl/curl_wrap.h
index e4878743..dca056fc 100644
--- a/FreeFileSync/Source/afs/libcurl/curl_wrap.h
+++ b/libcurl/curl_wrap.h
@@ -8,6 +8,7 @@
#define CURL_WRAP_H_2879058325032785032789645
#include <zen/scope_guard.h>
+#include <zen/sys_error.h>
@@ -21,6 +22,18 @@
namespace zen
{
+struct CurlOption
+{
+ template <class T>
+ CurlOption(CURLoption o, T val) : option(o), value(static_cast<uint64_t>(val)) { static_assert(sizeof(val) <= sizeof(value)); }
+
+ template <class T>
+ CurlOption(CURLoption o, T* val) : option(o), value(reinterpret_cast<uint64_t>(val)) { static_assert(sizeof(val) <= sizeof(value)); }
+
+ CURLoption option = CURLOPT_LASTENTRY;
+ uint64_t value = 0;
+};
+
namespace
{
std::wstring formatCurlStatusCode(CURLcode sc)
@@ -122,12 +135,25 @@ std::wstring formatCurlStatusCode(CURLcode sc)
ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_HTTP2_STREAM);
ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_RECURSIVE_API_CALL);
ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_AUTH_ERROR);
+ ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_HTTP3);
ZEN_CHECK_CASE_FOR_CONSTANT(CURL_LAST);
}
- static_assert(CURL_LAST == CURLE_AUTH_ERROR + 1);
+ static_assert(CURL_LAST == CURLE_HTTP3 + 1);
return replaceCpy<std::wstring>(L"Curl status %x.", L"%x", numberTo<std::wstring>(static_cast<int>(sc)));
}
+
+
+void applyCurlOptions(CURL* easyHandle, const std::vector<CurlOption>& options) //throw SysError
+{
+ for (const CurlOption& opt : options)
+ {
+ const CURLcode rc = ::curl_easy_setopt(easyHandle, opt.option, opt.value);
+ if (rc != CURLE_OK)
+ throw SysError(formatSystemError(L"curl_easy_setopt " + numberTo<std::wstring>(static_cast<int>(opt.option)),
+ formatCurlStatusCode(rc), utfTo<std::wstring>(::curl_easy_strerror(rc))));
+ }
+}
}
}
diff --git a/libcurl/rest.cpp b/libcurl/rest.cpp
new file mode 100644
index 00000000..22583483
--- /dev/null
+++ b/libcurl/rest.cpp
@@ -0,0 +1,185 @@
+// *****************************************************************************
+// * 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 "rest.h"
+#include <zen/system.h>
+#include <zen/http.h>
+
+using namespace zen;
+
+
+HttpSession::HttpSession(const Zstring& server, const Zstring& caCertFilePath, std::chrono::seconds timeOut) : //throw SysError
+ server_(utfTo<std::string>(server)),
+ caCertFilePath_(utfTo<std::string>(caCertFilePath)),
+ timeOutSec_(timeOut) {}
+
+
+HttpSession::~HttpSession()
+{
+ if (easyHandle_)
+ ::curl_easy_cleanup(easyHandle_);
+}
+
+
+HttpSession::Result HttpSession::perform(const std::string& serverRelPath,
+ const std::vector<std::string>& extraHeaders, const std::vector<CurlOption>& extraOptions, //throw SysError
+ const std::function<void (const void* buffer, size_t bytesToWrite)>& writeResponse /*throw X*/, //optional
+ const std::function<size_t( void* buffer, size_t bytesToRead )>& readRequest /*throw X*/) //
+{
+ if (!easyHandle_)
+ {
+ easyHandle_ = ::curl_easy_init();
+ if (!easyHandle_)
+ throw SysError(formatSystemError(L"curl_easy_init", formatCurlStatusCode(CURLE_OUT_OF_MEMORY), std::wstring()));
+ }
+ else
+ ::curl_easy_reset(easyHandle_);
+
+
+ std::vector<CurlOption> options;
+
+ char curlErrorBuf[CURL_ERROR_SIZE] = {};
+ options.emplace_back(CURLOPT_ERRORBUFFER, curlErrorBuf);
+
+ options.emplace_back(CURLOPT_USERAGENT, "FreeFileSync"); //default value; may be overwritten by caller
+
+ //lifetime: keep alive until after curl_easy_setopt() below
+ std::string curlPath = "https://" + server_ + serverRelPath;
+ options.emplace_back(CURLOPT_URL, curlPath.c_str());
+
+ options.emplace_back(CURLOPT_ACCEPT_ENCODING, "gzip"); //won't hurt + used by Google Drive
+
+ options.emplace_back(CURLOPT_NOSIGNAL, 1L); //thread-safety: https://curl.haxx.se/libcurl/c/threadsafe.html
+
+ options.emplace_back(CURLOPT_CONNECTTIMEOUT, timeOutSec_.count());
+
+ //CURLOPT_TIMEOUT: "Since this puts a hard limit for how long time a request is allowed to take, it has limited use in dynamic use cases with varying transfer times."
+ options.emplace_back(CURLOPT_LOW_SPEED_TIME, timeOutSec_.count());
+ options.emplace_back(CURLOPT_LOW_SPEED_LIMIT, 1L); //[bytes], can't use "0" which means "inactive", so use some low number
+
+
+ //libcurl forwards this char-string to OpenSSL as is, which - thank god - accepts UTF8
+ options.emplace_back(CURLOPT_CAINFO, caCertFilePath_.c_str()); //hopefully latest version from https://curl.haxx.se/docs/caextract.html
+ //CURLOPT_SSL_VERIFYPEER => already active by default
+ //CURLOPT_SSL_VERIFYHOST =>
+
+ //---------------------------------------------------
+ std::exception_ptr userCallbackException;
+
+ auto onBytesReceived = [&](const void* buffer, size_t len)
+ {
+ try
+ {
+ writeResponse(buffer, len); //throw X
+ return len;
+ }
+ catch (...)
+ {
+ userCallbackException = std::current_exception();
+ return len + 1; //signal error condition => CURLE_WRITE_ERROR
+ }
+ };
+ using ReadCbType = decltype(onBytesReceived);
+ using ReadCbWrapperType = size_t (*)(const void* buffer, size_t size, size_t nitems, ReadCbType* callbackData); //needed for cdecl function pointer cast
+ ReadCbWrapperType onBytesReceivedWrapper = [](const void* buffer, size_t size, size_t nitems, ReadCbType* callbackData)
+ {
+ return (*callbackData)(buffer, size * nitems); //free this poor little C-API from its shackles and redirect to a proper lambda
+ };
+ //---------------------------------------------------
+ auto getBytesToSend = [&](void* buffer, size_t len) -> size_t
+ {
+ try
+ {
+ //libcurl calls back until 0 bytes are returned (Posix read() semantics), or,
+ //if CURLOPT_INFILESIZE_LARGE was set, after exactly this amount of bytes
+ const size_t bytesRead = readRequest(buffer, len);//throw X; return "bytesToRead" bytes unless end of stream!
+ return bytesRead;
+ }
+ catch (...)
+ {
+ userCallbackException = std::current_exception();
+ return CURL_READFUNC_ABORT; //signal error condition => CURLE_ABORTED_BY_CALLBACK
+ }
+ };
+ using WriteCbType = decltype(getBytesToSend);
+ using WriteCbWrapperType = size_t (*)(void* buffer, size_t size, size_t nitems, WriteCbType* callbackData);
+ WriteCbWrapperType getBytesToSendWrapper = [](void* buffer, size_t size, size_t nitems, WriteCbType* callbackData)
+ {
+ return (*callbackData)(buffer, size * nitems); //free this poor little C-API from its shackles and redirect to a proper lambda
+ };
+ //---------------------------------------------------
+ if (writeResponse)
+ {
+ options.emplace_back(CURLOPT_WRITEDATA, &onBytesReceived);
+ options.emplace_back(CURLOPT_WRITEFUNCTION, onBytesReceivedWrapper);
+ }
+ if (readRequest)
+ {
+ if (std::all_of(extraOptions.begin(), extraOptions.end(), [](const CurlOption& o) { return o.option != CURLOPT_POST; }))
+ options.emplace_back(CURLOPT_UPLOAD, 1L); //issues HTTP PUT
+ options.emplace_back(CURLOPT_READDATA, &getBytesToSend);
+ options.emplace_back(CURLOPT_READFUNCTION, getBytesToSendWrapper);
+ }
+
+ if (std::any_of(extraOptions.begin(), extraOptions.end(), [](const CurlOption& o) { return o.option == CURLOPT_WRITEFUNCTION || o.option == CURLOPT_READFUNCTION; }))
+ throw std::logic_error("Contract violation! " + std::string(__FILE__) + ":" + numberTo<std::string>(__LINE__)); //Option already used here!
+
+ if (readRequest && std::any_of(extraOptions.begin(), extraOptions.end(), [](const CurlOption& o) { return o.option == CURLOPT_POSTFIELDS; }))
+ throw std::logic_error("Contract violation! " + std::string(__FILE__) + ":" + numberTo<std::string>(__LINE__)); //Contradicting options: CURLOPT_READFUNCTION, CURLOPT_POSTFIELDS
+
+ //---------------------------------------------------
+ curl_slist* headers = nullptr; //"libcurl will not copy the entire list so you must keep it!"
+ ZEN_ON_SCOPE_EXIT(::curl_slist_free_all(headers));
+
+ for (const std::string& headerLine : extraHeaders)
+ headers = ::curl_slist_append(headers, headerLine.c_str());
+
+ //WTF!!! 1 sec delay when server doesn't support "Expect: 100-continue!! https://stackoverflow.com/questions/49670008/how-to-disable-expect-100-continue-in-libcurl
+ headers = ::curl_slist_append(headers, "Expect:"); //guess, what: www.googleapis.com doesn't support it! e.g. gdriveUploadFile()
+
+ if (headers)
+ options.emplace_back(CURLOPT_HTTPHEADER, headers);
+ //---------------------------------------------------
+
+ append(options, extraOptions);
+
+ applyCurlOptions(easyHandle_, options); //throw SysError
+
+ //=======================================================================================================
+ const CURLcode rcPerf = ::curl_easy_perform(easyHandle_);
+ //WTF: curl_easy_perform() considers FTP response codes 4XX, 5XX as failure, but for HTTP response codes 4XX are considered success!! CONSISTENCY, people!!!
+ //=> at least libcurl is aware: CURLOPT_FAILONERROR: "request failure on HTTP response >= 400"; default: "0, do not fail on error"
+ //https://curl.haxx.se/docs/faq.html#curl_doesn_t_return_error_for_HT
+ //=> Curiously Google also screws up in their REST API design and returns HTTP 4XX status for domain-level errors!
+ //=> let caller handle HTTP status to work around this mess!
+
+ if (userCallbackException)
+ std::rethrow_exception(userCallbackException); //throw X
+ //=======================================================================================================
+
+ long httpStatusCode = 0; //optional
+ /*const CURLcode rc = */ ::curl_easy_getinfo(easyHandle_, CURLINFO_RESPONSE_CODE, &httpStatusCode);
+
+ if (rcPerf != CURLE_OK)
+ {
+ std::wstring errorMsg = trimCpy(utfTo<std::wstring>(curlErrorBuf)); //optional
+
+ if (httpStatusCode != 0) //optional
+ errorMsg += (errorMsg.empty() ? L"" : L"\n") + formatHttpStatusCode(httpStatusCode);
+#if 0
+ //utfTo<std::wstring>(::curl_easy_strerror(ec)) is uninteresting
+ //use CURLINFO_OS_ERRNO ?? https://curl.haxx.se/libcurl/c/CURLINFO_OS_ERRNO.html
+ long nativeErrorCode = 0;
+ if (::curl_easy_getinfo(easyHandle, CURLINFO_OS_ERRNO, &nativeErrorCode) == CURLE_OK)
+ if (nativeErrorCode != 0)
+ errorMsg += (errorMsg.empty() ? L"" : L"\n") + std::wstring(L"Native error code: ") + numberTo<std::wstring>(nativeErrorCode);
+#endif
+ throw SysError(formatSystemError(L"curl_easy_perform", formatCurlStatusCode(rcPerf), errorMsg));
+ }
+
+ lastSuccessfulUseTime_ = std::chrono::steady_clock::now();
+ return { static_cast<int>(httpStatusCode) /*, contentType ? contentType : ""*/ };
+}
diff --git a/libcurl/rest.h b/libcurl/rest.h
new file mode 100644
index 00000000..df41a3cc
--- /dev/null
+++ b/libcurl/rest.h
@@ -0,0 +1,52 @@
+// *****************************************************************************
+// * 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 *
+// *****************************************************************************
+
+#ifndef REST_H_07018456781346523454
+#define REST_H_07018456781346523454
+
+#include <chrono>
+#include <functional>
+#include <zen/sys_error.h>
+#include <zen/zstring.h>
+#include "curl_wrap.h" //DON'T include <curl/curl.h> directly!
+
+
+namespace zen
+{
+//Initialization requirement: 1. WSAStartup 2. OpenSSL 3. curl_global_init()
+// => use UniCounterCookie!
+
+class HttpSession
+{
+public:
+ HttpSession(const Zstring& server, const Zstring& caCertFilePath, std::chrono::seconds timeOut); //throw SysError
+ ~HttpSession();
+
+ struct Result
+ {
+ int statusCode = 0;
+ //std::string contentType;
+ };
+ Result perform(const std::string& serverRelPath,
+ const std::vector<std::string>& extraHeaders, const std::vector<CurlOption>& extraOptions, //throw SysError
+ const std::function<void (const void* buffer, size_t bytesToWrite)>& writeResponse /*throw X*/, //optional
+ const std::function<size_t( void* buffer, size_t bytesToRead )>& readRequest /*throw X*/); //
+
+ std::chrono::steady_clock::time_point getLastUseTime() const { return lastSuccessfulUseTime_; }
+
+private:
+ HttpSession (const HttpSession&) = delete;
+ HttpSession& operator=(const HttpSession&) = delete;
+
+ const std::string server_;
+ const std::string caCertFilePath_;
+ const std::chrono::seconds timeOutSec_;
+ CURL* easyHandle_ = nullptr;
+ std::chrono::steady_clock::time_point lastSuccessfulUseTime_ = std::chrono::steady_clock::now();
+};
+}
+
+#endif //REST_H_07018456781346523454
diff --git a/FreeFileSync/Source/afs/libssh2/libssh2_wrap.h b/libssh2/libssh2_wrap.h
index 37a62a24..37a62a24 100644
--- a/FreeFileSync/Source/afs/libssh2/libssh2_wrap.h
+++ b/libssh2/libssh2_wrap.h
diff --git a/wx+/image_resources.cpp b/wx+/image_resources.cpp
index 2a3b7039..070b9112 100644
--- a/wx+/image_resources.cpp
+++ b/wx+/image_resources.cpp
@@ -187,7 +187,7 @@ void GlobalBitmaps::init(const Zstring& zipPath)
{
assert(bitmaps_.empty() && anims_.empty());
- std::vector<std::pair<Zstring /*file name*/, std::string /*byte stream*/>> streams;
+ std::vector<std::pair<wxString /*file name*/, std::string /*byte stream*/>> streams;
try //to load from ZIP first:
{
@@ -199,7 +199,7 @@ void GlobalBitmaps::init(const Zstring& zipPath)
while (const auto& entry = std::unique_ptr<wxZipEntry>(zipStream.GetNextEntry())) //take ownership!
if (std::string stream(entry->GetSize(), '\0'); !stream.empty() && zipStream.ReadAll(&stream[0], stream.size()))
- streams.emplace_back(utfTo<Zstring>(entry->GetName()), std::move(stream));
+ streams.emplace_back(entry->GetName(), std::move(stream));
else
assert(false);
}
@@ -211,7 +211,7 @@ void GlobalBitmaps::init(const Zstring& zipPath)
try
{
std::string stream = loadBinContainer<std::string>(fi.fullPath, nullptr /*notifyUnbufferedIO*/); //throw FileError
- streams.emplace_back(fi.itemName, std::move(stream));
+ streams.emplace_back(utfTo<wxString>(fi.itemName), std::move(stream));
}
catch (FileError&) { assert(false); }
}, nullptr, nullptr, [](const std::wstring& errorMsg) { assert(false); }); //errors are not really critical in this context
@@ -229,12 +229,10 @@ void GlobalBitmaps::init(const Zstring& zipPath)
for (const auto& [fileName, stream] : streams)
{
- const wxString& name = utfTo<wxString>(afterLast(fileName, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_ALL));
-
wxMemoryInputStream wxstream(stream.c_str(), stream.size()); //stream does not take ownership of data
//bonus: work around wxWidgets bug: wxAnimation::Load() requires seekable input stream (zip-input stream is not seekable)
- if (endsWith(name, L".png"))
+ if (endsWith(fileName, L".png"))
{
wxImage img(wxstream, wxBITMAP_TYPE_PNG);
assert(img.IsOk());
@@ -244,14 +242,14 @@ void GlobalBitmaps::init(const Zstring& zipPath)
convertToVanillaImage(img);
if (dpiScaler_)
- dpiScaler_->add(name, img); //scale in parallel!
+ dpiScaler_->add(fileName, img); //scale in parallel!
else
- bitmaps_.emplace(name, img);
+ bitmaps_.emplace(fileName, img);
}
#if 0
else if (endsWith(name, L".gif"))
{
- [[maybe_unused]] const bool loadSuccess = anims_[name].Load(wxstream, wxANIMATION_TYPE_GIF);
+ [[maybe_unused]] const bool loadSuccess = anims_[fileName].Load(wxstream, wxANIMATION_TYPE_GIF);
assert(loadSuccess);
}
#endif
diff --git a/wx+/image_tools.h b/wx+/image_tools.h
index e2d42fb0..3e401f73 100644
--- a/wx+/image_tools.h
+++ b/wx+/image_tools.h
@@ -38,6 +38,8 @@ wxImage layOver(const wxImage& back, const wxImage& front, int alignment = wxALI
wxImage greyScale(const wxImage& img); //greyscale + brightness adaption
wxBitmap greyScale(const wxBitmap& bmp); //
+wxBitmap greyScaleIfDisabled(const wxBitmap& bmp, bool enabled);
+
//void moveImage(wxImage& img, int right, int up);
void adjustBrightness(wxImage& img, int targetLevel);
@@ -99,6 +101,16 @@ wxBitmap greyScale(const wxBitmap& bmp)
inline
+wxBitmap greyScaleIfDisabled(const wxBitmap& bmp, bool enabled)
+{
+ if (enabled) //avoid ternary WTF
+ return bmp;
+ else
+ return greyScale(bmp);
+}
+
+
+inline
double getAvgBrightness(const wxImage& img)
{
const int pixelCount = img.GetWidth() * img.GetHeight();
diff --git a/zen/error_log.h b/zen/error_log.h
index 5115e6ef..cc52fc6e 100644
--- a/zen/error_log.h
+++ b/zen/error_log.h
@@ -10,9 +10,10 @@
#include <cassert>
#include <algorithm>
#include <vector>
-#include <string>
+//#include <string>
#include "time.h"
#include "i18n.h"
+#include "utf.h"
#include "zstring.h"
@@ -30,7 +31,7 @@ struct LogEntry
{
time_t time = 0;
MessageType type = MSG_TYPE_FATAL_ERROR;
- Zstringw message; //std::wstring may employ small string optimization: we cannot accept bloating the "ErrorLog::entries" memory block below (think 1 million items)
+ Zstringw message; //std::wstring may employ small string optimization: we cannot accept bloating the "ErrorLog::entries_" memory block below (think 1 million items)
};
std::wstring formatMessage(const LogEntry& entry);
@@ -71,17 +72,14 @@ void ErrorLog::logMsg(const std::wstring& msg, MessageType type)
inline
int ErrorLog::getItemCount(int typeFilter) const
{
- return static_cast<int>(std::count_if(entries_.begin(), entries_.end(), [&](const LogEntry& e) { return e.type & typeFilter; }));
+ return static_cast<int>(std::count_if(entries_.begin(), entries_.end(), [typeFilter](const LogEntry& e) { return e.type & typeFilter; }));
}
-namespace
+inline
+std::wstring getMessageTypeLabel(MessageType type)
{
-std::wstring formatMessageImpl(const LogEntry& entry)
-{
- auto getTypeName = [&]
- {
- switch (entry.type)
+ switch (type)
{
case MSG_TYPE_INFO:
return _("Info");
@@ -94,32 +92,33 @@ std::wstring formatMessageImpl(const LogEntry& entry)
}
assert(false);
return std::wstring();
- };
+}
+
+
+inline
+std::wstring formatMessage(const LogEntry& entry)
+{
+ std::wstring msgFmt = L"[" + formatTime<std::wstring>(FORMAT_TIME, getLocalTime(entry.time)) + L"] " + getMessageTypeLabel(entry.type) + L": ";
+ const size_t prefixLen = unicodeLength(msgFmt); //consider Unicode!
- std::wstring msgFmt = L"[" + formatTime<std::wstring>(FORMAT_TIME, getLocalTime(entry.time)) + L"] " + getTypeName() + L": ";
- const size_t prefixLen = msgFmt.size(); //considers UTF-16 only!
+ const Zstringw msg = trimCpy(entry.message);
+ static_assert(std::is_same_v<decltype(msg), const Zstringw>, "don't worry about copying as long as we're using a ref-counted string!");
- for (auto it = entry.message.begin(); it != entry.message.end(); )
+ for (auto it = msg.begin(); it != msg.end(); )
if (*it == L'\n')
{
msgFmt += L'\n';
msgFmt.append(prefixLen, L' ');
-
- do //skip duplicate newlines
- {
- ++it;
- }
- while (it != entry.message.end() && *it == L'\n');
+ ++it;
+ //skip duplicate newlines
+ for (;it != msg.end() && *it == L'\n'; ++it)
+ ;
}
else
msgFmt += *it++;
- return msgFmt;
-}
+ return msgFmt += L'\n';
}
-
-inline
-std::wstring formatMessage(const LogEntry& entry) { return formatMessageImpl(entry); }
}
#endif //ERROR_LOG_H_8917590832147915
diff --git a/zen/http.cpp b/zen/http.cpp
index 4f2c5205..8cd99d7a 100644
--- a/zen/http.cpp
+++ b/zen/http.cpp
@@ -18,15 +18,19 @@ class HttpInputStream::Impl
{
public:
Impl(const Zstring& url,
- const std::vector<std::pair<std::string, std::string>>* postParams, //issue POST if bound, GET otherwise
+ const std::string* postBuf /*issue POST if bound, GET otherwise*/,
+ const Zstring& contentType, //required for POST
bool disableGetCache /*not relevant for POST (= never cached)*/,
const Zstring& userAgent,
const Zstring* caCertFilePath /*optional: enable certificate validation*/,
- const IOCallback& notifyUnbufferedIO) : //throw SysError
+ const IOCallback& notifyUnbufferedIO) : //throw SysError, X
notifyUnbufferedIO_(notifyUnbufferedIO)
{
ZEN_ON_SCOPE_FAIL(cleanup(); /*destructor call would lead to member double clean-up!!!*/);
+ //may be sending large POST: call back first
+ if (notifyUnbufferedIO_) notifyUnbufferedIO_(0); //throw X
+
const Zstring urlFmt = afterFirst(url, Zstr("://"), IF_MISSING_RETURN_NONE);
const Zstring server = beforeFirst(urlFmt, Zstr('/'), IF_MISSING_RETURN_ALL);
const Zstring page = Zstr('/') + afterFirst(urlFmt, Zstr('/'), IF_MISSING_RETURN_NONE);
@@ -40,6 +44,13 @@ public:
throw SysError(L"URL uses unexpected protocol.");
}();
+ assert(postBuf || contentType.empty());
+
+ std::map<std::string, std::string, LessAsciiNoCase> headers;
+
+ if (postBuf && !contentType.empty())
+ headers["Content-Type"] = utfTo<std::string>(contentType);
+
if (useTls) //HTTP default port: 443, see %WINDIR%\system32\drivers\etc\services
{
socket_ = std::make_unique<Socket>(server, Zstr("https")); //throw SysError
@@ -49,27 +60,23 @@ public:
socket_ = std::make_unique<Socket>(server, Zstr("http")); //throw SysError
//we don't support "chunked and gzip transfer encoding" => HTTP 1.0
- std::map<std::string, std::string, LessAsciiNoCase> headers;
headers["Host" ] = utfTo<std::string>(server); //only required for HTTP/1.1 but a few servers expect it even for HTTP/1.0
headers["User-Agent"] = utfTo<std::string>(userAgent);
headers["Accept" ] = "*/*"; //won't hurt?
- const std::string postBuf = postParams ? xWwwFormUrlEncode(*postParams) : "";
-
- if (!postParams /*HTTP GET*/ && disableGetCache)
+ if (!postBuf /*HTTP GET*/ && disableGetCache)
headers["Pragma"] = "no-cache"; //HTTP 1.0 only! superseeded by "Cache-Control"
- else //HTTP POST
- {
- headers["Content-Type"] = "application/x-www-form-urlencoded";
- headers["Content-Length"] = numberTo<std::string>(postBuf.size());
- }
+
+ if (postBuf)
+ headers["Content-Length"] = numberTo<std::string>(postBuf->size());
//https://www.w3.org/Protocols/HTTP/1.0/spec.html#Request-Line
- std::string msg = (postParams ? "POST " : "GET ") + utfTo<std::string>(page) + " HTTP/1.0\r\n";
+ std::string msg = (postBuf ? "POST " : "GET ") + utfTo<std::string>(page) + " HTTP/1.0\r\n";
for (const auto& [name, value] : headers)
msg += name + ": " + value + "\r\n";
msg += "\r\n";
- msg += postBuf;
+ if (postBuf)
+ msg += *postBuf;
//send request
for (size_t bytesToSend = msg.size(); bytesToSend > 0;)
@@ -121,6 +128,9 @@ public:
//try to get "Content-Length" header if available
if (const std::string* value = getHeader("Content-Length"))
contentRemaining_ = stringTo<int64_t>(*value) - (bufPosEnd_ - bufPos_);
+
+ //let's not get too finicky: at least report the logical amount of bytes sent/received (excluding HTTP headers)
+ if (notifyUnbufferedIO_) notifyUnbufferedIO_(postBuf ? postBuf->size() : 0); //throw X
}
~Impl() { cleanup(); }
@@ -225,16 +235,17 @@ std::string HttpInputStream::readAll() { return bufferedLoad<std::string>(*pimpl
namespace
{
std::unique_ptr<HttpInputStream::Impl> sendHttpRequestImpl(const Zstring& url,
- const std::vector<std::pair<std::string, std::string>>* postParams /*issue POST if bound, GET otherwise*/,
+ const std::string* postBuf /*issue POST if bound, GET otherwise*/,
+ const Zstring& contentType, //required for POST
const Zstring& userAgent,
const Zstring* caCertFilePath /*optional: enable certificate validation*/,
- const IOCallback& notifyUnbufferedIO) //throw SysError
+ const IOCallback& notifyUnbufferedIO) //throw SysError, X
{
Zstring urlRed = url;
//"A user agent should not automatically redirect a request more than five times, since such redirections usually indicate an infinite loop."
for (int redirects = 0; redirects < 6; ++redirects)
{
- auto response = std::make_unique<HttpInputStream::Impl>(urlRed, postParams, false /*disableGetCache*/, userAgent, caCertFilePath, notifyUnbufferedIO); //throw SysError
+ auto response = std::make_unique<HttpInputStream::Impl>(urlRed, postBuf, contentType, false /*disableGetCache*/, userAgent, caCertFilePath, notifyUnbufferedIO); //throw SysError, X
//https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_Redirection
const int httpStatusCode = response->getStatusCode();
@@ -258,48 +269,48 @@ std::unique_ptr<HttpInputStream::Impl> sendHttpRequestImpl(const Zstring& url,
}
-//encode into "application/x-www-form-urlencoded"
+//encode for "application/x-www-form-urlencoded"
std::string urlencode(const std::string& str)
{
- std::string out;
- for (const char c : str) //follow PHP spec: https://github.com/php/php-src/blob/master/ext/standard/url.c#L500
+ std::string output;
+ for (const char c : str) //follow PHP spec: https://github.com/php/php-src/blob/e99d5d39239c611e1e7304e79e88545c4e71a073/ext/standard/url.c#L455
if (c == ' ')
- out += '+';
+ output += '+';
else if (('0' <= c && c <= '9') ||
('A' <= c && c <= 'Z') ||
('a' <= c && c <= 'z') ||
c == '-' || c == '.' || c == '_') //note: "~" is encoded by PHP!
- out += c;
+ output += c;
else
{
const auto [high, low] = hexify(c);
- out += '%';
- out += high;
- out += low;
+ output += '%';
+ output += high;
+ output += low;
}
- return out;
+ return output;
}
std::string urldecode(const std::string& str)
{
- std::string out;
+ std::string output;
for (size_t i = 0; i < str.size(); ++i)
{
const char c = str[i];
if (c == '+')
- out += ' ';
+ output += ' ';
else if (c == '%' && str.size() - i >= 3 &&
isHexDigit(str[i + 1]) &&
isHexDigit(str[i + 2]))
{
- out += unhexify(str[i + 1], str[i + 2]);
+ output += unhexify(str[i + 1], str[i + 2]);
i += 2;
}
else
- out += c;
+ output += c;
}
- return out;
+ return output;
}
}
@@ -327,16 +338,24 @@ std::vector<std::pair<std::string, std::string>> zen::xWwwFormUrlDecode(const st
}
+HttpInputStream zen::sendHttpGet(const Zstring& url, const Zstring& userAgent, const Zstring* caCertFilePath, const IOCallback& notifyUnbufferedIO) //throw SysError, X
+{
+ return sendHttpRequestImpl(url, nullptr /*postBuf*/, Zstr("") /*contentType*/, userAgent, caCertFilePath, notifyUnbufferedIO); //throw SysError, X, X
+}
+
+
HttpInputStream zen::sendHttpPost(const Zstring& url, const std::vector<std::pair<std::string, std::string>>& postParams,
- const Zstring& userAgent, const Zstring* caCertFilePath, const IOCallback& notifyUnbufferedIO) //throw SysError
+ const Zstring& userAgent, const Zstring* caCertFilePath, const IOCallback& notifyUnbufferedIO) //throw SysError, X
{
- return sendHttpRequestImpl(url, &postParams, userAgent, caCertFilePath, notifyUnbufferedIO); //throw SysError
+ return sendHttpPost(url, xWwwFormUrlEncode(postParams), Zstr("application/x-www-form-urlencoded"), userAgent, caCertFilePath, notifyUnbufferedIO); //throw SysError, X
}
-HttpInputStream zen::sendHttpGet(const Zstring& url, const Zstring& userAgent, const Zstring* caCertFilePath, const IOCallback& notifyUnbufferedIO) //throw SysError
+
+HttpInputStream zen::sendHttpPost(const Zstring& url, const std::string& postBuf, const Zstring& contentType,
+ const Zstring& userAgent, const Zstring* caCertFilePath, const IOCallback& notifyUnbufferedIO) //throw SysError, X
{
- return sendHttpRequestImpl(url, nullptr /*postParams*/, userAgent, caCertFilePath, notifyUnbufferedIO); //throw SysError
+ return sendHttpRequestImpl(url, &postBuf, contentType, userAgent, caCertFilePath, notifyUnbufferedIO); //throw SysError, X
}
@@ -346,6 +365,7 @@ bool zen::internetIsAlive() //noexcept
{
auto response = std::make_unique<HttpInputStream::Impl>(Zstr("http://www.google.com/"),
nullptr /*postParams*/,
+ Zstr("") /*contentType*/,
true /*disableGetCache*/,
Zstr("FreeFileSync"),
nullptr /*caCertFilePath*/,
@@ -439,4 +459,79 @@ std::wstring zen::formatHttpStatusCode(int sc)
return trimCpy(replaceCpy<std::wstring>(L"HTTP status %x.", L"%x", numberTo<std::wstring>(sc)));
else
return trimCpy(replaceCpy<std::wstring>(L"HTTP status %x: ", L"%x", numberTo<std::wstring>(sc)) + statusText);
-} \ No newline at end of file
+}
+
+
+bool zen::isValidEmail(const Zstring& email)
+{
+ //https://en.wikipedia.org/wiki/Email_address#Syntax
+ //https://tools.ietf.org/html/rfc3696 => note errata! https://www.rfc-editor.org/errata_search.php?rfc=3696
+ //https://tools.ietf.org/html/rfc5321
+ std::string local = utfTo<std::string>(beforeLast(email, Zstr('@'), IF_MISSING_RETURN_NONE));
+ std::string domain = utfTo<std::string>( afterLast(email, Zstr('@'), IF_MISSING_RETURN_NONE));
+ //consider: "t@st"@email.com t\@st@email.com"
+
+ auto stripComments = [](std::string& part)
+ {
+ if (startsWith(part, '('))
+ part = afterFirst(part, ')', IF_MISSING_RETURN_NONE);
+
+ if (endsWith(part, ')'))
+ part = beforeLast(part, '(', IF_MISSING_RETURN_NONE);
+ };
+ stripComments(local);
+ stripComments(domain);
+
+ if (local .empty() || local .size() > 63 || // 64 octets -> 63 ASCII chars: https://devblogs.microsoft.com/oldnewthing/20120412-00/?p=7873
+ domain.empty() || domain.size() > 253) //255 octets -> 253 ASCII chars
+ return false;
+ //---------------------------------------------------------------------
+
+ const bool quoted = (startsWith(local, '"') && endsWith(local, '"')) ||
+ contains(local, '\\'); //e.g. "t\@st@email.com"
+ if (!quoted) //I'm not going to parse and validate this!
+ for (const std::string& comp : split(local, '.', SplitType::ALLOW_EMPTY))
+ if (comp.empty() || !std::all_of(comp.begin(), comp.end(), [](char c)
+ {
+ const char printable[] = "!#$%&'*+-/=?^_`{|}~";
+ return isAsciiAlpha(c) || isDigit(c) || makeUnsigned(c) >= 128 ||
+ std::find(std::begin(printable), std::end(printable), c) != std::end(printable);
+ }))
+ return false;
+ //---------------------------------------------------------------------
+
+ //e.g. jsmith@[192.168.2.1] jsmith@[IPv6:2001:db8::1]
+ const bool likelyIp = startsWith(domain, '[') && endsWith(domain, ']');
+ if (!likelyIp) //not interested in parsing IPs!
+ {
+ if (!contains(domain, '.'))
+ return false;
+
+ for (const std::string& comp : split(domain, '.', SplitType::ALLOW_EMPTY))
+ if (comp.empty() || comp.size() > 63 ||
+ !std::all_of(comp.begin(), comp.end(), [](char c) { return isAsciiAlpha(c) ||isDigit(c) || makeUnsigned(c) >= 128 || c == '-'; }))
+ return false;
+ }
+
+ return true;
+}
+
+
+std::string zen::htmlSpecialChars(const std::string& str)
+{
+ //mirror PHP: https://github.com/php/php-src/blob/e99d5d39239c611e1e7304e79e88545c4e71a073/ext/standard/html_tables.h#L6189
+ std::string output;
+ for (const char c : str)
+ switch (c)
+ {
+ //*INDENT-OFF*
+ case '&': output += "&amp;" ; break;
+ case '"': output += "&quot;"; break;
+ case '<': output += "&lt;" ; break;
+ case '>': output += "&gt;" ; break;
+ //case '\'': output += "&apos;"; break; -> not encoded by default (needs ENT_QUOTES)
+ default: output += c; break;
+ //*INDENT-ON*
+ }
+ return output;
+}
diff --git a/zen/http.h b/zen/http.h
index 42b0e279..fbaa09de 100644
--- a/zen/http.h
+++ b/zen/http.h
@@ -39,15 +39,24 @@ private:
HttpInputStream sendHttpGet(const Zstring& url,
const Zstring& userAgent,
const Zstring* caCertFilePath /*optional: enable certificate validation*/,
- const IOCallback& notifyUnbufferedIO /*throw X*/); //throw SysError
+ const IOCallback& notifyUnbufferedIO /*throw X*/); //throw SysError, X
HttpInputStream sendHttpPost(const Zstring& url,
const std::vector<std::pair<std::string, std::string>>& postParams,
const Zstring& userAgent,
const Zstring* caCertFilePath /*optional: enable certificate validation*/,
- const IOCallback& notifyUnbufferedIO /*throw X*/);
+ const IOCallback& notifyUnbufferedIO /*throw X*/); //throw SysError, X
+
+HttpInputStream sendHttpPost(const Zstring& url,
+ const std::string& postBuf, const Zstring& contentType,
+ const Zstring& userAgent,
+ const Zstring* caCertFilePath /*optional: enable certificate validation*/,
+ const IOCallback& notifyUnbufferedIO /*throw X*/); //throw SysError, X
+
bool internetIsAlive(); //noexcept
std::wstring formatHttpStatusCode(int httpStatusCode);
+bool isValidEmail(const Zstring& email);
+std::string htmlSpecialChars(const std::string& str);
std::string xWwwFormUrlEncode(const std::vector<std::pair<std::string, std::string>>& paramPairs);
std::vector<std::pair<std::string, std::string>> xWwwFormUrlDecode(const std::string& str);
diff --git a/zen/json.h b/zen/json.h
index e6464286..725874f7 100644
--- a/zen/json.h
+++ b/zen/json.h
@@ -27,16 +27,18 @@ struct JsonValue
};
explicit JsonValue() {}
- explicit JsonValue(Type t) : type(t) {}
- explicit JsonValue(bool b) : type(Type::boolean), primVal(b ? "true" : "false") {}
- explicit JsonValue(int64_t num) : type(Type::number), primVal(numberTo<std::string>(num)) {}
- explicit JsonValue(double num) : type(Type::number), primVal(numberTo<std::string>(num)) {}
- explicit JsonValue(const std::string& str) : type(Type::string), primVal(str) {}
+ explicit JsonValue(Type t) : type(t) {}
+ explicit JsonValue(bool b) : type(Type::boolean), primVal(b ? "true" : "false") {}
+ explicit JsonValue(int num) : type(Type::number), primVal(numberTo<std::string>(num)) {}
+ explicit JsonValue(int64_t num) : type(Type::number), primVal(numberTo<std::string>(num)) {}
+ explicit JsonValue(double num) : type(Type::number), primVal(numberTo<std::string>(num)) {}
+ explicit JsonValue(std::string str) : type(Type::string), primVal(std::move(str)) {} //unifying assignment
+ explicit JsonValue(const void*) = delete; //catch usage errors e.g. const char* -> JsonValue(bool)
Type type = Type::null;
- std::string primVal; //for primitive types
- std::map<std::string, std::unique_ptr<JsonValue>> objectVal; //"[...] most implementations of JSON libraries do not accept duplicate keys [...]" => fine!
- std::vector<std::unique_ptr<JsonValue>> arrayVal;
+ std::string primVal; //for primitive types
+ std::map<std::string, JsonValue> objectVal; //"[...] most implementations of JSON libraries do not accept duplicate keys [...]" => fine!
+ std::vector<JsonValue> arrayVal;
};
@@ -66,7 +68,7 @@ const JsonValue* getChildFromJsonObject(const JsonValue& jvalue, const std::stri
if (it == jvalue.objectVal.end())
return nullptr;
- return it->second.get();
+ return &it->second;
}
@@ -240,8 +242,8 @@ void serialize(const JsonValue& jval, std::string& stream,
stream += '"' + jsonEscape(childName) + "\":";
- if ((childValue->type == JsonValue::Type::object && !childValue->objectVal.empty()) ||
- (childValue->type == JsonValue::Type::array && !childValue->arrayVal .empty()))
+ if ((childValue.type == JsonValue::Type::object && !childValue.objectVal.empty()) ||
+ (childValue.type == JsonValue::Type::array && !childValue.arrayVal .empty()))
{
stream += lineBreak;
writeIndent(indentLevel + 1);
@@ -249,7 +251,7 @@ void serialize(const JsonValue& jval, std::string& stream,
else if (!indent.empty())
stream += ' ';
- serialize(*childValue, stream, lineBreak, indent, indentLevel + 1);
+ serialize(childValue, stream, lineBreak, indent, indentLevel + 1);
}
stream += lineBreak;
writeIndent(indentLevel);
@@ -263,7 +265,7 @@ void serialize(const JsonValue& jval, std::string& stream,
{
for (auto it = jval.arrayVal.begin(); it != jval.arrayVal.end(); ++it)
{
- const auto& childValue = **it;
+ const auto& childValue = *it;
if (it != jval.arrayVal.begin())
stream += ',';
@@ -462,7 +464,7 @@ private:
consumeToken(Token::Type::colon); //throw JsonParsingError
JsonValue value = parseValue(); //throw JsonParsingError
- jval.objectVal.emplace(std::move(name), std::make_unique<JsonValue>(std::move(value)));
+ jval.objectVal.emplace(std::move(name), std::move(value));
if (token().type != Token::Type::comma)
break;
@@ -482,7 +484,7 @@ private:
for (;;)
{
JsonValue value = parseValue(); //throw JsonParsingError
- jval.arrayVal.emplace_back(std::make_unique<JsonValue>(std::move(value)));
+ jval.arrayVal.emplace_back(std::move(value));
if (token().type != Token::Type::comma)
break;
diff --git a/zen/serialize.h b/zen/serialize.h
index bdeec858..dd884e3b 100644
--- a/zen/serialize.h
+++ b/zen/serialize.h
@@ -120,7 +120,7 @@ private:
template <class BinContainer>
struct MemoryStreamIn
{
- MemoryStreamIn(const BinContainer& cont) : buffer_(cont) {} //this better be cheap!
+ explicit MemoryStreamIn(const BinContainer& cont) : buffer_(cont) {} //this better be cheap!
size_t read(void* buffer, size_t bytesToRead) //return "bytesToRead" bytes unless end of stream!
{
@@ -207,11 +207,12 @@ BinContainer bufferedLoad(BufferedInputStream& streamIn) //throw X
for (;;)
{
buffer.resize(buffer.size() + blockSize);
- const size_t bytesRead = streamIn.read(&*(buffer.end() - blockSize), blockSize); //throw X; return "bytesToRead" bytes unless end of stream!
- buffer.resize(buffer.size() - blockSize + bytesRead); //caveat: unsigned arithmetics
-
+ const size_t bytesRead = streamIn.read(&*(buffer.end() - blockSize), blockSize); //throw X; return "blockSize" bytes unless end of stream!
if (bytesRead < blockSize) //end of file
+ {
+ buffer.resize(buffer.size() - (blockSize - bytesRead)); //caveat: unsigned arithmetics
return buffer;
+ }
}
}
@@ -270,12 +271,11 @@ C readContainer(BufferedInputStream& stream) //throw UnexpectedEndOfStreamError
{
try
{
- cont.resize(strLength); //throw std::bad_alloc
- }
- catch (std::bad_alloc&) //most likely this is due to data corruption!
- {
- throw UnexpectedEndOfStreamError();
+ cont.resize(strLength); //throw std::length_error
}
+ catch (std::length_error&) { throw UnexpectedEndOfStreamError(); } //most likely this is due to data corruption!
+ catch ( std::bad_alloc&) { throw UnexpectedEndOfStreamError(); } //
+
readArray(stream, &*cont.begin(), sizeof(typename C::value_type) * strLength); //throw UnexpectedEndOfStreamError
}
return cont;
diff --git a/zen/shell_execute.h b/zen/shell_execute.h
index 56322236..580c4558 100644
--- a/zen/shell_execute.h
+++ b/zen/shell_execute.h
@@ -19,15 +19,15 @@ namespace zen
//Windows: COM needs to be initialized before calling this function!
enum class ExecutionType
{
- SYNC,
- ASYNC
+ sync,
+ async
};
namespace
{
-void shellExecute(const Zstring& command, ExecutionType type, bool hideConsole) //throw FileError
+int shellExecute(const Zstring& command, ExecutionType type, bool hideConsole) //throw FileError
{
/*
we cannot use wxExecute due to various issues:
@@ -35,21 +35,23 @@ void shellExecute(const Zstring& command, ExecutionType type, bool hideConsole)
- does not provide any reasonable error information
- uses a zero-sized dummy window as a hack to keep focus which leaves a useless empty icon in ALT-TAB list in Windows
*/
- if (type == ExecutionType::SYNC)
+ if (type == ExecutionType::sync)
{
//Posix ::system() - execute a shell command
const int rv = ::system(command.c_str()); //do NOT use std::system as its documentation says nothing about "WEXITSTATUS(rv)", etc...
if (rv == -1 || WEXITSTATUS(rv) == 127)
- throw FileError(_("Incorrect command line:") + L"\n" + utfTo<std::wstring>(command));
+ throw FileError(_("Incorrect command line:") + L' ' + utfTo<std::wstring>(command));
//https://linux.die.net/man/3/system "In case /bin/sh could not be executed, the exit status will be that of a command that does exit(127)"
//Bonus: For an incorrect command line /bin/sh also returns with 127!
+
+ return /*int exitCode = */ WEXITSTATUS(rv);
}
else
{
//follow implemenation of ::system() except for waitpid():
const pid_t pid = ::fork();
if (pid < 0) //pids are never negative, empiric proof: https://linux.die.net/man/2/wait
- THROW_LAST_FILE_ERROR(_("Incorrect command line:") + L"\n" + utfTo<std::wstring>(command), L"fork");
+ THROW_LAST_FILE_ERROR(_("Incorrect command line:") + L' ' + utfTo<std::wstring>(command), L"fork");
if (pid == 0) //child process
{
@@ -64,7 +66,39 @@ void shellExecute(const Zstring& command, ExecutionType type, bool hideConsole)
::_exit(127); //[!] avoid flushing I/O buffers or doing other clean up from child process like with "exit(127)"!
}
//else //parent process
+ return 0;
+ }
+}
+
+
+std::string getCommandOutput(const Zstring& command) //throw SysError
+{
+ //https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/popen.3.html
+ FILE* pipe = ::popen(command.c_str(), "r");
+ if (!pipe)
+ THROW_LAST_SYS_ERROR(L"popen");
+ ZEN_ON_SCOPE_EXIT(::pclose(pipe));
+
+ std::string output;
+ const size_t blockSize = 64 * 1024;
+ do
+ {
+ output.resize(output.size() + blockSize);
+
+ //caveat: SIGCHLD is NOT ignored under macOS debugger => EINTR inside fread() => call ::siginterrupt(SIGCHLD, false) during startup
+ const size_t bytesRead = ::fread(&*(output.end() - blockSize), 1, blockSize, pipe);
+ if (::ferror(pipe))
+ THROW_LAST_SYS_ERROR(L"fread");
+
+ if (bytesRead > blockSize)
+ throw SysError(L"fread: buffer overflow");
+
+ if (bytesRead < blockSize)
+ output.resize(output.size() - (blockSize - bytesRead)); //caveat: unsigned arithmetics
}
+ while (!::feof(pipe));
+
+ return output;
}
}
@@ -72,7 +106,7 @@ void shellExecute(const Zstring& command, ExecutionType type, bool hideConsole)
inline
void openWithDefaultApplication(const Zstring& itemPath) //throw FileError
{
- shellExecute("xdg-open \"" + itemPath + '"', ExecutionType::ASYNC, false /*hideConsole*/); //throw FileError
+ shellExecute("xdg-open \"" + itemPath + '"', ExecutionType::async, false /*hideConsole*/); //throw FileError
}
}
diff --git a/zen/shutdown.cpp b/zen/shutdown.cpp
index 5ce586f0..89da55ee 100644
--- a/zen/shutdown.cpp
+++ b/zen/shutdown.cpp
@@ -18,7 +18,7 @@ void zen::shutdownSystem() //throw FileError
//https://linux.die.net/man/2/reboot => needs admin rights!
//"systemctl" should work without admin rights:
- shellExecute("systemctl poweroff", ExecutionType::SYNC, false/*hideConsole*/); //throw FileError
+ shellExecute("systemctl poweroff", ExecutionType::sync, false/*hideConsole*/); //throw FileError
}
@@ -26,7 +26,7 @@ void zen::shutdownSystem() //throw FileError
void zen::suspendSystem() //throw FileError
{
//"systemctl" should work without admin rights:
- shellExecute("systemctl suspend", ExecutionType::SYNC, false/*hideConsole*/); //throw FileError
+ shellExecute("systemctl suspend", ExecutionType::sync, false/*hideConsole*/); //throw FileError
}
diff --git a/zen/string_tools.h b/zen/string_tools.h
index 47271bc7..5c444830 100644
--- a/zen/string_tools.h
+++ b/zen/string_tools.h
@@ -27,7 +27,7 @@ template <class Char> bool isLineBreak (Char c);
template <class Char> bool isDigit (Char c); //not exactly the same as "std::isdigit" -> we consider '0'-'9' only!
template <class Char> bool isHexDigit (Char c);
template <class Char> bool isAsciiAlpha(Char c);
-template <class Char> bool isAsciiString(const Char* str);
+template <class S > bool isAsciiString(const S& str);
template <class Char> Char asciiToLower(Char c);
template <class Char> Char asciiToUpper(Char c);
@@ -150,14 +150,11 @@ bool isAsciiAlpha(Char c)
}
-template <class Char> inline
-bool isAsciiString(const Char* str)
+template <class S> inline
+bool isAsciiString(const S& str)
{
- static_assert(std::is_same_v<Char, char> || std::is_same_v<Char, wchar_t>);
- for (Char c = *str; c != 0; c = *++str)
- if (zen::makeUnsigned(c) >= 128)
- return false;
- return true;
+ const auto* const first = strBegin(str);
+ return std::all_of(first, first + strLength(str), [](auto c) { return makeUnsigned(c) < 128; });
}
@@ -170,8 +167,8 @@ Char asciiToLower(Char c)
}
- template <class Char> inline
- Char asciiToUpper(Char c)
+template <class Char> inline
+Char asciiToUpper(Char c)
{
if (static_cast<Char>('a') <= c && c <= static_cast<Char>('z'))
return static_cast<Char>(c - static_cast<Char>('a') + static_cast<Char>('A'));
@@ -604,7 +601,7 @@ S numberTo(const Num& number, std::integral_constant<NumberType, NumberType::FLO
char buffer[128]; //zero-initialize?
//let's give some leeway, but 24 chars should suffice: https://www.reddit.com/r/cpp/comments/dgj89g/cppcon_2019_stephan_t_lavavej_floatingpoint/f3j7d3q/
- const char* strEnd = zen::to_chars(std::begin(buffer), std::end(buffer), number);
+ const char* strEnd = to_chars(std::begin(buffer), std::end(buffer), number);
S output;
std::for_each(static_cast<const char*>(buffer), strEnd,
@@ -703,7 +700,7 @@ inline
double stringToFloat(const char* first, const char* last)
{
//don't use std::strtod(): 1. requires null-terminated string 2. SLOWER than std::from_chars()
- return zen::from_chars(first, last);
+ return from_chars(first, last);
}
@@ -713,7 +710,7 @@ double stringToFloat(const wchar_t* first, const wchar_t* last)
std::string buf(last - first, '\0');
std::transform(first, last, buf.begin(), [](wchar_t c) { return static_cast<char>(c); });
- return zen::from_chars(buf.c_str(), buf.c_str() + buf.size());
+ return from_chars(buf.c_str(), buf.c_str() + buf.size());
}
diff --git a/zen/sys_error.h b/zen/sys_error.h
index 57503732..a9347bdd 100644
--- a/zen/sys_error.h
+++ b/zen/sys_error.h
@@ -85,9 +85,8 @@ std::wstring formatSystemError(const std::wstring& functionName, long long lastE
inline
std::wstring formatSystemError(const std::wstring& functionName, ErrorCode ec)
{
- const std::wstring errorDescr = formatSystemErrorRaw(ec);
const std::wstring errorCode = numberTo<std::wstring>(ec);
- //const std::wstring errorCode = printNumber<std::wstring>(L"0x%08x", static_cast<int>(ec));
+ const std::wstring errorDescr = formatSystemErrorRaw(ec);
return formatSystemError(functionName, replaceCpy(_("Error Code %x"), L"%x", errorCode), errorDescr);
}
diff --git a/zen/system.cpp b/zen/system.cpp
new file mode 100644
index 00000000..5945484f
--- /dev/null
+++ b/zen/system.cpp
@@ -0,0 +1,84 @@
+// *****************************************************************************
+// * 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 "system.h"
+#include "file_access.h"
+#include "crc.h"
+
+ #include "symlink_target.h"
+ #include "file_io.h"
+ #include <ifaddrs.h>
+ #include <net/if.h> //IFF_LOOPBACK
+ #include <netpacket/packet.h> //sockaddr_ll
+
+ #include <unistd.h> //getuid()
+ #include <pwd.h> //getpwuid_r()
+ #include "shell_execute.h"
+
+using namespace zen;
+
+
+std::wstring zen::getUserName() //throw FileError
+{
+ const uid_t userIdNo = ::getuid(); //never fails
+
+ std::vector<char> buffer(std::max<long>(10000, ::sysconf(_SC_GETPW_R_SIZE_MAX))); //::sysconf may return long(-1)
+ struct passwd buffer2 = {};
+ struct passwd* pwsEntry = nullptr;
+ if (::getpwuid_r(userIdNo, &buffer2, &buffer[0], buffer.size(), &pwsEntry) != 0) //getlogin() is deprecated and not working on Ubuntu at all!!!
+ THROW_LAST_FILE_ERROR(_("Cannot get process information."), L"getpwuid_r");
+ if (!pwsEntry)
+ throw FileError(_("Cannot get process information."), L"no login found"); //should not happen?
+
+ return utfTo<std::wstring>(pwsEntry->pw_name);
+}
+
+
+namespace
+{
+}
+
+
+ComputerModel zen::getComputerModel() //throw FileError
+{
+ try
+ {
+ auto tryGetInfo = [](const Zstring& filePath)
+ {
+ if (!fileAvailable(filePath))
+ return std::wstring();
+ try
+ {
+ const std::string stream = loadBinContainer<std::string>(filePath, nullptr /*notifyUnbufferedIO*/); //throw FileError
+ return utfTo<std::wstring>(trimCpy(stream));
+ }
+ catch (const FileError& e) { throw SysError(e.toString()); } //errors should be further enriched by context info => SysError
+ };
+ return { tryGetInfo("/sys/devices/virtual/dmi/id/product_name"), //throw SysError
+ tryGetInfo("/sys/devices/virtual/dmi/id/sys_vendor") }; //
+
+ }
+ catch (const SysError& e) { throw FileError(_("Cannot get process information."), e.toString()); }
+}
+
+
+
+
+std::wstring zen::getOsDescription() //throw FileError
+{
+ try
+ {
+ const std::string osName = trimCpy(getCommandOutput("lsb_release --id -s" )); //throw SysError
+ const std::string osVersion = trimCpy(getCommandOutput("lsb_release --release -s")); //
+ return utfTo<std::wstring>(osName + " " + osVersion); //e.g. "CentOS 7.7.1908"
+
+ }
+ catch (const SysError& e) { throw FileError(_("Cannot get process information."), e.toString()); }
+}
+
+
+
+
diff --git a/zen/system.h b/zen/system.h
new file mode 100644
index 00000000..f10a6a40
--- /dev/null
+++ b/zen/system.h
@@ -0,0 +1,33 @@
+// *****************************************************************************
+// * 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 *
+// *****************************************************************************
+
+#ifndef SYSTEM_H_4189731847832147508915
+#define SYSTEM_H_4189731847832147508915
+
+#include "file_error.h"
+
+
+namespace zen
+{
+//COM needs to be initialized before calling any of these functions! CoInitializeEx/CoUninitialize
+
+std::wstring getUserName(); //throw FileError
+
+struct ComputerModel
+{
+ std::wstring model; //best-effort: empty if not available
+ std::wstring vendor; //
+};
+ComputerModel getComputerModel(); //throw FileError
+
+
+
+std::wstring getOsDescription(); //throw FileError
+
+
+}
+
+#endif //SYSTEM_H_4189731847832147508915
diff --git a/zen/zlib_wrap.cpp b/zen/zlib_wrap.cpp
index f7418b88..57a0f33c 100644
--- a/zen/zlib_wrap.cpp
+++ b/zen/zlib_wrap.cpp
@@ -149,3 +149,23 @@ private:
zen::InputStreamAsGzip::InputStreamAsGzip(const std::function<size_t(void* buffer, size_t bytesToRead)>& readBlock /*throw X*/) : pimpl_(std::make_unique<Impl>(readBlock)) {} //throw SysError
zen::InputStreamAsGzip::~InputStreamAsGzip() {}
size_t zen::InputStreamAsGzip::read(void* buffer, size_t bytesToRead) { return pimpl_->read(buffer, bytesToRead); } //throw SysError, X
+
+
+std::string zen::compressAsGzip(const void* buffer, size_t bufSize) //throw SysError
+{
+ struct MemoryStreamAsGzip : InputStreamAsGzip
+ {
+ explicit MemoryStreamAsGzip(const std::function<size_t(void* buffer, size_t bytesToRead)>& readBlock /*throw X*/) : InputStreamAsGzip(readBlock) {} //throw SysError
+ static size_t getBlockSize() { return 128 * 1024; } //InputStreamAsGzip has no idea what it's wrapping => has no getBlockSize() member!
+ };
+
+ MemoryStreamAsGzip gzipStream([&](void* bufIn, size_t bytesToRead) //throw SysError
+ {
+ const size_t bytesRead = std::min(bufSize, bytesToRead);
+ std::memcpy(bufIn, buffer, bytesRead);
+ buffer = static_cast<const char*>(buffer) + bytesRead;
+ bufSize -= bytesRead;
+ return bytesRead; //returning 0 signals EOF: Posix read() semantics
+ });
+ return bufferedLoad<std::string>(gzipStream); //throw SysError
+}
diff --git a/zen/zlib_wrap.h b/zen/zlib_wrap.h
index 9d9229ac..b820a4f8 100644
--- a/zen/zlib_wrap.h
+++ b/zen/zlib_wrap.h
@@ -27,8 +27,8 @@ BinContainer decompress(const BinContainer& stream); //throw SysError
class InputStreamAsGzip //convert input stream into gzip on the fly
{
public:
- InputStreamAsGzip( //throw SysError
- const std::function<size_t(void* buffer, size_t bytesToRead)>& readBlock /*throw X*/); //returning 0 signals EOF: Posix read() semantics
+ explicit InputStreamAsGzip( //throw SysError
+ const std::function<size_t(void* buffer, size_t bytesToRead)>& readBlock /*throw X; returning 0 signals EOF: Posix read() semantics*/);
~InputStreamAsGzip();
size_t read(void* buffer, size_t bytesToRead); //throw SysError, X; return "bytesToRead" bytes unless end of stream!
@@ -38,6 +38,7 @@ private:
const std::unique_ptr<Impl> pimpl_;
};
+std::string compressAsGzip(const void* buffer, size_t bufSize); //throw SysError
@@ -103,10 +104,9 @@ BinContainer decompress(const BinContainer& stream) //throw SysError
{
contOut.resize(static_cast<size_t>(uncompressedSize)); //throw std::bad_alloc
}
- catch (const std::bad_alloc& e) //most likely due to data corruption!
- {
- throw SysError(L"zlib error: " + _("Out of memory.") + L" " + utfTo<std::wstring>(e.what()));
- }
+ //most likely this is due to data corruption:
+ catch (const std::length_error& e) { throw SysError(L"zlib error: " + _("Out of memory.") + L" " + utfTo<std::wstring>(e.what())); }
+ catch (const std::bad_alloc& e) { throw SysError(L"zlib error: " + _("Out of memory.") + L" " + utfTo<std::wstring>(e.what())); }
const size_t bytesWritten = impl::zlib_decompress(&*stream.begin() + sizeof(uncompressedSize),
stream.size() - sizeof(uncompressedSize),
diff --git a/zen/zstring.cpp b/zen/zstring.cpp
index f018b14f..ff20b8cf 100644
--- a/zen/zstring.cpp
+++ b/zen/zstring.cpp
@@ -17,7 +17,7 @@ using namespace zen;
Zstring makeUpperCopy(const Zstring& str)
{
//fast pre-check:
- if (isAsciiString(str.c_str())) //perf: in the range of 3.5ns
+ if (isAsciiString(str)) //perf: in the range of 3.5ns
{
Zstring output = str;
for (Zchar& c : output) c = asciiToUpper(c);
@@ -49,7 +49,7 @@ Zstring makeUpperCopy(const Zstring& str)
Zstring getUnicodeNormalForm(const Zstring& str)
{
//fast pre-check:
- if (isAsciiString(str.c_str())) //perf: in the range of 3.5ns
+ if (isAsciiString(str)) //perf: in the range of 3.5ns
return str; //god bless our ref-counting! => save output string memory consumption!
//Example: const char* decomposed = "\x6f\xcc\x81";
bgstack15