commit 29a4d4494b9848374a28ef4a62cc587cd572ee60 Author: oxmc <67136658+oxmc@users.noreply.github.com> Date: Wed Dec 10 14:38:26 2025 -0800 v 14.6 diff --git a/Bugs.txt b/Bugs.txt new file mode 100644 index 0000000..4226b3a --- /dev/null +++ b/Bugs.txt @@ -0,0 +1,252 @@ +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 +the ones mentioned below. The remaining issues that are yet to be fixed are listed in the following: + + +----------------- +| libcurl 8.16.0| +----------------- +__________________________________________________________________________________________________________ +/lib/ftp.c +https://github.com/curl/curl/issues/1455 + ++ static bool is_routable_ip_v4(unsigned int ip[4]) ++ { ++ if (ip[0] == 127 || //127.0.0.0/8 (localhost) ++ ip[0] == 10 || //10.0.0.0/8 (private) ++ (ip[0] == 192 && ip[1] == 168) || //192.168.0.0/16 (private) ++ (ip[0] == 169 && ip[1] == 254) || //169.254.0.0/16 (link-local) ++ (ip[0] == 172 && ip[1] / 16 == 1)) //172.16.0.0/12 (private) ++ return false; ++ return true; ++ } + + +- if (data->set.ftp_skip_ip) ++ bool skipIp = data->set.ftp_skip_ip; ++ if (!skipIp && !is_routable_ip_v4(ip)) ++ { ++ unsigned int ip_ctrl[4]; ++ if (4 != sscanf(control_address(conn), "%u.%u.%u.%u", ++ &ip_ctrl[0], &ip_ctrl[1], &ip_ctrl[2], &ip_ctrl[3]) || ++ is_routable_ip_v4(ip_ctrl)) ++ skipIp = true; ++ } ++ ++ if (skipIp) + +__________________________________________________________________________________________________________ +/lib/ftp.c +https://github.com/curl/curl/issues/4342 + +- result = ftp_nb_type(conn, TRUE, FTP_LIST_TYPE); ++ result = ftp_nb_type(conn, data->set.prefer_ascii, FTP_LIST_TYPE); + +__________________________________________________________________________________________________________ + + +---------------------- +| libssh2 1.11.2_DEV | +---------------------- +__________________________________________________________________________________________________________ +src/session.c +memory leak: https://github.com/libssh2/libssh2/issues/28 + +-if (session->state & LIBSSH2_STATE_NEWKEYS) ++//if (session->state & LIBSSH2_STATE_NEWKEYS) + +__________________________________________________________________________________________________________ +move the following constants from src/sftp.h to include/libssh2_sftp.h: + + #define MAX_SFTP_OUTGOING_SIZE 30000 + #define MAX_SFTP_READ_SIZE 30000 +__________________________________________________________________________________________________________ + + +------------------- +| wxWidgets 3.3.1 | +------------------- +__________________________________________________________________________________________________________ +/include/wx/settings.h +Support changing system colors for proper dark mode support: + ++ struct wxColorHook ++ { ++ virtual ~wxColorHook() {} ++ virtual wxColor getColor(wxSystemColour index) const = 0; ++ }; ++ WXDLLIMPEXP_CORE inline std::unique_ptr& refGlobalColorHook() ++ { ++ static std::unique_ptr globalColorHook; ++ return globalColorHook; ++ } + +class WXDLLIMPEXP_CORE wxSystemSettings : public wxSystemSettingsNative +{ +public: ++ static wxColour GetColour(wxSystemColour index) ++ { ++ if (refGlobalColorHook()) ++ return refGlobalColorHook()->getColor(index); ++ ++ return wxSystemSettingsNative::GetColour(index); ++ } + +__________________________________________________________________________________________________________ +/src/aui/framemanager.cpp: +Fix incorrect pane height calculations: + +- // determine the dock's minimum size +- bool plus_border = false; +- bool plus_caption = false; +- int dock_min_size = 0; +- for (j = 0; j < dock_pane_count; ++j) +- { +- wxAuiPaneInfo& pane = *dock.panes.Item(j); +- if (pane.min_size != wxDefaultSize) +- { +- if (pane.HasBorder()) +- plus_border = true; +- if (pane.HasCaption()) +- plus_caption = true; +- if (dock.IsHorizontal()) +- { +- if (pane.min_size.y > dock_min_size) +- dock_min_size = pane.min_size.y; +- } +- else +- { +- if (pane.min_size.x > dock_min_size) +- dock_min_size = pane.min_size.x; +- } +- } +- } +- +- if (plus_border) +- dock_min_size += (pane_borderSize*2); +- if (plus_caption && dock.IsHorizontal()) +- dock_min_size += (caption_size); +- +- dock.min_size = dock_min_size; + ++ // determine the dock's minimum size ++ int dock_min_size = 0; ++ for (j = 0; j < dock_pane_count; ++j) ++ { ++ wxAuiPaneInfo& pane = *dock.panes.Item(j); ++ if (pane.min_size != wxDefaultSize) ++ { ++ int paneSize = dock.IsHorizontal() ? pane.min_size.y : pane.min_size.x; ++ if (pane.HasBorder()) ++ paneSize += 2 * pane_borderSize; ++ if (pane.HasCaption() && dock.IsHorizontal()) ++ paneSize += caption_size; ++ ++ if (paneSize > dock_min_size) ++ dock_min_size = paneSize; ++ } ++ } ++ ++ dock.min_size = dock_min_size; + +__________________________________________________________________________________________________________ +/src/gtk/menu.cpp + +-g_signal_connect(m_menu, "map", G_CALLBACK(menu_map), this); ++g_signal_connect(m_menu, "show", G_CALLBACK(menu_map), this); //"map" is never called on Ubuntu Unity, but "show" is + +__________________________________________________________________________________________________________ +/src/gtk/window.cpp +Backspace not working in filter dialog: http://www.freefilesync.org/forum/viewtopic.php?t=347 + + void wxWindowGTK::ConnectWidget( GtkWidget *widget ) + { +- static bool isSourceAttached; +- if (!isSourceAttached) +- { +- // attach GSource to detect new GDK events +- isSourceAttached = true; +- static GSourceFuncs funcs = +- { +- source_prepare, source_check, source_dispatch, +- NULL, NULL, NULL +- }; +- GSource* source = g_source_new(&funcs, sizeof(GSource)); +- // priority slightly higher than GDK_PRIORITY_EVENTS +- g_source_set_priority(source, GDK_PRIORITY_EVENTS - 1); +- g_source_attach(source, NULL); +- g_source_unref(source); +- } + + g_signal_connect (widget, "key_press_event", + G_CALLBACK (gtk_window_key_press_callback), this); + +__________________________________________________________________________________________________________ +/src/unix/sound.cpp +Fix crackling sound at the beginning of WAV playback: +See: http://soundfile.sapp.org/doc/WaveFormat/ => skip 8 bytes (Subchunk2ID and Subchunk2 Size) + +- m_data->m_data = (&m_data->m_dataWithHeader[data_offset]); ++ m_data->m_data = (&m_data->m_dataWithHeader[data_offset + 8]); + +__________________________________________________________________________________________________________ +/src/common/bmpbndl.cpp +DoGetPreferredSize()'s lossy "GetDefaultSize()*scaleBest" calculation can be 1-pixel-off compared to real image size => superfluous + ugly-looking image shrinking! + + wxSize wxBitmapBundleImplSet::GetPreferredBitmapSizeAtScale(double scale) const + { ++ //work around DoGetPreferredSize()'s flawed "GetDefaultSize()*scaleBest" calculation ++ for (const auto& entry : m_entries) ++ if (entry.bitmap.GetScaleFactor() == scale) ++ return entry.bitmap.GetSize(); ++ + return DoGetPreferredSize(scale); + } + +__________________________________________________________________________________________________________ +/src/gtk/button.cpp +now this is absurd: if we add wxTranslations for "&OK/&Cancel", wxWidgets will detect "wxIsStockLabel()", +throw away the label and do gtk_button_set_label(wxGetStockGtkID(wxID_OK/wxID_CANCEL)) instead, +which will result in *untranslated* confirmation buttons everywhere! +https://github.com/wxWidgets/wxWidgets/blob/6561ca020048de57ec28fec5b27f80b00d445cdd/src/gtk/button.cpp#L253 + +#ifndef __WXGTK4__ + wxGCC_WARNING_SUPPRESS(deprecated-declarations) +- if (wxIsStockID(m_windowId) && wxIsStockLabel(m_windowId, label)) +- { +- const char* stock = wxGetStockGtkID(m_windowId); +- if (stock) +- { +- gtk_button_set_label(GTK_BUTTON(m_widget), stock); +- gtk_button_set_use_stock(GTK_BUTTON(m_widget), TRUE); +- return; +- } +- } + wxGCC_WARNING_RESTORE() +#endif + +__________________________________________________________________________________________________________ +/src/common/toplvcmn.cpp +wxTopLevelWindow::Destroy() uses wxPendingDelete for deferred deletion during next idle event. +There might not be a next idle event! E.g. on GTK2 a hidden window doesn't receive idle events. +Reproduce on GTK2+KDE for toplevel window: Hide(); wxTheApp->Yield(); Destroy(); => process not exiting! https://freefilesync.org/forum/viewtopic.php?t=11935 + +- for ( wxWindowList::const_iterator i = wxTopLevelWindows.begin(), +- end = wxTopLevelWindows.end(); +- i != end; +- ++i ) +- { +- wxTopLevelWindow* const win = static_cast(*i); +- if ( win != this && win->IsShown() ) +- { +- // there remains at least one other visible TLW, we can hide this +- // one +- Hide(); +- +- break; +- } +- } ++ Hide(); ++ wxWakeUpIdle(); +__________________________________________________________________________________________________________ diff --git a/Changelog.txt b/Changelog.txt new file mode 100644 index 0000000..8010574 --- /dev/null +++ b/Changelog.txt @@ -0,0 +1,3244 @@ +FreeFileSync 14.6 [2025-12-02] +------------------------------ +Write sync statistics to stdout as JSON for .ffs_batch +Removed precompiled 32-bit bundle (Linux) +Avoid redundant window centering before finishing layout +GTK3-based build (Linux) +Dark mode support with GTK3 (Linux) +Stream errors to stderr instead of stdout (Linux) +Installer supports dark mode (Windows) +Added "New Window" dock menu item (macOS) + + +FreeFileSync 14.5 [2025-10-03] +------------------------------ +Quotation not needed anymore for external application macros +Unambiguous license key file extension +Fixed crash when resizing config panel during comparison +Fixed log file viewing when config name contains special characters +Dedicated installer for x86_64-only operating system (Linux) + + +FreeFileSync 14.4 [2025-07-26] +------------------------------ +Fixed FTP login error "dh key too small" +Updated all 3rd party libraries to latest versions + + +FreeFileSync 14.3 [2025-03-27] +------------------------------ +Support internationalized domain names (IDN) for (S)FTP and email +Log performance statistics for file content comparison +Support installation using Ptyxis terminal (Linux) +Support pausing countdown towards system shutdown +Support KDE Plasma 6 service menu (Linux) +Fixed crash on app exit when called by Cron (Linux) + + +FreeFileSync 14.2 [2025-02-20] +------------------------------ +Fixed crash when closing progress dialog after sync (Windows) + + +FreeFileSync 14.1 [2025-02-19] +------------------------------ +Further dark mode improvements +Fixed blurry icons due to image resizing glitch +Fixed RealTimeSync process not exiting while in taskbar (Linux) +Improved file icon loading performance +Improved extension handling for multi-file renaming +Mitigate icon size rendering bug for notification emails +Close popup dialogs using Ctrl+Enter while ignoring keyboard focus +Resume from system tray via single mouse click +Avoid white flash when resuming progress dialog from system tray (Windows) +Increased progress indicator UI update frequency + + +FreeFileSync 14.0 [2025-01-17] +------------------------------ +Dark mode support (Windows 10 20H1, macOS 10.14 (Mojave), Linux) +Fixed dock icon progress percentage divergence (macOS) +Prevent "App Napp during comparison/synchronization (macOS) +Enhance EINVAL error message for unsupported characters +Support running with background priority (Linux) +Fixed installer access denied when creating shell links (Windows) +Improved size and date formatting for file listing (macOS) +Improved context menu customization grid +Reduced peak memory consumption by 12% +Automatically set appropriate text color for config panel background +Revived and updated Italian translation + + +FreeFileSync 13.9 [2024-12-07] +------------------------------ +Fixed CURLE_SEND_ERROR: OpenSSL SSL_write: SSL_ERROR_SYSCALL, errno 0 +Added comparison and sync context menu options for multiple folder pairs +Show file include/exclude filter directly in tooltip +Fixed file not found error when cancelling file up-/download +Fixed showing cancelled config log status after nothing to sync +Updated translation files + + +FreeFileSync 13.8 [2024-11-04] +------------------------------ +Support raw IPv6 server address for (S)FTP +RealTimeSync: Fixed scrollbar when adding/removing folders +Don't set sync direction for partial folder pairs +Uniquely identify partial folder pairs in error message +Fixed network login prompt not showing in Windows 11 24H2 + + +FreeFileSync 13.7 [2024-06-23] +------------------------------ +Support copying symlinks between SFTP devices +Fixed input focus not being restored after comparison/sync +Fixed log file pruning not considering selected configuration +Show startup error details when running outside terminal (Linux) + + +FreeFileSync 13.6 [2024-05-10] +------------------------------ +Compact parent path display for medium/large row sizes +Fixed crash when mouse inputs are queued due to system lag +Don't steal focus from other app when sync progress dialog is shown +Fix crackling sound at the beginning of WAV playback (Linux) +Prevent middle grid tooltip from covering sync direction +Disable Nagle algorithm for SFTP connections + + +FreeFileSync 13.5 [2024-04-01] +------------------------------ +Wrap file grid folder paths instead of truncate +Fixed sync operation arrows for RTL layout +Fixed FTP hang during connection (libcurl regression) +Consider user-defined file time tolerance for DB comparisons +Don't log folder pair paths if nothing to sync + + +FreeFileSync 13.4 [2024-02-16] +------------------------------ +Ignore leading/trailing space when matching file names +Work around wxWidgets system logger clearing error code +Fixed registration info not found after App Translocation (macOS) +Avoid modal dialog hang on KDE when compiling with GTK3 +Change app location without losing Donation Edition status (macOS) + + +FreeFileSync 13.3 [2024-01-07] +------------------------------ +Completed CASA security assessment for Google Drive +Use system temp folder for auto-updating +Ignore errors when setting directory attributes is unsupported +Save GUI sync log file even when cancelled +Fixed Business Edition install over existing installation +Updated code signing certificates (Windows) + + +FreeFileSync 13.2 [2023-11-23] +------------------------------ +Complete high-DPI/Retina display support (macOS) +Prevent files from being moved to versioning recursively +Fixed tooltip line wrap bug for moved files (Windows) +Return first FTP parsing error when trying multiple variants +Allow file times from the future for Linux-style FTP listing +Fixed setting modification times on certain storage devices (Windows) +Fixed bogus "Sound playback failed" error message (macOS) +Fixed rename dialog text selection wobble (macOS) + + +FreeFileSync 13.1 [2023-10-23] +------------------------------ +Keep comparison results when only changing cloud connection settings +Sync button: indicate if database will be used +Remove leading/trailing space during manual file rename +Set environment variable "DISPLAY=:0" if missing (Linux) +Support dropping ffs_gui/ffs_real config on RealTimeSync directory input field + + +FreeFileSync 13.0 [2023-09-12] +------------------------------ +Rename (multiple) files manually (F2 key) +Configure individual directions for DB-based sync +Detect moved files with "Update" sync variant (requires sync.ffs_db files) +Update variant: Do not restore files that were deleted on target +Distinguish file renames from file moves and simplify grid display +Fixed ERROR_NOT_SUPPORTED when copying files with NTFS extended attributes +Fixed error during process initialization while connecting with quick launch +Avoid redundant file reopen when setting file times during copy +Set working directory to match FFS configuration file when double-clicking (Linux) + + +FreeFileSync 12.5 [2023-07-21] +------------------------------ +Merge logs of individual steps (comparison, manual operation, sync) +Show total percentage in progress dialog header +Log and report errors during cleanup or exception handling +Skip folder traversal if existence check fails for other side of the pair +Automatically adapt batch options to prevent hanging a non-interactive process (Windows) +Support path lists for external applications: %item_paths%, %local_paths%, %item_names%, %parent_paths% +Create directory lock files with hidden attribute +Don't clear other side when right-clicking file selection +Fixed passive FTP when using different IP than control connection +Work around FTP servers silently renaming unsupported characters of temporary file + + +FreeFileSync 12.4 [2023-06-20] +------------------------------ +Show dynamic error and warning count in progress dialogs +Show process elevation status in title bar (Administrator, root) +Fixed libcurl bug CURLE_URL_MALFORMAT for numerical host name +Don't discard config panel last log after no changes found +Set taskbar relaunch command to launcher executable (Windows) +Fixed Btrfs compression not being applied during copy (Linux) +Run on file systems with buggy GetFinalPathNameByHandle() implementation, e.g. Dokany-based +Save selected view mode (F11) in batch config file + + +FreeFileSync 12.3 [2023-05-17] +------------------------------ +Add custom notes to sync configurations +Highlight comparison and sync buttons +Show sync stats in config panel tool tip +Update config panel sync info even if cancelled +Support FTP listing format missing owner/group +Fixed "Class not registered" error during installation +Propagate process priority of launcher executable +Fixed config panel metadata being reset after renaming +Fixed config panel keyboard cursor after deletion/rename +Improved small icon resolution for high-DPI monitors + + +FreeFileSync 12.2 [2023-04-02] +------------------------------ +Fixed temporary access error when creating multiple folders in parallel +Log failure to copy folder attributes as warning only +Enable UTF-8, even if FTP server does not advertize in FEAT (vsftpd) +Fixed drag and drop for non-ASCII folders (macOS) +Explicitly detect MTP path without existence check +Fixed crash when parsing SFTP package from stream +Revert back to GTK2 build due to GTK3 hangs on KDE (Linux) +Fixed missing COM initialization for MTP path parsing + + +FreeFileSync 12.1 [2023-02-20] +------------------------------ +First official build based on GTK3 (Linux) +Allow cancel during folder path normalization (e.g. delay during HDD spin up) +Fixed slow FTP comparison performance due to libcurl regression +Open terminal with log messages on startup error (Linux) +Preserve changed config during auto-update +Save config during unexpected reboot (Linux) +Preserve config upon SIGTERM (Linux, macOS) +Fixed progress dialog z-order after switching windows (macOS) +Removed packet size limit for SFTP directory reading +Mouse hover effects for config and overview grid +Always update existing shortcuts during installation (Windows, Linux) +Fixed another "Some files will be synchronized as part of multiple base folders" false-negative + + +FreeFileSync 12.0 [2023-01-21] +------------------------------ +Don't save password and show prompt instead for (S)FTP +Fast path check failure on access errors +Support PuTTY private key file version 3 +Respect timeout during SFTP connect +Removed 20-sec timeout while checking directory existence +Avoid hitting (S)FTP connection limit for non-uniform configs +Fixed middle grid tooltip icon not always showing (Linux) +Optimized file accesses when checking file path existence +Fixed overview navigation marker not always showing on main grid +Clear all grid selections after view filter toggle +Fixed mouse selection starting on folder group +Don't require sudo during non-root installation (Linux) +Stricter type checking when deleting file/folder/symlinks +Succinct error messages when path component is not existing + + +FreeFileSync 11.29 [2022-12-16] +------------------------------- +Fixed crash after 1-byte file copy from MTP device +Fixed incorrect installer z-order during auto-update (macOS) +Compress copied file only if target folder is marked as NTFS-compressed (Windows) +Show install errors without requiring access to "System Events" (macOS) +Fall back to creation time if modification time is missing on MTP device +Copy/paste filter config via operating system clipboard +Show FreeFileSync startup error message when called from RealTimeSync +Avoid server round trip when preparing summary email +Show path conflict warning aggregated into groups +Don't assume path conflict if single write and multiple ignored items +Fixed CTRL + Insert clipboard copy for some text controls (Windows, Linux) + + +FreeFileSync 11.28 [2022-11-16] +------------------------------- +Recover from corrupted database file +Save database files pair-wise as a transaction +Fixed FTP access for Xiaomi "File Manager" +Fixed filter full path detection for root directory (Linux/macOS) +Fixed recycle bin double initialization bug (Windows) +Fixed incorrect case-insensitive string comparison for i and ı +Round progress percentage numbers down + + +FreeFileSync 11.27 [2022-10-17] +------------------------------- +Fixed "Some files will be synchronized as part of multiple base folders" false-negative +Fixed "Unexpected size of data stream" for Google Drive +Fixed crash when downloading empty file from Google Drive +RealTimeSync: fixed ffs_batch not accepted as valid configuration +Fixed top buttons vertical GUI layout +Fixed progress dialog font on Ubuntu MATE +Support cut/copy/paste for filter settings +Fixed free disk space calculation if target folder not yet created + + +FreeFileSync 11.26 [2022-10-06] +------------------------------- +Faster file copy for SSD-based hard drives (Linux, macOS) +Don't fill the OS file cache during file copy (macOS) +Removed redundant memory buffering during file copy +Fixed ERROR_FILE_EXISTS on Samba share when copying files with NTFS extended attributes +Show warning when recycle bin is not available (macOS, Linux) +Customize config item background colors +Fixed macOS menu bar not showing after app start +Fixed normalizing strings with broken UTF encoding +Fixed sound playback not working (Linux) +Don't allow creating file names ending with dot character (Windows) + + +FreeFileSync 11.25 [2022-08-31] +------------------------------- +Fixed crash when normalizing Unicode non-characters +Fixed crash when accesssing Google Drive +Fixed regession for decomposed Unicode comparison +Fixed "exit code 106: --sign is required" error on macOS +Reset icon cache after each comparison + + +FreeFileSync 11.24 [2022-08-28] +------------------------------- +Enhanced filter syntax to match files only (append ':') +Fixed "Some files will be synchronized as part of multiple base folders": no more false-positives +Detect full path filter items and convert to relative path +Auto-detect FTP server character encoding (UTF8 or ANSI) +Cancel grid selection via Escape key or second mouse button +Apply conflict preview limit accross all folder pairs +Require config type and file extension to match +Fixed view filter panel vertical layout +Strict validation of UTF encoding + + +FreeFileSync 11.23 [2022-07-23] +------------------------------- +Format local file times with no limits on time span +Deferred child item failure when traversing MTP folder +Fixed occasional wrong thumbnail orientation for MTP +Support additional image formats for MTP preview (e.g. CR2) +Fixed folder pair window being squashed after text size increase +Fixed wrong folder pair order when loading config (Linux) +Fixed some images being stretched on high-DPI monitors +Fixed config panel tab text being mirrored in RTL layout +Fixed parsing file times one second before Unix epoch (Gdrive, FTP) + + +FreeFileSync 11.22 [2022-06-23] +------------------------------- +Allow to change default log folder in global settings +Fixed sort order when items existing on one side only +Consider HOME environment variable for home path (Linux) +Fixed config selection using shift and arrow keys +Start comparison, then sync by only pressing Enter after startup +Fall back to default path when failing to save log file +Improved relative config path handling in portable mode + + +FreeFileSync 11.21 [2022-05-17] +------------------------------- +Support volume GUID as path: \\?\Volume{01234567-89ab-cdef-0123-456789abcdef} (Windows) +Avoid Two-Way conflict when changing folder name upper/lower-case +List hidden warning messages in options dialog +Fixed buffer overflow while receiving SFTP server banner +Create crash dumps even if FFS-internal crash handling doesn't kick in +Log time when error occured, not when it is reported +Swap sides: Require confirmation only after comparison +Updated translation files + + +FreeFileSync 11.20 [2022-04-17] +------------------------------- +Fixed broken icon scaling on high-DPI displays +Fixed user language set to English after update + + +FreeFileSync 11.19 [2022-04-16] +------------------------------- +Improved performance for huge exclusion filter lists: linear to constant(!) time +Support sync with Google Drive starred folders +Access "My Computers" (as created by Google Backup and Sync) if starred +Western Digital Mycloud NAS: fixed ERROR_ALREADY_EXISTS when changing case +Added per-file progress for "copy to" function +Have filter wildcard ? not match path separator +Work around WBEM_E_INVALID_NAMESPACE error during installation +Fixed login user incorrectly displayed as root (macOS) +Save Google Drive buffer before system shutdown + + +FreeFileSync 11.18 [2022-03-07] +------------------------------- +Add comparison time to sync log when using GUI +Added user-configurable timeout for Google Drive +Consider port when comparing (S)FTP paths for equality +Fixed SFTP key file login error on OpenSSH_8.8p1 +Add error details for NSFileReadUnknownError (macOS) +Disable new config button when already at default +Use user language instead of region locale during installation + + +FreeFileSync 11.17 [2022-02-04] +------------------------------- +Show per-file progress in percent when copying large files +Log app initialization errors +Fixed uncaught exception after installation +Defer testing for third-party buggy DLLs until after crashing +Consider ReFS 128-bit file ID failure states (Windows) +Refer to volume by name: support names including brackets +Support local installation with non-standard home (Linux) + + +FreeFileSync 11.16 [2022-01-02] +------------------------------- +Allow to select and remove invalid config file +Migrated all HTTPS requests to use libcurl (Linux, macOS) +Set keyboard focus on config panel after startup +Added computer name to log file trailer +Context menu instead of confirmation dialog for swap sides +Fixed config selection lost after auto-cleaning obsolete rows +Install app files with owner set to root (Linux) +Don't override keyboard shortcut "CTRL + W" (macOS) +Migrated key conversion routines deprecated in OpenSSL 3.0 +Boxed app icon to fit OS theme (macOS) +Fixed manual retry after automatic update check error +Fixed missing ampersands in middle grid tooltip + + +FreeFileSync 11.15 [2021-12-03] +------------------------------- +Play sound reminder when waiting for user confirmation +Enhanced crash diagnostics with known triggers +Defer reporting third-party incompatibilities until after crashing +Fixed Server 2019 not being detected for log file +Use native representation for modified config (macOS) +Improved WinMerge detection for external app integration + + +FreeFileSync 11.14 [2021-09-20] +------------------------------- +Authenticate (S)FTP connections using OpenSSL 3.0 +Fixed E_NOINTERFACE error after synchronization +Preempt crashes due to Nahimic Sonic Studio 3 +Hide main window when minimizing progress window (macOS) +Avoid second dock icon when minimizing progress window (macOS) + + +FreeFileSync 11.13 [2021-08-17] +------------------------------- +Manage default filter settings via GUI +Support arbitrary location for local app installation (macOS) +Fixed ERROR_FILE_NOT_FOUND masking real file access error (Windows) +Copy full file paths to clipboard (CTRL + C) +Preserve clipboard contents until after program exit +Always enable external command if independent of file items +Support installation without Rosetta2 on ARM64 (macOS) + + +FreeFileSync 11.12 [2021-07-15] +------------------------------- +Native ARM64 build to support Apple silicon M1 (macOS) +Non-intrusive mouse highlight on file grid +Fixed /lib/i386-linux-gnu/libgcc_s.so.1: version `GCC_7.0.0' not found +Parse file times with no limits on time span (e.g. year 0, year 3000) +Show folder icon during drag and drop (Windows) +Show user name for (S)FTP display paths +Fixed FTP connection lost error with TLS 1.3 +Present file sizes in powers of 1000 bytes (Linux, macOS) + + +FreeFileSync 11.11 [2021-06-11] +------------------------------- +Fixed Shared Drive synchronization with Google Drive +Directly open exported file list (.CSV) as temporary file +Avoid EIO error for F_PREALLOCATE (macOS) +Watch socket using "poll" instead of "select" (Linux, macOS) +Fixed user-specific time/date format (Windows) +Fixed system_profiler not found error (macOS) + + +FreeFileSync 11.10 [2021-05-09] +------------------------------- +Fixed comparison results cleared after mouse-scrolling the first folder pair +Stricter base folder existence checks before synchronization +Disable all file pairs when base folder status cannot be determined +Fixed sync statistics if base folder existence test failed +Work around glitch in grid scrollbar size calculation +Fixed folder drag and drop failing after locale conflict (macOS) +Fixed incorrect MIME permissions after installation (Linux) +Stricter server response validation during update check +Fixed incomplete item path in log if source item is missing +Fixed installation error when running ConEmu +Support starting FreeFileSync as root login user (Linux) + + +FreeFileSync 11.9 [2021-04-01] +------------------------------ +Save different layouts depending on screen resolution +Fixed large file icon scaling quality (Windows) +Fixed broken default filter excluding DocumentRevisions (macOS) +Don't immediately exit terminal when installer error is showing (Linux) +Explicitly set file permissions when installing missing directories (Linux) +Support installation using noexec temp directory (Linux) +Don't fail installation if root is the only user (Linux) +Added automatic socket close on execv (Linux, macOS) +Fixed Google Drive login hanging after authentication (Linux) +Correctly generate and parse Windows epoch time (Windows, macOS) + + +FreeFileSync 11.8 [2021-03-03] +------------------------------ +Fixed unexpected file size error when copying to (S)FTP, and Google Drive + + +FreeFileSync 11.7 [2021-03-01] +------------------------------ +Detect moved files on FTP (if server supports MLSD) +Allow installation only for current or all users (Linux) +Added application uninstaller: uninstall.sh (Linux) +Use login user config path when running as root (macOS, Linux) +Fixed detection of moved files with unstable device IDs (macOS, Linux) +Strict checking for duplicate file IDs +Avoid EINVAL invalid argument error when using F_PREALLOCATE (macOS) +Restore input focus after closing log panel +Double-click on file to open Google Drive web interface +Fixed alpha channel image scaling glitch +Fixed recycle bin folders being created recursively +Fixed thread count status message fluctuation +Don't quit FreeFileSync when parent terminal is closed (SIGHUP) +Fixed "Operation not supported" error when setting directory locks +Show folder picker despite SHCreateItemFromParsingName() error +Work around "OLE received a packet with an invalid header" error + + +FreeFileSync 11.6 [2021-02-01] +------------------------------ +New FreeFileSync installer (Linux) +New auto-updater for the Donation Edition (macOS, Linux) +Support reading FTP file symlinks +Added context menu option "Edit with FreeFileSync" (Linux, KDE) +Support starting via symlink (macOS) +Command line support with "freefilesync" symlink in /usr/local/bin (macOS) +Fixed starting via symlink found by PATH (Linux) +Preserve keyboard focus when starting sync via F9 +Don't show relative parent path if folder does not exist +Added high-resolution application icons (Linux, macOS) +Work around "500 'HELP' command unrecognized" FTP error +Fixed menu bar icon not being removed immediately (macOS Big Sur) +Don't allow folder names ending with dot character (Windows) +Mitigate ERROR_ALREADY_ASSIGNED: Local Device Name Already in Use [Wnetaddconnection2] +Fixed startup failure when app folder contains back quote char (macOS) +Fixed network card not found error on virtual machine (KVM Linux) +Fixed RTL layout direction in popup dialogs + + +FreeFileSync 11.5 [2021-01-02] +------------------------------ +New configuration context menu option to delete from disk +Start auto retry delay at time of error instead of reporting +Added error details to status message before retry +Improved color scheme to better integrate with system colors +Keep partial SFTP results after network failure +Fixed incorrect panel font (macOS Big Sur) +Fixed SFTP retry not working after network drop +Fixed crash on exit with floating panels (macOS Big Sur) +Fixed auto-close option not being remembered +Fixed installer high-DPI scaling issues +Fixed mouse hover issues with grid column header +Fixed menu bar icons not showing (Linux) +Removed redundant GUI layout recalculations +Keep correct panel sizes after log panel maximize +Support modern folder picker in installer +Don't raise progress dialog after sync when resuming from systray + + +FreeFileSync 11.4 [2020-12-04] +------------------------------ +New progress graph "this one sparks joy" +Remember progress dialog size +New config file context menu option "Show in file manager" +Work around libcurl performance bug during FTP upload +Only log modification time errors after comparing by size or content +Smaller icon size for efficient screen layout (Linux) +Use system-native recycle bin icon +Fixed DeviceIoControl(IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS): ERROR_MORE_DATA +Support MTP devices lacking a friendly name +Fix grid scrolling with small mouse rotations (macOS) +Faster mouse scrolling on high-DPI resolution displays +Keep previous windows size when maximized during auto-exit + + +FreeFileSync 11.3 [2020-11-01] +------------------------------ +Enhanced main grid color scheme +Mouse-highlight for file selection +Added file create/delete indicators +Show file list tooltip for missing items +Click folder name and scroll to group start +Log failure to create application default config folder +Added tooltips and fixed help link context menu +Fixed tooltip not updated when scrolling (macOS, Linux) +Move error dialogs to foreground during batch sync +Align context menu popup positions +Updated translation files + + +FreeFileSync 11.2 [2020-10-02] +------------------------------ +Improved grid layout with file icons hidden +Improved rendering of inactive and disabled grid items +Remember last user-selected paths for file and folder pickers +Fixed folder name hidden in "item name" view type +Fixed determination of unsupported trash folder (Linux) +Fixed copying broken symlinks (macOS) +Fixed default action when pressing Enter in popup dialogs +Fixed default popup dialog size (macOS) +Use localized start of week for %WeekDay% (Linux, macOS) +Swap sides using CTRL+W instead of F10 +Show confirmation dialog before swapping sides + + +FreeFileSync 11.1 [2020-08-31] +------------------------------ +New file group layout on main grid (reloaded) +Alternate colors for main grid folder groups +Added file group context menu +Quick selection of items in folder group +Fixed FTP access errors with Explicit SSL/TLS +Fixed Google Drive error when double quotes in file name +Fixed RTL layout bug with number input control +Fixed grid column default sizes +Fixed grid rendering performance during mouse scrolling +Update all config files transactionally +Respect user-preferred number/time format (Linux) +Fixed floating panels not being resizable (Linux) +Instantly open selection context menu on right mouse button down +Further improved high DPI support +Updated deprecated system API calls (requires macOS 10.10 or later) +Fixed crash when accessing Nexis storage (macOS) +Avoid buffer flush when aborting native file output +Clear preview after folder history selection +Pre-allocate target file without setting size +Unified system error message formatting + + +FreeFileSync 11.0 [2020-07-21] +------------------------------ +Revised file layout on main grid +Skip download/upload when copying Google Drive files inside account +Support moving Google Drive files between shared drives and My Drive +Support copying Google Drive shortcuts between accounts +Support copying Google Docs, Sheets, Slides, etc. within account +Fixed parsing uninitialized Google Drive modification time +Fixed Google Drive file already existing check running too late +Ignore slash/backslash differences during manual search +Avoid creating orphan database entry if one DB file fails to load +Limit modification time error count for log file warning message +Support copying WSL symlinks +Avoid duplicate MTP/Google Drive item creation from multiple threads +Fixed TMPDIR not found during startup (macOS) +Added sync variant icons +Avoid redundant icon format conversions +Buffer high-DPI image scaling results +Improved MTP thumbnail scaling performance +Avoid race condition during parallel file icon rendering (Linux) +Allow creating folder name with leading/trailing spaces +Start supporting GTK3 (Linux) + + +FreeFileSync 10.25 [2020-06-18] +------------------------------- +New file tree layout for main grid +Support Google Drive Shared Drives +Support Google Drive Shortcuts +Prioritize item name rendering if lacking horizontal space +Report "out of memory" during startup instead of crashing +Fixed excess memory consumption when loading variable-size data blocks +Fixed VERSION_ID missing on Arch Linux +Fixed IWbemServices::ConnectServer error during auto-update +Fixed row being skipped during main grid page up/down +Fixed MSSearch files not found when using Volume Shadow Copy +Allow creating folder names with trailing dot +Improved sort by full path speed and folder ordering +Report detailed error when failing to parse FTP MLSD +Sort by path component names instead of relative path +Support access to MEGAcmd FTP server +Fixed Google Drive error when removing last parent of shared item +Fixed Google Drive owned+shared files being unlinked instead of deleted +Fixed Google Drive change notification evaluation for item without parents +Support double-click/"Browse directory" for (S)FTP/Google Drive (Linux) + + +FreeFileSync 10.24 [2020-05-17] +------------------------------- +Increased SFTP buffer sizes for faster upload/download +New %WeekDay%, %WeekDayName", and %MonthName% macros +Support Linux systems without lsb_release +Don't exclude desktop.ini by default +Merge error messages of failed error handling +Added ".DocumentRevisions-V100" to default exclude filter (macOS) +Fixed deletion error not reported during versioning +RealTimeSync: don't block when command fails with exit code > 0 +Visualize error status in macOS Dock and Windows Superbar +Show error code constants for Windows Shell errors +Suppport ProFTPD with "MultilineRFC2228 on" +SFTP option to enable/disable zlib compression + + +FreeFileSync 10.23 [2020-04-17] +------------------------------- +Run "on completion" commands on console (no need for "cmd.exe /c") +Check exit code and report errors for external applications +Report stream output of failed command line calls (macOs, Linux) +Use Unicode symbols compatible with older macOS +RealTimeSync: invoke command using cmd.exe instead of ShellExecute (Windows) +Avoid hitting log file length limitations for aggregated jobs +Fix OpenSSL failing on HTTP 1.0 response without Content-Length +Don't allow creating folder names ending with space or dot +Support base folders with trailing blanks +Show system error descriptions on Volume Shadow Copy errors +Raise exit code if saving log file or sending email failed +Report all documented MTP error descriptions +Updated default exclude filter (macOS/Linux) +Added image outlines for improved dark mode support +Work around WBEM_E_INVALID_CLASS error during installation +Align file path rendering with app layout direction +Play sound notification also when "cancel on first error" is set +Cleaner file path formatting (macOs, Linux) +Added instructions when failing to start due to missing GTK2 (Ubuntu) +RealTimeSync: distinguish drive unmount from folder change notification +Avoid blocking command scripts waiting for user input +Updated translation files + + +FreeFileSync 10.22 [2020-03-18] +------------------------------- +Fixed upper-case conversion bug for non-ASCII strings + + +FreeFileSync 10.21 [2020-03-17] +------------------------------- +Preselect last-used email address +Select log file format (HTML or plain text) +Aggregate email notifications when hitting sending limits +Show code literals in system error messages +Limit conflict item count for log file warning message +Show log icon error indicator even if error occurred after sync +Disable background drag & drop when showing modal dialog +Hide dummy model, vendor names in log files +Fixed ANSI encoding used for log file time formatting +Reduced memory consumption for large number of log messages +Correctly parse lock files despite corrupted trail data +Show emoji instead of Unicode icon in email subject +Fixed IWbemServices::ConnectServer error after sync +Fixed aggregate email logs incomplete truncation + + +FreeFileSync 10.20 [2020-02-14] +------------------------------- +Send email notifications after sync (Donation Edition) +Generate log files in HTML format +Detect sync database consistency errors +Start log file with preview of first 25 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 +Count moved file pair as one update in view filter buttons +Fix command button default sizes (Windows) +Added %item_name%, %item_name2% context menu macros +Support deleting references to shared Google Drive files +Trash Google Drive files only when having single parent +Fixed high DPI scaling issue on image borders +Preserve system date format for RTL languages +Fall back to folder path if resource archives are missing + + +FreeFileSync 10.18 [2019-11-19] +------------------------------- +Save/load database files in parallel +Show item count for each view filter category +Group config history items via background colors +Allow grid sort by category and sync action +Reduced file accesses for faster start up +Buffer redundant database loads +Fix ibus initialization hang on Ubuntu 19.10 +Defer showing progress panel for short-lived tasks +Calculate stable scrollbar dimensions on GTK2 +Log mod time errors even when sync is cancelled +Show progress and errors when updating sync directions +Detect MLSD support despite invalid FTP FEAT response +Improved GUI responsiveness during config load +Added Vietnamese translation + + +FreeFileSync 10.17 [2019-10-17] +------------------------------- +Support PuTTY private key files for SFTP login +Enable zlib compression for SFTP servers if supported +Update last sync time despite differences if nothing to do +Reduce graph total time update interval +Remember folder history not just for first folder pair +Allow unprivileged symlink creation in Windows Developer Mode +Integrate latest libcurl FTP bug fixes +Detect common invalid SFTP key file formats +Fixed startup crash caused by corrupted HDD properties +Allow SFTP access via Ed25519 key in PKIX format + + +FreeFileSync 10.16 [2019-09-16] +------------------------------- +Redesigned progress indicator graphs +Avoid needless HTTP delay prior to Google Drive upload +Skip redundant CWDs during FTP metadata updates +Fixed MLSD 501 syntax error on Serv-U FTP server +Check FTP server status using FEAT/HELP instead of root folder +Avoid redundant TYPE changes during FTP directory listing +Access FTP files by full path and avoid CWDs +Support FTP home paths with non-ASCII chars +Work around libcurl bug failing to buffer FTP TLS authentication +Skip redundant FTP SIZE check before downloading file +Use ISO 8601 week of the year definition for %week% macro +Show login prompt for disconnected NAS share +Force icon resolution to 96 DPI in GTK2 build (Linux) +Notify missing full disk access permission (macOS) +Fixed accessibility issue with progress graph colors +Use short naming convention when deleting abandoned folder lock +Detect endless folder lock recursion on buggy file systems +Fixed Google Drive parsing error for invalid file time + + +FreeFileSync 10.15 [2019-08-15] +------------------------------- +Redesigned progress indicator stats +Fixed crash when progress dialog is closed right before showing error +Consider fail-safe file copy when creating sync.ffs_db files +Prepare support for GTK3 GUI framework (Linux) +Support sound output via SDL (Linux) +Shrink standard system icons if needed (Linux) +Add Windows Defender exclusions asynchronously +Fixed main dialog out-of-screen position on startup (macOS) +Activated CDN for all web accesses +Redirect error dialog to stderr during sound playback (Linux) +Updated translation files + + +FreeFileSync 10.14 [2019-07-14] +------------------------------- +Warn if versioning folder paths differ only in case +Fixed empty HTTP response during update check (macOS/Linux) +Warn if Donation Edition is active on unexpected number of machines +Use subdomain for application update checks +Consider cache control for HTTP GET requests +Access all web endpoints over TLS +Fixed character encoding issue in update reminder (macOS/Linux) + + +FreeFileSync 10.13 [2019-06-13] +------------------------------- +Allow to rename configurations via context menu +Work around hang on SMB network with broken FileFullDirectoryInformation +Work around SMB share returning empty item name +Detect and preempt keyman64.dll crash on exit +Manage notification sounds via global options dialog +Support 32-bit Debian Jessie and later releases +Work around silent failure to case-only rename on FAT drives (Windows 10) +Simplified installation folder structure +Update main grid scrollbars when resizing columns on other side +Preserve input focus when clicking on grid column label +Buffer result of process path normalization +Mirror middle grid icons for RTL layout (Linux) +Force LTR layout until wxWidgets supports RTL (macOS) +Fixed pair scrolling mismatch when grid height is exceeded by one row +Fixed startup failure due to missing /etc/machine-id (Linux) + + +FreeFileSync 10.12 [2019-05-12] +------------------------------- +Show sync start time and date in progress dialog title +Added duration of comparison to log +Show all total times in full HH:MM:SS format +Added sync start time to log file header +Add Windows Defender exclusions to fix CURLE_PARTIAL_FILE +New RealTimeSync option to hide console window +Support launching through symlink (Windows) +Dropped support for Windows XP, Server 2003, and Vista +Reduced installation size by 25% + + +FreeFileSync 10.11 [2019-04-11] +------------------------------- +Last FreeFileSync version supporting Windows XP and Vista +Fixed crash on multi-monitor set up +Fixed dialogs not showing after opening UAC prompt +Support launching through symlink (Linux) +Added example desktop starter files (Linux) +Fixed misleading error when determining file permissions support +Updated wxWidgets, libcurl, libssh2, VS, GCC, Xcode + + +FreeFileSync 10.10 [2019-03-10] +------------------------------- +New option: synchronize selection +Dynamically disable unsuitable context menu options +Support MTP devices without move command +Fall back to copy/delete when implicitly moving to different device (e.g. symlink) +Fixed incorrect statistics after parallel move +Fixed menu button not triggering context menu +Fixed crash on focus change while message popup is dismissed +Fixed crash when trying to shrink empty image +Fixed invisible dialogs when monitor is turned off in multi-monitor setup +Work around GetFileInformationByHandle error code 58 on WD My Cloud EX +Changing deletion handling now correctly triggers updated config +Support root-relative FTP file paths (e.g. FreeNAS) +Move and rename MTP items as a transaction +Exclude AppleDouble files (._) via default filter on macOS +Support home path for FTP folder picker +Use server default permissions when creating SFTP folder +Use native OpenSSL AES-CTR rather than libssh2 fallback +Added context information for cloud connection errors +Updated translation files + + +FreeFileSync 10.9 [2019-02-10] +------------------------------ +Added FTP, SFTP, Google Drive support for Linux +FreeFileSync Donation Edition available for Linux +Compress file stream during Google Drive upload +Navigate beyond access-denied parents in SFTP folder picker +Fixed unexpected stream size error during FTP upload +Support native recursive deletion for Google Drive +Support native recursive deletion for MTP +Deterministically save Google Drive state during exit +Work around missing TMPDIR variable (Linux) +Support SFTP servers returning large package sizes during folder reading +Start with home path when using SFTP folder picker +Aggregate device authentication prompts during comparison +Clean up temp file after unexpected stream size error +Work around FTP servers not supporting HELP command +Support parsing path by volume name when volume is missing +Parse and streamline Google Drive error messages +Load next item after deleting from config history +Avoid redundant Google Drive syncs after file/folder creation +Avoid duplicate MTP item creation by multiple threads + + +FreeFileSync 10.8 [2019-01-15] +------------------------------ +Support synchronization with Google Drive +Don't reset sync directions when changing versioning or deletion handling +Save last sync time before shutting down system +Support MTP devices that accept modTime only during file creation +Avoid dependency on file id to detect duplicate folders (buggy network drivers) +Check if path exists before creating duplicate MTP folder +Check for empty MTP item name during folder traversal +Check if multiple MTP items are referenced by the same path +Fixed sync config GUI distortion when toggling auto retry (Linux, macOS) +Fixed FreeFileSync sort order in Windows Uninstall Programs +Fixed log override path being squashed on high DPI +Fixed volume serial not considered when file id is missing + + +FreeFileSync 10.7 [2018-12-12] +------------------------------ +Correctly resolve ambiguous paths in (S)FTP folder picker +Fixed path alias check to not rely on volume serial number +Check already existing move target by ID instead of path (Linux, macOS) +Use native image conversion routines in installer +Added base folder info for unresolved conflicts message +Avoid silent failure when setting epoch modTime (Windows) +Fixed RealTimeSync failing to start FreeFileSync batch (macOS) +Support command arguments and exit code with launcher (macOS) +Consider UTF encoding when trimming long temp name during file copy +Exclude failed item paths containing backslash in names (Linux) +Fixed RealTimeSync GUI distortion after drag & drop (Linux) +Fixed parsing locale with unexpected format (Linux) + + +FreeFileSync 10.6 [2018-11-12] +------------------------------ +Detect and skip traversing folder path aliases +Report conflict when names differ only in Unicode normalization +Unified 32 and 64 bit into single package (Linux) +Notarized application package (macOS) +Save configuration files in user-specific paths (Linux) +Use XDG-style config file paths (Linux) +Fixed (fake) intermittent hangs during comparison (Linux, macOS) +Detect SMB mount points as separate devices (Linux) +Consider /mnt subfolders as device root paths (Linux) +Create missing default log folder upon first run +Don't consider final status for error/warning count +Discard invalid SFTP session after max channel determination +Fixed main dialog position not being remembered (Linux) +Fixed imprecise FTP times due to MLST parsing issue +Fixed application menu not being localized (macOS) +Fixed temp file name hitting file system length limitations +Fixed fatal errors not being written to console (Debian Linux) +Updated translation files + + +FreeFileSync 10.5 [2018-10-11] +------------------------------ +New file matching algorithm considering Unicode normalization +User-configurable timeout for FTP and SFTP connections +Ignore case sensitivity during filter matching (Linux) +Obsoleted old CHM manual in favor of PDF +Unicode-normalized and faster case-insensitive grid search +New button to save current view filter settings as default +Both slash and backslash can be used in filter expressions +Improved Unicode case conversion routines +Keyboard shortcuts for swap sides (F10) and view category (F11) +Don't steal input focus when closing progress dialog (macOS) +Fixed shutdown crash when accessing already destroyed state +Fixed file grid column order not being preserved +Fixed manual activation input fields being disabled (macOS) +Fixed FTP parsing error due to invalid folder time +Fixed statistics boxes background distortion (macOS) + + +FreeFileSync 10.4 [2018-09-09] +------------------------------ +Allow overriding log folder path for GUI and batch runs +Fixed RealTimeSync not triggering when using volume path by name +Fixed reading FTP folders including wildcard chars +Fixed image overlay graphics glitch (Linux) +Don't show error if versioning folder is not yet existing +Fixed crash when removing folder pair just before comparison (F5) +Fixed crash when parent folder of newly-moved file is deleted after comparison +Fixed statistics when folder containing moved files is found missing + + +FreeFileSync 10.3 [2018-08-07] +------------------------------ +New log panel showing details about the last operation +Show status of last syncs in configuration panel +Access log files via the configuration panel +Allow auto-retry and ignore errors during comparison +Show folder RealTimeSync is waiting on +New %logfile_path% macro for "on completion" command +Show errors and warnings count in log file header +Fixed crash when resizing panel during comparison +Fixed folders created hidden when source is a volume root path +Use steady clock while waiting in RealTimeSync +Fixed folder access error with Google Drive File Stream +Open global log folder path via options dialog +Limit global logs by age instead of size +Deprecated batch-level log files and LastSyncs.log + + +FreeFileSync 10.2 [2018-07-06] +------------------------------ +Limit number of file versions by age and count +Report not yet existing folders as warning instead of error +Improved comparison speed for high-latency traversals +Set up parallel file operations for versioning folder +Early clean up to avoid hitting (S)FTP connection limits +Support FTP servers with ANSI encoding +Fixed folder drag and drop for modal dialogs +Fixed progress graph glitch caused by unsteady system clock +Unbuffered folder lock file existence checking +Fixed macOS Donation Edition not being recognized after bundle rename +Updated translation files + + +FreeFileSync 10.1 [2018-06-03] +------------------------------ +Binary-compare multiple files in parallel +Copy file permissions when creating base folders +Fixed hang when scrolling file list (Windows) +Fixed file list mismatch when cancelling sync +Fixed delay when cancelling folder existence check +Fixed comparison and sync processing order to honor FIFO +Fixed startup delay when internet is offline (Linux, macOS) +Fixed crash when closing FreeFileSync via the macOS Dock +Support installation without admin rights (macOS) +Fixed bcrypt.dll not found on startup (Windows XP) +Respect Content-Length header for HTTP requests +Support parallel folder traversal on Ubuntu 16.4 +Fixed missing shared library dependencies (Linux) +Unified precompiled Linux binary packages + + +FreeFileSync 10.0 [2018-04-27] +------------------------------ +The installer is now ad-free! +Sync multiple files in parallel (Donation Edition) +Compare multiple files in parallel within a single folder tree +Aggregate worker threads per device during folder traversal +Reset GUI layout configuration for high DPI displays +Keep GUI responsive during synchronization +Remember maximum number of visible folder pairs +Fixed high DPI issues in installer +Don't delay errors by callback interval during comparison +Handle concurrent intermediate folder creation for versioning +Sync all folder level items before recursion (avoid CWDs) +Updated translation files + + +FreeFileSync 9.9 [2018-03-09] +----------------------------- +High DPI display support +Allow automatic retry at configuration level +Show error handling settings during sync +Avoid libpng.so dependency (Linux) +Fixed undefined behavior closing paused progress dialog +Check if buggy DLLs are loaded into address space (Windows) +Fixed FTP parsing error for Windows CE device +Workaround VSS provider implementation bug +Respect macOS user settings for date and thousands separator +Updated translation files + + +FreeFileSync 9.8 [2018-02-06] +----------------------------- +New option to auto-close progress dialog +Update last sync time if no differences found +Added 5 seconds countdown before shutdown/sleep +Preserve XML attribute creation order +Support HTTPS web accesses without redirect +Connect network share upon logon type not granted +Fixed invalid pointer error when reading MTP +Fixed temporary db file triggering RealTimeSync +Fixed runtime error during uninstallation +Continue status updates during sync cancellation +Log number of items found during comparison +Warn about outdated nviewH64.dll instead of crashing +Show default log file path when saving a batch job +Consider only full days for time since last sync + + +FreeFileSync 9.7 [2018-01-12] +----------------------------- +New configuration management panel +New column showing days since last sync +Support starting FreeFileSync via Windows Send To +Minimized memory operations for I/O buffer +Allow multiple config selections on Linux +New command line option -DirPair +Fixed Enter key not working for most dialogs (macOS) +Show only one warning about failed directory locks +Show correct synchronization time when resuming from system sleep +Don't resolve symlinks that are dropped via mouse +Detect and notify LCMapString compatibility mode bug +Fixed incorrect file permissions within macOS bundle +Fixed wrong results dialog panel selection (Linux) + + +FreeFileSync 9.6 [2017-12-07] +----------------------------- +New installation command line option /disable_updates +Fixed crash when closing main dialog during sync +Fixed RealTimeSync crash after recursive mutex locking +Improved file copy performance on macOS +Clean up obsolete files during installation +Don't use threads for running async command line (Linux) +Avoid main dialog flash after minimized sync +Disable file list export until after comparison +Directly close progress dialog during sync +Redirect escape key from main dialog to progress dialog +Fixed startup delay during consistency checks +Updated translation files + + +FreeFileSync 9.5 [2017-11-05] +----------------------------- +Allow to change error handling option on progress dialogs +Set up shutdown behavior during sync (summary, exit, sleep, shutdown) +Conditional execution of the post sync command line +Directly use native shutdown/sleep API (Windows and macOS) +Run post sync command even when fail on first error was set +Merged batch and GUI error handling options +Write post sync command to log file +Update GUI-specific options when saving as batch job +Progress graph area matches processed data ratio +Delete files permanently with Shift+Del +Apply correct quotation for CSV-exported folder list +Replace Unicode arrow chars with ASCII for variant description +Updated libcurl, OpenSSL to latest builds + + +FreeFileSync 9.4 [2017-10-05] +----------------------------- +Fixed copying files with locked byte ranges using VSS +Fixed wrong FTP working directory reuse in libcurl +Allow retry upon failure during online update check +Repackaged Donation Edition to reduce AV false positives (Norton) +Apply correct directory path encoding during FTP traversal +Fixed strict weak ordering for SFTP session ID sorting +Clean up read-only temporary files during failed sparse file copy +Fixed access denied file copy error for ADS while using BackupWrite +Workaround broken non-Windows SMB implementations reporting sparse support +Support hash characters in FTP directory listing +Prepared auto-updater to support new installer format +Refined installer error reporting +Streamlined sync config dialogs +Resized installer window dimensions + + +FreeFileSync 9.3 [2017-08-09] +----------------------------- +Support multiple connections per FTP folder traversal: N times speed up +Improved folder traversal time by 35% for FTP servers supporting MLSD +Use single CWD when changing FTP working directory +Maximize FTP input/output speed using prefetch/output buffers and async execution +Use larger socket buffer for significant FTP upload speed increase +Fixed out of memory error when copying large files via FTP +New popup dialog option to ignore all errors +Reduced memory peaks by enforcing streaming buffer size limits +Removed custom sync directions from config XML if not needed +Fixed EOPNOTSUPP error on GVFS-mounted FTP (Linux) +Prevent input focus stealing after manual comparison +Flash task bar after comparison if other app has input focus + + +FreeFileSync 9.2 [2017-07-03] +----------------------------- +Use direct copy instead of transaction to speed up versioning +Replaced file existing handling with use of unique temporary names +Support SFTP authentication via Pageant/SSH agent +New menu option to restore hidden panels individually +Fixed GTK button icon being truncated (Linux) +Fixed error dialog hiding behind progress dialog (macOS) +Round out FTP symlink deletion handling +Support four-digit year format on IIS FTP +Fixed FTP parsing error for epoch time on Windows server +Narrow contract for file system abstraction regarding existing files +Treat failure to load database as error rather than warning +Save root folder access for certain FTP path checks + + +FreeFileSync 9.1 [2017-05-24] +----------------------------- +Fixed crash when getting invalid data after item type check +Fixed copying symlinks pointing to network folders +Support resolving network paths in the NT namespace +Support FTP servers with broken MLST command (Pure-FTPd) +Fixed FTP access error on file names containing special chars +Include raw FTP server response in error message +Quickly check server connection using a single FEAT +Don't change working directory when sending a single FTP command +Support FTP Unix listings missing group name +Support RFC-2640-non-compliant FTP servers having UTF8 disabled +Support FTP servers returning non-routable IP in PASV response +Support IPv6 when establishing FTP connections +Start external application keyboard shortcuts with zero + + +FreeFileSync 9.0 [2017-04-16] +----------------------------- +Support synchronization via FTP (File Transfer Protocol) and FTPS (SSL/TLS) +Notify failure to set modification time as a warning instead of an error +Allow intermediate non-folder components when checking path status +Prevent file drop events from propagating to parent windows +Create Downloads folder if not yet existing when running auto-updater +Get all MTP input stream attributes as a single device access +Improved SFTP input stream copying time by 20% +Buffer (S)FTP sessions based on all login information +Finalize all installation steps before showing finished page +Updated translation files + + +FreeFileSync 8.10 [2017-03-12] +------------------------------ +Fully preserve case-sensitive file paths (Windows, macOS) +Support SFTP connections to local hosts +Warn if versioning folder is contained in a base folder +Use natural string sorting algorithm for item lists +Consider exclude filter settings for folder dependency checks +Fixed file not found error on case-sensitive SFTP volume +Fixed failure when creating MTP sub directories +Fixed crash when loading database file during comparison +Refactored UTF conversion routines +Use pipe symbol as filter separator instead of semicolon +Iterate over all matching SFTP connections available on a server (macOS) +Reduced folder matching time by 12%, average memory use by 11% +Added experimental FTP support + + +FreeFileSync 8.9 [2017-02-08] +----------------------------- +Detect when database file was copied and avoid "second part missing" error +Further reduced size of database files by 20% +Reduced amortized number of file operations during versioning +Added database file consistency checks to catch unexpected number of stream associations +Improved file I/O by detecting cross-device moves via path +Fixed path parsing failure when creating MTP directories +Implemented buffered stream I/O abstraction to prepare for FTP +Generalized file path handling for abstract file system implementations +Warn about outdated AvmSnd.dll before crashing during sound playback +Avoid libunity9 dependency for Ubuntu builds +Refactored OpenSSL and libssh2 initialization/shutdown +Case-insensitive grid sorting on Linux +Added 32-bit precompiled Debian/Ubuntu release + + +FreeFileSync 8.8 [2017-01-08] +----------------------------- +Distinguish file access failure from not existing during sync +Further optimized number of file I/O operations via file system abstraction +Report unexpected prompts for keyboard-interactive SFTP authentication +Mark followed directory symlinks on grid +Fixed parent path determination for UNC +Don't skip source files that cannot be accessed +Don't consider a symlink type for SFTP when comparing by content +Fixed invalid parameter error when setting file times on exFAT file system +Don't allow overwriting folder with equally named file when copying from main dialog +Fixed failure to create intermediate directories for Cryptomator/Webdav +Refactored file system abstraction layer for future FTP support +Fixed failure to change file name case on MTP devices +Fixed late failure for batch recycling when parsing of single item fails + + +FreeFileSync 8.7 [2016-12-06] +----------------------------- +New auto-updater feature for FreeFileSync Donation Edition +Download zip archive of portable FreeFileSync Donation Edition +New command line options to define parameters for silent installation +Support offline activation for portable Donation Edition +Use automatic keyboard-interactive SFTP authentication as fallback +Check for available SFTP authentication methods before login +Support cloud sync of portable edition installation files +Access donation transaction details from about dialog +Use width from flexible grid column when showing/hiding extra columns +Show item short names in middle column tooltip +Enhanced file category descriptions with modification times +Don't warn about missing recycle bin when only moving or updating attributes +Fixed crash when switching to main dialog during batch sync + + +FreeFileSync 8.6 [2016-10-25] +----------------------------- +Added SFTP support for OS X +Support SFTP authentication via public/private key +Remember configuration history scroll position +SFTP folder picker supports browsing hidden folders +Fixed failure to copy files with corrupted ADS +Signed application installer (OS X) +Increase config history default size to 100 items +Auto-close FreeFileSync processes before uninstallation +Simplified SFTP configuration syntax +Fixed update check sending incomplete keep-alive header +Detailed error reporting after failed web access +Suggest folder path macro substitutions also at inner positions +Transfer folder creation times (OS X) + + +FreeFileSync 8.5 [2016-09-16] +----------------------------- +Support multiple SSH connections per SFTP folder traversal: N times speed up +Support multiple SFTP channels per SSH connection: additional N times speed up +Fixed installer crashes by using correct DEP-compatibility +Fixed notification area icon being generated too often +Thread-safe SFTP uninitialization on shutdown +Thread-safe mini-dump creation during shutdown +Fixed case-insensitive migration of new csidl macro names +Reduced SFTP access serialization overhead +Buffer SFTP sessions independently from usage context +Detect and discard unstable SSH sessions +Pre-empt SFTP session disconnect via dedicated SFTP cleanup thread +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 +Support nanosecond-precision file time copying (Linux) +Start maximized instead of in full screen mode (OS X) +Fixed crash while setting privileges during shutdown +Fixed crash when failing to clean up log files +Fixed EOPNOTSUPP error when copying file to GVFS Samba share (Linux) +Fixed default external applications command line (Linux) +Thread-safe translation access and change during app shutdown +Don't consider port and password when comparing SFTP paths +Updated translation files + + +FreeFileSync 8.3 [2016-07-08] +----------------------------- +Make temporary local copy for non-native file paths: %local_path% +Support selections from both grid sides at a time for external applications +New external application macros: %item_path%, %folder_path%, %item_path2%, %folder_path2% +Migrate external application commands to new macro syntax +Support reverse grid search (Shift + F3) +Don't condense empty sub folders on overview panel +Show changelog delta in update notification +Center modal dialogs after layout redetermination +Warn about portable installation into programs folder +Calculate default message dialog height depending on screen size +Don't substitute external applications path for empty base folder +Fixed prolonged tooltip time not being evaluated + + +FreeFileSync 8.2 [2016-05-30] +----------------------------- +Unified item path representation on main grid +New progress indicator control for binary comparison +Fixed crash on exit when accessing already destructed constant +Fixed crash when FreeFileSync is still running during OS shutdown +Fixed crash on startup due to missing root certificates +Work around start up crash on Windows installations missing certain patches +Fixed in-place progress panel height being trimmed +Support drawing arbitrary polygons with graph control +Apply POSIX file name normalization (OS X) +Normalize keyboard input encoding for all text fields (OS X) +Report errors when cleaning up old log files +Integrate external app WinMerge if installation is found + + +FreeFileSync 8.1 [2016-04-21] +----------------------------- +Follow shell links during drag and drop on main dialog (Windows) +Significantly improved main grid rendering performance +Log info about non-default global settings +Establish new network connections only when needed (Windows) +Show only a single login dialog per network share +Show login dialogs for the same network address one after another +Fixed endless recursion for paths containing certain Unicode characters (OS X) +Support using portable version without direct installation +Fixed access denied error when verifying read-only target file (Windows) +New global option for sound cue after comparison +Updated help file + + +FreeFileSync 8.0 [2016-03-15] +----------------------------- +Fine-tuned buffer sizes for 70% improved SFTP stream I/O speed +Support incomplete read/write operations while maximizing buffer saturation +Automatically check consistency of FreeFileSync installation +Fixed crash when using SFTP on CPUs without SSE2 support +Improved GUI responsiveness during SFTP I/O +Disabled automatic quote substitution for file filter (OS X) +Work around invalid parameter error on FAT drives for broken create times +Avoid filter mismatches by using precomposed UTF (OS X) +Fixed main dialog close button not being disabled during sync (OS X) +Don't create AppleDouble files if extended attributes are unsupported (OS X) +Set content format metadata when copying to an MTP device +Fixed F-keys not working in sync config dialog (Linux) +Revert to default button margin values (Linux) +Fixed crash when thumbnail loading fails on MTP device +Fixed main grids not scrolling in parallel during mouse selection +Revert to default scaling for non-dpi-aware apps +Integrate FreeFileSync online manual +Added Slovak translation + + +FreeFileSync 7.9 [2016-02-13] +----------------------------- +New comparison variant: compare by file size +Buffer SFTP read/write accesses for optimal packet sizes +Configure folder access time out via GlobalSettings.xml +Drag and drop config files anywhere on main dialog +Work around "argument list too long" file copy error (OS X) +Work around "invalid argument" file copy error (OS X) +Support case-change when syncing to case-sensitive SFTP (Windows) +Select between sync completion sounds gong/harp.wav +Set up sync completion sound file in GlobalSettings.xml +Validate monitoring data to avoid RealTimeSync crash +Updated help file +Updated translation files + + +FreeFileSync 7.8 [2016-01-01] +----------------------------- +Correctly resolve environment variables containing MTP paths +Support at and colon characters in SFTP user name +New context buttons for quick sync config changes +Report specific error during folder existence check when starting sync +Fail lately when traversing available MTP devices +Correctly handle SFTP time-out error when checking folder existence +Updated on completion command lines for log off/standby/shut down (Linux) +Support HTML POST redirection for update checks +Calculate UTC file times like Windows Explorer for MTP devices +Don't reuse timed-out SFTP sessions with thread affinity +Workaround SFTP session hang after unsupported statvfs command +Updated OpenSSL to 1.0.2e + + +FreeFileSync 7.7 [2015-12-01] +----------------------------- +Support variable drive letters for config history when using FreeFileSync portable +Skip non-storage functional objects at MTP device level +Log and show error messages without hanging when running as a service +Navigate between sync settings panels with arrow keys +Fixed volume shadow copy file path generation +Handle integer overflows when comparing file times +Ignore more than one file time shift +Reworked grid to support mouse highlight areas +Allow minute precision for file time shifts +Warn about unsupported MTP and SFTP paths in RealTimeSync +Strip superfluous mode parameters when creating a directory (Linux, OS X) +Correctly detect system language for English UK +Store program language by name to handle changing ids +Fixed crash during application exit after using SFTP + + +FreeFileSync 7.6 [2015-11-01] +----------------------------- +Create missing synchronization base folders only on demand +Improved main grid text search performance by 40% +Restore correct main dialog height after restart (Linux) +Default to standard main dialog size after unmaximize (Linux) +Prevent creation of irregular folder names (Windows) +Support MTP devices over WiFi with null modification times +Do not apply invalid vertical main dialog positions (OS X) +Support Yosemite full screen window mode (OS X) +Use buffered lock file I/O (Windows) +Correctly set up OpenSSL for multithreaded use +Added COM initialization for worker threads (Windows) +Forward focus to sync button after comparison +Streamlined file system abstraction layer interfaces + + +FreeFileSync 7.5 [2015-10-01] +----------------------------- +Detect moved files on source even for targets with no (SFTP) or unstable (FAT) file id support +Improved performance for detection of moved files by over 50% +Added folder picker to select SFTP paths +Support additional SFTP ciphers by building upon OpenSSL backend +Added 10-seconds time out when SFTP command is hanging indefinitely +Work around unexpected SFTP session termination on Synology servers +Fixed various libssh2 and OpenSSL memory leaks +Fixed FreeFileSync taskbar link reuse (Windows 7) +Avoid last error code being overwritten by certain C runtimes before evaluation +Run online update check asynchronously (Windows) +Check source item existence before cleaning target during versioning (Linux, OS X) +Check folder recursion limit to catch stack overflows +Doubled potential folder traversal recursion depth (Windows) +Consider child elements of excluded folders during database clean up + + +FreeFileSync 7.4 [2015-09-01] +----------------------------- +Switch between all folder pair configurations directly in the sync config dialog +Support macros, path by volume name for config files on command line +Support slash as path separator on command line (Windows) +Allow slash as path separator in filter dialog (Windows) +Discard SFTP connection after 20 seconds of idle time +Fixed file already existing error when changing file name case (OS X) +New keyboard shortcuts to open external applications +Fixed clipboard being cleared when opening sync config dialog (OS X) +Workaround wxWidgets bug breaking copy/paste shortcuts (OS X) +Fixed disabled button icons not being updated in the config dialog +Fixed launcher error messages not being shown (Windows XP) +Fixed launcher showing incorrect error about missing service pack (Windows XP) +Revised help file and consolidated into online help + + +FreeFileSync 7.3 [2015-08-01] +----------------------------- +New context menu option to copy selected files to alternate folder (create diffs) +Fill a folder pair by dropping two folders at a time from Explorer +Added option to set non-standard SFTP port +Prevent recursive creation of temporary recycle bin directories (Windows) +Retrieve grid column label colors from the system +Fixed detection of already existing files when moving (Linux) +Follow OS convention for preferences (OS X) +Prevent progress dialog from hiding behind main dialog (OS X) +Fixed config saved status not updating when changing certain settings +Support for high dpi display settings +Fixed crash when help viewer is open during exit (Windows) +Show manual deletion progress within comparison status panel +Further reduced number of file accesses during versioning +Fixed folder picker failing to select Desktop folder (Windows) + + +FreeFileSync 7.2 [2015-07-01] +----------------------------- +Support synchronization via SFTP (SSH File Transfer Protocol) +Detailed error reporting when checking folder existence +Synchronize MTP devices with no modification time support +Set focus to comparison button on startup +Fixed transactional stream clean up error if target file already existing +Fixed incomplete input stream clean up on fadvise failure (Linux) +Consider non-native paths for direct comparison after startup +Revised algorithm generating folder pair display name +Reduced number of file accesses during versioning +Stricter language file consistency checking +Resolved crash when running Windows 7 on CPUs without SSE2 +Improved Minidump creation handling stack overflows +Revised path formatting to always match native representation +Fixed about dialog layout for large font sizes +Support Minidump creation for Windows XP +Updated translation files + + +FreeFileSync 7.1 [2015-06-06] +----------------------------- +Avoid various access denied errors when synchronizing with admin rights (Windows) +Accept Explorer drag and drop from MTP devices +Support showing MTP files with Explorer +Support opening MTP files with default application +Preselect active MTP folder in folder picker dialog +Work around file not found error when copying alternate data streams +Fixed access denied error when copying file times (Linux) +Work around boost bug causing RealTimeSync to wake PC (Windows) +Fixed naming convention "replace" for versioning +Skip space pre-allocation if not supported (OS X) +Use faster space pre-allocation method (Linux) +Transactional error handling when closing file streams +Fully initialize system image list for medium and large icons (Windows) +Handle XP backwards-compatibility with 32-bit build (Windows 64-bit) +Work around hang due to unsupported AVX2 instructions (Vista 64-bit) +Fixed invalid argument exception during app launch (OS X) +Fixed binary comparison checking for wrong buffer size +Fixed GetLogicalProcessorInformation not found startup error (Windows XP SP2) +Support IP-based UNC paths with folder selector (Windows) +Use standard file permissions for application bundle (OS X) +Updated help file and added tips and tricks chapter + + +FreeFileSync 7.0 [2015-05-11] +----------------------------- +Support synchronization with MTP devices (Android, iPhone, tablet, digital camera) +Implemented file system abstraction layer +New database format supporting generic file ids +Pre-allocate disk space when writing file output stream +Late failure when moving multiple items to recycle bin +Keep UI responsive while loading/saving database file +Improved error reporting indicating failed item when moving to recycle bin +Pass correct thread id when creating Minidump (Windows) +Fixed directory icon loading resource leak (Linux) +Fixed RealTimeSync message provider exception safety issue (Windows) +Avoid locking issues by creating the log file after batch synchronization +Fixed RealTimeSync monitoring for items beyond subfolders (Linux) +Fall back to file extension during file icon load error +Show file icon by extension as temporary placeholder +Work around silent failure to copy file times to external drives (Linux) + + +FreeFileSync 6.15 [2015-04-07] +------------------------------ +Revert to log file naming convention without colon character +Prevent endless recursion when traversing into folder on corrupted file system +Fixed view filter button rendering issue for RTL languages +Fixed grid losing far scroll positions when increasing icon sizes +Flush file buffers before verifying file copy +Update existing items when retrying failed folder traversal +Harmonized bitmap file loading by removing format variance +Fixed invalid argument error when setting file times (Linux) +Fixed application hang when loading icon for named pipe (Linux) +Improved file copy read-ahead performance (Linux) +Use native file I/O for stream operations (Linux, OS X) +Fixed file copy creating zero-sized files (OS X) +Automatically create Minidump files during an application crash (Windows) +Check for missing service pack to help diagnose crash (Windows 7) +New menu item with download link after a version update +Work around C-function memory race condition when formatting time +Added Hindi language + + +FreeFileSync 6.14 [2015-02-10] +------------------------------ +New buttons allow changing the order of folder pairs +New keyboard shortcuts for rearranging folder pairs +Preserve comparison results when deleting a specific folder pair +Allow inserting new folder pairs into the middle of the list +Append status to log file names when warnings occur +Don't interrupt immediate comparison when starting a .ffs_gui file for slow devices +Work around wxWidgets bug eating up command keys in text boxes (Linux) +Fixed incorrect parameter error when checking recycle bin on drive mounted with Paragon ExtFS (Windows) +Use colon as time stamp separator in log file names +Refactored basic low-level file traversal routine +Optimized file icon startup procedure +Fixed occasional failure to set modification times on Samba shares (OS X) +Transfer creation times during file copy (OS X) +Support copying file times with nanosecond precision (OS X) + + +FreeFileSync 6.13 [2015-01-11] +------------------------------ +Fixed crash when failing to create log file during batch run +Show directory traversal errors as conflict category on grid +Improved file filter behavior for certain edge cases when updating the database +Fixed crash when task scheduler ends FreeFileSync after a certain time (Windows) +Don't show alternative folder paths if volume name is empty +Support silent installation for Inno Setup (Windows) +Fixed recursive yield when minimized into notification area (Linux, OS X) +Include ACLs when copying file and folder permissions (OS X) +New file copy routine including extended attributes (OS X) +Fixed failure to permanently delete directories containing symlinks +Copy extended attributes when creating new folders and symlinks (OS X) +Restore process umask after creating lock file (Linux, OS X) +Copy directory permissions by default (Linux, OS X) +Optimized construction of merged path filters +Exclude items subject to traversal errors when updating the database + + +FreeFileSync 6.12 [2014-12-01] +------------------------------ +New "Actions" menu bar entry with basic operations +Fixed crash after comparison while needlessly copying traversal results +Support update-checker URL redirection (Linux, OS X) +Merged installer translations into .lng files +Fully translated FreeFileSync context menu options and file types in Windows Explorer +More structured symlink handling options +Scroll to active selection in config list box on startup +Fixed delete key to remove items in config history panel (OS X) +Fixed language file parser showing incorrect row on error +Fixed crash during sync due to unsupported SSE instructions (Server 2003, XP 64-bit) +Fixed startup error due to invalid handle type +Always log folder pair paths even if there is nothing to sync +Updated translation files + + +FreeFileSync 6.11 [2014-11-03] +------------------------------ +Updated recycle bin access for Windows 10 +New command line option "-edit" to load configuration without executing +Case-insensitive command line argument evaluation +New Explorer context menu options for ffs_gui, ffs_batch files +Added sync variant to folder pair info in log file +Don't process and log folder pair if nothing to do except writing DB file +Fixed liblzma.5.dylib not found during startup (OS X 10.8) +Added version info to application bundles (OS X) +Fixed incorrect warning when configuration contains empty folder pairs +Replaced misleading inotify error message "No space left on device" (Linux) +Fixed FreeFileSync launcher blocking app folder move (OS X) +Updated default main dialog layout +Fixed async error evaluation when creating volume shadow copies +Keep user interface responsive while creating a volume shadow copy +Fixed error when starting asynchronously from a batch script +Show progress of writing log files +Fixed updated file being left deleted when copying permissions failed +New Project website: https://freefilesync.org/ + + +FreeFileSync 6.10 [2014-10-01] +------------------------------ +Fixed crash when accessing recycle bin in compatibility mode (Windows 7, 8) +Draw middle grid selection irrespective of focus column +Don't show parts of progress graph if nothing to sync +Break on missing directories before evaluating warnings +Ignore leading/trailing whitespace in search panel +Disable search panel during comparison +Disable shortkeys during comparison +Log folder pair only if files are synced +Fixed number separator formatting for English locale +Copying locked files now inactive by default +Show all affected folders when warning about a shared sub folder + + +FreeFileSync 6.9 [2014-09-01] +----------------------------- +Reuse FreeFileSync taskbar link when available (Windows 7) +Limit number of retries when creating temporary files +Fixed bitmap rendering issue for high-contrast color schemes +Revised and fixed unclear GUI texts +Updated deprecated system call when suspending idle (OS X) +Fixed retry when failing to determine recycle bin status +Added progress graph legend +Updated translation files + + +FreeFileSync 6.8 [2014-08-01] +----------------------------- +New comparison option to ignore file time shift in hours +Tentatively disabled DST hack affecting FAT file creation times +New menu option to reset GUI layout +File sizes ignore sync direction in overview panel +Sort by file name also sorts folder names +Main grid column "full path" includes file name +Always position comparison progress below main buttons +Fixed high-precision tick count calculations +Fully restart directory traversal on errors +Updated help file with steps to schedule a batch job (OS X) + + +FreeFileSync 6.7 [2014-07-01] +----------------------------- +Redesigned comparison progress statistics +Fixed crash when loading incompatible config file +Added "new" button to config panel +Avoid sync progress dialog repositioning +Resolved crash when loading sync settings for Arabic locale +Restored cancel button width +Help window not forced to float over main dialog (Windows) +Fixed overwriting old-format batch files +Harmonized view category sequence +Merged similar translation items +Fixed crash when scrolling help window without focus + + +FreeFileSync 6.6 [2014-06-01] +----------------------------- +Fixed large font size standard button layout +Fixed config dialog graphics glitch with large font sizes +Exit FreeFileSync launcher process during update +Exclude temporary files from RealTimeSync monitor +Implement correct standard button spacing (OS X) +Fixed SELinux compilation issue (Linux) +Installer adds RealTimeSync link to desktop (Windows) +Improved makefile (Linux, OS X) +Reduced binary file size (Linux) +Updated translation files + + +FreeFileSync 6.5 [2014-05-01] +----------------------------- +Support preview for RAW CR2 image files (Windows Vista and later) +Fixed startup exception when using task scheduler (Windows XP) +Correctly resolve SystemRoot NT path syntax for symbolic links +Fixed incorrect error codes being reported (Windows XP) +Fixed config dialog shortcut key presses getting lost (OS X) +Allow vertical layout for top button panel +Code cleanup: removed support for old database and XML config formats +Center sync progress dialog +Updated help file + + +FreeFileSync 6.4 [2014-04-01] +----------------------------- +Combined comparison, filter and sync config dialogs +Support alternate GlobalSettings.xml file via command line +Toggle between config panels with F6, F7, F8 +Show config status icons in notebook panel caption +Redesigned configuration dialog layouts +Fixed startup error after moving installation directory +Fixed retry on failure to resolve path by volume name +Resolved ERROR_ALREADY_EXISTS when creating temporary recycle bin subdirectory +Added "save as GUI job" button on main dialog +Added Bulgarian language + + +FreeFileSync 6.3 [2014-03-01] +----------------------------- +No wait time anymore while searching for recycle bin (Windows Vista and later) +Revised synchronization progress graph +Clean up "On completion" considering last usage +Fixed CTRL + C keyboard short cut in filter dialog (OS X) +Resolved static initialization order issues +Reduced disk accesses when resolving directory name +Added view filter labels +Updated translation files +Updated help file + + +FreeFileSync 6.2 [2014-02-01] +----------------------------- +New synchronization progress graph +Skip binary comparison for files excluded via time span or size +Fixed configuration panel ordering for entries starting with numbers +Filled gap after last grid column to cover full window width +Work around wxWidgets image button bug showing obsolete disabled state +Refined file existence checks to handle restricted permissions +Disable file filter button during comparison +Fixed mouse wheel grid scrolling for GTK2 (Linux) +Avoid dummy texts during progress dialog init (OS X) +Translated external application default commands in global settings +Support cancel while encoding extended time information +Highlight non-zero synchronization statistics + + +FreeFileSync 6.1 [2014-01-01] +----------------------------- +Released RealTimeSync for OS X +Handle errors loading reference batch config +Disable user mode exception swallowing for Windows 7 SP1 +Always exclude root nodes on manual selection when excluded items are hidden +Fixed showing duplicate custom "on completion" commands +Close old directory handle first before executing directory traversal fallback +Show negative batch synchronization result in log file name +Avoid file system race when creating temporary files +Transfer creation and modification times on folder creation +Fixed empty main dialog configuration migration issue on Mac OS X + + +FreeFileSync 6.0 [2013-12-01] +----------------------------- +Revised main dialog panel layout +Show arrow icon for shortcut files and symlinks +Execute the "on completion" command asynchronously +Resolved invalid grid background when context menu is shown +Set negative file time tolerance to disable file time check +Optimized sequence of steps when saving database files +Prevent temporary incorrect statistics after unexpected increase in workload +Fixed default height when mixing panels with and without caption on main dialog +New view filter button "show excluded items" +New keyboard shortcuts for file filter and sync settings +Removed libpng15.so dependency for openSUSE 13.1 +Updated help file +Updated translation files + + +FreeFileSync 5.23 [2013-11-01] +------------------------------ +Allow sorting root nodes on overview panel +Support retry on failure to resolve path by volume name +Copy high-precision modification times for files and symlinks +Align top panel height with comparison and sync buttons +Show lock owner while waiting on a locked directory +Resolved help file W3C validation issues +Fixed high-contrast accessibility issues in help +Fixed crash for CPUs without SSE2 when using VSS (Windows XP) +Prevent progress statistics timer overflow +Save RealTimeSync settings before forced exit due to shutdown or log off +Resolved contract violation error due to out of memory +RealTimeSync does not block system shutdown anymore +Added "select all" context menu option for progress log +Have progress log keyboard input ignore focus +Fixed category icon background color issues +Report error when reading active config file failed during save +Preload adjacent file icons on grid + + +FreeFileSync 5.22 [2013-10-01] +------------------------------ +New options for automatic retry after error +Improved compliance with Windows User Experience guidelines +Harmonized popup dialog layouts +Correctly show program menu when main dialog receives focus (OS X) +Revised configuration dialog layouts and designs +Fixed crash on startup for CPUs without SSE2 support (Windows XP) +Work around wxWidgets bug for sorted list boxes (Linux) +Updated and revised help file +Early parameter validation for filter and sync config dialogs +Fixed followed directory symlinks being incorrectly excluded +Automatically calculate best initial message box size +Progress graph and status icons support high-contrast color schemes +Include directory child-elements when manually setting filter +Allow manual filter for short name on overview panel +Don't treat file drops on directory input fields as URI (Linux) +Updated translation files + + +FreeFileSync 5.21 [2013-09-02] +------------------------------ +Detect moved/renamed files in mirror and custom variants +New database format for two way variant: old database files are converted automatically +Support double-clicking ffs_gui/ffs_batch files (OS X) +Integrated search panel (CTRL + F, F3) into main dialog +Merged variant names into top button labels +Hide dock icon while minimized to notification area (OS X) +New keyboard shortcuts: F5, F6, F7, F8, F9, F10 +Further reduced size of database files by 10% +Fixed Outlook *.ost files found missing on VSS snapshot volumes +Added include filter context menu option +Correctly scroll to search hits on different grid +Always remove .ffs_tmp files permanently +Fixed layout for buttons with text and graphics for RTL languages (Arabic, Hebrew) +Revised file filter parser: new syntax for excluding items in subdirectories +Improved configuration merge algorithm +Fixed crash when showing help due to wxWidgets 64-bit bug in help component (Windows 8) +Avoid progress dialog graph flicker during resize when too few samples are available +Progress status when deleting files not greyed-out anymore +Increased time-out to 20 seconds when checking for directory existence +Exclude broken symlinks via filter before showing error message +Follow symlinks when checking file/directory existence (Linux) +Consistently set batch error codes during startup phase +Updated translation files + + +FreeFileSync 5.20 [2013-08-03] +------------------------------ +Fixed crash on startup due to wxWidgets 64-bit bug in font enumeration (Windows 8) + + +FreeFileSync 5.19 [2013-08-02] +------------------------------ +Redesigned progress dialog including new items graph +New command line syntax: set directory names of a .ffs_gui/.ffs_batch externally +Explicit button on progress dialog to minimize to systray +Fixed progress graph labels being truncated (Debian, Ubuntu, openSUSE) +Resolved main dialog z-order issues during sync (OS X) +Reduced progress dialog layout twitching +Further improved comparison speed by 10% +Use proper config file path in file picker dialog (OS X) +Never interrupt when updating a file with fail-safe file copy after target was deleted +Prevent crash when closing progress dialog while paused (OS X) +Support external command lines starting with whitespace (Windows) +Show warning before starting external applications for more than 10 items +Start external applications synchronously if needed to avoid running out of system resources +Don't show hidden progress dialog when showing an error message in silent batch mode (OS X) +Correctly show file names containing ampersand characters in progress dialog +Adapt size of results dialog to fit contents +Correctly execute file move before parent directory will be deleted +Show a blinking system tray icon on errors instead of a modal dialog in RealTimeSync +Added installation size for Windows' Add/Remove Programs + + +FreeFileSync 5.18 [2013-07-02] +------------------------------ +Work around boost 1.54 bug "The procedure entry point GetTickCount64 could not be located in the dynamic link library KERNEL32.dll" (Windows XP) + + +FreeFileSync 5.17 [2013-07-02] +------------------------------ +Consider target file when updating followed file symlinks +Support moving files to recycle bin contained in followed directory symlinks +Move instead of copy updated files into versioning directory +Reduced memory peak when loading large database files after comparison +Check recycle bin existence only once per base folder and only if deletions occur (Windows) +Revised and enhanced error messages +Show moved files in same category as updated files +More pessimistic calculation of required disk space reducing false positives +Implemented platform-specific standard button ordering (Linux, OS X) +Set configuration panel primary orientation to vertical +Added new checks and error message strings for translation file parser +Revised middle grid inactive color and duplicate equality symbol +Skip XML comments while parsing config files +Redesigned confirmation popup dialogs +Standard button spacing conforms to operating system conventions +Shrink memory consumption of file hierarchy data structures +Don't show file deletion dialog if selection is empty +Fixed incorrect progress statistics if a file or directory is deleted externally after comparison +Focus grid cursor row after switching sides with keyboard direction keys +Improved localization process: find translation deltas more easily, better error reporting +Reset initiated grid selection when changing grid cursor +Improved sync progress dialog layout +Suppress dubious wxWidgets error message "locale 'es_AR' can not be set". (OS X) +Don't show busy cursor on synchronization results dialog +Log error message upon retry as type info only +Updated translation files + + +FreeFileSync 5.16 [2013-06-01] +------------------------------ +Integrated both category and sync action view into middle grid +Condensed folder pair display names on overview panel +Consider symlinks and junctions when copying locked files (Windows Vista) +Resolved failure to set directory lock within Windows XP as Virtual Box guest +Period resolves to working directory again +Fixed "DecodePointer could not be located in KERNEL32.dll" (Windows 2000) +Support closing progress dialog forcefully during sync (OS X) +Don't disable all child items if directory traversal fails for a single item only +Simplified deletion confirmation dialog (removed "delete on both sides") +Work around wxWidgets leaking memory on exit (OS X) +Avoid wxWidgets crash when deleting folder pair control (OS X) +Prevent wxWidgets corrupting stack when wxLocale is allocated statically (Linux) +Use GetUserDefaultLangID to determine installer default language +Avoid progress speed and remaining time jitter +Check existence only once for duplicate base directories +Detect invalid file symlinks pointing to directories (Windows) +Disable unsuitable buttons in pop up dialogs when checkbox is set +Copy folder attributes if source is a junction already on Windows XP instead of Vista +Mark failed UTF conversions with replacement character +Do not restore main dialog position outside visible screen area (multi monitor setup) +Support detection of moved files through symlinks +Reduced memory consumption when detecting moved files +Check for duplicate file ids when detecting renamed files +Redetermine volume id for followed directory symlinks +Removed "Compare_Complete.wav" +Don't accept file deletion confirmation in less than 50ms +Systematically resolved translation bugs +Added Serbian language + + +FreeFileSync 5.15 [2013-05-01] +------------------------------ +New menu option to activate/deactivate automatic update checking +Show status message while checking for program updates +Faster start up times through asynchronous config file checking +Automatically migrate configuration files to new format +New context menu options to copy and paste filter settings +Support file and folder names with trailing space or period characters +Do not show superfluous scroll bars for multiple folder pairs +Correctly show long file paths when moving to recycle bin failed (Windows Vista and later) +Status feedback before blocking while creating a Volume Shadow Copy +Do not show dummy texts while initializing progress dialog (OS X) +Allow to maximize filter dialog +New column for item count on overview panel +Allow CTRL + C to copy selection to clipboard on overview panel +Consider current view filter for file selection on overview panel +Work around silent failure to set modification times on NTFS volumes (Linux) +Avoid main dialog flash when closing progress dialog (Linux) +Do not show middle grid tooltip when dragging outside visible area +Reduced file accesses when loading XML files +Simplified structure of GlobalSettings.xml +Allow to change default exclusion filter via GlobalSettings.xml: "DefaultExclusionFilter" +Split filter entries over multiple rows in ffs_gui/ffs_batch XML files +Resolved failed assert during start up (ReactOS) +Create directory locks after one-time existence check +Show warning when locking directory failed +Reset main dialog layout to fix top panel default height being too small +New help file topic "Expert Settings" +Updated translation files + + +FreeFileSync 5.14 [2013-03-31] +------------------------------ +Do not process child elements when parent directory creation fails +Start comparison after pressing Enter in directory input fields +Lead grid is determined via keyboard input instead of input focus change +Ignore empty directory entries in RealTimeSync +Restored mouse cursor "snap to default button" +Implemented file icon support for sync preview (OS X) +RealTimeSync exit via menu working again +Restore main dialog even if "close progress dialog" is selected +Show full path when failing to create directory on not existing target drive +Middle grid tooltip shown correctly again (SUSE Linux/X11) +Prevent process hang when manually writing to directory history (Linux and OS X, wxWidgets 2.9.4) +Resolved crash after showing help dialog (OS X) +Properly handle non-ASCII characters for external commands (OS X) +Support UTF8 format restrictions on file systems like HFS (OS X) +Do not stretch small thumbnail icons (Linux) +Use 32x32 instead of 48x48 as medium icon size on Windows XP +Properly size non-jumbo icons in thumbnail view (Windows Vista and later) +Reduced GDI resources for file icon buffer (Windows) +Automatically check for updates weekly without showing pop up on first start +Restored program logo in systray progress indicator +Fit grid row label to match wide font sizes +Added macros %csidl_Downloads%, %csidl_PublicDownloads%, %csidl_QuickLaunch% (Windows Vista and later) + + +FreeFileSync 5.13 [2013-03-06] +------------------------------ +Prepared support for new build on Mac OS X +Time out for not existing directories after 10 seconds +Check directory existence in parallel +Inform about all missing directories via a single error message +Show remaining time considering relative error of 10% +Check for grid icon updates only when needed +Revised directory lock process detection +Implemented high resolution icons +Accessibility: fixed unreadable labels +More polished user interfaces +Fixed time stamp not being set on NFS/Samba shares (Linux) + + +FreeFileSync 5.12 [2013-02-04] +------------------------------ +Dynamic statistics adjustment during synchronization +Allow to save active view filter settings as default (context menu) +Stay responsive while checking recycle bin existence on slow disks +Reset option "Delete on both sides" upon each manual deletion +Added context menu to allow deletion of last used configurations +Support numpad add/subtract keys for overview tree +Revised external application integration +Call external applications for multiple selected items +Automatically schedule abandoned recycle bin temp directories (.ffs_tmp) for deletion +Binary comparison speed estimate considers errors and short-circuit evaluation +Use full time window of sync phase when calculating overall speed +Added Arabic language + + +FreeFileSync 5.11 [2013-01-06] +------------------------------ +New file versioning scheme: move to folder replacing existing files +Fixed high CPU consumption after longer syncs +Improved .ffs_batch configuration file handling +Allow to quick save .ffs_batch files on main dialog and program exit +Convert batch-exclusive settings when opening a .ffs_batch file on main dialog +Redesigned configuration dialog layout +Enhanced all file I/O error messages to show locking processes (Windows Vista and later) +Separator in CSV file now locale dependent +Avoid "Windows Error Code 2" for truly empty directories +Macro %month% resolves to decimal number +New macro %timestamp% +Revised sync progress graph +Fixed progress graph graphics glitch for RTL layout +Allow XML element values to contain non-escaped quotation marks +Updated help file +Updated translation files + + +FreeFileSync 5.10 [2012-12-03] +------------------------------ +Show synchronization log as a grid in results dialog +Improved grid scrolling performance (most noticeable on Linux) +Allow grid selection starting from outside of the grid +RealTimeSync: Support drag & drop on main dialog for *.ffs_real and *.ffs_batch files +Optimized memory consumption when generating log for millions of items +Optimized memory consumption when exporting to CSV file +Have grid row height match window default font size +Catch out of memory when copying huge lists into clipboard +Fixed failure to resume aborted sync after having FFS implicitly create target directory +Fixed horizontal mouse wheel scrolling direction for RTL languages (Hebrew) +RealTimeSync: Fixed drag and drop not working (Linux) +Set maximum size of LastSyncs.log in GlobalSettings.xml element +Show error when trying to copy a named pipe rather than hang (Linux) +Improved copy routine minimizing file accesses (Linux) +Copy file access permissions by default (Linux) +Fixed unexpected "File or Directory not existing" error during file copy (Linux) + + +FreeFileSync 5.9 [2012-11-03] +----------------------------- +Scroll grid under mouse cursor +Move files directly to recycle bin without parent "FFS 2012-05-15 131513" temporary folders +Offer $HOME directory alias in directory drop down list (Linux) +Support for tilde (~) character in input folder paths (Linux) +New environment variables for RealTimeSync: %change_action%, "%change_path% +Use Internet Explorer proxy settings for new version check (Windows) +Show proper error message after failed symlink creation +Start comparison upon double-clicking config list +New batch return code: "Synchronization completed with warnings" +Hide files that won't be copied by default if direction "none" is part of the rule set (e.g. update variant) +Remember save config and folder picker dialog positions separately +New sync completion sound +Fixed sync completion sound not playing (Ubuntu) + + +FreeFileSync 5.8 [2012-10-01] +----------------------------- +New icon theme +Dynamic save button and dialog title show unsaved configuration +Exclude all folders if file size or time span filters are active +Added macros %csidl_Nethood%, %csidl_Programs%, %csidl_Startup% +Fixed crash on failed CRT parameter validation (Windows) +Update-checker handles moved web address +Fixed configuration conversion error when deleting into versioning folder +Avoid modal error dialogs in batch mode unless error handling is set to "pop up" +Set return codes in batch mode even if modal dialogs are shown +Disabled UAC virtualization for 32-bit user-mode process +Descriptive error message when setting invalid dates on FAT volumes + + +FreeFileSync 5.7 [2012-09-04] +----------------------------- +Modern directory selection dialog (Windows Vista and later) +New file versioning scheme appending revision number to files +New sync option to limit number of versions per file +Revised configuration format for *.ffs_gui/*.ffs_batch files: old format will be supported for some time +Fixed crash on invalid file modification times +Fixed zlib error on empty database stream +GlobalSettings.xml: added "MaxSize" parameter to "ConfigHistory" +Fixed occasional crash on GTK 2 (Linux) +Always show "items processed" in log file +Simplified configuration dialogs +Fixed password prompt not always coming up when connecting to a network share +Support environment variables everywhere: +on completion; +external applications; +RTS command +Harmonized external application macros: %item_path%, %item_folder%, %item2_path%, %item2_folder% +Updated translation files + + +FreeFileSync 5.6 [2012-08-02] +----------------------------- +Resize left and right grids equally +Allow to move middle grid position via mouse +Automatically resize file name columns +Do not follow reparse points other than symlinks and mount points +Warn if recycle bin is not available during manual deletion +Fixed error when saving log file into volume root directory +Show files which differ in attributes only in the same category as "equal" files +Apply hidden attribute to lock file +Fixed potential "access denied" problem when updating the database file +Show errors when saving configuration files during exit (ignore for batch mode) +Mark begin of comparison phase in the log file +More detailed tooltip describing items that differ in attributes only +Added Scottish Gaelic translation + + +FreeFileSync 5.5 [2012-07-01] +----------------------------- +New database format for variant: old database files are converted automatically +Tuned performance for variant when saving database for millions of files: > 95% faster +Support partial database updates for variant respecting current filter +Reduced size of database files by 30% +Fine-tuned algorithm to avoid certain conflicts after changing comparison settings +Lower peak memory consumption when reading database participating in multiple sync jobs +Refined symlink categorization and variant handling +Always save log of last syncs to %appdata%\FreeFileSync\LastSyncs.log (128 kB limit) +"Save" and "Save As" menu options +Properly show status message after save configuration +Avoid issues applying file modification time on certain NAS +Refined last-used configuration handling +Avoid race-condition: database file is only read if directory is existing +Protect against temporary network drop between comparison and synchronization +Rearranged statistics panel to save vertical space when vertically aligned +Removed limitation for number of conflicts shown in the warning message and log +Consider both global and local filter when estimating whether folder could contain matches +Updated translation files + + +FreeFileSync 5.4 [2012-06-01] +----------------------------- +Copy all NTFS extended attributes +Improved statistics panel +Improved main grid +Support context menu for files in overview tree +Process double-clicks outside main grid +Allow quoted paths ending with backslash in command line: "C:\" +Fully localized number formatting (Windows) +Fixed deletion dialog header being trimmed (Linux) +Fixed exclusion via context menu (Linux) +Preserve row label width after comparison (Linux) +Updated help file +New batch mode return codes, see help file +Prefix custom deletion directory with job name +Use the same time stamp for log file and versioning +Handle folder drag and drop outside main grid +Avoid name clash having multiple folder pairs delete into the same versioning folder +Exit FreeFileSync automatically while upgrading to new version +Accessibility: Support high-contrast color schemes +Yet another UI design overhaul +Fixed "access denied" issue on OS X-hosted network shares +Support Citrix folder shares +Support Arch Linux (Chakra) +Updated translation files + + +FreeFileSync 5.3 [2012-05-02] +----------------------------- +Show which processes lock a file during synchronization (Windows Vista and later) +Use unbuffered copy to speed up copying large files (Windows Vista and later) +Preserve NTFS sparse files +Support referencing all logical volumes by name (including FreeOTFE virtual drives) +Fixed lag showing "Searching for directory" on comparison +New context menu filter option: exclude by short name +Use clicked-on row rather than anchor when determining action for shift-selection +Refresh grid after pressing "CTRL + A" +Add base folder pairs to CSV export +Show full path in tooltip if multiple folder pairs are used +Show child dialogs on same monitor as parent dialog on multiple monitor systems +Added statistics at beginning of batch log file +Fixed batch mode final speed statistic and reset graph after binary comparison +RealTimeSync: Automatically retry after 15 seconds if an error occurs +Show button images untrimmed (Linux) +Fixed problems with auto-closing progress dialog (Linux) +Fixed unresponsive progress dialog and systray icon (Linux) +New option in GlobalSettings.xml: "LockDirectoriesDuringSync" +Added Lithuanian translation +Added Norwegian translation +Updated translation files + + +FreeFileSync 5.2 [2012-04-01] +----------------------------- +Fixed runtime error "Error comparing strings! (LCMapString)" (Windows 2000, XP) + + +FreeFileSync 5.1 [2012-03-31] +----------------------------- +New category for time span filter: last x days +Fixed "Error loading library function: GetVolumeInformationByHandleW" if NTFS permissions are copied +Fixed command line issues: allow config name without extension, allow multiple directories instead of a config file +Reenabled global shortcut F8 to toggle data shown in middle grid +Unified error handling on failure to create log directory +Do not close batch creation dialog after save +Tree view: compress and filter root nodes the same way as regular folder nodes +Fixed wrong tooltip being shown if directory name changes +Date range selector does not trim year field anymore +Show action "do nothing" on mouse-hover for conflicts in middle grid +Fixed "Windows Error Code 59: An unexpected network error occurred" +New filter pattern: *\* matches all files in sub directories of base directories +Fixed "*?" filter sub-sequence +Fixed "Cannot convert from the charset 'Unknown encoding (-1)'!" +Support CTRL + A in filter dialog +Support large filter lists > 32 kByte +Allow to hide file icons +Avoid switching monitor when main dialog is maximized on multiple monitor systems +Improved huge XML file loading times by a factor of 3000, saving by a factor of 3 +Restore grid scroll position after repeated comparisons +Show log after sync when non-fatal errors occurred +Fixed crash in UTF8 conversion when processing a corrupted ffs_db file +Even more pedantic user interface fine-tuning +Compiles and runs on openSuse 12.1 +Fixed grid page-up/down keys scrolling twice (Linux, wxGTK 2.9.3) +Fixed unwanted grid scrolling when toggling middle column (Linux, wxGTK 2.9.3) +Fixed middle grid tooltip occasionally going blank (Linux) +Support single shift-click to check/set direction of multiple rows +Removed gtkmm dependency (Linux) +Installer remembers all settings for next installation (local installation only) +All executables digitally signed +Updated translation files + + +FreeFileSync 5.0 [2012-01-30] +----------------------------- +New grid control +New tree control +Revised Right to Left layout for Hebrew +Updated translation files + + +FreeFileSync 4.6 [2011-12-25] +----------------------------- +Execute user-defined command after synchronization +Option to automatically close synchronization progress dialog +Automatically adjust statistics during sync if changes happened after comparison +Fixed "DecodePointer could not be located in KERNEL32.dll" (Windows 2000) +Fixed "Windows Error Code 31: A device attached to the system is not functioning" +Mouse wheel will scroll list of folder pairs instead of toggle through directory history +No error message when scanning a single directory +Minimized disk accesses when deleting files +Less mouse-clicks required when overwriting configuration +Pause timers while showing error messages +Show error message for malformed external commands +Support detection of moved files over "subst" alias +New default font: Segoe UI (Windows Vista and later) +Save settings before forced exit due to shutdown or log off +Updated translation files + + +FreeFileSync 4.5 [2011-11-25] +----------------------------- +Fixed "Windows Error Code 50: The request is not supported" +Fixed "Windows Error Code 124: The system call level is not correct" +Fixed config load performance problem if network drive is not reachable +Support traversing truly empty directories (no ., ..) (Windows) + + +FreeFileSync 4.4 [2011-11-22] +----------------------------- +Fixed error copying files containing alternate data streams (Windows) + + +FreeFileSync 4.3 [2011-11-20] +----------------------------- +Detection of moved and renamed files +New database format for mode: a full sync is suggested before upgrading +Fixed overwrite symlink with regular file +Fixed synchronization result dialog GUI glitch (Windows XP) +Fixed macro %weekday% +RealTimeSync: Fixed support for manual volume unmount (Windows) +Added Croatian language +Updated translation files + + +FreeFileSync 4.2 [2011-11-02] +----------------------------- +Implemented workaround for compiler bug leading to uncaught exceptions (Windows 32 bit) +Shadow Copy Service: Native support for Windows7/Server 2008 +Fixed reference by volume name parsing issue +Rearranged synchronization progress dialog +More concise log message format +Fixed default file icon (Kubuntu) +Support for wxWidgets 2.9 series (Ubuntu/Kubuntu) +FAT 2 sec tolerance for files dated in the future +Honor DACL/SACL inheritance flags when copying NTFS permissions (Windows) +New option in GlobalSettings.xml: "RunWithBackgroundPriority" (Windows Vista and later) + + +FreeFileSync 4.1 [2011-10-09] +----------------------------- +Improved synchronization progress dialog +Show all available aliases in directory history list +Show password prompt when connecting to mapped network share +Removed busy cursor after program start up +RealTimeSync: atomically detect missing directories +Handle not existing reference by volume name as an invalid path +Improved start up responsiveness by checking dir/file existence asynchronously +Fixed loading incorrect directory name when using multiple folder pairs +Allow passing multiple configurations via command line +Allow passing multiple directory names via command line + + +FreeFileSync 4.0 [2011-09-25] +----------------------------- +Thumbnail list view +Option to specify comparison settings at folder pair level +Correctly update parent-child relationship when changing sync directions +Show history list for additional folder pairs +Switch between volume name and full path in directory history list +Perf: shrinked folder matching CPU time by over 70% +Show windows environment strings in directory history list +Show windows special folder IDs in directory history list +Fixed progress dialog going into background on heavy load +Support creating old 8.3 directories +Take over configuration name when creating new batch job +Remember batch-specific settings when loading a ffs_batch file from main dialog +Drag & drop ffs_batch files on main dialog to test and edit batch settings +Automatically resolve objects deleted externally after comparison +Date column context menu: manual time range selector +New categories for time span filter: today, this week, this month, this year +Respect both sides when sorting by relative path +Updated COM error message reporting resolving "Unknown error" +Smarter configuration merge algorithm +Correctly show existing folders on both sides when using include filter +Fixed network access using WebDrive +Update modification times during file copy to write current values to database +RealTimeSync: write name of changed file into environment variable "changed_file" +RealTimeSync: fixed network drop incorrectly being handled as a failure +Set default direction according to current configuration when deleting manually +Plenty of GUI improvements +Updated help file +Updated translation files + + +FreeFileSync 3.21 [2011-08-19] +------------------------------ +Fixed deleting to user-defined directory +Fixed crash when using include filter +New global option to disable transactional file copy + + +FreeFileSync 3.20 [2011-08-11] +------------------------------ +Scan multiple directories in parallel +Automatically resolve disconnected network maps +Fixed temporal hang when dropping large files on main dialog + mode: Fixed issue regarding directory names differing in case during first sync +Delete permanently if recycle bin is not available (Linux) +Keep FreeFileSync responsive when trying to access non-existent network folder +Support for Ubuntu Unity Launcher (Linux) +RealTimeSync: Failure notification if command line is invalid (Linux) + + +FreeFileSync 3.19 [2011-07-23] +------------------------------ +Exclude sub directories from synchronization which cannot be accessed during comparison +Warning if Recycle Bin is not available instead of deleting silently (Windows) +Adapted log message if missing recycler leads to permanent deletion (Windows) +Revert to per file recycle bin handling if creating temp recycler folder fails +Avoid orphaned deletion temp directories on network drives +Quick-select comparison and synchronization options via double-click +New right-click drop down menu on comparison and synchronization settings button +New database design: copying the database file does not lead to complications anymore +Full support for "retry" while comparing +Don't copy empty folders when filtering by time span +Allow loading/merging multiple configurations files via file open dialog +Allow loading/merging multiple configurations in last used config list +Fixed system shutdown interruption during batch mode +Allow saving log files in both silent and non-silent batch jobs +Reduced main dialog flicker when switching configurations +Database and lock files created by FreeFileSync do not trigger RealTimeSync anymore +Restrict maximum number of visible folder pairs to 6 (configurable via GlobalSettings.xml) +New macros: %day%, %hour%, %min%, %sec% + + +FreeFileSync 3.18 [2011-07-03] +------------------------------ +Launcher running synchronously and returning application error code +Fixed sort by file extension +Fixed drag and drop of SAMBA network folder +Render (all) invalid file dates correctly on GUI +Correct layout selection for RTL and LTR languages +Correct GUI status texts while waiting for directory lock +Properly set default directory when loading configuration +New XML framework: zen::Xml +Added Hebrew language +Added Danish language +Updated translation files + + +FreeFileSync 3.17 [2011-05-20] +------------------------------ +Filter files by size +Filter latest files by time span +Launcher automatically selecting 32/64 bit executable on start up +More detailed systray progress indicator +New database format for mode: a full sync is suggested before upgrading +Update database at individual file level (support for partial and aborted syncs) +New translation file format +Dynamically load existing translation files +Correct translation plural forms +Improved directory locking strategy +Restructured installation package +One button-click synchronization +Fixed CSV character encoding +Put CSV values in quotes if they contain semicolons +Explicit button and settings for "Custom" sync variant -> old configurations need to be migrated +Keyboard shortcuts also on middle grid +Minimize progress dialog by clicking on taskbar +Render invalid file dates correctly on GUI +Process user-defined commands via shell execution (FFS and RTS) +Allow base directory names having trailing white-space +Added Ukrainian language +Updated translation files + + +FreeFileSync 3.16 [2011-04-21] +------------------------------ +Fixed file copy issues on SAMBA shares +Small GUI fixes + + +FreeFileSync 3.15 [2011-04-19] +------------------------------ +Overwriting a file as fully transactional operation +Optimized synchronization speed (non-cached volumes, e.g. memory sticks in particular) +Volumes can be specified by name: []\ (use case: variable drive letters, RealTimeSync) +Copy NTFS compressed, encrypted and sparse file attributes +Copy NTFS compressed and encrypted directory attributes +Copy NTFS alternate data stream +Improved performance: CSV export, copy to clipboard, sync log display +Improved color theme support +Fixed crash on certain system text color settings +Fixed progress numbers for manual deletion +Allow aborting manual deletion via escape key +Use relative name for file tooltip +Automatically redirect arrow keys to main grid +More tolerant directory creation (operation not supported/wrong parameter) +More tolerant file move: ignore existing files (user-defined deletion directory) +Added macro %weekday% + + +FreeFileSync 3.14 [2011-03-20] +------------------------------ +New keyboard shortcuts: F5: compare F6: synchronize +Skip to next folder pair if fatal error occurred (instead of abort) +Reload last selected configuration on start up +Abort with error when copying to empty directory field +Full log information after comparison (including file transfer) +Check read access for source file before overwriting target +Fixed possible application crash after comparison +Fixed possible network freeze when comparing +Maximum number of log files can be specified +Don't condense white-space when loading XML configuration +RealTimeSync: Put executable name in quotes when parsing *.ffs_batch file +Large program icons - 256 x 256 +Handle daylight saving time(DST) on FAT network shares +Skip DST handling if drive does not support accurate file times +Many small GUI/usability fixes +Added Korean translation + + +FreeFileSync 3.13 [2011-01-16] +------------------------------ +Implemented Advanced User Interface to allow user specified layout customizations +Process case sensitive file/directory/symlink names +Synchronize name/attributes only avoiding full copy if appropriate +Prevent hibernation/sleep mode during comparison and synchronization (Windows) +New database format: single file for FreeFileSync 32 and 64 bit versions + - full sync suggested before migrating to v3.13 + - old sync.x64.ffs_db files may be deleted +Improved algorithm to calculate remaining time +Allow resizing window containing multiple folder pairs +Show folder short names in column file name +Correctly report message "nothing to sync" in batch mode +Removed libjpg-8 dependency (Linux) +Fixed loading correct maximized position on multi-screen desktop +RealTimeSync: Removed blank icons in ALT-TAB list during execution of command line +Show RealTimeSync job name as systray tooltip +Last used configurations as sorted list without size limitation +Remove redundant configuration when merging multiple ffs_gui/ffs_batch files +Warning if folder is modified that is part of multiple folder pairs +Aggregated warning messages for all folder pairs instead of one per pair +Added privilege to access restricted symlink content +Added Greek translation + + +FreeFileSync 3.12 [2010-11-28] +------------------------------ +Allow empty folder pairs without complaining +Automatically exclude database and lock files from all (sub-)directories (not only from base) +Resize grid columns on both sides in parallel +Fixed tooltip foreground text color (Linux) +Search via CTRL + F and F3 now as global hotkeys +Fully portable use of directory locking (Windows/Linux, 32/64 bit) +RealTimeSync: Treat missing network path the same as missing local path +Show current job name during synchronization (batch/gui) +Allow copying dereferenced (=followed) directory Symlinks over network share +Fail to copy Symlinks (=direct) over network share instead of silently creating empty folder (Windows XP) +Copy NTFS junctions as Symlinks (avoiding permission checks) +RealTimeSync: ignore request for device removal on network mapped drives +Support for copying SELinux security contexts +Fixed moving buttons in synchronization dialog +Allow deleting currently selected item from list of last used folders (not before wxWidgets 2.9.1) +Avoid losing focus after manually deleting a file +Preserve custom changes to sync directions after manually deleting a file +Handle empty tooltips correctly (Linux) +Updated translation files + + +FreeFileSync 3.11 [2010-09-20] +------------------------------ +Fixed migration issue: reasonable default value for number of folder pairs +Better message box background color + + +FreeFileSync 3.10 [2010-09-19] +------------------------------ +Automatically solve daylight saving time and time zone shift issues on FAT/FAT32 (finally) +Instantly resolve abandoned directory locks associated with local computer +Show expanded directory name as tooltip and label text (resolves macros and relative paths) +Do not copy relative file attributes for base target directories that are created implicitly +Move dialogs by clicking (almost) anywhere +RealTimeSync: ignore request for device removal on Samba shares +Added UTF-8 BOM for CSV export +Correctly handle window position on multi-screen desktop +Disabled warning "database not yet existing" +RealTimeSync: replaced delay by minimum idle time +Maximum number of folder pairs configurable via GlobalSettings.xml (XML node ) +Added tooltips to display long filenames on main grid +Keep application responsive when deleting large directories +Vista/Windows 7: harmonize modification times shown on main grid with Windows Explorer +Changed background color to avoid unreadable texts in combination with certain color themes +Toggle middle grid comparison result/sync preview with right mouse button click +Further GUI enhancements/polishment/standard conformance +Updated translation files + + +FreeFileSync 3.9 [2010-08-10] +----------------------------- +Advanced locking strategy to allow multiple processes synchronize the same directories (e.g. via network share) +Merge multiple *.ffs_batch, *.ffs_gui files or combinations of both via drag & drop +Copy file and folder permissions (requires admin rights): + - Windows: owner, group, DACL, SACL + - Linux: owner, group, permissions + - correctly handle Symbolic Links + - new option in global settings +Compare by content evaluates Symbolic Links +32-Bit build compiled with MinGW/GCC to preserve Windows 2000 compatibility +RealTimeSync: Handle requests for device removal (USB stick) while monitoring +Sort by file size: group symlinks before directories +Added macros %week%, %month%, %year% for creating time-stamped directories +Touch database file when changes occurred only +Moved settings "file time tolerance" and "verify copied files" to GlobalSettings.xml +Updated translation files + + +FreeFileSync 3.8 [2010-06-20] +----------------------------- +New options handling Symlinks: ignore/direct/follow => warning: new database format for mode +Fixed crash when starting sync for Windows XP SP2 +Prevent tooltip from stealing focus +Show associated file icons (Linux) +Run folder existence checks in separate thread (faster network share access) +Write mode database file even if both sides are already in sync +Don't raise status dialog to the top after synchronization +Embedded version information into executable (Windows) +Migrated compiler to Visual C++ 2010 (Windows) +Avoid losing manual changes when excluding via context menu +Adjusted update-checker web-address +Updated translation files + + +FreeFileSync 3.7 [2010-05-16] +----------------------------- +RealTimeSync: Trigger command line only if all directories are existing +Allow for drag and drop of very large files +Batch modus: New "Switch" button opens GUI modus when warnings occur +Support copying old 8.3 filenames correctly +Handling of Symbolic Links configurable via GUI +Fine tuned calculation of remaining disk space for custom deletion directories +Save default config files only if actually changed +NSIS installer: Support for /D and /S switches +Fixed resource loading if installation folder is not working directory (Linux build) +Consolidated batch creation dialog + mode: Detect conflict when a directory shall be deleted while new sub-elements are to be copied +Automatically mark left behind temporary files (*.ffs_tmp) for deletion with next sync +New Project website: freefilesync.sourceforge.net +A lot of small GUI fixes +Updated translation files + + +FreeFileSync 3.6 [2010-03-31] +----------------------------- +Fixed occasional crash when starting FreeFileSync + + +FreeFileSync 3.5 [2010-03-27] +----------------------------- +Allow mode syncs between 32 bit, 64 bit, Windows and Linux builds +Show progress indicator in window title +Support for progress indicator in Windows 7 Superbar +Reduced progress indicator flicker +Prevent silent batch mode from taking keyboard focus +Improved error messages (loading/saving/copying files) +Improved environment variable tolerance: strip blanks and double-quotes +RealTimeSync: Fixed crash when double-clicking systray icon +Allow aborting all operations via Escape key +Added British English translation + + +FreeFileSync 3.4 [2010-03-04] +----------------------------- +Performance: Reduced Recycle Bin access time by 90% +Recycle Bin support for Linux +Performance: Reduced binary comparison sequential read time (by up to 75% for CD/DVD access) +Improved synchronization sequence to avoid disk space shortage: overwrite large files by small ones first +Fixed problems with file renaming on Samba share +New free text grid search via shortcuts CTRL + F and F3 +Show number of processed files at end of synchronization +New optional grid column: file extension +New comparison category icons +Fixed handling sync-config of first folder pair +Allow moving main dialog by dragging client area instead of title bar only +Enhanced help file: Run RealTimeSync as Service +Prefix log files with name of batch job +Fixed GUI right-to-left mirroring for locales Hebrew and Arabic +Portable version: save configuration in installation folder +Many small GUI enhancements +Updated translation files +New Linux .deb package: ppa:freefilesync/ffs + + +FreeFileSync 3.3 [2010-02-02] +----------------------------- +New installer package for portable/local/32/64-bit versions +Built-in support for very long filenames: apply \\?\-prefix automatically +New button for synchronization preview: show equal files +RealTimeSync: Respond to directory or volume arrival, e.g. USB stick insert +Start comparison automatically when double-clicking on *.ffs_gui files +Visual progress indicator for sys-tray icon +Fixed string comparison for 'ß' and 'ss' (all Windows versions) +Fixed general string comparison for Windows 2000 +Significantly faster file icon loading +Applied new IFileOperation interface for recycle bin (Windows >= Vista) +Patched mode to handle FAT32 2-second file time precision +Play optional sound after comparison: "Compare_Complete.wav" +Allow environment variables for log file-directory +Enhanced conflict reporting +Added Swedish translation +Updated translation files + + +FreeFileSync 3.2 [2009-12-13] +----------------------------- +Native Windows 64-Bit version (including Volume Shadow Copy Service) +Harmonized filter handling: global and local file filters +Unified handling of first folder pair: all pairs now semantically equal +Use environment variables within directory names (e.g. %USERNAME%) +New keyboard shortcuts to set sync-direction: ALT + +Allow copying to non-encrypted target directory +Fixed sort by filename +Fixed GDI resource leak when scrolling large grids +Fixed string comparison for 'ß' and 'ss' (Windows >= Vista) +Faster file icon loading +Remove elements in folder drop down list via DEL key +New integrated help file +Play optional sound after synchronization: "Sync_Complete.wav" +Several GUI/usability improvements +Created package for PortableApps.com +Added Finnish translation +Updated translation files + + +FreeFileSync 3.1 [2009-10-26] +----------------------------- +Support for multiple data sources in Automatic mode +Copy file and folder create/access/modification times when synchronizing +Progress dialog can be minimized to systray (Batch and GUI mode) +Allow switching between silent/non-silent batch mode interactively +Some GUI improvements + + +FreeFileSync 3.0 [2009-10-15] +----------------------------- +New synchronization mode: +Consolidated batch mode error handling +Fixed crash when comparing multiple pairs by content +Fixed calculation of remaining objects +Fixed swapping grids +Show scanned files when traversing with filter enabled +New default filter values +New macros %time%, %date% for creating time-stamped directories +Avoid corrupted data when program is terminated unexpectedly +Prevent deletion when source-directory (temporarily) is not accessible +Native Unicode support for Linux build +Added Romanian translation +Added Turkish translation +Updated translation files + + +FreeFileSync 2.3 [2009-09-27] +----------------------------- +New filter and sync configuration at folder pair level +Improved sorting: sort across multiple folder pairs + stable sorting in middle grid + consolidated sorting of sync-direction +Open external applications via context menu(customizable) +Removed performance penalty when using include filters +Improved filter syntax for strings beginning with wildcards +Default handling for conflict files now configurable +New option to show all hidden dialogs again +Fixed issue with macros %nameCo, %dirCo +New option in *.ffs_gui/ffs_batch files: Verify copied files +Use Windows Volume Shadow Copy for shared and locked files(new) +More detailed information in *.cvs export +Use current working directory to save global configuration (portable version) +Respect sub directories when manually changing sync-direction +Allow import of batch configuration into GUI mode +Some small GUI improvements +New shortcuts: SPACE: (de-)select rows; ENTER: start external application +Performance improvements: Reduced CPU time by 28%, (peak) memory consumption by 20% +Added Traditional Chinese translation +Updated translation files + + +FreeFileSync 2.2 [2009-08-16] +----------------------------- +New user-defined recycle bin directory +Possibility to create synchronization directories automatically (if not existing) +Support for relative directory names (e.g. \foo, ..\bar) respecting current working directory +New tooltip in middle grid showing detailed information (including conflicts) +Status feedback and new abort button for manual deletion +Options to add/remove folder pairs in batch dialog +Added tooltip showing progress for silent batch mode +New view filter buttons in synchronization preview +Revisioned handling of symbolic links (Linux/Windows) +GUI optimizations removing flicker +Possibility to create new folders via browse folder dialog +Open files with associated application by special command string +Improved warning/error handling +Auto-adjust columns automatically or manually with CTRL + '+' +New macros for double-click command line: %name, %dir, %nameCo, %dirCo +Fixed runtime error when multiple folder pairs are used +New tool 'RealTimeSync': Watch directories for changes and start synchronization automatically +Improved XML parsing, fault tolerance and concept revisioned +More detailed statistics before start of synchronization +Removed superfluous border for bitmap buttons (Linux only) +Added Czech translation +Updated translation files + + +FreeFileSync 2.1 [2009-07-03] +----------------------------- +Fixed bug that could cause FreeFileSync to crash after synchronization +Compiled with MS Visual C++ 2008 using static runtime library + + +FreeFileSync 2.0 [2009-06-30] +----------------------------- +Copy locked files using Windows Volume Shadow Copy +Load file icons asynchronously for maximum display performance +Handle include filter correctly when comparing +Display optional summary window before starting synchronization +Adjust sync direction properly when switching sides +Info about sync variant on main dialog +Issue a warning message for each conflict type when comparing +Save default configuration in user application path (Installer based version) +Limit main dialog minimum size +Update grid row labels while scrolling +Right-click selects cell before opening context menu +New context menu options to manually assign a sync-direction +Moved sync-preview switch into middle grid's context menu +Possibility to remove top folder pair +Fixed calculation of row total in sync preview +File icons configurable for each side +Many small GUI improvements +Compiled successfully with GCC 4.4.0 and MS Visual C++ 2008 +Added Russian translation +Updated translation files + + +FreeFileSync 1.19 [2009-06-01] +------------------------------ +New synchronization preview +Sync-direction can be adapted manually +New category type "conflict" +New check for unresolved conflicts +Improved overall GUI layout +New check for erroneous file modification dates +Optional pop up to notify on changed configuration +Files with invalid dates (e.g. year 30.000) do not result in a program abort anymore +Replaced column "full name" by "full path" to be combined with "filename" +Apply filtering WHILE comparing (if activated) and avoid traversing excluded directories +New filter paradigm: use relative instead of absolute names +New option "ignore DST +/- 1-hour" to correctly handle daylight saving changes +Sync preview statistics now on main dialog +Show only relevant synchronization options +File icon display configurable via grid column context menu +Updated translation files + + +FreeFileSync 1.18 [2009-05-10] +------------------------------ +Linux build officially released: all major problems solved! +New statistic: remaining time +New statistic: bytes per second +Automatically check for program updates every week +Finally got rid of scroll bar in middle grid for Linux build +Fixed issue with file icon display +Fixed overlapping grid cells +Alternate log file directory configurable via GUI +Added drag & drop support for batch job assembly +Simplified filter usage: - matches "\*" as well as "\" + - only distinct filter entries are considered +Platform dependent line breaks in configuration *.xml files +"Significant difference check" runs at folder pair level +Sorting runs at folder pair level +New check for sufficient free disk space (considering recycle bin usage) +New optional grid column: directory +New sort by directory name +Reduced memory consumption by 10% +A lot of smaller improvements +Added Brazilian Portuguese translation +Updated translation files + + +FreeFileSync 1.17 [2009-04-05] +------------------------------ +Full support for Windows/Linux symbolic links: + - traverse, copy, delete symbolic links + - handle broken symbolic links + - new options in GlobalSettings.xml: TraverseDirectorySymlinks, CopyFileSymlinks +New menu option: "Check for new version" +Copy folder attributes and security settings when implicitly creating folders +Maximum file time difference now fully configurable +New history of last selected folders +Fixed "Year-2038-Problem" for time_t +Upgraded to wxWidgets 2.8.10 +Individual folder pairs can be selected for removal +Performance: Reduced CPU time by 9%, memory consumption by 36% +Support for cancellation when copying and comparing large files +Smooth progress indicators when copying and comparing large files +Support for Shift-PageUp/PageDown +Support for Home/End and Shift-Home/End +Alternative log file directory configurable via *.ffs_batch Xml +Show explorer file icons in grid (windows only) +Fixed compilation issues for Linux build +Fixed grid alignment issue in Linux build +Enhanced error messages for Linux build +Optimized traversing algorithm for Linux build +Fixed graphical misalignment with multiple folder pairs +Added Slovenian translation +Added Hungarian translation +Added Spanish translation +Updated translation files + + +FreeFileSync 1.16 [2009-03-13] +------------------------------ +Support for \\?\ path prefix for unrestricted path length (directory names > 255 characters) (windows only) +Copy files even if target folder does not exist +Fixed occasional error when switching languages +Added sys-tray icon for silent batch mode (pause, abort, about) +Support for numeric DEL-key +Avoid endless loops with Vista symbolic links (don't traverse into symbolic links - configurable) +New functionality for loading batch files (load button or drag & drop to main/batch window) +New options for batch file error handling: "pop up, ignore errors, exit with returncode < 0" +New option to reset all warning messages +Allow marking both sides of the main grid via CTRL + mouse-click +Allow manual deletion of files on both or one side only (respecting selections on both sides) +Special recycler option for manual deletion +New optional grid column: Full name +Fixed locale related issue when comparing. Big thanks to Persson Henric for providing support! +New check if more than 50% of files will be overwritten/deleted +Save memory by clearing old results before re-comparing +Usability improvements: + - name of config file in window title + - refresh view filters on configuration load + - default to ascending sort when changing column + - maximum length of config file history customizable through xml + - new "load configuration" button + - check/uncheck option for middle grid + - support for CTRL + A (select all) + - enhanced error messages (windows only) +Updated translation files + + +FreeFileSync 1.15 [2009-02-22] +------------------------------ +Fixed performance bottleneck in batch mode (non-silent) +Improved performance of comparison by another 10% +Configure column settings by right-click context menu +Remember column positions on main grid +Hide/Show individual columns +Added "sort by comparison result" +Sort file list by relative name after comparison (GUI mode only) +Removed Windows registry usage for portable version +Restored line breaks in status texts for better readability +Revised German translation. Thanks to «Latino»! +Created custom button control to finally translate "compare" and "synchronize" +Allow manual setup of file manager integration (Windows and Linux) +Added Step-By-Step guide for manual compilation (Windows and Linux) +Added checkboxes to manually select/deselect rows +New option: Treat files with time deviation of less-equal 1 hour as equal (FAT/FAT32 drives only) +Added Polish translation +Added Portuguese translation +Added Italian translation +Updated translation files + + +FreeFileSync 1.14 [2009-02-01] +------------------------------ +Massive performance improvements: +- comprehensive analysis and optimization of comparison functionality +- new, fast directory traversing algorithm +- improved folder hierarchy compare algorithm +- lazy evaluation of formatted date strings +- new high-performance string class +=> reduction of CPU time by more than 90%! +Folder attributes are copied during synchronization +Sorting now case-insensitive (Windows-only) +Allow column positioning on main grid +Many small fixes +Added Chinese translation +Updated translation files + + +FreeFileSync 1.13 [2009-01-06] +------------------------------ +Automatically detect daylight saving time (DST) change for FAT/FAT32 drives +Added directory dependency check when synchronizing multiple folder pairs +New synchronization option: "update" +Reduced status screen flicker when comparing and synchronizing +Fixed bug when sorting by filename +Further GUI improvements +Updated translation files + + +FreeFileSync 1.12 [2008-12-23] +------------------------------ +Significantly improved speed of all sorting algorithms +Keep sorting sequence when adding or removing rows +'Sort by relative path' secondarily sorts by filename and respects folders +Allow adding multiple files/folders to exclude filter via context menu +Exclude full relative path instead of short filenames via context menu +Fixed possible memory leak when canceling compare +New option to manually adjust file modification times (To be used e.g. for FAT32 volumes on DST switch) +Handling of different types of configuration (GUI, batch, global) +Enhanced exception handling +Multiple GUI improvements +Added Dutch translation +Updated translation files + + +FreeFileSync 1.11 [2008-11-23] +------------------------------ +Support for multiple folder pairs +Optimized performance of multiple pairs to scan each folder just once +Enhanced batch file format +New context menu option to add files, file types or directories to exclude filter +Reworked file filter dialog +Updated translation files + + +FreeFileSync 1.10 [2008-11-09] +------------------------------ +Transformed configuration file format to XML +Exchanged batch files with shell links for full Unicode support (Windows-only) +Improved filter usage: ignore leading/trailing white-space, upper/lower-case (Windows-only) chars +Removed screen-flicker when clicking on compare: +Added elapsed time to compare status +Calculate height of middle grid independently of OS window layout +Multiple GUI improvements +Added Japanese translation +Updated translation files + + +FreeFileSync 1.9 [2008-10-26] +----------------------------- +Fixed wxWidgets multithreading issue that could cause synchronization to hang occasionally +Fixed issue with %1 parameter +Fixed issue with recycle bin usage in Unicode mode +Added uninstaller +New installer option to associate *.ffs files with FreeFileSync +Transformed language files to Unicode (UTF-8) +Delete elements in configuration history list via DELETE key + + +FreeFileSync 1.8 [2008-10-19] +----------------------------- +Enhanced status bar information +Enhanced log file information +Enhanced progress information +Added Unicode support +Program now waits until work is completed when abort is triggered during synchronization +Added French translation +Updated German translation + + +FreeFileSync 1.7 [2008-10-12] +----------------------------- +Display only those view filter buttons that are actually needed +Compare by size and date: last write time may differ by up to 2 seconds (NTFS vs FAT32) +Fixed minor issue with trailing path separator when creating batch jobs +Fixed minor issue with window sizes not being remembered in some special situation +Further improved Unicode compliance +Updated German translation + + +FreeFileSync 1.6 [2008-10-05] +----------------------------- +Significantly improved speed of filtering files and view (< 10 ms for > 200,000 rows(!)) +Fixed minor grid mis-alignment under some special conditions +Enhanced status bar with centered texts +Flexible filter options depending on compare variant +Improved synchronization statistics +Fixed issue when trying to delete system folders +Usability improvements +Recycle Bin usage as command line parameter +New menu bar +Program language selectable from menu +UI-option to create sync jobs (batch files) for automated synchronization +Updated German translation + + +FreeFileSync 1.5 [2008-09-21] +----------------------------- +Improved speed of comparison by file content +Simplified and optimized calculation of accumulated file sizes +Added right-click context menu to main dialog +New installer for Windows +Improved usability of filtering and selecting rows +Solved possible issue with different file time precisions in multi-OS environments +Updated German translation + + +FreeFileSync 1.4 [2008-09-14] +----------------------------- +Implemented generic multithreading class to keep "compare by content" and "file synchronization" responsive +Added status bar when comparing files (with additional status information for "compare by content") +Some further speed optimizations +Added option to skip error messages and have them listed after synchronization +Restructured loading of configuration files +The result grid after synchronization now always consists of items that have not been synchronized (even if abort was pressed) +Added "remaining files" as sync-progress information +Updated German translation + + +FreeFileSync 1.3 [2008-09-07] +----------------------------- +Maintain and load different configurations by drag&drop, load-button or command line +New function to delete files (or move them to recycle bin) manually on the UI (without having to re-compare): + Deleting folders results in deletion of all dependent files, subfolders on UI grid (also no re-compare needed) + while catching error situations and allowing to resolve them +Improved manual filtering of rows: If folders are marked all dependent subfolders and files are marked as well +(keeping sort sequence when "hide filtered elements" is marked) +Comprehensive performance optimization of the two features above (manual filtering, deletion) for large grids (> 200,000 rows) +Improved usability: resizable borders, keyboard shortcuts, default buttons, dialog standard focus +Main window will remember restored position even if maximized +Updated sources to become more Linux and Unicode friendly +Updated German translation + + +FreeFileSync 1.2 [2008-08-31] +----------------------------- +New progress indicator and status information when synchronizing: + ->available for command line mode and UI mode: Status update and final error report +New progress information when comparing directories +Multithreading for copying of files to keep program responsive +Optimized all status dialogs and progress indicators for high performance: practically NO performance loss +Possibility to abort all performance critical operations (comparison, synchronization) at any time +New options in case of an error: "Continue, retry, abort" for UI and command line +New command line option "-skiperrors" to continue synchronization despite errors +Enhanced log file (-silent mode) to include all errors during compare and synchronization +Do not synchronize folders that have been deleted externally (but show an error message) +Manually filter out ranges from synchronization instead of just single rows +Some UI improvements +New option to use Recycle Bin when deleting or overwriting files +New synchronization sequence: first delete files, then copy files to avoid disc space shortages +Added different return values when used in command line mode to report success or failure +Updated German translation + + +FreeFileSync 1.1 [2008-08-24] +----------------------------- +Some further speed optimizations (sorting) +Written custom wxGrid class to avoid mapping of data to UI: huge performance increase (especially with formatted grids > 100,000 items) +Filter files to include/exclude them from synchronization +Minor UI and text adaptions +Allow direct keyboard input for directory names +Added possibility to continue on error +Added indicator for sort direction +Simplified code concerning loading of UI resources +Prepared code to support Unicode in some future version +Updated German translation + + +FreeFileSync 1.0 [2008-08-10] +----------------------------- +Initial release diff --git a/FreeFileSync/Build/Resources/Gtk2Styles.rc b/FreeFileSync/Build/Resources/Gtk2Styles.rc new file mode 100644 index 0000000..b3cd5a5 --- /dev/null +++ b/FreeFileSync/Build/Resources/Gtk2Styles.rc @@ -0,0 +1,16 @@ +style "no-inner-border" +{ + GtkButton::inner-border = {0, 0, 0, 0} /*remove excessive borders on Gnome*/ + /*GtkButton::focus-padding = 0 => keep default: minor difference + looks better on KDE */ +} + +class "GtkButton" style "no-inner-border" + + +style "no-scrollbar-spacing" +{ + /* see wx+/grid.cpp: implementation assumes no spacing! */ + GtkScrolledWindow::scrollbar-spacing = 0 +} + +class "GtkScrolledWindow" style "no-scrollbar-spacing" diff --git a/FreeFileSync/Build/Resources/Gtk3Styles.css b/FreeFileSync/Build/Resources/Gtk3Styles.css new file mode 100644 index 0000000..ca82fe1 --- /dev/null +++ b/FreeFileSync/Build/Resources/Gtk3Styles.css @@ -0,0 +1,34 @@ +/* CSS format as required by CentOS (GTK 3.22.30) + pkg-config --modversion gtk+-3.0 + + https://docs.gtk.org/gtk3/css-overview.html + https://docs.gtk.org/gtk3/css-properties.html */ +* +{ + /* see wx+/grid.cpp: spacing wouldn't hurt, but let's be consistent */ + -GtkScrolledWindow-scrollbar-spacing: 0; +} + +button +{ + padding: 4px 5px; /*remove excessive inner border from bitmap buttons*/ + min-width: 0; + min-height: 0; + /*border-radius: 5px;*/ +} + +entry +{ + padding: 2px 5px; /*default is too small for text input*/ +} + +combobox entry +{ + padding: 0 5px; +} + +spinbutton entry +{ + padding: 0 5px; + /*margin-right: -50px; possible hack! but not needed right now */ +} diff --git a/FreeFileSync/Build/Resources/Gtk3Styles.old.css b/FreeFileSync/Build/Resources/Gtk3Styles.old.css new file mode 100644 index 0000000..ad11061 --- /dev/null +++ b/FreeFileSync/Build/Resources/Gtk3Styles.old.css @@ -0,0 +1,43 @@ +/* CSS format as required by Debian (GTK 3.14.5) + pkg-config --modversion gtk+-3.0 + + https://docs.gtk.org/gtk3/css-overview.html + https://docs.gtk.org/gtk3/css-properties.html */ +* +{ + /* see wx+/grid.cpp: spacing wouldn't hurt, but let's be consistent */ + -GtkScrolledWindow-scrollbar-spacing: 0; +} + +GtkButton +{ + padding: 4px 5px; /*remove excessive inner border*/ + /* min-width: 0; => Debian: Error code 3: Gtk3Styles.css:13:10'min-width' is not a valid property name [gtk_css_provider_load_from_path] + min-height: 0; */ +} + +GtkPaned +{ + border: 10px solid #d0d0d0; /*hack wxAUI panel splitter: not sure why "color" and "background-color" are not working*/ +} + +GtkEntry +{ + padding: 2px 5px; /*fix excessive padding for text input fields*/ +} + +GtkComboBox GtkEntry +{ + padding: 4px 5px; +} + +GtkSpinButton /*GtkEntry*/ +{ + padding: 4px 5px; +} + +.tooltip /* why not GtkTooltip!? */ +{ + color: white; + background-color: #343434; /*fix "Adwaita" theme glitch (Debian): background is *light grey*, while text color is white!*/ +} diff --git a/FreeFileSync/Build/Resources/Icons.zip b/FreeFileSync/Build/Resources/Icons.zip new file mode 100644 index 0000000..850025a Binary files /dev/null and b/FreeFileSync/Build/Resources/Icons.zip differ diff --git a/FreeFileSync/Build/Resources/Languages.zip b/FreeFileSync/Build/Resources/Languages.zip new file mode 100644 index 0000000..b415e90 Binary files /dev/null and b/FreeFileSync/Build/Resources/Languages.zip differ diff --git a/FreeFileSync/Build/Resources/bell.wav b/FreeFileSync/Build/Resources/bell.wav new file mode 100644 index 0000000..5c3e245 Binary files /dev/null and b/FreeFileSync/Build/Resources/bell.wav differ diff --git a/FreeFileSync/Build/Resources/bell2.wav b/FreeFileSync/Build/Resources/bell2.wav new file mode 100644 index 0000000..2a7610f Binary files /dev/null and b/FreeFileSync/Build/Resources/bell2.wav differ diff --git a/FreeFileSync/Build/Resources/cacert.pem b/FreeFileSync/Build/Resources/cacert.pem new file mode 100644 index 0000000..65be891 --- /dev/null +++ b/FreeFileSync/Build/Resources/cacert.pem @@ -0,0 +1,3511 @@ +## +## Bundle of CA Root Certificates +## +## Certificate data from Mozilla as of: Tue Dec 2 04:12:02 2025 GMT +## +## Find updated versions here: https://curl.se/docs/caextract.html +## +## This is a bundle of X.509 certificates of public Certificate Authorities +## (CA). These were automatically extracted from Mozilla's root certificates +## file (certdata.txt). This file can be found in the mozilla source tree: +## https://raw.githubusercontent.com/mozilla-firefox/firefox/refs/heads/release/security/nss/lib/ckfw/builtins/certdata.txt +## +## It contains the certificates in PEM format and therefore +## can be directly used with curl / libcurl / php_curl, or with +## an Apache+mod_ssl webserver for SSL client authentication. +## Just configure this file as the SSLCACertificateFile. +## +## Conversion done with mk-ca-bundle.pl version 1.30. +## SHA256: a903b3cd05231e39332515ef7ebe37e697262f39515a52015c23c62805b73cd0 +## + + +Entrust Root Certification Authority +==================================== +-----BEGIN CERTIFICATE----- +MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMCVVMxFjAUBgNV +BAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0Lm5ldC9DUFMgaXMgaW5jb3Jw +b3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMWKGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsG +A1UEAxMkRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0 +MloXDTI2MTEyNzIwNTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMu +MTkwNwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSByZWZlcmVu +Y2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNVBAMTJEVudHJ1c3QgUm9v +dCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +ALaVtkNC+sZtKm9I35RMOVcF7sN5EUFoNu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYsz +A9u3g3s+IIRe7bJWKKf44LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOww +Cj0Yzfv9KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGIrb68 +j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi94DkZfs0Nw4pgHBN +rziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOBsDCBrTAOBgNVHQ8BAf8EBAMCAQYw +DwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAigA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1 +MzQyWjAfBgNVHSMEGDAWgBRokORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DH +hmak8fdLQ/uEvW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA +A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9tO1KzKtvn1ISM +Y/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6ZuaAGAT/3B+XxFNSRuzFVJ7yVTa +v52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTS +W3iDVuycNsMm4hH2Z0kdkquM++v/eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0 +tHuu2guQOHXvgR1m0vdXcDazv/wor3ElhVsT/h5/WrQ8 +-----END CERTIFICATE----- + +QuoVadis Root CA 2 +================== +-----BEGIN CERTIFICATE----- +MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoT +EFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJvb3QgQ0EgMjAeFw0wNjExMjQx +ODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMRswGQYDVQQDExJRdW9WYWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQCaGMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6 +XJxgFyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55JWpzmM+Yk +lvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bBrrcCaoF6qUWD4gXmuVbB +lDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp+ARz8un+XJiM9XOva7R+zdRcAitMOeGy +lZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt +66/3FsvbzSUr5R/7mp/iUcw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1Jdxn +wQ5hYIizPtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og/zOh +D7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UHoycR7hYQe7xFSkyy +BNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuIyV77zGHcizN300QyNQliBJIWENie +J0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1Ud +DgQWBBQahGK8SEwzJQTU7tD2A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGU +a6FJpEcwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT +ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2fBluornFdLwUv +Z+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzng/iN/Ae42l9NLmeyhP3ZRPx3 +UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2BlfF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodm +VjB3pjd4M1IQWK4/YY7yarHvGH5KWWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK ++JDSV6IZUaUtl0HaB0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrW +IozchLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPRTUIZ3Ph1 +WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWDmbA4CD/pXvk1B+TJYm5X +f6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0ZohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II +4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8 +VCLAAVBpQ570su9t+Oza8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u +-----END CERTIFICATE----- + +QuoVadis Root CA 3 +================== +-----BEGIN CERTIFICATE----- +MIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoT +EFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJvb3QgQ0EgMzAeFw0wNjExMjQx +OTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMRswGQYDVQQDExJRdW9WYWRpcyBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQDMV0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNgg +DhoB4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUrH556VOij +KTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd8lyyBTNvijbO0BNO/79K +DDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9CabwvvWhDFlaJKjdhkf2mrk7AyxRllDdLkgbv +BNDInIjbC3uBr7E9KsRlOni27tyAsdLTmZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwp +p5ijJUMv7/FfJuGITfhebtfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8 +nT8KKdjcT5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDtWAEX +MJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZc6tsgLjoC2SToJyM +Gf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A4iLItLRkT9a6fUg+qGkM17uGcclz +uD87nSVL2v9A6wIDAQABo4IBlTCCAZEwDwYDVR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHT +BgkrBgEEAb5YAAMwgcUwgZMGCCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmlj +YXRlIGNvbnN0aXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0 +aWZpY2F0ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVudC4wLQYIKwYB +BQUHAgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2NwczALBgNVHQ8EBAMCAQYwHQYD +VR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4GA1UdIwRnMGWAFPLAE+CCQz777i9nMpY1XNu4 +ywLQoUmkRzBFMQswCQYDVQQGEwJCTTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEbMBkGA1UE +AxMSUXVvVmFkaXMgUm9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62gLEz6wPJv92ZV +qyM07ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSemd1o417+s +hvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd+LJ2w/w4E6oM3kJpK27z +POuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2 +Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadNt54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp +8kokUvd0/bpO5qgdAm6xDYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBC +bjPsMZ57k8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6szHXu +g/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0jWy10QJLZYxkNc91p +vGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeTmJlglFwjz1onl14LBQaTNx47aTbr +qZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK4SVhM7JZG+Ju1zdXtg2pEto= +-----END CERTIFICATE----- + +DigiCert Assured ID Root CA +=========================== +-----BEGIN CERTIFICATE----- +MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQw +IgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzEx +MTEwMDAwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQL +ExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0Ew +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7cJpSIqvTO +9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYPmDI2dsze3Tyoou9q+yHy +UmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW +/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpy +oeb6pNnVFzF1roV9Iq4/AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whf +GHdPAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRF +66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzANBgkq +hkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRCdWKuh+vy1dneVrOfzM4UKLkNl2Bc +EkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTffwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38Fn +SbNd67IJKusm7Xi+fT8r87cmNW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i +8b5QZ7dsvfPxH2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe ++o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== +-----END CERTIFICATE----- + +DigiCert Global Root CA +======================= +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBhMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAw +HgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBDQTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAw +MDAwMDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 +dy5kaWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsBCSDMAZOn +TjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97nh6Vfe63SKMI2tavegw5 +BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt43C/dxC//AH2hdmoRBBYMql1GNXRor5H +4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7PT19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y +7vrTC0LUq7dBMtoM1O/4gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQAB +o2MwYTAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbRTLtm +8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUwDQYJKoZIhvcNAQEF +BQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/EsrhMAtudXH/vTBH1jLuG2cenTnmCmr +EbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIt +tep3Sp+dWOIrWcBAI+0tKIJFPnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886 +UAb3LujEV0lsYSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE----- + +DigiCert High Assurance EV Root CA +================================== +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBsMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSsw +KQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5jZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAw +MFoXDTMxMTExMDAwMDAwMFowbDELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZ +MBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFu +Y2UgRVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm+9S75S0t +Mqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTWPNt0OKRKzE0lgvdKpVMS +OO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEMxChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3 +MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFBIk5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQ +NAQTXKFx01p8VdteZOE3hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUe +h10aUAsgEsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMB +Af8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaAFLE+w2kD+L9HAdSY +JhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3NecnzyIZgYIVyHbIUf4KmeqvxgydkAQ +V8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6zeM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFp +myPInngiK3BD41VHMWEZ71jFhS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkK +mNEVX58Svnw2Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe +vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep+OkuE6N36B9K +-----END CERTIFICATE----- + +SwissSign Gold CA - G2 +====================== +-----BEGIN CERTIFICATE----- +MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNVBAYTAkNIMRUw +EwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2lnbiBHb2xkIENBIC0gRzIwHhcN +MDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBFMQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dp +c3NTaWduIEFHMR8wHQYDVQQDExZTd2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUq +t2/876LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+bbqBHH5C +jCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c6bM8K8vzARO/Ws/BtQpg +vd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqEemA8atufK+ze3gE/bk3lUIbLtK/tREDF +ylqM2tIrfKjuvqblCqoOpd8FUrdVxyJdMmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvR +AiTysybUa9oEVeXBCsdtMDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuend +jIj3o02yMszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69yFGkO +peUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPiaG59je883WX0XaxR +7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxMgI93e2CaHt+28kgeDrpOVG2Y4OGi +GqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUWyV7lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64 +OfPAeGZe6Drn8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov +L3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe645R88a7A3hfm +5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczOUYrHUDFu4Up+GC9pWbY9ZIEr +44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOf +Mke6UiI0HTJ6CVanfCU2qT1L2sCCbwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6m +Gu6uLftIdxf+u+yvGPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxp +mo/a77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCChdiDyyJk +vC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid392qgQmwLOM7XdVAyksLf +KzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEppLd6leNcG2mqeSz53OiATIgHQv2ieY2Br +NU0LbbqhPcCT4H8js1WtciVORvnSFu+wZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6Lqj +viOvrv1vA+ACOzB2+httQc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ +-----END CERTIFICATE----- + +SecureTrust CA +============== +-----BEGIN CERTIFICATE----- +MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBIMQswCQYDVQQG +EwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xFzAVBgNVBAMTDlNlY3VyZVRy +dXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIzMTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAe +BgNVBAoTF1NlY3VyZVRydXN0IENvcnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQX +OZEzZum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO0gMdA+9t +DWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIaowW8xQmxSPmjL8xk037uH +GFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b +01k/unK8RCSc43Oz969XL0Imnal0ugBS8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmH +ursCAwEAAaOBnTCBmjATBgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCegJYYj +aHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQAwDQYJ +KoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt36Z3q059c4EVlew3KW+JwULKUBRSu +SceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHf +mbx8IVQr5Fiiu1cprp6poxkmD5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZ +nMUFdAvnZyPSCPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR +3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE= +-----END CERTIFICATE----- + +Secure Global CA +================ +-----BEGIN CERTIFICATE----- +MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBKMQswCQYDVQQG +EwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBH +bG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkxMjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEg +MB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwg +Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jx +YDiJiQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa/FHtaMbQ +bqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJjnIFHovdRIWCQtBJwB1g +8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnIHmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYV +HDGA76oYa8J719rO+TMg1fW9ajMtgQT7sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi +0XPnj3pDAgMBAAGjgZ0wgZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1Ud +EwEB/wQFMAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCswKaAn +oCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsGAQQBgjcVAQQDAgEA +MA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0LURYD7xh8yOOvaliTFGCRsoTciE6+ +OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXOH0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cn +CDpOGR86p1hcF895P4vkp9MmI50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/5 +3CYNv6ZHdAbYiNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc +f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW +-----END CERTIFICATE----- + +COMODO Certification Authority +============================== +-----BEGIN CERTIFICATE----- +MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCBgTELMAkGA1UE +BhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgG +A1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNVBAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1 +dGhvcml0eTAeFw0wNjEyMDEwMDAwMDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEb +MBkGA1UECBMSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFD +T01PRE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3UcEbVASY06m/weaKXTuH ++7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI2GqGd0S7WWaXUF601CxwRM/aN5VCaTww +xHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV +4EajcNxo2f8ESIl33rXp+2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA +1KGzqSX+DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5OnKVI +rLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW/zAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6gPKA6hjhodHRwOi8vY3JsLmNvbW9k +b2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOC +AQEAPpiem/Yb6dc5t3iuHXIYSdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CP +OGEIqB6BCsAvIC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/ +RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4zJVSk/BwJVmc +IGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5ddBA6+C4OmF4O5MBKgxTMVBbkN ++8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IBZQ== +-----END CERTIFICATE----- + +COMODO ECC Certification Authority +================================== +-----BEGIN CERTIFICATE----- +MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTELMAkGA1UEBhMC +R0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UE +ChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwHhcNMDgwMzA2MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0Ix +GzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR +Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRo +b3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSRFtSrYpn1PlILBs5BAH+X +4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0JcfRK9ChQtP6IHG4/bC8vCVlbpVsLM5ni +wz2J+Wos77LTBumjQjBAMB0GA1UdDgQWBBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VG +FAkK+qDmfQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdvGDeA +U/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= +-----END CERTIFICATE----- + +Certigna +======== +-----BEGIN CERTIFICATE----- +MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNVBAYTAkZSMRIw +EAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4XDTA3MDYyOTE1MTMwNVoXDTI3 +MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwI +Q2VydGlnbmEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7q +XOEm7RFHYeGifBZ4QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyH +GxnygQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbwzBfsV1/p +ogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q130yGLMLLGq/jj8UEYkg +DncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKf +Irjxwo1p3Po6WAbfAgMBAAGjgbwwgbkwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQ +tCRZvgHyUtVF9lo53BEwZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJ +BgNVBAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzjAQ/J +SP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG9w0BAQUFAAOCAQEA +hQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8hbV6lUmPOEvjvKtpv6zf+EwLHyzs+ +ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFncfca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1klu +PBS1xp81HlDQwY9qcEQCYsuuHWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY +1gkIl2PlwS6wt0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw +WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== +-----END CERTIFICATE----- + +ePKI Root Certification Authority +================================= +-----BEGIN CERTIFICATE----- +MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBeMQswCQYDVQQG +EwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0ZC4xKjAoBgNVBAsMIWVQS0kg +Um9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMx +MjdaMF4xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEq +MCgGA1UECwwhZVBLSSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAHSyZbCUNs +IZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAhijHyl3SJCRImHJ7K2RKi +lTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3XDZoTM1PRYfl61dd4s5oz9wCGzh1NlDiv +qOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX +12ruOzjjK9SXDrkb5wdJfzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0O +WQqraffAsgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uUWH1+ +ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLSnT0IFaUQAS2zMnao +lQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pHdmX2Os+PYhcZewoozRrSgx4hxyy/ +vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJipNiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXi +Zo1jDiVN1Rmy5nk3pyKdVDECAwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/Qkqi +MAwGA1UdEwQFMAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH +ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGBuvl2ICO1J2B0 +1GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6YlPwZpVnPDimZI+ymBV3QGypzq +KOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkPJXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdV +xrsStZf0X4OFunHB2WyBEXYKCrC/gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEP +NXubrjlpC2JgQCA2j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+r +GNm65ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUBo2M3IUxE +xJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS/jQ6fbjpKdx2qcgw+BRx +gMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2zGp1iro2C6pSe3VkQw63d4k3jMdXH7Ojy +sP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTEW9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmOD +BCEIZ43ygknQW/2xzQ+DhNQ+IIX3Sj0rnP0qCglN6oH4EZw= +-----END CERTIFICATE----- + +certSIGN ROOT CA +================ +-----BEGIN CERTIFICATE----- +MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYTAlJPMREwDwYD +VQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTAeFw0wNjA3MDQxNzIwMDRa +Fw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UE +CxMQY2VydFNJR04gUk9PVCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7I +JUqOtdu0KBuqV5Do0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHH +rfAQUySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5dRdY4zTW2 +ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQOA7+j0xbm0bqQfWwCHTD +0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwvJoIQ4uNllAoEwF73XVv4EOLQunpL+943 +AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8B +Af8EBAMCAcYwHQYDVR0OBBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IB +AQA+0hyJLjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecYMnQ8 +SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ44gx+FkagQnIl6Z0 +x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6IJd1hJyMctTEHBDa0GpC9oHRxUIlt +vBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNwi/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7Nz +TogVZ96edhBiIL5VaZVDADlN9u6wWk5JRFRYX0KD +-----END CERTIFICATE----- + +NetLock Arany (Class Gold) Főtanúsítvány +======================================== +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQGEwJIVTERMA8G +A1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3MDUGA1UECwwuVGFuw7pzw610 +dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNlcnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBB +cmFueSAoQ2xhc3MgR29sZCkgRsWRdGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgx +MjA2MTUwODIxWjCBpzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxO +ZXRMb2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlmaWNhdGlv +biBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNzIEdvbGQpIEbFkXRhbsO6 +c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxCRec75LbRTDofTjl5Bu +0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrTlF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw +/HpYzY6b7cNGbIRwXdrzAZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAk +H3B5r9s5VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRGILdw +fzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2BJtr+UBdADTHLpl1 +neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAGAQH/AgEEMA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2MU9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwW +qZw8UQCgwBEIBaeZ5m8BiFRhbvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTta +YtOUZcTh5m2C+C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC +bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2FuLjbvrW5Kfna +NwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2XjG4Kvte9nHfRCaexOYNkbQu +dZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= +-----END CERTIFICATE----- + +Microsec e-Szigno Root CA 2009 +============================== +-----BEGIN CERTIFICATE----- +MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYDVQQGEwJIVTER +MA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jv +c2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o +dTAeFw0wOTA2MTYxMTMwMThaFw0yOTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UE +BwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUt +U3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvPkd6mJviZpWNwrZuuyjNA +fW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tccbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG +0IMZfcChEhyVbUr02MelTTMuhTlAdX4UfIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKA +pxn1ntxVUwOXewdI/5n7N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm +1HxdrtbCxkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1+rUC +AwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTLD8bf +QkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAbBgNVHREE +FDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqGSIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0o +lZMEyL/azXm4Q5DwpL7v8u8hmLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfX +I/OMn74dseGkddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 +tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c2Pm2G2JwCz02 +yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5tHMN1Rq41Bab2XD0h7lbwyYIi +LXpUq3DDfSJlgnCW +-----END CERTIFICATE----- + +GlobalSign Root CA - R3 +======================= +-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4GA1UECxMXR2xv +YmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2Jh +bFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxT +aWduIFJvb3QgQ0EgLSBSMzETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2ln +bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWt +iHL8RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsTgHeMCOFJ +0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmmKPZpO/bLyCiR5Z2KYVc3 +rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zdQQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjl +OCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZXriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2 +xmmFghcCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE +FI/wS3+oLkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZURUm7 +lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMpjjM5RcOO5LlXbKr8 +EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK6fBdRoyV3XpYKBovHd7NADdBj+1E +bddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQXmcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18 +YIvDQVETI53O9zJrlAGomecsMx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7r +kpeDMdmztcpHWD9f +-----END CERTIFICATE----- + +Izenpe.com +========== +-----BEGIN CERTIFICATE----- +MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4MQswCQYDVQQG +EwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5jb20wHhcNMDcxMjEz +MTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMu +QS4xEzARBgNVBAMMCkl6ZW5wZS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ +03rKDx6sp4boFmVqscIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAK +ClaOxdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6HLmYRY2xU ++zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFXuaOKmMPsOzTFlUFpfnXC +PCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQDyCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxT +OTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbK +F7jJeodWLBoBHmy+E60QrLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK +0GqfvEyNBjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8Lhij+ +0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIBQFqNeb+Lz0vPqhbB +leStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+HMh3/1uaD7euBUbl8agW7EekFwID +AQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2luZm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+ +SVpFTlBFIFMuQS4gLSBDSUYgQTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBG +NjIgUzgxQzBBBgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx +MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0O +BBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUAA4ICAQB4pgwWSp9MiDrAyw6l +Fn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWblaQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbga +kEyrkgPH7UIBzg/YsfqikuFgba56awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8q +hT/AQKM6WfxZSzwoJNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Cs +g1lwLDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCTVyvehQP5 +aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGkLhObNA5me0mrZJfQRsN5 +nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJbUjWumDqtujWTI6cfSN01RpiyEGjkpTHC +ClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZo +Q0iy2+tzJOeRf1SktoA+naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1Z +WrOZyGlsQyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== +-----END CERTIFICATE----- + +Go Daddy Root Certificate Authority - G2 +======================================== +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMxEDAOBgNVBAgT +B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoTEUdvRGFkZHkuY29tLCBJbmMu +MTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 +MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 +b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8G +A1UEAxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKDE6bFIEMBO4Tx5oVJnyfq +9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD ++qK+ihVqf94Lw7YZFAXK6sOoBJQ7RnwyDfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutd +fMh8+7ArU6SSYmlRJQVhGkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMl +NAJWJwGRtDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEAAaNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFDqahQcQZyi27/a9 +BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmXWWcDYfF+OwYxdS2hII5PZYe096ac +vNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r +5N9ss4UXnT3ZJE95kTXWXwTrgIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYV +N8Gb5DKj7Tjo2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO +LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI4uJEvlz36hz1 +-----END CERTIFICATE----- + +Starfield Root Certificate Authority - G2 +========================================= +-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMxEDAOBgNVBAgT +B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9s +b2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVsZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0 +eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAw +DgYDVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQg +VGVjaG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZpY2F0ZSBB +dXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL3twQP89o/8ArFv +W59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMgnLRJdzIpVv257IzdIvpy3Cdhl+72WoTs +bhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNk +N3mSwOxGXn/hbVNMYq/NHwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7Nf +ZTD4p7dNdloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0HZbU +JtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0GCSqGSIb3DQEBCwUAA4IBAQARWfol +TwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjUsHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx +4mcujJUDJi5DnUox9g61DLu34jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUw +F5okxBDgBPfg8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K +pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1mMpYjn0q7pBZ +c2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 +-----END CERTIFICATE----- + +Starfield Services Root Certificate Authority - G2 +================================================== +-----BEGIN CERTIFICATE----- +MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMxEDAOBgNVBAgT +B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9s +b2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVsZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRl +IEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNV +BAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxT +dGFyZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2VydmljZXMg +Um9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20pOsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2 +h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm28xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4Pa +hHQUw2eeBGg6345AWh1KTs9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLP +LJGmpufehRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk6mFB +rMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAwDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+qAdcwKziIorhtSpzyEZGDMA0GCSqG +SIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMIbw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPP +E95Dz+I0swSdHynVv/heyNXBve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTy +xQGjhdByPq1zqwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd +iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn0q23KXB56jza +YyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCNsSi6 +-----END CERTIFICATE----- + +AffirmTrust Commercial +====================== +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UEBhMCVVMxFDAS +BgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBDb21tZXJjaWFsMB4XDTEw +MDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmly +bVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6Eqdb +DuKPHx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yrba0F8PrV +C8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPALMeIrJmqbTFeurCA+ukV6 +BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1yHp52UKqK39c/s4mT6NmgTWvRLpUHhww +MmWd5jyTXlBOeuM61G7MGvv50jeuJCqrVwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNV +HQ4EFgQUnZPGU4teyq8/nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwDQYJKoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYGXUPG +hi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNjvbz4YYCanrHOQnDi +qX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivtZ8SOyUOyXGsViQK8YvxO8rUzqrJv +0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9gN53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0kh +sUlHRUe072o0EclNmsxZt9YCnlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8= +-----END CERTIFICATE----- + +AffirmTrust Networking +====================== +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UEBhMCVVMxFDAS +BgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBOZXR3b3JraW5nMB4XDTEw +MDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmly +bVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SE +Hi3yYJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbuakCNrmreI +dIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRLQESxG9fhwoXA3hA/Pe24 +/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gb +h+0t+nvujArjqWaJGctB+d1ENmHP4ndGyH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNV +HQ4EFgQUBx/S55zawm6iQLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwDQYJKoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfOtDIu +UFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzuQY0x2+c06lkh1QF6 +12S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZLgo/bNjR9eUJtGxUAArgFU2HdW23 +WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4uolu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9 +/ZFvgrG+CJPbFEfxojfHRZ48x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s= +-----END CERTIFICATE----- + +AffirmTrust Premium +=================== +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UEBhMCVVMxFDAS +BgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVzdCBQcmVtaXVtMB4XDTEwMDEy +OTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRy +dXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEAxBLfqV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtn +BKAQJG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ+jjeRFcV +5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrSs8PhaJyJ+HoAVt70VZVs ++7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmd +GPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d770O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5R +p9EixAqnOEhss/n/fauGV+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NI +S+LI+H+SqHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S5u04 +6uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4IaC1nEWTJ3s7xgaVY5 +/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TXOwF0lkLgAOIua+rF7nKsu7/+6qqo ++Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYEFJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByv +MiPIs0laUZx2KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg +Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B8OWycvpEgjNC +6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQMKSOyARiqcTtNd56l+0OOF6S +L5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK ++4w1IX2COPKpVJEZNZOUbWo6xbLQu4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmV +BtWVyuEklut89pMFu+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFg +IxpHYoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8GKa1qF60 +g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaORtGdFNrHF+QFlozEJLUb +zxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6eKeC2uAloGRwYQw== +-----END CERTIFICATE----- + +AffirmTrust Premium ECC +======================= +-----BEGIN CERTIFICATE----- +MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMCVVMxFDASBgNV +BAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQcmVtaXVtIEVDQzAeFw0xMDAx +MjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1U +cnVzdDEgMB4GA1UEAwwXQWZmaXJtVHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAQNMF4bFZ0D0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQ +N8O9ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0GA1UdDgQW +BBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAK +BggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/VsaobgxCd05DhT1wV/GzTjxi+zygk8N53X +57hG8f2h4nECMEJZh0PUUd+60wkyWs6Iflc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKM +eQ== +-----END CERTIFICATE----- + +Certum Trusted Network CA +========================= +-----BEGIN CERTIFICATE----- +MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBMMSIwIAYDVQQK +ExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBUcnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIy +MTIwNzM3WhcNMjkxMjMxMTIwNzM3WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBU +ZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +MSIwIAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rHUV+rpDKmYYe2bg+G0jAC +l/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LMTXPb865Px1bVWqeWifrzq2jUI4ZZJ88J +J7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVUBBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4 +fOQtf/WsX+sWn7Et0brMkUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0 +cvW0QM8xAcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNVHRMB +Af8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNVHQ8BAf8EBAMCAQYw +DQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15ysHhE49wcrwn9I0j6vSrEuVUEtRCj +jSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfLI9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1 +mS1FhIrlQgnXdAIv94nYmem8J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5aj +Zt3hrvJBW8qYVoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI +03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= +-----END CERTIFICATE----- + +TWCA Root Certification Authority +================================= +-----BEGIN CERTIFICATE----- +MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJ +VEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMzWhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQG +EwJUVzESMBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NB +IFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFEAcK0HMMx +QhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HHK3XLfJ+utdGdIzdjp9xC +oi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeXRfwZVzsrb+RH9JlF/h3x+JejiB03HFyP +4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/zrX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1r +y+UPizgN7gr8/g+YnzAx3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIB +BjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkqhkiG +9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeCMErJk/9q56YAf4lC +mtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdlsXebQ79NqZp4VKIV66IIArB6nCWlW +QtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62Dlhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVY +T0bf+215WfKEIlKuD8z7fDvnaspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocny +Yh0igzyXxfkZYiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw== +-----END CERTIFICATE----- + +Security Communication RootCA2 +============================== +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDElMCMGA1UEChMc +U0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMeU2VjdXJpdHkgQ29tbXVuaWNh +dGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoXDTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMC +SlAxJTAjBgNVBAoTHFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3Vy +aXR5IENvbW11bmljYXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +ANAVOVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGrzbl+dp++ ++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVMVAX3NuRFg3sUZdbcDE3R +3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQhNBqyjoGADdH5H5XTz+L62e4iKrFvlNV +spHEfbmwhRkGeC7bYRr6hfVKkaHnFtWOojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1K +EOtOghY6rCcMU/Gt1SSwawNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8 +QIH4D5csOPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEB +CwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpFcoJxDjrSzG+ntKEj +u/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXcokgfGT+Ok+vx+hfuzU7jBBJV1uXk +3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6q +tnRGEmyR7jTV7JqR50S+kDFy1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29 +mvVXIwAHIRc/SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 +-----END CERTIFICATE----- + +Actalis Authentication Root CA +============================== +-----BEGIN CERTIFICATE----- +MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCSVQxDjAM +BgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1ODUyMDk2NzEnMCUGA1UE +AwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDky +MjExMjIwMlowazELMAkGA1UEBhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlz +IFMucC5BLi8wMzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 +IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNvUTufClrJ +wkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX4ay8IMKx4INRimlNAJZa +by/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9KK3giq0itFZljoZUj5NDKd45RnijMCO6 +zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1f +YVEiVRvjRuPjPdA1YprbrxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2 +oxgkg4YQ51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2Fbe8l +EfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxeKF+w6D9Fz8+vm2/7 +hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4Fv6MGn8i1zeQf1xcGDXqVdFUNaBr8 +EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbnfpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5 +jF66CyCU3nuDuP/jVo23Eek7jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLY +iDrIn3hm7YnzezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt +ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQALe3KHwGCmSUyI +WOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70jsNjLiNmsGe+b7bAEzlgqqI0 +JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDzWochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKx +K3JCaKygvU5a2hi/a5iB0P2avl4VSM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+ +Xlff1ANATIGk0k9jpwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC +4yyXX04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+OkfcvHlXHo +2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7RK4X9p2jIugErsWx0Hbhz +lefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btUZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXem +OR/qnuOf0GZvBeyqdn6/axag67XH/JJULysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9 +vwGYT7JZVEc+NHt4bVaTLnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== +-----END CERTIFICATE----- + +Buypass Class 2 Root CA +======================= +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU +QnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3MgQ2xhc3MgMiBSb290IENBMB4X +DTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1owTjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1 +eXBhc3MgQVMtOTgzMTYzMzI3MSAwHgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIw +DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1 +g1Lr6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPVL4O2fuPn +9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC911K2GScuVr1QGbNgGE41b +/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHxMlAQTn/0hpPshNOOvEu/XAFOBz3cFIqU +CqTqc/sLUegTBxj6DvEr0VQVfTzh97QZQmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeff +awrbD02TTqigzXsu8lkBarcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgI +zRFo1clrUs3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLiFRhn +Bkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRSP/TizPJhk9H9Z2vX +Uq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN9SG9dKpN6nIDSdvHXx1iY8f93ZHs +M+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxPAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD +VR0OBBYEFMmAd+BikoL1RpzzuvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsF +AAOCAgEAU18h9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s +A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3tOluwlN5E40EI +osHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo+fsicdl9sz1Gv7SEr5AcD48S +aq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYd +DnkM/crqJIByw5c/8nerQyIKx+u2DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWD +LfJ6v9r9jv6ly0UsH8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0 +oyLQI+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK75t98biGC +wWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h3PFaTWwyI0PurKju7koS +CTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPzY11aWOIv4x3kqdbQCtCev9eBCfHJxyYN +rJgWVqA= +-----END CERTIFICATE----- + +Buypass Class 3 Root CA +======================= +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU +QnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3MgQ2xhc3MgMyBSb290IENBMB4X +DTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFowTjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1 +eXBhc3MgQVMtOTgzMTYzMzI3MSAwHgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIw +DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRH +sJ8YZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3EN3coTRiR +5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9tznDDgFHmV0ST9tD+leh +7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX0DJq1l1sDPGzbjniazEuOQAnFN44wOwZ +ZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH +2xc519woe2v1n/MuwU8XKhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV +/afmiSTYzIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvSO1UQ +RwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D34xFMFbG02SrZvPA +Xpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgPK9Dx2hzLabjKSWJtyNBjYt1gD1iq +j6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD +VR0OBBYEFEe4zf/lb+74suwvTg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsF +AAOCAgEAACAjQTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV +cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXSIGrs/CIBKM+G +uIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2HJLw5QY33KbmkJs4j1xrG0aG +Q0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsaO5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8 +ZORK15FTAaggiG6cX0S5y2CBNOxv033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2 +KSb12tjE8nVhz36udmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz +6MkEkbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg413OEMXbug +UZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvDu79leNKGef9JOxqDDPDe +eOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq4/g7u9xN12TyUb7mqqta6THuBrxzvxNi +Cp/HuZc= +-----END CERTIFICATE----- + +T-TeleSec GlobalRoot Class 3 +============================ +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoM +IlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBU +cnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgx +MDAxMTAyOTU2WhcNMzMxMDAxMjM1OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lz +dGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBD +ZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN8ELg63iIVl6bmlQdTQyK +9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/RLyTPWGrTs0NvvAgJ1gORH8EGoel15YU +NpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZF +iP0Zf3WHHx+xGwpzJFu5ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W +0eDrXltMEnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1A/d2O2GCahKqGFPr +AyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOyWL6ukK2YJ5f+AbGwUgC4TeQbIXQb +fsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzT +ucpH9sry9uetuUg/vBa3wW306gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7h +P0HHRwA11fXT91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml +e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4pTpPDpFQUWw== +-----END CERTIFICATE----- + +D-TRUST Root Class 3 CA 2 2009 +============================== +-----BEGIN CERTIFICATE----- +MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQK +DAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTAe +Fw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NThaME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxE +LVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOAD +ER03UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42tSHKXzlA +BF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9RySPocq60vFYJfxLLHLGv +KZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsMlFqVlNpQmvH/pStmMaTJOKDfHR+4CS7z +p+hnUquVH+BGPtikw8paxTGA6Eian5Rp/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUC +AwEAAaOCARowggEWMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ +4PGEMA4GA1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVjdG9y +eS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUyMENBJTIwMiUyMDIw +MDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRlcmV2b2NhdGlvbmxpc3QwQ6BBoD+G +PWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAw +OS5jcmwwDQYJKoZIhvcNAQELBQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm +2H6NMLVwMeniacfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 +o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4KzCUqNQT4YJEV +dT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8PIWmawomDeCTmGCufsYkl4ph +X5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3YJohw1+qRzT65ysCQblrGXnRl11z+o+I= +-----END CERTIFICATE----- + +D-TRUST Root Class 3 CA 2 EV 2009 +================================= +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQK +DAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAw +OTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUwNDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQK +DAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAw +OTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfS +egpnljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM03TP1YtHh +zRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6ZqQTMFexgaDbtCHu39b+T +7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lRp75mpoo6Kr3HGrHhFPC+Oh25z1uxav60 +sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure35 +11H3a6UCAwEAAaOCASQwggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyv +cop9NteaHNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFwOi8v +ZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xhc3MlMjAzJTIwQ0El +MjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1ERT9jZXJ0aWZpY2F0ZXJldm9jYXRp +b25saXN0MEagRKBChkBodHRwOi8vd3d3LmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xh +c3NfM19jYV8yX2V2XzIwMDkuY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+ +PPoeUSbrh/Yp3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 +nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNFCSuGdXzfX2lX +ANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7naxpeG0ILD5EJt/rDiZE4OJudA +NCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqXKVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVv +w9y4AyHqnxbxLFS1 +-----END CERTIFICATE----- + +CA Disig Root R2 +================ +-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNVBAYTAlNLMRMw +EQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMuMRkwFwYDVQQDExBDQSBEaXNp +ZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQyMDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sx +EzARBgNVBAcTCkJyYXRpc2xhdmExEzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERp +c2lnIFJvb3QgUjIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbC +w3OeNcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNHPWSb6Wia +xswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3Ix2ymrdMxp7zo5eFm1tL7 +A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbeQTg06ov80egEFGEtQX6sx3dOy1FU+16S +GBsEWmjGycT6txOgmLcRK7fWV8x8nhfRyyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqV +g8NTEQxzHQuyRpDRQjrOQG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa +5Beny912H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJQfYE +koopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUDi/ZnWejBBhG93c+A +Ak9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORsnLMOPReisjQS1n6yqEm70XooQL6i +Fh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5u +Qu0wDQYJKoZIhvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM +tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqfGopTpti72TVV +sRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkblvdhuDvEK7Z4bLQjb/D907Je +dR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka+elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W8 +1k/BfDxujRNt+3vrMNDcTa/F1balTFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjx +mHHEt38OFdAlab0inSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01 +utI3gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18DrG5gPcFw0 +sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3OszMOl6W8KjptlwlCFtaOg +UxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8xL4ysEr3vQCj8KWefshNPZiTEUxnpHikV +7+ZtsH8tZ/3zbBt1RqPlShfppNcL +-----END CERTIFICATE----- + +ACCVRAIZ1 +========= +-----BEGIN CERTIFICATE----- +MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UEAwwJQUNDVlJB +SVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQswCQYDVQQGEwJFUzAeFw0xMTA1 +MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQBgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwH +UEtJQUNDVjENMAsGA1UECgwEQUNDVjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQCbqau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gM +jmoYHtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWoG2ioPej0 +RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpAlHPrzg5XPAOBOp0KoVdD +aaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhrIA8wKFSVf+DuzgpmndFALW4ir50awQUZ +0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDG +WuzndN9wrqODJerWx5eHk6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs7 +8yM2x/474KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMOm3WR +5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpacXpkatcnYGMN285J +9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPluUsXQA+xtrn13k/c4LOsOxFwYIRK +Q26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYIKwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRw +Oi8vd3d3LmFjY3YuZXMvZmlsZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEu +Y3J0MB8GCCsGAQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2 +VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeTVfZW6oHlNsyM +Hj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIGCCsGAQUFBwICMIIBFB6CARAA +QQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUAcgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBh +AO0AegAgAGQAZQAgAGwAYQAgAEEAQwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUA +YwBuAG8AbABvAGcA7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBj +AHQAcgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAAQwBQAFMA +IABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUAczAwBggrBgEFBQcCARYk +aHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2MuaHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0 +dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRtaW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2 +MV9kZXIuY3JsMA4GA1UdDwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZI +hvcNAQEFBQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdpD70E +R9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gUJyCpZET/LtZ1qmxN +YEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+mAM/EKXMRNt6GGT6d7hmKG9Ww7Y49 +nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepDvV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJ +TS+xJlsndQAJxGJ3KQhfnlmstn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3 +sCPdK6jT2iWH7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h +I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szAh1xA2syVP1Xg +Nce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xFd3+YJ5oyXSrjhO7FmGYvliAd +3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2HpPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3p +EfbRD0tVNEYqi4Y7 +-----END CERTIFICATE----- + +TWCA Global Root CA +=================== +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcxEjAQBgNVBAoT +CVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMTVFdDQSBHbG9iYWwgUm9vdCBD +QTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQK +EwlUQUlXQU4tQ0ExEDAOBgNVBAsTB1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3Qg +Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2C +nJfF10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz0ALfUPZV +r2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfChMBwqoJimFb3u/Rk28OKR +Q4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbHzIh1HrtsBv+baz4X7GGqcXzGHaL3SekV +tTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1W +KKD+u4ZqyPpcC1jcxkt2yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99 +sy2sbZCilaLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYPoA/p +yJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQABDzfuBSO6N+pjWxn +kjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcEqYSjMq+u7msXi7Kx/mzhkIyIqJdI +zshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMC +AQYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6g +cFGn90xHNcgL1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn +LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WFH6vPNOw/KP4M +8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNoRI2T9GRwoD2dKAXDOXC4Ynsg +/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlg +lPx4mI88k1HtQJAH32RjJMtOcQWh15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryP +A9gK8kxkRr05YuWW6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3m +i4TWnsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5jwa19hAM8 +EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWzaGHQRiapIVJpLesux+t3 +zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmyKwbQBM0= +-----END CERTIFICATE----- + +TeliaSonera Root CA v1 +====================== +-----BEGIN CERTIFICATE----- +MIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAwNzEUMBIGA1UE +CgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJvb3QgQ0EgdjEwHhcNMDcxMDE4 +MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYDVQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwW +VGVsaWFTb25lcmEgUm9vdCBDQSB2MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+ +6yfwIaPzaSZVfp3FVRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA +3GV17CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+XZ75Ljo1k +B1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+/jXh7VB7qTCNGdMJjmhn +Xb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxH +oLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkmdtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3 +F0fUTPHSiXk+TT2YqGHeOh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJ +oWjiUIMusDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4pgd7 +gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fsslESl1MpWtTwEhDc +TwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQarMCpgKIv7NHfirZ1fpoeDVNAgMB +AAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qW +DNXr+nuqF+gTEjANBgkqhkiG9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNm +zqjMDfz1mgbldxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx +0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1TjTQpgcmLNkQfW +pb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBedY2gea+zDTYa4EzAvXUYNR0PV +G6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7Q4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpc +c41teyWRyu5FrgZLAMzTsVlQ2jqIOylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOT +JsjrDNYmiLbAJM+7vVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2 +qReWt88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcnHL/EVlP6 +Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVxSK236thZiNSQvxaz2ems +WWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY= +-----END CERTIFICATE----- + +T-TeleSec GlobalRoot Class 2 +============================ +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoM +IlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBU +cnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgx +MDAxMTA0MDE0WhcNMzMxMDAxMjM1OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lz +dGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBD +ZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUdAqSzm1nzHoqvNK38DcLZ +SBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiCFoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/F +vudocP05l03Sx5iRUKrERLMjfTlH6VJi1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx970 +2cu+fjOlbpSD8DT6IavqjnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGV +WOHAD3bZwI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/WSA2AHmgoCJrjNXy +YdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhyNsZt+U2e+iKo4YFWz827n+qrkRk4 +r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPACuvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNf +vNoBYimipidx5joifsFvHZVwIEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR +3p1m0IvVVGb6g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN +9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlPBSeOE6Fuwg== +-----END CERTIFICATE----- + +Atos TrustedRoot 2011 +===================== +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UEAwwVQXRvcyBU +cnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0xMTA3MDcxNDU4 +MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMMFUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsG +A1UECgwEQXRvczELMAkGA1UEBhMCREUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCV +hTuXbyo7LjvPpvMpNb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr +54rMVD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+SZFhyBH+ +DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ4J7sVaE3IqKHBAUsR320 +HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0Lcp2AMBYHlT8oDv3FdU9T1nSatCQujgKR +z3bFmx5VdJx4IbHwLfELn8LVlhgf8FQieowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7R +l+lwrrw7GWzbITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZ +bNshMBgGA1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB +CwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8jvZfza1zv7v1Apt+h +k6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kPDpFrdRbhIfzYJsdHt6bPWHJxfrrh +TZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pcmaHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a9 +61qn8FYiqTxlVMYVqL2Gns2Dlmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G +3mB/ufNPRJLvKrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed +-----END CERTIFICATE----- + +QuoVadis Root CA 1 G3 +===================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQELBQAwSDELMAkG +A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv +b3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJN +MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEg +RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakE +PBtVwedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWerNrwU8lm +PNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF34168Xfuw6cwI2H44g4hWf6 +Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh4Pw5qlPafX7PGglTvF0FBM+hSo+LdoIN +ofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXpUhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/l +g6AnhF4EwfWQvTA9xO+oabw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV +7qJZjqlc3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/GKubX +9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSthfbZxbGL0eUQMk1f +iyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KOTk0k+17kBL5yG6YnLUlamXrXXAkg +t3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOtzCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZI +hvcNAQELBQADggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC +MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2cDMT/uFPpiN3 +GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUNqXsCHKnQO18LwIE6PWThv6ct +Tr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP ++V04ikkwj+3x6xn0dxoxGE1nVGwvb2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh +3jRJjehZrJ3ydlo28hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fa +wx/kNSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNjZgKAvQU6 +O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhpq1467HxpvMc7hU6eFbm0 +FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFtnh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOV +hMJKzRwuJIczYOXD +-----END CERTIFICATE----- + +QuoVadis Root CA 2 G3 +===================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQELBQAwSDELMAkG +A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv +b3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJN +MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIg +RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFh +ZiFfqq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMWn4rjyduY +NM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ymc5GQYaYDFCDy54ejiK2t +oIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+O7q414AB+6XrW7PFXmAqMaCvN+ggOp+o +MiwMzAkd056OXbxMmO7FGmh77FOm6RQ1o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+l +V0POKa2Mq1W/xPtbAd0jIaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZo +L1NesNKqIcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz8eQQ +sSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43ehvNURG3YBZwjgQQvD +6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l7ZizlWNof/k19N+IxWA1ksB8aRxh +lRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALGcC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZI +hvcNAQELBQADggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 +AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RCroijQ1h5fq7K +pVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0GaW/ZZGYjeVYg3UQt4XAoeo0L9 +x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4nlv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgz +dWqTHBLmYF5vHX/JHyPLhGGfHoJE+V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6X +U/IyAgkwo1jwDQHVcsaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+Nw +mNtddbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNgKCLjsZWD +zYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeMHVOyToV7BjjHLPj4sHKN +JeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4WSr2Rz0ZiC3oheGe7IUIarFsNMkd7Egr +O3jtZsSOeWmD3n+M +-----END CERTIFICATE----- + +QuoVadis Root CA 3 G3 +===================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQELBQAwSDELMAkG +A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv +b3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJN +MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMg +RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286 +IxSR/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNuFoM7pmRL +Mon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXRU7Ox7sWTaYI+FrUoRqHe +6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+cra1AdHkrAj80//ogaX3T7mH1urPnMNA3 +I4ZyYUUpSFlob3emLoG+B01vr87ERRORFHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3U +VDmrJqMz6nWB2i3ND0/kA9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f7 +5li59wzweyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634RylsSqi +Md5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBpVzgeAVuNVejH38DM +dyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0QA4XN8f+MFrXBsj6IbGB/kE+V9/Yt +rQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZI +hvcNAQELBQADggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px +KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnIFUBhynLWcKzS +t/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5WvvoxXqA/4Ti2Tk08HS6IT7SdEQ +TXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFgu/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9Du +DcpmvJRPpq3t/O5jrFc/ZSXPsoaP0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGib +Ih6BJpsQBJFxwAYf3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmD +hPbl8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+DhcI00iX +0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HNPlopNLk9hM6xZdRZkZFW +dSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ywaZWWDYWGWVjUTR939+J399roD1B0y2 +PpxxVJkES/1Y+Zj0 +-----END CERTIFICATE----- + +DigiCert Assured ID Root G2 +=========================== +-----BEGIN CERTIFICATE----- +MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQw +IgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgw +MTE1MTIwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQL +ExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSAn61UQbVH +35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4HteccbiJVMWWXvdMX0h5i89vq +bFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9HpEgjAALAcKxHad3A2m67OeYfcgnDmCXRw +VWmvo2ifv922ebPynXApVfSr/5Vh88lAbx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OP +YLfykqGxvYmJHzDNw6YuYjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+Rn +lTGNAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTO +w0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPIQW5pJ6d1Ee88hjZv +0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I0jJmwYrA8y8678Dj1JGG0VDjA9tz +d29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4GnilmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAW +hsI6yLETcDbYz+70CjTVW0z9B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0M +jomZmWzwPDCvON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo +IhNzbM8m9Yop5w== +-----END CERTIFICATE----- + +DigiCert Assured ID Root G3 +=========================== +-----BEGIN CERTIFICATE----- +MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYD +VQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1 +MTIwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQ +BgcqhkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJfZn4f5dwb +RXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17QRSAPWXYQ1qAk8C3eNvJs +KTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgF +UaFNN6KDec6NHSrkhDAKBggqhkjOPQQDAwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5Fy +YZ5eEJJZVrmDxxDnOOlYJjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy +1vUhZscv6pZjamVFkpUBtA== +-----END CERTIFICATE----- + +DigiCert Global Root G2 +======================= +-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBhMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAw +HgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUx +MjAwMDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 +dy5kaWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI2/Ou8jqJ +kTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx1x7e/dfgy5SDN67sH0NO +3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQq2EGnI/yuum06ZIya7XzV+hdG82MHauV +BJVJ8zUtluNJbd134/tJS7SsVQepj5WztCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyM +UNGPHgm+F6HmIcr9g+UQvIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQAB +o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV5uNu +5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY1Yl9PMWLSn/pvtsr +F9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4NeF22d+mQrvHRAiGfzZ0JFrabA0U +WTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NGFdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBH +QRFXGU7Aj64GxJUTFy8bJZ918rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/ +iyK5S9kJRaTepLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl +MrY= +-----END CERTIFICATE----- + +DigiCert Global Root G3 +======================= +-----BEGIN CERTIFICATE----- +MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAwHgYD +VQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAw +MDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5k +aWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0C +AQYFK4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FGfp4tn+6O +YwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPOZ9wj/wMco+I+o0IwQDAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNp +Yim8S8YwCgYIKoZIzj0EAwMDaAAwZQIxAK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y +3maTD/HMsQmP3Wyr+mt/oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34 +VOKa5Vt8sycX +-----END CERTIFICATE----- + +DigiCert Trusted Root G4 +======================== +-----BEGIN CERTIFICATE----- +MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBiMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSEw +HwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1 +MTIwMDAwWjBiMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0G +CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3yithZwuEp +pz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1Ifxp4VpX6+n6lXFllVcq9o +k3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDVySAdYyktzuxeTsiT+CFhmzTrBcZe7Fsa +vOvJz82sNEBfsXpm7nfISKhmV1efVFiODCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGY +QJB5w3jHtrHEtWoYOAMQjdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6 +MUSaM0C/CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCiEhtm +mnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADMfRyVw4/3IbKyEbe7 +f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QYuKZ3AeEPlAwhHbJUKSWJbOUOUlFH +dL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXKchYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8 +oR7FwI+isX4KJpn15GkvmB0t9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud +DwEB/wQEAwIBhjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD +ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2SV1EY+CtnJYY +ZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd+SeuMIW59mdNOj6PWTkiU0Tr +yF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWcfFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy +7zBZLq7gcfJW5GqXb5JQbZaNaHqasjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iah +ixTXTBmyUEFxPT9NcCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN +5r5N0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie4u1Ki7wb +/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mIr/OSmbaz5mEP0oUA51Aa +5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tK +G48BtieVU+i2iW1bvGjUI+iLUaJW+fCmgKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP +82Z+ +-----END CERTIFICATE----- + +COMODO RSA Certification Authority +================================== +-----BEGIN CERTIFICATE----- +MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCBhTELMAkGA1UE +BhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgG +A1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwHhcNMTAwMTE5MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMC +R0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UE +ChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR6FSS0gpWsawNJN3Fz0Rn +dJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8Xpz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZ +FGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+ +5eNu/Nio5JIk2kNrYrhV/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pG +x8cgoLEfZd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z+pUX +2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7wqP/0uK3pN/u6uPQL +OvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZahSL0896+1DSJMwBGB7FY79tOi4lu3 +sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVICu9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+C +GCe01a60y1Dma/RMhnEw6abfFobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5 +WdYgGq/yapiqcrxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E +FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8w +DQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvlwFTPoCWOAvn9sKIN9SCYPBMt +rFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+ +nq6PK7o9mfjYcwlYRm6mnPTXJ9OV2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSg +tZx8jb8uk2IntznaFxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwW +sRqZCuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiKboHGhfKp +pC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmckejkk9u+UJueBPSZI9FoJA +zMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yLS0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHq +ZJx64SIDqZxubw5lT2yHh17zbqD5daWbQOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk52 +7RH89elWsn2/x20Kk4yl0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7I +LaZRfyHBNVOFBkpdn627G190 +-----END CERTIFICATE----- + +USERTrust RSA Certification Authority +===================================== +-----BEGIN CERTIFICATE----- +MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCBiDELMAkGA1UE +BhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQK +ExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwHhcNMTAwMjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UE +BhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQK +ExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCAEmUXNg7D2wiz +0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2j +Y0K2dvKpOyuR+OJv0OwWIJAJPuLodMkYtJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFn +RghRy4YUVD+8M/5+bJz/Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O ++T23LLb2VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT79uq +/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6c0Plfg6lZrEpfDKE +Y1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmTYo61Zs8liM2EuLE/pDkP2QKe6xJM +lXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97lc6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8 +yexDJtC/QV9AqURE9JnnV4eeUB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+ +eLf8ZxXhyVeEHg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd +BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF +MAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPFUp/L+M+ZBn8b2kMVn54CVVeW +FPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KOVWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ +7l8wXEskEVX/JJpuXior7gtNn3/3ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQ +Eg9zKC7F4iRO/Fjs8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM +8WcRiQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYzeSf7dNXGi +FSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZXHlKYC6SQK5MNyosycdi +yA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9c +J2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRBVXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGw +sAvgnEzDHNb842m1R0aBL6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gx +Q+6IHdfGjjxDah2nGN59PRbxYvnKkKj9 +-----END CERTIFICATE----- + +USERTrust ECC Certification Authority +===================================== +-----BEGIN CERTIFICATE----- +MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDELMAkGA1UEBhMC +VVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwHhcNMTAwMjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMC +VVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqfloI+d61SRvU8Za2EurxtW2 +0eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinngo4N+LZfQYcTxmdwlkWOrfzCjtHDix6Ez +nPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0GA1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNV +HQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBB +HU6+4WMBzzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbWRNZu +9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= +-----END CERTIFICATE----- + +GlobalSign ECC Root CA - R5 +=========================== +-----BEGIN CERTIFICATE----- +MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEkMCIGA1UECxMb +R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD +EwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoXDTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMb +R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD +EwpHbG9iYWxTaWduMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6 +SFkc8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8kehOvRnkmS +h5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAd +BgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYIKoZIzj0EAwMDaAAwZQIxAOVpEslu28Yx +uglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7 +yFz9SO8NdCKoCOJuxUnOxwy8p2Fp8fc74SrL+SvzZpA3 +-----END CERTIFICATE----- + +IdenTrust Commercial Root CA 1 +============================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBKMQswCQYDVQQG +EwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBS +b290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQwMTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzES +MBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENB +IDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ld +hNlT3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU+ehcCuz/ +mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gpS0l4PJNgiCL8mdo2yMKi +1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1bVoE/c40yiTcdCMbXTMTEl3EASX2MN0C +XZ/g1Ue9tOsbobtJSdifWwLziuQkkORiT0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl +3ZBWzvurpWCdxJ35UrCLvYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzy +NeVJSQjKVsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZKdHzV +WYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHTc+XvvqDtMwt0viAg +xGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hvl7yTmvmcEpB4eoCHFddydJxVdHix +uuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5NiGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMC +AQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZI +hvcNAQELBQADggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH +6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwtLRvM7Kqas6pg +ghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93nAbowacYXVKV7cndJZ5t+qnt +ozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3+wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmV +YjzlVYA211QC//G5Xc7UI2/YRYRKW2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUX +feu+h1sXIFRRk0pTAwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/ro +kTLql1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG4iZZRHUe +2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZmUlO+KWA2yUPHGNiiskz +Z2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7R +cGzM7vRX+Bi6hG6H +-----END CERTIFICATE----- + +IdenTrust Public Sector Root CA 1 +================================= +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBNMQswCQYDVQQG +EwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3Rv +ciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcNMzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJV +UzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBS +b290IENBIDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTy +P4o7ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGyRBb06tD6 +Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlSbdsHyo+1W/CD80/HLaXI +rcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF/YTLNiCBWS2ab21ISGHKTN9T0a9SvESf +qy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoS +mJxZZoY+rfGwyj4GD3vwEUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFn +ol57plzy9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9VGxyh +LrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ2fjXctscvG29ZV/v +iDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsVWaFHVCkugyhfHMKiq3IXAAaOReyL +4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gDW/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8B +Af8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMw +DQYJKoZIhvcNAQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj +t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHVDRDtfULAj+7A +mgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9TaDKQGXSc3z1i9kKlT/YPyNt +GtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8GlwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFt +m6/n6J91eEyrRjuazr8FGF1NFTwWmhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMx +NRF4eKLg6TCMf4DfWN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4 +Mhn5+bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJtshquDDI +ajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhAGaQdp/lLQzfcaFpPz+vC +ZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ +3Wl9af0AVqW3rLatt8o+Ae+c +-----END CERTIFICATE----- + +Entrust Root Certification Authority - G2 +========================================= +-----BEGIN CERTIFICATE----- +MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMCVVMxFjAUBgNV +BAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVy +bXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ug +b25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIw +HhcNMDkwNzA3MTcyNTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoT +DUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMx +OTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25s +eTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP +/vaCeb9zYQYKpSfYs1/TRU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXz +HHfV1IWNcCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hWwcKU +s/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1U1+cPvQXLOZprE4y +TGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0jaWvYkxN4FisZDQSA/i2jZRjJKRx +AgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ6 +0B7vfec7aVHUbI2fkBJmqzANBgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5Z +iXMRrEPR9RP/jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ +Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v1fN2D807iDgi +nWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4RnAuknZoh8/CbCzB428Hch0P+ +vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmHVHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xO +e4pIb4tF9g== +-----END CERTIFICATE----- + +Entrust Root Certification Authority - EC1 +========================================== +-----BEGIN CERTIFICATE----- +MIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkGA1UEBhMCVVMx +FjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVn +YWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXpl +ZCB1c2Ugb25seTEzMDEGA1UEAxMqRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +IC0gRUMxMB4XDTEyMTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8xCzAJBgNVBAYTAlVTMRYw +FAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0L2xlZ2Fs +LXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhvcml6ZWQg +dXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAt +IEVDMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABIQTydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHy +AsWfoPZb1YsGGYZPUxBtByQnoaD41UcZYUx9ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef +9eNi1KlHBz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE +FLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVCR98crlOZF7ZvHH3h +vxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nXhTcGtXsI/esni0qU+eH6p44mCOh8 +kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G +-----END CERTIFICATE----- + +CFCA EV ROOT +============ +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJDTjEwMC4GA1UE +CgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNB +IEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkxMjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEw +MC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQD +DAxDRkNBIEVWIFJPT1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnV +BU03sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpLTIpTUnrD +7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5/ZOkVIBMUtRSqy5J35DN +uF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp7hZZLDRJGqgG16iI0gNyejLi6mhNbiyW +ZXvKWfry4t3uMCz7zEasxGPrb382KzRzEpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7 +xzbh72fROdOXW3NiGUgthxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9f +py25IGvPa931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqotaK8K +gWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNgTnYGmE69g60dWIol +hdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfVPKPtl8MeNPo4+QgO48BdK4PRVmrJ +tqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hvcWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAf +BgNVHSMEGDAWgBTj/i39KNALtbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB +/wQEAwIBBjAdBgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB +ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObTej/tUxPQ4i9q +ecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdLjOztUmCypAbqTuv0axn96/Ua +4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBSESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sG +E5uPhnEFtC+NiWYzKXZUmhH4J/qyP5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfX +BDrDMlI1Dlb4pd19xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjn +aH9dCi77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN5mydLIhy +PDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe/v5WOaHIz16eGWRGENoX +kbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+ZAAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3C +ekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su +-----END CERTIFICATE----- + +OISTE WISeKey Global Root GB CA +=============================== +-----BEGIN CERTIFICATE----- +MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBtMQswCQYDVQQG +EwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNl +ZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAw +MzJaFw0zOTEyMDExNTEwMzFaMG0xCzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYD +VQQLExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEds +b2JhbCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3HEokKtaX +scriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGxWuR51jIjK+FTzJlFXHtP +rby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk +9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNku7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4o +Qnc/nSMbsrY9gBQHTC5P99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvg +GUpuuy9rM2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZI +hvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrghcViXfa43FK8+5/ea4n32cZiZBKpD +dHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0 +VQreUGdNZtGn//3ZwLWoo4rOZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEui +HZeeevJuQHHfaPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic +Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM= +-----END CERTIFICATE----- + +SZAFIR ROOT CA2 +=============== +-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQELBQAwUTELMAkG +A1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6ZW5pb3dhIFMuQS4xGDAWBgNV +BAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkwNzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJ +BgNVBAYTAlBMMSgwJgYDVQQKDB9LcmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYD +VQQDDA9TWkFGSVIgUk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5Q +qEvNQLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT3PSQ1hNK +DJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw3gAeqDRHu5rr/gsUvTaE +2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr63fE9biCloBK0TXC5ztdyO4mTp4CEHCdJ +ckm1/zuVnsHMyAHs6A6KCpbns6aH5db5BSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwi +ieDhZNRnvDF5YTy7ykHNXGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0P +AQH/BAQDAgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsFAAOC +AQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw8PRBEew/R40/cof5 +O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOGnXkZ7/e7DDWQw4rtTw/1zBLZpD67 +oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCPoky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul +4+vJhaAlIDf7js4MNIThPIGyd05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6 ++/NNIxuZMzSgLvWpCz/UXeHPhJ/iGcJfitYgHuNztw== +-----END CERTIFICATE----- + +Certum Trusted Network CA 2 +=========================== +-----BEGIN CERTIFICATE----- +MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCBgDELMAkGA1UE +BhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMuQS4xJzAlBgNVBAsTHkNlcnR1 +bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIGA1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29y +ayBDQSAyMCIYDzIwMTExMDA2MDgzOTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQ +TDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENl +cnRpZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENB +IDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWADGSdhhuWZGc/IjoedQF9 +7/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+o +CgCXhVqqndwpyeI1B+twTUrWwbNWuKFBOJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40b +Rr5HMNUuctHFY9rnY3lEfktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2p +uTRZCr+ESv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1mo130 +GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02isx7QBlrd9pPPV3WZ +9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOWOZV7bIBaTxNyxtd9KXpEulKkKtVB +Rgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgezTv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pye +hizKV/Ma5ciSixqClnrDvFASadgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vM +BhBgu4M1t15n3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZI +hvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQF/xlhMcQSZDe28cmk4gmb3DW +Al45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTfCVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuA +L55MYIR4PSFk1vtBHxgP58l1cb29XN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMo +clm2q8KMZiYcdywmdjWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tM +pkT/WjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jbAoJnwTnb +w3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksqP/ujmv5zMnHCnsZy4Ypo +J/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Kob7a6bINDd82Kkhehnlt4Fj1F4jNy3eFm +ypnTycUm/Q1oBEauttmbjL4ZvrHG8hnjXALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLX +is7VmFxWlgPF7ncGNf/P5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7 +zAYspsbiDrW5viSP +-----END CERTIFICATE----- + +Hellenic Academic and Research Institutions RootCA 2015 +======================================================= +-----BEGIN CERTIFICATE----- +MIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1IxDzANBgNVBAcT +BkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0 +aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNl +YXJjaCBJbnN0aXR1dGlvbnMgUm9vdENBIDIwMTUwHhcNMTUwNzA3MTAxMTIxWhcNNDAwNjMwMTAx +MTIxWjCBpjELMAkGA1UEBhMCR1IxDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMg +QWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNV +BAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgUm9vdENBIDIw +MTUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDC+Kk/G4n8PDwEXT2QNrCROnk8Zlrv +bTkBSRq0t89/TSNTt5AA4xMqKKYx8ZEA4yjsriFBzh/a/X0SWwGDD7mwX5nh8hKDgE0GPt+sr+eh +iGsxr/CL0BgzuNtFajT0AoAkKAoCFZVedioNmToUW/bLy1O8E00BiDeUJRtCvCLYjqOWXjrZMts+ +6PAQZe104S+nfK8nNLspfZu2zwnI5dMK/IhlZXQK3HMcXM1AsRzUtoSMTFDPaI6oWa7CJ06CojXd +FPQf/7J31Ycvqm59JCfnxssm5uX+Zwdj2EUN3TpZZTlYepKZcj2chF6IIbjV9Cz82XBST3i4vTwr +i5WY9bPRaM8gFH5MXF/ni+X1NYEZN9cRCLdmvtNKzoNXADrDgfgXy5I2XdGj2HUb4Ysn6npIQf1F +GQatJ5lOwXBH3bWfgVMS5bGMSF0xQxfjjMZ6Y5ZLKTBOhE5iGV48zpeQpX8B653g+IuJ3SWYPZK2 +fu/Z8VFRfS0myGlZYeCsargqNhEEelC9MoS+L9xy1dcdFkfkR2YgP/SWxa+OAXqlD3pk9Q0Yh9mu +iNX6hME6wGkoLfINaFGq46V3xqSQDqE3izEjR8EJCOtu93ib14L8hCCZSRm2Ekax+0VVFqmjZayc +Bw/qa9wfLgZy7IaIEuQt218FL+TwA9MmM+eAws1CoRc0CwIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUcRVnyMjJvXVdctA4GGqd83EkVAswDQYJKoZI +hvcNAQELBQADggIBAHW7bVRLqhBYRjTyYtcWNl0IXtVsyIe9tC5G8jH4fOpCtZMWVdyhDBKg2mF+ +D1hYc2Ryx+hFjtyp8iY/xnmMsVMIM4GwVhO+5lFc2JsKT0ucVlMC6U/2DWDqTUJV6HwbISHTGzrM +d/K4kPFox/la/vot9L/J9UUbzjgQKjeKeaO04wlshYaT/4mWJ3iBj2fjRnRUjtkNaeJK9E10A/+y +d+2VZ5fkscWrv2oj6NSU4kQoYsRL4vDY4ilrGnB+JGGTe08DMiUNRSQrlrRGar9KC/eaj8GsGsVn +82800vpzY4zvFrCopEYq+OsS7HK07/grfoxSwIuEVPkvPuNVqNxmsdnhX9izjFk0WaSrT2y7Hxjb +davYy5LNlDhhDgcGH0tGEPEVvo2FXDtKK4F5D7Rpn0lQl033DlZdwJVqwjbDG2jJ9SrcR5q+ss7F +Jej6A7na+RZukYT1HCjI/CbM1xyQVqdfbzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVt +J94Cj8rDtSvK6evIIVM4pcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGa +JI7ZjnHKe7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0vm9q +p/UsQu0yrbYhnr68 +-----END CERTIFICATE----- + +Hellenic Academic and Research Institutions ECC RootCA 2015 +=========================================================== +-----BEGIN CERTIFICATE----- +MIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzANBgNVBAcTBkF0 +aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9u +cyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJj +aCBJbnN0aXR1dGlvbnMgRUNDIFJvb3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEw +MzcxMlowgaoxCzAJBgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmlj +IEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5MUQwQgYD +VQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIEVDQyBSb290 +Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKgQehLgoRc4vgxEZmGZE4JJS+dQS8KrjVP +dJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJajq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoK +Vlp8aQuqgAkkbH7BRqNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0O +BBYEFLQiC4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaeplSTA +GiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7SofTUwJCA3sS61kFyjn +dc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR +-----END CERTIFICATE----- + +ISRG Root X1 +============ +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAwTzELMAkGA1UE +BhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2VhcmNoIEdyb3VwMRUwEwYDVQQD +EwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQG +EwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMT +DElTUkcgUm9vdCBYMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54r +Vygch77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+0TM8ukj1 +3Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6UA5/TR5d8mUgjU+g4rk8K +b4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sWT8KOEUt+zwvo/7V3LvSye0rgTBIlDHCN +Aymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyHB5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ +4Q7e2RCOFvu396j3x+UCB5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf +1b0SHzUvKBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWnOlFu +hjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTnjh8BCNAw1FtxNrQH +usEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbwqHyGO0aoSCqI3Haadr8faqU9GY/r +OPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CIrU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4G +A1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY +9umbbjANBgkqhkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ3BebYhtF8GaV +0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KKNFtY2PwByVS5uCbMiogziUwt +hDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJw +TdwJx4nLCgdNbOhdjsnvzqvHu7UrTkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nx +e5AW0wdeRlN8NwdCjNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZA +JzVcoyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq4RgqsahD +YVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPAmRGunUHBcnWEvgJBQl9n +JEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57demyPxgcYxn/eR44/KJ4EBs+lVDR3veyJ +m+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- + +AC RAIZ FNMT-RCM +================ +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsxCzAJBgNVBAYT +AkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBGTk1ULVJDTTAeFw0wODEw +MjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJD +TTEZMBcGA1UECwwQQUMgUkFJWiBGTk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC +ggIBALpxgHpMhm5/yBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcf +qQgfBBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAzWHFctPVr +btQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxFtBDXaEAUwED653cXeuYL +j2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z374jNUUeAlz+taibmSXaXvMiwzn15Cou +08YfxGyqxRxqAQVKL9LFwag0Jl1mpdICIfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mw +WsXmo8RZZUc1g16p6DULmbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnT +tOmlcYF7wk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peSMKGJ +47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2ZSysV4999AeU14EC +ll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMetUqIJ5G+GR4of6ygnXYMgrwTJbFaa +i0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FPd9xf3E6Jobd2Sn9R2gzL+HYJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1o +dHRwOi8vd3d3LmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD +nFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1RXxlDPiyN8+s +D8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYMLVN0V2Ue1bLdI4E7pWYjJ2cJ +j+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrT +Qfv6MooqtyuGC2mDOL7Nii4LcK2NJpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW ++YJF1DngoABd15jmfZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7 +Ixjp6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp1txyM/1d +8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B9kiABdcPUXmsEKvU7ANm +5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wokRqEIr9baRRmW1FMdW4R58MD3R++Lj8UG +rp1MYp3/RgT408m2ECVAdf4WqslKYIYvuu8wd+RU4riEmViAqhOLUTpPSPaLtrM= +-----END CERTIFICATE----- + +Amazon Root CA 1 +================ +-----BEGIN CERTIFICATE----- +MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsFADA5MQswCQYD +VQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSAxMB4XDTE1 +MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpv +bjEZMBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBALJ4gHHKeNXjca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgH +FzZM9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qwIFAGbHrQ +gLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6VOujw5H5SNz/0egwLX0t +dHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L93FcXmn/6pUCyziKrlA4b9v7LWIbxcce +VOF34GfID5yHI9Y/QCB/IIDEgEw+OyQmjgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3 +DQEBCwUAA4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDIU5PM +CCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUsN+gDS63pYaACbvXy +8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vvo/ufQJVtMVT8QtPHRh8jrdkPSHCa +2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2 +xJNDd2ZhwLnoQdeXeGADbkpyrqXRfboQnoZsG4q5WTP468SQvvG5 +-----END CERTIFICATE----- + +Amazon Root CA 2 +================ +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwFADA5MQswCQYD +VQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSAyMB4XDTE1 +MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpv +bjEZMBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC +ggIBAK2Wny2cSkxKgXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4 +kHbZW0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg1dKmSYXp +N+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K8nu+NQWpEjTj82R0Yiw9 +AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvd +fLC6HM783k81ds8P+HgfajZRRidhW+mez/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAEx +kv8LV/SasrlX6avvDXbR8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSS +btqDT6ZjmUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz7Mt0 +Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6+XUyo05f7O0oYtlN +c/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI0u1ufm8/0i2BWSlmy5A5lREedCf+ +3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSw +DPBMMPQFWAJI/TPlUq9LhONmUjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oA +A7CXDpO8Wqj2LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY ++gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kSk5Nrp+gvU5LE +YFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl7uxMMne0nxrpS10gxdr9HIcW +xkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygmbtmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQ +gj9sAq+uEjonljYE1x2igGOpm/HlurR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbW +aQbLU8uz/mtBzUF+fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoV +Yh63n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE76KlXIx3 +KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H9jVlpNMKVv/1F2Rs76gi +JUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT4PsJYGw= +-----END CERTIFICATE----- + +Amazon Root CA 3 +================ +-----BEGIN CERTIFICATE----- +MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5MQswCQYDVQQG +EwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSAzMB4XDTE1MDUy +NjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZ +MBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZB +f8ANm+gBG1bG8lKlui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjr +Zt6jQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSrttvXBp43 +rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkrBqWTrBqYaGFy+uGh0Psc +eGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteMYyRIHN8wfdVoOw== +-----END CERTIFICATE----- + +Amazon Root CA 4 +================ +-----BEGIN CERTIFICATE----- +MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5MQswCQYDVQQG +EwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSA0MB4XDTE1MDUy +NjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZ +MBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN +/sGKe0uoe0ZLY7Bi9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri +83BkM6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV +HQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WBMAoGCCqGSM49BAMDA2gA +MGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlwCkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1 +AE47xDqUEpHJWEadIRNyp4iciuRMStuW1KyLa2tJElMzrdfkviT8tQp21KW8EA== +-----END CERTIFICATE----- + +TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 +============================================= +-----BEGIN CERTIFICATE----- +MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIxGDAWBgNVBAcT +D0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxpbXNlbCB2ZSBUZWtub2xvamlr +IEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0wKwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24g +TWVya2V6aSAtIEthbXUgU00xNjA0BgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRp +ZmlrYXNpIC0gU3VydW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYD +VQQGEwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXllIEJpbGlt +c2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklUQUsxLTArBgNVBAsTJEth +bXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBTTTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11 +IFNNIFNTTCBLb2sgU2VydGlmaWthc2kgLSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAr3UwM6q7a9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y8 +6Ij5iySrLqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INrN3wc +wv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2XYacQuFWQfw4tJzh0 +3+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/iSIzL+aFCr2lqBs23tPcLG07xxO9 +WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4fAJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQU +ZT/HiobGPN08VFw1+DrtUgxHV8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJ +KoZIhvcNAQELBQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh +AHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPfIPP54+M638yc +lNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4lzwDGrpDxpa5RXI4s6ehlj2R +e37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0j +q5Rm+K37DwhuJi1/FwcJsoz7UMCflo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM= +-----END CERTIFICATE----- + +GDCA TrustAUTH R5 ROOT +====================== +-----BEGIN CERTIFICATE----- +MIIFiDCCA3CgAwIBAgIIfQmX/vBH6nowDQYJKoZIhvcNAQELBQAwYjELMAkGA1UEBhMCQ04xMjAw +BgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZIENPLixMVEQuMR8wHQYDVQQD +DBZHRENBIFRydXN0QVVUSCBSNSBST09UMB4XDTE0MTEyNjA1MTMxNVoXDTQwMTIzMTE1NTk1OVow +YjELMAkGA1UEBhMCQ04xMjAwBgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZ +IENPLixMVEQuMR8wHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEA2aMW8Mh0dHeb7zMNOwZ+Vfy1YI92hhJCfVZmPoiC7XJjDp6L3TQs +AlFRwxn9WVSEyfFrs0yw6ehGXTjGoqcuEVe6ghWinI9tsJlKCvLriXBjTnnEt1u9ol2x8kECK62p +OqPseQrsXzrj/e+APK00mxqriCZ7VqKChh/rNYmDf1+uKU49tm7srsHwJ5uu4/Ts765/94Y9cnrr +pftZTqfrlYwiOXnhLQiPzLyRuEH3FMEjqcOtmkVEs7LXLM3GKeJQEK5cy4KOFxg2fZfmiJqwTTQJ +9Cy5WmYqsBebnh52nUpmMUHfP/vFBu8btn4aRjb3ZGM74zkYI+dndRTVdVeSN72+ahsmUPI2JgaQ +xXABZG12ZuGR224HwGGALrIuL4xwp9E7PLOR5G62xDtw8mySlwnNR30YwPO7ng/Wi64HtloPzgsM +R6flPri9fcebNaBhlzpBdRfMK5Z3KpIhHtmVdiBnaM8Nvd/WHwlqmuLMc3GkL30SgLdTMEZeS1SZ +D2fJpcjyIMGC7J0R38IC+xo70e0gmu9lZJIQDSri3nDxGGeCjGHeuLzRL5z7D9Ar7Rt2ueQ5Vfj4 +oR24qoAATILnsn8JuLwwoC8N9VKejveSswoAHQBUlwbgsQfZxw9cZX08bVlX5O2ljelAU58VS6Bx +9hoh49pwBiFYFIeFd3mqgnkCAwEAAaNCMEAwHQYDVR0OBBYEFOLJQJ9NzuiaoXzPDj9lxSmIahlR +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQDRSVfg +p8xoWLoBDysZzY2wYUWsEe1jUGn4H3++Fo/9nesLqjJHdtJnJO29fDMylyrHBYZmDRd9FBUb1Ov9 +H5r2XpdptxolpAqzkT9fNqyL7FeoPueBihhXOYV0GkLH6VsTX4/5COmSdI31R9KrO9b7eGZONn35 +6ZLpBN79SWP8bfsUcZNnL0dKt7n/HipzcEYwv1ryL3ml4Y0M2fmyYzeMN2WFcGpcWwlyua1jPLHd ++PwyvzeG5LuOmCd+uh8W4XAR8gPfJWIyJyYYMoSf/wA6E7qaTfRPuBRwIrHKK5DOKcFw9C+df/KQ +HtZa37dG/OaG+svgIHZ6uqbL9XzeYqWxi+7egmaKTjowHz+Ay60nugxe19CxVsp3cbK1daFQqUBD +F8Io2c9Si1vIY9RCPqAzekYu9wogRlR+ak8x8YF+QnQ4ZXMn7sZ8uI7XpTrXmKGcjBBV09tL7ECQ +8s1uV9JiDnxXk7Gnbc2dg7sq5+W2O3FYrf3RRbxake5TFW/TRQl1brqQXR4EzzffHqhmsYzmIGrv +/EhOdJhCrylvLmrH+33RZjEizIYAfmaDDEL0vTSSwxrqT8p+ck0LcIymSLumoRT2+1hEmRSuqguT +aaApJUqlyyvdimYHFngVV3Eb7PVHhPOeMTd61X8kreS8/f3MboPoDKi3QWwH3b08hpcv0g== +-----END CERTIFICATE----- + +SSL.com Root Certification Authority RSA +======================================== +-----BEGIN CERTIFICATE----- +MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxDjAM +BgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9TU0wgQ29ycG9yYXRpb24x +MTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYw +MjEyMTczOTM5WhcNNDEwMjEyMTczOTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMx +EDAOBgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NM +LmNvbSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2RxFdHaxh3a3by/ZPkPQ/C +Fp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aXqhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8 +P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcCC52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/ge +oeOy3ZExqysdBP+lSgQ36YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkp +k8zruFvh/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrFYD3Z +fBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93EJNyAKoFBbZQ+yODJ +gUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVcUS4cK38acijnALXRdMbX5J+tB5O2 +UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi8 +1xtZPCvM8hnIk2snYxnP/Okm+Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4s +bE6x/c+cCbqiM+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4GA1UdDwEB/wQE +AwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGVcpNxJK1ok1iOMq8bs3AD/CUr +dIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBcHadm47GUBwwyOabqG7B52B2ccETjit3E+ZUf +ijhDPwGFpUenPUayvOUiaPd7nNgsPgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAsl +u1OJD7OAUN5F7kR/q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjq +erQ0cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jra6x+3uxj +MxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90IH37hVZkLId6Tngr75qNJ +vTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/YK9f1JmzJBjSWFupwWRoyeXkLtoh/D1JI +Pb9s2KJELtFOt3JY04kTlf5Eq/jXixtunLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406y +wKBjYZC6VWg3dGq2ktufoYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NI +WuuA8ShYIc2wBlX7Jz9TkHCpBB5XJ7k= +-----END CERTIFICATE----- + +SSL.com Root Certification Authority ECC +======================================== +-----BEGIN CERTIFICATE----- +MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMCVVMxDjAMBgNV +BAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9TU0wgQ29ycG9yYXRpb24xMTAv +BgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEy +MTgxNDAzWhcNNDEwMjEyMTgxNDAzWjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAO +BgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv +bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuBBAAiA2IA +BEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI7Z4INcgn64mMU1jrYor+ +8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPgCemB+vNH06NjMGEwHQYDVR0OBBYEFILR +hXMw5zUE044CkvvlpNHEIejNMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTT +jgKS++Wk0cQh6M0wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCW +e+0F+S8Tkdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+gA0z +5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl +-----END CERTIFICATE----- + +SSL.com EV Root Certification Authority RSA R2 +============================================== +-----BEGIN CERTIFICATE----- +MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNVBAYTAlVTMQ4w +DAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9u +MTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy +MB4XDTE3MDUzMTE4MTQzN1oXDTQyMDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQI +DAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYD +VQQDDC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvqM0fNTPl9fb69LT3w23jh +hqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssufOePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7w +cXHswxzpY6IXFJ3vG2fThVUCAtZJycxa4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTO +Zw+oz12WGQvE43LrrdF9HSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+ +B6KjBSYRaZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcAb9Zh +CBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQGp8hLH94t2S42Oim +9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQVPWKchjgGAGYS5Fl2WlPAApiiECto +RHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMOpgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+Slm +JuwgUHfbSguPvuUCYHBBXtSuUDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48 ++qvWBkofZ6aYMBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV +HSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa49QaAJadz20Zp +qJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBWs47LCp1Jjr+kxJG7ZhcFUZh1 +++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5Sm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nx +Y/hoLVUE0fKNsKTPvDxeH3jnpaAgcLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2G +guDKBAdRUNf/ktUM79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDz +OFSz/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXtll9ldDz7 +CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEmKf7GUmG6sXP/wwyc5Wxq +lD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKKQbNmC1r7fSOl8hqw/96bg5Qu0T/fkreR +rwU7ZcegbLHNYhLDkBvjJc40vG93drEQw/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1 +hlMYegouCRw2n5H9gooiS9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX +9hwJ1C07mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w== +-----END CERTIFICATE----- + +SSL.com EV Root Certification Authority ECC +=========================================== +-----BEGIN CERTIFICATE----- +MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMCVVMxDjAMBgNV +BAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9TU0wgQ29ycG9yYXRpb24xNDAy +BgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYw +MjEyMTgxNTIzWhcNNDEwMjEyMTgxNTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMx +EDAOBgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NM +LmNvbSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMAVIbc/R/fALhBYlzccBYy +3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1KthkuWnBaBu2+8KGwytAJKaNjMGEwHQYDVR0O +BBYEFFvKXuXe0oGqzagtZFG22XKbl+ZPMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe +5d7SgarNqC1kUbbZcpuX5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJ +N+vp1RPZytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZgh5Mm +m7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== +-----END CERTIFICATE----- + +GlobalSign Root CA - R6 +======================= +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEgMB4GA1UECxMX +R2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkds +b2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQxMjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9i +YWxTaWduIFJvb3QgQ0EgLSBSNjETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFs +U2lnbjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQss +grRIxutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1kZguSgMpE +3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxDaNc9PIrFsmbVkJq3MQbF +vuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJwLnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqM +PKq0pPbzlUoSB239jLKJz9CgYXfIWHSw1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+ +azayOeSsJDa38O+2HBNXk7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05O +WgtH8wY2SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/hbguy +CLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4nWUx2OVvq+aWh2IMP +0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpYrZxCRXluDocZXFSxZba/jJvcE+kN +b7gu3GduyYsRtYQUigAZcIN5kZeR1BonvzceMgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQE +AwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNV +HSMEGDAWgBSubAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN +nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGtIxg93eFyRJa0 +lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr6155wsTLxDKZmOMNOsIeDjHfrY +BzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLjvUYAGm0CuiVdjaExUd1URhxN25mW7xocBFym +Fe944Hn+Xds+qkxV/ZoVqW/hpvvfcDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr +3TsTjxKM4kEaSHpzoHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB1 +0jZpnOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfspA9MRf/T +uTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+vJJUEeKgDu+6B5dpffItK +oZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+t +JDfLRVpOoERIyNiwmcUVhAn21klJwGW45hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA= +-----END CERTIFICATE----- + +OISTE WISeKey Global Root GC CA +=============================== +-----BEGIN CERTIFICATE----- +MIICaTCCAe+gAwIBAgIQISpWDK7aDKtARb8roi066jAKBggqhkjOPQQDAzBtMQswCQYDVQQGEwJD +SDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNlZDEo +MCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQyBDQTAeFw0xNzA1MDkwOTQ4MzRa +Fw00MjA1MDkwOTU4MzNaMG0xCzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQL +ExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh +bCBSb290IEdDIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETOlQwMYPchi82PG6s4nieUqjFqdr +VCTbUf/q9Akkwwsin8tqJ4KBDdLArzHkdIJuyiXZjHWd8dvQmqJLIX4Wp2OQ0jnUsYd4XxiWD1Ab +NTcPasbc2RNNpI6QN+a9WzGRo1QwUjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAd +BgNVHQ4EFgQUSIcUrOPDnpBgOtfKie7TrYy0UGYwEAYJKwYBBAGCNxUBBAMCAQAwCgYIKoZIzj0E +AwMDaAAwZQIwJsdpW9zV57LnyAyMjMPdeYwbY9XJUpROTYJKcx6ygISpJcBMWm1JKWB4E+J+SOtk +AjEA2zQgMgj/mkkCtojeFK9dbJlxjRo/i9fgojaGHAeCOnZT/cKi7e97sIBPWA9LUzm9 +-----END CERTIFICATE----- + +UCA Global G2 Root +================== +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIQXd+x2lqj7V2+WmUgZQOQ7zANBgkqhkiG9w0BAQsFADA9MQswCQYDVQQG +EwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxGzAZBgNVBAMMElVDQSBHbG9iYWwgRzIgUm9vdDAeFw0x +NjAzMTEwMDAwMDBaFw00MDEyMzEwMDAwMDBaMD0xCzAJBgNVBAYTAkNOMREwDwYDVQQKDAhVbmlU +cnVzdDEbMBkGA1UEAwwSVUNBIEdsb2JhbCBHMiBSb290MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEAxeYrb3zvJgUno4Ek2m/LAfmZmqkywiKHYUGRO8vDaBsGxUypK8FnFyIdK+35KYmT +oni9kmugow2ifsqTs6bRjDXVdfkX9s9FxeV67HeToI8jrg4aA3++1NDtLnurRiNb/yzmVHqUwCoV +8MmNsHo7JOHXaOIxPAYzRrZUEaalLyJUKlgNAQLx+hVRZ2zA+te2G3/RVogvGjqNO7uCEeBHANBS +h6v7hn4PJGtAnTRnvI3HLYZveT6OqTwXS3+wmeOwcWDcC/Vkw85DvG1xudLeJ1uK6NjGruFZfc8o +LTW4lVYa8bJYS7cSN8h8s+1LgOGN+jIjtm+3SJUIsUROhYw6AlQgL9+/V087OpAh18EmNVQg7Mc/ +R+zvWr9LesGtOxdQXGLYD0tK3Cv6brxzks3sx1DoQZbXqX5t2Okdj4q1uViSukqSKwxW/YDrCPBe +KW4bHAyvj5OJrdu9o54hyokZ7N+1wxrrFv54NkzWbtA+FxyQF2smuvt6L78RHBgOLXMDj6DlNaBa +4kx1HXHhOThTeEDMg5PXCp6dW4+K5OXgSORIskfNTip1KnvyIvbJvgmRlld6iIis7nCs+dwp4wwc +OxJORNanTrAmyPPZGpeRaOrvjUYG0lZFWJo8DA+DuAUlwznPO6Q0ibd5Ei9Hxeepl2n8pndntd97 +8XplFeRhVmUCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O +BBYEFIHEjMz15DD/pQwIX4wVZyF0Ad/fMA0GCSqGSIb3DQEBCwUAA4ICAQATZSL1jiutROTL/7lo +5sOASD0Ee/ojL3rtNtqyzm325p7lX1iPyzcyochltq44PTUbPrw7tgTQvPlJ9Zv3hcU2tsu8+Mg5 +1eRfB70VVJd0ysrtT7q6ZHafgbiERUlMjW+i67HM0cOU2kTC5uLqGOiiHycFutfl1qnN3e92mI0A +Ds0b+gO3joBYDic/UvuUospeZcnWhNq5NXHzJsBPd+aBJ9J3O5oUb3n09tDh05S60FdRvScFDcH9 +yBIw7m+NESsIndTUv4BFFJqIRNow6rSn4+7vW4LVPtateJLbXDzz2K36uGt/xDYotgIVilQsnLAX +c47QN6MUPJiVAAwpBVueSUmxX8fjy88nZY41F7dXyDDZQVu5FLbowg+UMaeUmMxq67XhJ/UQqAHo +jhJi6IjMtX9Gl8CbEGY4GjZGXyJoPd/JxhMnq1MGrKI8hgZlb7F+sSlEmqO6SWkoaY/X5V+tBIZk +bxqgDMUIYs6Ao9Dz7GjevjPHF1t/gMRMTLGmhIrDO7gJzRSBuhjjVFc2/tsvfEehOjPI+Vg7RE+x +ygKJBJYoaMVLuCaJu9YzL1DV/pqJuhgyklTGW+Cd+V7lDSKb9triyCGyYiGqhkCyLmTTX8jjfhFn +RR8F/uOi77Oos/N9j/gMHyIfLXC0uAE0djAA5SN4p1bXUB+K+wb1whnw0A== +-----END CERTIFICATE----- + +UCA Extended Validation Root +============================ +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgIQT9Irj/VkyDOeTzRYZiNwYDANBgkqhkiG9w0BAQsFADBHMQswCQYDVQQG +EwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9u +IFJvb3QwHhcNMTUwMzEzMDAwMDAwWhcNMzgxMjMxMDAwMDAwWjBHMQswCQYDVQQGEwJDTjERMA8G +A1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCpCQcoEwKwmeBkqh5DFnpzsZGgdT6o+uM4AHrs +iWogD4vFsJszA1qGxliG1cGFu0/GnEBNyr7uaZa4rYEwmnySBesFK5pI0Lh2PpbIILvSsPGP2KxF +Rv+qZ2C0d35qHzwaUnoEPQc8hQ2E0B92CvdqFN9y4zR8V05WAT558aopO2z6+I9tTcg1367r3CTu +eUWnhbYFiN6IXSV8l2RnCdm/WhUFhvMJHuxYMjMR83dksHYf5BA1FxvyDrFspCqjc/wJHx4yGVMR +59mzLC52LqGj3n5qiAno8geK+LLNEOfic0CTuwjRP+H8C5SzJe98ptfRr5//lpr1kXuYC3fUfugH +0mK1lTnj8/FtDw5lhIpjVMWAtuCeS31HJqcBCF3RiJ7XwzJE+oJKCmhUfzhTA8ykADNkUVkLo4KR +el7sFsLzKuZi2irbWWIQJUoqgQtHB0MGcIfS+pMRKXpITeuUx3BNr2fVUbGAIAEBtHoIppB/TuDv +B0GHr2qlXov7z1CymlSvw4m6WC31MJixNnI5fkkE/SmnTHnkBVfblLkWU41Gsx2VYVdWf6/wFlth +WG82UBEL2KwrlRYaDh8IzTY0ZRBiZtWAXxQgXy0MoHgKaNYs1+lvK9JKBZP8nm9rZ/+I8U6laUpS +NwXqxhaN0sSZ0YIrO7o1dfdRUVjzyAfd5LQDfwIDAQABo0IwQDAdBgNVHQ4EFgQU2XQ65DA9DfcS +3H5aBZ8eNJr34RQwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQEL +BQADggIBADaNl8xCFWQpN5smLNb7rhVpLGsaGvdftvkHTFnq88nIua7Mui563MD1sC3AO6+fcAUR +ap8lTwEpcOPlDOHqWnzcSbvBHiqB9RZLcpHIojG5qtr8nR/zXUACE/xOHAbKsxSQVBcZEhrxH9cM +aVr2cXj0lH2RC47skFSOvG+hTKv8dGT9cZr4QQehzZHkPJrgmzI5c6sq1WnIeJEmMX3ixzDx/BR4 +dxIOE/TdFpS/S2d7cFOFyrC78zhNLJA5wA3CXWvp4uXViI3WLL+rG761KIcSF3Ru/H38j9CHJrAb ++7lsq+KePRXBOy5nAliRn+/4Qh8st2j1da3Ptfb/EX3C8CSlrdP6oDyp+l3cpaDvRKS+1ujl5BOW +F3sGPjLtx7dCvHaj2GU4Kzg1USEODm8uNBNA4StnDG1KQTAYI1oyVZnJF+A83vbsea0rWBmirSwi +GpWOvpaQXUJXxPkUAzUrHC1RVwinOt4/5Mi0A3PCwSaAuwtCH60NryZy2sy+s6ODWA2CxR9GUeOc +GMyNm43sSet1UNWMKFnKdDTajAshqx7qG+XH/RU+wBeq+yNuJkbL+vmxcmtpzyKEC2IPrNkZAJSi +djzULZrtBJ4tBmIQN1IchXIbJ+XMxjHsN+xjWZsLHXbMfjKaiJUINlK73nZfdklJrX+9ZSCyycEr +dhh2n1ax +-----END CERTIFICATE----- + +Certigna Root CA +================ +-----BEGIN CERTIFICATE----- +MIIGWzCCBEOgAwIBAgIRAMrpG4nxVQMNo+ZBbcTjpuEwDQYJKoZIhvcNAQELBQAwWjELMAkGA1UE +BhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczEcMBoGA1UECwwTMDAwMiA0ODE0NjMwODEwMDAzNjEZ +MBcGA1UEAwwQQ2VydGlnbmEgUm9vdCBDQTAeFw0xMzEwMDEwODMyMjdaFw0zMzEwMDEwODMyMjda +MFoxCzAJBgNVBAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxHDAaBgNVBAsMEzAwMDIgNDgxNDYz +MDgxMDAwMzYxGTAXBgNVBAMMEENlcnRpZ25hIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQDNGDllGlmx6mQWDoyUJJV8g9PFOSbcDO8WV43X2KyjQn+Cyu3NW9sOty3tRQgX +stmzy9YXUnIo245Onoq2C/mehJpNdt4iKVzSs9IGPjA5qXSjklYcoW9MCiBtnyN6tMbaLOQdLNyz +KNAT8kxOAkmhVECe5uUFoC2EyP+YbNDrihqECB63aCPuI9Vwzm1RaRDuoXrC0SIxwoKF0vJVdlB8 +JXrJhFwLrN1CTivngqIkicuQstDuI7pmTLtipPlTWmR7fJj6o0ieD5Wupxj0auwuA0Wv8HT4Ks16 +XdG+RCYyKfHx9WzMfgIhC59vpD++nVPiz32pLHxYGpfhPTc3GGYo0kDFUYqMwy3OU4gkWGQwFsWq +4NYKpkDfePb1BHxpE4S80dGnBs8B92jAqFe7OmGtBIyT46388NtEbVncSVmurJqZNjBBe3YzIoej +wpKGbvlw7q6Hh5UbxHq9MfPU0uWZ/75I7HX1eBYdpnDBfzwboZL7z8g81sWTCo/1VTp2lc5ZmIoJ +lXcymoO6LAQ6l73UL77XbJuiyn1tJslV1c/DeVIICZkHJC1kJWumIWmbat10TWuXekG9qxf5kBdI +jzb5LdXF2+6qhUVB+s06RbFo5jZMm5BX7CO5hwjCxAnxl4YqKE3idMDaxIzb3+KhF1nOJFl0Mdp/ +/TBt2dzhauH8XwIDAQABo4IBGjCCARYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw +HQYDVR0OBBYEFBiHVuBud+4kNTxOc5of1uHieX4rMB8GA1UdIwQYMBaAFBiHVuBud+4kNTxOc5of +1uHieX4rMEQGA1UdIAQ9MDswOQYEVR0gADAxMC8GCCsGAQUFBwIBFiNodHRwczovL3d3d3cuY2Vy +dGlnbmEuZnIvYXV0b3JpdGVzLzBtBgNVHR8EZjBkMC+gLaArhilodHRwOi8vY3JsLmNlcnRpZ25h +LmZyL2NlcnRpZ25hcm9vdGNhLmNybDAxoC+gLYYraHR0cDovL2NybC5kaGlteW90aXMuY29tL2Nl +cnRpZ25hcm9vdGNhLmNybDANBgkqhkiG9w0BAQsFAAOCAgEAlLieT/DjlQgi581oQfccVdV8AOIt +OoldaDgvUSILSo3L6btdPrtcPbEo/uRTVRPPoZAbAh1fZkYJMyjhDSSXcNMQH+pkV5a7XdrnxIxP +TGRGHVyH41neQtGbqH6mid2PHMkwgu07nM3A6RngatgCdTer9zQoKJHyBApPNeNgJgH60BGM+RFq +7q89w1DTj18zeTyGqHNFkIwgtnJzFyO+B2XleJINugHA64wcZr+shncBlA2c5uk5jR+mUYyZDDl3 +4bSb+hxnV29qao6pK0xXeXpXIs/NX2NGjVxZOob4Mkdio2cNGJHc+6Zr9UhhcyNZjgKnvETq9Emd +8VRY+WCv2hikLyhF3HqgiIZd8zvn/yk1gPxkQ5Tm4xxvvq0OKmOZK8l+hfZx6AYDlf7ej0gcWtSS +6Cvu5zHbugRqh5jnxV/vfaci9wHYTfmJ0A6aBVmknpjZbyvKcL5kwlWj9Omvw5Ip3IgWJJk8jSaY +tlu3zM63Nwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayhjWZS +aX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw3kAP+HwV96LOPNde +E4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0= +-----END CERTIFICATE----- + +emSign Root CA - G1 +=================== +-----BEGIN CERTIFICATE----- +MIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYDVQQGEwJJTjET +MBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRl +ZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBHMTAeFw0xODAyMTgxODMwMDBaFw00MzAyMTgx +ODMwMDBaMGcxCzAJBgNVBAYTAklOMRMwEQYDVQQLEwplbVNpZ24gUEtJMSUwIwYDVQQKExxlTXVk +aHJhIFRlY2hub2xvZ2llcyBMaW1pdGVkMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEcxMIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk0u76WaK7p1b1TST0Bsew+eeuGQzf2N4aLTN +LnF115sgxk0pvLZoYIr3IZpWNVrzdr3YzZr/k1ZLpVkGoZM0Kd0WNHVO8oG0x5ZOrRkVUkr+PHB1 +cM2vK6sVmjM8qrOLqs1D/fXqcP/tzxE7lM5OMhbTI0Aqd7OvPAEsbO2ZLIvZTmmYsvePQbAyeGHW +DV/D+qJAkh1cF+ZwPjXnorfCYuKrpDhMtTk1b+oDafo6VGiFbdbyL0NVHpENDtjVaqSW0RM8LHhQ +6DqS0hdW5TUaQBw+jSztOd9C4INBdN+jzcKGYEho42kLVACL5HZpIQ15TjQIXhTCzLG3rdd8cIrH +hQIDAQABo0IwQDAdBgNVHQ4EFgQU++8Nhp6w492pufEhF38+/PB3KxowDgYDVR0PAQH/BAQDAgEG +MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFn/8oz1h31xPaOfG1vR2vjTnGs2 +vZupYeveFix0PZ7mddrXuqe8QhfnPZHr5X3dPpzxz5KsbEjMwiI/aTvFthUvozXGaCocV685743Q +NcMYDHsAVhzNixl03r4PEuDQqqE/AjSxcM6dGNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q ++Mri/Tm3R7nrft8EI6/6nAYH6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeih +U80Bv2noWgbyRQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx +iN66zB+Afko= +-----END CERTIFICATE----- + +emSign ECC Root CA - G3 +======================= +-----BEGIN CERTIFICATE----- +MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQGEwJJTjETMBEG +A1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEg +MB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4 +MTgzMDAwWjBrMQswCQYDVQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11 +ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g +RzMwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0WXTsuwYc +58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xySfvalY8L1X44uT6EYGQIr +MgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuBzhccLikenEhjQjAOBgNVHQ8BAf8EBAMC +AQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+D +CBeQyh+KTOgNG3qxrdWBCUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7 +jHvrZQnD+JbNR6iC8hZVdyR+EhCVBCyj +-----END CERTIFICATE----- + +emSign Root CA - C1 +=================== +-----BEGIN CERTIFICATE----- +MIIDczCCAlugAwIBAgILAK7PALrEzzL4Q7IwDQYJKoZIhvcNAQELBQAwVjELMAkGA1UEBhMCVVMx +EzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQDExNlbVNp +Z24gUm9vdCBDQSAtIEMxMB4XDTE4MDIxODE4MzAwMFoXDTQzMDIxODE4MzAwMFowVjELMAkGA1UE +BhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQD +ExNlbVNpZ24gUm9vdCBDQSAtIEMxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+up +ufGZBczYKCFK83M0UYRWEPWgTywS4/oTmifQz/l5GnRfHXk5/Fv4cI7gklL35CX5VIPZHdPIWoU/ +Xse2B+4+wM6ar6xWQio5JXDWv7V7Nq2s9nPczdcdioOl+yuQFTdrHCZH3DspVpNqs8FqOp099cGX +OFgFixwR4+S0uF2FHYP+eF8LRWgYSKVGczQ7/g/IdrvHGPMF0Ybzhe3nudkyrVWIzqa2kbBPrH4V +I5b2P/AgNBbeCsbEBEV5f6f9vtKppa+cxSMq9zwhbL2vj07FOrLzNBL834AaSaTUqZX3noleooms +lMuoaJuvimUnzYnu3Yy1aylwQ6BpC+S5DwIDAQABo0IwQDAdBgNVHQ4EFgQU/qHgcB4qAzlSWkK+ +XJGFehiqTbUwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQAD +ggEBAMJKVvoVIXsoounlHfv4LcQ5lkFMOycsxGwYFYDGrK9HWS8mC+M2sO87/kOXSTKZEhVb3xEp +/6tT+LvBeA+snFOvV71ojD1pM/CjoCNjO2RnIkSt1XHLVip4kqNPEjE2NuLe/gDEo2APJ62gsIq1 +NnpSob0n9CAnYuhNlCQT5AoE6TyrLshDCUrGYQTlSTR+08TI9Q/Aqum6VF7zYytPT1DU/rl7mYw9 +wC68AivTxEDkigcxHpvOJpkT+xHqmiIMERnHXhuBUDDIlhJu58tBf5E7oke3VIAb3ADMmpDqw8NQ +BmIMMMAVSKeoWXzhriKi4gp6D/piq1JM4fHfyr6DDUI= +-----END CERTIFICATE----- + +emSign ECC Root CA - C3 +======================= +-----BEGIN CERTIFICATE----- +MIICKzCCAbGgAwIBAgIKe3G2gla4EnycqDAKBggqhkjOPQQDAzBaMQswCQYDVQQGEwJVUzETMBEG +A1UECxMKZW1TaWduIFBLSTEUMBIGA1UEChMLZU11ZGhyYSBJbmMxIDAeBgNVBAMTF2VtU2lnbiBF +Q0MgUm9vdCBDQSAtIEMzMB4XDTE4MDIxODE4MzAwMFoXDTQzMDIxODE4MzAwMFowWjELMAkGA1UE +BhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMSAwHgYDVQQD +ExdlbVNpZ24gRUNDIFJvb3QgQ0EgLSBDMzB2MBAGByqGSM49AgEGBSuBBAAiA2IABP2lYa57JhAd +6bciMK4G9IGzsUJxlTm801Ljr6/58pc1kjZGDoeVjbk5Wum739D+yAdBPLtVb4OjavtisIGJAnB9 +SMVK4+kiVCJNk7tCDK93nCOmfddhEc5lx/h//vXyqaNCMEAwHQYDVR0OBBYEFPtaSNCAIEDyqOkA +B2kZd6fmw/TPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMDA2gA +MGUCMQC02C8Cif22TGK6Q04ThHK1rt0c3ta13FaPWEBaLd4gTCKDypOofu4SQMfWh0/434UCMBwU +ZOR8loMRnLDRWmFLpg9J0wD8ofzkpf9/rdcw0Md3f76BB1UwUCAU9Vc4CqgxUQ== +-----END CERTIFICATE----- + +Hongkong Post Root CA 3 +======================= +-----BEGIN CERTIFICATE----- +MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQELBQAwbzELMAkG +A1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJSG9uZyBLb25nMRYwFAYDVQQK +Ew1Ib25na29uZyBQb3N0MSAwHgYDVQQDExdIb25na29uZyBQb3N0IFJvb3QgQ0EgMzAeFw0xNzA2 +MDMwMjI5NDZaFw00MjA2MDMwMjI5NDZaMG8xCzAJBgNVBAYTAkhLMRIwEAYDVQQIEwlIb25nIEtv +bmcxEjAQBgNVBAcTCUhvbmcgS29uZzEWMBQGA1UEChMNSG9uZ2tvbmcgUG9zdDEgMB4GA1UEAxMX +SG9uZ2tvbmcgUG9zdCBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz +iNfqzg8gTr7m1gNt7ln8wlffKWihgw4+aMdoWJwcYEuJQwy51BWy7sFOdem1p+/l6TWZ5Mwc50tf +jTMwIDNT2aa71T4Tjukfh0mtUC1Qyhi+AViiE3CWu4mIVoBc+L0sPOFMV4i707mV78vH9toxdCim +5lSJ9UExyuUmGs2C4HDaOym71QP1mbpV9WTRYA6ziUm4ii8F0oRFKHyPaFASePwLtVPLwpgchKOe +sL4jpNrcyCse2m5FHomY2vkALgbpDDtw1VAliJnLzXNg99X/NWfFobxeq81KuEXryGgeDQ0URhLj +0mRiikKYvLTGCAj4/ahMZJx2Ab0vqWwzD9g/KLg8aQFChn5pwckGyuV6RmXpwtZQQS4/t+TtbNe/ +JgERohYpSms0BpDsE9K2+2p20jzt8NYt3eEV7KObLyzJPivkaTv/ciWxNoZbx39ri1UbSsUgYT2u +y1DhCDq+sI9jQVMwCFk8mB13umOResoQUGC/8Ne8lYePl8X+l2oBlKN8W4UdKjk60FSh0Tlxnf0h ++bV78OLgAo9uliQlLKAeLKjEiafv7ZkGL7YKTE/bosw3Gq9HhS2KX8Q0NEwA/RiTZxPRN+ZItIsG +xVd7GYYKecsAyVKvQv83j+GjHno9UKtjBucVtT+2RTeUN7F+8kjDf8V1/peNRY8apxpyKBpADwID +AQABo2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQXnc0e +i9Y5K3DTXNSguB+wAPzFYTAdBgNVHQ4EFgQUF53NHovWOStw01zUoLgfsAD8xWEwDQYJKoZIhvcN +AQELBQADggIBAFbVe27mIgHSQpsY1Q7XZiNc4/6gx5LS6ZStS6LG7BJ8dNVI0lkUmcDrudHr9Egw +W62nV3OZqdPlt9EuWSRY3GguLmLYauRwCy0gUCCkMpXRAJi70/33MvJJrsZ64Ee+bs7Lo3I6LWld +y8joRTnU+kLBEUx3XZL7av9YROXrgZ6voJmtvqkBZss4HTzfQx/0TW60uhdG/H39h4F5ag0zD/ov ++BS5gLNdTaqX4fnkGMX41TiMJjz98iji7lpJiCzfeT2OnpA8vUFKOt1b9pq0zj8lMH8yfaIDlNDc +eqFS3m6TjRgm/VWsvY+b0s+v54Ysyx8Jb6NvqYTUc79NoXQbTiNg8swOqn+knEwlqLJmOzj/2ZQw +9nKEvmhVEA/GcywWaZMH/rFF7buiVWqw2rVKAiUnhde3t4ZEFolsgCs+l6mc1X5VTMbeRRAc6uk7 +nwNT7u56AQIWeNTowr5GdogTPyK7SBIdUgC0An4hGh6cJfTzPV4e0hz5sy229zdcxsshTrD3mUcY +hcErulWuBurQB7Lcq9CClnXO0lD+mefPL5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB +60PZ2Pierc+xYw5F9KBaLJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fq +dBb9HxEGmpv0 +-----END CERTIFICATE----- + +Microsoft ECC Root Certificate Authority 2017 +============================================= +-----BEGIN CERTIFICATE----- +MIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQswCQYDVQQGEwJV +UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNyb3NvZnQgRUND +IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwHhcNMTkxMjE4MjMwNjQ1WhcNNDIwNzE4 +MjMxNjA0WjBlMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw +NAYDVQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQ +BgcqhkjOPQIBBgUrgQQAIgNiAATUvD0CQnVBEyPNgASGAlEvaqiBYgtlzPbKnR5vSmZRogPZnZH6 +thaxjG7efM3beaYvzrvOcS/lpaso7GMEZpn4+vKTEAXhgShC48Zo9OYbhGBKia/teQ87zvH2RPUB +eMCjVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTIy5lycFIM ++Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3FQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlf +Xu5gKcs68tvWMoQZP3zVL8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaR +eNtUjGUBiudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M= +-----END CERTIFICATE----- + +Microsoft RSA Root Certificate Authority 2017 +============================================= +-----BEGIN CERTIFICATE----- +MIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBlMQswCQYDVQQG +EwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNyb3NvZnQg +UlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwHhcNMTkxMjE4MjI1MTIyWhcNNDIw +NzE4MjMwMDIzWjBlMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u +MTYwNAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcw +ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZNt9GkMml +7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/xztokLaCLeX0ZdDMbRnMlfl7rEqUrQ7e +S0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1HLUX6NZFndiyJrDKxHBKrmCk3bPZ7Pw7 +1VdyvD/IybLeS2v4I2wDwAW9lcfNcztmgGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI+ +dkC0zVJhUXAoP8XFWvLJjEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49F +yGcohJUcaDDv70ngNXtk55iwlNpNhTs+VcQor1fznhPbRiefHqJeRIOkpcrVE7NLP8TjwuaGYaRS +MLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6W6IYZVcSn2i51BVr +lMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4KUGsTuqwPN1q3ErWQgR5WrlcihtnJ +0tHXUeOrO8ZV/R4O03QK0dqq6mm4lyiPSMQH+FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJ +ClTUFLkqqNfs+avNJVgyeY+QW5g5xAgGwax/Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tNsiMwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZCLgLNFgVZJ8og +6Lq46BrsTaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OCgMNPOsduET/m4xaRhPtthH80 +dK2Jp86519efhGSSvpWhrQlTM93uCupKUY5vVau6tZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk ++ONVFT24bcMKpBLBaYVu32TxU5nhSnUgnZUP5NbcA/FZGOhHibJXWpS2qdgXKxdJ5XbLwVaZOjex +/2kskZGT4d9Mozd2TaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDy +AmH3pvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGRxpl/j8nW +ZjEgQRCHLQzWwa80mMpkg/sTV9HB8Dx6jKXB/ZUhoHHBk2dxEuqPiAppGWSZI1b7rCoucL5mxAyE +7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9dOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKT +c0QWbej09+CVgI+WXTik9KveCjCHk9hNAHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D +5KbvtwEwXlGjefVwaaZBRA+GsCyRxj3qrg+E +-----END CERTIFICATE----- + +e-Szigno Root CA 2017 +===================== +-----BEGIN CERTIFICATE----- +MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNVBAYTAkhVMREw +DwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRkLjEXMBUGA1UEYQwOVkFUSFUt +MjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJvb3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZa +Fw00MjA4MjIxMjA3MDZaMHExCzAJBgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UE +CgwNTWljcm9zZWMgTHRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3pp +Z25vIFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtvxie+RJCx +s1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+HWyx7xf58etqjYzBhMA8G +A1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSHERUI0arBeAyxr87GyZDv +vzAEwDAfBgNVHSMEGDAWgBSHERUI0arBeAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEA +tVfd14pVCzbhhkT61NlojbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxO +svxyqltZ+efcMQ== +-----END CERTIFICATE----- + +certSIGN Root CA G2 +=================== +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNVBAYTAlJPMRQw +EgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04gUk9PVCBDQSBHMjAeFw0xNzAy +MDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJBgNVBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lH +TiBTQTEcMBoGA1UECxMTY2VydFNJR04gUk9PVCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBAMDFdRmRfUR0dIf+DjuW3NgBFszuY5HnC2/OOwppGnzC46+CjobXXo9X69MhWf05 +N0IwvlDqtg+piNguLWkh59E3GE59kdUWX2tbAMI5Qw02hVK5U2UPHULlj88F0+7cDBrZuIt4Imfk +abBoxTzkbFpG583H+u/E7Eu9aqSs/cwoUe+StCmrqzWaTOTECMYmzPhpn+Sc8CnTXPnGFiWeI8Mg +wT0PPzhAsP6CRDiqWhqKa2NYOLQV07YRaXseVO6MGiKscpc/I1mbySKEwQdPzH/iV8oScLumZfNp +dWO9lfsbl83kqK/20U6o2YpxJM02PbyWxPFsqa7lzw1uKA2wDrXKUXt4FMMgL3/7FFXhEZn91Qqh +ngLjYl/rNUssuHLoPj1PrCy7Lobio3aP5ZMqz6WryFyNSwb/EkaseMsUBzXgqd+L6a8VTxaJW732 +jcZZroiFDsGJ6x9nxUWO/203Nit4ZoORUSs9/1F3dmKh7Gc+PoGD4FapUB8fepmrY7+EF3fxDTvf +95xhszWYijqy7DwaNz9+j5LP2RIUZNoQAhVB/0/E6xyjyfqZ90bp4RjZsbgyLcsUDFDYg2WD7rlc +z8sFWkz6GZdr1l0T08JcVLwyc6B49fFtHsufpaafItzRUZ6CeWRgKRM+o/1Pcmqr4tTluCRVLERL +iohEnMqE0yo7AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1Ud +DgQWBBSCIS1mxteg4BXrzkwJd8RgnlRuAzANBgkqhkiG9w0BAQsFAAOCAgEAYN4auOfyYILVAzOB +ywaK8SJJ6ejqkX/GM15oGQOGO0MBzwdw5AgeZYWR5hEit/UCI46uuR59H35s5r0l1ZUa8gWmr4UC +b6741jH/JclKyMeKqdmfS0mbEVeZkkMR3rYzpMzXjWR91M08KCy0mpbqTfXERMQlqiCA2ClV9+BB +/AYm/7k29UMUA2Z44RGx2iBfRgB4ACGlHgAoYXhvqAEBj500mv/0OJD7uNGzcgbJceaBxXntC6Z5 +8hMLnPddDnskk7RI24Zf3lCGeOdA5jGokHZwYa+cNywRtYK3qq4kNFtyDGkNzVmf9nGvnAvRCjj5 +BiKDUyUM/FHE5r7iOZULJK2v0ZXkltd0ZGtxTgI8qoXzIKNDOXZbbFD+mpwUHmUUihW9o4JFWklW +atKcsWMy5WHgUyIOpwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tU +Sxfj03k9bWtJySgOLnRQvwzZRjoQhsmnP+mg7H/rpXdYaXHmgwo38oZJar55CJD2AhZkPuXaTH4M +NMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE1LlSVHJ7liXMvGnjSG4N +0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MXQRBdJ3NghVdJIgc= +-----END CERTIFICATE----- + +Trustwave Global Certification Authority +======================================== +-----BEGIN CERTIFICATE----- +MIIF2jCCA8KgAwIBAgIMBfcOhtpJ80Y1LrqyMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJV +UzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAfBgNVBAoMGFRydXN0d2F2 +ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEdsb2JhbCBDZXJ0aWZpY2F0aW9u +IEF1dGhvcml0eTAeFw0xNzA4MjMxOTM0MTJaFw00MjA4MjMxOTM0MTJaMIGIMQswCQYDVQQGEwJV +UzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAfBgNVBAoMGFRydXN0d2F2 +ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEdsb2JhbCBDZXJ0aWZpY2F0aW9u +IEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALldUShLPDeS0YLOvR29 +zd24q88KPuFd5dyqCblXAj7mY2Hf8g+CY66j96xz0XznswuvCAAJWX/NKSqIk4cXGIDtiLK0thAf +LdZfVaITXdHG6wZWiYj+rDKd/VzDBcdu7oaJuogDnXIhhpCujwOl3J+IKMujkkkP7NAP4m1ET4Bq +stTnoApTAbqOl5F2brz81Ws25kCI1nsvXwXoLG0R8+eyvpJETNKXpP7ScoFDB5zpET71ixpZfR9o +WN0EACyW80OzfpgZdNmcc9kYvkHHNHnZ9GLCQ7mzJ7Aiy/k9UscwR7PJPrhq4ufogXBeQotPJqX+ +OsIgbrv4Fo7NDKm0G2x2EOFYeUY+VM6AqFcJNykbmROPDMjWLBz7BegIlT1lRtzuzWniTY+HKE40 +Cz7PFNm73bZQmq131BnW2hqIyE4bJ3XYsgjxroMwuREOzYfwhI0Vcnyh78zyiGG69Gm7DIwLdVcE +uE4qFC49DxweMqZiNu5m4iK4BUBjECLzMx10coos9TkpoNPnG4CELcU9402x/RpvumUHO1jsQkUm ++9jaJXLE9gCxInm943xZYkqcBW89zubWR2OZxiRvchLIrH+QtAuRcOi35hYQcRfO3gZPSEF9NUqj +ifLJS3tBEW1ntwiYTOURGa5CgNz7kAXU+FDKvuStx8KU1xad5hePrzb7AgMBAAGjQjBAMA8GA1Ud +EwEB/wQFMAMBAf8wHQYDVR0OBBYEFJngGWcNYtt2s9o9uFvo/ULSMQ6HMA4GA1UdDwEB/wQEAwIB +BjANBgkqhkiG9w0BAQsFAAOCAgEAmHNw4rDT7TnsTGDZqRKGFx6W0OhUKDtkLSGm+J1WE2pIPU/H +PinbbViDVD2HfSMF1OQc3Og4ZYbFdada2zUFvXfeuyk3QAUHw5RSn8pk3fEbK9xGChACMf1KaA0H +ZJDmHvUqoai7PF35owgLEQzxPy0QlG/+4jSHg9bP5Rs1bdID4bANqKCqRieCNqcVtgimQlRXtpla +4gt5kNdXElE1GYhBaCXUNxeEFfsBctyV3lImIJgm4nb1J2/6ADtKYdkNy1GTKv0WBpanI5ojSP5R +vbbEsLFUzt5sQa0WZ37b/TjNuThOssFgy50X31ieemKyJo90lZvkWx3SD92YHJtZuSPTMaCm/zjd +zyBP6VhWOmfD0faZmZ26NraAL4hHT4a/RDqA5Dccprrql5gR0IRiR2Qequ5AvzSxnI9O4fKSTx+O +856X3vOmeWqJcU9LJxdI/uz0UA9PSX3MReO9ekDFQdxhVicGaeVyQYHTtgGJoC86cnn+OjC/QezH +Yj6RS8fZMXZC+fc8Y+wmjHMMfRod6qh8h6jCJ3zhM0EPz8/8AKAigJ5Kp28AsEFFtyLKaEjFQqKu +3R3y4G5OBVixwJAWKqQ9EEC+j2Jjg6mcgn0tAumDMHzLJ8n9HmYAsC7TIS+OMxZsmO0QqAfWzJPP +29FpHOTKyeC2nOnOcXHebD8WpHk= +-----END CERTIFICATE----- + +Trustwave Global ECC P256 Certification Authority +================================================= +-----BEGIN CERTIFICATE----- +MIICYDCCAgegAwIBAgIMDWpfCD8oXD5Rld9dMAoGCCqGSM49BAMCMIGRMQswCQYDVQQGEwJVUzER +MA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0d2F2ZSBI +b2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBFQ0MgUDI1NiBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMxOTM1MTBaFw00MjA4MjMxOTM1MTBaMIGRMQswCQYD +VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRy +dXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBFQ0MgUDI1 +NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABH77bOYj +43MyCMpg5lOcunSNGLB4kFKA3TjASh3RqMyTpJcGOMoNFWLGjgEqZZ2q3zSRLoHB5DOSMcT9CTqm +P62jQzBBMA8GA1UdEwEB/wQFMAMBAf8wDwYDVR0PAQH/BAUDAwcGADAdBgNVHQ4EFgQUo0EGrJBt +0UrrdaVKEJmzsaGLSvcwCgYIKoZIzj0EAwIDRwAwRAIgB+ZU2g6gWrKuEZ+Hxbb/ad4lvvigtwjz +RM4q3wghDDcCIC0mA6AFvWvR9lz4ZcyGbbOcNEhjhAnFjXca4syc4XR7 +-----END CERTIFICATE----- + +Trustwave Global ECC P384 Certification Authority +================================================= +-----BEGIN CERTIFICATE----- +MIICnTCCAiSgAwIBAgIMCL2Fl2yZJ6SAaEc7MAoGCCqGSM49BAMDMIGRMQswCQYDVQQGEwJVUzER +MA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0d2F2ZSBI +b2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBFQ0MgUDM4NCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMxOTM2NDNaFw00MjA4MjMxOTM2NDNaMIGRMQswCQYD +VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRy +dXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBFQ0MgUDM4 +NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTB2MBAGByqGSM49AgEGBSuBBAAiA2IABGvaDXU1CDFH +Ba5FmVXxERMuSvgQMSOjfoPTfygIOiYaOs+Xgh+AtycJj9GOMMQKmw6sWASr9zZ9lCOkmwqKi6vr +/TklZvFe/oyujUF5nQlgziip04pt89ZF1PKYhDhloKNDMEEwDwYDVR0TAQH/BAUwAwEB/zAPBgNV +HQ8BAf8EBQMDBwYAMB0GA1UdDgQWBBRVqYSJ0sEyvRjLbKYHTsjnnb6CkDAKBggqhkjOPQQDAwNn +ADBkAjA3AZKXRRJ+oPM+rRk6ct30UJMDEr5E0k9BpIycnR+j9sKS50gU/k6bpZFXrsY3crsCMGcl +CrEMXu6pY5Jv5ZAL/mYiykf9ijH3g/56vxC+GCsej/YpHpRZ744hN8tRmKVuSw== +-----END CERTIFICATE----- + +NAVER Global Root Certification Authority +========================================= +-----BEGIN CERTIFICATE----- +MIIFojCCA4qgAwIBAgIUAZQwHqIL3fXFMyqxQ0Rx+NZQTQ0wDQYJKoZIhvcNAQEMBQAwaTELMAkG +A1UEBhMCS1IxJjAkBgNVBAoMHU5BVkVSIEJVU0lORVNTIFBMQVRGT1JNIENvcnAuMTIwMAYDVQQD +DClOQVZFUiBHbG9iYWwgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MTgwODU4 +NDJaFw0zNzA4MTgyMzU5NTlaMGkxCzAJBgNVBAYTAktSMSYwJAYDVQQKDB1OQVZFUiBCVVNJTkVT +UyBQTEFURk9STSBDb3JwLjEyMDAGA1UEAwwpTkFWRVIgR2xvYmFsIFJvb3QgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC21PGTXLVAiQqrDZBb +UGOukJR0F0Vy1ntlWilLp1agS7gvQnXp2XskWjFlqxcX0TM62RHcQDaH38dq6SZeWYp34+hInDEW ++j6RscrJo+KfziFTowI2MMtSAuXaMl3Dxeb57hHHi8lEHoSTGEq0n+USZGnQJoViAbbJAh2+g1G7 +XNr4rRVqmfeSVPc0W+m/6imBEtRTkZazkVrd/pBzKPswRrXKCAfHcXLJZtM0l/aM9BhK4dA9WkW2 +aacp+yPOiNgSnABIqKYPszuSjXEOdMWLyEz59JuOuDxp7W87UC9Y7cSw0BwbagzivESq2M0UXZR4 +Yb8ObtoqvC8MC3GmsxY/nOb5zJ9TNeIDoKAYv7vxvvTWjIcNQvcGufFt7QSUqP620wbGQGHfnZ3z +VHbOUzoBppJB7ASjjw2i1QnK1sua8e9DXcCrpUHPXFNwcMmIpi3Ua2FzUCaGYQ5fG8Ir4ozVu53B +A0K6lNpfqbDKzE0K70dpAy8i+/Eozr9dUGWokG2zdLAIx6yo0es+nPxdGoMuK8u180SdOqcXYZai +cdNwlhVNt0xz7hlcxVs+Qf6sdWA7G2POAN3aCJBitOUt7kinaxeZVL6HSuOpXgRM6xBtVNbv8ejy +YhbLgGvtPe31HzClrkvJE+2KAQHJuFFYwGY6sWZLxNUxAmLpdIQM201GLQIDAQABo0IwQDAdBgNV +HQ4EFgQU0p+I36HNLL3s9TsBAZMzJ7LrYEswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB +Af8wDQYJKoZIhvcNAQEMBQADggIBADLKgLOdPVQG3dLSLvCkASELZ0jKbY7gyKoNqo0hV4/GPnrK +21HUUrPUloSlWGB/5QuOH/XcChWB5Tu2tyIvCZwTFrFsDDUIbatjcu3cvuzHV+YwIHHW1xDBE1UB +jCpD5EHxzzp6U5LOogMFDTjfArsQLtk70pt6wKGm+LUx5vR1yblTmXVHIloUFcd4G7ad6Qz4G3bx +hYTeodoS76TiEJd6eN4MUZeoIUCLhr0N8F5OSza7OyAfikJW4Qsav3vQIkMsRIz75Sq0bBwcupTg +E34h5prCy8VCZLQelHsIJchxzIdFV4XTnyliIoNRlwAYl3dqmJLJfGBs32x9SuRwTMKeuB330DTH +D8z7p/8Dvq1wkNoL3chtl1+afwkyQf3NosxabUzyqkn+Zvjp2DXrDige7kgvOtB5CTh8piKCk5XQ +A76+AqAF3SAi428diDRgxuYKuQl1C/AH6GmWNcf7I4GOODm4RStDeKLRLBT/DShycpWbXgnbiUSY +qqFJu3FS8r/2/yehNq+4tneI3TqkbZs0kNwUXTC/t+sX5Ie3cdCh13cV1ELX8vMxmV2b3RZtP+oG +I/hGoiLtk/bdmuYqh7GYVPEi92tF4+KOdh2ajcQGjTa3FPOdVGm3jjzVpG2Tgbet9r1ke8LJaDmg +kpzNNIaRkPpkUZ3+/uul9XXeifdy +-----END CERTIFICATE----- + +AC RAIZ FNMT-RCM SERVIDORES SEGUROS +=================================== +-----BEGIN CERTIFICATE----- +MIICbjCCAfOgAwIBAgIQYvYybOXE42hcG2LdnC6dlTAKBggqhkjOPQQDAzB4MQswCQYDVQQGEwJF +UzERMA8GA1UECgwIRk5NVC1SQ00xDjAMBgNVBAsMBUNlcmVzMRgwFgYDVQRhDA9WQVRFUy1RMjgy +NjAwNEoxLDAqBgNVBAMMI0FDIFJBSVogRk5NVC1SQ00gU0VSVklET1JFUyBTRUdVUk9TMB4XDTE4 +MTIyMDA5MzczM1oXDTQzMTIyMDA5MzczM1oweDELMAkGA1UEBhMCRVMxETAPBgNVBAoMCEZOTVQt +UkNNMQ4wDAYDVQQLDAVDZXJlczEYMBYGA1UEYQwPVkFURVMtUTI4MjYwMDRKMSwwKgYDVQQDDCNB +QyBSQUlaIEZOTVQtUkNNIFNFUlZJRE9SRVMgU0VHVVJPUzB2MBAGByqGSM49AgEGBSuBBAAiA2IA +BPa6V1PIyqvfNkpSIeSX0oNnnvBlUdBeh8dHsVnyV0ebAAKTRBdp20LHsbI6GA60XYyzZl2hNPk2 +LEnb80b8s0RpRBNm/dfF/a82Tc4DTQdxz69qBdKiQ1oKUm8BA06Oi6NCMEAwDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFAG5L++/EYZg8k/QQW6rcx/n0m5JMAoGCCqG +SM49BAMDA2kAMGYCMQCuSuMrQMN0EfKVrRYj3k4MGuZdpSRea0R7/DjiT8ucRRcRTBQnJlU5dUoD +zBOQn5ICMQD6SmxgiHPz7riYYqnOK8LZiqZwMR2vsJRM60/G49HzYqc8/5MuB1xJAWdpEgJyv+c= +-----END CERTIFICATE----- + +GlobalSign Root R46 +=================== +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgISEdK7udcjGJ5AXwqdLdDfJWfRMA0GCSqGSIb3DQEBDAUAMEYxCzAJBgNV +BAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQDExNHbG9iYWxTaWduIFJv +b3QgUjQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAX +BgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBSNDYwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCsrHQy6LNl5brtQyYdpokNRbopiLKkHWPd08Es +CVeJOaFV6Wc0dwxu5FUdUiXSE2te4R2pt32JMl8Nnp8semNgQB+msLZ4j5lUlghYruQGvGIFAha/ +r6gjA7aUD7xubMLL1aa7DOn2wQL7Id5m3RerdELv8HQvJfTqa1VbkNud316HCkD7rRlr+/fKYIje +2sGP1q7Vf9Q8g+7XFkyDRTNrJ9CG0Bwta/OrffGFqfUo0q3v84RLHIf8E6M6cqJaESvWJ3En7YEt +bWaBkoe0G1h6zD8K+kZPTXhc+CtI4wSEy132tGqzZfxCnlEmIyDLPRT5ge1lFgBPGmSXZgjPjHvj +K8Cd+RTyG/FWaha/LIWFzXg4mutCagI0GIMXTpRW+LaCtfOW3T3zvn8gdz57GSNrLNRyc0NXfeD4 +12lPFzYE+cCQYDdF3uYM2HSNrpyibXRdQr4G9dlkbgIQrImwTDsHTUB+JMWKmIJ5jqSngiCNI/on +ccnfxkF0oE32kRbcRoxfKWMxWXEM2G/CtjJ9++ZdU6Z+Ffy7dXxd7Pj2Fxzsx2sZy/N78CsHpdls +eVR2bJ0cpm4O6XkMqCNqo98bMDGfsVR7/mrLZqrcZdCinkqaByFrgY/bxFn63iLABJzjqls2k+g9 +vXqhnQt2sQvHnf3PmKgGwvgqo6GDoLclcqUC4wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA1yrc4GHqMywptWU4jaWSf8FmSwwDQYJKoZIhvcNAQEM +BQADggIBAHx47PYCLLtbfpIrXTncvtgdokIzTfnvpCo7RGkerNlFo048p9gkUbJUHJNOxO97k4Vg +JuoJSOD1u8fpaNK7ajFxzHmuEajwmf3lH7wvqMxX63bEIaZHU1VNaL8FpO7XJqti2kM3S+LGteWy +gxk6x9PbTZ4IevPuzz5i+6zoYMzRx6Fcg0XERczzF2sUyQQCPtIkpnnpHs6i58FZFZ8d4kuaPp92 +CC1r2LpXFNqD6v6MVenQTqnMdzGxRBF6XLE+0xRFFRhiJBPSy03OXIPBNvIQtQ6IbbjhVp+J3pZm +OUdkLG5NrmJ7v2B0GbhWrJKsFjLtrWhV/pi60zTe9Mlhww6G9kuEYO4Ne7UyWHmRVSyBQ7N0H3qq +JZ4d16GLuc1CLgSkZoNNiTW2bKg2SnkheCLQQrzRQDGQob4Ez8pn7fXwgNNgyYMqIgXQBztSvwye +qiv5u+YfjyW6hY0XHgL+XVAEV8/+LbzvXMAaq7afJMbfc2hIkCwU9D9SGuTSyxTDYWnP4vkYxboz +nxSjBF25cfe1lNj2M8FawTSLfJvdkzrnE6JwYZ+vj+vYxXX4M2bUdGc6N3ec592kD3ZDZopD8p/7 +DEJ4Y9HiD2971KE9dJeFt0g5QdYg/NA6s/rob8SKunE3vouXsXgxT7PntgMTzlSdriVZzH81Xwj3 +QEUxeCp6 +-----END CERTIFICATE----- + +GlobalSign Root E46 +=================== +-----BEGIN CERTIFICATE----- +MIICCzCCAZGgAwIBAgISEdK7ujNu1LzmJGjFDYQdmOhDMAoGCCqGSM49BAMDMEYxCzAJBgNVBAYT +AkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQDExNHbG9iYWxTaWduIFJvb3Qg +RTQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNV +BAoTEEdsb2JhbFNpZ24gbnYtc2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBFNDYwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAAScDrHPt+ieUnd1NPqlRqetMhkytAepJ8qUuwzSChDH2omwlwxwEwkB +jtjqR+q+soArzfwoDdusvKSGN+1wCAB16pMLey5SnCNoIwZD7JIvU4Tb+0cUB+hflGddyXqBPCCj +QjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQxCpCPtsad0kRL +gLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ7Zvvi5QCkxeCmb6zniz2C5GMn0oUsfZk +vLtoURMMA/cVi4RguYv/Uo7njLwcAjA8+RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+ +CAezNIm8BZ/3Hobui3A= +-----END CERTIFICATE----- + +GLOBALTRUST 2020 +================ +-----BEGIN CERTIFICATE----- +MIIFgjCCA2qgAwIBAgILWku9WvtPilv6ZeUwDQYJKoZIhvcNAQELBQAwTTELMAkGA1UEBhMCQVQx +IzAhBgNVBAoTGmUtY29tbWVyY2UgbW9uaXRvcmluZyBHbWJIMRkwFwYDVQQDExBHTE9CQUxUUlVT +VCAyMDIwMB4XDTIwMDIxMDAwMDAwMFoXDTQwMDYxMDAwMDAwMFowTTELMAkGA1UEBhMCQVQxIzAh +BgNVBAoTGmUtY29tbWVyY2UgbW9uaXRvcmluZyBHbWJIMRkwFwYDVQQDExBHTE9CQUxUUlVTVCAy +MDIwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAri5WrRsc7/aVj6B3GyvTY4+ETUWi +D59bRatZe1E0+eyLinjF3WuvvcTfk0Uev5E4C64OFudBc/jbu9G4UeDLgztzOG53ig9ZYybNpyrO +VPu44sB8R85gfD+yc/LAGbaKkoc1DZAoouQVBGM+uq/ufF7MpotQsjj3QWPKzv9pj2gOlTblzLmM +CcpL3TGQlsjMH/1WljTbjhzqLL6FLmPdqqmV0/0plRPwyJiT2S0WR5ARg6I6IqIoV6Lr/sCMKKCm +fecqQjuCgGOlYx8ZzHyyZqjC0203b+J+BlHZRYQfEs4kUmSFC0iAToexIiIwquuuvuAC4EDosEKA +A1GqtH6qRNdDYfOiaxaJSaSjpCuKAsR49GiKweR6NrFvG5Ybd0mN1MkGco/PU+PcF4UgStyYJ9OR +JitHHmkHr96i5OTUawuzXnzUJIBHKWk7buis/UDr2O1xcSvy6Fgd60GXIsUf1DnQJ4+H4xj04KlG +DfV0OoIu0G4skaMxXDtG6nsEEFZegB31pWXogvziB4xiRfUg3kZwhqG8k9MedKZssCz3AwyIDMvU +clOGvGBG85hqwvG/Q/lwIHfKN0F5VVJjjVsSn8VoxIidrPIwq7ejMZdnrY8XD2zHc+0klGvIg5rQ +mjdJBKuxFshsSUktq6HQjJLyQUp5ISXbY9e2nKd+Qmn7OmMCAwEAAaNjMGEwDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFNwuH9FhN3nkq9XVsxJxaD1qaJwiMB8GA1Ud +IwQYMBaAFNwuH9FhN3nkq9XVsxJxaD1qaJwiMA0GCSqGSIb3DQEBCwUAA4ICAQCR8EICaEDuw2jA +VC/f7GLDw56KoDEoqoOOpFaWEhCGVrqXctJUMHytGdUdaG/7FELYjQ7ztdGl4wJCXtzoRlgHNQIw +4Lx0SsFDKv/bGtCwr2zD/cuz9X9tAy5ZVp0tLTWMstZDFyySCstd6IwPS3BD0IL/qMy/pJTAvoe9 +iuOTe8aPmxadJ2W8esVCgmxcB9CpwYhgROmYhRZf+I/KARDOJcP5YBugxZfD0yyIMaK9MOzQ0MAS +8cE54+X1+NZK3TTN+2/BT+MAi1bikvcoskJ3ciNnxz8RFbLEAwW+uxF7Cr+obuf/WEPPm2eggAe2 +HcqtbepBEX4tdJP7wry+UUTF72glJ4DjyKDUEuzZpTcdN3y0kcra1LGWge9oXHYQSa9+pTeAsRxS +vTOBTI/53WXZFM2KJVj04sWDpQmQ1GwUY7VA3+vA/MRYfg0UFodUJ25W5HCEuGwyEn6CMUO+1918 +oa2u1qsgEu8KwxCMSZY13At1XrFP1U80DhEgB3VDRemjEdqso5nCtnkn4rnvyOL2NSl6dPrFf4IF +YqYK6miyeUcGbvJXqBUzxvd4Sj1Ce2t+/vdG6tHrju+IaFvowdlxfv1k7/9nR4hYJS8+hge9+6jl +gqispdNpQ80xiEmEU5LAsTkbOYMBMMTyqfrQA71yN2BWHzZ8vTmR9W0Nv3vXkg== +-----END CERTIFICATE----- + +ANF Secure Server Root CA +========================= +-----BEGIN CERTIFICATE----- +MIIF7zCCA9egAwIBAgIIDdPjvGz5a7EwDQYJKoZIhvcNAQELBQAwgYQxEjAQBgNVBAUTCUc2MzI4 +NzUxMDELMAkGA1UEBhMCRVMxJzAlBgNVBAoTHkFORiBBdXRvcmlkYWQgZGUgQ2VydGlmaWNhY2lv +bjEUMBIGA1UECxMLQU5GIENBIFJhaXoxIjAgBgNVBAMTGUFORiBTZWN1cmUgU2VydmVyIFJvb3Qg +Q0EwHhcNMTkwOTA0MTAwMDM4WhcNMzkwODMwMTAwMDM4WjCBhDESMBAGA1UEBRMJRzYzMjg3NTEw +MQswCQYDVQQGEwJFUzEnMCUGA1UEChMeQU5GIEF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uMRQw +EgYDVQQLEwtBTkYgQ0EgUmFpejEiMCAGA1UEAxMZQU5GIFNlY3VyZSBTZXJ2ZXIgUm9vdCBDQTCC +AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANvrayvmZFSVgpCjcqQZAZ2cC4Ffc0m6p6zz +BE57lgvsEeBbphzOG9INgxwruJ4dfkUyYA8H6XdYfp9qyGFOtibBTI3/TO80sh9l2Ll49a2pcbnv +T1gdpd50IJeh7WhM3pIXS7yr/2WanvtH2Vdy8wmhrnZEE26cLUQ5vPnHO6RYPUG9tMJJo8gN0pcv +B2VSAKduyK9o7PQUlrZXH1bDOZ8rbeTzPvY1ZNoMHKGESy9LS+IsJJ1tk0DrtSOOMspvRdOoiXse +zx76W0OLzc2oD2rKDF65nkeP8Nm2CgtYZRczuSPkdxl9y0oukntPLxB3sY0vaJxizOBQ+OyRp1RM +VwnVdmPF6GUe7m1qzwmd+nxPrWAI/VaZDxUse6mAq4xhj0oHdkLePfTdsiQzW7i1o0TJrH93PB0j +7IKppuLIBkwC/qxcmZkLLxCKpvR/1Yd0DVlJRfbwcVw5Kda/SiOL9V8BY9KHcyi1Swr1+KuCLH5z +JTIdC2MKF4EA/7Z2Xue0sUDKIbvVgFHlSFJnLNJhiQcND85Cd8BEc5xEUKDbEAotlRyBr+Qc5RQe +8TZBAQIvfXOn3kLMTOmJDVb3n5HUA8ZsyY/b2BzgQJhdZpmYgG4t/wHFzstGH6wCxkPmrqKEPMVO +Hj1tyRRM4y5Bu8o5vzY8KhmqQYdOpc5LMnndkEl/AgMBAAGjYzBhMB8GA1UdIwQYMBaAFJxf0Gxj +o1+TypOYCK2Mh6UsXME3MB0GA1UdDgQWBBScX9BsY6Nfk8qTmAitjIelLFzBNzAOBgNVHQ8BAf8E +BAMCAYYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEATh65isagmD9uw2nAalxJ +UqzLK114OMHVVISfk/CHGT0sZonrDUL8zPB1hT+L9IBdeeUXZ701guLyPI59WzbLWoAAKfLOKyzx +j6ptBZNscsdW699QIyjlRRA96Gejrw5VD5AJYu9LWaL2U/HANeQvwSS9eS9OICI7/RogsKQOLHDt +dD+4E5UGUcjohybKpFtqFiGS3XNgnhAY3jyB6ugYw3yJ8otQPr0R4hUDqDZ9MwFsSBXXiJCZBMXM +5gf0vPSQ7RPi6ovDj6MzD8EpTBNO2hVWcXNyglD2mjN8orGoGjR0ZVzO0eurU+AagNjqOknkJjCb +5RyKqKkVMoaZkgoQI1YS4PbOTOK7vtuNknMBZi9iPrJyJ0U27U1W45eZ/zo1PqVUSlJZS2Db7v54 +EX9K3BR5YLZrZAPbFYPhor72I5dQ8AkzNqdxliXzuUJ92zg/LFis6ELhDtjTO0wugumDLmsx2d1H +hk9tl5EuT+IocTUW0fJz/iUrB0ckYyfI+PbZa/wSMVYIwFNCr5zQM378BvAxRAMU8Vjq8moNqRGy +g77FGr8H6lnco4g175x2MjxNBiLOFeXdntiP2t7SxDnlF4HPOEfrf4htWRvfn0IUrn7PqLBmZdo3 +r5+qPeoott7VMVgWglvquxl1AnMaykgaIZOQCo6ThKd9OyMYkomgjaw= +-----END CERTIFICATE----- + +Certum EC-384 CA +================ +-----BEGIN CERTIFICATE----- +MIICZTCCAeugAwIBAgIQeI8nXIESUiClBNAt3bpz9DAKBggqhkjOPQQDAzB0MQswCQYDVQQGEwJQ +TDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkxGTAXBgNVBAMTEENlcnR1bSBFQy0zODQgQ0EwHhcNMTgwMzI2 +MDcyNDU0WhcNNDMwMzI2MDcyNDU0WjB0MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERh +dGEgU3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkx +GTAXBgNVBAMTEENlcnR1bSBFQy0zODQgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATEKI6rGFtq +vm5kN2PkzeyrOvfMobgOgknXhimfoZTy42B4mIF4Bk3y7JoOV2CDn7TmFy8as10CW4kjPMIRBSqn +iBMY81CE1700LCeJVf/OTOffph8oxPBUw7l8t1Ot68KjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD +VR0OBBYEFI0GZnQkdjrzife81r1HfS+8EF9LMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNo +ADBlAjADVS2m5hjEfO/JUG7BJw+ch69u1RsIGL2SKcHvlJF40jocVYli5RsJHrpka/F2tNQCMQC0 +QoSZ/6vnnvuRlydd3LBbMHHOXjgaatkl5+r3YZJW+OraNsKHZZYuciUvf9/DE8k= +-----END CERTIFICATE----- + +Certum Trusted Root CA +====================== +-----BEGIN CERTIFICATE----- +MIIFwDCCA6igAwIBAgIQHr9ZULjJgDdMBvfrVU+17TANBgkqhkiG9w0BAQ0FADB6MQswCQYDVQQG +EwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0g +Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkxHzAdBgNVBAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0Ew +HhcNMTgwMzE2MTIxMDEzWhcNNDMwMzE2MTIxMDEzWjB6MQswCQYDVQQGEwJQTDEhMB8GA1UEChMY +QXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkxHzAdBgNVBAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQDRLY67tzbqbTeRn06TpwXkKQMlzhyC93yZn0EGze2jusDbCSzBfN8p +fktlL5On1AFrAygYo9idBcEq2EXxkd7fO9CAAozPOA/qp1x4EaTByIVcJdPTsuclzxFUl6s1wB52 +HO8AU5853BSlLCIls3Jy/I2z5T4IHhQqNwuIPMqw9MjCoa68wb4pZ1Xi/K1ZXP69VyywkI3C7Te2 +fJmItdUDmj0VDT06qKhF8JVOJVkdzZhpu9PMMsmN74H+rX2Ju7pgE8pllWeg8xn2A1bUatMn4qGt +g/BKEiJ3HAVz4hlxQsDsdUaakFjgao4rpUYwBI4Zshfjvqm6f1bxJAPXsiEodg42MEx51UGamqi4 +NboMOvJEGyCI98Ul1z3G4z5D3Yf+xOr1Uz5MZf87Sst4WmsXXw3Hw09Omiqi7VdNIuJGmj8PkTQk +fVXjjJU30xrwCSss0smNtA0Aq2cpKNgB9RkEth2+dv5yXMSFytKAQd8FqKPVhJBPC/PgP5sZ0jeJ +P/J7UhyM9uH3PAeXjA6iWYEMspA90+NZRu0PqafegGtaqge2Gcu8V/OXIXoMsSt0Puvap2ctTMSY +njYJdmZm/Bo/6khUHL4wvYBQv3y1zgD2DGHZ5yQD4OMBgQ692IU0iL2yNqh7XAjlRICMb/gv1SHK +HRzQ+8S1h9E6Tsd2tTVItQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSM+xx1 +vALTn04uSNn5YFSqxLNP+jAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQENBQADggIBAEii1QAL +LtA/vBzVtVRJHlpr9OTy4EA34MwUe7nJ+jW1dReTagVphZzNTxl4WxmB82M+w85bj/UvXgF2Ez8s +ALnNllI5SW0ETsXpD4YN4fqzX4IS8TrOZgYkNCvozMrnadyHncI013nR03e4qllY/p0m+jiGPp2K +h2RX5Rc64vmNueMzeMGQ2Ljdt4NR5MTMI9UGfOZR0800McD2RrsLrfw9EAUqO0qRJe6M1ISHgCq8 +CYyqOhNf6DR5UMEQGfnTKB7U0VEwKbOukGfWHwpjscWpxkIxYxeU72nLL/qMFH3EQxiJ2fAyQOaA +4kZf5ePBAFmo+eggvIksDkc0C+pXwlM2/KfUrzHN/gLldfq5Jwn58/U7yn2fqSLLiMmq0Uc9Nneo +WWRrJ8/vJ8HjJLWG965+Mk2weWjROeiQWMODvA8s1pfrzgzhIMfatz7DP78v3DSk+yshzWePS/Tj +6tQ/50+6uaWTRRxmHyH6ZF5v4HaUMst19W7l9o/HuKTMqJZ9ZPskWkoDbGs4xugDQ5r3V7mzKWmT +OPQD8rv7gmsHINFSH5pkAnuYZttcTVoP0ISVoDwUQwbKytu4QTbaakRnh6+v40URFWkIsr4WOZck +bxJF0WddCajJFdr60qZfE2Efv4WstK2tBZQIgx51F9NxO5NQI1mg7TyRVJ12AMXDuDjb +-----END CERTIFICATE----- + +TunTrust Root CA +================ +-----BEGIN CERTIFICATE----- +MIIFszCCA5ugAwIBAgIUEwLV4kBMkkaGFmddtLu7sms+/BMwDQYJKoZIhvcNAQELBQAwYTELMAkG +A1UEBhMCVE4xNzA1BgNVBAoMLkFnZW5jZSBOYXRpb25hbGUgZGUgQ2VydGlmaWNhdGlvbiBFbGVj +dHJvbmlxdWUxGTAXBgNVBAMMEFR1blRydXN0IFJvb3QgQ0EwHhcNMTkwNDI2MDg1NzU2WhcNNDQw +NDI2MDg1NzU2WjBhMQswCQYDVQQGEwJUTjE3MDUGA1UECgwuQWdlbmNlIE5hdGlvbmFsZSBkZSBD +ZXJ0aWZpY2F0aW9uIEVsZWN0cm9uaXF1ZTEZMBcGA1UEAwwQVHVuVHJ1c3QgUm9vdCBDQTCCAiIw +DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMPN0/y9BFPdDCA61YguBUtB9YOCfvdZn56eY+hz +2vYGqU8ftPkLHzmMmiDQfgbU7DTZhrx1W4eI8NLZ1KMKsmwb60ksPqxd2JQDoOw05TDENX37Jk0b +bjBU2PWARZw5rZzJJQRNmpA+TkBuimvNKWfGzC3gdOgFVwpIUPp6Q9p+7FuaDmJ2/uqdHYVy7BG7 +NegfJ7/Boce7SBbdVtfMTqDhuazb1YMZGoXRlJfXyqNlC/M4+QKu3fZnz8k/9YosRxqZbwUN/dAd +gjH8KcwAWJeRTIAAHDOFli/LQcKLEITDCSSJH7UP2dl3RxiSlGBcx5kDPP73lad9UKGAwqmDrViW +VSHbhlnUr8a83YFuB9tgYv7sEG7aaAH0gxupPqJbI9dkxt/con3YS7qC0lH4Zr8GRuR5KiY2eY8f +Tpkdso8MDhz/yV3A/ZAQprE38806JG60hZC/gLkMjNWb1sjxVj8agIl6qeIbMlEsPvLfe/ZdeikZ +juXIvTZxi11Mwh0/rViizz1wTaZQmCXcI/m4WEEIcb9PuISgjwBUFfyRbVinljvrS5YnzWuioYas +DXxU5mZMZl+QviGaAkYt5IPCgLnPSz7ofzwB7I9ezX/SKEIBlYrilz0QIX32nRzFNKHsLA4KUiwS +VXAkPcvCFDVDXSdOvsC9qnyW5/yeYa1E0wCXAgMBAAGjYzBhMB0GA1UdDgQWBBQGmpsfU33x9aTI +04Y+oXNZtPdEITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFAaamx9TffH1pMjThj6hc1m0 +90QhMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAqgVutt0Vyb+zxiD2BkewhpMl +0425yAA/l/VSJ4hxyXT968pk21vvHl26v9Hr7lxpuhbI87mP0zYuQEkHDVneixCwSQXi/5E/S7fd +Ao74gShczNxtr18UnH1YeA32gAm56Q6XKRm4t+v4FstVEuTGfbvE7Pi1HE4+Z7/FXxttbUcoqgRY +YdZ2vyJ/0Adqp2RT8JeNnYA/u8EH22Wv5psymsNUk8QcCMNE+3tjEUPRahphanltkE8pjkcFwRJp +adbGNjHh/PqAulxPxOu3Mqz4dWEX1xAZufHSCe96Qp1bWgvUxpVOKs7/B9dPfhgGiPEZtdmYu65x +xBzndFlY7wyJz4sfdZMaBBSSSFCp61cpABbjNhzI+L/wM9VBD8TMPN3pM0MBkRArHtG5Xc0yGYuP +jCB31yLEQtyEFpslbei0VXF/sHyz03FJuc9SpAQ/3D2gu68zngowYI7bnV2UqL1g52KAdoGDDIzM +MEZJ4gzSqK/rYXHv5yJiqfdcZGyfFoxnNidF9Ql7v/YQCvGwjVRDjAS6oz/v4jXH+XTgbzRB0L9z +ZVcg+ZtnemZoJE6AZb0QmQZZ8mWvuMZHu/2QeItBcy6vVR/cO5JyboTT0GFMDcx2V+IthSIVNg3r +AZ3r2OvEhJn7wAzMMujjd9qDRIueVSjAi1jTkD5OGwDxFa2DK5o= +-----END CERTIFICATE----- + +HARICA TLS RSA Root CA 2021 +=========================== +-----BEGIN CERTIFICATE----- +MIIFpDCCA4ygAwIBAgIQOcqTHO9D88aOk8f0ZIk4fjANBgkqhkiG9w0BAQsFADBsMQswCQYDVQQG +EwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9u +cyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBSU0EgUm9vdCBDQSAyMDIxMB4XDTIxMDIxOTEwNTUz +OFoXDTQ1MDIxMzEwNTUzN1owbDELMAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRl +bWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgUlNB +IFJvb3QgQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAIvC569lmwVnlskN +JLnQDmT8zuIkGCyEf3dRywQRNrhe7Wlxp57kJQmXZ8FHws+RFjZiPTgE4VGC/6zStGndLuwRo0Xu +a2s7TL+MjaQenRG56Tj5eg4MmOIjHdFOY9TnuEFE+2uva9of08WRiFukiZLRgeaMOVig1mlDqa2Y +Ulhu2wr7a89o+uOkXjpFc5gH6l8Cct4MpbOfrqkdtx2z/IpZ525yZa31MJQjB/OCFks1mJxTuy/K +5FrZx40d/JiZ+yykgmvwKh+OC19xXFyuQnspiYHLA6OZyoieC0AJQTPb5lh6/a6ZcMBaD9YThnEv +dmn8kN3bLW7R8pv1GmuebxWMevBLKKAiOIAkbDakO/IwkfN4E8/BPzWr8R0RI7VDIp4BkrcYAuUR +0YLbFQDMYTfBKnya4dC6s1BG7oKsnTH4+yPiAwBIcKMJJnkVU2DzOFytOOqBAGMUuTNe3QvboEUH +GjMJ+E20pwKmafTCWQWIZYVWrkvL4N48fS0ayOn7H6NhStYqE613TBoYm5EPWNgGVMWX+Ko/IIqm +haZ39qb8HOLubpQzKoNQhArlT4b4UEV4AIHrW2jjJo3Me1xR9BQsQL4aYB16cmEdH2MtiKrOokWQ +CPxrvrNQKlr9qEgYRtaQQJKQCoReaDH46+0N0x3GfZkYVVYnZS6NRcUk7M7jAgMBAAGjQjBAMA8G +A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFApII6ZgpJIKM+qTW8VX6iVNvRLuMA4GA1UdDwEB/wQE +AwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAPpBIqm5iFSVmewzVjIuJndftTgfvnNAUX15QvWiWkKQU +EapobQk1OUAJ2vQJLDSle1mESSmXdMgHHkdt8s4cUCbjnj1AUz/3f5Z2EMVGpdAgS1D0NTsY9FVq +QRtHBmg8uwkIYtlfVUKqrFOFrJVWNlar5AWMxajaH6NpvVMPxP/cyuN+8kyIhkdGGvMA9YCRotxD +QpSbIPDRzbLrLFPCU3hKTwSUQZqPJzLB5UkZv/HywouoCjkxKLR9YjYsTewfM7Z+d21+UPCfDtcR +j88YxeMn/ibvBZ3PzzfF0HvaO7AWhAw6k9a+F9sPPg4ZeAnHqQJyIkv3N3a6dcSFA1pj1bF1BcK5 +vZStjBWZp5N99sXzqnTPBIWUmAD04vnKJGW/4GKvyMX6ssmeVkjaef2WdhW+o45WxLM0/L5H9MG0 +qPzVMIho7suuyWPEdr6sOBjhXlzPrjoiUevRi7PzKzMHVIf6tLITe7pTBGIBnfHAT+7hOtSLIBD6 +Alfm78ELt5BGnBkpjNxvoEppaZS3JGWg/6w/zgH7IS79aPib8qXPMThcFarmlwDB31qlpzmq6YR/ +PFGoOtmUW4y/Twhx5duoXNTSpv4Ao8YWxw/ogM4cKGR0GQjTQuPOAF1/sdwTsOEFy9EgqoZ0njnn +kf3/W9b3raYvAwtt41dU63ZTGI0RmLo= +-----END CERTIFICATE----- + +HARICA TLS ECC Root CA 2021 +=========================== +-----BEGIN CERTIFICATE----- +MIICVDCCAdugAwIBAgIQZ3SdjXfYO2rbIvT/WeK/zjAKBggqhkjOPQQDAzBsMQswCQYDVQQGEwJH +UjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBD +QTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBFQ0MgUm9vdCBDQSAyMDIxMB4XDTIxMDIxOTExMDExMFoX +DTQ1MDIxMzExMDEwOVowbDELMAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWlj +IGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgRUNDIFJv +b3QgQ0EgMjAyMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABDgI/rGgltJ6rK9JOtDA4MM7KKrxcm1l +AEeIhPyaJmuqS7psBAqIXhfyVYf8MLA04jRYVxqEU+kw2anylnTDUR9YSTHMmE5gEYd103KUkE+b +ECUqqHgtvpBBWJAVcqeht6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUyRtTgRL+BNUW +0aq8mm+3oJUZbsowDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2cAMGQCMBHervjcToiwqfAi +rcJRQO9gcS3ujwLEXQNwSaSS6sUUiHCm0w2wqsosQJz76YJumgIwK0eaB8bRwoF8yguWGEEbo/Qw +CZ61IygNnxS2PFOiTAZpffpskcYqSUXm7LcT4Tps +-----END CERTIFICATE----- + +Autoridad de Certificacion Firmaprofesional CIF A62634068 +========================================================= +-----BEGIN CERTIFICATE----- +MIIGFDCCA/ygAwIBAgIIG3Dp0v+ubHEwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCRVMxQjBA +BgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1hcHJvZmVzaW9uYWwgQ0lGIEE2 +MjYzNDA2ODAeFw0xNDA5MjMxNTIyMDdaFw0zNjA1MDUxNTIyMDdaMFExCzAJBgNVBAYTAkVTMUIw +QAYDVQQDDDlBdXRvcmlkYWQgZGUgQ2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBB +NjI2MzQwNjgwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDD +Utd9thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQMcas9UX4P +B99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefGL9ItWY16Ck6WaVICqjaY +7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15iNA9wBj4gGFrO93IbJWyTdBSTo3OxDqqH +ECNZXyAFGUftaI6SEspd/NYrspI8IM/hX68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyI +plD9amML9ZMWGxmPsu2bm8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctX +MbScyJCyZ/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirjaEbsX +LZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/TKI8xWVvTyQKmtFLK +bpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF6NkBiDkal4ZkQdU7hwxu+g/GvUgU +vzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVhOSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMB0GA1Ud +DgQWBBRlzeurNR4APn7VdMActHNHDhpkLzASBgNVHRMBAf8ECDAGAQH/AgEBMIGmBgNVHSAEgZ4w +gZswgZgGBFUdIAAwgY8wLwYIKwYBBQUHAgEWI2h0dHA6Ly93d3cuZmlybWFwcm9mZXNpb25hbC5j +b20vY3BzMFwGCCsGAQUFBwICMFAeTgBQAGEAcwBlAG8AIABkAGUAIABsAGEAIABCAG8AbgBhAG4A +bwB2AGEAIAA0ADcAIABCAGEAcgBjAGUAbABvAG4AYQAgADAAOAAwADEANzAOBgNVHQ8BAf8EBAMC +AQYwDQYJKoZIhvcNAQELBQADggIBAHSHKAIrdx9miWTtj3QuRhy7qPj4Cx2Dtjqn6EWKB7fgPiDL +4QjbEwj4KKE1soCzC1HA01aajTNFSa9J8OA9B3pFE1r/yJfY0xgsfZb43aJlQ3CTkBW6kN/oGbDb +LIpgD7dvlAceHabJhfa9NPhAeGIQcDq+fUs5gakQ1JZBu/hfHAsdCPKxsIl68veg4MSPi3i1O1il +I45PVf42O+AMt8oqMEEgtIDNrvx2ZnOorm7hfNoD6JQg5iKj0B+QXSBTFCZX2lSX3xZEEAEeiGaP +cjiT3SC3NL7X8e5jjkd5KAb881lFJWAiMxujX6i6KtoaPc1A6ozuBRWV1aUsIC+nmCjuRfzxuIgA +LI9C2lHVnOUTaHFFQ4ueCyE8S1wF3BqfmI7avSKecs2tCsvMo2ebKHTEm9caPARYpoKdrcd7b/+A +lun4jWq9GJAd/0kakFI3ky88Al2CdgtR5xbHV/g4+afNmyJU72OwFW1TZQNKXkqgsqeOSQBZONXH +9IBk9W6VULgRfhVwOEqwf9DEMnDAGf/JOC0ULGb0QkTmVXYbgBVX/8Cnp6o5qtjTcNAuuuuUavpf +NIbnYrX9ivAwhZTJryQCL2/W3Wf+47BVTwSYT6RBVuKT0Gro1vP7ZeDOdcQxWQzugsgMYDNKGbqE +ZycPvEJdvSRUDewdcAZfpLz6IHxV +-----END CERTIFICATE----- + +vTrus ECC Root CA +================= +-----BEGIN CERTIFICATE----- +MIICDzCCAZWgAwIBAgIUbmq8WapTvpg5Z6LSa6Q75m0c1towCgYIKoZIzj0EAwMwRzELMAkGA1UE +BhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4xGjAYBgNVBAMTEXZUcnVzIEVDQyBS +b290IENBMB4XDTE4MDczMTA3MjY0NFoXDTQzMDczMTA3MjY0NFowRzELMAkGA1UEBhMCQ04xHDAa +BgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4xGjAYBgNVBAMTEXZUcnVzIEVDQyBSb290IENBMHYw +EAYHKoZIzj0CAQYFK4EEACIDYgAEZVBKrox5lkqqHAjDo6LN/llWQXf9JpRCux3NCNtzslt188+c +ToL0v/hhJoVs1oVbcnDS/dtitN9Ti72xRFhiQgnH+n9bEOf+QP3A2MMrMudwpremIFUde4BdS49n +TPEQo0IwQDAdBgNVHQ4EFgQUmDnNvtiyjPeyq+GtJK97fKHbH88wDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwMDaAAwZQIwV53dVvHH4+m4SVBrm2nDb+zDfSXkV5UT +QJtS0zvzQBm8JsctBp61ezaf9SXUY2sAAjEA6dPGnlaaKsyh2j/IZivTWJwghfqrkYpwcBE4YGQL +YgmRWAD5Tfs0aNoJrSEGGJTO +-----END CERTIFICATE----- + +vTrus Root CA +============= +-----BEGIN CERTIFICATE----- +MIIFVjCCAz6gAwIBAgIUQ+NxE9izWRRdt86M/TX9b7wFjUUwDQYJKoZIhvcNAQELBQAwQzELMAkG +A1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4xFjAUBgNVBAMTDXZUcnVzIFJv +b3QgQ0EwHhcNMTgwNzMxMDcyNDA1WhcNNDMwNzMxMDcyNDA1WjBDMQswCQYDVQQGEwJDTjEcMBoG +A1UEChMTaVRydXNDaGluYSBDby4sTHRkLjEWMBQGA1UEAxMNdlRydXMgUm9vdCBDQTCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAL1VfGHTuB0EYgWgrmy3cLRB6ksDXhA/kFocizuwZots +SKYcIrrVQJLuM7IjWcmOvFjai57QGfIvWcaMY1q6n6MLsLOaXLoRuBLpDLvPbmyAhykUAyyNJJrI +ZIO1aqwTLDPxn9wsYTwaP3BVm60AUn/PBLn+NvqcwBauYv6WTEN+VRS+GrPSbcKvdmaVayqwlHeF +XgQPYh1jdfdr58tbmnDsPmcF8P4HCIDPKNsFxhQnL4Z98Cfe/+Z+M0jnCx5Y0ScrUw5XSmXX+6KA +YPxMvDVTAWqXcoKv8R1w6Jz1717CbMdHflqUhSZNO7rrTOiwCcJlwp2dCZtOtZcFrPUGoPc2BX70 +kLJrxLT5ZOrpGgrIDajtJ8nU57O5q4IikCc9Kuh8kO+8T/3iCiSn3mUkpF3qwHYw03dQ+A0Em5Q2 +AXPKBlim0zvc+gRGE1WKyURHuFE5Gi7oNOJ5y1lKCn+8pu8fA2dqWSslYpPZUxlmPCdiKYZNpGvu +/9ROutW04o5IWgAZCfEF2c6Rsffr6TlP9m8EQ5pV9T4FFL2/s1m02I4zhKOQUqqzApVg+QxMaPnu +1RcN+HFXtSXkKe5lXa/R7jwXC1pDxaWG6iSe4gUH3DRCEpHWOXSuTEGC2/KmSNGzm/MzqvOmwMVO +9fSddmPmAsYiS8GVP1BkLFTltvA8Kc9XAgMBAAGjQjBAMB0GA1UdDgQWBBRUYnBj8XWEQ1iO0RYg +scasGrz2iTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOC +AgEAKbqSSaet8PFww+SX8J+pJdVrnjT+5hpk9jprUrIQeBqfTNqK2uwcN1LgQkv7bHbKJAs5EhWd +nxEt/Hlk3ODg9d3gV8mlsnZwUKT+twpw1aA08XXXTUm6EdGz2OyC/+sOxL9kLX1jbhd47F18iMjr +jld22VkE+rxSH0Ws8HqA7Oxvdq6R2xCOBNyS36D25q5J08FsEhvMKar5CKXiNxTKsbhm7xqC5PD4 +8acWabfbqWE8n/Uxy+QARsIvdLGx14HuqCaVvIivTDUHKgLKeBRtRytAVunLKmChZwOgzoy8sHJn +xDHO2zTlJQNgJXtxmOTAGytfdELSS8VZCAeHvsXDf+eW2eHcKJfWjwXj9ZtOyh1QRwVTsMo554Wg +icEFOwE30z9J4nfrI8iIZjs9OXYhRvHsXyO466JmdXTBQPfYaJqT4i2pLr0cox7IdMakLXogqzu4 +sEb9b91fUlV1YvCXoHzXOP0l382gmxDPi7g4Xl7FtKYCNqEeXxzP4padKar9mK5S4fNBUvupLnKW +nyfjqnN9+BojZns7q2WwMgFLFT49ok8MKzWixtlnEjUwzXYuFrOZnk1PTi07NEPhmg4NpGaXutIc +SkwsKouLgU9xGqndXHt7CMUADTdA43x7VF8vhV929vensBxXVsFy6K2ir40zSbofitzmdHxghm+H +l3s= +-----END CERTIFICATE----- + +ISRG Root X2 +============ +-----BEGIN CERTIFICATE----- +MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQswCQYDVQQGEwJV +UzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElT +UkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVT +MSkwJwYDVQQKEyBJbnRlcm5ldCBTZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNS +RyBSb290IFgyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0H +ttwW+1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9ItgKbppb +d9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZIzj0EAwMDaAAwZQIwe3lORlCEwkSHRhtF +cP9Ymd70/aTSVaYgLXTWNLxBo1BfASdWtL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5 +U6VR5CmD1/iQMVtCnwr1/q4AaOeMSQ+2b1tbFfLn +-----END CERTIFICATE----- + +HiPKI Root CA - G1 +================== +-----BEGIN CERTIFICATE----- +MIIFajCCA1KgAwIBAgIQLd2szmKXlKFD6LDNdmpeYDANBgkqhkiG9w0BAQsFADBPMQswCQYDVQQG +EwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0ZC4xGzAZBgNVBAMMEkhpUEtJ +IFJvb3QgQ0EgLSBHMTAeFw0xOTAyMjIwOTQ2MDRaFw0zNzEyMzExNTU5NTlaME8xCzAJBgNVBAYT +AlRXMSMwIQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEbMBkGA1UEAwwSSGlQS0kg +Um9vdCBDQSAtIEcxMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9B5/UnMyDHPkvRN0 +o9QwqNCuS9i233VHZvR85zkEHmpwINJaR3JnVfSl6J3VHiGh8Ge6zCFovkRTv4354twvVcg3Px+k +wJyz5HdcoEb+d/oaoDjq7Zpy3iu9lFc6uux55199QmQ5eiY29yTw1S+6lZgRZq2XNdZ1AYDgr/SE +YYwNHl98h5ZeQa/rh+r4XfEuiAU+TCK72h8q3VJGZDnzQs7ZngyzsHeXZJzA9KMuH5UHsBffMNsA +GJZMoYFL3QRtU6M9/Aes1MU3guvklQgZKILSQjqj2FPseYlgSGDIcpJQ3AOPgz+yQlda22rpEZfd +hSi8MEyr48KxRURHH+CKFgeW0iEPU8DtqX7UTuybCeyvQqww1r/REEXgphaypcXTT3OUM3ECoWqj +1jOXTyFjHluP2cFeRXF3D4FdXyGarYPM+l7WjSNfGz1BryB1ZlpK9p/7qxj3ccC2HTHsOyDry+K4 +9a6SsvfhhEvyovKTmiKe0xRvNlS9H15ZFblzqMF8b3ti6RZsR1pl8w4Rm0bZ/W3c1pzAtH2lsN0/ +Vm+h+fbkEkj9Bn8SV7apI09bA8PgcSojt/ewsTu8mL3WmKgMa/aOEmem8rJY5AIJEzypuxC00jBF +8ez3ABHfZfjcK0NVvxaXxA/VLGGEqnKG/uY6fsI/fe78LxQ+5oXdUG+3Se0CAwEAAaNCMEAwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU8ncX+l6o/vY9cdVouslGDDjYr7AwDgYDVR0PAQH/BAQD +AgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBQUfB13HAE4/+qddRxosuej6ip0691x1TPOhwEmSKsxBHi +7zNKpiMdDg1H2DfHb680f0+BazVP6XKlMeJ45/dOlBhbQH3PayFUhuaVevvGyuqcSE5XCV0vrPSl +tJczWNWseanMX/mF+lLFjfiRFOs6DRfQUsJ748JzjkZ4Bjgs6FzaZsT0pPBWGTMpWmWSBUdGSquE +wx4noR8RkpkndZMPvDY7l1ePJlsMu5wP1G4wB9TcXzZoZjmDlicmisjEOf6aIW/Vcobpf2Lll07Q +JNBAsNB1CI69aO4I1258EHBGG3zgiLKecoaZAeO/n0kZtCW+VmWuF2PlHt/o/0elv+EmBYTksMCv +5wiZqAxeJoBF1PhoL5aPruJKHJwWDBNvOIf2u8g0X5IDUXlwpt/L9ZlNec1OvFefQ05rLisY+Gpz +jLrFNe85akEez3GoorKGB1s6yeHvP2UEgEcyRHCVTjFnanRbEEV16rCf0OY1/k6fi8wrkkVbbiVg +hUbN0aqwdmaTd5a+g744tiROJgvM7XpWGuDpWsZkrUx6AEhEL7lAuxM+vhV4nYWBSipX3tUZQ9rb +yltHhoMLP7YNdnhzeSJesYAfz77RP1YQmCuVh6EfnWQUYDksswBVLuT1sw5XxJFBAJw/6KXf6vb/ +yPCtbVKoF6ubYfwSUTXkJf2vqmqGOQ== +-----END CERTIFICATE----- + +GlobalSign ECC Root CA - R4 +=========================== +-----BEGIN CERTIFICATE----- +MIIB3DCCAYOgAwIBAgINAgPlfvU/k/2lCSGypjAKBggqhkjOPQQDAjBQMSQwIgYDVQQLExtHbG9i +YWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkds +b2JhbFNpZ24wHhcNMTIxMTEzMDAwMDAwWhcNMzgwMTE5MDMxNDA3WjBQMSQwIgYDVQQLExtHbG9i +YWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkds +b2JhbFNpZ24wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAS4xnnTj2wlDp8uORkcA6SumuU5BwkW +ymOxuYb4ilfBV85C+nOh92VC/x7BALJucw7/xyHlGKSq2XE/qNS5zowdo0IwQDAOBgNVHQ8BAf8E +BAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVLB7rUW44kB/+wpu+74zyTyjhNUwCgYI +KoZIzj0EAwIDRwAwRAIgIk90crlgr/HmnKAWBVBfw147bmF0774BxL4YSFlhgjICICadVGNA3jdg +UM/I2O2dgq43mLyjj0xMqTQrbO/7lZsm +-----END CERTIFICATE----- + +GTS Root R1 +=========== +-----BEGIN CERTIFICATE----- +MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQswCQYDVQQGEwJV +UzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3Qg +UjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UE +ChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0G +CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaM +f/vo27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7wCl7raKb0 +xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjwTcLCeoiKu7rPWRnWr4+w +B7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0PfyblqAj+lug8aJRT7oM6iCsVlgmy4HqMLnXW +nOunVmSPlk9orj2XwoSPwLxAwAtcvfaHszVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk +9+aCEI3oncKKiPo4Zor8Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zq +kUspzBmkMiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92wO1A +K/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70paDPvOmbsB4om3xPX +V2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrNVjzRlwW5y0vtOUucxD/SVRNuJLDW +cfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQAD +ggIBAJ+qQibbC5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe +QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuyh6f88/qBVRRi +ClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM47HLwEXWdyzRSjeZ2axfG34ar +J45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8JZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYci +NuaCp+0KueIHoI17eko8cdLiA6EfMgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5me +LMFrUKTX5hgUvYU/Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJF +fbdT6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ0E6yove+ +7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm2tIMPNuzjsmhDYAPexZ3 +FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bbbP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3 +gm3c +-----END CERTIFICATE----- + +GTS Root R2 +=========== +-----BEGIN CERTIFICATE----- +MIIFVzCCAz+gAwIBAgINAgPlrsWNBCUaqxElqjANBgkqhkiG9w0BAQwFADBHMQswCQYDVQQGEwJV +UzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3Qg +UjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UE +ChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwggIiMA0G +CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDO3v2m++zsFDQ8BwZabFn3GTXd98GdVarTzTukk3Lv +CvptnfbwhYBboUhSnznFt+4orO/LdmgUud+tAWyZH8QiHZ/+cnfgLFuv5AS/T3KgGjSY6Dlo7JUl +e3ah5mm5hRm9iYz+re026nO8/4Piy33B0s5Ks40FnotJk9/BW9BuXvAuMC6C/Pq8tBcKSOWIm8Wb +a96wyrQD8Nr0kLhlZPdcTK3ofmZemde4wj7I0BOdre7kRXuJVfeKH2JShBKzwkCX44ofR5GmdFrS ++LFjKBC4swm4VndAoiaYecb+3yXuPuWgf9RhD1FLPD+M2uFwdNjCaKH5wQzpoeJ/u1U8dgbuak7M +kogwTZq9TwtImoS1mKPV+3PBV2HdKFZ1E66HjucMUQkQdYhMvI35ezzUIkgfKtzra7tEscszcTJG +r61K8YzodDqs5xoic4DSMPclQsciOzsSrZYuxsN2B6ogtzVJV+mSSeh2FnIxZyuWfoqjx5RWIr9q +S34BIbIjMt/kmkRtWVtd9QCgHJvGeJeNkP+byKq0rxFROV7Z+2et1VsRnTKaG73VululycslaVNV +J1zgyjbLiGH7HrfQy+4W+9OmTN6SpdTi3/UGVN4unUu0kzCqgc7dGtxRcw1PcOnlthYhGXmy5okL +dWTK1au8CcEYof/UVKGFPP0UJAOyh9OktwIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQUu//KjiOfT5nK2+JopqUVJxce2Q4wDQYJKoZIhvcNAQEMBQAD +ggIBAB/Kzt3HvqGf2SdMC9wXmBFqiN495nFWcrKeGk6c1SuYJF2ba3uwM4IJvd8lRuqYnrYb/oM8 +0mJhwQTtzuDFycgTE1XnqGOtjHsB/ncw4c5omwX4Eu55MaBBRTUoCnGkJE+M3DyCB19m3H0Q/gxh +swWV7uGugQ+o+MePTagjAiZrHYNSVc61LwDKgEDg4XSsYPWHgJ2uNmSRXbBoGOqKYcl3qJfEycel +/FVL8/B/uWU9J2jQzGv6U53hkRrJXRqWbTKH7QMgyALOWr7Z6v2yTcQvG99fevX4i8buMTolUVVn +jWQye+mew4K6Ki3pHrTgSAai/GevHyICc/sgCq+dVEuhzf9gR7A/Xe8bVr2XIZYtCtFenTgCR2y5 +9PYjJbigapordwj6xLEokCZYCDzifqrXPW+6MYgKBesntaFJ7qBFVHvmJ2WZICGoo7z7GJa7Um8M +7YNRTOlZ4iBgxcJlkoKM8xAfDoqXvneCbT+PHV28SSe9zE8P4c52hgQjxcCMElv924SgJPFI/2R8 +0L5cFtHvma3AH/vLrrw4IgYmZNralw4/KBVEqE8AyvCazM90arQ+POuV7LXTWtiBmelDGDfrs7vR +WGJB82bSj6p4lVQgw1oudCvV0b4YacCs1aTPObpRhANl6WLAYv7YTVWW4tAR+kg0Eeye7QUd5MjW +HYbL +-----END CERTIFICATE----- + +GTS Root R3 +=========== +-----BEGIN CERTIFICATE----- +MIICCTCCAY6gAwIBAgINAgPluILrIPglJ209ZjAKBggqhkjOPQQDAzBHMQswCQYDVQQGEwJVUzEi +MCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMw +HhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZ +R29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjO +PQIBBgUrgQQAIgNiAAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout +736GjOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL24CejQjBA +MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTB8Sa6oC2uhYHP0/Eq +Er24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEA9uEglRR7VKOQFhG/hMjqb2sXnh5GmCCbn9MN2azT +L818+FsuVbu/3ZL3pAzcMeGiAjEA/JdmZuVDFhOD3cffL74UOO0BzrEXGhF16b0DjyZ+hOXJYKaV +11RZt+cRLInUue4X +-----END CERTIFICATE----- + +GTS Root R4 +=========== +-----BEGIN CERTIFICATE----- +MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYDVQQGEwJVUzEi +MCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQw +HhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZ +R29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjO +PQIBBgUrgQQAIgNiAATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzu +hXyiQHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvRHYqjQjBA +MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSATNbrdP9JNqPV2Py1 +PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/C +r8deVl5c1RxYIigL9zC2L7F8AjEA8GE8p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh +4rsUecrNIdSUtUlD +-----END CERTIFICATE----- + +Telia Root CA v2 +================ +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIPAWdfJ9b+euPkrL4JWwWeMA0GCSqGSIb3DQEBCwUAMEQxCzAJBgNVBAYT +AkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZMBcGA1UEAwwQVGVsaWEgUm9vdCBDQSB2 +MjAeFw0xODExMjkxMTU1NTRaFw00MzExMjkxMTU1NTRaMEQxCzAJBgNVBAYTAkZJMRowGAYDVQQK +DBFUZWxpYSBGaW5sYW5kIE95ajEZMBcGA1UEAwwQVGVsaWEgUm9vdCBDQSB2MjCCAiIwDQYJKoZI +hvcNAQEBBQADggIPADCCAgoCggIBALLQPwe84nvQa5n44ndp586dpAO8gm2h/oFlH0wnrI4AuhZ7 +6zBqAMCzdGh+sq/H1WKzej9Qyow2RCRj0jbpDIX2Q3bVTKFgcmfiKDOlyzG4OiIjNLh9vVYiQJ3q +9HsDrWj8soFPmNB06o3lfc1jw6P23pLCWBnglrvFxKk9pXSW/q/5iaq9lRdU2HhE8Qx3FZLgmEKn +pNaqIJLNwaCzlrI6hEKNfdWV5Nbb6WLEWLN5xYzTNTODn3WhUidhOPFZPY5Q4L15POdslv5e2QJl +tI5c0BE0312/UqeBAMN/mUWZFdUXyApT7GPzmX3MaRKGwhfwAZ6/hLzRUssbkmbOpFPlob/E2wnW +5olWK8jjfN7j/4nlNW4o6GwLI1GpJQXrSPjdscr6bAhR77cYbETKJuFzxokGgeWKrLDiKca5JLNr +RBH0pUPCTEPlcDaMtjNXepUugqD0XBCzYYP2AgWGLnwtbNwDRm41k9V6lS/eINhbfpSQBGq6WT0E +BXWdN6IOLj3rwaRSg/7Qa9RmjtzG6RJOHSpXqhC8fF6CfaamyfItufUXJ63RDolUK5X6wK0dmBR4 +M0KGCqlztft0DbcbMBnEWg4cJ7faGND/isgFuvGqHKI3t+ZIpEYslOqodmJHixBTB0hXbOKSTbau +BcvcwUpej6w9GU7C7WB1K9vBykLVAgMBAAGjYzBhMB8GA1UdIwQYMBaAFHKs5DN5qkWH9v2sHZ7W +xy+G2CQ5MB0GA1UdDgQWBBRyrOQzeapFh/b9rB2e1scvhtgkOTAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAoDtZpwmUPjaE0n4vOaWWl/oRrfxn83EJ +8rKJhGdEr7nv7ZbsnGTbMjBvZ5qsfl+yqwE2foH65IRe0qw24GtixX1LDoJt0nZi0f6X+J8wfBj5 +tFJ3gh1229MdqfDBmgC9bXXYfef6xzijnHDoRnkDry5023X4blMMA8iZGok1GTzTyVR8qPAs5m4H +eW9q4ebqkYJpCh3DflminmtGFZhb069GHWLIzoBSSRE/yQQSwxN8PzuKlts8oB4KtItUsiRnDe+C +y748fdHif64W1lZYudogsYMVoe+KTTJvQS8TUoKU1xrBeKJR3Stwbbca+few4GeXVtt8YVMJAygC +QMez2P2ccGrGKMOF6eLtGpOg3kuYooQ+BXcBlj37tCAPnHICehIv1aO6UXivKitEZU61/Qrowc15 +h2Er3oBXRb9n8ZuRXqWk7FlIEA04x7D6w0RtBPV4UBySllva9bguulvP5fBqnUsvWHMtTy3EHD70 +sz+rFQ47GUGKpMFXEmZxTPpT41frYpUJnlTd0cI8Vzy9OK2YZLe4A5pTVmBds9hCG1xLEooc6+t9 +xnppxyd/pPiL8uSUZodL6ZQHCRJ5irLrdATczvREWeAWysUsWNc8e89ihmpQfTU2Zqf7N+cox9jQ +raVplI/owd8k+BsHMYeB2F326CjYSlKArBPuUBQemMc= +-----END CERTIFICATE----- + +D-TRUST BR Root CA 1 2020 +========================= +-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQfMmPK4TX3+oPyWWa00tNljAKBggqhkjOPQQDAzBIMQswCQYDVQQGEwJE +RTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRSVVNUIEJSIFJvb3QgQ0EgMSAy +MDIwMB4XDTIwMDIxMTA5NDUwMFoXDTM1MDIxMTA5NDQ1OVowSDELMAkGA1UEBhMCREUxFTATBgNV +BAoTDEQtVHJ1c3QgR21iSDEiMCAGA1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDEgMjAyMDB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABMbLxyjR+4T1mu9CFCDhQ2tuda38KwOE1HaTJddZO0Flax7mNCq7 +dPYSzuht56vkPE4/RAiLzRZxy7+SmfSk1zxQVFKQhYN4lGdnoxwJGT11NIXe7WB9xwy0QVK5buXu +QqOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHOREKv/VbNafAkl1bK6CKBrqx9t +MA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6gPKA6hjhodHRwOi8vY3JsLmQtdHJ1c3Qu +bmV0L2NybC9kLXRydXN0X2JyX3Jvb3RfY2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVj +dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwQlIlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxP +PUQtVHJ1c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjOPQQD +AwNpADBmAjEAlJAtE/rhY/hhY+ithXhUkZy4kzg+GkHaQBZTQgjKL47xPoFWwKrY7RjEsK70Pvom +AjEA8yjixtsrmfu3Ubgko6SUeho/5jbiA1czijDLgsfWFBHVdWNbFJWcHwHP2NVypw87 +-----END CERTIFICATE----- + +D-TRUST EV Root CA 1 2020 +========================= +-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQXwJB13qHfEwDo6yWjfv/0DAKBggqhkjOPQQDAzBIMQswCQYDVQQGEwJE +RTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRSVVNUIEVWIFJvb3QgQ0EgMSAy +MDIwMB4XDTIwMDIxMTEwMDAwMFoXDTM1MDIxMTA5NTk1OVowSDELMAkGA1UEBhMCREUxFTATBgNV +BAoTDEQtVHJ1c3QgR21iSDEiMCAGA1UEAxMZRC1UUlVTVCBFViBSb290IENBIDEgMjAyMDB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABPEL3YZDIBnfl4XoIkqbz52Yv7QFJsnL46bSj8WeeHsxiamJrSc8 +ZRCC/N/DnU7wMyPE0jL1HLDfMxddxfCxivnvubcUyilKwg+pf3VlSSowZ/Rk99Yad9rDwpdhQntJ +raOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFH8QARY3OqQo5FD4pPfsazK2/umL +MA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6gPKA6hjhodHRwOi8vY3JsLmQtdHJ1c3Qu +bmV0L2NybC9kLXRydXN0X2V2X3Jvb3RfY2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVj +dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwRVYlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxP +PUQtVHJ1c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjOPQQD +AwNpADBmAjEAyjzGKnXCXnViOTYAYFqLwZOZzNnbQTs7h5kXO9XMT8oi96CAy/m0sRtW9XLS/BnR +AjEAkfcwkz8QRitxpNA7RJvAKQIFskF3UfN5Wp6OFKBOQtJbgfM0agPnIjhQW+0ZT0MW +-----END CERTIFICATE----- + +DigiCert TLS ECC P384 Root G5 +============================= +-----BEGIN CERTIFICATE----- +MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQswCQYDVQQGEwJV +UzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURpZ2lDZXJ0IFRMUyBFQ0MgUDM4 +NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMx +FzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQg +Um9vdCBHNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1Tzvd +lHJS7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp0zVozptj +n4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICISB4CIfBFqMA4GA1UdDwEB +/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMDA2gAMGUCMQCJao1H5+z8blUD2Wds +Jk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQLgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIx +AJSdYsiJvRmEFOml+wG4DXZDjC5Ty3zfDBeWUA== +-----END CERTIFICATE----- + +DigiCert TLS RSA4096 Root G5 +============================ +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCPm0eKj6ftpqMzeJ3nzPijANBgkqhkiG9w0BAQwFADBNMQswCQYDVQQG +EwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0 +MDk2IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcNNDYwMTE0MjM1OTU5WjBNMQswCQYDVQQGEwJV +UzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0MDk2 +IFJvb3QgRzUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz0PTJeRGd/fxmgefM1eS8 +7IE+ajWOLrfn3q/5B03PMJ3qCQuZvWxX2hhKuHisOjmopkisLnLlvevxGs3npAOpPxG02C+JFvuU +AT27L/gTBaF4HI4o4EXgg/RZG5Wzrn4DReW+wkL+7vI8toUTmDKdFqgpwgscONyfMXdcvyej/Ces +tyu9dJsXLfKB2l2w4SMXPohKEiPQ6s+d3gMXsUJKoBZMpG2T6T867jp8nVid9E6P/DsjyG244gXa +zOvswzH016cpVIDPRFtMbzCe88zdH5RDnU1/cHAN1DrRN/BsnZvAFJNY781BOHW8EwOVfH/jXOnV +DdXifBBiqmvwPXbzP6PosMH976pXTayGpxi0KcEsDr9kvimM2AItzVwv8n/vFfQMFawKsPHTDU9q +TXeXAaDxZre3zu/O7Oyldcqs4+Fj97ihBMi8ez9dLRYiVu1ISf6nL3kwJZu6ay0/nTvEF+cdLvvy +z6b84xQslpghjLSR6Rlgg/IwKwZzUNWYOwbpx4oMYIwo+FKbbuH2TbsGJJvXKyY//SovcfXWJL5/ +MZ4PbeiPT02jP/816t9JXkGPhvnxd3lLG7SjXi/7RgLQZhNeXoVPzthwiHvOAbWWl9fNff2C+MIk +wcoBOU+NosEUQB+cZtUMCUbW8tDRSHZWOkPLtgoRObqME2wGtZ7P6wIDAQABo0IwQDAdBgNVHQ4E +FgQUUTMc7TZArxfTJc1paPKvTiM+s0EwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8w +DQYJKoZIhvcNAQEMBQADggIBAGCmr1tfV9qJ20tQqcQjNSH/0GEwhJG3PxDPJY7Jv0Y02cEhJhxw +GXIeo8mH/qlDZJY6yFMECrZBu8RHANmfGBg7sg7zNOok992vIGCukihfNudd5N7HPNtQOa27PShN +lnx2xlv0wdsUpasZYgcYQF+Xkdycx6u1UQ3maVNVzDl92sURVXLFO4uJ+DQtpBflF+aZfTCIITfN +MBc9uPK8qHWgQ9w+iUuQrm0D4ByjoJYJu32jtyoQREtGBzRj7TG5BO6jm5qu5jF49OokYTurWGT/ +u4cnYiWB39yhL/btp/96j1EuMPikAdKFOV8BmZZvWltwGUb+hmA+rYAQCd05JS9Yf7vSdPD3Rh9G +OUrYU9DzLjtxpdRv/PNn5AeP3SYZ4Y1b+qOTEZvpyDrDVWiakuFSdjjo4bq9+0/V77PnSIMx8IIh +47a+p6tv75/fTM8BuGJqIz3nCU2AG3swpMPdB380vqQmsvZB6Akd4yCYqjdP//fx4ilwMUc/dNAU +FvohigLVigmUdy7yWSiLfFCSCmZ4OIN1xLVaqBHG5cGdZlXPU8Sv13WFqUITVuwhd4GTWgzqltlJ +yqEI8pc7bZsEGCREjnwB8twl2F6GmrE52/WRMmrRpnCKovfepEWFJqgejF0pW8hL2JpqA15w8oVP +bEtoL8pU9ozaMv7Da4M/OMZ+ +-----END CERTIFICATE----- + +Certainly Root R1 +================= +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIRAI4P+UuQcWhlM1T01EQ5t+AwDQYJKoZIhvcNAQELBQAwPTELMAkGA1UE +BhMCVVMxEjAQBgNVBAoTCUNlcnRhaW5seTEaMBgGA1UEAxMRQ2VydGFpbmx5IFJvb3QgUjEwHhcN +MjEwNDAxMDAwMDAwWhcNNDYwNDAxMDAwMDAwWjA9MQswCQYDVQQGEwJVUzESMBAGA1UEChMJQ2Vy +dGFpbmx5MRowGAYDVQQDExFDZXJ0YWlubHkgUm9vdCBSMTCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBANA21B/q3avk0bbm+yLA3RMNansiExyXPGhjZjKcA7WNpIGD2ngwEc/csiu+kr+O +5MQTvqRoTNoCaBZ0vrLdBORrKt03H2As2/X3oXyVtwxwhi7xOu9S98zTm/mLvg7fMbedaFySpvXl +8wo0tf97ouSHocavFwDvA5HtqRxOcT3Si2yJ9HiG5mpJoM610rCrm/b01C7jcvk2xusVtyWMOvwl +DbMicyF0yEqWYZL1LwsYpfSt4u5BvQF5+paMjRcCMLT5r3gajLQ2EBAHBXDQ9DGQilHFhiZ5shGI +XsXwClTNSaa/ApzSRKft43jvRl5tcdF5cBxGX1HpyTfcX35pe0HfNEXgO4T0oYoKNp43zGJS4YkN +KPl6I7ENPT2a/Z2B7yyQwHtETrtJ4A5KVpK8y7XdeReJkd5hiXSSqOMyhb5OhaRLWcsrxXiOcVTQ +AjeZjOVJ6uBUcqQRBi8LjMFbvrWhsFNunLhgkR9Za/kt9JQKl7XsxXYDVBtlUrpMklZRNaBA2Cnb +rlJ2Oy0wQJuK0EJWtLeIAaSHO1OWzaMWj/Nmqhexx2DgwUMFDO6bW2BvBlyHWyf5QBGenDPBt+U1 +VwV/J84XIIwc/PH72jEpSe31C4SnT8H2TsIonPru4K8H+zMReiFPCyEQtkA6qyI6BJyLm4SGcprS +p6XEtHWRqSsjAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBTgqj8ljZ9EXME66C6ud0yEPmcM9DANBgkqhkiG9w0BAQsFAAOCAgEAuVevuBLaV4OPaAsz +HQNTVfSVcOQrPbA56/qJYv331hgELyE03fFo8NWWWt7CgKPBjcZq91l3rhVkz1t5BXdm6ozTaw3d +8VkswTOlMIAVRQdFGjEitpIAq5lNOo93r6kiyi9jyhXWx8bwPWz8HA2YEGGeEaIi1wrykXprOQ4v +MMM2SZ/g6Q8CRFA3lFV96p/2O7qUpUzpvD5RtOjKkjZUbVwlKNrdrRT90+7iIgXr0PK3aBLXWopB +GsaSpVo7Y0VPv+E6dyIvXL9G+VoDhRNCX8reU9ditaY1BMJH/5n9hN9czulegChB8n3nHpDYT3Y+ +gjwN/KUD+nsa2UUeYNrEjvn8K8l7lcUq/6qJ34IxD3L/DCfXCh5WAFAeDJDBlrXYFIW7pw0WwfgH +JBu6haEaBQmAupVjyTrsJZ9/nbqkRxWbRHDxakvWOF5D8xh+UG7pWijmZeZ3Gzr9Hb4DJqPb1OG7 +fpYnKx3upPvaJVQTA945xsMfTZDsjxtK0hzthZU4UHlG1sGQUDGpXJpuHfUzVounmdLyyCwzk5Iw +x06MZTMQZBf9JBeW0Y3COmor6xOLRPIh80oat3df1+2IpHLlOR+Vnb5nwXARPbv0+Em34yaXOp/S +X3z7wJl8OSngex2/DaeP0ik0biQVy96QXr8axGbqwua6OV+KmalBWQewLK8= +-----END CERTIFICATE----- + +Certainly Root E1 +================= +-----BEGIN CERTIFICATE----- +MIIB9zCCAX2gAwIBAgIQBiUzsUcDMydc+Y2aub/M+DAKBggqhkjOPQQDAzA9MQswCQYDVQQGEwJV +UzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0YWlubHkgUm9vdCBFMTAeFw0yMTA0 +MDEwMDAwMDBaFw00NjA0MDEwMDAwMDBaMD0xCzAJBgNVBAYTAlVTMRIwEAYDVQQKEwlDZXJ0YWlu +bHkxGjAYBgNVBAMTEUNlcnRhaW5seSBSb290IEUxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3m/4 +fxzf7flHh4axpMCK+IKXgOqPyEpeKn2IaKcBYhSRJHpcnqMXfYqGITQYUBsQ3tA3SybHGWCA6TS9 +YBk2QNYphwk8kXr2vBMj3VlOBF7PyAIcGFPBMdjaIOlEjeR2o0IwQDAOBgNVHQ8BAf8EBAMCAQYw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU8ygYy2R17ikq6+2uI1g4hevIIgcwCgYIKoZIzj0E +AwMDaAAwZQIxALGOWiDDshliTd6wT99u0nCK8Z9+aozmut6Dacpps6kFtZaSF4fC0urQe87YQVt8 +rgIwRt7qy12a7DLCZRawTDBcMPPaTnOGBtjOiQRINzf43TNRnXCve1XYAS59BWQOhriR +-----END CERTIFICATE----- + +Security Communication ECC RootCA1 +================================== +-----BEGIN CERTIFICATE----- +MIICODCCAb6gAwIBAgIJANZdm7N4gS7rMAoGCCqGSM49BAMDMGExCzAJBgNVBAYTAkpQMSUwIwYD +VQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMSswKQYDVQQDEyJTZWN1cml0eSBDb21t +dW5pY2F0aW9uIEVDQyBSb290Q0ExMB4XDTE2MDYxNjA1MTUyOFoXDTM4MDExODA1MTUyOFowYTEL +MAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xKzApBgNV +BAMTIlNlY3VyaXR5IENvbW11bmljYXRpb24gRUNDIFJvb3RDQTEwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAASkpW9gAwPDvTH00xecK4R1rOX9PVdu12O/5gSJko6BnOPpR27KkBLIE+CnnfdldB9sELLo +5OnvbYUymUSxXv3MdhDYW72ixvnWQuRXdtyQwjWpS4g8EkdtXP9JTxpKULGjQjBAMB0GA1UdDgQW +BBSGHOf+LaVKiwj+KBH6vqNm+GBZLzAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAK +BggqhkjOPQQDAwNoADBlAjAVXUI9/Lbu9zuxNuie9sRGKEkz0FhDKmMpzE2xtHqiuQ04pV1IKv3L +snNdo4gIxwwCMQDAqy0Obe0YottT6SXbVQjgUMzfRGEWgqtJsLKB7HOHeLRMsmIbEvoWTSVLY70e +N9k= +-----END CERTIFICATE----- + +BJCA Global Root CA1 +==================== +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIQVW9l47TZkGobCdFsPsBsIDANBgkqhkiG9w0BAQsFADBUMQswCQYDVQQG +EwJDTjEmMCQGA1UECgwdQkVJSklORyBDRVJUSUZJQ0FURSBBVVRIT1JJVFkxHTAbBgNVBAMMFEJK +Q0EgR2xvYmFsIFJvb3QgQ0ExMB4XDTE5MTIxOTAzMTYxN1oXDTQ0MTIxMjAzMTYxN1owVDELMAkG +A1UEBhMCQ04xJjAkBgNVBAoMHUJFSUpJTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZMR0wGwYDVQQD +DBRCSkNBIEdsb2JhbCBSb290IENBMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAPFm +CL3ZxRVhy4QEQaVpN3cdwbB7+sN3SJATcmTRuHyQNZ0YeYjjlwE8R4HyDqKYDZ4/N+AZspDyRhyS +sTphzvq3Rp4Dhtczbu33RYx2N95ulpH3134rhxfVizXuhJFyV9xgw8O558dnJCNPYwpj9mZ9S1Wn +P3hkSWkSl+BMDdMJoDIwOvqfwPKcxRIqLhy1BDPapDgRat7GGPZHOiJBhyL8xIkoVNiMpTAK+BcW +yqw3/XmnkRd4OJmtWO2y3syJfQOcs4ll5+M7sSKGjwZteAf9kRJ/sGsciQ35uMt0WwfCyPQ10WRj +eulumijWML3mG90Vr4TqnMfK9Q7q8l0ph49pczm+LiRvRSGsxdRpJQaDrXpIhRMsDQa4bHlW/KNn +MoH1V6XKV0Jp6VwkYe/iMBhORJhVb3rCk9gZtt58R4oRTklH2yiUAguUSiz5EtBP6DF+bHq/pj+b +OT0CFqMYs2esWz8sgytnOYFcuX6U1WTdno9uruh8W7TXakdI136z1C2OVnZOz2nxbkRs1CTqjSSh +GL+9V/6pmTW12xB3uD1IutbB5/EjPtffhZ0nPNRAvQoMvfXnjSXWgXSHRtQpdaJCbPdzied9v3pK +H9MiyRVVz99vfFXQpIsHETdfg6YmV6YBW37+WGgHqel62bno/1Afq8K0wM7o6v0PvY1NuLxxAgMB +AAGjQjBAMB0GA1UdDgQWBBTF7+3M2I0hxkjk49cULqcWk+WYATAPBgNVHRMBAf8EBTADAQH/MA4G +A1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAUoKsITQfI/Ki2Pm4rzc2IInRNwPWaZ+4 +YRC6ojGYWUfo0Q0lHhVBDOAqVdVXUsv45Mdpox1NcQJeXyFFYEhcCY5JEMEE3KliawLwQ8hOnThJ +dMkycFRtwUf8jrQ2ntScvd0g1lPJGKm1Vrl2i5VnZu69mP6u775u+2D2/VnGKhs/I0qUJDAnyIm8 +60Qkmss9vk/Ves6OF8tiwdneHg56/0OGNFK8YT88X7vZdrRTvJez/opMEi4r89fO4aL/3Xtw+zuh +TaRjAv04l5U/BXCga99igUOLtFkNSoxUnMW7gZ/NfaXvCyUeOiDbHPwfmGcCCtRzRBPbUYQaVQNW +4AB+dAb/OMRyHdOoP2gxXdMJxy6MW2Pg6Nwe0uxhHvLe5e/2mXZgLR6UcnHGCyoyx5JO1UbXHfmp +GQrI+pXObSOYqgs4rZpWDW+N8TEAiMEXnM0ZNjX+VVOg4DwzX5Ze4jLp3zO7Bkqp2IRzznfSxqxx +4VyjHQy7Ct9f4qNx2No3WqB4K/TUfet27fJhcKVlmtOJNBir+3I+17Q9eVzYH6Eze9mCUAyTF6ps +3MKCuwJXNq+YJyo5UOGwifUll35HaBC07HPKs5fRJNz2YqAo07WjuGS3iGJCz51TzZm+ZGiPTx4S +SPfSKcOYKMryMguTjClPPGAyzQWWYezyr/6zcCwupvI= +-----END CERTIFICATE----- + +BJCA Global Root CA2 +==================== +-----BEGIN CERTIFICATE----- +MIICJTCCAaugAwIBAgIQLBcIfWQqwP6FGFkGz7RK6zAKBggqhkjOPQQDAzBUMQswCQYDVQQGEwJD +TjEmMCQGA1UECgwdQkVJSklORyBDRVJUSUZJQ0FURSBBVVRIT1JJVFkxHTAbBgNVBAMMFEJKQ0Eg +R2xvYmFsIFJvb3QgQ0EyMB4XDTE5MTIxOTAzMTgyMVoXDTQ0MTIxMjAzMTgyMVowVDELMAkGA1UE +BhMCQ04xJjAkBgNVBAoMHUJFSUpJTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZMR0wGwYDVQQDDBRC +SkNBIEdsb2JhbCBSb290IENBMjB2MBAGByqGSM49AgEGBSuBBAAiA2IABJ3LgJGNU2e1uVCxA/jl +SR9BIgmwUVJY1is0j8USRhTFiy8shP8sbqjV8QnjAyEUxEM9fMEsxEtqSs3ph+B99iK++kpRuDCK +/eHeGBIK9ke35xe/J4rUQUyWPGCWwf0VHKNCMEAwHQYDVR0OBBYEFNJKsVF/BvDRgh9Obl+rg/xI +1LCRMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gAMGUCMBq8 +W9f+qdJUDkpd0m2xQNz0Q9XSSpkZElaA94M04TVOSG0ED1cxMDAtsaqdAzjbBgIxAMvMh1PLet8g +UXOQwKhbYdDFUDn9hf7B43j4ptZLvZuHjw/l1lOWqzzIQNph91Oj9w== +-----END CERTIFICATE----- + +Sectigo Public Server Authentication Root E46 +============================================= +-----BEGIN CERTIFICATE----- +MIICOjCCAcGgAwIBAgIQQvLM2htpN0RfFf51KBC49DAKBggqhkjOPQQDAzBfMQswCQYDVQQGEwJH +QjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBTZXJ2 +ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwHhcNMjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5 +WjBfMQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0 +aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUr +gQQAIgNiAAR2+pmpbiDt+dd34wc7qNs9Xzjoq1WmVk/WSOrsfy2qw7LFeeyZYX8QeccCWvkEN/U0 +NSt3zn8gj1KjAIns1aeibVvjS5KToID1AZTc8GgHHs3u/iVStSBDHBv+6xnOQ6OjQjBAMB0GA1Ud +DgQWBBTRItpMWfFLXyY4qp3W7usNw/upYTAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB +/zAKBggqhkjOPQQDAwNnADBkAjAn7qRaqCG76UeXlImldCBteU/IvZNeWBj7LRoAasm4PdCkT0RH +lAFWovgzJQxC36oCMB3q4S6ILuH5px0CMk7yn2xVdOOurvulGu7t0vzCAxHrRVxgED1cf5kDW21U +SAGKcw== +-----END CERTIFICATE----- + +Sectigo Public Server Authentication Root R46 +============================================= +-----BEGIN CERTIFICATE----- +MIIFijCCA3KgAwIBAgIQdY39i658BwD6qSWn4cetFDANBgkqhkiG9w0BAQwFADBfMQswCQYDVQQG +EwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBT +ZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYwHhcNMjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1 +OTU5WjBfMQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1T +ZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYwggIiMA0GCSqGSIb3 +DQEBAQUAA4ICDwAwggIKAoICAQCTvtU2UnXYASOgHEdCSe5jtrch/cSV1UgrJnwUUxDaef0rty2k +1Cz66jLdScK5vQ9IPXtamFSvnl0xdE8H/FAh3aTPaE8bEmNtJZlMKpnzSDBh+oF8HqcIStw+Kxwf +GExxqjWMrfhu6DtK2eWUAtaJhBOqbchPM8xQljeSM9xfiOefVNlI8JhD1mb9nxc4Q8UBUQvX4yMP +FF1bFOdLvt30yNoDN9HWOaEhUTCDsG3XME6WW5HwcCSrv0WBZEMNvSE6Lzzpng3LILVCJ8zab5vu +ZDCQOc2TZYEhMbUjUDM3IuM47fgxMMxF/mL50V0yeUKH32rMVhlATc6qu/m1dkmU8Sf4kaWD5Qaz +Yw6A3OASVYCmO2a0OYctyPDQ0RTp5A1NDvZdV3LFOxxHVp3i1fuBYYzMTYCQNFu31xR13NgESJ/A +wSiItOkcyqex8Va3e0lMWeUgFaiEAin6OJRpmkkGj80feRQXEgyDet4fsZfu+Zd4KKTIRJLpfSYF +plhym3kT2BFfrsU4YjRosoYwjviQYZ4ybPUHNs2iTG7sijbt8uaZFURww3y8nDnAtOFr94MlI1fZ +EoDlSfB1D++N6xybVCi0ITz8fAr/73trdf+LHaAZBav6+CuBQug4urv7qv094PPK306Xlynt8xhW +6aWWrL3DkJiy4Pmi1KZHQ3xtzwIDAQABo0IwQDAdBgNVHQ4EFgQUVnNYZJX5khqwEioEYnmhQBWI +IUkwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAC9c +mTz8Bl6MlC5w6tIyMY208FHVvArzZJ8HXtXBc2hkeqK5Duj5XYUtqDdFqij0lgVQYKlJfp/imTYp +E0RHap1VIDzYm/EDMrraQKFz6oOht0SmDpkBm+S8f74TlH7Kph52gDY9hAaLMyZlbcp+nv4fjFg4 +exqDsQ+8FxG75gbMY/qB8oFM2gsQa6H61SilzwZAFv97fRheORKkU55+MkIQpiGRqRxOF3yEvJ+M +0ejf5lG5Nkc/kLnHvALcWxxPDkjBJYOcCj+esQMzEhonrPcibCTRAUH4WAP+JWgiH5paPHxsnnVI +84HxZmduTILA7rpXDhjvLpr3Etiga+kFpaHpaPi8TD8SHkXoUsCjvxInebnMMTzD9joiFgOgyY9m +pFuiTdaBJQbpdqQACj7LzTWb4OE4y2BThihCQRxEV+ioratF4yUQvNs+ZUH7G6aXD+u5dHn5Hrwd +Vw1Hr8Mvn4dGp+smWg9WY7ViYG4A++MnESLn/pmPNPW56MORcr3Ywx65LvKRRFHQV80MNNVIIb/b +E/FmJUNS0nAiNs2fxBx1IK1jcmMGDw4nztJqDby1ORrp0XZ60Vzk50lJLVU3aPAaOpg+VBeHVOmm +J1CJeyAvP/+/oYtKR5j/K3tJPsMpRmAYQqszKbrAKbkTidOIijlBO8n9pu0f9GBj39ItVQGL +-----END CERTIFICATE----- + +SSL.com TLS RSA Root CA 2022 +============================ +-----BEGIN CERTIFICATE----- +MIIFiTCCA3GgAwIBAgIQb77arXO9CEDii02+1PdbkTANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQG +EwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQDDBxTU0wuY29tIFRMUyBSU0Eg +Um9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzQyMloXDTQ2MDgxOTE2MzQyMVowTjELMAkGA1UEBhMC +VVMxGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgUlNBIFJv +b3QgQ0EgMjAyMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANCkCXJPQIgSYT41I57u +9nTPL3tYPc48DRAokC+X94xI2KDYJbFMsBFMF3NQ0CJKY7uB0ylu1bUJPiYYf7ISf5OYt6/wNr/y +7hienDtSxUcZXXTzZGbVXcdotL8bHAajvI9AI7YexoS9UcQbOcGV0insS657Lb85/bRi3pZ7Qcac +oOAGcvvwB5cJOYF0r/c0WRFXCsJbwST0MXMwgsadugL3PnxEX4MN8/HdIGkWCVDi1FW24IBydm5M +R7d1VVm0U3TZlMZBrViKMWYPHqIbKUBOL9975hYsLfy/7PO0+r4Y9ptJ1O4Fbtk085zx7AGL0SDG +D6C1vBdOSHtRwvzpXGk3R2azaPgVKPC506QVzFpPulJwoxJF3ca6TvvC0PeoUidtbnm1jPx7jMEW +TO6Af77wdr5BUxIzrlo4QqvXDz5BjXYHMtWrifZOZ9mxQnUjbvPNQrL8VfVThxc7wDNY8VLS+YCk +8OjwO4s4zKTGkH8PnP2L0aPP2oOnaclQNtVcBdIKQXTbYxE3waWglksejBYSd66UNHsef8JmAOSq +g+qKkK3ONkRN0VHpvB/zagX9wHQfJRlAUW7qglFA35u5CCoGAtUjHBPW6dvbxrB6y3snm/vg1UYk +7RBLY0ulBY+6uB0rpvqR4pJSvezrZ5dtmi2fgTIFZzL7SAg/2SW4BCUvAgMBAAGjYzBhMA8GA1Ud +EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU+y437uOEeicuzRk1sTN8/9REQrkwHQYDVR0OBBYEFPsu +N+7jhHonLs0ZNbEzfP/UREK5MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAjYlt +hEUY8U+zoO9opMAdrDC8Z2awms22qyIZZtM7QbUQnRC6cm4pJCAcAZli05bg4vsMQtfhWsSWTVTN +j8pDU/0quOr4ZcoBwq1gaAafORpR2eCNJvkLTqVTJXojpBzOCBvfR4iyrT7gJ4eLSYwfqUdYe5by +iB0YrrPRpgqU+tvT5TgKa3kSM/tKWTcWQA673vWJDPFs0/dRa1419dvAJuoSc06pkZCmF8NsLzjU +o3KUQyxi4U5cMj29TH0ZR6LDSeeWP4+a0zvkEdiLA9z2tmBVGKaBUfPhqBVq6+AL8BQx1rmMRTqo +ENjwuSfr98t67wVylrXEj5ZzxOhWc5y8aVFjvO9nHEMaX3cZHxj4HCUp+UmZKbaSPaKDN7Egkaib +MOlqbLQjk2UEqxHzDh1TJElTHaE/nUiSEeJ9DU/1172iWD54nR4fK/4huxoTtrEoZP2wAgDHbICi +vRZQIA9ygV/MlP+7mea6kMvq+cYMwq7FGc4zoWtcu358NFcXrfA/rs3qr5nsLFR+jM4uElZI7xc7 +P0peYNLcdDa8pUNjyw9bowJWCZ4kLOGGgYz+qxcs+sjiMho6/4UIyYOf8kpIEFR3N+2ivEC+5BB0 +9+Rbu7nzifmPQdjH5FCQNYA+HLhNkNPU98OwoX6EyneSMSy4kLGCenROmxMmtNVQZlR4rmA= +-----END CERTIFICATE----- + +SSL.com TLS ECC Root CA 2022 +============================ +-----BEGIN CERTIFICATE----- +MIICOjCCAcCgAwIBAgIQFAP1q/s3ixdAW+JDsqXRxDAKBggqhkjOPQQDAzBOMQswCQYDVQQGEwJV +UzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQDDBxTU0wuY29tIFRMUyBFQ0MgUm9v +dCBDQSAyMDIyMB4XDTIyMDgyNTE2MzM0OFoXDTQ2MDgxOTE2MzM0N1owTjELMAkGA1UEBhMCVVMx +GDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgRUNDIFJvb3Qg +Q0EgMjAyMjB2MBAGByqGSM49AgEGBSuBBAAiA2IABEUpNXP6wrgjzhR9qLFNoFs27iosU8NgCTWy +JGYmacCzldZdkkAZDsalE3D07xJRKF3nzL35PIXBz5SQySvOkkJYWWf9lCcQZIxPBLFNSeR7T5v1 +5wj4A4j3p8OSSxlUgaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBSJjy+j6CugFFR7 +81a4Jl9nOAuc0DAdBgNVHQ4EFgQUiY8vo+groBRUe/NWuCZfZzgLnNAwDgYDVR0PAQH/BAQDAgGG +MAoGCCqGSM49BAMDA2gAMGUCMFXjIlbp15IkWE8elDIPDAI2wv2sdDJO4fscgIijzPvX6yv/N33w +7deedWo1dlJF4AIxAMeNb0Igj762TVntd00pxCAgRWSGOlDGxK0tk/UYfXLtqc/ErFc2KAhl3zx5 +Zn6g6g== +-----END CERTIFICATE----- + +Atos TrustedRoot Root CA ECC TLS 2021 +===================================== +-----BEGIN CERTIFICATE----- +MIICFTCCAZugAwIBAgIQPZg7pmY9kGP3fiZXOATvADAKBggqhkjOPQQDAzBMMS4wLAYDVQQDDCVB +dG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgRUNDIFRMUyAyMDIxMQ0wCwYDVQQKDARBdG9zMQswCQYD +VQQGEwJERTAeFw0yMTA0MjIwOTI2MjNaFw00MTA0MTcwOTI2MjJaMEwxLjAsBgNVBAMMJUF0b3Mg +VHJ1c3RlZFJvb3QgUm9vdCBDQSBFQ0MgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYT +AkRFMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEloZYKDcKZ9Cg3iQZGeHkBQcfl+3oZIK59sRxUM6K +DP/XtXa7oWyTbIOiaG6l2b4siJVBzV3dscqDY4PMwL502eCdpO5KTlbgmClBk1IQ1SQ4AjJn8ZQS +b+/Xxd4u/RmAo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR2KCXWfeBmmnoJsmo7jjPX +NtNPojAOBgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMDaAAwZQIwW5kp85wxtolrbNa9d+F851F+ +uDrNozZffPc8dz7kUK2o59JZDCaOMDtuCCrCp1rIAjEAmeMM56PDr9NJLkaCI2ZdyQAUEv049OGY +a3cpetskz2VAv9LcjBHo9H1/IISpQuQo +-----END CERTIFICATE----- + +Atos TrustedRoot Root CA RSA TLS 2021 +===================================== +-----BEGIN CERTIFICATE----- +MIIFZDCCA0ygAwIBAgIQU9XP5hmTC/srBRLYwiqipDANBgkqhkiG9w0BAQwFADBMMS4wLAYDVQQD +DCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgUlNBIFRMUyAyMDIxMQ0wCwYDVQQKDARBdG9zMQsw +CQYDVQQGEwJERTAeFw0yMTA0MjIwOTIxMTBaFw00MTA0MTcwOTIxMDlaMEwxLjAsBgNVBAMMJUF0 +b3MgVHJ1c3RlZFJvb3QgUm9vdCBDQSBSU0EgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNV +BAYTAkRFMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtoAOxHm9BYx9sKOdTSJNy/BB +l01Z4NH+VoyX8te9j2y3I49f1cTYQcvyAh5x5en2XssIKl4w8i1mx4QbZFc4nXUtVsYvYe+W/CBG +vevUez8/fEc4BKkbqlLfEzfTFRVOvV98r61jx3ncCHvVoOX3W3WsgFWZkmGbzSoXfduP9LVq6hdK +ZChmFSlsAvFr1bqjM9xaZ6cF4r9lthawEO3NUDPJcFDsGY6wx/J0W2tExn2WuZgIWWbeKQGb9Cpt +0xU6kGpn8bRrZtkh68rZYnxGEFzedUlnnkL5/nWpo63/dgpnQOPF943HhZpZnmKaau1Fh5hnstVK +PNe0OwANwI8f4UDErmwh3El+fsqyjW22v5MvoVw+j8rtgI5Y4dtXz4U2OLJxpAmMkokIiEjxQGMY +sluMWuPD0xeqqxmjLBvk1cbiZnrXghmmOxYsL3GHX0WelXOTwkKBIROW1527k2gV+p2kHYzygeBY +Br3JtuP2iV2J+axEoctr+hbxx1A9JNr3w+SH1VbxT5Aw+kUJWdo0zuATHAR8ANSbhqRAvNncTFd+ +rrcztl524WWLZt+NyteYr842mIycg5kDcPOvdO3GDjbnvezBc6eUWsuSZIKmAMFwoW4sKeFYV+xa +fJlrJaSQOoD0IJ2azsct+bJLKZWD6TWNp0lIpw9MGZHQ9b8Q4HECAwEAAaNCMEAwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUdEmZ0f+0emhFdcN+tNzMzjkz2ggwDgYDVR0PAQH/BAQDAgGGMA0G +CSqGSIb3DQEBDAUAA4ICAQAjQ1MkYlxt/T7Cz1UAbMVWiLkO3TriJQ2VSpfKgInuKs1l+NsW4AmS +4BjHeJi78+xCUvuppILXTdiK/ORO/auQxDh1MoSf/7OwKwIzNsAQkG8dnK/haZPso0UvFJ/1TCpl +Q3IM98P4lYsU84UgYt1UU90s3BiVaU+DR3BAM1h3Egyi61IxHkzJqM7F78PRreBrAwA0JrRUITWX +AdxfG/F851X6LWh3e9NpzNMOa7pNdkTWwhWaJuywxfW70Xp0wmzNxbVe9kzmWy2B27O3Opee7c9G +slA9hGCZcbUztVdF5kJHdWoOsAgMrr3e97sPWD2PAzHoPYJQyi9eDF20l74gNAf0xBLh7tew2Vkt +afcxBPTy+av5EzH4AXcOPUIjJsyacmdRIXrMPIWo6iFqO9taPKU0nprALN+AnCng33eU0aKAQv9q +TFsR0PXNor6uzFFcw9VUewyu1rkGd4Di7wcaaMxZUa1+XGdrudviB0JbuAEFWDlN5LuYo7Ey7Nmj +1m+UI/87tyll5gfp77YZ6ufCOB0yiJA8EytuzO+rdwY0d4RPcuSBhPm5dDTedk+SKlOxJTnbPP/l +PqYO5Wue/9vsL3SD3460s6neFE3/MaNFcyT6lSnMEpcEoji2jbDwN/zIIX8/syQbPYtuzE2wFg2W +HYMfRsCbvUOZ58SWLs5fyQ== +-----END CERTIFICATE----- + +TrustAsia Global Root CA G3 +=========================== +-----BEGIN CERTIFICATE----- +MIIFpTCCA42gAwIBAgIUZPYOZXdhaqs7tOqFhLuxibhxkw8wDQYJKoZIhvcNAQEMBQAwWjELMAkG +A1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dpZXMsIEluYy4xJDAiBgNVBAMM +G1RydXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHMzAeFw0yMTA1MjAwMjEwMTlaFw00NjA1MTkwMjEw +MTlaMFoxCzAJBgNVBAYTAkNOMSUwIwYDVQQKDBxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMu +MSQwIgYDVQQDDBtUcnVzdEFzaWEgR2xvYmFsIFJvb3QgQ0EgRzMwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQDAMYJhkuSUGwoqZdC+BqmHO1ES6nBBruL7dOoKjbmzTNyPtxNST1QY4Sxz +lZHFZjtqz6xjbYdT8PfxObegQ2OwxANdV6nnRM7EoYNl9lA+sX4WuDqKAtCWHwDNBSHvBm3dIZwZ +Q0WhxeiAysKtQGIXBsaqvPPW5vxQfmZCHzyLpnl5hkA1nyDvP+uLRx+PjsXUjrYsyUQE49RDdT/V +P68czH5GX6zfZBCK70bwkPAPLfSIC7Epqq+FqklYqL9joDiR5rPmd2jE+SoZhLsO4fWvieylL1Ag +dB4SQXMeJNnKziyhWTXAyB1GJ2Faj/lN03J5Zh6fFZAhLf3ti1ZwA0pJPn9pMRJpxx5cynoTi+jm +9WAPzJMshH/x/Gr8m0ed262IPfN2dTPXS6TIi/n1Q1hPy8gDVI+lhXgEGvNz8teHHUGf59gXzhqc +D0r83ERoVGjiQTz+LISGNzzNPy+i2+f3VANfWdP3kXjHi3dqFuVJhZBFcnAvkV34PmVACxmZySYg +WmjBNb9Pp1Hx2BErW+Canig7CjoKH8GB5S7wprlppYiU5msTf9FkPz2ccEblooV7WIQn3MSAPmea +mseaMQ4w7OYXQJXZRe0Blqq/DPNL0WP3E1jAuPP6Z92bfW1K/zJMtSU7/xxnD4UiWQWRkUF3gdCF +TIcQcf+eQxuulXUtgQIDAQABo2MwYTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFEDk5PIj +7zjKsK5Xf/IhMBY027ySMB0GA1UdDgQWBBRA5OTyI+84yrCuV3/yITAWNNu8kjAOBgNVHQ8BAf8E +BAMCAQYwDQYJKoZIhvcNAQEMBQADggIBACY7UeFNOPMyGLS0XuFlXsSUT9SnYaP4wM8zAQLpw6o1 +D/GUE3d3NZ4tVlFEbuHGLige/9rsR82XRBf34EzC4Xx8MnpmyFq2XFNFV1pF1AWZLy4jVe5jaN/T +G3inEpQGAHUNcoTpLrxaatXeL1nHo+zSh2bbt1S1JKv0Q3jbSwTEb93mPmY+KfJLaHEih6D4sTNj +duMNhXJEIlU/HHzp/LgV6FL6qj6jITk1dImmasI5+njPtqzn59ZW/yOSLlALqbUHM/Q4X6RJpstl +cHboCoWASzY9M/eVVHUl2qzEc4Jl6VL1XP04lQJqaTDFHApXB64ipCz5xUG3uOyfT0gA+QEEVcys ++TIxxHWVBqB/0Y0n3bOppHKH/lmLmnp0Ft0WpWIp6zqW3IunaFnT63eROfjXy9mPX1onAX1daBli +2MjN9LdyR75bl87yraKZk62Uy5P2EgmVtqvXO9A/EcswFi55gORngS1d7XB4tmBZrOFdRWOPyN9y +aFvqHbgB8X7754qz41SgOAngPN5C8sLtLpvzHzW2NtjjgKGLzZlkD8Kqq7HK9W+eQ42EVJmzbsAS +ZthwEPEGNTNDqJwuuhQxzhB/HIbjj9LV+Hfsm6vxL2PZQl/gZ4FkkfGXL/xuJvYz+NO1+MRiqzFR +JQJ6+N1rZdVtTTDIZbpoFGWsJwt0ivKH +-----END CERTIFICATE----- + +TrustAsia Global Root CA G4 +=========================== +-----BEGIN CERTIFICATE----- +MIICVTCCAdygAwIBAgIUTyNkuI6XY57GU4HBdk7LKnQV1tcwCgYIKoZIzj0EAwMwWjELMAkGA1UE +BhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dpZXMsIEluYy4xJDAiBgNVBAMMG1Ry +dXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHNDAeFw0yMTA1MjAwMjEwMjJaFw00NjA1MTkwMjEwMjJa +MFoxCzAJBgNVBAYTAkNOMSUwIwYDVQQKDBxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQw +IgYDVQQDDBtUcnVzdEFzaWEgR2xvYmFsIFJvb3QgQ0EgRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AATxs8045CVD5d4ZCbuBeaIVXxVjAd7Cq92zphtnS4CDr5nLrBfbK5bKfFJV4hrhPVbwLxYI+hW8 +m7tH5j/uqOFMjPXTNvk4XatwmkcN4oFBButJ+bAp3TPsUKV/eSm4IJijYzBhMA8GA1UdEwEB/wQF +MAMBAf8wHwYDVR0jBBgwFoAUpbtKl86zK3+kMd6Xg1mDpm9xy94wHQYDVR0OBBYEFKW7SpfOsyt/ +pDHel4NZg6ZvccveMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNnADBkAjBe8usGzEkxn0AA +bbd+NvBNEU/zy4k6LHiRUKNbwMp1JvK/kF0LgoxgKJ/GcJpo5PECMFxYDlZ2z1jD1xCMuo6u47xk +dUfFVZDj/bpV6wfEU6s3qe4hsiFbYI89MvHVI5TWWA== +-----END CERTIFICATE----- + +Telekom Security TLS ECC Root 2020 +================================== +-----BEGIN CERTIFICATE----- +MIICQjCCAcmgAwIBAgIQNjqWjMlcsljN0AFdxeVXADAKBggqhkjOPQQDAzBjMQswCQYDVQQGEwJE +RTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0eSBHbWJIMSswKQYDVQQDDCJUZWxl +a29tIFNlY3VyaXR5IFRMUyBFQ0MgUm9vdCAyMDIwMB4XDTIwMDgyNTA3NDgyMFoXDTQ1MDgyNTIz +NTk1OVowYzELMAkGA1UEBhMCREUxJzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkg +R21iSDErMCkGA1UEAwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgRUNDIFJvb3QgMjAyMDB2MBAGByqG +SM49AgEGBSuBBAAiA2IABM6//leov9Wq9xCazbzREaK9Z0LMkOsVGJDZos0MKiXrPk/OtdKPD/M1 +2kOLAoC+b1EkHQ9rK8qfwm9QMuU3ILYg/4gND21Ju9sGpIeQkpT0CdDPf8iAC8GXs7s1J8nCG6NC +MEAwHQYDVR0OBBYEFONyzG6VmUex5rNhTNHLq+O6zd6fMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0P +AQH/BAQDAgEGMAoGCCqGSM49BAMDA2cAMGQCMHVSi7ekEE+uShCLsoRbQuHmKjYC2qBuGT8lv9pZ +Mo7k+5Dck2TOrbRBR2Diz6fLHgIwN0GMZt9Ba9aDAEH9L1r3ULRn0SyocddDypwnJJGDSA3PzfdU +ga/sf+Rn27iQ7t0l +-----END CERTIFICATE----- + +Telekom Security TLS RSA Root 2023 +================================== +-----BEGIN CERTIFICATE----- +MIIFszCCA5ugAwIBAgIQIZxULej27HF3+k7ow3BXlzANBgkqhkiG9w0BAQwFADBjMQswCQYDVQQG +EwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0eSBHbWJIMSswKQYDVQQDDCJU +ZWxla29tIFNlY3VyaXR5IFRMUyBSU0EgUm9vdCAyMDIzMB4XDTIzMDMyODEyMTY0NVoXDTQ4MDMy +NzIzNTk1OVowYzELMAkGA1UEBhMCREUxJzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJp +dHkgR21iSDErMCkGA1UEAwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgUlNBIFJvb3QgMjAyMzCCAiIw +DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAO01oYGA88tKaVvC+1GDrib94W7zgRJ9cUD/h3VC +KSHtgVIs3xLBGYSJwb3FKNXVS2xE1kzbB5ZKVXrKNoIENqil/Cf2SfHVcp6R+SPWcHu79ZvB7JPP +GeplfohwoHP89v+1VmLhc2o0mD6CuKyVU/QBoCcHcqMAU6DksquDOFczJZSfvkgdmOGjup5czQRx +UX11eKvzWarE4GC+j4NSuHUaQTXtvPM6Y+mpFEXX5lLRbtLevOP1Czvm4MS9Q2QTps70mDdsipWo +l8hHD/BeEIvnHRz+sTugBTNoBUGCwQMrAcjnj02r6LX2zWtEtefdi+zqJbQAIldNsLGyMcEWzv/9 +FIS3R/qy8XDe24tsNlikfLMR0cN3f1+2JeANxdKz+bi4d9s3cXFH42AYTyS2dTd4uaNir73Jco4v +zLuu2+QVUhkHM/tqty1LkCiCc/4YizWN26cEar7qwU02OxY2kTLvtkCJkUPg8qKrBC7m8kwOFjQg +rIfBLX7JZkcXFBGk8/ehJImr2BrIoVyxo/eMbcgByU/J7MT8rFEz0ciD0cmfHdRHNCk+y7AO+oML +KFjlKdw/fKifybYKu6boRhYPluV75Gp6SG12mAWl3G0eQh5C2hrgUve1g8Aae3g1LDj1H/1Joy7S +WWO/gLCMk3PLNaaZlSJhZQNg+y+TS/qanIA7AgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAdBgNV +HQ4EFgQUtqeXgj10hZv3PJ+TmpV5dVKMbUcwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBS2 +p5eCPXSFm/c8n5OalXl1UoxtRzANBgkqhkiG9w0BAQwFAAOCAgEAqMxhpr51nhVQpGv7qHBFfLp+ +sVr8WyP6Cnf4mHGCDG3gXkaqk/QeoMPhk9tLrbKmXauw1GLLXrtm9S3ul0A8Yute1hTWjOKWi0Fp +kzXmuZlrYrShF2Y0pmtjxrlO8iLpWA1WQdH6DErwM807u20hOq6OcrXDSvvpfeWxm4bu4uB9tPcy +/SKE8YXJN3nptT+/XOR0so8RYgDdGGah2XsjX/GO1WfoVNpbOms2b/mBsTNHM3dA+VKq3dSDz4V4 +mZqTuXNnQkYRIer+CqkbGmVps4+uFrb2S1ayLfmlyOw7YqPta9BO1UAJpB+Y1zqlklkg5LB9zVtz +aL1txKITDmcZuI1CfmwMmm6gJC3VRRvcxAIU/oVbZZfKTpBQCHpCNfnqwmbU+AGuHrS+w6jv/naa +oqYfRvaE7fzbzsQCzndILIyy7MMAo+wsVRjBfhnu4S/yrYObnqsZ38aKL4x35bcF7DvB7L6Gs4a8 +wPfc5+pbrrLMtTWGS9DiP7bY+A4A7l3j941Y/8+LN+ljX273CXE2whJdV/LItM3z7gLfEdxquVeE +HVlNjM7IDiPCtyaaEBRx/pOyiriA8A4QntOoUAw3gi/q4Iqd4Sw5/7W0cwDk90imc6y/st53BIe0 +o82bNSQ3+pCTE4FCxpgmdTdmQRCsu/WU48IxK63nI1bMNSWSs1A= +-----END CERTIFICATE----- + +FIRMAPROFESIONAL CA ROOT-A WEB +============================== +-----BEGIN CERTIFICATE----- +MIICejCCAgCgAwIBAgIQMZch7a+JQn81QYehZ1ZMbTAKBggqhkjOPQQDAzBuMQswCQYDVQQGEwJF +UzEcMBoGA1UECgwTRmlybWFwcm9mZXNpb25hbCBTQTEYMBYGA1UEYQwPVkFURVMtQTYyNjM0MDY4 +MScwJQYDVQQDDB5GSVJNQVBST0ZFU0lPTkFMIENBIFJPT1QtQSBXRUIwHhcNMjIwNDA2MDkwMTM2 +WhcNNDcwMzMxMDkwMTM2WjBuMQswCQYDVQQGEwJFUzEcMBoGA1UECgwTRmlybWFwcm9mZXNpb25h +bCBTQTEYMBYGA1UEYQwPVkFURVMtQTYyNjM0MDY4MScwJQYDVQQDDB5GSVJNQVBST0ZFU0lPTkFM +IENBIFJPT1QtQSBXRUIwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARHU+osEaR3xyrq89Zfe9MEkVz6 +iMYiuYMQYneEMy3pA4jU4DP37XcsSmDq5G+tbbT4TIqk5B/K6k84Si6CcyvHZpsKjECcfIr28jlg +st7L7Ljkb+qbXbdTkBgyVcUgt5SjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUk+FD +Y1w8ndYn81LsF7Kpryz3dvgwHQYDVR0OBBYEFJPhQ2NcPJ3WJ/NS7Beyqa8s93b4MA4GA1UdDwEB +/wQEAwIBBjAKBggqhkjOPQQDAwNoADBlAjAdfKR7w4l1M+E7qUW/Runpod3JIha3RxEL2Jq68cgL +cFBTApFwhVmpHqTm6iMxoAACMQD94vizrxa5HnPEluPBMBnYfubDl94cT7iJLzPrSA8Z94dGXSaQ +pYXFuXqUPoeovQA= +-----END CERTIFICATE----- + +TWCA CYBER Root CA +================== +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIQQAE0jMIAAAAAAAAAATzyxjANBgkqhkiG9w0BAQwFADBQMQswCQYDVQQG +EwJUVzESMBAGA1UEChMJVEFJV0FOLUNBMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJUV0NB +IENZQkVSIFJvb3QgQ0EwHhcNMjIxMTIyMDY1NDI5WhcNNDcxMTIyMTU1OTU5WjBQMQswCQYDVQQG +EwJUVzESMBAGA1UEChMJVEFJV0FOLUNBMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJUV0NB +IENZQkVSIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDG+Moe2Qkgfh1s +Ts6P40czRJzHyWmqOlt47nDSkvgEs1JSHWdyKKHfi12VCv7qze33Kc7wb3+szT3vsxxFavcokPFh +V8UMxKNQXd7UtcsZyoC5dc4pztKFIuwCY8xEMCDa6pFbVuYdHNWdZsc/34bKS1PE2Y2yHer43CdT +o0fhYcx9tbD47nORxc5zb87uEB8aBs/pJ2DFTxnk684iJkXXYJndzk834H/nY62wuFm40AZoNWDT +Nq5xQwTxaWV4fPMf88oon1oglWa0zbfuj3ikRRjpJi+NmykosaS3Om251Bw4ckVYsV7r8Cibt4LK +/c/WMw+f+5eesRycnupfXtuq3VTpMCEobY5583WSjCb+3MX2w7DfRFlDo7YDKPYIMKoNM+HvnKkH +IuNZW0CP2oi3aQiotyMuRAlZN1vH4xfyIutuOVLF3lSnmMlLIJXcRolftBL5hSmO68gnFSDAS9TM +fAxsNAwmmyYxpjyn9tnQS6Jk/zuZQXLB4HCX8SS7K8R0IrGsayIyJNN4KsDAoS/xUgXJP+92ZuJF +2A09rZXIx4kmyA+upwMu+8Ff+iDhcK2wZSA3M2Cw1a/XDBzCkHDXShi8fgGwsOsVHkQGzaRP6AzR +wyAQ4VRlnrZR0Bp2a0JaWHY06rc3Ga4udfmW5cFZ95RXKSWNOkyrTZpB0F8mAwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBSdhWEUfMFib5do5E83 +QOGt4A1WNzAdBgNVHQ4EFgQUnYVhFHzBYm+XaORPN0DhreANVjcwDQYJKoZIhvcNAQEMBQADggIB +AGSPesRiDrWIzLjHhg6hShbNcAu3p4ULs3a2D6f/CIsLJc+o1IN1KriWiLb73y0ttGlTITVX1olN +c79pj3CjYcya2x6a4CD4bLubIp1dhDGaLIrdaqHXKGnK/nZVekZn68xDiBaiA9a5F/gZbG0jAn/x +X9AKKSM70aoK7akXJlQKTcKlTfjF/biBzysseKNnTKkHmvPfXvt89YnNdJdhEGoHK4Fa0o635yDR +IG4kqIQnoVesqlVYL9zZyvpoBJ7tRCT5dEA7IzOrg1oYJkK2bVS1FmAwbLGg+LhBoF1JSdJlBTrq +/p1hvIbZv97Tujqxf36SNI7JAG7cmL3c7IAFrQI932XtCwP39xaEBDG6k5TY8hL4iuO/Qq+n1M0R +FxbIQh0UqEL20kCGoE8jypZFVmAGzbdVAaYBlGX+bgUJurSkquLvWL69J1bY73NxW0Qz8ppy6rBe +Pm6pUlvscG21h483XjyMnM7k8M4MZ0HMzvaAq07MTFb1wWFZk7Q+ptq4NxKfKjLji7gh7MMrZQzv +It6IKTtM1/r+t+FHvpw+PoP7UV31aPcuIYXcv/Fa4nzXxeSDwWrruoBa3lwtcHb4yOWHh8qgnaHl +IhInD0Q9HWzq1MKLL295q39QpsQZp6F6t5b5wR9iWqJDB0BeJsas7a5wFsWqynKKTbDPAYsDP27X +-----END CERTIFICATE----- + +SecureSign Root CA12 +==================== +-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIUZvnHwa/swlG07VOX5uaCwysckBYwDQYJKoZIhvcNAQELBQAwUTELMAkG +A1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28uLCBMdGQuMR0wGwYDVQQDExRT +ZWN1cmVTaWduIFJvb3QgQ0ExMjAeFw0yMDA0MDgwNTM2NDZaFw00MDA0MDgwNTM2NDZaMFExCzAJ +BgNVBAYTAkpQMSMwIQYDVQQKExpDeWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMU +U2VjdXJlU2lnbiBSb290IENBMTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6OcE3 +emhFKxS06+QT61d1I02PJC0W6K6OyX2kVzsqdiUzg2zqMoqUm048luT9Ub+ZyZN+v/mtp7JIKwcc +J/VMvHASd6SFVLX9kHrko+RRWAPNEHl57muTH2SOa2SroxPjcf59q5zdJ1M3s6oYwlkm7Fsf0uZl +fO+TvdhYXAvA42VvPMfKWeP+bl+sg779XSVOKik71gurFzJ4pOE+lEa+Ym6b3kaosRbnhW70CEBF +EaCeVESE99g2zvVQR9wsMJvuwPWW0v4JhscGWa5Pro4RmHvzC1KqYiaqId+OJTN5lxZJjfU+1Uef +NzFJM3IFTQy2VYzxV4+Kh9GtxRESOaCtAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0P +AQH/BAQDAgEGMB0GA1UdDgQWBBRXNPN0zwRL1SXm8UC2LEzZLemgrTANBgkqhkiG9w0BAQsFAAOC +AQEAPrvbFxbS8hQBICw4g0utvsqFepq2m2um4fylOqyttCg6r9cBg0krY6LdmmQOmFxv3Y67ilQi +LUoT865AQ9tPkbeGGuwAtEGBpE/6aouIs3YIcipJQMPTw4WJmBClnW8Zt7vPemVV2zfrPIpyMpce +mik+rY3moxtt9XUa5rBouVui7mlHJzWhhpmA8zNL4WukJsPvdFlseqJkth5Ew1DgDzk9qTPxpfPS +vWKErI4cqc1avTc7bgoitPQV55FYxTpE05Uo2cBl6XLK0A+9H7MV2anjpEcJnuDLN/v9vZfVvhga +aaI5gdka9at/yOPiZwud9AzqVN/Ssq+xIvEg37xEHA== +-----END CERTIFICATE----- + +SecureSign Root CA14 +==================== +-----BEGIN CERTIFICATE----- +MIIFcjCCA1qgAwIBAgIUZNtaDCBO6Ncpd8hQJ6JaJ90t8sswDQYJKoZIhvcNAQEMBQAwUTELMAkG +A1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28uLCBMdGQuMR0wGwYDVQQDExRT +ZWN1cmVTaWduIFJvb3QgQ0ExNDAeFw0yMDA0MDgwNzA2MTlaFw00NTA0MDgwNzA2MTlaMFExCzAJ +BgNVBAYTAkpQMSMwIQYDVQQKExpDeWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMU +U2VjdXJlU2lnbiBSb290IENBMTQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDF0nqh +1oq/FjHQmNE6lPxauG4iwWL3pwon71D2LrGeaBLwbCRjOfHw3xDG3rdSINVSW0KZnvOgvlIfX8xn +bacuUKLBl422+JX1sLrcneC+y9/3OPJH9aaakpUqYllQC6KxNedlsmGy6pJxaeQp8E+BgQQ8sqVb +1MWoWWd7VRxJq3qdwudzTe/NCcLEVxLbAQ4jeQkHO6Lo/IrPj8BGJJw4J+CDnRugv3gVEOuGTgpa +/d/aLIJ+7sr2KeH6caH3iGicnPCNvg9JkdjqOvn90Ghx2+m1K06Ckm9mH+Dw3EzsytHqunQG+bOE +kJTRX45zGRBdAuVwpcAQ0BB8b8VYSbSwbprafZX1zNoCr7gsfXmPvkPx+SgojQlD+Ajda8iLLCSx +jVIHvXiby8posqTdDEx5YMaZ0ZPxMBoH064iwurO8YQJzOAUbn8/ftKChazcqRZOhaBgy/ac18iz +ju3Gm5h1DVXoX+WViwKkrkMpKBGk5hIwAUt1ax5mnXkvpXYvHUC0bcl9eQjs0Wq2XSqypWa9a4X0 +dFbD9ed1Uigspf9mR6XU/v6eVL9lfgHWMI+lNpyiUBzuOIABSMbHdPTGrMNASRZhdCyvjG817XsY +AFs2PJxQDcqSMxDxJklt33UkN4Ii1+iW/RVLApY+B3KVfqs9TC7XyvDf4Fg/LS8EmjijAQIDAQAB +o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUBpOjCl4oaTeq +YR3r6/wtbyPk86AwDQYJKoZIhvcNAQEMBQADggIBAJaAcgkGfpzMkwQWu6A6jZJOtxEaCnFxEM0E +rX+lRVAQZk5KQaID2RFPeje5S+LGjzJmdSX7684/AykmjbgWHfYfM25I5uj4V7Ibed87hwriZLoA +ymzvftAj63iP/2SbNDefNWWipAA9EiOWWF3KY4fGoweITedpdopTzfFP7ELyk+OZpDc8h7hi2/Ds +Hzc/N19DzFGdtfCXwreFamgLRB7lUe6TzktuhsHSDCRZNhqfLJGP4xjblJUK7ZGqDpncllPjYYPG +FrojutzdfhrGe0K22VoF3Jpf1d+42kd92jjbrDnVHmtsKheMYc2xbXIBw8MgAGJoFjHVdqqGuw6q +nsb58Nn4DSEC5MUoFlkRudlpcyqSeLiSV5sI8jrlL5WwWLdrIBRtFO8KvH7YVdiI2i/6GaX7i+B/ +OfVyK4XELKzvGUWSTLNhB9xNH27SgRNcmvMSZ4PPmz+Ln52kuaiWA3rF7iDeM9ovnhp6dB7h7sxa +OgTdsxoEqBRjrLdHEoOabPXm6RUVkRqEGQ6UROcSjiVbgGcZ3GOTEAtlLor6CZpO2oYofaphNdgO +pygau1LgePhsumywbrmHXumZNTfxPWQrqaA0k89jL9WB365jJ6UeTo3cKXhZ+PmhIIynJkBugnLN +eLLIjzwec+fBH7/PzqUqm9tEZDKgu39cJRNItX+S +-----END CERTIFICATE----- + +SecureSign Root CA15 +==================== +-----BEGIN CERTIFICATE----- +MIICIzCCAamgAwIBAgIUFhXHw9hJp75pDIqI7fBw+d23PocwCgYIKoZIzj0EAwMwUTELMAkGA1UE +BhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28uLCBMdGQuMR0wGwYDVQQDExRTZWN1 +cmVTaWduIFJvb3QgQ0ExNTAeFw0yMDA0MDgwODMyNTZaFw00NTA0MDgwODMyNTZaMFExCzAJBgNV +BAYTAkpQMSMwIQYDVQQKExpDeWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2Vj +dXJlU2lnbiBSb290IENBMTUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQLUHSNZDKZmbPSYAi4Io5G +dCx4wCtELW1fHcmuS1Iggz24FG1Th2CeX2yF2wYUleDHKP+dX+Sq8bOLbe1PL0vJSpSRZHX+AezB +2Ot6lHhWGENfa4HL9rzatAy2KZMIaY+jQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMB0GA1UdDgQWBBTrQciu/NWeUUj1vYv0hyCTQSvT9DAKBggqhkjOPQQDAwNoADBlAjEA2S6J +fl5OpBEHvVnCB96rMjhTKkZEBhd6zlHp4P9mLQlO4E/0BdGF9jVg3PVys0Z9AjBEmEYagoUeYWmJ +SwdLZrWeqrqgHkHZAXQ6bkU6iYAZezKYVWOr62Nuk22rGwlgMU4= +-----END CERTIFICATE----- + +D-TRUST BR Root CA 2 2023 +========================= +-----BEGIN CERTIFICATE----- +MIIFqTCCA5GgAwIBAgIQczswBEhb2U14LnNLyaHcZjANBgkqhkiG9w0BAQ0FADBIMQswCQYDVQQG +EwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRSVVNUIEJSIFJvb3QgQ0Eg +MiAyMDIzMB4XDTIzMDUwOTA4NTYzMVoXDTM4MDUwOTA4NTYzMFowSDELMAkGA1UEBhMCREUxFTAT +BgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAGA1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDIgMjAyMzCC +AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK7/CVmRgApKaOYkP7in5Mg6CjoWzckjYaCT +cfKri3OPoGdlYNJUa2NRb0kz4HIHE304zQaSBylSa053bATTlfrdTIzZXcFhfUvnKLNEgXtRr90z +sWh81k5M/itoucpmacTsXld/9w3HnDY25QdgrMBM6ghs7wZ8T1soegj8k12b9py0i4a6Ibn08OhZ +WiihNIQaJZG2tY/vsvmA+vk9PBFy2OMvhnbFeSzBqZCTRphny4NqoFAjpzv2gTng7fC5v2Xx2Mt6 +++9zA84A9H3X4F07ZrjcjrqDy4d2A/wl2ecjbwb9Z/Pg/4S8R7+1FhhGaRTMBffb00msa8yr5LUL +QyReS2tNZ9/WtT5PeB+UcSTq3nD88ZP+npNa5JRal1QMNXtfbO4AHyTsA7oC9Xb0n9Sa7YUsOCIv +x9gvdhFP/Wxc6PWOJ4d/GUohR5AdeY0cW/jPSoXk7bNbjb7EZChdQcRurDhaTyN0dKkSw/bSuREV +MweR2Ds3OmMwBtHFIjYoYiMQ4EbMl6zWK11kJNXuHA7e+whadSr2Y23OC0K+0bpwHJwh5Q8xaRfX +/Aq03u2AnMuStIv13lmiWAmlY0cL4UEyNEHZmrHZqLAbWt4NDfTisl01gLmB1IRpkQLLddCNxbU9 +CZEJjxShFHR5PtbJFR2kWVki3PaKRT08EtY+XTIvAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQUZ5Dw1t61GNVGKX5cq/ieCLxklRAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRC +MEAwPqA8oDqGOGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfYnJfcm9vdF9jYV8y +XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQA097N3U9swFrktpSHxQCF16+tIFoE9c+CeJyrr +d6kTpGoKWloUMz1oH4Guaf2Mn2VsNELZLdB/eBaxOqwjMa1ef67nriv6uvw8l5VAk1/DLQOj7aRv +U9f6QA4w9QAgLABMjDu0ox+2v5Eyq6+SmNMW5tTRVFxDWy6u71cqqLRvpO8NVhTaIasgdp4D/Ca4 +nj8+AybmTNudX0KEPUUDAxxZiMrcLmEkWqTqJwtzEr5SswrPMhfiHocaFpVIbVrg0M8JkiZmkdij +YQ6qgYF/6FKC0ULn4B0Y+qSFNueG4A3rvNTJ1jxD8V1Jbn6Bm2m1iWKPiFLY1/4nwSPFyysCu7Ff +/vtDhQNGvl3GyiEm/9cCnnRK3PgTFbGBVzbLZVzRHTF36SXDw7IyN9XxmAnkbWOACKsGkoHU6XCP +pz+y7YaMgmo1yEJagtFSGkUPFaUA8JR7ZSdXOUPPfH/mvTWze/EZTN46ls/pdu4D58JDUjxqgejB +WoC9EV2Ta/vH5mQ/u2kc6d0li690yVRAysuTEwrt+2aSEcr1wPrYg1UDfNPFIkZ1cGt5SAYqgpq/ +5usWDiJFAbzdNpQ0qTUmiteXue4Icr80knCDgKs4qllo3UCkGJCy89UDyibK79XH4I9TjvAA46jt +n/mtd+ArY0+ew+43u3gJhJ65bvspmZDogNOfJA== +-----END CERTIFICATE----- + +TrustAsia TLS ECC Root CA +========================= +-----BEGIN CERTIFICATE----- +MIICMTCCAbegAwIBAgIUNnThTXxlE8msg1UloD5Sfi9QaMcwCgYIKoZIzj0EAwMwWDELMAkGA1UE +BhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dpZXMsIEluYy4xIjAgBgNVBAMTGVRy +dXN0QXNpYSBUTFMgRUNDIFJvb3QgQ0EwHhcNMjQwNTE1MDU0MTU2WhcNNDQwNTE1MDU0MTU1WjBY +MQswCQYDVQQGEwJDTjElMCMGA1UEChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEiMCAG +A1UEAxMZVHJ1c3RBc2lhIFRMUyBFQ0MgUm9vdCBDQTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLh/ +pVs/AT598IhtrimY4ZtcU5nb9wj/1WrgjstEpvDBjL1P1M7UiFPoXlfXTr4sP/MSpwDpguMqWzJ8 +S5sUKZ74LYO1644xST0mYekdcouJtgq7nDM1D9rs3qlKH8kzsaNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQULIVTu7FDzTLqnqOH/qKYqKaT6RAwDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49 +BAMDA2gAMGUCMFRH18MtYYZI9HlaVQ01L18N9mdsd0AaRuf4aFtOJx24mH1/k78ITcTaRTChD15K +eAIxAKORh/IRM4PDwYqROkwrULG9IpRdNYlzg8WbGf60oenUoWa2AaU2+dhoYSi3dOGiMQ== +-----END CERTIFICATE----- + +TrustAsia TLS RSA Root CA +========================= +-----BEGIN CERTIFICATE----- +MIIFgDCCA2igAwIBAgIUHBjYz+VTPyI1RlNUJDxsR9FcSpwwDQYJKoZIhvcNAQEMBQAwWDELMAkG +A1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dpZXMsIEluYy4xIjAgBgNVBAMT +GVRydXN0QXNpYSBUTFMgUlNBIFJvb3QgQ0EwHhcNMjQwNTE1MDU0MTU3WhcNNDQwNTE1MDU0MTU2 +WjBYMQswCQYDVQQGEwJDTjElMCMGA1UEChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEi +MCAGA1UEAxMZVHJ1c3RBc2lhIFRMUyBSU0EgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBAMMWuBtqpERz5dZO9LnPWwvB0ZqB9WOwj0PBuwhaGnrhB3YmH49pVr7+NmDQDIPN +lOrnxS1cLwUWAp4KqC/lYCZUlviYQB2srp10Zy9U+5RjmOMmSoPGlbYJQ1DNDX3eRA5gEk9bNb2/ +mThtfWza4mhzH/kxpRkQcwUqwzIZheo0qt1CHjCNP561HmHVb70AcnKtEj+qpklz8oYVlQwQX1Fk +zv93uMltrOXVmPGZLmzjyUT5tUMnCE32ft5EebuyjBza00tsLtbDeLdM1aTk2tyKjg7/D8OmYCYo +zza/+lcK7Fs/6TAWe8TbxNRkoDD75f0dcZLdKY9BWN4ArTr9PXwaqLEX8E40eFgl1oUh63kd0Nyr +z2I8sMeXi9bQn9P+PN7F4/w6g3CEIR0JwqH8uyghZVNgepBtljhb//HXeltt08lwSUq6HTrQUNoy +IBnkiz/r1RYmNzz7dZ6wB3C4FGB33PYPXFIKvF1tjVEK2sUYyJtt3LCDs3+jTnhMmCWr8n4uIF6C +FabW2I+s5c0yhsj55NqJ4js+k8UTav/H9xj8Z7XvGCxUq0DTbE3txci3OE9kxJRMT6DNrqXGJyV1 +J23G2pyOsAWZ1SgRxSHUuPzHlqtKZFlhaxP8S8ySpg+kUb8OWJDZgoM5pl+z+m6Ss80zDoWo8SnT +q1mt1tve1CuBAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFLgHkXlcBvRG/XtZ +ylomkadFK/hTMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQwFAAOCAgEAIZtqBSBdGBanEqT3 +Rz/NyjuujsCCztxIJXgXbODgcMTWltnZ9r96nBO7U5WS/8+S4PPFJzVXqDuiGev4iqME3mmL5Dw8 +veWv0BIb5Ylrc5tvJQJLkIKvQMKtuppgJFqBTQUYo+IzeXoLH5Pt7DlK9RME7I10nYEKqG/odv6L +TytpEoYKNDbdgptvT+Bz3Ul/KD7JO6NXBNiT2Twp2xIQaOHEibgGIOcberyxk2GaGUARtWqFVwHx +tlotJnMnlvm5P1vQiJ3koP26TpUJg3933FEFlJ0gcXax7PqJtZwuhfG5WyRasQmr2soaB82G39tp +27RIGAAtvKLEiUUjpQ7hRGU+isFqMB3iYPg6qocJQrmBktwliJiJ8Xw18WLK7nn4GS/+X/jbh87q +qA8MpugLoDzga5SYnH+tBuYc6kIQX+ImFTw3OffXvO645e8D7r0i+yiGNFjEWn9hongPXvPKnbwb +PKfILfanIhHKA9jnZwqKDss1jjQ52MjqjZ9k4DewbNfFj8GQYSbbJIweSsCI3zWQzj8C9GRh3sfI +B5XeMhg6j6JCQCTl1jNdfK7vsU1P1FeQNWrcrgSXSYk0ly4wBOeY99sLAZDBHwo/+ML+TvrbmnNz +FrwFuHnYWa8G5z9nODmxfKuU4CkUpijy323imttUQ/hHWKNddBWcwauwxzQ= +-----END CERTIFICATE----- + +D-TRUST EV Root CA 2 2023 +========================= +-----BEGIN CERTIFICATE----- +MIIFqTCCA5GgAwIBAgIQaSYJfoBLTKCnjHhiU19abzANBgkqhkiG9w0BAQ0FADBIMQswCQYDVQQG +EwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRSVVNUIEVWIFJvb3QgQ0Eg +MiAyMDIzMB4XDTIzMDUwOTA5MTAzM1oXDTM4MDUwOTA5MTAzMlowSDELMAkGA1UEBhMCREUxFTAT +BgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAGA1UEAxMZRC1UUlVTVCBFViBSb290IENBIDIgMjAyMzCC +AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANiOo4mAC7JXUtypU0w3uX9jFxPvp1sjW2l1 +sJkKF8GLxNuo4MwxusLyzV3pt/gdr2rElYfXR8mV2IIEUD2BCP/kPbOx1sWy/YgJ25yE7CUXFId/ +MHibaljJtnMoPDT3mfd/06b4HEV8rSyMlD/YZxBTfiLNTiVR8CUkNRFeEMbsh2aJgWi6zCudR3Mf +vc2RpHJqnKIbGKBv7FD0fUDCqDDPvXPIEysQEx6Lmqg6lHPTGGkKSv/BAQP/eX+1SH977ugpbzZM +lWGG2Pmic4ruri+W7mjNPU0oQvlFKzIbRlUWaqZLKfm7lVa/Rh3sHZMdwGWyH6FDrlaeoLGPaxK3 +YG14C8qKXO0elg6DpkiVjTujIcSuWMYAsoS0I6SWhjW42J7YrDRJmGOVxcttSEfi8i4YHtAxq910 +7PncjLgcjmgjutDzUNzPZY9zOjLHfP7KgiJPvo5iR2blzYfi6NUPGJ/lBHJLRjwQ8kTCZFZxTnXo +nMkmdMV9WdEKWw9t/p51HBjGGjp82A0EzM23RWV6sY+4roRIPrN6TagD4uJ+ARZZaBhDM7DS3LAa +QzXupdqpRlyuhoFBAUp0JuyfBr/CBTdkdXgpaP3F9ev+R/nkhbDhezGdpn9yo7nELC7MmVcOIQxF +AZRl62UJxmMiCzNJkkg8/M3OsD6Onov4/knFNXJHAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQUqvyREBuHkV8Wub9PS5FeAByxMoAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRC +MEAwPqA8oDqGOGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfZXZfcm9vdF9jYV8y +XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQCTy6UfmRHsmg1fLBWTxj++EI14QvBukEdHjqOS +Mo1wj/Zbjb6JzkcBahsgIIlbyIIQbODnmaprxiqgYzWRaoUlrRc4pZt+UPJ26oUFKidBK7GB0aL2 +QHWpDsvxVUjY7NHss+jOFKE17MJeNRqrphYBBo7q3C+jisosketSjl8MmxfPy3MHGcRqwnNU73xD +UmPBEcrCRbH0O1P1aa4846XerOhUt7KR/aypH/KH5BfGSah82ApB9PI+53c0BFLd6IHyTS9URZ0V +4U/M5d40VxDJI3IXcI1QcB9WbMy5/zpaT2N6w25lBx2Eof+pDGOJbbJAiDnXH3dotfyc1dZnaVuo +dNv8ifYbMvekJKZ2t0dT741Jj6m2g1qllpBFYfXeA08mD6iL8AOWsKwV0HFaanuU5nCT2vFp4LJi +TZ6P/4mdm13NRemUAiKN4DV/6PEEeXFsVIP4M7kFMhtYVRFP0OUnR3Hs7dpn1mKmS00PaaLJvOwi +S5THaJQXfuKOKD62xur1NGyfN4gHONuGcfrNlUhDbqNPgofXNJhuS5N5YHVpD/Aa1VP6IQzCP+k/ +HxiMkl14p3ZnGbuy6n/pcAlWVqOwDAstNl7F6cTVg8uGF5csbBNvh1qvSaYd2804BC5f4ko1Di1L ++KIkBI3Y4WNeApI02phhXBxvWHZks/wCuPWdCg== +-----END CERTIFICATE----- + +SwissSign RSA TLS Root CA 2022 - 1 +================================== +-----BEGIN CERTIFICATE----- +MIIFkzCCA3ugAwIBAgIUQ/oMX04bgBhE79G0TzUfRPSA7cswDQYJKoZIhvcNAQELBQAwUTELMAkG +A1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzErMCkGA1UEAxMiU3dpc3NTaWduIFJTQSBU +TFMgUm9vdCBDQSAyMDIyIC0gMTAeFw0yMjA2MDgxMTA4MjJaFw00NzA2MDgxMTA4MjJaMFExCzAJ +BgNVBAYTAkNIMRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxKzApBgNVBAMTIlN3aXNzU2lnbiBSU0Eg +VExTIFJvb3QgQ0EgMjAyMiAtIDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDLKmji +C8NXvDVjvHClO/OMPE5Xlm7DTjak9gLKHqquuN6orx122ro10JFwB9+zBvKK8i5VUXu7LCTLf5Im +gKO0lPaCoaTo+nUdWfMHamFk4saMla+ju45vVs9xzF6BYQ1t8qsCLqSX5XH8irCRIFucdFJtrhUn +WXjyCcplDn/L9Ovn3KlMd/YrFgSVrpxxpT8q2kFC5zyEEPThPYxr4iuRR1VPuFa+Rd4iUU1OKNlf +GUEGjw5NBuBwQCMBauTLE5tzrE0USJIt/m2n+IdreXXhvhCxqohAWVTXz8TQm0SzOGlkjIHRI36q +OTw7D59Ke4LKa2/KIj4x0LDQKhySio/YGZxH5D4MucLNvkEM+KRHBdvBFzA4OmnczcNpI/2aDwLO +EGrOyvi5KaM2iYauC8BPY7kGWUleDsFpswrzd34unYyzJ5jSmY0lpx+Gs6ZUcDj8fV3oT4MM0ZPl +EuRU2j7yrTrePjxF8CgPBrnh25d7mUWe3f6VWQQvdT/TromZhqwUtKiE+shdOxtYk8EXlFXIC+OC +eYSf8wCENO7cMdWP8vpPlkwGqnj73mSiI80fPsWMvDdUDrtaclXvyFu1cvh43zcgTFeRc5JzrBh3 +Q4IgaezprClG5QtO+DdziZaKHG29777YtvTKwP1H8K4LWCDFyB02rpeNUIMmJCn3nTsPBQIDAQAB +o2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBRvjmKLk0Ow +4UD2p8P98Q+4DxU4pTAdBgNVHQ4EFgQUb45ii5NDsOFA9qfD/fEPuA8VOKUwDQYJKoZIhvcNAQEL +BQADggIBAKwsKUF9+lz1GpUYvyypiqkkVHX1uECry6gkUSsYP2OprphWKwVDIqO310aewCoSPY6W +lkDfDDOLazeROpW7OSltwAJsipQLBwJNGD77+3v1dj2b9l4wBlgzHqp41eZUBDqyggmNzhYzWUUo +8aWjlw5DI/0LIICQ/+Mmz7hkkeUFjxOgdg3XNwwQiJb0Pr6VvfHDffCjw3lHC1ySFWPtUnWK50Zp +y1FVCypM9fJkT6lc/2cyjlUtMoIcgC9qkfjLvH4YoiaoLqNTKIftV+Vlek4ASltOU8liNr3Cjlvr +zG4ngRhZi0Rjn9UMZfQpZX+RLOV/fuiJz48gy20HQhFRJjKKLjpHE7iNvUcNCfAWpO2Whi4Z2L6M +OuhFLhG6rlrnub+xzI/goP+4s9GFe3lmozm1O2bYQL7Pt2eLSMkZJVX8vY3PXtpOpvJpzv1/THfQ +wUY1mFwjmwJFQ5Ra3bxHrSL+ul4vkSkphnsh3m5kt8sNjzdbowhq6/TdAo9QAwKxuDdollDruF/U +KIqlIgyKhPBZLtU30WHlQnNYKoH3dtvi4k0NX/a3vgW0rk4N3hY9A4GzJl5LuEsAz/+MF7psYC0n +hzck5npgL7XTgwSqT0N1osGDsieYK7EOgLrAhV5Cud+xYJHT6xh+cHiudoO+cVrQkOPKwRYlZ0rw +tnu64ZzZ +-----END CERTIFICATE----- + +OISTE Server Root ECC G1 +======================== +-----BEGIN CERTIFICATE----- +MIICNTCCAbqgAwIBAgIQI/nD1jWvjyhLH/BU6n6XnTAKBggqhkjOPQQDAzBLMQswCQYDVQQGEwJD +SDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UEAwwYT0lTVEUgU2VydmVyIFJvb3Qg +RUNDIEcxMB4XDTIzMDUzMTE0NDIyOFoXDTQ4MDUyNDE0NDIyN1owSzELMAkGA1UEBhMCQ0gxGTAX +BgNVBAoMEE9JU1RFIEZvdW5kYXRpb24xITAfBgNVBAMMGE9JU1RFIFNlcnZlciBSb290IEVDQyBH +MTB2MBAGByqGSM49AgEGBSuBBAAiA2IABBcv+hK8rBjzCvRE1nZCnrPoH7d5qVi2+GXROiFPqOuj +vqQycvO2Ackr/XeFblPdreqqLiWStukhEaivtUwL85Zgmjvn6hp4LrQ95SjeHIC6XG4N2xml4z+c +KrhAS93mT6NjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBQ3TYhlz/w9itWj8UnATgwQ +b0K0nDAdBgNVHQ4EFgQUN02IZc/8PYrVo/FJwE4MEG9CtJwwDgYDVR0PAQH/BAQDAgGGMAoGCCqG +SM49BAMDA2kAMGYCMQCpKjAd0MKfkFFRQD6VVCHNFmb3U2wIFjnQEnx/Yxvf4zgAOdktUyBFCxxg +ZzFDJe0CMQCSia7pXGKDYmH5LVerVrkR3SW+ak5KGoJr3M/TvEqzPNcum9v4KGm8ay3sMaE641c= +-----END CERTIFICATE----- + + OISTE Server Root RSA G1 +========================= +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIQVaXZZ5Qoxu0M+ifdWwFNGDANBgkqhkiG9w0BAQwFADBLMQswCQYDVQQG +EwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UEAwwYT0lTVEUgU2VydmVyIFJv +b3QgUlNBIEcxMB4XDTIzMDUzMTE0MzcxNloXDTQ4MDUyNDE0MzcxNVowSzELMAkGA1UEBhMCQ0gx +GTAXBgNVBAoMEE9JU1RFIEZvdW5kYXRpb24xITAfBgNVBAMMGE9JU1RFIFNlcnZlciBSb290IFJT +QSBHMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKqu9KuCz/vlNwvn1ZatkOhLKdxV +YOPMvLO8LZK55KN68YG0nnJyQ98/qwsmtO57Gmn7KNByXEptaZnwYx4M0rH/1ow00O7brEi56rAU +jtgHqSSY3ekJvqgiG1k50SeH3BzN+Puz6+mTeO0Pzjd8JnduodgsIUzkik/HEzxux9UTl7Ko2yRp +g1bTacuCErudG/L4NPKYKyqOBGf244ehHa1uzjZ0Dl4zO8vbUZeUapU8zhhabkvG/AePLhq5Svdk +NCncpo1Q4Y2LS+VIG24ugBA/5J8bZT8RtOpXaZ+0AOuFJJkk9SGdl6r7NH8CaxWQrbueWhl/pIzY ++m0o/DjH40ytas7ZTpOSjswMZ78LS5bOZmdTaMsXEY5Z96ycG7mOaES3GK/m5Q9l3JUJsJMStR8+ +lKXHiHUhsd4JJCpM4rzsTGdHwimIuQq6+cF0zowYJmXa92/GjHtoXAvuY8BeS/FOzJ8vD+HomnqT +8eDI278n5mUpezbgMxVz8p1rhAhoKzYHKyfMeNhqhw5HdPSqoBNdZH702xSu+zrkL8Fl47l6QGzw +Brd7KJvX4V84c5Ss2XCTLdyEr0YconosP4EmQufU2MVshGYRi3drVByjtdgQ8K4p92cIiBdcuJd5 +z+orKu5YM+Vt6SmqZQENghPsJQtdLEByFSnTkCz3GkPVavBpAgMBAAGjYzBhMA8GA1UdEwEB/wQF +MAMBAf8wHwYDVR0jBBgwFoAU8snBDw1jALvsRQ5KH7WxszbNDo0wHQYDVR0OBBYEFPLJwQ8NYwC7 +7EUOSh+1sbM2zQ6NMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQwFAAOCAgEANGd5sjrG5T33 +I3K5Ce+SrScfoE4KsvXaFwyihdJ+klH9FWXXXGtkFu6KRcoMQzZENdl//nk6HOjG5D1rd9QhEOP2 +8yBOqb6J8xycqd+8MDoX0TJD0KqKchxRKEzdNsjkLWd9kYccnbz8qyiWXmFcuCIzGEgWUOrKL+ml +Sdx/PKQZvDatkuK59EvV6wit53j+F8Bdh3foZ3dPAGav9LEDOr4SfEE15fSmG0eLy3n31r8Xbk5l +8PjaV8GUgeV6Vg27Rn9vkf195hfkgSe7BYhW3SCl95gtkRlpMV+bMPKZrXJAlszYd2abtNUOshD+ +FKrDgHGdPY3ofRRsYWSGRqbXVMW215AWRqWFyp464+YTFrYVI8ypKVL9AMb2kI5Wj4kI3Zaq5tNq +qYY19tVFeEJKRvwDyF7YZvZFZSS0vod7VSCd9521Kvy5YhnLbDuv0204bKt7ph6N/Ome/msVuduC +msuY33OhkKCgxeDoAaijFJzIwZqsFVAzje18KotzlUBDJvyBpCpfOZC3J8tRd/iWkx7P8nd9H0aT +olkelUTFLXVksNb54Dxp6gS1HAviRkRNQzuXSXERvSS2wq1yVAb+axj5d9spLFKebXd7Yv0PTY6Y +MjAwcRLWJTXjn/hvnLXrahut6hDTlhZyBiElxky8j3C7DOReIoMt0r7+hVu05L0= +-----END CERTIFICATE----- diff --git a/FreeFileSync/Build/Resources/ding.wav b/FreeFileSync/Build/Resources/ding.wav new file mode 100644 index 0000000..256844e Binary files /dev/null and b/FreeFileSync/Build/Resources/ding.wav differ diff --git a/FreeFileSync/Build/Resources/fail.wav b/FreeFileSync/Build/Resources/fail.wav new file mode 100644 index 0000000..50a8d08 Binary files /dev/null and b/FreeFileSync/Build/Resources/fail.wav differ diff --git a/FreeFileSync/Build/Resources/fail2.wav b/FreeFileSync/Build/Resources/fail2.wav new file mode 100644 index 0000000..1b6a8c3 Binary files /dev/null and b/FreeFileSync/Build/Resources/fail2.wav differ diff --git a/FreeFileSync/Build/Resources/gong.wav b/FreeFileSync/Build/Resources/gong.wav new file mode 100644 index 0000000..e01130c Binary files /dev/null and b/FreeFileSync/Build/Resources/gong.wav differ diff --git a/FreeFileSync/Build/Resources/harp.wav b/FreeFileSync/Build/Resources/harp.wav new file mode 100644 index 0000000..87742dc Binary files /dev/null and b/FreeFileSync/Build/Resources/harp.wav differ diff --git a/FreeFileSync/Build/Resources/notify.wav b/FreeFileSync/Build/Resources/notify.wav new file mode 100644 index 0000000..5263e32 Binary files /dev/null and b/FreeFileSync/Build/Resources/notify.wav differ diff --git a/FreeFileSync/Build/Resources/notify2.wav b/FreeFileSync/Build/Resources/notify2.wav new file mode 100644 index 0000000..7f8d558 Binary files /dev/null and b/FreeFileSync/Build/Resources/notify2.wav differ diff --git a/FreeFileSync/Build/Resources/remind.wav b/FreeFileSync/Build/Resources/remind.wav new file mode 100644 index 0000000..d5b4704 Binary files /dev/null and b/FreeFileSync/Build/Resources/remind.wav differ diff --git a/FreeFileSync/Source/Makefile b/FreeFileSync/Source/Makefile new file mode 100644 index 0000000..9a9e8aa --- /dev/null +++ b/FreeFileSync/Source/Makefile @@ -0,0 +1,135 @@ +CXX ?= g++ +exeName = FreeFileSync_$(shell arch) + +CXXFLAGS += -std=c++23 -pipe -DWXINTL_NO_GETTEXT_MACRO -I../.. -I../../zenXml -include "zen/i18n.h" -include "zen/warn_static.h" \ + -Wall -Wfatal-errors -Wmissing-include-dirs -Wswitch-enum -Wcast-align -Wnon-virtual-dtor -Wno-unused-function -Wshadow -Wno-maybe-uninitialized \ + -O3 -DNDEBUG `wx-config --cxxflags --debug=no` -pthread + +LDFLAGS += -s `wx-config --libs std, aui, richtext --debug=no` -pthread + + +CXXFLAGS += `pkg-config --cflags openssl` +LDFLAGS += `pkg-config --libs openssl` + +CXXFLAGS += `pkg-config --cflags libcurl` +LDFLAGS += `pkg-config --libs libcurl` + +CXXFLAGS += `pkg-config --cflags libidn2` +LDFLAGS += `pkg-config --libs libidn2` + +CXXFLAGS += `pkg-config --cflags libssh2` +LDFLAGS += `pkg-config --libs libssh2` + +CXXFLAGS += `pkg-config --cflags gtk+-3.0` +#treat as system headers so that warnings are hidden: +CXXFLAGS += -isystem/usr/include/gtk-3.0 + +#support for SELinux (optional) +SELINUX_EXISTING=$(shell pkg-config --exists libselinux && echo YES) +ifeq ($(SELINUX_EXISTING),YES) +CXXFLAGS += `pkg-config --cflags libselinux` -DHAVE_SELINUX +LDFLAGS += `pkg-config --libs libselinux` +endif + +cppFiles= +cppFiles+=application.cpp +cppFiles+=base_tools.cpp +cppFiles+=config.cpp +cppFiles+=ffs_paths.cpp +cppFiles+=icon_buffer.cpp +cppFiles+=localization.cpp +cppFiles+=log_file.cpp +cppFiles+=status_handler.cpp +cppFiles+=base/algorithm.cpp +cppFiles+=base/binary.cpp +cppFiles+=base/comparison.cpp +cppFiles+=base/db_file.cpp +cppFiles+=base/dir_lock.cpp +cppFiles+=base/file_hierarchy.cpp +cppFiles+=base/icon_loader.cpp +cppFiles+=base/multi_rename.cpp +cppFiles+=base/parallel_scan.cpp +cppFiles+=base/path_filter.cpp +cppFiles+=base/speed_test.cpp +cppFiles+=base/structures.cpp +cppFiles+=base/synchronization.cpp +cppFiles+=base/versioning.cpp +cppFiles+=afs/abstract.cpp +cppFiles+=afs/concrete.cpp +cppFiles+=afs/ftp.cpp +cppFiles+=afs/gdrive.cpp +cppFiles+=afs/init_curl_libssh2.cpp +cppFiles+=afs/native.cpp +cppFiles+=afs/sftp.cpp +cppFiles+=ui/batch_config.cpp +cppFiles+=ui/abstract_folder_picker.cpp +cppFiles+=ui/batch_status_handler.cpp +cppFiles+=ui/cfg_grid.cpp +cppFiles+=ui/command_box.cpp +cppFiles+=ui/folder_history_box.cpp +cppFiles+=ui/folder_selector.cpp +cppFiles+=ui/file_grid.cpp +cppFiles+=ui/file_view.cpp +cppFiles+=ui/log_panel.cpp +cppFiles+=ui/tree_grid.cpp +cppFiles+=ui/gui_generated.cpp +cppFiles+=ui/gui_status_handler.cpp +cppFiles+=ui/main_dlg.cpp +cppFiles+=ui/progress_indicator.cpp +cppFiles+=ui/rename_dlg.cpp +cppFiles+=ui/search_grid.cpp +cppFiles+=ui/small_dlgs.cpp +cppFiles+=ui/sync_cfg.cpp +cppFiles+=ui/tray_icon.cpp +cppFiles+=ui/triple_splitter.cpp +cppFiles+=ui/version_check.cpp +cppFiles+=../../libcurl/curl_wrap.cpp +cppFiles+=../../zen/argon2.cpp +cppFiles+=../../zen/file_access.cpp +cppFiles+=../../zen/file_io.cpp +cppFiles+=../../zen/file_path.cpp +cppFiles+=../../zen/file_traverser.cpp +cppFiles+=../../zen/http.cpp +cppFiles+=../../zen/zstring.cpp +cppFiles+=../../zen/format_unit.cpp +cppFiles+=../../zen/legacy_compiler.cpp +cppFiles+=../../zen/open_ssl.cpp +cppFiles+=../../zen/process_priority.cpp +cppFiles+=../../zen/recycler.cpp +cppFiles+=../../zen/resolve_path.cpp +cppFiles+=../../zen/process_exec.cpp +cppFiles+=../../zen/shutdown.cpp +cppFiles+=../../zen/sys_error.cpp +cppFiles+=../../zen/sys_info.cpp +cppFiles+=../../zen/sys_version.cpp +cppFiles+=../../zen/thread.cpp +cppFiles+=../../zen/zlib_wrap.cpp +cppFiles+=../../wx+/darkmode.cpp +cppFiles+=../../wx+/file_drop.cpp +cppFiles+=../../wx+/grid.cpp +cppFiles+=../../wx+/image_tools.cpp +cppFiles+=../../wx+/graph.cpp +cppFiles+=../../wx+/taskbar.cpp +cppFiles+=../../wx+/tooltip.cpp +cppFiles+=../../wx+/image_resources.cpp +cppFiles+=../../wx+/popup_dlg.cpp +cppFiles+=../../wx+/popup_dlg_generated.cpp +cppFiles+=../../xBRZ/src/xbrz.cpp + +tmpPath = $(shell dirname "$(shell mktemp -u)")/$(exeName)_Make + +objFiles = $(cppFiles:%=$(tmpPath)/ffs/src/%.o) + +all: ../Build/Bin/$(exeName) + +../Build/Bin/$(exeName): $(objFiles) + mkdir -p $(dir $@) + $(CXX) -o $@ $^ $(LDFLAGS) + +$(tmpPath)/ffs/src/%.o : % + mkdir -p $(dir $@) + $(CXX) $(CXXFLAGS) -c $< -o $@ + +clean: + rm -rf $(tmpPath) + rm -f ../Build/Bin/$(exeName) diff --git a/FreeFileSync/Source/RealTimeSync/Makefile b/FreeFileSync/Source/RealTimeSync/Makefile new file mode 100644 index 0000000..b43e284 --- /dev/null +++ b/FreeFileSync/Source/RealTimeSync/Makefile @@ -0,0 +1,68 @@ +CXX ?= g++ +exeName = RealTimeSync_$(shell arch) + +CXXFLAGS += -std=c++23 -pipe -DWXINTL_NO_GETTEXT_MACRO -I../../.. -I../../../zenXml -include "zen/i18n.h" -include "zen/warn_static.h" \ + -Wall -Wfatal-errors -Wmissing-include-dirs -Wswitch-enum -Wcast-align -Wnon-virtual-dtor -Wno-unused-function -Wshadow -Wno-maybe-uninitialized \ + -O3 -DNDEBUG `wx-config --cxxflags --debug=no` -pthread + +LDFLAGS += -s `wx-config --libs std, aui, richtext --debug=no` -pthread + + +CXXFLAGS += `pkg-config --cflags gtk+-3.0` +#treat as system headers so that warnings are hidden: +CXXFLAGS += -isystem/usr/include/gtk-3.0 + +cppFiles= +cppFiles+=application.cpp +cppFiles+=config.cpp +cppFiles+=gui_generated.cpp +cppFiles+=main_dlg.cpp +cppFiles+=tray_menu.cpp +cppFiles+=monitor.cpp +cppFiles+=folder_selector2.cpp +cppFiles+=../afs/abstract.cpp +cppFiles+=../base/icon_loader.cpp +cppFiles+=../ffs_paths.cpp +cppFiles+=../icon_buffer.cpp +cppFiles+=../localization.cpp +cppFiles+=../../../wx+/darkmode.cpp +cppFiles+=../../../wx+/file_drop.cpp +cppFiles+=../../../wx+/image_tools.cpp +cppFiles+=../../../wx+/image_resources.cpp +cppFiles+=../../../wx+/popup_dlg.cpp +cppFiles+=../../../wx+/popup_dlg_generated.cpp +cppFiles+=../../../wx+/taskbar.cpp +cppFiles+=../../../xBRZ/src/xbrz.cpp +cppFiles+=../../../zen/dir_watcher.cpp +cppFiles+=../../../zen/file_access.cpp +cppFiles+=../../../zen/file_io.cpp +cppFiles+=../../../zen/file_path.cpp +cppFiles+=../../../zen/file_traverser.cpp +cppFiles+=../../../zen/format_unit.cpp +cppFiles+=../../../zen/legacy_compiler.cpp +cppFiles+=../../../zen/resolve_path.cpp +cppFiles+=../../../zen/process_exec.cpp +cppFiles+=../../../zen/shutdown.cpp +cppFiles+=../../../zen/sys_error.cpp +cppFiles+=../../../zen/sys_info.cpp +cppFiles+=../../../zen/sys_version.cpp +cppFiles+=../../../zen/thread.cpp +cppFiles+=../../../zen/zstring.cpp + +tmpPath = $(shell dirname "$(shell mktemp -u)")/$(exeName)_Make + +objFiles = $(cppFiles:%=$(tmpPath)/ffs/src/rts/%.o) + +all: ../../Build/Bin/$(exeName) + +../../Build/Bin/$(exeName): $(objFiles) + mkdir -p $(dir $@) + $(CXX) -o $@ $^ $(LDFLAGS) + +$(tmpPath)/ffs/src/rts/%.o : % + mkdir -p $(dir $@) + $(CXX) $(CXXFLAGS) -c $< -o $@ + +clean: + rm -rf $(tmpPath) + rm -f ../../Build/Bin/$(exeName) diff --git a/FreeFileSync/Source/RealTimeSync/app_icon.h b/FreeFileSync/Source/RealTimeSync/app_icon.h new file mode 100644 index 0000000..2e47322 --- /dev/null +++ b/FreeFileSync/Source/RealTimeSync/app_icon.h @@ -0,0 +1,27 @@ +// ***************************************************************************** +// * 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 APP_ICON_H_8914578394545342 +#define APP_ICON_H_8914578394545342 + +#include +#include + +namespace zen +{ +inline +wxIcon getRtsIcon() //see FFS/app_icon.h +{ + assert(loadImage("RealTimeSync").GetWidth () == loadImage("RealTimeSync").GetHeight() && + loadImage("RealTimeSync").GetWidth() == dipToScreen(128)); + wxIcon icon; + icon.CopyFromBitmap(loadImage("RealTimeSync", dipToScreen(64))); + return icon; + +} +} + +#endif //APP_ICON_H_8914578394545342 diff --git a/FreeFileSync/Source/RealTimeSync/application.cpp b/FreeFileSync/Source/RealTimeSync/application.cpp new file mode 100644 index 0000000..8f72c0c --- /dev/null +++ b/FreeFileSync/Source/RealTimeSync/application.cpp @@ -0,0 +1,223 @@ +// ***************************************************************************** +// * 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 "application.h" +#include "main_dlg.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include "config.h" +#include "../localization.h" +#include "../ffs_paths.h" +#include "../return_codes.h" + + #include + +using namespace zen; +using namespace rts; + +using fff::FfsExitCode; + +#ifdef __WXGTK3__ //deprioritize Wayland: see FFS' application.cpp + GLOBAL_RUN_ONCE(::gdk_set_allowed_backends("x11,*")); //call *before* gtk_init() +#endif + +IMPLEMENT_APP(Application) + + +namespace +{ +void notifyAppError(const std::wstring& msg) +{ + //error handling strategy unknown and no sync log output available at this point! + std::cerr << utfTo(_("Error") + L": " + msg) + '\n'; + //alternative0: std::wcerr: cannot display non-ASCII at all, so why does it exist??? + //alternative1: wxSafeShowMessage => NO console output on Debian x86, WTF! + //alternative2: wxMessageBox() => works, but we probably shouldn't block during command line usage +} +} + + +bool Application::OnInit() +{ + //do not call wxApp::OnInit() to avoid using wxWidgets command line parser + + initExtraLog([](const ErrorLog& log) //don't call functions depending on global state (which might be destroyed already!) + { + std::wstring msg; + for (const LogEntry& e : log) + msg += utfTo(formatMessage(e)); + trim(msg); + notifyAppError(msg); + }); + + //tentatively set program language to OS default until GlobalSettings.xml is read later + try { fff::localizationInit(appendPath(fff::getResourceDirPath(), Zstr("Languages.zip"))); } //throw FileError + catch (const FileError& e) { logExtraError(e.toString()); } + + GlobalConfig globalCfg; + try { globalCfg = getGlobalConfig(); } //throw FileError + catch (const FileError& e) { logExtraError(e.toString()); } + + try { fff::setLanguage(globalCfg.programLanguage); } //throw FileError + catch (const FileError& e) { logExtraError(e.toString()); } + + try { imageResourcesInit(appendPath(fff::getResourceDirPath(), Zstr("Icons.zip"))); } + catch (const FileError& e) { logExtraError(e.toString()); } //not critical in this context + + //GTK should already have been initialized by wxWidgets (see \src\gtk\app.cpp:wxApp::Initialize) +#if GTK_MAJOR_VERSION == 2 + ::gtk_rc_parse(appendPath(fff::getResourceDirPath(), "Gtk2Styles.rc").c_str()); + + //fix hang on Ubuntu 19.10 (see FFS's application.cpp) + [[maybe_unused]] GVfs* defaultFs = ::g_vfs_get_default(); //not owned by us! + +#elif GTK_MAJOR_VERSION == 3 + auto loadCSS = [&](const char* fileName) + { + GtkCssProvider* provider = ::gtk_css_provider_new(); + ZEN_ON_SCOPE_EXIT(::g_object_unref(provider)); + + GError* error = nullptr; + ZEN_ON_SCOPE_EXIT(if (error) ::g_error_free(error)); + + ::gtk_css_provider_load_from_path(provider, //GtkCssProvider* css_provider + appendPath(fff::getResourceDirPath(), fileName).c_str(), //const gchar* path + &error); //GError** error + if (error) + throw SysError(formatGlibError("gtk_css_provider_load_from_path", error)); + + ::gtk_style_context_add_provider_for_screen(::gdk_screen_get_default(), //GdkScreen* screen + GTK_STYLE_PROVIDER(provider), //GtkStyleProvider* provider + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); //guint priority + }; + try + { + loadCSS("Gtk3Styles.css"); //throw SysError + } + catch (const SysError& e) + { + std::cerr << "[RealTimeSync] " + utfTo(e.toString()) + "\n" "Loading GTK3\'s old CSS format instead..." "\n"; + try + { + loadCSS("Gtk3Styles.old.css"); //throw SysError + } + catch (const SysError& e3) { logExtraError(_("Failed to update the color theme.") + L"\n\n" + e3.toString()); } + } +#else +#error unknown GTK version! +#endif + + /* we're a GUI app: ignore SIGHUP when the parent terminal quits! (or process is killed!) + => the FFS launcher will still be killed => fine + => macOS: apparently not needed! interestingly the FFS launcher does receive SIGHUP and *is* killed */ + if (sighandler_t oldHandler = ::signal(SIGHUP, SIG_IGN); + oldHandler == SIG_ERR) + logExtraError(_("Error during process initialization.") + L"\n\n" + formatSystemError("signal(SIGHUP)", getLastError())); + else assert(!oldHandler); + + + try { colorThemeInit(*this, globalCfg.appColorTheme); } //throw FileError + catch (const FileError& e) { logExtraError(e.toString()); } //not critical in this context + + //Windows User Experience Interaction Guidelines: tool tips should have 5s timeout, info tips no timeout => compromise: + wxToolTip::Enable(true); //wxWidgets screw-up: wxToolTip::SetAutoPop is no-op if global tooltip window is not yet constructed: wxToolTip::Enable creates it + wxToolTip::SetAutoPop(15'000); //https://docs.microsoft.com/en-us/windows/win32/uxguide/ctrl-tooltips-and-infotips + + SetAppName(L"RealTimeSync"); //if not set, defaults to executable name + + + auto onSystemShutdown = [](int /*unused*/ = 0) + { + onSystemShutdownRunTasks(); + + //it's futile to try and clean up while the process is in full swing (CRASH!) => just terminate! + terminateProcess(static_cast(FfsExitCode::cancelled)); + }; + Bind(wxEVT_QUERY_END_SESSION, [onSystemShutdown](wxCloseEvent& event) { onSystemShutdown(); }); //can veto + Bind(wxEVT_END_SESSION, [onSystemShutdown](wxCloseEvent& event) { onSystemShutdown(); }); //can *not* veto + if (auto /*sighandler_t n.a. on macOS*/ oldHandler = ::signal(SIGTERM, onSystemShutdown);//"graceful" exit requested, unlike SIGKILL + oldHandler == SIG_ERR) + logExtraError(_("Error during process initialization.") + L"\n\n" + formatSystemError("signal(SIGTERM)", getLastError())); + else assert(!oldHandler); + + //Note: app start is deferred: -> see FreeFileSync + CallAfter([&] { onEnterEventLoop(); }); + + return true; //true: continue processing; false: exit immediately +} + + +void Application::onEnterEventLoop() +{ + //wxWidgets app exit handling is weird... we want to exit only if the logical main window is closed, not just *any* window! + wxTheApp->SetExitOnFrameDelete(false); //prevent popup-windows from becoming temporary top windows leading to program exit after closure + ZEN_ON_SCOPE_EXIT(if (!wxTheApp->GetExitOnFrameDelete()) wxTheApp->ExitMainLoop()); //quit application, if no main window was set (batch silent mode) + + //try to set config/batch- filepath set by %1 parameter + std::vector commandArgs; + + try + { + for (int i = 1; i < argc; ++i) + { + const Zstring& filePath = getResolvedFilePath(utfTo(argv[i])); +#if 0 + if (!fileAvailable(filePath)) //...be a little tolerant + for (const Zchar* ext : {Zstr(".ffs_real"), Zstr(".ffs_batch")}) + if (fileAvailable(filePath + ext)) + filePath += ext; +#endif + if (endsWithAsciiNoCase(filePath, Zstr(".ffs_real")) || + endsWithAsciiNoCase(filePath, Zstr(".ffs_batch"))) + commandArgs.push_back(filePath); + else + throw FileError(replaceCpy(_("Cannot open file %x."), L"%x", fmtPath(filePath)), + _("Unexpected file extension:") + L' ' + fmtPath(getFileExtension(filePath)) + L'\n' + + _("Expected:") + L" ffs_real, ffs_batch"); + } + + Zstring cfgFilePath; + if (!commandArgs.empty()) + cfgFilePath = commandArgs[0]; + + MainDialog::create(cfgFilePath); + } + catch (const FileError& e) + { + notifyAppError(e.toString()); + } +} + + +int Application::OnExit() +{ + [[maybe_unused]] const bool rv = wxClipboard::Get()->Flush(); //see wx+/context_menu.h + //assert(rv); -> fails if clipboard wasn't used + fff::localizationCleanup(); + imageResourcesCleanup(); + return wxApp::OnExit(); +} + + +wxLayoutDirection Application::GetLayoutDirection() const { return languageLayoutIsRtl() ? wxLayout_RightToLeft : wxLayout_LeftToRight; } + + +int Application::OnRun() +{ +#if wxUSE_EXCEPTIONS +#error why is wxWidgets uncaught exception handling enabled!? +#endif + + //exception => Windows: let it crash and create mini dump!!! Linux/macOS: std::exception::what() logged to console + [[maybe_unused]] const int rc = wxApp::OnRun(); + return static_cast(FfsExitCode::success); //process exit code +} diff --git a/FreeFileSync/Source/RealTimeSync/application.h b/FreeFileSync/Source/RealTimeSync/application.h new file mode 100644 index 0000000..a39cf04 --- /dev/null +++ b/FreeFileSync/Source/RealTimeSync/application.h @@ -0,0 +1,27 @@ +// ***************************************************************************** +// * 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 APPLICATION_H_18506781708176342677 +#define APPLICATION_H_18506781708176342677 + +#include + + +namespace rts +{ +class Application : public wxApp +{ +private: + bool OnInit() override; + int OnRun () override; + int OnExit() override; + wxLayoutDirection GetLayoutDirection() const override; + + void onEnterEventLoop(); +}; +} + +#endif //APPLICATION_H_18506781708176342677 diff --git a/FreeFileSync/Source/RealTimeSync/config.cpp b/FreeFileSync/Source/RealTimeSync/config.cpp new file mode 100644 index 0000000..56d34e3 --- /dev/null +++ b/FreeFileSync/Source/RealTimeSync/config.cpp @@ -0,0 +1,221 @@ +// ***************************************************************************** +// * This file is part of the FreeFileSync project. It is distributed under * +// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * +// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * +// ***************************************************************************** + +#include "config.h" +#include +#include +#include +#include +#include "../ffs_paths.h" + +using namespace zen; +using namespace rts; + +//------------------------------------------------------------------------------------------------------------------------------- +const int XML_FORMAT_RTS_CFG = 2; //2020-04-14 +//------------------------------------------------------------------------------------------------------------------------------- + + +namespace zen +{ +template <> inline +bool readText(const std::string& input, wxLanguage& value) +{ + if (const wxLanguageInfo* lngInfo = wxUILocale::FindLanguageInfo(utfTo(input))) + { + value = static_cast(lngInfo->Language); + return true; + } + return false; +} + + +template <> inline +bool readText(const std::string& input, ColorTheme& value) +{ + const std::string tmp = trimCpy(input); + if (tmp == "Default") + value = ColorTheme::System; + else if (tmp == "Light") + value = ColorTheme::Light; + else if (tmp == "Dark") + value = ColorTheme::Dark; + else + return false; + return true; +} +} + + +namespace +{ +std::string getConfigType(const XmlDoc& doc) +{ + if (doc.root().getName() == "FreeFileSync") + { + std::string type; + if (doc.root().getAttribute("XmlType", type)) + return type; + } + return {}; +} + + +void readConfig(const XmlIn& in, FfsRealConfig& cfg, int /*formatVer*/) +{ + in["Directories"](cfg.directories); + in["Delay" ](cfg.delay); + in["Commandline"](cfg.commandline); +} + + +void writeConfig(const FfsRealConfig& cfg, XmlOut& out) +{ + out["Directories"](cfg.directories); + out["Delay" ](cfg.delay); + out["Commandline"](cfg.commandline); +} +} + + +std::pair rts::readConfig(const Zstring& filePath) //throw FileError +{ + XmlDoc doc = loadXml(filePath); //throw FileError + + if (getConfigType(doc) != "REAL") + throw FileError(replaceCpy(_("File %x does not contain a valid configuration."), L"%x", fmtPath(filePath))); + + int formatVer = 0; + /*bool success =*/ doc.root().getAttribute("XmlFormat", formatVer); + + XmlIn in(doc); + FfsRealConfig cfg; + ::readConfig(in, cfg, formatVer); + + std::wstring warningMsg; + if (const std::wstring& errors = in.getErrors(); + !errors.empty()) + warningMsg = replaceCpy(_("Configuration file %x is incomplete. The missing elements have been set to their default values."), L"%x", fmtPath(filePath)) + L"\n\n" + + _("The following XML elements could not be read:") + L'\n' + errors; + else //(try to) migrate old configuration automatically + if (formatVer < XML_FORMAT_RTS_CFG) + try + { + rts::writeConfig(cfg, filePath); //throw FileError + } + catch (const FileError& e) { warningMsg = e.toString(); } + + return {cfg, warningMsg}; +} + + +void rts::writeConfig(const FfsRealConfig& cfg, const Zstring& filePath) //throw FileError +{ + XmlDoc doc("FreeFileSync"); + doc.root().setAttribute("XmlType", "REAL"); + doc.root().setAttribute("XmlFormat", XML_FORMAT_RTS_CFG); + + XmlOut out(doc); + ::writeConfig(cfg, out); + + saveXml(doc, filePath); //throw FileError +} + + +std::pair rts::readRealOrBatchConfig(const Zstring& filePath) //throw FileError +{ + XmlDoc doc = loadXml(filePath); //throw FileError + //quick exit if file is not an FFS XML + + //convert batch config to RealTimeSync config + if (getConfigType(doc) == "BATCH") + { + XmlIn in(doc); + + //read folder pairs + std::set uniqueFolders; + + in["FolderPairs"].visitChildren([&](const XmlIn& inPair) + { + assert(*inPair.getName() == "Pair"); + + Zstring folderPathPhraseLeft; + Zstring folderPathPhraseRight; + inPair["Left" ](folderPathPhraseLeft); + inPair["Right"](folderPathPhraseRight); + + uniqueFolders.insert(folderPathPhraseLeft); + uniqueFolders.insert(folderPathPhraseRight); + }); + + if (const std::wstring& errors = in.getErrors(); + !errors.empty()) + throw FileError(replaceCpy(_("File %x does not contain a valid configuration."), L"%x", fmtPath(filePath)), + _("The following XML elements could not be read:") + L'\n' + errors); + + //--------------------------------------------------------------------------------------- + + std::erase_if(uniqueFolders, [](const Zstring& str) { return trimCpy(str).empty(); }); + + std::wstring warningMsg; + const Zstring ffsLaunchPath = [&]() -> Zstring + { + try + { + return fff::getFreeFileSyncLauncherPath(); //throw FileError + } + catch (const FileError& e) + { + warningMsg = e.toString(); + return Zstr("FreeFileSync"); //fallback: at least give some hint... + } + }(); + + FfsRealConfig cfg + { + .directories = {uniqueFolders.begin(), uniqueFolders.end()}, + .commandline = escapeCommandArg(ffsLaunchPath) + Zstr(' ') + escapeCommandArg(filePath), + }; + return {cfg, warningMsg}; + } + else + return readConfig(filePath); //throw FileError +} + + +GlobalConfig rts::getGlobalConfig() //throw FileError +{ + GlobalConfig globalCfg; + + const Zstring& filePath = appendPath(fff::getConfigDirPath(), Zstr("GlobalSettings.xml")); + + XmlDoc doc; + try + { + doc = loadXml(filePath); //throw FileError + } + catch (FileError&) + { + if (!itemExists(filePath)) //throw FileError + return globalCfg; + throw; + } + + if (getConfigType(doc) != "GLOBAL") + throw FileError(replaceCpy(_("File %x does not contain a valid configuration."), L"%x", fmtPath(filePath))); + + XmlIn in(doc); + + in["Language"].attribute("Code", globalCfg.programLanguage); + in["ColorTheme"].attribute("Appearance", globalCfg.appColorTheme); + + if (const std::wstring& errors = in.getErrors(); + !errors.empty()) + throw FileError(replaceCpy(_("File %x does not contain a valid configuration."), L"%x", fmtPath(filePath)), + _("The following XML elements could not be read:") + L'\n' + errors); + + return globalCfg; +} diff --git a/FreeFileSync/Source/RealTimeSync/config.h b/FreeFileSync/Source/RealTimeSync/config.h new file mode 100644 index 0000000..c94ef30 --- /dev/null +++ b/FreeFileSync/Source/RealTimeSync/config.h @@ -0,0 +1,40 @@ +// ***************************************************************************** +// * 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 XML_PROC_H_0813748158321813490 +#define XML_PROC_H_0813748158321813490 + +#include +#include +#include +#include +#include "../localization.h" + +namespace rts +{ +struct FfsRealConfig +{ + std::vector directories; + Zstring commandline; + unsigned int delay = 10; +}; + +std::pair readConfig(const Zstring& filePath); //throw FileError +void writeConfig(const FfsRealConfig& config, const Zstring& filePath); //throw FileError + + +//reuse (some of) FreeFileSync's xml files +std::pair readRealOrBatchConfig(const Zstring& filePath); //throw FileError + +struct GlobalConfig +{ + wxLanguage programLanguage = fff::getDefaultLanguage(); + zen::ColorTheme appColorTheme = zen::ColorTheme::System; +}; +GlobalConfig getGlobalConfig(); //throw FileError +} + +#endif //XML_PROC_H_0813748158321813490 diff --git a/FreeFileSync/Source/RealTimeSync/folder_selector2.cpp b/FreeFileSync/Source/RealTimeSync/folder_selector2.cpp new file mode 100644 index 0000000..0a0e46a --- /dev/null +++ b/FreeFileSync/Source/RealTimeSync/folder_selector2.cpp @@ -0,0 +1,191 @@ +// ***************************************************************************** +// * 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 "folder_selector2.h" +#include +#include +#include +#include +#include +#include + #include + +using namespace zen; +using namespace rts; + + +namespace +{ +constexpr std::chrono::milliseconds FOLDER_SELECTED_EXISTENCE_CHECK_TIME_MAX(200); + + +void setFolderPath(const Zstring& dirpath, wxTextCtrl* txtCtrl, wxWindow& tooltipWnd, wxStaticText* staticText) //pointers are optional +{ + if (txtCtrl) + txtCtrl->ChangeValue(utfTo(dirpath)); + + const Zstring folderPathFmt = getResolvedFilePath(dirpath); //may block when resolving [] + + if (folderPathFmt.empty()) + tooltipWnd.UnsetToolTip(); //wxGTK doesn't allow wxToolTip with empty text! + else + tooltipWnd.SetToolTip(utfTo(folderPathFmt)); + + if (staticText) //change static box label only if there is a real difference to what is shown in wxTextCtrl anyway + staticText->SetLabel(equalNativePath(appendSeparator(trimCpy(dirpath)), appendSeparator(folderPathFmt)) ? + wxString(_("Drag && drop")) : utfTo(folderPathFmt)); +} + + +} + +//############################################################################################################## + +FolderSelector2::FolderSelector2(wxWindow* parent, + wxWindow& dropWindow, + wxButton& selectButton, + wxTextCtrl& folderPathCtrl, + Zstring& folderLastSelected, + wxStaticText* staticText, + const std::function& shellItemPaths)>& droppedPathsFilter) : + droppedPathsFilter_ (droppedPathsFilter), + parent_(parent), + dropWindow_(dropWindow), + selectButton_(selectButton), + folderPathCtrl_(folderPathCtrl), + folderLastSelected_(folderLastSelected), + staticText_(staticText) +{ + //file drag and drop directly into the text control unhelpfully inserts in format "file://.."; see folder_history_box.cpp + if (GtkWidget* widget = folderPathCtrl.GetConnectWidget()) + ::gtk_drag_dest_unset(widget); + + setupFileDrop(dropWindow_); + dropWindow_.Bind(EVENT_DROP_FILE, &FolderSelector2::onFilesDropped, this); + + //keep folderSelector and dirpath synchronous + folderPathCtrl_.Bind(wxEVT_MOUSEWHEEL, &FolderSelector2::onMouseWheel, this); + folderPathCtrl_.Bind(wxEVT_COMMAND_TEXT_UPDATED, &FolderSelector2::onEditFolderPath, this); + selectButton_ .Bind(wxEVT_COMMAND_BUTTON_CLICKED, &FolderSelector2::onSelectDir, this); +} + + +FolderSelector2::~FolderSelector2() +{ + [[maybe_unused]] bool ubOk1 = dropWindow_.Unbind(EVENT_DROP_FILE, &FolderSelector2::onFilesDropped, this); + + [[maybe_unused]] bool ubOk2 = folderPathCtrl_.Unbind(wxEVT_MOUSEWHEEL, &FolderSelector2::onMouseWheel, this); + [[maybe_unused]] bool ubOk3 = folderPathCtrl_.Unbind(wxEVT_COMMAND_TEXT_UPDATED, &FolderSelector2::onEditFolderPath, this); + [[maybe_unused]] bool ubOk4 = selectButton_ .Unbind(wxEVT_COMMAND_BUTTON_CLICKED, &FolderSelector2::onSelectDir, this); + assert(ubOk1 && ubOk2 && ubOk3 && ubOk4); +} + + +void FolderSelector2::onMouseWheel(wxMouseEvent& event) +{ + //for combobox: although switching through available items is wxWidgets default, this is NOT Windows default, e.g. Explorer + //additionally this will delete manual entries, although all the users wanted is scroll the parent window! + + //redirect to parent scrolled window! + for (wxWindow* wnd = folderPathCtrl_.GetParent(); wnd; wnd = wnd->GetParent()) + if (dynamic_cast(wnd) != nullptr) + return wnd->GetEventHandler()->AddPendingEvent(event); + assert(false); + event.Skip(); +} + + +void FolderSelector2::onFilesDropped(FileDropEvent& event) +{ + if (event.itemPaths_.empty()) + return; + + if (!droppedPathsFilter_ || droppedPathsFilter_(event.itemPaths_)) + { + Zstring itemPath = event.itemPaths_[0]; + try + { + if (getItemType(itemPath) == ItemType::file) //throw FileError + if (const std::optional& parentPath = getParentFolderPath(itemPath)) + itemPath = *parentPath; + } + catch (FileError&) {} //e.g. good for inactive mapped network shares, not so nice for C:\pagefile.sys + + if (endsWith(itemPath, Zstr(' '))) //prevent getResolvedFilePath() from trimming legit trailing blank! + itemPath += FILE_NAME_SEPARATOR; + + setPath(itemPath); + } + //event.Skip(); +} + + +void FolderSelector2::onEditFolderPath(wxCommandEvent& event) +{ + setFolderPath(utfTo(event.GetString()), nullptr, folderPathCtrl_, staticText_); + event.Skip(); +} + + +void FolderSelector2::onSelectDir(wxCommandEvent& event) +{ + //IFileDialog requirements for default path: 1. accepts native paths only!!! 2. path must exist! + Zstring defaultFolderPath; + { + auto folderAccessible = [stopTime = std::chrono::steady_clock::now() + FOLDER_SELECTED_EXISTENCE_CHECK_TIME_MAX](const Zstring& folderPath) + { + auto ft = runAsync([folderPath] + { + try + { + return getItemType(folderPath) != ItemType::file; //throw FileError + } + catch (FileError&) { return false; } + }); + + return ft.wait_until(stopTime) == std::future_status::ready && ft.get(); //potentially slow network access: wait 200ms at most + }; + + auto trySetDefaultPath = [&](const Zstring& folderPathPhrase) + { + + if (const Zstring folderPath = getResolvedFilePath(folderPathPhrase); + !folderPath.empty()) + if (folderAccessible(folderPath)) + defaultFolderPath = folderPath; + }; + + const Zstring& currentFolderPath = getPath(); + trySetDefaultPath(currentFolderPath); + + if (defaultFolderPath.empty() && //=> fallback: use last user-selected path + trimCpy(folderLastSelected_) != trimCpy(currentFolderPath) /*case-sensitive comp for path phrase!*/) + trySetDefaultPath(folderLastSelected_); + } + + Zstring newFolderPath; + wxDirDialog folderSelector(parent_, _("Select a folder"), utfTo(defaultFolderPath), wxDD_DEFAULT_STYLE | wxDD_SHOW_HIDDEN); + if (folderSelector.ShowModal() != wxID_OK) + return; + newFolderPath = utfTo(folderSelector.GetPath()); + if (endsWith(newFolderPath, Zstr(' '))) //prevent getResolvedFilePath() from trimming legit trailing blank! + newFolderPath += FILE_NAME_SEPARATOR; + + setPath(newFolderPath); + folderLastSelected_ = newFolderPath; +} + + +Zstring FolderSelector2::getPath() const +{ + return utfTo(folderPathCtrl_.GetValue()); +} + + +void FolderSelector2::setPath(const Zstring& dirpath) +{ + setFolderPath(dirpath, &folderPathCtrl_, folderPathCtrl_, staticText_); +} diff --git a/FreeFileSync/Source/RealTimeSync/folder_selector2.h b/FreeFileSync/Source/RealTimeSync/folder_selector2.h new file mode 100644 index 0000000..10bd590 --- /dev/null +++ b/FreeFileSync/Source/RealTimeSync/folder_selector2.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 FOLDER_SELECTOR2_H_073246031245342566 +#define FOLDER_SELECTOR2_H_073246031245342566 + +#include +#include +#include +#include + +namespace rts +{ +//handle drag and drop, tooltip, label and manual input, coordinating a wxWindow, wxButton, and wxTextCtrl + +class FolderSelector2 : public wxEvtHandler +{ +public: + FolderSelector2(wxWindow* parent, + wxWindow& dropWindow, + wxButton& selectButton, + wxTextCtrl& folderPathCtrl, + Zstring& folderLastSelected, + wxStaticText* staticText, //optional + const std::function& shellItemPaths)>& droppedPathsFilter); //optional + + ~FolderSelector2(); + + Zstring getPath() const; + void setPath(const Zstring& dirpath); + +private: + void onMouseWheel (wxMouseEvent& event); + void onFilesDropped (zen::FileDropEvent& event); + void onEditFolderPath(wxCommandEvent& event); + void onSelectDir (wxCommandEvent& event); + + const std::function& shellItemPaths)> droppedPathsFilter_; + + wxWindow* parent_; + wxWindow& dropWindow_; + wxButton& selectButton_; + wxTextCtrl& folderPathCtrl_; + Zstring& folderLastSelected_; + wxStaticText* staticText_ = nullptr; //optional +}; +} + +#endif //FOLDER_SELECTOR2_H_073246031245342566 diff --git a/FreeFileSync/Source/RealTimeSync/gui_generated.cpp b/FreeFileSync/Source/RealTimeSync/gui_generated.cpp new file mode 100644 index 0000000..e14502b --- /dev/null +++ b/FreeFileSync/Source/RealTimeSync/gui_generated.cpp @@ -0,0 +1,299 @@ +/////////////////////////////////////////////////////////////////////////// +// C++ code generated with wxFormBuilder (version 3.10.1-0-g8feb16b3) +// http://www.wxformbuilder.org/ +// +// PLEASE DO *NOT* EDIT THIS FILE! +/////////////////////////////////////////////////////////////////////////// + +#include "wx+/bitmap_button.h" + +#include "gui_generated.h" + +/////////////////////////////////////////////////////////////////////////// + +MainDlgGenerated::MainDlgGenerated( wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style ) : wxFrame( parent, id, title, pos, size, style ) +{ + this->SetSizeHints( wxSize( -1, -1 ), wxDefaultSize ); + this->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) ); + + m_menubar1 = new wxMenuBar( 0 ); + m_menuFile = new wxMenu(); + wxMenuItem* m_menuItem6; + m_menuItem6 = new wxMenuItem( m_menuFile, wxID_NEW, wxString( _("&New") ) + wxT('\t') + wxT("Ctrl+N"), wxEmptyString, wxITEM_NORMAL ); + m_menuFile->Append( m_menuItem6 ); + + wxMenuItem* m_menuItem13; + m_menuItem13 = new wxMenuItem( m_menuFile, wxID_OPEN, wxString( _("&Open...") ) + wxT('\t') + wxT("CTRL+O"), wxEmptyString, wxITEM_NORMAL ); + m_menuFile->Append( m_menuItem13 ); + + wxMenuItem* m_menuItem14; + m_menuItem14 = new wxMenuItem( m_menuFile, wxID_SAVEAS, wxString( _("Save &as...") ), wxEmptyString, wxITEM_NORMAL ); + m_menuFile->Append( m_menuItem14 ); + + m_menuFile->AppendSeparator(); + + m_menuItemQuit = new wxMenuItem( m_menuFile, wxID_EXIT, wxString( _("E&xit") ), wxEmptyString, wxITEM_NORMAL ); + m_menuFile->Append( m_menuItemQuit ); + + m_menubar1->Append( m_menuFile, _("&File") ); + + m_menuHelp = new wxMenu(); + wxMenuItem* m_menuItemContent; + m_menuItemContent = new wxMenuItem( m_menuHelp, wxID_HELP, wxString( _("&View help") ) + wxT('\t') + wxT("F1"), wxEmptyString, wxITEM_NORMAL ); + m_menuHelp->Append( m_menuItemContent ); + + m_menuHelp->AppendSeparator(); + + m_menuItemAbout = new wxMenuItem( m_menuHelp, wxID_ABOUT, wxString( _("&About") ) + wxT('\t') + wxT("SHIFT+F1"), wxEmptyString, wxITEM_NORMAL ); + m_menuHelp->Append( m_menuItemAbout ); + + m_menubar1->Append( m_menuHelp, _("&Help") ); + + this->SetMenuBar( m_menubar1 ); + + bSizerMain = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer161; + bSizer161 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer152; + bSizer152 = new wxBoxSizer( wxHORIZONTAL ); + + wxStaticText* m_staticText811; + m_staticText811 = new wxStaticText( this, wxID_ANY, _("To get started, just import a \"ffs_batch\" file."), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText811->Wrap( -1 ); + m_staticText811->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + bSizer152->Add( m_staticText811, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 ); + + wxStaticText* m_staticText10; + m_staticText10 = new wxStaticText( this, wxID_ANY, _("("), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText10->Wrap( -1 ); + m_staticText10->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + bSizer152->Add( m_staticText10, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT, 2 ); + + m_bitmapBatch = new wxStaticBitmap( this, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer152->Add( m_bitmapBatch, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + wxStaticText* m_staticText11; + m_staticText11 = new wxStaticText( this, wxID_ANY, _(")"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText11->Wrap( -1 ); + m_staticText11->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + bSizer152->Add( m_staticText11, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT, 2 ); + + wxHyperlinkCtrl* m_hyperlink243; + m_hyperlink243 = new wxHyperlinkCtrl( this, wxID_ANY, _("Show examples"), wxT("https://freefilesync.org/manual.php?topic=realtimesync"), wxDefaultPosition, wxDefaultSize, wxHL_DEFAULT_STYLE ); + m_hyperlink243->SetToolTip( _("https://freefilesync.org/manual.php?topic=realtimesync") ); + + bSizer152->Add( m_hyperlink243, 0, wxALIGN_CENTER_VERTICAL|wxLEFT, 5 ); + + + bSizer161->Add( bSizer152, 0, wxALL|wxALIGN_CENTER_HORIZONTAL, 5 ); + + + bSizerMain->Add( bSizer161, 0, wxALL|wxEXPAND, 5 ); + + wxStaticLine* m_staticline2; + m_staticline2 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizerMain->Add( m_staticline2, 0, wxEXPAND, 5 ); + + m_panelMain = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panelMain->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer1; + bSizer1 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer151; + bSizer151 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer142; + bSizer142 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapFolders = new wxStaticBitmap( m_panelMain, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer142->Add( m_bitmapFolders, 0, wxTOP|wxBOTTOM|wxLEFT, 5 ); + + wxStaticText* m_staticText7; + m_staticText7 = new wxStaticText( m_panelMain, wxID_ANY, _("Folders to watch for changes:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText7->Wrap( -1 ); + bSizer142->Add( m_staticText7, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer151->Add( bSizer142, 0, 0, 5 ); + + m_panelMainFolder = new wxPanel( m_panelMain, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panelMainFolder->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer143; + bSizer143 = new wxBoxSizer( wxHORIZONTAL ); + + m_bpButtonAddFolder = new wxBitmapButton( m_panelMainFolder, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + m_bpButtonAddFolder->SetToolTip( _("Add folder") ); + + bSizer143->Add( m_bpButtonAddFolder, 0, wxEXPAND, 5 ); + + m_bpButtonRemoveTopFolder = new wxBitmapButton( m_panelMainFolder, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + m_bpButtonRemoveTopFolder->SetToolTip( _("Remove folder") ); + + bSizer143->Add( m_bpButtonRemoveTopFolder, 0, wxEXPAND, 5 ); + + m_txtCtrlDirectoryMain = new wxTextCtrl( m_panelMainFolder, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizer143->Add( m_txtCtrlDirectoryMain, 1, wxALIGN_CENTER_VERTICAL, 5 ); + + m_buttonSelectFolderMain = new wxButton( m_panelMainFolder, wxID_ANY, _("Browse"), wxDefaultPosition, wxDefaultSize, 0 ); + m_buttonSelectFolderMain->SetToolTip( _("Select a folder") ); + + bSizer143->Add( m_buttonSelectFolderMain, 0, wxEXPAND, 5 ); + + + m_panelMainFolder->SetSizer( bSizer143 ); + m_panelMainFolder->Layout(); + bSizer143->Fit( m_panelMainFolder ); + bSizer151->Add( m_panelMainFolder, 0, wxRIGHT|wxLEFT|wxEXPAND, 5 ); + + m_scrolledWinFolders = new wxScrolledWindow( m_panelMain, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL|wxVSCROLL ); + m_scrolledWinFolders->SetScrollRate( 5, 5 ); + m_scrolledWinFolders->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + bSizerFolders = new wxBoxSizer( wxVERTICAL ); + + + m_scrolledWinFolders->SetSizer( bSizerFolders ); + m_scrolledWinFolders->Layout(); + bSizerFolders->Fit( m_scrolledWinFolders ); + bSizer151->Add( m_scrolledWinFolders, 1, wxEXPAND|wxBOTTOM|wxRIGHT|wxLEFT, 5 ); + + + bSizer1->Add( bSizer151, 1, wxALL|wxEXPAND, 10 ); + + wxStaticLine* m_staticline212; + m_staticline212 = new wxStaticLine( m_panelMain, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer1->Add( m_staticline212, 0, wxEXPAND, 5 ); + + wxBoxSizer* bSizer131; + bSizer131 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer14; + bSizer14 = new wxBoxSizer( wxHORIZONTAL ); + + wxStaticText* m_staticText8; + m_staticText8 = new wxStaticText( m_panelMain, wxID_ANY, _("Idle time (in seconds):"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText8->Wrap( -1 ); + bSizer14->Add( m_staticText8, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 5 ); + + m_spinCtrlDelay = new wxSpinCtrl( m_panelMain, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 0, 2000000000, 0 ); + m_spinCtrlDelay->SetToolTip( _("Idle time between last detected change and execution of command") ); + + bSizer14->Add( m_spinCtrlDelay, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL|wxALL, 5 ); + + + bSizer131->Add( bSizer14, 0, 0, 5 ); + + wxStaticText* m_staticText71; + m_staticText71 = new wxStaticText( m_panelMain, wxID_ANY, _("Ensures folders are not in heavy use when running the command."), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText71->Wrap( -1 ); + m_staticText71->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + bSizer131->Add( m_staticText71, 0, wxBOTTOM|wxRIGHT|wxLEFT, 5 ); + + + bSizer1->Add( bSizer131, 0, wxALL, 10 ); + + wxStaticLine* m_staticline211; + m_staticline211 = new wxStaticLine( m_panelMain, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer1->Add( m_staticline211, 0, wxEXPAND, 5 ); + + wxBoxSizer* bSizer141; + bSizer141 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer13; + bSizer13 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapConsole = new wxStaticBitmap( m_panelMain, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer13->Add( m_bitmapConsole, 0, wxTOP|wxBOTTOM|wxLEFT|wxALIGN_CENTER_VERTICAL, 5 ); + + wxStaticText* m_staticText6; + m_staticText6 = new wxStaticText( m_panelMain, wxID_ANY, _("Command line to run when changes are detected:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText6->Wrap( -1 ); + bSizer13->Add( m_staticText6, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer141->Add( bSizer13, 0, 0, 5 ); + + m_textCtrlCommand = new wxTextCtrl( m_panelMain, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0 ); + m_textCtrlCommand->SetToolTip( _("The command is triggered if:\n- files or subfolders change\n- new folders arrive (e.g. USB stick insert)") ); + + bSizer141->Add( m_textCtrlCommand, 0, wxEXPAND|wxBOTTOM|wxRIGHT|wxLEFT, 5 ); + + + bSizer1->Add( bSizer141, 0, wxALL|wxEXPAND, 10 ); + + + m_panelMain->SetSizer( bSizer1 ); + m_panelMain->Layout(); + bSizer1->Fit( m_panelMain ); + bSizerMain->Add( m_panelMain, 1, wxEXPAND, 5 ); + + wxStaticLine* m_staticline5; + m_staticline5 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizerMain->Add( m_staticline5, 0, wxEXPAND, 5 ); + + m_buttonStart = new zen::BitmapTextButton( this, wxID_OK, _("Start"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + + m_buttonStart->SetDefault(); + m_buttonStart->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL, false, wxEmptyString ) ); + + bSizerMain->Add( m_buttonStart, 0, wxALIGN_CENTER_HORIZONTAL|wxALL, 5 ); + + + this->SetSizer( bSizerMain ); + this->Layout(); + bSizerMain->Fit( this ); + + // Connect Events + this->Connect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( MainDlgGenerated::onClose ) ); + m_menuFile->Bind(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDlgGenerated::onConfigNew ), this, m_menuItem6->GetId()); + m_menuFile->Bind(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDlgGenerated::onConfigLoad ), this, m_menuItem13->GetId()); + m_menuFile->Bind(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDlgGenerated::onConfigSave ), this, m_menuItem14->GetId()); + m_menuFile->Bind(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDlgGenerated::onMenuQuit ), this, m_menuItemQuit->GetId()); + m_menuHelp->Bind(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDlgGenerated::onShowHelp ), this, m_menuItemContent->GetId()); + m_menuHelp->Bind(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDlgGenerated::onMenuAbout ), this, m_menuItemAbout->GetId()); + m_bpButtonAddFolder->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDlgGenerated::onAddFolder ), NULL, this ); + m_bpButtonRemoveTopFolder->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDlgGenerated::onRemoveTopFolder ), NULL, this ); + m_buttonStart->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDlgGenerated::onStart ), NULL, this ); +} + +MainDlgGenerated::~MainDlgGenerated() +{ +} + +FolderGenerated::FolderGenerated( wxWindow* parent, wxWindowID id, const wxPoint& pos, const wxSize& size, long style, const wxString& name ) : wxPanel( parent, id, pos, size, style, name ) +{ + this->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer114; + bSizer114 = new wxBoxSizer( wxHORIZONTAL ); + + m_bpButtonRemoveFolder = new wxBitmapButton( this, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + m_bpButtonRemoveFolder->SetToolTip( _("Remove folder") ); + + bSizer114->Add( m_bpButtonRemoveFolder, 0, wxEXPAND, 5 ); + + m_txtCtrlDirectory = new wxTextCtrl( this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer114->Add( m_txtCtrlDirectory, 1, wxALIGN_CENTER_VERTICAL, 5 ); + + m_buttonSelectFolder = new wxButton( this, wxID_ANY, _("Browse"), wxDefaultPosition, wxDefaultSize, 0 ); + m_buttonSelectFolder->SetToolTip( _("Select a folder") ); + + bSizer114->Add( m_buttonSelectFolder, 0, wxEXPAND, 5 ); + + + this->SetSizer( bSizer114 ); + this->Layout(); + bSizer114->Fit( this ); +} + +FolderGenerated::~FolderGenerated() +{ +} diff --git a/FreeFileSync/Source/RealTimeSync/gui_generated.h b/FreeFileSync/Source/RealTimeSync/gui_generated.h new file mode 100644 index 0000000..ea94daf --- /dev/null +++ b/FreeFileSync/Source/RealTimeSync/gui_generated.h @@ -0,0 +1,110 @@ +/////////////////////////////////////////////////////////////////////////// +// C++ code generated with wxFormBuilder (version 3.10.1-0-g8feb16b3) +// http://www.wxformbuilder.org/ +// +// PLEASE DO *NOT* EDIT THIS FILE! +/////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +namespace zen { class BitmapTextButton; } + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "zen/i18n.h" + +/////////////////////////////////////////////////////////////////////////// + +/////////////////////////////////////////////////////////////////////////////// +/// Class MainDlgGenerated +/////////////////////////////////////////////////////////////////////////////// +class MainDlgGenerated : public wxFrame +{ +private: + +protected: + wxMenuBar* m_menubar1; + wxMenu* m_menuFile; + wxMenuItem* m_menuItemQuit; + wxMenu* m_menuHelp; + wxMenuItem* m_menuItemAbout; + wxBoxSizer* bSizerMain; + wxStaticBitmap* m_bitmapBatch; + wxPanel* m_panelMain; + wxStaticBitmap* m_bitmapFolders; + wxPanel* m_panelMainFolder; + wxBitmapButton* m_bpButtonAddFolder; + wxBitmapButton* m_bpButtonRemoveTopFolder; + wxTextCtrl* m_txtCtrlDirectoryMain; + wxButton* m_buttonSelectFolderMain; + wxScrolledWindow* m_scrolledWinFolders; + wxBoxSizer* bSizerFolders; + wxSpinCtrl* m_spinCtrlDelay; + wxStaticBitmap* m_bitmapConsole; + wxTextCtrl* m_textCtrlCommand; + zen::BitmapTextButton* m_buttonStart; + + // Virtual event handlers, override them in your derived class + virtual void onClose( wxCloseEvent& event ) { event.Skip(); } + virtual void onConfigNew( wxCommandEvent& event ) { event.Skip(); } + virtual void onConfigLoad( wxCommandEvent& event ) { event.Skip(); } + virtual void onConfigSave( wxCommandEvent& event ) { event.Skip(); } + virtual void onMenuQuit( wxCommandEvent& event ) { event.Skip(); } + virtual void onShowHelp( wxCommandEvent& event ) { event.Skip(); } + virtual void onMenuAbout( wxCommandEvent& event ) { event.Skip(); } + virtual void onAddFolder( wxCommandEvent& event ) { event.Skip(); } + virtual void onRemoveTopFolder( wxCommandEvent& event ) { event.Skip(); } + virtual void onStart( wxCommandEvent& event ) { event.Skip(); } + + +public: + + MainDlgGenerated( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = _("dummy"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize( -1, -1 ), long style = wxDEFAULT_FRAME_STYLE|wxTAB_TRAVERSAL ); + + ~MainDlgGenerated(); + +}; + +/////////////////////////////////////////////////////////////////////////////// +/// Class FolderGenerated +/////////////////////////////////////////////////////////////////////////////// +class FolderGenerated : public wxPanel +{ +private: + +protected: + wxButton* m_buttonSelectFolder; + +public: + wxBitmapButton* m_bpButtonRemoveFolder; + wxTextCtrl* m_txtCtrlDirectory; + + FolderGenerated( wxWindow* parent, wxWindowID id = wxID_ANY, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize( -1, -1 ), long style = 0, const wxString& name = wxEmptyString ); + + ~FolderGenerated(); + +}; + diff --git a/FreeFileSync/Source/RealTimeSync/main_dlg.cpp b/FreeFileSync/Source/RealTimeSync/main_dlg.cpp new file mode 100644 index 0000000..e5f3bf7 --- /dev/null +++ b/FreeFileSync/Source/RealTimeSync/main_dlg.cpp @@ -0,0 +1,512 @@ +// ***************************************************************************** +// * 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 "main_dlg.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "config.h" +#include "tray_menu.h" +#include "app_icon.h" +#include "../icon_buffer.h" +#include "../ffs_paths.h" +#include "../version/version.h" + + #include + +using namespace zen; +using namespace rts; + + +namespace +{ + static const size_t MAX_ADD_FOLDERS = 6; + + +std::wstring extractJobName(const Zstring& cfgFilePath) +{ + const Zstring fileName = getItemName(cfgFilePath); + const Zstring jobName = beforeLast(fileName, Zstr('.'), IfNotFoundReturn::all); + return utfTo(jobName); +} + + +bool acceptDialogFileDrop(const std::vector& shellItemPaths) +{ + if (shellItemPaths.empty()) + return false; + + const Zstring ext = getFileExtension(shellItemPaths[0]); + return equalAsciiNoCase(ext, "ffs_real") || + equalAsciiNoCase(ext, "ffs_batch"); +} +} + + +std::function& shellItemPaths)> getDroppedPathsFilter(MainDialog& mainDlg) +{ + return [&mainDlg](const std::vector& shellItemPaths) + { + if (acceptDialogFileDrop(shellItemPaths)) + { + assert(!shellItemPaths.empty()); + mainDlg.loadConfig(shellItemPaths[0]); + return false; //don't set dropped paths + } + return true; //do set dropped paths + }; +} + + +class rts::DirectoryPanel : public FolderGenerated +{ +public: + DirectoryPanel(wxWindow* parent, MainDialog& mainDlg, Zstring& folderLastSelected) : + FolderGenerated(parent), + folderSelector_(parent, *this, *m_buttonSelectFolder, *m_txtCtrlDirectory, folderLastSelected, nullptr /*staticText*/, getDroppedPathsFilter(mainDlg)) + { + setImage(*m_bpButtonRemoveFolder, loadImage("item_remove")); + } + + void setPath(const Zstring& dirpath) { folderSelector_.setPath(dirpath); } + Zstring getPath() const { return folderSelector_.getPath(); } + +private: + FolderSelector2 folderSelector_; +}; + + +void MainDialog::create(const Zstring& cfgFilePath) +{ + /*MainDialog* frame = */ new MainDialog(cfgFilePath); +} + + +MainDialog::MainDialog(const Zstring& cfgFilePath) : + MainDlgGenerated(nullptr), + lastRunConfigPath_(appendPath(fff::getConfigDirPath(), Zstr("LastRun.ffs_real"))) +{ + SetIcon(getRtsIcon()); //set application icon + + setRelativeFontSize(*m_buttonStart, 1.5); + + const int scrollDelta = m_buttonSelectFolderMain->GetSize().y; //more approriate than GetCharHeight() here + m_scrolledWinFolders->SetScrollRate(scrollDelta, scrollDelta); + + m_txtCtrlDirectoryMain->SetMinSize({dipToWxsize(300), -1}); + setDefaultWidth(*m_spinCtrlDelay); + + m_bpButtonRemoveTopFolder->Hide(); + m_panelMainFolder->Layout(); + + setImage(*m_bitmapBatch, loadImage("cfg_batch", dipToScreen(20))); + setImage(*m_bitmapFolders, fff::IconBuffer::genericDirIcon(fff::IconBuffer::IconSize::small)); + setImage(*m_bitmapConsole, loadImage("command_line", dipToScreen(20))); + + setImage(*m_bpButtonAddFolder, loadImage("item_add")); + setImage(*m_bpButtonRemoveTopFolder, loadImage("item_remove")); + setBitmapTextLabel(*m_buttonStart, loadImage("start_rts"), m_buttonStart->GetLabelText(), dipToWxsize(5), dipToWxsize(8)); + + Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onLocalKeyEvent(event); }); + + + //notify about (logical) application main window => program won't quit, but stay on this dialog + wxTheApp->SetTopWindow(this); + wxTheApp->SetExitOnFrameDelete(true); + + //prepare drag & drop + firstFolderPanel_ = std::make_unique(this, *m_panelMainFolder, *m_buttonSelectFolderMain, *m_txtCtrlDirectoryMain, folderLastSelected_, + nullptr /*staticText*/, getDroppedPathsFilter(*this)); + + //--------------------------- load config values ------------------------------------ + FfsRealConfig newConfig; + + Zstring currentConfigFile = cfgFilePath; + if (currentConfigFile.empty()) + try + { + if (itemExists(lastRunConfigPath_)) //throw FileError + currentConfigFile = lastRunConfigPath_; + } + catch (FileError&) { currentConfigFile = lastRunConfigPath_; } //access error? => user should be informed + + bool loadCfgSuccess = false; + if (!currentConfigFile.empty()) + try + { + std::wstring warningMsg; + std::tie(newConfig, warningMsg) = readRealOrBatchConfig(currentConfigFile); //throw FileError + + if (!warningMsg.empty()) + showNotificationDialog(this, DialogInfoType::warning, PopupDialogCfg().setDetailInstructions(warningMsg)); + + loadCfgSuccess = warningMsg.empty(); + } + catch (const FileError& e) + { + showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); + } + + const bool startWatchingImmediately = loadCfgSuccess && !cfgFilePath.empty(); + + setConfiguration(newConfig); + setLastUsedConfig(currentConfigFile); + //----------------------------------------------------------------------------------------- + + onSystemShutdownRegister(onBeforeSystemShutdownCookie_); + + if (startWatchingImmediately) //start watch mode directly + { + wxCommandEvent dummy2(wxEVT_COMMAND_BUTTON_CLICKED); + this->onStart(dummy2); + //don't Show()! + } + else + { + //GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() => already called by setConfiguration() -> insertAddFolder() +#ifdef __WXGTK3__ + Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 + //Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404 +#endif + Center(); //apply *after* dialog size change! + + Show(); + m_buttonStart->SetFocus(); //don't "steal" focus if program is running from sys-tray" + } + + //drag and drop .ffs_real and .ffs_batch on main dialog + setupFileDrop(*this); + Bind(EVENT_DROP_FILE, [this](FileDropEvent& event) { onFilesDropped(event); }); +} + + +MainDialog::~MainDialog() +{ + const FfsRealConfig currentCfg = getConfiguration(); + try + { + writeConfig(currentCfg, lastRunConfigPath_); //throw FileError + } + catch (const FileError& e) + { + showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); + } +} + + +void MainDialog::onBeforeSystemShutdown() +{ + try { writeConfig(getConfiguration(), lastRunConfigPath_); } + catch (const FileError& e) { logExtraError(e.toString()); } +} + + +void MainDialog::onMenuAbout(wxCommandEvent& event) +{ + wxString build = utfTo(fff::ffsVersion); +#ifndef wxUSE_UNICODE +#error what is going on? +#endif + + const wchar_t* const SPACED_BULLET = L" \u2022 "; + build += SPACED_BULLET; + + build += LTR_MARK; //fix Arabic + build += utfTo(cpuArchName); + + build += SPACED_BULLET; + build += utfTo(formatTime(formatDateTag, getCompileTime())); + + showNotificationDialog(this, DialogInfoType::info, PopupDialogCfg(). + setTitle(_("About")). + setMainInstructions(L"RealTimeSync" L"\n\n" + replaceCpy(_("Version: %x"), L"%x", build))); +} + + +void MainDialog::onLocalKeyEvent(wxKeyEvent& event) +{ + switch (event.GetKeyCode()) + { + case WXK_ESCAPE: + Close(); + return; + } + event.Skip(); +} + + +void MainDialog::onStart(wxCommandEvent& event) +{ + Hide(); + + FfsRealConfig currentCfg = getConfiguration(); + const Zstring activeCfgFilePath = !equalNativePath(activeConfigFile_, lastRunConfigPath_) ? activeConfigFile_ : Zstring(); + + switch (runFolderMonitor(currentCfg, ::extractJobName(activeCfgFilePath))) + { + case CancelReason::requestExit: + Close(); + return; + + case CancelReason::requestGui: + break; + } + + //need to center in case of "startWatchingImmediately" +#ifdef __WXGTK3__ + Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 + //Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404 +#endif + Center(); //apply *after* dialog size change! + + Show(); //don't show for CancelReason::requestExit + + Raise(); + m_buttonStart->SetFocus(); +} + + +void MainDialog::onConfigSave(wxCommandEvent& event) +{ + const Zstring activeCfgFilePath = !equalNativePath(activeConfigFile_, lastRunConfigPath_) ? activeConfigFile_ : Zstring(); + + std::optional defaultFolderPath = getParentFolderPath(activeCfgFilePath); + + Zstring defaultFileName = !activeCfgFilePath.empty() ? + getItemName(activeCfgFilePath) : + Zstr("RealTime.ffs_real"); + + //attention: activeConfigFile_ may be an imported *.ffs_batch file! We don't want to overwrite it with a RTS config! + defaultFileName = beforeLast(defaultFileName, Zstr('.'), IfNotFoundReturn::all) + Zstr(".ffs_real"); + + wxFileDialog fileSelector(this, wxString() /*message*/, utfTo(defaultFolderPath ? *defaultFolderPath : Zstr("")), utfTo(defaultFileName), + wxString(L"RealTimeSync (*.ffs_real)|*.ffs_real") + L"|" +_("All files") + L" (*.*)|*", + wxFD_SAVE | wxFD_OVERWRITE_PROMPT); + if (fileSelector.ShowModal() != wxID_OK) + return; + + Zstring targetFilePath = utfTo(fileSelector.GetPath()); + if (!endsWithAsciiNoCase(targetFilePath, Zstr(".ffs_real"))) //no weird shit! + targetFilePath += Zstr(".ffs_real"); //https://freefilesync.org/forum/viewtopic.php?t=9451#p34724 + + const FfsRealConfig currentCfg = getConfiguration(); + try + { + writeConfig(currentCfg, targetFilePath); //throw FileError + setLastUsedConfig(targetFilePath); + } + catch (const FileError& e) + { + showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); + } +} + + +void MainDialog::loadConfig(const Zstring& filepath) +{ + FfsRealConfig newConfig; + + if (!filepath.empty()) + try + { + std::wstring warningMsg; + std::tie(newConfig, warningMsg) = readRealOrBatchConfig(filepath); //throw FileError + + if (!warningMsg.empty()) + showNotificationDialog(this, DialogInfoType::warning, PopupDialogCfg().setDetailInstructions(warningMsg)); + } + catch (const FileError& e) + { + showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); + return; + } + + setConfiguration(newConfig); + setLastUsedConfig(filepath); +} + + +void MainDialog::setLastUsedConfig(const Zstring& filepath) +{ + activeConfigFile_ = filepath; + + const Zstring activeCfgFilePath = !equalNativePath(activeConfigFile_, lastRunConfigPath_) ? activeConfigFile_ : Zstring(); + + if (!activeCfgFilePath.empty()) + SetTitle(utfTo(activeCfgFilePath)); + else + SetTitle(L"RealTimeSync " + utfTo(fff::ffsVersion) + SPACED_DASH + _("Automated Synchronization")); + +} + + +void MainDialog::onConfigLoad(wxCommandEvent& event) +{ + const Zstring activeCfgFilePath = !equalNativePath(activeConfigFile_, lastRunConfigPath_) ? activeConfigFile_ : Zstring(); + //better: use last user-selected config path instead! + + std::optional defaultFolderPath = getParentFolderPath(activeCfgFilePath); + + wxFileDialog fileSelector(this, wxString() /*message*/, utfTo(defaultFolderPath ? *defaultFolderPath : Zstr("")), wxString() /*default file name*/, + wxString(L"RealTimeSync (*.ffs_real; *.ffs_batch)|*.ffs_real;*.ffs_batch") + L"|" +_("All files") + L" (*.*)|*", + wxFD_OPEN); + if (fileSelector.ShowModal() != wxID_OK) + return; + + loadConfig(utfTo(fileSelector.GetPath())); +} + + +void MainDialog::onFilesDropped(FileDropEvent& event) +{ + if (!event.itemPaths_.empty()) + loadConfig(event.itemPaths_[0]); +} + + +void MainDialog::setConfiguration(const FfsRealConfig& cfg) +{ + const Zstring& firstFolderPath = cfg.directories.empty() ? Zstring() : cfg.directories[0]; + const std::vector addFolderPaths = cfg.directories.empty() ? std::vector() : + std::vector(cfg.directories.begin() + 1, cfg.directories.end()); + + firstFolderPanel_->setPath(firstFolderPath); + + bSizerFolders->Clear(true); + additionalFolderPanels_.clear(); + + insertAddFolder(addFolderPaths, 0); + + m_textCtrlCommand->SetValue(utfTo(cfg.commandline)); + m_spinCtrlDelay ->SetValue(static_cast(cfg.delay)); +} + + +FfsRealConfig MainDialog::getConfiguration() +{ + FfsRealConfig output; + + output.directories.push_back(firstFolderPanel_->getPath()); + + for (const DirectoryPanel* dp : additionalFolderPanels_) + output.directories.push_back(dp->getPath()); + + output.commandline = utfTo(m_textCtrlCommand->GetValue()); + output.delay = m_spinCtrlDelay->GetValue(); + + return output; +} + + +void MainDialog::onAddFolder(wxCommandEvent& event) +{ + const Zstring topFolder = firstFolderPanel_->getPath(); + + //clear existing top folder first + firstFolderPanel_->setPath(Zstring()); + + insertAddFolder({topFolder}, 0); +} + + +void MainDialog::onRemoveFolder(wxCommandEvent& event) +{ + //find folder pair originating the event + const wxObject* const eventObj = event.GetEventObject(); + for (auto it = additionalFolderPanels_.begin(); it != additionalFolderPanels_.end(); ++it) + if (eventObj == static_cast((*it)->m_bpButtonRemoveFolder)) + { + removeAddFolder(it - additionalFolderPanels_.begin()); + return; + } +} + + +void MainDialog::onRemoveTopFolder(wxCommandEvent& event) +{ + if (!additionalFolderPanels_.empty()) + { + firstFolderPanel_->setPath(additionalFolderPanels_[0]->getPath()); + removeAddFolder(0); //remove first of additional folders + } +} + + +void MainDialog::insertAddFolder(const std::vector& newFolders, size_t pos) +{ + assert(pos <= additionalFolderPanels_.size() && additionalFolderPanels_.size() == bSizerFolders->GetItemCount()); + pos = std::min(pos, additionalFolderPanels_.size()); + + for (size_t i = 0; i < newFolders.size(); ++i) + { + //add new folder pair + DirectoryPanel* newFolder = new DirectoryPanel(m_scrolledWinFolders, *this, folderLastSelected_); + + bSizerFolders->Insert(pos + i, newFolder, 0, wxEXPAND); + additionalFolderPanels_.insert(additionalFolderPanels_.begin() + pos + i, newFolder); + + //register events + newFolder->m_bpButtonRemoveFolder->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](wxCommandEvent& event) { onRemoveFolder(event); }); + + //make sure panel has proper default height + newFolder->GetSizer()->SetSizeHints(newFolder); //~=Fit() + SetMinSize() + + newFolder->setPath(newFolders[i]); + } + + //set size of scrolled window + const int folderHeight = additionalFolderPanels_.empty() ? 0 : additionalFolderPanels_[0]->GetSize().GetHeight(); + const size_t visibleRows = std::min(additionalFolderPanels_.size(), MAX_ADD_FOLDERS); //up to MAX_ADD_FOLDERS additional folders shall be shown + + m_scrolledWinFolders->SetMinSize({-1, folderHeight * static_cast(visibleRows)}); + m_panelMain->Layout(); //[!] get scrollbars to update correctly + + //adapt delete top folder pair button + m_bpButtonRemoveTopFolder->Show(!additionalFolderPanels_.empty()); + + GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() + + Refresh(); //remove a little flicker near the start button +} + + +void MainDialog::removeAddFolder(size_t pos) +{ + if (pos < additionalFolderPanels_.size()) + { + //remove folder pairs from window + DirectoryPanel* pairToDelete = additionalFolderPanels_[pos]; + + bSizerFolders->Detach(pairToDelete); //Remove() does not work on Window*, so do it manually + additionalFolderPanels_.erase(additionalFolderPanels_.begin() + pos); //remove last element in vector + //more (non-portable) wxWidgets bullshit: on OS X wxWindow::Destroy() screws up and calls "operator delete" directly rather than + //the deferred deletion it is expected to do (and which is implemented correctly on Windows and Linux) + //http://bb10.com/python-wxpython-devel/2012-09/msg00004.html + //=> since we're in a mouse button callback of a sub-component of "pairToDelete" we need to delay deletion ourselves: + guiQueue_.processAsync([] {}, [pairToDelete] { pairToDelete->Destroy(); }); + + //set size of scrolled window + const int folderHeight = additionalFolderPanels_.empty() ? 0 : additionalFolderPanels_[0]->GetSize().GetHeight(); + const size_t visibleRows = std::min(additionalFolderPanels_.size(), MAX_ADD_FOLDERS); //up to MAX_ADD_FOLDERS additional folders shall be shown + + m_scrolledWinFolders->SetMinSize({-1, folderHeight * static_cast(visibleRows)}); + m_panelMain->Layout(); //[!] get scrollbars to update correctly + + //adapt delete top folder pair button + m_bpButtonRemoveTopFolder->Show(!additionalFolderPanels_.empty()); + + GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() + + Refresh(); //remove a little flicker near the start button + } +} diff --git a/FreeFileSync/Source/RealTimeSync/main_dlg.h b/FreeFileSync/Source/RealTimeSync/main_dlg.h new file mode 100644 index 0000000..888747c --- /dev/null +++ b/FreeFileSync/Source/RealTimeSync/main_dlg.h @@ -0,0 +1,75 @@ +// ***************************************************************************** +// * 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 MAIN_DLG_H_2384790842252445 +#define MAIN_DLG_H_2384790842252445 + +#include "gui_generated.h" +//#include +//#include +#include +#include +#include +//#include +#include "folder_selector2.h" + + +namespace rts +{ +struct FfsRealConfig; +class DirectoryPanel; + + +class MainDialog: public MainDlgGenerated +{ +public: + static void create(const Zstring& cfgFilePath); + + void loadConfig(const Zstring& filepath); + +private: + MainDialog(const Zstring& cfgFilePath); + ~MainDialog(); + + void onBeforeSystemShutdown(); //last chance to do something useful before killing the application! + + void onClose (wxCloseEvent& event ) override { Destroy(); } + void onShowHelp (wxCommandEvent& event) override { wxLaunchDefaultBrowser(L"https://freefilesync.org/manual.php?topic=realtimesync"); } + void onMenuAbout (wxCommandEvent& event) override; + void onAddFolder (wxCommandEvent& event) override; + void onRemoveFolder (wxCommandEvent& event); + void onRemoveTopFolder(wxCommandEvent& event) override; + void onLocalKeyEvent (wxKeyEvent& event); + void onStart (wxCommandEvent& event) override; + void onConfigNew (wxCommandEvent& event) override { loadConfig({}); } + void onConfigSave (wxCommandEvent& event) override; + void onConfigLoad (wxCommandEvent& event) override; + void onMenuQuit (wxCommandEvent& event) override { Close(); } + void onFilesDropped(zen::FileDropEvent& event); + + void setConfiguration(const FfsRealConfig& cfg); + FfsRealConfig getConfiguration(); + void setLastUsedConfig(const Zstring& filepath); + + void insertAddFolder(const std::vector& newFolders, size_t pos); + void removeAddFolder(size_t pos); + + std::unique_ptr firstFolderPanel_; + std::vector additionalFolderPanels_; //additional pairs to the standard pair + + + const Zstring lastRunConfigPath_; + Zstring activeConfigFile_; //optional + + Zstring folderLastSelected_; + + zen::AsyncGuiQueue guiQueue_; //schedule and run long-running tasks asynchronously, but process results on GUI queue + + const zen::SharedRef> onBeforeSystemShutdownCookie_ = zen::makeSharedRef>([this] { onBeforeSystemShutdown(); }); +}; +} + +#endif //MAIN_DLG_H_2384790842252445 diff --git a/FreeFileSync/Source/RealTimeSync/monitor.cpp b/FreeFileSync/Source/RealTimeSync/monitor.cpp new file mode 100644 index 0000000..da1061d --- /dev/null +++ b/FreeFileSync/Source/RealTimeSync/monitor.cpp @@ -0,0 +1,280 @@ +// ***************************************************************************** +// * 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 "monitor.h" +#include +#include +#include +#include +//#include "../library/db_file.h" //SYNC_DB_FILE_ENDING -> complete file too much of a dependency; file ending too little to decouple into single header +//#include "../library/lock_holder.h" //LOCK_FILE_ENDING +//TEMP_FILE_ENDING + +using namespace zen; + + +namespace +{ +constexpr std::chrono::seconds FOLDER_EXISTENCE_CHECK_INTERVAL(1); + + +//wait until all directories become available (again) + logs in network share +std::set waitForMissingDirs(const std::vector& folderPathPhrases, //throw FileError + const std::function& requestUiUpdate, std::chrono::milliseconds cbInterval) +{ + //early failure! check for unsupported folder paths: + for (const char* protoName : {"ftp", "sftp", "mtp", "gdrive"}) + for (const Zstring& phrase : folderPathPhrases) + //hopefully clear enough now: https://freefilesync.org/forum/viewtopic.php?t=4302 + if (startsWithAsciiNoCase(trimCpy(phrase), std::string(protoName) + ':')) + throw FileError(replaceCpy(_("The %x protocol does not support directory monitoring:"), L"%x", utfTo(protoName)) + L"\n\n" + fmtPath(phrase)); + + for (;;) + { + struct FolderInfo + { + Zstring folderPathPhrase; + std::future folderAvailable; + }; + std::map folderInfos; + + for (const Zstring& phrase : folderPathPhrases) + { + const Zstring& folderPath = getResolvedFilePath(phrase); + + //start all folder checks asynchronously (non-existent network path may block) + if (!folderInfos.contains(folderPath)) + folderInfos[folderPath] = { phrase, runAsync([folderPath] + { + try + { + getItemType(folderPath); //throw FileError + return true; + } + catch (FileError&) { return false; } + }) + }; + } + + std::set availablePaths; + std::set missingPathPhrases; + for (auto& [folderPath, folderInfo] : folderInfos) + { + std::future& folderAvailable = folderInfo.folderAvailable; + + while (folderAvailable.wait_for(cbInterval) == std::future_status::timeout) + requestUiUpdate(folderPath); //throw X + + if (folderAvailable.get()) + availablePaths.insert(folderPath); + else + missingPathPhrases.insert(folderInfo.folderPathPhrase); + } + if (missingPathPhrases.empty()) + return availablePaths; //only return when all folders were found on *first* try! + + + auto delayUntil = std::chrono::steady_clock::now() + FOLDER_EXISTENCE_CHECK_INTERVAL; + + for (const Zstring& folderPathPhrase : missingPathPhrases) + for (;;) + { + //support specifying volume by name => call getResolvedFilePath() repeatedly + const Zstring folderPath = getResolvedFilePath(folderPathPhrase); + + //wait some time... + for (auto now = std::chrono::steady_clock::now(); now < delayUntil; now = std::chrono::steady_clock::now()) + { + requestUiUpdate(folderPath); //throw X + std::this_thread::sleep_for(cbInterval); + } + + std::future folderAvailable = runAsync([folderPath] + { + try + { + getItemType(folderPath); //throw FileError + return true; + } + catch (FileError&) { return false; } + }); + + while (folderAvailable.wait_for(cbInterval) == std::future_status::timeout) + requestUiUpdate(folderPath); //throw X + + if (folderAvailable.get()) + break; + //else: wait until folder is available: do not needlessly poll existing folders again! + delayUntil = std::chrono::steady_clock::now() + FOLDER_EXISTENCE_CHECK_INTERVAL; + } + } +} + + +//wait until changes are detected or if a directory is not available (anymore) +DirWatcher::Change waitForChanges(const std::set& folderPaths, //throw FileError + const std::function& requestUiUpdate, std::chrono::milliseconds cbInterval) +{ + if (folderPaths.empty()) //pathological case, but we have to check or this function waits forever + throw FileError(_("A folder input field is empty.")); //should have been checked by caller! + + std::vector>> watches; + + for (const Zstring& folderPath : folderPaths) + try + { + watches.emplace_back(folderPath, std::make_unique(folderPath)); //throw FileError + } + catch (FileError&) + { + try { getItemType(folderPath); } //throw FileError + catch (FileError&) + { + assert(false); //why "unavailable"!? violating waitForChanges() precondition! + return {DirWatcher::ChangeType::baseFolderUnavailable, folderPath}; + } + + throw; + } + + auto lastCheckTime = std::chrono::steady_clock::now(); + for (;;) + { + const bool checkDirNow = [&] //checking once per sec should suffice + { + const auto now = std::chrono::steady_clock::now(); + if (now > lastCheckTime + FOLDER_EXISTENCE_CHECK_INTERVAL) + { + lastCheckTime = now; + return true; + } + return false; + }(); + + for (const auto& [folderPath, watcher] : watches) + { + //IMPORTANT CHECK: DirWatcher has problems detecting removal of top watched directories! + if (checkDirNow) + try //catch errors related to directory removal, e.g. ERROR_NETNAME_DELETED + { + getItemType(folderPath); //throw FileError + } + catch (FileError&) { return {DirWatcher::ChangeType::baseFolderUnavailable, folderPath}; } + + try + { + std::vector changes = watcher->fetchChanges([&] { requestUiUpdate(false /*readyForSync*/); /*throw X*/ }, + cbInterval); //throw FileError + + //give precedence to ChangeType::baseFolderUnavailable + for (const DirWatcher::Change& change : changes) + if (change.type == DirWatcher::ChangeType::baseFolderUnavailable) + return change; + + std::erase_if(changes, [](const DirWatcher::Change& e) + { + return + endsWith(e.itemPath, Zstr(".ffs_tmp")) || //sync.8ea2.ffs_tmp + endsWith(e.itemPath, Zstr(".ffs_lock")) || //sync.ffs_lock, sync.Del.ffs_lock + endsWith(e.itemPath, Zstr(".ffs_db")); //sync.ffs_db + //no need to ignore temporary recycle bin directory: this must be caused by a file deletion anyway + }); + + if (!changes.empty()) + return changes[0]; + } + catch (FileError&) + { + try { getItemType(folderPath); } //throw FileError + catch (FileError&) { return {DirWatcher::ChangeType::baseFolderUnavailable, folderPath}; } + + throw; + } + } + + std::this_thread::sleep_for(cbInterval); + requestUiUpdate(true /*readyForSync*/); //throw X: may start sync at this presumably idle time + } +} + + +std::wstring getChangeTypeName(DirWatcher::ChangeType type) +{ + switch (type) + { + case DirWatcher::ChangeType::create: + return L"Create"; + case DirWatcher::ChangeType::update: + return L"Update"; + case DirWatcher::ChangeType::remove: + return L"Delete"; + case DirWatcher::ChangeType::baseFolderUnavailable: + return L"Base Folder Unavailable"; + } + assert(false); + return L"Error"; +} + +struct ExecCommandNowException {}; +} + + +void rts::monitorDirectories(const std::vector& folderPathPhrases, std::chrono::seconds delay, + const std::function& executeExternalCommand /*throw FileError*/, + const std::function& requestUiUpdate, + const std::function& reportError, + std::chrono::milliseconds cbInterval) +{ + assert(!folderPathPhrases.empty()); + if (folderPathPhrases.empty()) + return; + + for (;;) + try + { + std::set folderPaths = waitForMissingDirs(folderPathPhrases, [&](const Zstring& folderPath) { requestUiUpdate(&folderPath); }, cbInterval); //throw FileError + + //schedule initial execution (*after* all directories have arrived) + auto nextExecTime = std::chrono::steady_clock::now() + delay; + + for (;;) //command executions + { + DirWatcher::Change lastChangeDetected; + try + { + for (;;) //detected changes + { + lastChangeDetected = waitForChanges(folderPaths, [&](bool readyForSync) //throw FileError, ExecCommandNowException + { + requestUiUpdate(nullptr); + + if (readyForSync && std::chrono::steady_clock::now() >= nextExecTime) + throw ExecCommandNowException(); //abort wait and start sync + }, cbInterval); + + if (lastChangeDetected.type == DirWatcher::ChangeType::baseFolderUnavailable) + //don't execute the command before all directories are available! + folderPaths = waitForMissingDirs(folderPathPhrases, [&](const Zstring& folderPath) { requestUiUpdate(&folderPath); }, cbInterval); //throw FileError + + nextExecTime = std::chrono::steady_clock::now() + delay; + } + } + catch (ExecCommandNowException&) {} + + try + { + executeExternalCommand(lastChangeDetected.itemPath, getChangeTypeName(lastChangeDetected.type)); //throw FileError + } + catch (const FileError& e) { reportError(e.toString()); } + + nextExecTime = std::chrono::steady_clock::time_point::max(); + } + } + catch (const FileError& e) + { + reportError(e.toString()); + } +} diff --git a/FreeFileSync/Source/RealTimeSync/monitor.h b/FreeFileSync/Source/RealTimeSync/monitor.h new file mode 100644 index 0000000..8a660f4 --- /dev/null +++ b/FreeFileSync/Source/RealTimeSync/monitor.h @@ -0,0 +1,26 @@ +// ***************************************************************************** +// * 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 MONITOR_H_345087425834253425 +#define MONITOR_H_345087425834253425 + +#include +#include +#include + + +namespace rts +{ +void monitorDirectories(const std::vector& folderPathPhrases, + //non-formatted paths that yet require call to getFormattedDirectoryName(); empty directories must be checked by caller! + std::chrono::seconds delay, + const std::function& executeExternalCommand, + const std::function& requestUiUpdate, //either waiting for change notifications or at least one folder is missing + const std::function& reportError, //automatically retries after return! + std::chrono::milliseconds cbInterval); +} + +#endif //MONITOR_H_345087425834253425 diff --git a/FreeFileSync/Source/RealTimeSync/tray_menu.cpp b/FreeFileSync/Source/RealTimeSync/tray_menu.cpp new file mode 100644 index 0000000..1d1e025 --- /dev/null +++ b/FreeFileSync/Source/RealTimeSync/tray_menu.cpp @@ -0,0 +1,314 @@ +// ***************************************************************************** +// * 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 "tray_menu.h" +#include +#include +#include +#include //Linux needs this +#include +#include +#include +#include +#include +#include +#include +#include +#include "monitor.h" + +using namespace zen; +using namespace rts; + + +namespace +{ +constexpr std::chrono::seconds RETRY_AFTER_ERROR_INTERVAL(15); +constexpr std::chrono::milliseconds UI_UPDATE_INTERVAL(100); //perform ui updates not more often than necessary, 100 seems to be a good value with only a minimal performance loss + + +std::chrono::steady_clock::time_point lastExec; + + +bool uiUpdateDue() +{ + const auto now = std::chrono::steady_clock::now(); + + if (now > lastExec + UI_UPDATE_INTERVAL) + { + lastExec = now; + return true; + } + return false; +} + + +enum TrayMode +{ + active, + waiting, + error, +}; + + +class TrayIcon : public wxTaskBarIcon +{ +public: + explicit TrayIcon(const wxString& jobname) : + jobName_(jobname) + { + Bind(wxEVT_TASKBAR_LEFT_UP, [this](wxTaskBarIconEvent& event) { onMouseClick(event); }); + + assert(mode_ != TrayMode::active); //setMode() supports polling! + setMode(TrayMode::active, Zstring()); + + timer_.Bind(wxEVT_TIMER, [this](wxTimerEvent& event) { onErrorFlashIcon(event); }); + } + + //require polling: + bool resumeIsRequested() const { return resumeRequested_; } + bool abortIsRequested () const { return exitRequested_; } + + //during TrayMode::error those two functions are available: + void clearShowErrorRequested() { assert(mode_ == TrayMode::error); showErrorMsgRequested_ = false; } + bool getShowErrorRequested() const { assert(mode_ == TrayMode::error); return showErrorMsgRequested_; } + + void setMode(TrayMode m, const Zstring& missingFolderPath) + { + if (mode_ == m && missingFolderPath_ == missingFolderPath) + return; //support polling + + mode_ = m; + missingFolderPath_ = missingFolderPath; + + timer_.Stop(); + switch (m) + { + case TrayMode::active: + setTrayIcon(trayImg_, _("Directory monitoring active")); + break; + + case TrayMode::waiting: + assert(!missingFolderPath.empty()); + setTrayIcon(greyScale(trayImg_), _("Waiting until directory is available:") + L' ' + fmtPath(missingFolderPath)); + break; + + case TrayMode::error: + timer_.Start(500); //timer interval in [ms] + break; + } + } + +private: + void onErrorFlashIcon(wxEvent& event) + { + iconFlashStatusLast_ = !iconFlashStatusLast_; + setTrayIcon(greyScaleIfDisabled(trayImg_, iconFlashStatusLast_), _("Error")); + } + + void setTrayIcon(const wxImage& img, const wxString& statusTxt) + { + wxString tooltip = L"RealTimeSync"; + if (!jobName_.empty()) + tooltip += SPACED_DASH + jobName_; + + tooltip += L"\n" + statusTxt; + + SetIcon(toScaledBitmap(img), tooltip); + } + + wxMenu* CreatePopupMenu() override + { + wxMenu* contextMenu = new wxMenu; + + wxMenuItem* defaultItem = nullptr; + switch (mode_) + { + case TrayMode::active: + case TrayMode::waiting: + defaultItem = new wxMenuItem(contextMenu, wxID_ANY, _("&Configure")); //better than "Restore"? https://freefilesync.org/forum/viewtopic.php?t=2044&p=20391#p20391 + contextMenu->Bind(wxEVT_COMMAND_MENU_SELECTED, [this](wxCommandEvent& event) { resumeRequested_ = true; }, defaultItem->GetId()); + break; + + case TrayMode::error: + defaultItem = new wxMenuItem(contextMenu, wxID_ANY, _("&Show error message")); + contextMenu->Bind(wxEVT_COMMAND_MENU_SELECTED, [this](wxCommandEvent& event) { showErrorMsgRequested_ = true; }, defaultItem->GetId()); + break; + } + contextMenu->Append(defaultItem); + + contextMenu->AppendSeparator(); + + wxMenuItem* itemAbort = contextMenu->Append(wxID_ANY, _("&Quit")); + contextMenu->Bind(wxEVT_COMMAND_MENU_SELECTED, [this](wxCommandEvent& event) { exitRequested_ = true; }, itemAbort->GetId()); + + return contextMenu; //ownership transferred to caller + } + + void onMouseClick(wxEvent& event) + { + switch (mode_) + { + case TrayMode::active: + case TrayMode::waiting: + resumeRequested_ = true; //never throw exceptions through a C-Layer call stack (GUI)! + break; + case TrayMode::error: + showErrorMsgRequested_ = true; + break; + } + } + + bool resumeRequested_ = false; + bool exitRequested_ = false; + bool showErrorMsgRequested_ = false; + + TrayMode mode_ = TrayMode::waiting; + Zstring missingFolderPath_; + + bool iconFlashStatusLast_ = false; //flash try icon for TrayMode::error + wxTimer timer_; // + + const wxString jobName_; //RTS job name, may be empty + + const wxImage trayImg_ = loadImage("start_rts", dipToScreen(24)); //use 24x24 bitmap for perfect fit +}; + + +struct AbortMonitoring //exception class +{ + AbortMonitoring(CancelReason reasonCode) : reasonCode_(reasonCode) {} + CancelReason reasonCode_; +}; + + +//=> don't derive from wxEvtHandler or any other wxWidgets object unless instance is safely deleted (deferred) during idle event!!tray_icon.h +class TrayIconHolder +{ +public: + explicit TrayIconHolder(const wxString& jobname) : + trayIcon_(new TrayIcon(jobname)) {} + + ~TrayIconHolder() + { + //harmonize with tray_icon.cpp!!! + trayIcon_->RemoveIcon(); + //*schedule* for destruction: delete during next idle event (handle late window messages, e.g. when double-clicking) + trayIcon_->Destroy(); //uses wxPendingDelete + } + + void doUiRefreshNow() //throw AbortMonitoring + { + wxTheApp->Yield(); //yield is UI-layer which is represented by this tray icon + + //advantage of polling vs callbacks: we can throw exceptions! + if (trayIcon_->resumeIsRequested()) + throw AbortMonitoring(CancelReason::requestGui); + + if (trayIcon_->abortIsRequested()) + throw AbortMonitoring(CancelReason::requestExit); + } + + void setMode(TrayMode m, const Zstring& missingFolderPath) { trayIcon_->setMode(m, missingFolderPath); } + + bool getShowErrorRequested() const { return trayIcon_->getShowErrorRequested(); } + void clearShowErrorRequested() { trayIcon_->clearShowErrorRequested(); } + +private: + TrayIcon* const trayIcon_; +}; + +//############################################################################################################## +} + + +rts::CancelReason rts::runFolderMonitor(const FfsRealConfig& config, const wxString& jobname) +{ + std::vector dirNamesNonFmt = config.directories; + std::erase_if(dirNamesNonFmt, [](const Zstring& str) { return trimCpy(str).empty(); }); //remove empty entries WITHOUT formatting paths yet! + + if (dirNamesNonFmt.empty()) + { + showNotificationDialog(nullptr, DialogInfoType::error, PopupDialogCfg().setMainInstructions(_("A folder input field is empty."))); + return CancelReason::requestGui; + } + + const Zstring cmdLine = trimCpy(config.commandline); + + if (cmdLine.empty()) + { + showNotificationDialog(nullptr, DialogInfoType::error, PopupDialogCfg().setMainInstructions(replaceCpy(_("Command %x failed."), L"%x", fmtPath(cmdLine)))); + return CancelReason::requestGui; + } + + + TrayIconHolder trayIcon(jobname); + + auto executeExternalCommand = [&](const Zstring& changedItemPath, const std::wstring& actionName) //throw FileError + { + ::wxSetEnv(L"change_path", utfTo(changedItemPath)); //crude way to report changed file + ::wxSetEnv(L"change_action", actionName); // + auto cmdLineExp = expandMacros(cmdLine); + + try + { + if (const auto& [exitCode, output] = consoleExecute(cmdLineExp, std::nullopt /*timeoutMs*/); //throw SysError, (SysErrorTimeOut) + exitCode != 0) + throw SysError(formatSystemError("", replaceCpy(_("Exit code %x"), L"%x", numberTo(exitCode)), utfTo(output))); + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Command %x failed."), L"%x", fmtPath(cmdLineExp)), e.toString()); } + }; + + auto requestUiUpdate = [&](const Zstring* missingFolderPath) + { + if (missingFolderPath) + trayIcon.setMode(TrayMode::waiting, *missingFolderPath); + else + trayIcon.setMode(TrayMode::active, Zstring()); + + if (uiUpdateDue()) + trayIcon.doUiRefreshNow(); //throw AbortMonitoring + }; + + auto reportError = [&](const std::wstring& msg) + { + trayIcon.setMode(TrayMode::error, Zstring()); + trayIcon.clearShowErrorRequested(); + + //wait for some time, then return to retry + const auto delayUntil = std::chrono::steady_clock::now() + RETRY_AFTER_ERROR_INTERVAL; + for (auto now = std::chrono::steady_clock::now(); now < delayUntil; now = std::chrono::steady_clock::now()) + { + trayIcon.doUiRefreshNow(); //throw AbortMonitoring + + if (trayIcon.getShowErrorRequested()) + switch (showConfirmationDialog(nullptr, DialogInfoType::error, PopupDialogCfg(). + setDetailInstructions(msg), _("&Retry"))) + { + case ConfirmationButton::accept: //retry + return; + + case ConfirmationButton::cancel: + throw AbortMonitoring(CancelReason::requestGui); + } + std::this_thread::sleep_for(UI_UPDATE_INTERVAL); + } + }; + + try + { + monitorDirectories(dirNamesNonFmt, std::chrono::seconds(config.delay), + executeExternalCommand /*throw FileError*/, + requestUiUpdate, //throw AbortMonitoring + reportError, // + UI_UPDATE_INTERVAL / 2); + assert(false); + return CancelReason::requestGui; + } + catch (const AbortMonitoring& ab) + { + return ab.reasonCode_; + } +} diff --git a/FreeFileSync/Source/RealTimeSync/tray_menu.h b/FreeFileSync/Source/RealTimeSync/tray_menu.h new file mode 100644 index 0000000..01a894f --- /dev/null +++ b/FreeFileSync/Source/RealTimeSync/tray_menu.h @@ -0,0 +1,24 @@ +// ***************************************************************************** +// * 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 TRAY_MENU_H_3967857420987534253245 +#define TRAY_MENU_H_3967857420987534253245 + +#include +#include "config.h" + + +namespace rts +{ +enum class CancelReason +{ + requestGui, + requestExit +}; +CancelReason runFolderMonitor(const FfsRealConfig& config, const wxString& jobname); //jobname may be empty +} + +#endif //TRAY_MENU_H_3967857420987534253245 diff --git a/FreeFileSync/Source/afs/abstract.cpp b/FreeFileSync/Source/afs/abstract.cpp new file mode 100644 index 0000000..da7ff9a --- /dev/null +++ b/FreeFileSync/Source/afs/abstract.cpp @@ -0,0 +1,501 @@ +// ***************************************************************************** +// * 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 "abstract.h" +#include +#include +#include +#include +#include + +using namespace zen; +using namespace fff; +using AFS = AbstractFileSystem; + + +AfsPath fff::sanitizeDeviceRelativePath(Zstring relPath) +{ + if constexpr (FILE_NAME_SEPARATOR != Zstr('/' )) replace(relPath, Zstr('/'), FILE_NAME_SEPARATOR); + if constexpr (FILE_NAME_SEPARATOR != Zstr('\\')) replace(relPath, Zstr('\\'), FILE_NAME_SEPARATOR); + trim(relPath, TrimSide::both, [](Zchar c) { return c == FILE_NAME_SEPARATOR; }); + return AfsPath(relPath); +} + + +std::weak_ordering AFS::compareDevice(const AbstractFileSystem& lhs, const AbstractFileSystem& rhs) +{ + //note: in worst case, order is guaranteed to be stable only during each program run + //caveat: typeid returns static type for pointers, dynamic type for references!!! + if (const std::strong_ordering cmp = std::type_index(typeid(lhs)) <=> std::type_index(typeid(rhs)); + cmp != std::strong_ordering::equal) + return cmp; + + return lhs.compareDeviceSameAfsType(rhs); +} + + +std::optional AFS::getParentPath(const AbstractPath& itemPath) +{ + if (const std::optional parentPath = getParentPath(itemPath.afsPath)) + return AbstractPath(itemPath.afsDevice, *parentPath); + + return {}; +} + + +std::optional AFS::getParentPath(const AfsPath& itemPath) +{ + if (!itemPath.value.empty()) + return AfsPath(beforeLast(itemPath.value, FILE_NAME_SEPARATOR, IfNotFoundReturn::none)); + + return {}; +} + + +namespace +{ +struct FlatTraverserCallback : public AFS::TraverserCallback +{ + FlatTraverserCallback(const std::function& onFile, + const std::function& onFolder, + const std::function& onSymlink) : + onFile_ (onFile), + onFolder_ (onFolder), + onSymlink_(onSymlink) {} + +private: + void onFile (const AFS::FileInfo& fi) override { if (onFile_) onFile_ (fi); } + std::shared_ptr onFolder (const AFS::FolderInfo& fi) override { if (onFolder_) onFolder_ (fi); return nullptr; } + HandleLink onSymlink(const AFS::SymlinkInfo& si) override { if (onSymlink_) onSymlink_(si); return TraverserCallback::HandleLink::skip; } + + HandleError reportDirError (const ErrorInfo& errorInfo) override { throw FileError(errorInfo.msg); } + HandleError reportItemError(const ErrorInfo& errorInfo, const Zstring& itemName) override { throw FileError(errorInfo.msg); } + + const std::function onFile_; + const std::function onFolder_; + const std::function onSymlink_; +}; +} + + +void AFS::traverseFolder(const AfsPath& folderPath, //throw FileError + const std::function& onFile, + const std::function& onFolder, + const std::function& onSymlink) const +{ + auto ft = std::make_shared(onFile, onFolder, onSymlink); //throw FileError + traverseFolderRecursive({{folderPath, ft}}, 1 /*parallelOps*/); //throw FileError +} + + +//already existing: undefined behavior! (e.g. fail/overwrite/auto-rename) +AFS::FileCopyResult AFS::copyFileAsStream(const AfsPath& sourcePath, const StreamAttributes& attrSource, //throw FileError, ErrorFileLocked, X + const AbstractPath& targetPath, const IoCallback& notifyUnbufferedIO /*throw X*/) const +{ + auto streamIn = getInputStream(sourcePath); //throw FileError, ErrorFileLocked + +#warning("maybe only call if deviating from attrSource!? support file append in progress") + + StreamAttributes attrSourceNew = {}; + //try to get the most current attributes if possible (input file might have changed after comparison!) + if (std::optional attr = streamIn->tryGetAttributesFast()) //throw FileError + attrSourceNew = *attr; //Native/MTP/Google Drive + else //use possibly stale ones: + attrSourceNew = attrSource; //SFTP/FTP + //TODO: evaluate: consequences of stale attributes + + //already existing: undefined behavior! (e.g. fail/overwrite/auto-rename) + auto streamOut = getOutputStream(targetPath, attrSourceNew.fileSize, attrSourceNew.modTime); //throw FileError + + int64_t totalBytesNotified = 0; + IOCallbackDivider notifyIoDiv(notifyUnbufferedIO, totalBytesNotified); + + const uint64_t streamSize = unbufferedStreamCopy([&](void* buffer, size_t bytesToRead) + { + return streamIn->tryRead(buffer, bytesToRead, notifyIoDiv); //throw FileError, ErrorFileLocked, X + }, + streamIn->getBlockSize() /*throw FileError*/, + + [&](const void* buffer, size_t bytesToWrite) + { + return streamOut->tryWrite(buffer, bytesToWrite, notifyIoDiv); //throw FileError, X + }, + streamOut->getBlockSize() /*throw FileError*/); //throw FileError, ErrorFileLocked, X + + //check incomplete input *before* failing with (slightly) misleading error message in OutputStream::finalize() + if (streamSize != attrSourceNew.fileSize) + throw FileError(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(getDisplayPath(sourcePath))), + _("Unexpected size of data stream:") + L' ' + formatNumber(streamSize) + L'\n' + + _("Expected:") + L' ' + formatNumber(attrSourceNew.fileSize) + L" [unbufferedStreamCopy]"); + + const FinalizeResult finResult = streamOut->finalize(notifyIoDiv); //throw FileError, X + + ZEN_ON_SCOPE_FAIL(try { removeFilePlain(targetPath); } + catch (const FileError& e) { logExtraError(e.toString()); }); //after finalize(): not guarded by ~AFS::OutputStream() anymore! + //-------------------------------------------------------------------------------------------------------- + + //catch file I/O notification bugs => should never happen in *cross-device* context... OTOH BackupRead/BackupWrite may notify less data when copying sparse files + if (totalBytesNotified != makeSigned(2 * streamSize)) + throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(getDisplayPath(targetPath))), + _("Unexpected size of data stream:") + L' ' + formatNumber(totalBytesNotified) + L'\n' + + _("Expected:") + L' ' + formatNumber(2 * streamSize) + L" [IOCallbackDivider]"); + return + { + .fileSize = attrSourceNew.fileSize, + .modTime = attrSourceNew.modTime, + .sourceFilePrint = attrSourceNew.filePrint, + .targetFilePrint = finResult.filePrint, + .errorModTime = finResult.errorModTime, + /* Failing to set modification time is not a fatal error from synchronization perspective (treat like external update) + => Support additional scenarios: + - GVFS failing to set modTime for FTP: https://freefilesync.org/forum/viewtopic.php?t=2372 + - GVFS failing to set modTime for MTP: https://freefilesync.org/forum/viewtopic.php?t=2803 + - MTP failing to set modTime in general: fail non-silently rather than silently during file creation + - FTP failing to set modTime for servers without MFMT-support */ + }; +} + + +//already existing + no onDeleteTargetFile: undefined behavior! (e.g. fail/overwrite/auto-rename) +AFS::FileCopyResult AFS::copyFileTransactional(const AbstractPath& sourcePath, const StreamAttributes& attrSource, //throw FileError, ErrorFileLocked, X + const AbstractPath& targetPath, + bool copyFilePermissions, + bool transactionalCopy, + const std::function& onDeleteTargetFile, + const IoCallback& notifyUnbufferedIO /*throw X*/) +{ + auto copyFilePlain = [&](const AbstractPath& targetPathTmp) + { + //caveat: typeid returns static type for pointers, dynamic type for references!!! + if (typeid(sourcePath.afsDevice.ref()) == typeid(targetPathTmp.afsDevice.ref())) + return sourcePath.afsDevice.ref().copyFileForSameAfsType(sourcePath.afsPath, attrSource, + targetPathTmp, copyFilePermissions, notifyUnbufferedIO); //throw FileError, ErrorFileLocked, X + //already existing: undefined behavior! (e.g. fail/overwrite/auto-rename) + + //fall back to stream-based file copy: + if (copyFilePermissions) + throw FileError(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(getDisplayPath(targetPathTmp))), + _("Operation not supported between different devices.")); + + //already existing: undefined behavior! (e.g. fail/overwrite/auto-rename) + return sourcePath.afsDevice.ref().copyFileAsStream(sourcePath.afsPath, attrSource, targetPathTmp, notifyUnbufferedIO); //throw FileError, ErrorFileLocked, X + }; + + if (transactionalCopy && !hasNativeTransactionalCopy(targetPath)) + { + const std::optional parentPath = getParentPath(targetPath); + if (!parentPath) + throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(getDisplayPath(targetPath))), L"Path is device root."); + const Zstring fileName = getItemName(targetPath); + + //- generate (hopefully) unique file name to avoid clashing with some remnant ffs_tmp file + //- do not loop: avoid pathological cases, e.g. https://freefilesync.org/forum/viewtopic.php?t=1592 + Zstring tmpName = beforeLast(fileName, Zstr('.'), IfNotFoundReturn::all); + + //don't make the temp name longer than the original when hitting file system name length limitations: "lpMaximumComponentLength is commonly 255 characters" + while (tmpName.size() > 200) //BUT don't trim short names! we want early failure on filename-related issues + tmpName = getUnicodeSubstring(tmpName, 0 /*uniPosFirst*/, unicodeLength(tmpName) / 2 /*uniPosLast*/); //consider UTF encoding when cutting in the middle! (e.g. for macOS) + + const Zstring& shortGuid = printNumber(Zstr("%04x"), static_cast(getCrc16(generateGUID()))); + + const AbstractPath targetPathTmp = appendRelPath(*parentPath, tmpName + Zstr('-') + //don't use '~': some FTP servers *silently* replace it with '_'! + shortGuid + TEMP_FILE_ENDING); + //------------------------------------------------------------------------------------------- + + const FileCopyResult result = copyFilePlain(targetPathTmp); //throw FileError, ErrorFileLocked + + //transactional behavior: ensure cleanup; not needed before copyFilePlain() which is already transactional + ZEN_ON_SCOPE_FAIL( try { removeFilePlain(targetPathTmp); } + catch (const FileError& e) { logExtraError(e.toString()); }); + + //have target file deleted (after read access on source and target has been confirmed) => allow for almost transactional overwrite + if (onDeleteTargetFile) + onDeleteTargetFile(); //throw X + + //already existing: undefined behavior! (e.g. fail/overwrite) + moveAndRenameItem(targetPathTmp, targetPath); //throw FileError, (ErrorMoveUnsupported) + //perf: this call is REALLY expensive on unbuffered volumes! ~40% performance decrease on FAT USB stick! + + /* CAVEAT on FAT/FAT32: the sequence of deleting the target file and renaming "file.txt.ffs_tmp" to "file.txt" does + NOT PRESERVE the creation time of the .ffs_tmp file, but SILENTLY "reuses" whatever creation time the old "file.txt" had! + This "feature" is called "File System Tunneling": + https://devblogs.microsoft.com/oldnewthing/?p=34923 + https://support.microsoft.com/kb/172190/en-us */ + return result; + } + else + { + /* Note: non-transactional file copy solves at least four problems: + -> skydrive - doesn't allow for .ffs_tmp extension and returns ERROR_INVALID_PARAMETER + -> network renaming issues + -> allow for true delete before copy to handle low disk space problems + -> higher performance on unbuffered drives (e.g. USB-sticks) */ + if (onDeleteTargetFile) + onDeleteTargetFile(); + + return copyFilePlain(targetPath); //throw FileError, ErrorFileLocked + } +} + + +void AFS::createFolderIfMissingRecursion(const AbstractPath& folderPath) //throw FileError +{ + auto getItemType2 = [&](const AbstractPath& itemPath) //throw FileError + { + try + { return getItemType(itemPath); } //throw FileError + catch (const FileError& e) //need to add context! + { + throw FileError(replaceCpy(_("Cannot create directory %x."), L"%x", fmtPath(getDisplayPath(folderPath))), + replaceCpy(e.toString(), L"\n\n", L'\n')); + } + }; + + try + { + //- path most likely already exists (see: versioning, base folder, log file path) => check first + //- do NOT use getItemTypeIfExists()! race condition when multiple threads are calling createDirectoryIfMissingRecursion(): https://freefilesync.org/forum/viewtopic.php?t=10137#p38062 + //- find first existing + accessible parent folder (backwards iteration): + AbstractPath folderPathEx = folderPath; + RingBuffer folderNames; //caveat: 1. might have been created in the meantime 2. getItemType2() may have failed with access error + for (;;) + try + { + if (getItemType2(folderPathEx) == ItemType::file /*obscure, but possible*/) //throw FileError + throw SysError(replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(getItemName(folderPathEx)))); + break; + } + catch (FileError&) //not yet existing or access error + { + const std::optional parentPath = getParentPath(folderPathEx); + if (!parentPath)//device root => quick access test + throw; + folderNames.push_front(getItemName(folderPathEx)); + folderPathEx = *parentPath; + } + //----------------------------------------------------------- + + AbstractPath folderPathNew = folderPathEx; + for (const Zstring& folderName : folderNames) + try + { + folderPathNew = appendRelPath(folderPathNew, folderName); + + createFolderPlain(folderPathNew); //throw FileError + } + catch (FileError&) + { + try + { + if (getItemType2(folderPathNew) == ItemType::file /*obscure, but possible*/) //throw FileError + throw SysError(replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(getItemName(folderPathNew)))); + else + continue; //already existing => possible, if createDirectoryIfMissingRecursion() is run in parallel + } + catch (FileError&) {} //not yet existing or access error + + throw; + } + } + catch (const SysError& e) + { + throw FileError(replaceCpy(_("Cannot create directory %x."), L"%x", fmtPath(getDisplayPath(folderPath))), e.toString()); + } +} + + +//default implementation: folder traversal +void AFS::removeFolderIfExistsRecursion(const AfsPath& folderPath, //throw FileError + const std::function& onBeforeFileDeletion /*throw X*/, // + const std::function& onBeforeSymlinkDeletion /*throw X*/, //optional; one call for each object! + const std::function& onBeforeFolderDeletion /*throw X*/) const +{ + std::function removeFolderRecursionImpl; + removeFolderRecursionImpl = [this, &onBeforeFileDeletion, &onBeforeSymlinkDeletion, &onBeforeFolderDeletion, &removeFolderRecursionImpl](const AfsPath& folderPath2) //throw FileError + { + std::vector folderNames; + { + std::vector fileNames; + std::vector symlinkNames; + try + { + traverseFolder(folderPath2, //throw FileError + [&](const FileInfo& fi) { fileNames.push_back(fi.itemName); }, + [&](const FolderInfo& fi) { folderNames.push_back(fi.itemName); }, + [&](const SymlinkInfo& si) { symlinkNames.push_back(si.itemName); }); + } + catch (const FileError& e) //add context + { + throw FileError(replaceCpy(_("Cannot delete directory %x."), L"%x", fmtPath(getDisplayPath(folderPath2))), + replaceCpy(e.toString(), L"\n\n", L'\n')); + } + + for (const Zstring& fileName : fileNames) + { + const AfsPath filePath(appendPath(folderPath2.value, fileName)); + if (onBeforeFileDeletion) + onBeforeFileDeletion(getDisplayPath(filePath)); //throw X + + removeFilePlain(filePath); //throw FileError + } + + for (const Zstring& symlinkName : symlinkNames) + { + const AfsPath linkPath(appendPath(folderPath2.value, symlinkName)); + if (onBeforeSymlinkDeletion) + onBeforeSymlinkDeletion(getDisplayPath(linkPath)); //throw X + + removeSymlinkPlain(linkPath); //throw FileError + } + } //=> save stack space and allow deletion of extremely deep hierarchies! + + for (const Zstring& folderName : folderNames) + removeFolderRecursionImpl(AfsPath(appendPath(folderPath2.value, folderName))); //throw FileError + + if (onBeforeFolderDeletion) + onBeforeFolderDeletion(getDisplayPath(folderPath2)); //throw X + + removeFolderPlain(folderPath2); //throw FileError + }; + //-------------------------------------------------------------------------------------------------------------- + + const std::optional type = [&] + { + try + { + return getItemTypeIfExists(folderPath); //throw FileError + } + catch (const FileError& e) //add context + { + throw FileError(replaceCpy(_("Cannot delete directory %x."), L"%x", fmtPath(getDisplayPath(folderPath))), + replaceCpy(e.toString(), L"\n\n", L'\n')); + } + }(); + + if (type) + { + assert(*type != ItemType::symlink); + + if (*type == ItemType::symlink) + { + if (onBeforeSymlinkDeletion) + onBeforeSymlinkDeletion(getDisplayPath(folderPath)); //throw X + + removeSymlinkPlain(folderPath); //throw FileError + } + else + removeFolderRecursionImpl(folderPath); //throw FileError + } + else //no error situation if directory is not existing! manual deletion relies on it! significant I/O work was done => report: + if (onBeforeFolderDeletion) onBeforeFolderDeletion(getDisplayPath(folderPath)); //throw X +} + + +void AFS::removeFileIfExists(const AbstractPath& filePath) //throw FileError +{ + try + { + removeFilePlain(filePath); //throw FileError + } + catch (const FileError& e) + { + try + { + if (!itemExists(filePath)) //throw FileError + return; + } + //abstract context => unclear which exception is more relevant/useless: + catch (const FileError& e2) { throw FileError(replaceCpy(e.toString(), L"\n\n", L'\n'), replaceCpy(e2.toString(), L"\n\n", L'\n')); } + + throw; + } +} + + +void AFS::removeSymlinkIfExists(const AbstractPath& linkPath) //throw FileError +{ + try + { + removeSymlinkPlain(linkPath); //throw FileError + } + catch (const FileError& e) + { + try + { + if (!itemExists(linkPath)) //throw FileError + return; + } + //abstract context => unclear which exception is more relevant/useless: + catch (const FileError& e2) { throw FileError(replaceCpy(e.toString(), L"\n\n", L'\n'), replaceCpy(e2.toString(), L"\n\n", L'\n')); } + + throw; + } +} + + +void AFS::removeEmptyFolderIfExists(const AbstractPath& folderPath) //throw FileError +{ + try + { + removeFolderPlain(folderPath); //throw FileError + } + catch (const FileError& e) + { + try + { + if (!itemExists(folderPath)) //throw FileError + return; + } + //abstract context => unclear which exception is more relevant/useless: + catch (const FileError& e2) { throw FileError(replaceCpy(e.toString(), L"\n\n", L'\n'), replaceCpy(e2.toString(), L"\n\n", L'\n')); } + + throw; + } +} + + +void AFS::RecycleSession::moveToRecycleBinIfExists(const AbstractPath& itemPath, const Zstring& logicalRelPath) //throw FileError, RecycleBinUnavailable +{ + try + { + moveToRecycleBin(itemPath, logicalRelPath); //throw FileError, RecycleBinUnavailable + } + catch (RecycleBinUnavailable&) { throw; } //[!] no need for itemExists() file access! + catch (const FileError& e) + { + try + { + if (!itemExists(itemPath)) //throw FileError + return; + } + //abstract context => unclear which exception is more relevant/useless: + catch (const FileError& e2) { throw FileError(replaceCpy(e.toString(), L"\n\n", L'\n'), replaceCpy(e2.toString(), L"\n\n", L'\n')); } + + throw; + } +} + + +void AFS::moveToRecycleBinIfExists(const AbstractPath& itemPath) //throw FileError, RecycleBinUnavailable +{ + try + { + moveToRecycleBin(itemPath); //throw FileError, RecycleBinUnavailable + } + catch (RecycleBinUnavailable&) { throw; } //[!] no need for itemExists() file access! + catch (const FileError& e) + { + try + { + if (!itemExists(itemPath)) //throw FileError + return; + } + //abstract context => unclear which exception is more relevant/useless: + catch (const FileError& e2) { throw FileError(replaceCpy(e.toString(), L"\n\n", L'\n'), replaceCpy(e2.toString(), L"\n\n", L'\n')); } + + throw; + } +} diff --git a/FreeFileSync/Source/afs/abstract.h b/FreeFileSync/Source/afs/abstract.h new file mode 100644 index 0000000..94c14ea --- /dev/null +++ b/FreeFileSync/Source/afs/abstract.h @@ -0,0 +1,580 @@ +// ***************************************************************************** +// * 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 ABSTRACT_H_873450978453042524534234 +#define ABSTRACT_H_873450978453042524534234 + +#include +#include +#include +#include +#include //InputStream/OutputStream support buffered stream concept +#include //NOT a wxWidgets dependency! + + +namespace fff +{ +struct AfsPath; +AfsPath sanitizeDeviceRelativePath(Zstring relPath); + +struct AbstractFileSystem; + +//============================================================================================================== +using AfsDevice = zen::SharedRef; + +struct AfsPath //= path relative to the file system root folder (no leading/traling separator) +{ + AfsPath() {} + explicit AfsPath(const Zstring& p) : value(p) { assert(zen::isValidRelPath(value)); } + Zstring value; + + std::strong_ordering operator<=>(const AfsPath&) const = default; +}; + +struct AbstractPath //THREAD-SAFETY: like an int! +{ + AbstractPath(const AfsDevice& deviceIn, const AfsPath& pathIn) : afsDevice(deviceIn), afsPath(pathIn) {} + + //template -> don't use forwarding constructor: it circumvents AfsPath's explicit constructor! + //AbstractPath(T1&& deviceIn, T2&& pathIn) : afsDevice(std::forward(deviceIn)), afsPath(std::forward(pathIn)) {} + + AfsDevice afsDevice; //"const AbstractFileSystem" => all accesses expected to be thread-safe!!! + AfsPath afsPath; //relative to device root +}; +//============================================================================================================== + +struct AbstractFileSystem //THREAD-SAFETY: "const" member functions must model thread-safe access! +{ + //=============== convenience ================= + static Zstring getItemName(const AbstractPath& itemPath) { assert(getParentPath(itemPath)); return getItemName(itemPath.afsPath); } + static Zstring getItemName(const AfsPath& itemPath) { using namespace zen; return afterLast(itemPath.value, FILE_NAME_SEPARATOR, IfNotFoundReturn::all); } + + static bool isNullPath(const AbstractPath& itemPath) { return isNullDevice(itemPath.afsDevice) /*&& itemPath.afsPath.value.empty()*/; } + + static AbstractPath appendRelPath(const AbstractPath& itemPath, const Zstring& relPath); + + static std::optional getParentPath(const AbstractPath& itemPath); + static std::optional getParentPath(const AfsPath& itemPath); + //============================================= + + static std::weak_ordering compareDevice(const AbstractFileSystem& lhs, const AbstractFileSystem& rhs); + + static bool isNullDevice(const AfsDevice& afsDevice) { return afsDevice.ref().isNullFileSystem(); } + + static std::wstring getDisplayPath(const AbstractPath& itemPath) { return itemPath.afsDevice.ref().getDisplayPath(itemPath.afsPath); } + + static Zstring getInitPathPhrase(const AbstractPath& itemPath) { return itemPath.afsDevice.ref().getInitPathPhrase(itemPath.afsPath); } + + static std::vector getPathPhraseAliases(const AbstractPath& itemPath) { return itemPath.afsDevice.ref().getPathPhraseAliases(itemPath.afsPath); } + + //---------------------------------------------------------------------------------------------------------------- + using RequestPasswordFun = std::function; //throw X + static void authenticateAccess(const AfsDevice& afsDevice, const RequestPasswordFun& requestPassword /*throw X*/) //throw FileError, X + { return afsDevice.ref().authenticateAccess(requestPassword); } + + static bool supportPermissionCopy(const AbstractPath& sourcePath, const AbstractPath& targetPath); //throw FileError + + static bool hasNativeTransactionalCopy(const AbstractPath& itemPath) { return itemPath.afsDevice.ref().hasNativeTransactionalCopy(); } + //---------------------------------------------------------------------------------------------------------------- + + using FingerPrint = uint64_t; //AfsDevice-dependent persistent unique ID + + enum class ItemType : unsigned char + { + file, + folder, + symlink, + }; + //(hopefully) fast: does not distinguish between error/not existing + //root path? => do access test + static ItemType getItemType(const AbstractPath& itemPath) { return itemPath.afsDevice.ref().getItemType(itemPath.afsPath); } //throw FileError + + //assumes: - folder traversal access right (=> yes, because we can assume base path exist at this point; e.g. avoids problem when SFTP parent paths might deny access) + // - all child item path parts must correspond to folder traversal + // => conclude whether an item is *not* existing anymore by doing a *case-sensitive* name search => potentially SLOW! + // - root path? => do access test + static std::optional getItemTypeIfExists(const AbstractPath& itemPath) + { return itemPath.afsDevice.ref().getItemTypeIfExists(itemPath.afsPath); } //throw FileError + + static bool itemExists(const AbstractPath& itemPath) { return static_cast(getItemTypeIfExists(itemPath)); } //throw FileError + //---------------------------------------------------------------------------------------------------------------- + + //already existing: fail + //does NOT create parent directories recursively if not existing + static void createFolderPlain(const AbstractPath& folderPath) { folderPath.afsDevice.ref().createFolderPlain(folderPath.afsPath); } //throw FileError + + //creates directories recursively if not existing + //returns false if folder already exists + static void createFolderIfMissingRecursion(const AbstractPath& folderPath); //throw FileError + + static void removeFolderIfExistsRecursion(const AbstractPath& folderPath, //throw FileError + const std::function& onBeforeFileDeletion /*throw X*/, // + const std::function& onBeforeSymlinkDeletion /*throw X*/, //optional; one call for each object! + const std::function& onBeforeFolderDeletion /*throw X*/) // + { return folderPath.afsDevice.ref().removeFolderIfExistsRecursion(folderPath.afsPath, onBeforeFileDeletion, onBeforeSymlinkDeletion, onBeforeFolderDeletion); } + + static void removeFileIfExists (const AbstractPath& filePath); // + static void removeSymlinkIfExists (const AbstractPath& linkPath); //throw FileError + static void removeEmptyFolderIfExists(const AbstractPath& folderPath); // + + static void removeFilePlain (const AbstractPath& filePath ) { filePath .afsDevice.ref().removeFilePlain (filePath .afsPath); } // + static void removeSymlinkPlain(const AbstractPath& linkPath ) { linkPath .afsDevice.ref().removeSymlinkPlain(linkPath .afsPath); } //throw FileError + static void removeFolderPlain (const AbstractPath& folderPath) { folderPath.afsDevice.ref().removeFolderPlain (folderPath.afsPath); } // + //---------------------------------------------------------------------------------------------------------------- + //static void setModTime(const AbstractPath& itemPath, time_t modTime) { itemPath.afsDevice.ref().setModTime(itemPath.afsPath, modTime); } //throw FileError, follows symlinks + + static AbstractPath getSymlinkResolvedPath(const AbstractPath& linkPath) { return linkPath.afsDevice.ref().getSymlinkResolvedPath(linkPath.afsPath); } //throw FileError + static bool equalSymlinkContent(const AbstractPath& linkPathL, const AbstractPath& linkPathR); //throw FileError + //---------------------------------------------------------------------------------------------------------------- + static zen::FileIconHolder getFileIcon (const AbstractPath& filePath, int pixelSize) { return filePath.afsDevice.ref().getFileIcon (filePath.afsPath, pixelSize); } //throw FileError; optional return value + static zen::ImageHolder getThumbnailImage(const AbstractPath& filePath, int pixelSize) { return filePath.afsDevice.ref().getThumbnailImage(filePath.afsPath, pixelSize); } //throw FileError; optional return value + //---------------------------------------------------------------------------------------------------------------- + + struct StreamAttributes + { + time_t modTime; //number of seconds since Jan. 1st 1970 GMT + uint64_t fileSize; + FingerPrint filePrint; //optional + }; + + //---------------------------------------------------------------------------------------------------------------- + struct InputStream + { + virtual ~InputStream() {} + virtual size_t getBlockSize() = 0; //throw FileError; non-zero block size is AFS contract! + virtual size_t tryRead(void* buffer, size_t bytesToRead, const zen::IoCallback& notifyUnbufferedIO /*throw X*/) = 0; //throw FileError, ErrorFileLocked, X + //may return short; only 0 means EOF! CONTRACT: bytesToRead > 0! + + //only returns attributes if they are already buffered within stream handle and determination would be otherwise expensive (e.g. FTP/SFTP): + virtual std::optional tryGetAttributesFast() = 0; //throw FileError + }; + //return value always bound: + static std::unique_ptr getInputStream(const AbstractPath& filePath) { return filePath.afsDevice.ref().getInputStream(filePath.afsPath); } //throw FileError, ErrorFileLocked + + //---------------------------------------------------------------------------------------------------------------- + + struct FinalizeResult + { + FingerPrint filePrint = 0; //optional + std::optional errorModTime; + }; + + struct OutputStreamImpl + { + virtual ~OutputStreamImpl() {} + virtual size_t getBlockSize() = 0; //throw FileError; non-zero block size is AFS contract + virtual size_t tryWrite(const void* buffer, size_t bytesToWrite, const zen::IoCallback& notifyUnbufferedIO /*throw X*/) = 0; //throw FileError, X; may return short! CONTRACT: bytesToWrite > 0 + virtual FinalizeResult finalize(const zen::IoCallback& notifyUnbufferedIO /*throw X*/) = 0; //throw FileError, X + }; + + struct OutputStream + { + OutputStream(std::unique_ptr&& outStream, const AbstractPath& filePath, std::optional streamSize); + ~OutputStream(); + size_t getBlockSize() { return outStream_->getBlockSize(); } //throw FileError + size_t tryWrite(const void* buffer, size_t bytesToWrite, const zen::IoCallback& notifyUnbufferedIO /*throw X*/); //throw FileError, X may return short! + FinalizeResult finalize(const zen::IoCallback& notifyUnbufferedIO /*throw X*/); //throw FileError, X + //call finalize when done!() when done, or (incomplete) file will be automatically deleted + + private: + std::unique_ptr outStream_; //bound! + const AbstractPath filePath_; + const std::optional bytesExpected_; + uint64_t bytesWrittenTotal_ = 0; + }; + //already existing: undefined behavior! (e.g. fail/overwrite/auto-rename) + static std::unique_ptr getOutputStream(const AbstractPath& filePath, //throw FileError + std::optional streamSize, + std::optional modTime) + { return std::make_unique(filePath.afsDevice.ref().getOutputStream(filePath.afsPath, streamSize, modTime), filePath, streamSize); } + //---------------------------------------------------------------------------------------------------------------- + + struct SymlinkInfo + { + Zstring itemName; + time_t modTime; + }; + + struct FileInfo + { + Zstring itemName; + uint64_t fileSize; //unit: bytes! + time_t modTime; //number of seconds since Jan. 1st 1970 GMT + FingerPrint filePrint; //optional; persistent + unique (relative to device) or 0! + bool isFollowedSymlink; + }; + + struct FolderInfo + { + Zstring itemName; + bool isFollowedSymlink; + }; + + struct TraverserCallback + { + virtual ~TraverserCallback() {} + + enum class HandleLink + { + follow, //follows link, then calls "onFolder()" or "onFile()" + skip + }; + + enum class HandleError + { + retry, + ignore + }; + + virtual void onFile (const FileInfo& fi) = 0; // + virtual HandleLink onSymlink(const SymlinkInfo& si) = 0; //throw X + virtual std::shared_ptr onFolder (const FolderInfo& fi) = 0; // + //nullptr: ignore directory, non-nullptr: traverse into, using the (new) callback + + struct ErrorInfo + { + std::wstring msg; + std::chrono::steady_clock::time_point failTime; + size_t retryNumber = 0; + }; + + virtual HandleError reportDirError (const ErrorInfo& errorInfo) = 0; //failed directory traversal -> consider directory data at current level as incomplete! + virtual HandleError reportItemError(const ErrorInfo& errorInfo, const Zstring& itemName) = 0; //failed to get data for single file/dir/symlink only! + }; + + using TraverserWorkload = std::vector /*throw X*/>>; + + //- client needs to handle duplicate file reports! (FilePlusTraverser fallback, retrying to read directory contents, ...) + static void traverseFolderRecursive(const AfsDevice& afsDevice, const TraverserWorkload& workload /*throw X*/, size_t parallelOps) { afsDevice.ref().traverseFolderRecursive(workload, parallelOps); } + + static void traverseFolder(const AbstractPath& folderPath, //throw FileError + const std::function& onFile, // + const std::function& onFolder, //optional + const std::function& onSymlink) // + { folderPath.afsDevice.ref().traverseFolder(folderPath.afsPath, onFile, onFolder, onSymlink); } + //---------------------------------------------------------------------------------------------------------------- + + //already existing: undefined behavior! (e.g. fail/overwrite) + static void moveAndRenameItem(const AbstractPath& pathFrom, const AbstractPath& pathTo); //throw FileError, ErrorMoveUnsupported + + static std::wstring generateMoveErrorMsg(const AbstractPath& pathFrom, const AbstractPath& pathTo) { return pathFrom.afsDevice.ref().generateMoveErrorMsg(pathFrom.afsPath, pathTo); } + + + //Note: it MAY happen that copyFileTransactional() leaves temp files behind, e.g. temporary network drop. + // => clean them up at an appropriate time (automatically set sync directions to delete them). They have the following ending: + static inline constexpr ZstringView TEMP_FILE_ENDING = Zstr(".ffs_tmp"); //don't use Zstring as global constant: avoid static initialization order problem in global namespace! + // caveat: ending is hard-coded by RealTimeSync + + struct FileCopyResult + { + uint64_t fileSize = 0; + time_t modTime = 0; //number of seconds since Jan. 1st 1970 GMT + FingerPrint sourceFilePrint = 0; //optional + FingerPrint targetFilePrint = 0; // + std::optional errorModTime; //failure to set modification time + }; + + //symlink handling: follow + //already existing + no onDeleteTargetFile: undefined behavior! (e.g. fail/overwrite/auto-rename) + //returns current attributes at the time of copy + static FileCopyResult copyFileTransactional(const AbstractPath& sourcePath, const StreamAttributes& attrSource, //throw FileError, ErrorFileLocked, X + const AbstractPath& targetPath, + bool copyFilePermissions, + bool transactionalCopy, + //if target is existing user *must* implement deletion to avoid undefined behavior + //if transactionalCopy == true, full read access on source had been proven at this point, so it's safe to delete it. + const std::function& onDeleteTargetFile /*throw X*/, + //accummulated delta != file size! consider ADS, sparse, compressed files + const zen::IoCallback& notifyUnbufferedIO /*throw X*/); + //already existing: fail + //symlink handling: follow + static void copyNewFolder(const AbstractPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions); //throw FileError + + //already existing: fail + static void copySymlink(const AbstractPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions); //throw FileError + + //---------------------------------------------------------------------------------------------------------------- + + //- returns < 0 if not available + //- folderPath does not need to exist (yet) + static int64_t getFreeDiskSpace(const AbstractPath& folderPath) { return folderPath.afsDevice.ref().getFreeDiskSpace(folderPath.afsPath); } //throw FileError + + struct RecycleSession + { + virtual ~RecycleSession() {} + + //- multi-threaded access: internally synchronized! + void moveToRecycleBinIfExists(const AbstractPath& itemPath, const Zstring& logicalRelPath); //throw FileError, RecycleBinUnavailable + + //- fails if item is not existing: don't leave user wonder why it isn't in the recycle bin! + //- multi-threaded access: internally synchronized! + virtual void moveToRecycleBin(const AbstractPath& itemPath, const Zstring& logicalRelPath) = 0; //throw FileError, RecycleBinUnavailable + + virtual void tryCleanup(const std::function& notifyDeletionStatus /*throw X*; displayPath may be empty*/) = 0; //throw FileError, X + }; + + //- return value always bound! + //- constructor will be running on main thread => *no* file I/O! + static std::unique_ptr createRecyclerSession(const AbstractPath& folderPath) { return folderPath.afsDevice.ref().createRecyclerSession(folderPath.afsPath); } //throw FileError, RecycleBinUnavailable + + //- returns empty on success, item type if recycle bin is not available + static void moveToRecycleBinIfExists(const AbstractPath& itemPath); //throw FileError, RecycleBinUnavailable + + //fails if item is not existing + static void moveToRecycleBin(const AbstractPath& itemPath) { itemPath.afsDevice.ref().moveToRecycleBin(itemPath.afsPath); }; //throw FileError, RecycleBinUnavailable + + //================================================================================================================ + + //no need to protect access: + virtual ~AbstractFileSystem() {} + + +protected: + //default implementation: folder traversal + virtual void removeFolderIfExistsRecursion(const AfsPath& folderPath, //throw FileError + const std::function& onBeforeFileDeletion, + const std::function& onBeforeSymlinkDeletion, + const std::function& onBeforeFolderDeletion) const = 0; + + void traverseFolder(const AfsPath& folderPath, //throw FileError + const std::function& onFile, // + const std::function& onFolder, //optional + const std::function& onSymlink) const; // + + //already existing: undefined behavior! (e.g. fail/overwrite/auto-rename) + FileCopyResult copyFileAsStream(const AfsPath& sourcePath, const StreamAttributes& attrSource, //throw FileError, ErrorFileLocked, X + const AbstractPath& targetPath, const zen::IoCallback& notifyUnbufferedIO /*throw X*/) const; + + + std::wstring generateMoveErrorMsg(const AfsPath& pathFrom, const AbstractPath& pathTo) const + { + using namespace zen; + + if (getParentPath(pathFrom) == getParentPath(pathTo.afsPath)) //pure "rename" + return replaceCpy(replaceCpy(_("Cannot rename %x to %y."), + L"%x", fmtPath(getDisplayPath(pathFrom))), + L"%y", fmtPath(getItemName(pathTo))); + else //"move" or "move + rename" + return trimCpy(replaceCpy(replaceCpy(_("Cannot move %x to %y."), + L"%x", L'\n' + fmtPath(getDisplayPath(pathFrom))), + L"%y", L'\n' + fmtPath(getDisplayPath(pathTo)))); + } + +private: + virtual std::optional getNativeItemPath(const AfsPath& itemPath) const { return {}; }; + + virtual Zstring getInitPathPhrase(const AfsPath& itemPath) const = 0; + + virtual std::vector getPathPhraseAliases(const AfsPath& itemPath) const = 0; + + virtual std::wstring getDisplayPath(const AfsPath& itemPath) const = 0; + + virtual bool isNullFileSystem() const = 0; + + virtual std::weak_ordering compareDeviceSameAfsType(const AbstractFileSystem& afsRhs) const = 0; + + //---------------------------------------------------------------------------------------------------------------- + virtual ItemType getItemType(const AfsPath& itemPath) const = 0; //throw FileError + + virtual std::optional getItemTypeIfExists(const AfsPath& itemPath) const = 0; //throw FileError + + //already existing: fail + virtual void createFolderPlain(const AfsPath& folderPath) const = 0; //throw FileError + + //non-recursive folder deletion: + virtual void removeFilePlain (const AfsPath& filePath ) const = 0; //throw FileError + virtual void removeSymlinkPlain(const AfsPath& linkPath ) const = 0; //throw FileError + virtual void removeFolderPlain (const AfsPath& folderPath) const = 0; //throw FileError + + //---------------------------------------------------------------------------------------------------------------- + //virtual void setModTime(const AfsPath& itemPath, time_t modTime) const = 0; //throw FileError, follows symlinks + + virtual AbstractPath getSymlinkResolvedPath(const AfsPath& linkPath) const = 0; //throw FileError + virtual bool equalSymlinkContentForSameAfsType(const AfsPath& linkPathL, const AbstractPath& linkPathR) const = 0; //throw FileError + + //---------------------------------------------------------------------------------------------------------------- + virtual std::unique_ptr getInputStream(const AfsPath& filePath) const = 0; //throw FileError, ErrorFileLocked + + //already existing: undefined behavior! (e.g. fail/overwrite/auto-rename) + virtual std::unique_ptr getOutputStream(const AfsPath& filePath, //throw FileError + std::optional streamSize, + std::optional modTime) const = 0; + //---------------------------------------------------------------------------------------------------------------- + virtual void traverseFolderRecursive(const TraverserWorkload& workload /*throw X*/, size_t parallelOps) const = 0; + //---------------------------------------------------------------------------------------------------------------- + virtual bool supportsPermissions(const AfsPath& folderPath) const = 0; //throw FileError + + //already existing: undefined behavior! (e.g. fail/overwrite) + virtual void moveAndRenameItemForSameAfsType(const AfsPath& pathFrom, const AbstractPath& pathTo) const = 0; //throw FileError, ErrorMoveUnsupported + + //symlink handling: follow + //already existing: undefined behavior! (e.g. fail/overwrite/auto-rename) + virtual FileCopyResult copyFileForSameAfsType(const AfsPath& sourcePath, const StreamAttributes& attrSource, //throw FileError, ErrorFileLocked, X + const AbstractPath& targetPath, bool copyFilePermissions, + //accummulated delta != file size! consider ADS, sparse, compressed files + const zen::IoCallback& notifyUnbufferedIO /*throw X*/) const = 0; + + + //symlink handling: follow + //already existing: fail + virtual void copyNewFolderForSameAfsType(const AfsPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions) const = 0; //throw FileError + + //already existing: fail + virtual void copySymlinkForSameAfsType(const AfsPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions) const = 0; //throw FileError + + //---------------------------------------------------------------------------------------------------------------- + virtual zen::FileIconHolder getFileIcon (const AfsPath& filePath, int pixelSize) const = 0; //throw FileError; optional return value + virtual zen::ImageHolder getThumbnailImage(const AfsPath& filePath, int pixelSize) const = 0; //throw FileError; optional return value + + virtual void authenticateAccess(const RequestPasswordFun& requestPassword /*throw X*/) const = 0; //throw FileError, X + + virtual bool hasNativeTransactionalCopy() const = 0; + //---------------------------------------------------------------------------------------------------------------- + + virtual int64_t getFreeDiskSpace(const AfsPath& folderPath) const = 0; //throw FileError, returns < 0 if not available + virtual std::unique_ptr createRecyclerSession(const AfsPath& folderPath) const = 0; //throw FileError, RecycleBinUnavailable + virtual void moveToRecycleBin(const AfsPath& itemPath) const = 0; //throw FileError, RecycleBinUnavailable +}; + + +inline std::weak_ordering operator<=>(const AfsDevice& lhs, const AfsDevice& rhs) { return AbstractFileSystem::compareDevice(lhs.ref(), rhs.ref()); } +inline bool operator== (const AfsDevice& lhs, const AfsDevice& rhs) { return (lhs <=> rhs) == std::weak_ordering::equivalent; } + +inline +std::weak_ordering operator<=>(const AbstractPath& lhs, const AbstractPath& rhs) +{ + return std::tie(lhs.afsDevice, lhs.afsPath) <=> + std::tie(rhs.afsDevice, rhs.afsPath); +} + +inline +bool operator==(const AbstractPath& lhs, const AbstractPath& rhs) { return lhs.afsPath == rhs.afsPath && lhs.afsDevice == rhs.afsDevice; } + + + + + + + + +//------------------------------------ implementation ----------------------------------------- +inline +AbstractPath AbstractFileSystem::appendRelPath(const AbstractPath& itemPath, const Zstring& relPath) +{ + return AbstractPath(itemPath.afsDevice, AfsPath(appendPath(itemPath.afsPath.value, relPath))); +} + +//--------------------------------------------------------------------------------------------- + +inline +AbstractFileSystem::OutputStream::OutputStream(std::unique_ptr&& outStream, const AbstractPath& filePath, std::optional streamSize) : + outStream_(std::move(outStream)), + filePath_(filePath), + bytesExpected_(streamSize) {} + + +inline +AbstractFileSystem::OutputStream::~OutputStream() +{ +} + + +inline +size_t AbstractFileSystem::OutputStream::tryWrite(const void* buffer, size_t bytesToWrite, const zen::IoCallback& notifyUnbufferedIO /*throw X*/) //throw FileError, X +{ + const size_t bytesWritten = outStream_->tryWrite(buffer, bytesToWrite, notifyUnbufferedIO /*throw X*/); //throw FileError, X may return short! + bytesWrittenTotal_ += bytesWritten; + return bytesWritten; +} + + +inline +AbstractFileSystem::FinalizeResult AbstractFileSystem::OutputStream::finalize(const zen::IoCallback& notifyUnbufferedIO /*throw X*/) //throw FileError, X +{ + using namespace zen; + + //important check: catches corrupt SFTP download with libssh2! + if (bytesExpected_ && *bytesExpected_ != bytesWrittenTotal_) + throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(getDisplayPath(filePath_))), //instead we should report the source file, but don't have it here... + _("Unexpected size of data stream:") + L' ' + formatNumber(bytesWrittenTotal_) + L'\n' + + _("Expected:") + L' ' + formatNumber(*bytesExpected_)); + + const FinalizeResult result = outStream_->finalize(notifyUnbufferedIO); //throw FileError, X + return result; +} + +//-------------------------------------------------------------------------- + +inline +bool AbstractFileSystem::supportPermissionCopy(const AbstractPath& sourcePath, const AbstractPath& targetPath) //throw FileError +{ + if (typeid(sourcePath.afsDevice.ref()) != typeid(targetPath.afsDevice.ref())) + return false; + + return sourcePath.afsDevice.ref().supportsPermissions(sourcePath.afsPath) && //throw FileError + targetPath.afsDevice.ref().supportsPermissions(targetPath.afsPath); +} + + +inline +bool AbstractFileSystem::equalSymlinkContent(const AbstractPath& linkPathL, const AbstractPath& linkPathR) //throw FileError +{ + if (typeid(linkPathL.afsDevice.ref()) != typeid(linkPathR.afsDevice.ref())) + return false; + + return linkPathL.afsDevice.ref().equalSymlinkContentForSameAfsType(linkPathL.afsPath, linkPathR); //throw FileError +} + + +inline +void AbstractFileSystem::moveAndRenameItem(const AbstractPath& pathFrom, const AbstractPath& pathTo) //throw FileError, ErrorMoveUnsupported +{ + using namespace zen; + + if (typeid(pathFrom.afsDevice.ref()) != typeid(pathTo.afsDevice.ref())) + throw ErrorMoveUnsupported(generateMoveErrorMsg(pathFrom, pathTo), _("Operation not supported between different devices.")); + + //already existing: undefined behavior! (e.g. fail/overwrite) + pathFrom.afsDevice.ref().moveAndRenameItemForSameAfsType(pathFrom.afsPath, pathTo); //throw FileError, ErrorMoveUnsupported +} + + +inline +void AbstractFileSystem::copyNewFolder(const AbstractPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions) //throw FileError +{ + using namespace zen; + + if (typeid(sourcePath.afsDevice.ref()) != typeid(targetPath.afsDevice.ref())) //fall back: + { + //already existing: fail + createFolderPlain(targetPath); //throw FileError + + if (copyFilePermissions) + throw FileError(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(getDisplayPath(targetPath))), + _("Operation not supported between different devices.")); + } + else + sourcePath.afsDevice.ref().copyNewFolderForSameAfsType(sourcePath.afsPath, targetPath, copyFilePermissions); //throw FileError +} + + +//already existing: fail +inline +void AbstractFileSystem::copySymlink(const AbstractPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions) //throw FileError +{ + using namespace zen; + + if (typeid(sourcePath.afsDevice.ref()) != typeid(targetPath.afsDevice.ref())) + throw FileError(replaceCpy(replaceCpy(_("Cannot copy symbolic link %x to %y."), + L"%x", L'\n' + fmtPath(getDisplayPath(sourcePath))), + L"%y", L'\n' + fmtPath(getDisplayPath(targetPath))), _("Operation not supported between different devices.")); + + //already existing: fail + sourcePath.afsDevice.ref().copySymlinkForSameAfsType(sourcePath.afsPath, targetPath, copyFilePermissions); //throw FileError +} +} + +#endif //ABSTRACT_H_873450978453042524534234 diff --git a/FreeFileSync/Source/afs/abstract_impl.h b/FreeFileSync/Source/afs/abstract_impl.h new file mode 100644 index 0000000..ad34f79 --- /dev/null +++ b/FreeFileSync/Source/afs/abstract_impl.h @@ -0,0 +1,154 @@ +// ***************************************************************************** +// * 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 IMPL_HELPER_H_873450978453042524534234 +#define IMPL_HELPER_H_873450978453042524534234 + +#include "abstract.h" +#include +#include + + +namespace fff +{ +template inline //return ignored error message if available +std::wstring tryReportingDirError(Function cmd /*throw FileError, X*/, AbstractFileSystem::TraverserCallback& cb /*throw X*/) +{ + for (size_t retryNumber = 0;; ++retryNumber) + try + { + cmd(); //throw FileError, X + return std::wstring(); + } + catch (const zen::FileError& e) + { + assert(!e.toString().empty()); + switch (cb.reportDirError({e.toString(), std::chrono::steady_clock::now(), retryNumber})) //throw X + { + case AbstractFileSystem::TraverserCallback::HandleError::ignore: + return e.toString(); + case AbstractFileSystem::TraverserCallback::HandleError::retry: + break; //continue with loop + } + } +} + +template inline +bool tryReportingItemError(Command cmd, AbstractFileSystem::TraverserCallback& callback, const Zstring& itemName) //throw X, return "true" on success, "false" if error was ignored +{ + for (size_t retryNumber = 0;; ++retryNumber) + try + { + cmd(); //throw FileError + return true; + } + catch (const zen::FileError& e) + { + switch (callback.reportItemError({e.toString(), std::chrono::steady_clock::now(), retryNumber}, itemName)) //throw X + { + case AbstractFileSystem::TraverserCallback::HandleError::retry: + break; + case AbstractFileSystem::TraverserCallback::HandleError::ignore: + return false; + } + } +} + +//========================================================================================== + +//Google Drive/MTP happily create duplicate files/folders with the same names, without failing +//=> however, FFS's "check if already exists after failure" idiom *requires* failure +//=> best effort: serialize access (at path level) so that GdriveFileState existence check and file/folder creation act as a single operation +template +class PathAccessLocker +{ + struct BlockInfo + { + std::mutex m; + bool itemInUse = false; //protected by mutex! + /* can we get rid of BlockType::fail and save "bool itemInUse" "somewhere else"? + Google Drive => put dummy entry in GdriveFileState? problem: there is no fail-free removal: accessGlobalFileState() can throw! + MTP => no (buffered) state */ + }; +public: + PathAccessLocker() {} + + //how to handle *other* access attempts while holding the lock: + enum class BlockType + { + otherWait, + otherFail + }; + + class Lock + { + public: + Lock(const NativePath& nativePath, BlockType blockType) : blockType_(blockType) //throw SysError + { + using namespace zen; + + if (const std::shared_ptr pal = getGlobalInstance()) + pal->protPathLocks_.access([&](std::map>& pathLocks) + { + //clean up obsolete entries + std::erase_if(pathLocks, [](const auto& v) { return v.second.expired(); }); + + //get or create: + std::weak_ptr& weakPtr = pathLocks[nativePath]; + blockInfo_ = weakPtr.lock(); + if (!blockInfo_) + weakPtr = blockInfo_ = std::make_shared(); + }); + else + throw SysError(L"PathAccessLocker::Lock() function call not allowed during init/shutdown."); + + blockInfo_->m.lock(); + + if (blockInfo_->itemInUse) + { + blockInfo_->m.unlock(); + throw SysError(replaceCpy(_("The item %x is currently in use."), L"%x", fmtPath(getItemName(nativePath)))); + } + + if (blockType == BlockType::otherFail) + { + blockInfo_->itemInUse = true; + blockInfo_->m.unlock(); + } + } + + ~Lock() + { + if (blockType_ == BlockType::otherFail) + { + blockInfo_->m.lock(); + blockInfo_->itemInUse = false; + } + + blockInfo_->m.unlock(); + } + + private: + Lock (const Lock&) = delete; + Lock& operator=(const Lock&) = delete; + + const BlockType blockType_; //[!] needed: we can't instead check "itemInUse" (without locking first) + std::shared_ptr blockInfo_; + }; + +private: + PathAccessLocker (const PathAccessLocker&) = delete; + PathAccessLocker& operator=(const PathAccessLocker&) = delete; + + static std::shared_ptr getGlobalInstance(); + static Zstring getItemName(const NativePath& nativePath); + + zen::Protected>> protPathLocks_; +}; + +} + +#endif //IMPL_HELPER_H_873450978453042524534234 diff --git a/FreeFileSync/Source/afs/concrete.cpp b/FreeFileSync/Source/afs/concrete.cpp new file mode 100644 index 0000000..b700183 --- /dev/null +++ b/FreeFileSync/Source/afs/concrete.cpp @@ -0,0 +1,59 @@ +// ***************************************************************************** +// * 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 "concrete.h" +#include "native.h" +#include "ftp.h" +#include "sftp.h" +#include "gdrive.h" + +using namespace fff; +using namespace zen; + + +void fff::initAfs(const AfsConfig& cfg) +{ + ftpInit(); + sftpInit(); + gdriveInit(appendPath(cfg.configDirPath, Zstr("GoogleDrive")), + appendPath(cfg.resourceDirPath, Zstr("cacert.pem"))); +} + + +void fff::teardownAfs() +{ + gdriveTeardown(); + sftpTeardown(); + ftpTeardown(); +} + + +AbstractPath fff::getNullPath() +{ + return createItemPathNativeNoFormatting(Zstring()); +} + + +AbstractPath fff::createAbstractPath(const Zstring& itemPathPhrase) //noexcept +{ + //greedy: try native evaluation first + if (acceptsItemPathPhraseNative(itemPathPhrase)) //noexcept + return createItemPathNative(itemPathPhrase); //noexcept + + //then the rest: + if (acceptsItemPathPhraseFtp(itemPathPhrase)) //noexcept + return createItemPathFtp(itemPathPhrase); //noexcept + + if (acceptsItemPathPhraseSftp(itemPathPhrase)) //noexcept + return createItemPathSftp(itemPathPhrase); //noexcept + + if (acceptsItemPathPhraseGdrive(itemPathPhrase)) //noexcept + return createItemPathGdrive(itemPathPhrase); //noexcept + + + //no idea? => native! + return createItemPathNative(itemPathPhrase); +} diff --git a/FreeFileSync/Source/afs/concrete.h b/FreeFileSync/Source/afs/concrete.h new file mode 100644 index 0000000..81e2910 --- /dev/null +++ b/FreeFileSync/Source/afs/concrete.h @@ -0,0 +1,26 @@ +// ***************************************************************************** +// * 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 FS_CONCRETE_348787329573243 +#define FS_CONCRETE_348787329573243 + +#include "abstract.h" + +namespace fff +{ +struct AfsConfig +{ + Zstring resourceDirPath; //directory to read AFS-specific files + Zstring configDirPath; //directory to store AFS-specific files +}; +void initAfs(const AfsConfig& cfg); +void teardownAfs(); + +AbstractPath getNullPath(); +AbstractPath createAbstractPath(const Zstring& itemPathPhrase); //noexcept +} + +#endif //FS_CONCRETE_348787329573243 diff --git a/FreeFileSync/Source/afs/ftp.cpp b/FreeFileSync/Source/afs/ftp.cpp new file mode 100644 index 0000000..c4f9f78 --- /dev/null +++ b/FreeFileSync/Source/afs/ftp.cpp @@ -0,0 +1,2757 @@ +// ***************************************************************************** +// * 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 "ftp.h" +#include +#include +#include +#include +#include //DON'T include directly! +#include "init_curl_libssh2.h" +#include "ftp_common.h" +#include "abstract_impl.h" + //#include + #include + +using namespace zen; +using namespace fff; +using AFS = AbstractFileSystem; + + +namespace +{ +//Extensions to FTP: https://tools.ietf.org/html/rfc3659 +//FTP commands: https://en.wikipedia.org/wiki/List_of_FTP_commands + +constexpr std::chrono::seconds FTP_SESSION_MAX_IDLE_TIME (20); +constexpr std::chrono::seconds FTP_SESSION_CLEANUP_INTERVAL(4); + +const size_t FTP_BLOCK_SIZE_DOWNLOAD = 64 * 1024; //libcurl returns blocks of only 16 kB as returned by recv() even if we request larger blocks via CURLOPT_BUFFERSIZE +const size_t FTP_BLOCK_SIZE_UPLOAD = 64 * 1024; //libcurl requests blocks of 64 kB. larger blocksizes set via CURLOPT_UPLOAD_BUFFERSIZE do not seem to make a difference +const size_t FTP_STREAM_BUFFER_SIZE = 1024 * 1024; //unit: [byte] +//stream buffer should be big enough to facilitate prefetching during alternating read/write operations => e.g. see serialize.h::unbufferedStreamCopy() + +constexpr ZstringView ftpPrefix = Zstr("ftp:"); + + +enum class ServerEncoding +{ + unknown, + utf8, + ansi, +}; + + +inline +uint16_t getEffectivePort(int portOption) +{ + if (portOption > 0) + return static_cast(portOption); + return DEFAULT_PORT_FTP; +} + + +struct FtpDeviceId //= what defines a unique FTP location +{ + FtpDeviceId(const FtpLogin& login) : + server(login.server), + port(getEffectivePort(login.portCfg)), + username(login.username) {} + + Zstring server; + uint16_t port; //must be valid port! + Zstring username; +}; +std::weak_ordering operator<=>(const FtpDeviceId& lhs, const FtpDeviceId& rhs) +{ + //exactly the type of case insensitive comparison we need for server names! https://docs.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfow#IDNs + if (const std::weak_ordering cmp = compareAsciiNoCase(lhs.server, rhs.server); + cmp != std::weak_ordering::equivalent) + return cmp; + + return std::tie(lhs.port, lhs.username) <=> //username: case sensitive! + std::tie(rhs.port, rhs.username); +} +//also needed by compareDeviceSameAfsType(), so can't just replace with hash and use std::unordered_map + + +struct FtpSessionCfg //= config for buffered FTP session +{ + FtpDeviceId deviceId; + Zstring password; + bool useTls = false; +}; +bool operator==(const FtpSessionCfg& lhs, const FtpSessionCfg& rhs) +{ + if (lhs.deviceId <=> rhs.deviceId != std::weak_ordering::equivalent) + return false; + + return std::tie(lhs.password, lhs.useTls) == //password: case sensitive! + std::tie(rhs.password, rhs.useTls); +} + + +Zstring concatenateFtpFolderPathPhrase(const FtpLogin& login, const AfsPath& itemPath); //noexcept + + +Zstring ansiToUtfEncoding(const std::string_view& str) //throw SysError +{ + if (str.empty()) return {}; + + gsize bytesWritten = 0; //not including the terminating null + + GError* error = nullptr; + ZEN_ON_SCOPE_EXIT(if (error) ::g_error_free(error)); + + //https://developer.gnome.org/glib/stable/glib-Character-Set-Conversion.html#g-convert + gchar* utfStr = ::g_convert(str.data(), //const gchar* str + str.size(), //gssize len + "UTF-8", //const gchar* to_codeset + "LATIN1", //const gchar* from_codeset + nullptr, //gsize* bytes_read + &bytesWritten, //gsize* bytes_written + &error); //GError** error + if (!utfStr) + throw SysError(formatGlibError("g_convert(" + std::string(str) + ", LATIN1 -> UTF-8)", error)); + ZEN_ON_SCOPE_EXIT(::g_free(utfStr)); + + return {utfStr, bytesWritten}; + + +} + + +std::string utfToAnsiEncoding(const Zstring& str) //throw SysError +{ + if (str.empty()) return {}; + + const Zstring& strNorm = getUnicodeNormalForm(str); //convert to pre-composed *before* attempting conversion + + gsize bytesWritten = 0; //not including the terminating null + + GError* error = nullptr; + ZEN_ON_SCOPE_EXIT(if (error) ::g_error_free(error)); + + //fails for: 1. broken UTF-8 2. not-ANSI-encodable Unicode + gchar* ansiStr = ::g_convert(strNorm.c_str(), //const gchar* str + strNorm.size(), //gssize len + "LATIN1", //const gchar* to_codeset + "UTF-8", //const gchar* from_codeset + nullptr, //gsize* bytes_read + &bytesWritten, //gsize* bytes_written + &error); //GError** error + if (!ansiStr) + throw SysError(formatGlibError("g_convert(" + utfTo(strNorm) + ", UTF-8 -> LATIN1)", error)); + ZEN_ON_SCOPE_EXIT(::g_free(ansiStr)); + + return {ansiStr, bytesWritten}; + +} + + +std::wstring getCurlDisplayPath(const FtpDeviceId& deviceId, const AfsPath& itemPath) +{ + Zstring displayPath = Zstring(ftpPrefix) + Zstr("//"); + + if (!deviceId.username.empty()) //show username! consider AFS::compareDeviceSameAfsType() + displayPath += deviceId.username + Zstr('@'); + + //if (parseIpv6Address(deviceId.server) && deviceId.port != DEFAULT_PORT_FTP) + // displayPath += Zstr('[') + deviceId.server + Zstr(']'); + //else + displayPath += deviceId.server; + + //if (deviceId.port != DEFAULT_PORT_FTP) + // displayPath += Zstr(':') + numberTo(deviceId.port); + + const Zstring& relPath = getServerRelPath(itemPath); + if (relPath != Zstr("/")) + displayPath += relPath; + + return utfTo(displayPath); +} + + +std::vector splitFtpResponse(std::string&&) = delete; + +std::vector splitFtpResponse(const std::string& buf) +{ + std::vector lines; + + split2(buf, [](const char c) { return isLineBreak(c) || c == '\0'; }, //is 0-char check even needed? + [&lines](const std::string_view block) + { + if (!block.empty()) //consider Windows' + lines.push_back(block); + }); + + return lines; +} + + +class FtpLineParser +{ +public: + explicit FtpLineParser(const std::string_view& line) : it_(line.begin()), itEnd_(line.end()) {} + /**/ FtpLineParser(std::string_view&&) = delete; + + template + std::string_view readRange(size_t count, Function acceptChar) //throw SysError + { + if (static_cast(count) > itEnd_ - it_) + throw SysError(L"Unexpected end of line."); + + const auto rngEnd = it_ + count; + + if (!std::all_of(it_, rngEnd, acceptChar)) + throw SysError(L"Expected char type not found."); + + return makeStringView(std::exchange(it_, rngEnd), rngEnd); + } + + template //expects non-empty range! + std::string_view readRange(Function acceptChar) //throw SysError + { + auto rngEnd = std::find_if_not(it_, itEnd_, acceptChar); + if (rngEnd == it_) + throw SysError(L"Expected char range not found."); + + return makeStringView(std::exchange(it_, rngEnd), rngEnd); + } + + char peekNextChar() const { return it_ == itEnd_ ? '\0' : *it_; } + +private: + /**/ + std::string_view::const_iterator it_; + const std::string_view::const_iterator itEnd_; +}; + +//---------------------------------------------------------------------------------------------------------------- + +std::wstring formatFtpStatus(int sc) +{ + const wchar_t* statusText = [&] //https://en.wikipedia.org/wiki/List_of_FTP_server_return_codes + { + switch (sc) + { + case 400: return L"The command was not accepted but the error condition is temporary."; + case 421: return L"Service not available, closing control connection."; + case 425: return L"Cannot open data connection."; + case 426: return L"Connection closed; transfer aborted."; + case 430: return L"Invalid username or password."; + case 431: return L"Need some unavailable resource to process security."; + case 434: return L"Requested host unavailable."; + case 450: return L"Requested file action not taken."; + case 451: return L"Local error in processing."; + case 452: return L"Insufficient storage space in system. File unavailable, e.g. file busy."; + + case 500: return L"Syntax error, command unrecognized or command line too long."; + case 501: return L"Syntax error in parameters or arguments."; + case 502: return L"Command not implemented."; + case 503: return L"Bad sequence of commands."; + case 504: return L"Command not implemented for that parameter."; + case 521: return L"Data connection cannot be opened with this PROT setting."; + case 522: return L"Server does not support the requested network protocol."; + case 530: return L"User not logged in."; + case 532: return L"Need account for storing files."; + case 533: return L"Command protection level denied for policy reasons."; + case 534: return L"Could not connect to server; issue regarding SSL."; + case 535: return L"Failed security check."; + case 536: return L"Requested PROT level not supported by mechanism."; + case 537: return L"Command protection level not supported by security mechanism."; + case 550: return L"File unavailable, e.g. file not found, no access."; + case 551: return L"Requested action aborted. Page type unknown."; + case 552: return L"Requested file action aborted. Exceeded storage allocation."; + case 553: return L"File name not allowed."; + + default: return L""; + } + }(); + + if (strLength(statusText) == 0) + return trimCpy(replaceCpy(L"FTP status %x.", L"%x", numberTo(sc))); + else + return trimCpy(replaceCpy(L"FTP status %x: ", L"%x", numberTo(sc)) + statusText); +} + +//================================================================================================================ +//================================================================================================================ + +struct SysErrorFtpProtocol : public zen::SysError +{ + SysErrorFtpProtocol(const std::wstring& msg, long ftpError) : SysError(msg), ftpErrorCode(ftpError) {} + + long ftpErrorCode; +}; + +DEFINE_NEW_SYS_ERROR(SysErrorPassword) + + +constinit Global globalFtpSessionCount; +GLOBAL_RUN_ONCE(globalFtpSessionCount.set(createUniSessionCounter())); + + +class FtpSession +{ +public: + explicit FtpSession(const FtpSessionCfg& sessionCfg) : //throw SysError + sessionCfg_(sessionCfg) + { + lastSuccessfulUseTime_ = std::chrono::steady_clock::now(); + } + + ~FtpSession() + { + if (easyHandle_) + ::curl_easy_cleanup(easyHandle_); + } + + const FtpSessionCfg& getSessionCfg() const { return sessionCfg_; } + + //set *before* calling any of the subsequent functions; see FtpSessionManager::access() + void setContextTimeout(const std::weak_ptr& timeoutSec) { timeoutSec_ = timeoutSec; } + + //returns server response (header data) + std::string perform(const AfsPath& itemPath, bool isDir, long pathMethod, + const std::vector& extraOptions, bool requestUtf8) //throw SysError, SysErrorPassword, SysErrorFtpProtocol + { + if (requestUtf8) //avoid endless recursion + initUtf8(); //throw SysError, SysErrorFtpProtocol + + if (!easyHandle_) + { + easyHandle_ = ::curl_easy_init(); + if (!easyHandle_) + throw SysError(formatSystemError("curl_easy_init", formatCurlStatusCode(CURLE_OUT_OF_MEMORY), L"")); + } + else + ::curl_easy_reset(easyHandle_); + + auto setCurlOption = [easyHandle = easyHandle_](const CurlOption& curlOpt) //throw SysError + { + if (const CURLcode rc = ::curl_easy_setopt(easyHandle, curlOpt.option, curlOpt.value); + rc != CURLE_OK) + throw SysError(formatSystemError("curl_easy_setopt(" + numberTo(static_cast(curlOpt.option)) + ")", + formatCurlStatusCode(rc), utfTo(::curl_easy_strerror(rc)))); + }; + + char curlErrorBuf[CURL_ERROR_SIZE] = {}; + setCurlOption({CURLOPT_ERRORBUFFER, curlErrorBuf}); //throw SysError + + std::string headerData; + curl_write_callback onHeaderReceived = [](/*const*/ char* buffer, size_t size, size_t nitems, void* callbackData) + { + auto& output = *static_cast(callbackData); + output.append(buffer, size * nitems); + return size * nitems; + }; + setCurlOption({CURLOPT_HEADERDATA, &headerData}); //throw SysError + setCurlOption({CURLOPT_HEADERFUNCTION, onHeaderReceived}); //throw SysError + + setCurlOption({CURLOPT_URL, getCurlUrlPath(itemPath, isDir).c_str()}); //throw SysError + + assert(pathMethod != CURLFTPMETHOD_MULTICWD); //too slow! + setCurlOption({CURLOPT_FTP_FILEMETHOD, pathMethod}); //throw SysError + + if (!sessionCfg_.deviceId.username.empty()) //else: libcurl will default to CURL_DEFAULT_USER("anonymous") and CURL_DEFAULT_PASSWORD("ftp@example.com") + { + //ANSI or UTF encoding? + // "modern" FTP servers (implementing RFC 2640) have UTF8 enabled by default => pray and hope for the best. + // What about ANSI-FTP servers and "Microsoft FTP Service" which requires "OPTS UTF8 ON"? => *psh* + // CURLOPT_PREQUOTE to the rescue? Nope, issued long after USER/PASS + setCurlOption({CURLOPT_USERNAME, utfTo(sessionCfg_.deviceId.username).c_str()}); //throw SysError + setCurlOption({CURLOPT_PASSWORD, utfTo(sessionCfg_.password ).c_str()}); //throw SysError + //curious: libcurl will *not* default to CURL_DEFAULT_USER when setting password but no username + } + + setCurlOption({CURLOPT_PORT, sessionCfg_.deviceId.port}); //throw SysError + + //thread-safety: https://curl.haxx.se/libcurl/c/threadsafe.html + setCurlOption({CURLOPT_NOSIGNAL, 1}); //throw SysError + + //allow PASV IP: some FTP servers really use IP different from control connection + setCurlOption({CURLOPT_FTP_SKIP_PASV_IP, 0}); //throw SysError + //let's not hold our breath until Curl adds a reasonable PASV handling => patch libcurl accordingly! + //https://github.com/curl/curl/issues/1455 + //https://github.com/curl/curl/pull/1470 + //support broken servers like this one: https://freefilesync.org/forum/viewtopic.php?t=4301 + + + const std::shared_ptr timeoutSec = timeoutSec_.lock(); + assert(timeoutSec); + if (!timeoutSec) + throw std::runtime_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] FtpSession: Timeout duration was not set."); + + setCurlOption({CURLOPT_CONNECTTIMEOUT, *timeoutSec}); //throw SysError + + //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." + setCurlOption({CURLOPT_LOW_SPEED_TIME, *timeoutSec}); //throw SysError + setCurlOption({CURLOPT_LOW_SPEED_LIMIT, 1 /*[bytes]*/}); //throw SysError + //can't use "0" which means "inactive", so use some low number + + setCurlOption({CURLOPT_SERVER_RESPONSE_TIMEOUT, *timeoutSec}); //throw SysError + //FTP only; unlike CURLOPT_TIMEOUT, this one is NOT a limit on the total transfer time + + //CURLOPT_ACCEPTTIMEOUT_MS? => only relevant for "active" FTP connections + + //long-running file uploads require keep-alives for the TCP control connection: https://freefilesync.org/forum/viewtopic.php?t=6928 + setCurlOption({CURLOPT_TCP_KEEPALIVE, 1}); //throw SysError + //=> CURLOPT_TCP_KEEPIDLE (=delay until sending first keepalive probe) and + // CURLOPT_TCP_KEEPINTVL (interval between probes) both default to 60 sec, + // CURLOPT_TCP_KEEPCNT (number of probes with *no server response* before dropping connection) defaults to 9 + + + std::optional socketException; + //libcurl does *not* set FD_CLOEXEC for us! https://github.com/curl/curl/issues/2252 + auto onSocketCreate = [&](curl_socket_t curlfd, curlsocktype purpose) + { + assert(::fcntl(curlfd, F_GETFD) == 0); + if (::fcntl(curlfd, F_SETFD, FD_CLOEXEC) == -1) //=> RACE-condition if other thread calls fork/execv before this thread sets FD_CLOEXEC! + { + socketException = SysError(formatSystemError("fcntl(FD_CLOEXEC)", errno)); + return CURL_SOCKOPT_ERROR; + } + return CURL_SOCKOPT_OK; + }; + + using SocketCbType = decltype(onSocketCreate); + using SocketCbWrapperType = int (*)(SocketCbType* clientp, curl_socket_t curlfd, curlsocktype purpose); //needed for cdecl function pointer cast + SocketCbWrapperType onSocketCreateWrapper = [](SocketCbType* clientp, curl_socket_t curlfd, curlsocktype purpose) + { + return (*clientp)(curlfd, purpose); //free this poor little C-API from its shackles and redirect to a proper lambda + }; + + setCurlOption({CURLOPT_SOCKOPTFUNCTION, onSocketCreateWrapper}); //throw SysError + setCurlOption({CURLOPT_SOCKOPTDATA, &onSocketCreate}); //throw SysError + + //Use share interface? https://curl.haxx.se/libcurl/c/libcurl-share.html + //perf test, 4 and 8 parallel threads: + // CURL_LOCK_DATA_DNS => no measurable total time difference + // CURL_LOCK_DATA_SSL_SESSION => freefilesync.org; not working at all: lots of CURLE_RECV_ERROR (seems nobody ever tested this with truly parallel FTP accesses!) +#if 0 + do not include this into release! + static CURLSH* curlShare = [] + { + struct ShareLocks + { + std::mutex lockIntenal; + std::mutex lockDns; + std::mutex lockSsl; + }; + static ShareLocks globalLocksTestingOnly; + + using LockFunType = void (*)(CURL* handle, curl_lock_data data, curl_lock_access access, void* userptr); //needed for cdecl function pointer cast + LockFunType lockFun = [](CURL* handle, curl_lock_data data, curl_lock_access access, void* userptr) + { + auto& locks = *static_cast(userptr); + switch (data) + { + case CURL_LOCK_DATA_SHARE: + return locks.lockIntenal.lock(); + case CURL_LOCK_DATA_DNS: + return locks.lockDns.lock(); + case CURL_LOCK_DATA_SSL_SESSION: + return locks.lockSsl.lock(); + } + assert(false); + }; + using UnlockFunType = void (*)(CURL *handle, curl_lock_data data, void* userptr); + UnlockFunType unlockFun = [](CURL *handle, curl_lock_data data, void* userptr) + { + auto& locks = *static_cast(userptr); + switch (data) + { + case CURL_LOCK_DATA_SHARE: + return locks.lockIntenal.unlock(); + case CURL_LOCK_DATA_DNS: + return locks.lockDns.unlock(); + case CURL_LOCK_DATA_SSL_SESSION: + return locks.lockSsl.unlock(); + } + assert(false); + }; + + CURLSH* cs = ::curl_share_init(); + assert(cs); + CURLSHcode rc = CURLSHE_OK; + rc = ::curl_share_setopt(cs, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS); + assert(rc == CURLSHE_OK); + rc = ::curl_share_setopt(cs, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION); //buggy!? + assert(rc == CURLSHE_OK); + rc = ::curl_share_setopt(cs, CURLSHOPT_LOCKFUNC, lockFun); + assert(rc == CURLSHE_OK); + rc = ::curl_share_setopt(cs, CURLSHOPT_UNLOCKFUNC, unlockFun); + assert(rc == CURLSHE_OK); + rc = ::curl_share_setopt(cs, CURLSHOPT_USERDATA, &globalLocksTestingOnly); + assert(rc == CURLSHE_OK); + return cs; + }(); + //CURLSHcode ::curl_share_cleanup(curlShare); + setCurlOption({CURLOPT_SHARE, curlShare}); //throw SysError +#endif + + //TODO: FTP option to require certificate checking? +#if 0 + setCurlOption({CURLOPT_CAINFO, "cacert.pem"}); //throw SysError + //hopefully latest version from https://curl.haxx.se/docs/caextract.html + //libcurl forwards this char-string to OpenSSL as is, which (thank god) accepts UTF8 +#else + setCurlOption({CURLOPT_CAINFO, 0}); //throw SysError + //be explicit: "even when [CURLOPT_SSL_VERIFYPEER] is disabled [...] curl may still load the certificate file specified in CURLOPT_CAINFO." + + //check if server certificate can be trusted? (Default: 1L) + // => may fail with: "CURLE_PEER_FAILED_VERIFICATION: SSL certificate problem: certificate has expired" + setCurlOption({CURLOPT_SSL_VERIFYPEER, 0}); //throw SysError + //check that server name matches the name in the certificate? (Default: 2L) + // => may fail with: "CURLE_PEER_FAILED_VERIFICATION: SSL: no alternative certificate subject name matches target host name 'freefilesync.org'" + setCurlOption({CURLOPT_SSL_VERIFYHOST, 0}); //throw SysError +#endif + if (sessionCfg_.useTls) //https://tools.ietf.org/html/rfc4217 + { + //require SSL for both control and data: + setCurlOption({CURLOPT_USE_SSL, CURLUSESSL_ALL}); //throw SysError + //try TLS first, then SSL (currently: CURLFTPAUTH_DEFAULT == CURLFTPAUTH_SSL): + setCurlOption({CURLOPT_FTPSSLAUTH, CURLFTPAUTH_TLS}); //throw SysError + } + + //support older FTP servers with less than 2048 bit TLS keys ("CURLE_SSL_CONNECT_ERROR: TLS connect error: error:0A00018A:SSL routines::dh key too small") + //=> OpenSSL defaults to security level 2 (unless OPENSSL_TLS_SECURITY_LEVEL=level is defined during compilation) https://docs.openssl.org/master/man3/SSL_CTX_set_security_level/ + setCurlOption({CURLOPT_SSL_CIPHER_LIST, "DEFAULT:@SECLEVEL=1"}); //throw SysError + + for (const CurlOption& option : extraOptions) + setCurlOption(option); //throw SysError + + //======================================================================================================= + const CURLcode rcPerf = ::curl_easy_perform(easyHandle_); + //WTF: curl_easy_perform() considers FTP response codes >= 400 as failure, but for HTTP response codes 4XX are considered success!! CONSISTENCY, people!!! + //note: CURLOPT_FAILONERROR(default:off) is only available for HTTP => BUT at least we can prefix FTP commands with * for same effect: https://curl.se/libcurl/c/CURLOPT_QUOTE.html + + if (socketException) + throw* socketException; //throw SysError + //======================================================================================================= + + if (rcPerf != CURLE_OK) + { + std::wstring errorMsg = trimCpy(utfTo(curlErrorBuf)); //optional + + if (const std::vector& headerLines = splitFtpResponse(headerData); + !headerLines.empty()) + if (const std::string_view& response = trimCpy(headerLines.back()); //that *should* be the server's error response + !response.empty()) + errorMsg += (errorMsg.empty() ? L"" : L"\n") + utfTo(response); +#if 0 + //utfTo(::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(nativeErrorCode); +#endif + if (rcPerf == CURLE_LOGIN_DENIED) + throw SysErrorPassword(formatSystemError("curl_easy_perform", formatCurlStatusCode(rcPerf), errorMsg)); + + long ftpStatusCode = 0; //optional + /*const CURLcode rc =*/ ::curl_easy_getinfo(easyHandle_, CURLINFO_RESPONSE_CODE, &ftpStatusCode); + //https://en.wikipedia.org/wiki/List_of_FTP_server_return_codes + assert(rcPerf == CURLE_OPERATION_TIMEDOUT || rcPerf == CURLE_ABORTED_BY_CALLBACK || ftpStatusCode == 0 || 400 <= ftpStatusCode && ftpStatusCode < 600); + if (ftpStatusCode != 0) + throw SysErrorFtpProtocol(formatSystemError("curl_easy_perform", formatCurlStatusCode(rcPerf), errorMsg), ftpStatusCode); + + throw SysError(formatSystemError("curl_easy_perform", formatCurlStatusCode(rcPerf), errorMsg)); + } + + lastSuccessfulUseTime_ = std::chrono::steady_clock::now(); + return headerData; + } + + //returns server response (header data) + std::string runSingleFtpCommand(const std::string& ftpCmd, bool requestUtf8) //throw SysError, SysErrorFtpProtocol + { + curl_slist* quote = nullptr; + ZEN_ON_SCOPE_EXIT(::curl_slist_free_all(quote)); + quote = ::curl_slist_append(quote, ftpCmd.c_str()); + + return perform(AfsPath(), true /*isDir*/, CURLFTPMETHOD_NOCWD /*avoid needless CWDs*/, + { + {CURLOPT_NOBODY, 1L}, + {CURLOPT_QUOTE, quote}, + }, requestUtf8); //throw SysError, SysErrorPassword, SysErrorFtpProtocol + } + + void testConnection() //throw SysError + { + /* https://en.wikipedia.org/wiki/List_of_FTP_commands + FEAT: are there servers that don't support this command? fuck, yes: "550 FEAT: Operation not permitted" => buggy server not granting access, despite support! + PWD? will fail if last access deleted the working dir! + "TYPE I"? might interfere with libcurls internal handling, but that's an improvement, right? right? :> + => but "HELP", and "NOOP" work, right?? + Fuck my life: even "HELP" is not always implemented: https://freefilesync.org/forum/viewtopic.php?t=6002 + => are there servers supporting neither FEAT nor HELP? only time will tell... + ... and it tells! FUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU https://freefilesync.org/forum/viewtopic.php?t=8041 */ + + //=> '*' to the rescue: as long as we get an FTP response - *any* FTP response (including 550) - the connection itself is fine! + const std::string& featBuf = runSingleFtpCommand("*FEAT", false /*requestUtf8*/); //throw SysError, SysErrorFtpProtocol + + for (const std::string_view& line : splitFtpResponse(featBuf)) + if (startsWith(line, "211 ") || + startsWith(line, "500 ") || + startsWith(line, "550 ")) + return; + + //ever get here? + throw SysError(L"Unexpected FTP response. (" + utfTo(featBuf) + L')'); + } + + AfsPath getHomePath() //throw SysError + { + if (!homePathCached_) + homePathCached_ = [&] + { + if (easyHandle_) + { + const char* homePathCurl = nullptr; //not owned + /*CURLcode rc =*/ ::curl_easy_getinfo(easyHandle_, CURLINFO_FTP_ENTRY_PATH, &homePathCurl); + + if (homePathCurl && isAsciiString(homePathCurl)) + return sanitizeDeviceRelativePath(utfTo(homePathCurl)); + + //home path with non-ASCII chars: libcurl issues PWD right after login *before* server was set up for UTF8 + //=> CURLINFO_FTP_ENTRY_PATH could be in any encoding => useless! + // Test case: Windows 10 IIS FTP with non-Ascii entry path + //=> start new FTP session and parse PWD *after* UTF8 is enabled: + ::curl_easy_cleanup(easyHandle_); + easyHandle_ = nullptr; + } + + const std::string& pwdBuf = runSingleFtpCommand("PWD", true /*requestUtf8*/); //throw SysError, SysErrorFtpProtocol + + for (const std::string_view& line : splitFtpResponse(pwdBuf)) + if (startsWith(line, "257 ")) + { + /* 257[rubbish]"" according to libcurl + + "The directory name can contain any character; embedded double-quotes should be escaped by + double-quotes (the "quote-doubling" convention)." https://tools.ietf.org/html/rfc959 */ + auto itBegin = std::find(line.begin(), line.end(), '"'); + if (itBegin != line.end()) + for (auto it = ++itBegin; it != line.end(); ++it) + if (*it == '"') + { + if (it + 1 != line.end() && it[1] == '"') + ++it; //skip double quote + else + { + const std::string homePathRaw = replaceCpy(std::string{itBegin, it}, "\"\"", '"'); + const Zstring homePathUtf = serverToUtfEncoding(homePathRaw); //throw SysError + return sanitizeDeviceRelativePath(homePathUtf); + } + } + break; + } + throw SysError(L"Unexpected FTP response. (" + utfTo(pwdBuf) + L')'); + }(); + return *homePathCached_; + } + + void ensureBinaryMode() //throw SysError + { + if (std::optional currentSocket = getActiveSocket()) //throw SysError + if (*currentSocket == binaryEnabledSocket_) + return; + + runSingleFtpCommand("TYPE I", false /*requestUtf8*/); //throw SysError, SysErrorFtpProtocol + + //make sure our binary-enabled session is still there (== libcurl behaves as we expect) + std::optional currentSocket = getActiveSocket(); //throw SysError + if (currentSocket) + binaryEnabledSocket_ = *currentSocket; //remember what we did + //libcurl already buffers "conn->proto.ftpc.transfertype" but selfishly keeps it for itself! + //=> pray libcurl doesn't internally set "TYPE A"! + //=> this seems to be the only place where it does: https://github.com/curl/curl/issues/4342 + else + throw SysError(L"Curl failed to cache FTP session."); //why is libcurl not caching the session??? + } + + //------------------------------------------------------------------------------------------------------------ + bool supportsMlsd() { return getFeatureSupport(&Features::mlsd); } // + bool supportsMfmt() { return getFeatureSupport(&Features::mfmt); } //throw SysError + bool supportsClnt() { return getFeatureSupport(&Features::clnt); } // + bool supportsUtf8() + { + if (getFeatureSupport(&Features::utf8)) + return true; + + initUtf8(); //vsFTPd (ftp.sunet.se): supports UTF8 via "OPTS UTF8 ON", even if "UTF8" is missing from "FEAT" + return socketUsesUtf8_; + } + + bool isHealthy() const + { + return std::chrono::steady_clock::now() - lastSuccessfulUseTime_ <= FTP_SESSION_MAX_IDLE_TIME; + } + + std::string getServerPathInternal(const AfsPath& itemPath) //throw SysError + { + const Zstring serverPath = getServerRelPath(itemPath); + + if (itemPath.value.empty()) //endless recursion caveat!! utfToServerEncoding() transitively depends on getServerPathInternal() + return utfTo(serverPath); + + return utfToServerEncoding(serverPath); //throw SysError + } + + Zstring serverToUtfEncoding(const std::string_view& str) //throw SysError + { + if (isAsciiString(str)) //fast path + return {str.begin(), str.end()}; + + switch (encoding_) //throw SysError + { + case ServerEncoding::unknown: + /* "UTF-8 encodings [2] contain enough internal structure that it is always, in practice, possible to determine whether a UTF-8 or raw encoding has been used" + - https://www.rfc-editor.org/rfc/rfc3659#section-2.2 + "encoding rules make it very unlikely that a character sequence from a different character set will be mistaken for a UTF-8 encoded character sequence." + - https://www.rfc-editor.org/rfc/rfc2640#section-2.2 + + => auto-detect encoding even if FEAT does not advertize UTF8: https://freefilesync.org/forum/viewtopic.php?t=9564 */ + encoding_ = supportsUtf8() || isValidUtf(str) ? ServerEncoding::utf8 : ServerEncoding::ansi; + return serverToUtfEncoding(str); //throw SysError + + case ServerEncoding::utf8: + if (!isValidUtf(str)) + throw SysError(_("Invalid character encoding:") + L' ' + utfTo(str) + L' ' + _("Expected:") + L" [UTF-8]"); + + return utfTo(str); + + case ServerEncoding::ansi: + return ansiToUtfEncoding(str); //throw SysError + } + assert(false); + return {}; + } + + std::string utfToServerEncoding(const Zstring& str) //throw SysError + { + if (isAsciiString(str)) //fast path + return {str.begin(), str.end()}; + switch (encoding_) //throw SysError + { + case ServerEncoding::unknown: + if (!supportsUtf8()) + throw SysError(_("Failed to auto-detect character encoding:") + L' ' + utfTo(str)); //might be ANSI or UTF8 with non-compliant server... + + encoding_ = ServerEncoding::utf8; + return utfToServerEncoding(str); //throw SysError + + case ServerEncoding::utf8: + //validate! we consider REPLACEMENT_CHAR as indication for server using ANSI encoding in serverToUtfEncoding() + if (!isValidUtf(str)) + throw SysError(_("Invalid character encoding:") + L' ' + utfTo(str) + L' ' + _("Expected:") + (sizeof(str[0]) == 1 ? L" [UTF-8]" : L" [UTF-16]")); + static_assert(sizeof(str[0]) == 1 || sizeof(str[0]) == 2); + + return utfTo(str); + + case ServerEncoding::ansi: + return utfToAnsiEncoding(str); //throw SysError + } + assert(false); + return {}; + } + +private: + FtpSession (const FtpSession&) = delete; + FtpSession& operator=(const FtpSession&) = delete; + + std::string getCurlUrlPath(const AfsPath& itemPath /*optional*/, bool isDir) //throw SysError + { + std::string curlRelPath; //libcurl expects encoded paths (except for '/' char!!!) => bug: https://github.com/curl/curl/pull/4423 + + split(getServerPathInternal(itemPath), //throw SysError + '/', [&](std::string_view comp) + { + if (!comp.empty()) + { + char* compFmt = ::curl_easy_escape(easyHandle_, comp.data(), static_cast(comp.size())); + if (!compFmt) + throw SysError(formatSystemError(std::string("curl_easy_escape(") + comp + ')', L"", L"Conversion failure")); + ZEN_ON_SCOPE_EXIT(::curl_free(compFmt)); + + if (!curlRelPath.empty()) + curlRelPath += '/'; + curlRelPath += compFmt; + } + }); + + if (trimCpy(sessionCfg_.deviceId.server).empty()) + throw SysError(_("Server name must not be empty.")); + + static_assert(LIBCURL_VERSION_MAJOR > 7 || (LIBCURL_VERSION_MAJOR == 7 && LIBCURL_VERSION_MINOR >= 67)); + /* 1. CURLFTPMETHOD_NOCWD requires absolute paths to unconditionally skip CWDs: https://github.com/curl/curl/pull/4382 + 2. CURLFTPMETHOD_SINGLECWD requires absolute paths to skip one needless "CWD entry path": https://github.com/curl/curl/pull/4332 + => https://curl.se/docs/faq.html#How_do_I_list_the_root_directory + => use // because /%2f had bugs (but they should be fixed: https://github.com/curl/curl/pull/4348) */ + std::string path = utfTo(Zstring(ftpPrefix) + Zstr("//") + sessionCfg_.deviceId.server) + "//" + curlRelPath; + + if (isDir && !endsWith(path, '/')) //curl-FTP needs directory paths to end with a slash + path += '/'; + return path; + } + + void initUtf8() //throw SysError, SysErrorFtpProtocol + { + /* 1. Some RFC-2640-non-compliant servers require UTF8 to be explicitly enabled: https://wiki.filezilla-project.org/Character_Encoding#Conflicting_specification + - e.g. Microsoft FTP Service: https://freefilesync.org/forum/viewtopic.php?t=4303 + + 2. Others do not advertize "UTF8" in "FEAT", but *still* allow enabling it via "OPTS UTF8 ON": + - https://freefilesync.org/forum/viewtopic.php?t=9564 + - vsFTPd: ftp.sunet.se https://security.appspot.com/vsftpd.html#download + + "OPTS UTF8 ON" needs to be activated each time libcurl internally creates a new session + hopyfully libcurl will offer a better solution: https://github.com/curl/curl/issues/1457 */ + + if (std::optional currentSocket = getActiveSocket()) //throw SysError + if (*currentSocket == utf8RequestedSocket_) //caveat: a non-UTF8-enabled session might already exist, e.g. from a previous call to supportsMlsd() + return; + + //some (broken!?) servers require "CLNT" before accepting "OPTS UTF8 ON": https://social.msdn.microsoft.com/Forums/en-US/d602574f-8a69-4d69-b337-52b6081902cf/problem-with-ftpwebrequestopts-utf8-on-501-please-clnt-first + if (supportsClnt()) //throw SysError + runSingleFtpCommand("CLNT FreeFileSync", false /*requestUtf8*/); //throw SysError, SysErrorFtpProtocol + + //"prefix the command with an asterisk to make libcurl continue even if the command fails" + //-> ignore if server does not know this legacy command (but report all *other* issues; else getActiveSocket() below won't have a socket and we've hidden the real error!) + const std::string& optsBuf = runSingleFtpCommand("*OPTS UTF8 ON", false /*requestUtf8*/); //throw SysError, (SysErrorFtpProtocol) + + //get *last* FTP status code (can there be more than one!?) + int ftpStatusCode = 0; + for (const std::string_view& line : splitFtpResponse(optsBuf)) + if (line.size() >= 4 && + isDigit(line[0]) && + isDigit(line[1]) && + isDigit(line[2]) && + line[3] == ' ') + ftpStatusCode = stringTo(line); + + socketUsesUtf8_ = ftpStatusCode == 200 || //"200 Always in UTF8 mode." "200 UTF8 set to on" + ftpStatusCode == 202; //"202 UTF8 mode is always enabled." + + //make sure our Unicode-enabled session is still there (== libcurl behaves as we expect) + std::optional currentSocket = getActiveSocket(); //throw SysError + if (currentSocket) + utf8RequestedSocket_ = *currentSocket; //remember what we did + else + throw SysError(L"Curl failed to cache FTP session."); //why is libcurl not caching the session??? + + } + + std::optional getActiveSocket() //throw SysError + { + if (easyHandle_) + { + curl_socket_t currentSocket = 0; + const CURLcode rc = ::curl_easy_getinfo(easyHandle_, CURLINFO_ACTIVESOCKET, ¤tSocket); + if (rc != CURLE_OK) + throw SysError(formatSystemError("curl_easy_getinfo(CURLINFO_ACTIVESOCKET)", formatCurlStatusCode(rc), utfTo(::curl_easy_strerror(rc)))); + if (currentSocket != CURL_SOCKET_BAD) + return currentSocket; + } + return {}; + } + + struct Features + { + bool mlsd = false; + bool mfmt = false; + bool clnt = false; + bool utf8 = false; + }; + using FeatureList = std::unordered_map; + + bool getFeatureSupport(bool Features::* status) //throw SysError + { + if (!featureCache_) + { + static constinit FunStatGlobal> globalServerFeatures; + globalServerFeatures.setOnce([] { return std::make_unique>(); }); + + const auto sf = globalServerFeatures.get(); + if (!sf) + throw SysError(formatSystemError("FtpSession::getFeatureSupport", L"", L"Function call not allowed during application shutdown.")); + + sf->access([&](const FeatureList& featList) + { + auto it = featList.find(sessionCfg_.deviceId.server); + if (it != featList.end()) + featureCache_ = it->second; + }); + + if (!featureCache_) + { + //*: ignore error if server does not support/allow FEAT + featureCache_ = parseFeatResponse(runSingleFtpCommand("*FEAT", false /*requestUtf8*/)); //throw SysError, (SysErrorFtpProtocol) + //used by initUtf8()! => requestUtf8 = false!!! + + sf->access([&](FeatureList& feat) { feat.emplace(sessionCfg_.deviceId.server, *featureCache_); }); + } + } + return (*featureCache_).*status; + } + + static Features parseFeatResponse(const std::string& featResponse) + { + Features output; //FEAT command: https://tools.ietf.org/html/rfc2389#page-4 + std::vector lines = splitFtpResponse(featResponse); + + auto it = std::find_if(lines.begin(), lines.end(), [](const std::string_view& line) { return startsWith(line, "211-") || startsWith(line, "211 "); }); + if (it != lines.end()) + { + ++it; + for (; it != lines.end(); ++it) + { + if (equalAsciiNoCase (*it, "211 End") || //Serv-U: "211 End (for details use "HELP commmand" where command is the command of interest)" + startsWithAsciiNoCase(*it, "211 End ")) //Home Ftp Server: "211 End of extentions." + break; + + std::string line(*it); + //suppport ProFTPD with "MultilineRFC2228 = on" https://freefilesync.org/forum/viewtopic.php?t=7243 + if (startsWith(line, "211-")) + line = ' ' + afterFirst(line, '-', IfNotFoundReturn::none); + + //https://tools.ietf.org/html/rfc3659#section-7.8 + //"a server-FTP process that supports MLST, and MLSD [...] MUST indicate that this support exists" + //"there is no distinct FEAT output for MLSD. The presence of the MLST feature indicates that both MLST and MLSD are supported" + if (equalAsciiNoCase (line, " MLST") || + startsWithAsciiNoCase(line, " MLST ") || //SP "MLST" [SP factlist] CRLF + //so much the theory. In practice FTP server implementers can't read (specs): https://freefilesync.org/forum/viewtopic.php?t=6752 + equalAsciiNoCase(line, " MLSD")) + output.mlsd = true; + + //https://tools.ietf.org/html/draft-somers-ftp-mfxx-04#section-3.3 + //"Where a server-FTP process supports the MFMT command [...] it MUST include the response to the FEAT command" + else if (equalAsciiNoCase(line, " MFMT")) //SP "MFMT" CRLF + output.mfmt = true; + + else if (equalAsciiNoCase(line, " UTF8") || + equalAsciiNoCase(line, " UTF8 ON") || //support non-compliant servers: https://freefilesync.org/forum/viewtopic.php?t=7355#p24694 + equalAsciiNoCase(line, " UTF-8")) //Android 12: "File Manager" by Xiaomi + output.utf8 = true; + + else if (equalAsciiNoCase(line, " CLNT")) + output.clnt = true; + } + } + return output; + } + + const FtpSessionCfg sessionCfg_; + CURL* easyHandle_ = nullptr; + + curl_socket_t utf8RequestedSocket_ = 0; + curl_socket_t binaryEnabledSocket_ = 0; + + bool socketUsesUtf8_ = false; + + ServerEncoding encoding_ = ServerEncoding::unknown; + + std::optional featureCache_; + std::optional homePathCached_; + + const std::shared_ptr libsshCurlUnifiedInitCookie_{getLibsshCurlUnifiedInitCookie(globalFtpSessionCount)}; //throw SysError + std::chrono::steady_clock::time_point lastSuccessfulUseTime_; + std::weak_ptr timeoutSec_; +}; + +//================================================================================================================ +//================================================================================================================ + +class FtpSessionManager //reuse (healthy) FTP sessions globally +{ + struct FtpSessionCache; + +public: + FtpSessionManager() : sessionCleaner_([this] + { + setCurrentThreadName(Zstr("Session Cleaner[FTP]")); + runGlobalSessionCleanUp(); /*throw ThreadStopRequest*/ + }) {} + + void access(const FtpLogin& login, const std::function& useFtpSession /*throw X*/) //throw SysError, X + { + Protected& sessionCache = getSessionCache(login); + + std::unique_ptr ftpSession; //either or + std::optional sessionCfg; // + + sessionCache.access([&](FtpSessionCache& cache) + { + if (!cache.activeCfg) //AFS::authenticateAccess() not called => authenticate implicitly! + setActiveConfig(cache, login); + + //assume "isHealthy()" to avoid hitting server connection limits: (clean up of !isHealthy() after use, idle sessions via worker thread) + if (!cache.idleFtpSessions.empty()) + { + ftpSession = std::move(cache.idleFtpSessions.back ()); + /**/ cache.idleFtpSessions.pop_back(); + } + else + sessionCfg = *cache.activeCfg; + }); + + //create new FTP session outside the lock: 1. don't block other threads 2. non-atomic regarding "sessionCache"! => one session too many is not a problem! + if (!ftpSession) + ftpSession = std::make_unique(*sessionCfg); //throw SysError + + const std::shared_ptr timeoutSec = std::make_shared(login.timeoutSec); //context option: valid only for duration of this call! + ftpSession->setContextTimeout(timeoutSec); + + ZEN_ON_SCOPE_EXIT + ( + //*INDENT-OFF* + if (ftpSession->isHealthy()) //thread that created the "!isHealthy()" session is responsible for clean up (avoid hitting server connection limits!) + sessionCache.access([&](FtpSessionCache& cache) + { + if (ftpSession->getSessionCfg() == *cache.activeCfg) //created outside the lock => check *again* + cache.idleFtpSessions.push_back(std::move(ftpSession)); //pass ownership + }); + //*INDENT-ON* + ); + + useFtpSession(*ftpSession); //throw X + } + + void setActiveConfig(const FtpLogin& login) + { + getSessionCache(login).access([&](FtpSessionCache& cache) { setActiveConfig(cache, login); }); + } + + void setSessionPassword(const FtpLogin& login, const Zstring& password) + { + getSessionCache(login).access([&](FtpSessionCache& cache) + { + cache.sessionPassword = password; + setActiveConfig(cache, login); + }); + } + +private: + FtpSessionManager (const FtpSessionManager&) = delete; + FtpSessionManager& operator=(const FtpSessionManager&) = delete; + + Protected& getSessionCache(const FtpDeviceId& deviceId) + { + //single global session cache per login; life-time bound to globalInstance => never remove a sessionCache!!! + Protected* sessionCache = nullptr; + + globalSessionCache_.access([&](GlobalFtpSessions& sessionsById) + { + sessionCache = &sessionsById[deviceId]; //get or create + }); + static_assert(std::is_same_v>>, "require std::map so that the pointers we return remain stable"); + + return *sessionCache; + } + + void setActiveConfig(FtpSessionCache& cache, const FtpLogin& login) + { + if (cache.activeCfg) + assert(std::all_of(cache.idleFtpSessions.begin(), cache.idleFtpSessions.end(), + [&](const std::unique_ptr& session) { return session->getSessionCfg() == cache.activeCfg; })); + else + assert(cache.idleFtpSessions.empty()); + + const std::optional prevCfg = cache.activeCfg; + + cache.activeCfg = + { + .deviceId{login}, + .password = login.password ? *login.password : cache.sessionPassword, + .useTls = login.useTls, + }; + + /* remove incompatible sessions: + - avoid hitting FTP connection limit if some config uses TLS, but not the other: https://freefilesync.org/forum/viewtopic.php?t=8532 + - logically consistent with AFS::compareDevice() + - don't allow different authentication methods, when authenticateAccess() is called *once* per device in getFolderStatusParallel() + - what user expects, e.g. when tesing changed settings in FTP login dialog */ + if (cache.activeCfg != prevCfg) + cache.idleFtpSessions.clear(); //run ~FtpSession *inside* the lock! => avoid hitting server limits! + } + + //run a dedicated clean-up thread => it's unclear when the server let's a connection time out, so we do it preemptively + //context of worker thread: + void runGlobalSessionCleanUp() //throw ThreadStopRequest + { + std::chrono::steady_clock::time_point lastCleanupTime; + for (;;) + { + const auto now = std::chrono::steady_clock::now(); + + if (now < lastCleanupTime + FTP_SESSION_CLEANUP_INTERVAL) + interruptibleSleep(lastCleanupTime + FTP_SESSION_CLEANUP_INTERVAL - now); //throw ThreadStopRequest + + lastCleanupTime = std::chrono::steady_clock::now(); + + std::vector*> sessionCaches; //pointers remain stable, thanks to std::map<> + + globalSessionCache_.access([&](GlobalFtpSessions& sessionsById) + { + for (auto& [sessionId, idleSession] : sessionsById) + sessionCaches.push_back(&idleSession); + }); + + for (Protected* sessionCache : sessionCaches) + for (;;) + { + bool done = false; + sessionCache->access([&](FtpSessionCache& cache) + { + for (std::unique_ptr& ftpSession : cache.idleFtpSessions) + if (!ftpSession->isHealthy()) //!isHealthy() sessions are destroyed after use => in this context this means they have been idle for too long + { + ftpSession.swap(cache.idleFtpSessions.back()); + /**/ cache.idleFtpSessions.pop_back(); //run ~FtpSession *inside* the lock! => avoid hitting server limits! + return; //don't hold lock for too long: delete only one session at a time, then yield... + } + done = true; + }); + if (done) + break; + std::this_thread::yield(); //outside the lock + } + } + } + + struct FtpSessionCache + { + //invariant: all cached sessions correspond to activeCfg at any time! + std::vector> idleFtpSessions; //extract *temporarily* from this list during use + std::optional activeCfg; + Zstring sessionPassword; + }; + + using GlobalFtpSessions = std::map>; + Protected globalSessionCache_; + + InterruptibleThread sessionCleaner_; +}; + +//-------------------------------------------------------------------------------------- +UniInitializer globalStartupInitFtp(*globalFtpSessionCount.get()); + +constinit Global globalFtpSessionManager; //caveat: life time must be subset of static UniInitializer! +//-------------------------------------------------------------------------------------- + +void accessFtpSession(const FtpLogin& login, const std::function& useFtpSession /*throw X*/) //throw SysError, X +{ + if (const std::shared_ptr mgr = globalFtpSessionManager.get()) + mgr->access(login, useFtpSession); //throw SysError, X + else + throw SysError(formatSystemError("accessFtpSession", L"", L"Function call not allowed during init/shutdown.")); +} + +//=========================================================================================================================== + +struct FtpItem +{ + AFS::ItemType type = AFS::ItemType::file; + Zstring itemName; + uint64_t fileSize = 0; + time_t modTime = 0; + AFS::FingerPrint filePrint = 0; //optional +}; + + +//get info about *existing* symlink! +FtpItem getFtpSymlinkInfo(const FtpLogin& login, const AfsPath& linkPath) //throw FileError +{ + try + { + FtpItem output; + assert(output.type == AFS::ItemType::file); + output.itemName = AFS::getItemName(linkPath); + + std::string mdtmBuf; + accessFtpSession(login, [&](FtpSession& session) //throw SysError + { + /* first test if we have a file; if it's a folder expect FTP code 550 + alternative: assume folder and try traversal? NOPE: this can *succeed* for file symlinks with MLSD! (e.g. on freefilesync.org FTP) + + -> can't replace SIZE + MDTM with MLSD which doesn't follow symlinks! */ + + session.ensureBinaryMode(); //throw SysError + //...or some server return ASCII size or fail with '550 SIZE not allowed in ASCII mode: https://freefilesync.org/forum/viewtopic.php?t=7669&start=30#p27742 + const std::string sizeBuf = session.runSingleFtpCommand("*SIZE " + session.getServerPathInternal(linkPath), + true /*requestUtf8*/); //throw SysError, SysErrorFtpProtocol + //alternative: use libcurl + CURLINFO_CONTENT_LENGTH_DOWNLOAD_T? => nah, surprise (motherfucker)! libcurl adds needless "REST 0" command! + for (const std::string_view& line : splitFtpResponse(sizeBuf)) + if (startsWith(line, "213 ")) // 213[rubbish] according to libcurl + { + if (isDigit(line.back())) //https://tools.ietf.org/html/rfc3659#section-4 + { + auto it = std::find_if(line.rbegin(), line.rend(), [](const char c) { return !isDigit(c); }); + output.fileSize = stringTo(makeStringView(it.base(), line.end())); + + mdtmBuf = session.runSingleFtpCommand("MDTM " + session.getServerPathInternal(linkPath), + true /*requestUtf8*/); //throw SysError, SysErrorFtpProtocol + return; + } + break; + } + else if (startsWith(line, "550 ")) //e.g. "550 I can only retrieve regular files" + { + output.type = AFS::ItemType::folder; + return; + } + throw SysError(L"Unexpected FTP response. (" + utfTo(sizeBuf) + L')'); + }); + + if (output.type == AFS::ItemType::folder) + return output; + + output.modTime = [&] //https://tools.ietf.org/html/rfc3659#section-3 + { + for (const std::string_view& line : splitFtpResponse(mdtmBuf)) + if (startsWith(line, "213 ")) // 213 YYYYMMDDHHMMSS[.sss] "Time values are always represented in UTC (GMT)" ...and libcurl thinks so, too + { + const auto itStart = line.begin() + 4; + const auto itEnd = std::find(itStart, line.end(), '.'); + + if (const TimeComp tc = parseTime("%Y%m%d%H%M%S", makeStringView(itStart, itEnd)); + tc != TimeComp()) + if (const auto [modTime, timeValid] = utcToTimeT(tc); + timeValid) + return modTime; + break; + } + throw SysError(L"Unexpected FTP response. (" + utfTo(mdtmBuf) + L')'); + }(); + + return output; + } + catch (const SysError& e) + { + throw FileError(replaceCpy(_("Cannot resolve symbolic link %x."), L"%x", fmtPath(getCurlDisplayPath(login, linkPath))), e.toString()); + } +} + + +class FtpDirectoryReader +{ +public: + static std::vector execute(const FtpLogin& login, const AfsPath& dirPath) //throw SysError, SysErrorFtpProtocol + { + std::string rawListing; //get raw FTP directory listing + + curl_write_callback onBytesReceived = [](/*const*/ char* buffer, size_t size, size_t nitems, void* callbackData) + { + auto& listing = *static_cast(callbackData); + listing.append(buffer, size * nitems); + return size * nitems; + //folder reading might take up to a minute in extreme cases (50,000 files): https://freefilesync.org/forum/viewtopic.php?t=5312 + }; + + std::vector output; + + accessFtpSession(login, [&](FtpSession& session) //throw SysError + { + std::vector options = + { + {CURLOPT_WRITEDATA, &rawListing}, + {CURLOPT_WRITEFUNCTION, onBytesReceived}, + }; + long pathMethod = CURLFTPMETHOD_SINGLECWD; + + if (session.supportsMlsd()) //throw SysError + { + options.emplace_back(CURLOPT_CUSTOMREQUEST, "MLSD"); + + //some FTP servers abuse https://tools.ietf.org/html/rfc3659#section-7.1 + //and process wildcards characters inside the "dirpath"; see http://www.proftpd.org/docs/howto/Globbing.html + // [] matches any character in the character set enclosed in the brackets + // * (not between brackets) matches any string, including the empty string + // ? (not between brackets) matches any single character + // + //of course this "helpfulness" blows up with MLSD + paths that incidentally contain wildcards: https://freefilesync.org/forum/viewtopic.php?t=5575 + const bool pathHasWildcards = //=> globbing is reproducible even with freefilesync.org's FTP! + contains(afterFirst(dirPath.value, Zstr('['), IfNotFoundReturn::none), Zstr(']')) || + contains(dirPath.value, Zstr('*')) || + contains(dirPath.value, Zstr('?')); + + if (!pathHasWildcards) + pathMethod = CURLFTPMETHOD_NOCWD; //16% faster traversal compared to CURLFTPMETHOD_SINGLECWD (35% faster than CURLFTPMETHOD_MULTICWD) + } + //else: use "LIST" + CURLFTPMETHOD_SINGLECWD + //caveat: let's better not use LIST parameters: https://cr.yp.to/ftp/list.html + + session.perform(dirPath, true /*isDir*/, pathMethod, options, true /*requestUtf8*/); //throw SysError, SysErrorPassword, SysErrorFtpProtocol + + if (session.supportsMlsd()) //throw SysError + output = parseMlsd(rawListing, session); //throw SysError + else + output = parseUnknown(rawListing, session); //throw SysError + }); + + return output; + } + +private: + FtpDirectoryReader (const FtpDirectoryReader&) = delete; + FtpDirectoryReader& operator=(const FtpDirectoryReader&) = delete; + + static std::vector parseMlsd(const std::string& buf, FtpSession& session) //throw SysError + { + std::vector output; + for (const std::string_view& line : splitFtpResponse(buf)) + { + FtpItem item = parseMlstLine(line, session); //throw SysError + if (item.itemName != Zstr(".") && + item.itemName != Zstr("..")) + output.push_back(std::move(item)); + } + return output; + } + + static FtpItem parseMlstLine(const std::string_view& rawLine, FtpSession& session) //throw SysError + { + /* https://tools.ietf.org/html/rfc3659 + type=cdir;sizd=4096;modify=20170116230740;UNIX.mode=0755;UNIX.uid=874;UNIX.gid=869;unique=902g36e1c55; . + type=pdir;sizd=4096;modify=20170116230740;UNIX.mode=0755;UNIX.uid=874;UNIX.gid=869;unique=902g36e1c55; .. + type=file;size=4;modify=20170113063314;UNIX.mode=0600;UNIX.uid=874;UNIX.gid=869;unique=902g36e1c5d; readme.txt + type=dir;sizd=4096;modify=20170117144634;UNIX.mode=0755;UNIX.uid=874;UNIX.gid=869;unique=902g36e418a; folder */ + try + { + FtpItem item; + + auto itBegin = rawLine.begin(); + if (startsWith(rawLine, ' ')) //leading blank is already trimmed if MLSD was processed by curl + ++itBegin; + auto itBlank = std::find(itBegin, rawLine.end(), ' '); + if (itBlank == rawLine.end()) + throw SysError(L"Item name not available."); + + const std::string_view facts = makeStringView(itBegin, itBlank); + item.itemName = session.serverToUtfEncoding(makeStringView(itBlank + 1, rawLine.end())); //throw SysError + + std::string_view typeFact; + std::string_view fileSize; + + split(facts, ';', [&](const std::string_view fact) + { + if (!fact.empty()) + { + if (startsWithAsciiNoCase(fact, "type=")) //must be case-insensitive!!! + { + const std::string_view tmp = afterFirst(fact, '=', IfNotFoundReturn::none); + typeFact = beforeFirst(tmp, ':', IfNotFoundReturn::all); + } + else if (startsWithAsciiNoCase(fact, "size=")) + fileSize = afterFirst(fact, '=', IfNotFoundReturn::none); + else if (startsWithAsciiNoCase(fact, "modify=")) + { + std::string_view modifyFact = afterFirst(fact, '=', IfNotFoundReturn::none); + modifyFact = beforeLast(modifyFact, '.', IfNotFoundReturn::all); //truncate millisecond precision if available + + const TimeComp tc = parseTime("%Y%m%d%H%M%S", modifyFact); + if (tc == TimeComp()) + throw SysError(L"Modification time is invalid."); + + if (const auto [modTime, timeValid] = utcToTimeT(tc); + timeValid) + item.modTime = modTime; + else + throw SysError(L"Modification time is invalid."); + } + else if (startsWithAsciiNoCase(fact, "unique=")) + { + /* https://tools.ietf.org/html/rfc3659#section-7.5.2 + "The mapping between files, and unique fact tokens should be maintained, [...] for + *at least* the lifetime of the control connection from user-PI to server-PI." + + => not necessarily *persistent* as far as the RFC goes! + BUT: practially this will be the inode ID/file index, so we can assume persistence */ + const std::string_view uniqueId = afterFirst(fact, '=', IfNotFoundReturn::none); + assert(!uniqueId.empty()); + item.filePrint = hashString(uniqueId); + //other metadata to hash e.g. create fact? => not available on Linux-hosted FTP! + } + } + }); + + if (equalAsciiNoCase(typeFact, "cdir")) + return {AFS::ItemType::folder, Zstr("."), 0, 0}; + if (equalAsciiNoCase(typeFact, "pdir")) + return {AFS::ItemType::folder, Zstr(".."), 0, 0}; + + if (equalAsciiNoCase(typeFact, "dir")) + item.type = AFS::ItemType::folder; + else if (equalAsciiNoCase(typeFact, "OS.unix=slink") || //the OS.unix=slink:/target syntax is a hack and often skips + equalAsciiNoCase(typeFact, "OS.unix=symlink")) //the target path after the colon: http://www.proftpd.org/docs/modules/mod_facts.html + item.type = AFS::ItemType::symlink; + //It may be a good idea to NOT check for type "file" explicitly: see comment in native.cpp + + //evaluate parsing errors right now (+ report raw entry in error message!) + if (item.itemName.empty()) + throw SysError(L"Item name not available."); + + if (item.type == AFS::ItemType::file) + { + if (fileSize.empty() || !std::all_of(fileSize.begin(), fileSize.end(), &isDigit)) + throw SysError(L"File size not available."); //crazy, but can be "-1": https://freefilesync.org/forum/viewtopic.php?t=9720#p35757 + item.fileSize = stringTo(fileSize); + } + return item; + } + catch (const SysError& e) + { + throw SysError(L"Unexpected FTP response. (" + utfTo(rawLine) + L") " + e.toString()); + } + } + + static std::vector parseUnknown(const std::string& buf, FtpSession& session) //throw SysError + { + if (!buf.empty() && isDigit(buf[0])) //lame test to distinguish Unix/Dos formats as internally used by libcurl + return parseWindows(buf, session); //throw SysError + return parseUnix(buf, session); // + } + + //"ls -l" + static std::vector parseUnix(const std::string& buf, FtpSession& session) //throw SysError + { + const std::vector lines = splitFtpResponse(buf); + auto it = lines.begin(); + + if (it != lines.end() && startsWith(*it, "total ")) + ++it; + + const time_t utcTimeNow = std::time(nullptr); + const TimeComp tc = getUtcTime(utcTimeNow); + if (tc == TimeComp()) + throw SysError(L"Failed to determine current time: " + numberTo(utcTimeNow)); + + const int utcCurrentYear = tc.year; + + //different listing formats: better store at session level!? + std::optional dirOwnerGroupCount; // + std::optional fileOwnerGroupCount; //caveat: differentiate per item type: see alternative formats! + std::optional linkOwnerGroupCount; // + + std::vector output; + + std::for_each(it, lines.end(), [&](const std::string_view line) + { + auto& ownerGroupCount = [&]() -> std::optional& + { + assert(!line.empty()); //see splitFtpResponse() + switch (line[0]) + { + case 'd': return dirOwnerGroupCount; + case 'l': return linkOwnerGroupCount; + default : return fileOwnerGroupCount; + } + }(); + + //unix listing without group: https://freefilesync.org/forum/viewtopic.php?t=4306 + if (!ownerGroupCount) + ownerGroupCount = [&] + { + std::optional firstError; + + for (int i = 3; i-- > 0;) + try + { + parseUnixLine(line, utcTimeNow, utcCurrentYear, i /*ownerGroupCount*/, session); //throw SysError + return i; + } + catch (const SysError& e) + { + if (!firstError) + firstError = e; + } + throw* firstError; //most likely the relevant one: https://freefilesync.org/forum/viewtopic.php?t=10798 + }(); + + const FtpItem item = parseUnixLine(line, utcTimeNow, utcCurrentYear, *ownerGroupCount, session); //throw SysError + if (item.itemName != Zstr(".") && + item.itemName != Zstr("..")) + output.push_back(item); + }); + + return output; + } + + static FtpItem parseUnixLine(const std::string_view& rawLine, time_t utcTimeNow, int utcCurrentYear, int ownerGroupCount, FtpSession& session) //throw SysError + { + /* Unix standard listing: "ls -l --all" + + total 4953 <- optional first line + drwxr-xr-x 1 root root 4096 Jan 10 11:58 version + -rwxr-xr-x 1 root root 1084 Sep 2 01:17 Unit Test.vcxproj.user + -rwxr-xr-x 1 1000 300 2217 Feb 28 2016 win32.manifest + lrwxr-xr-x 1 root root 18 Apr 26 15:17 Projects -> /mnt/hgfs/Projects + + file type: -:file l:symlink d:directory b:block device p:named pipe c:char device s:socket + + permissions: (r|-)(w|-)(x|s|S|-) user + (r|-)(w|-)(x|s|S|-) group s := S + x S = Setgid + (r|-)(w|-)(x|t|T|-) others t := T + x T = sticky bit + + Alternative formats + ------------------- + No group: "ls -l --no-group" https://freefilesync.org/forum/viewtopic.php?t=4306 + dr-xr-xr-x 2 root 512 Apr 8 1994 etc + + No owner, no group, trailing slash (but only for directories!????): "ls -g --no-group --file-type" https://freefilesync.org/forum/viewtopic.php?t=10227 + -rwxrwxrwx 1 ownername groupname 8064383 Mar 30 11:58 file.mp3 + drwxrwxrwx 1 0 Jan 1 00:00 dirname/ + + Yet to be seen in the wild: + Netware: + d [R----F--] supervisor 512 Jan 16 18:53 login + - [R----F--] rhesus 214059 Oct 20 15:27 cx.exe + + NetPresenz for the Mac: + -------r-- 326 1391972 1392298 Nov 22 1995 MegaPhone.sit + drwxrwxr-x folder 2 May 10 1996 network */ + try + { + FtpLineParser parser(rawLine); + + const std::string_view typeTag = parser.readRange(1, [](const char c) //throw SysError + { + return c == '-' || c == 'b' || c == 'c' || c == 'd' || c == 'l' || c == 'p' || c == 's'; + }); + //------------------------------------------------------------------------------------ + //permissions + parser.readRange(9, [](const char c) //throw SysError + { + return c == '-' || c == 'r' || c == 'w' || c == 'x' || c == 's' || c == 'S' || c == 't' || c == 'T'; + }); + parser.readRange(&isWhiteSpace); //throw SysError + //------------------------------------------------------------------------------------ + //hard-link count (no separators) + parser.readRange(&isDigit); //throw SysError + parser.readRange(&isWhiteSpace); //throw SysError + //------------------------------------------------------------------------------------ + //both owner + group, owner only, or none at all + assert(0 <= ownerGroupCount && ownerGroupCount <=2); + for (int i = 0; i < ownerGroupCount; ++i) + { + parser.readRange(std::not_fn(isWhiteSpace)); //throw SysError + parser.readRange(&isWhiteSpace); //throw SysError + } + //------------------------------------------------------------------------------------ + //file size (no separators) + const uint64_t fileSize = stringTo(parser.readRange(&isDigit)); //throw SysError + parser.readRange(&isWhiteSpace); //throw SysError + //------------------------------------------------------------------------------------ + const std::string_view monthStr = parser.readRange(std::not_fn(isWhiteSpace)); //throw SysError + parser.readRange(&isWhiteSpace); //throw SysError + + const char* months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; + auto itMonth = std::find_if(std::begin(months), std::end(months), [&](const char* name) { return equalAsciiNoCase(name, monthStr); }); + if (itMonth == std::end(months)) + throw SysError(L"Failed to parse month name."); + //------------------------------------------------------------------------------------ + const int day = stringTo(parser.readRange(&isDigit)); //throw SysError + parser.readRange(&isWhiteSpace); //throw SysError + if (day < 1 || day > 31) + throw SysError(L"Failed to parse day of month."); + //------------------------------------------------------------------------------------ + const std::string_view timeOrYear = parser.readRange([](const char c) { return c == ':' || isDigit(c); }); //throw SysError + parser.readRange(&isWhiteSpace); //throw SysError + + TimeComp timeComp; + timeComp.month = 1 + static_cast(itMonth - std::begin(months)); + timeComp.day = day; + + if (contains(timeOrYear, ':')) + { + const int hour = stringTo(beforeFirst(timeOrYear, ':', IfNotFoundReturn::none)); + const int minute = stringTo(afterFirst (timeOrYear, ':', IfNotFoundReturn::none)); + if (hour < 0 || hour > 23 || minute < 0 || minute > 59) + throw SysError(L"Failed to parse modification time."); + + timeComp.hour = hour; + timeComp.minute = minute; + timeComp.year = utcCurrentYear; //tentatively + + const auto [serverLocalTime, timeValid] = utcToTimeT(timeComp); + if (!timeValid) + throw SysError(L"Modification time is invalid."); + + if (serverLocalTime > utcTimeNow + 24 * 3600 ) //time-zones range from UTC-12:00 to UTC+14:00, consider DST; FileZilla uses 1 day tolerance + --timeComp.year; //"more likely" this time is from last year + } + else if (timeOrYear.size() == 4) + { + timeComp.year = stringTo(timeOrYear); + + if (timeComp.year < 1600 || timeComp.year >= 3000) + throw SysError(L"Failed to parse modification time."); + } + else + throw SysError(L"Failed to parse modification time."); + + //let's pretend the time listing is UTC (same behavior as FileZilla): hopefully MLSD will make this mess obsolete soon... + // => find exact offset with some MDTM hackery? yes, could do that, but this doesn't solve the bigger problem of imprecise LIST file times, so why bother? + const auto [modTime, timeValid] = utcToTimeT(timeComp); + if (!timeValid) + throw SysError(L"Modification time is invalid."); + //------------------------------------------------------------------------------------ + const std::string_view trail = parser.readRange([](char) { return true; }); //throw SysError + std::string_view itemName; + if (typeTag == "l") + itemName = beforeFirst(trail, " -> ", IfNotFoundReturn::none); + else + itemName = trail; + if (itemName.empty()) + throw SysError(L"Item name not available."); + + if (itemName == "." || itemName == "..") //sometimes returned, e.g. by freefilesync.org + return {AFS::ItemType::folder, utfTo(itemName), 0, 0}; + //------------------------------------------------------------------------------------ + FtpItem item; + if (typeTag == "d") + item.type = AFS::ItemType::folder; + else if (typeTag == "l") + item.type = AFS::ItemType::symlink; + else + item.fileSize = fileSize; + + item.itemName = session.serverToUtfEncoding(itemName); //throw SysError + if (item.type == AFS::ItemType::folder && endsWith(item.itemName, Zstr('/'))) + item.itemName.pop_back(); + + item.modTime = modTime; + + return item; + } + catch (const SysError& e) + { + throw SysError(L"Unexpected FTP response. (" + utfTo(rawLine) + L") [ownerGroupCount: " + numberTo(ownerGroupCount) + L"] " + e.toString()); + } + } + + + //"dir" + static std::vector parseWindows(const std::string& buf, FtpSession& session) //throw SysError + { + /* Test server: test.rebex.net username:demo pw:password useTls = true + + listing supported by libcurl (US server) + 10-27-15 03:46AM pub + 04-08-14 03:09PM 11,399 readme.txt + + Datalogic Windows CE 5.0 + 01-01-98 13:00 Storage Card + + IIS option "four-digit years" + 06-22-2017 04:25PM test + 06-20-2017 12:50PM 1875499 zstring.obj + + Alternative formats (yet to be seen in the wild) + "dir" on Windows, US: + 10/27/2015 03:46 AM pub + 04/08/2014 03:09 PM 11,399 readme.txt + + "dir" on Windows, German: + 21.09.2016 18:31 Favorites + 12.01.2017 19:57 11.399 gsview64.ini */ + + const TimeComp tc = getUtcTime(); + if (tc == TimeComp()) + throw SysError(L"Failed to determine current time: " + numberTo(std::time(nullptr))); + const int utcCurrentYear = tc.year; + + std::vector output; + for (const std::string_view& line : splitFtpResponse(buf)) + { + try + { + FtpLineParser parser(line); + + const int month = stringTo(parser.readRange(2, &isDigit)); //throw SysError + parser.readRange(1, [](const char c) { return c == '-' || c == '/'; }); //throw SysError + const int day = stringTo(parser.readRange(2, &isDigit)); //throw SysError + parser.readRange(1, [](const char c) { return c == '-' || c == '/'; }); //throw SysError + const std::string_view yearString = parser.readRange(&isDigit); //throw SysError + parser.readRange(&isWhiteSpace); //throw SysError + + if (month < 1 || month > 12 || day < 1 || day > 31) + throw SysError(L"Failed to parse modification time."); + + int year = 0; + if (yearString.size() == 2) + { + year = (utcCurrentYear / 100) * 100 + stringTo(yearString); + if (year > utcCurrentYear + 1 /*local time leeway*/) + year -= 100; + } + else if (yearString.size() == 4) + year = stringTo(yearString); + else + throw SysError(L"Failed to parse modification time."); + //------------------------------------------------------------------------------------ + int hour = stringTo(parser.readRange(2, &isDigit)); //throw SysError + parser.readRange(1, [](const char c) { return c == ':'; }); //throw SysError + const int minute = stringTo(parser.readRange(2, &isDigit)); //throw SysError + if (!isWhiteSpace(parser.peekNextChar())) + { + const std::string_view period = parser.readRange(2, [](const char c) { return c == 'A' || c == 'P' || c == 'M'; }); //throw SysError + if (period == "PM") + { + if (0 <= hour && hour < 12) + hour += 12; + } + else if (hour == 12) + hour = 0; + } + parser.readRange(&isWhiteSpace); //throw SysError + + if (hour < 0 || hour > 23 || minute < 0 || minute > 59) + throw SysError(L"Failed to parse modification time."); + //------------------------------------------------------------------------------------ + TimeComp timeComp; + timeComp.year = year; + timeComp.month = month; + timeComp.day = day; + timeComp.hour = hour; + timeComp.minute = minute; + //let's pretend the time listing is UTC (same behavior as FileZilla): hopefully MLSD will make this mess obsolete soon... + // => find exact offset with some MDTM hackery? yes, could do that, but this doesn't solve the bigger problem of imprecise LIST file times, so why bother? + const auto [modTime, timeValid] = utcToTimeT(timeComp); + if (!timeValid) + throw SysError(L"Modification time is invalid."); + //------------------------------------------------------------------------------------ + const std::string_view dirTagOrSize = parser.readRange(std::not_fn(isWhiteSpace)); //throw SysError + parser.readRange(&isWhiteSpace); //throw SysError + + const bool isDir = dirTagOrSize == ""; + uint64_t fileSize = 0; + if (!isDir) + { + std::string sizeStr(dirTagOrSize); + replace(sizeStr, ',', ""); + replace(sizeStr, '.', ""); + if (sizeStr.empty() || !std::all_of(sizeStr.begin(), sizeStr.end(), &isDigit)) + throw SysError(L"Failed to parse file size."); + fileSize = stringTo(sizeStr); + } + //------------------------------------------------------------------------------------ + const std::string_view itemName = parser.readRange([](char) { return true; }); //throw SysError + if (itemName.empty()) + throw SysError(L"Folder contains an item without name."); + + //------------------------------------------------------------------------------------ + if (itemName != "." && + itemName != "..") + { + FtpItem item; + if (isDir) + item.type = AFS::ItemType::folder; + item.itemName = session.serverToUtfEncoding(itemName); //throw SysError + item.fileSize = fileSize; + item.modTime = modTime; + + output.push_back(item); + } + } + catch (const SysError& e) + { + throw SysError(L"Unexpected FTP response. (" + utfTo(line) + L") " + e.toString()); + } + } + + return output; + } +}; + + +class SingleFolderTraverser +{ +public: + SingleFolderTraverser(const FtpLogin& login, const std::vector>>& workload /*throw X*/) + : workload_(workload), login_(login) + { + while (!workload_.empty()) + { + auto wi = std::move(workload_. back()); //yes, no strong exception guarantee (std::bad_alloc) + /**/ workload_.pop_back(); // + const auto& [folderPath, cb] = wi; + + tryReportingDirError([&] //throw X + { + traverseWithException(folderPath, *cb); //throw FileError, X + }, *cb); + } + } + +private: + SingleFolderTraverser (const SingleFolderTraverser&) = delete; + SingleFolderTraverser& operator=(const SingleFolderTraverser&) = delete; + + void traverseWithException(const AfsPath& dirPath, AFS::TraverserCallback& cb) //throw FileError, X + { + std::vector items; + try + { + items = FtpDirectoryReader::execute(login_, dirPath); //throw SysError, SysErrorFtpProtocol + } + catch (const SysError& e) + { + throw FileError(replaceCpy(_("Cannot read directory %x."), L"%x", fmtPath(getCurlDisplayPath(login_, dirPath))), e.toString()); + } + + for (const FtpItem& item : items) + { + const AfsPath itemPath(appendPath(dirPath.value, item.itemName)); + + switch (item.type) + { + case AFS::ItemType::file: + cb.onFile({item.itemName, item.fileSize, item.modTime, item.filePrint, false /*isFollowedSymlink*/}); //throw X + break; + + case AFS::ItemType::folder: + if (std::shared_ptr cbSub = cb.onFolder({item.itemName, false /*isFollowedSymlink*/})) //throw X + workload_.push_back({itemPath, std::move(cbSub)}); + break; + + case AFS::ItemType::symlink: + switch (cb.onSymlink({item.itemName, item.modTime})) //throw X + { + case AFS::TraverserCallback::HandleLink::follow: + { + FtpItem target = {}; + if (!tryReportingItemError([&] //throw X + { + target = getFtpSymlinkInfo(login_, itemPath); //throw FileError + }, cb, item.itemName)) + continue; + + if (target.type == AFS::ItemType::folder) + { + if (std::shared_ptr cbSub = cb.onFolder({item.itemName, true /*isFollowedSymlink*/})) //throw X + workload_.push_back({itemPath, std::move(cbSub)}); + } + else //a file or named pipe, etc. + cb.onFile({item.itemName, target.fileSize, target.modTime, item.filePrint, true /*isFollowedSymlink*/}); //throw X + } + break; + + case AFS::TraverserCallback::HandleLink::skip: + break; + } + break; + } + } + } + + std::vector>> workload_; + const FtpLogin login_; +}; + + +void traverseFolderRecursiveFTP(const FtpLogin& login, const std::vector>>& workload /*throw X*/, size_t) //throw X +{ + SingleFolderTraverser dummy(login, workload); //throw X +} +//=========================================================================================================================== +//=========================================================================================================================== + +void ftpFileDownload(const FtpLogin& login, const AfsPath& afsFilePath, //throw FileError, X + const std::function& writeBlock /*throw X*/) +{ + std::exception_ptr exception; + + auto onBytesReceived = [&](const void* buffer, size_t bytesToWrite) + { + try + { + writeBlock(buffer, bytesToWrite); //throw X + //[!] let's NOT use "incomplete write Posix semantics" for libcurl! + //who knows if libcurl buffers properly, or if it sends incomplete packages!? + return bytesToWrite; + } + catch (...) + { + exception = std::current_exception(); + return bytesToWrite + 1; //signal error condition => CURLE_WRITE_ERROR + } + }; + curl_write_callback onBytesReceivedWrapper = [](char* buffer, size_t size, size_t nitems, void* callbackData) + { + return (*static_cast(callbackData))(buffer, size * nitems); //free this poor little C-API from its shackles and redirect to a proper lambda + }; + + try + { + accessFtpSession(login, [&](FtpSession& session) //throw SysError + { + session.perform(afsFilePath, false /*isDir*/, CURLFTPMETHOD_NOCWD, //are there any servers that require CURLFTPMETHOD_SINGLECWD? let's find out + { + {CURLOPT_WRITEDATA, &onBytesReceived}, + {CURLOPT_WRITEFUNCTION, onBytesReceivedWrapper}, + {CURLOPT_IGNORE_CONTENT_LENGTH, 1L}, //skip FTP "SIZE" command before download (=> download until actual EOF if file size changes) + + //{CURLOPT_BUFFERSIZE, 256 * 1024} -> default is 16 kB which seems to correspond to TLS packet size + //=> setting larger buffer size does nothing (recv still returns only 16 kB) + }, true /*requestUtf8*/); //throw SysError, SysErrorPassword, SysErrorFtpProtocol + }); + } + catch (const SysError& e) + { + if (exception) + std::rethrow_exception(exception); + + throw FileError(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(getCurlDisplayPath(login, afsFilePath))), e.toString()); + } +} + + +/* File already existing: + freefilesync.org: overwrites + FileZilla Server: overwrites + Windows IIS: overwrites */ +void ftpFileUpload(const FtpLogin& login, const AfsPath& afsFilePath, + const std::function& readBlock /*throw X*/) //throw FileError, X; return "bytesToRead" bytes unless end of stream +{ + std::exception_ptr exception; + + auto getBytesToSend = [&](void* buffer, size_t bytesToRead) -> 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 + + [!] let's NOT use "incomplete read Posix semantics" for libcurl! + who knows if libcurl buffers properly, or if it requests incomplete packages!? */ + const size_t bytesRead = readBlock(buffer, bytesToRead); //throw X; return "bytesToRead" bytes unless end of stream + assert(bytesRead == bytesToRead || bytesRead == 0 || readBlock(buffer, bytesToRead) == 0); + return bytesRead; + } + catch (...) + { + exception = std::current_exception(); + return CURL_READFUNC_ABORT; //signal error condition => CURLE_ABORTED_BY_CALLBACK + } + }; + curl_read_callback getBytesToSendWrapper = [](char* buffer, size_t size, size_t nitems, void* callbackData) + { + return (*static_cast(callbackData))(buffer, size * nitems); //free this poor little C-API from its shackles and redirect to a proper lambda + }; + + try + { + accessFtpSession(login, [&](FtpSession& session) //throw SysError + { + /* curl_slist* quote = nullptr; + ZEN_ON_SCOPE_EXIT(::curl_slist_free_all(quote)); + + //"prefix the command with an asterisk to make libcurl continue even if the command fails" + quote = ::curl_slist_append(quote, ("*DELE " + session.getServerPathInternal(afsFilePath)).c_str()); //throw SysError + + //optimize fail-safe copy with RNFR/RNTO as CURLOPT_POSTQUOTE? -> even slightly *slower* than RNFR/RNTO as additional curl_easy_perform() */ + + session.perform(afsFilePath, false /*isDir*/, CURLFTPMETHOD_NOCWD, //are there any servers that require CURLFTPMETHOD_SINGLECWD? let's find out + { + {CURLOPT_UPLOAD, 1L}, + {CURLOPT_READDATA, &getBytesToSend}, + {CURLOPT_READFUNCTION, getBytesToSendWrapper}, + + //{CURLOPT_UPLOAD_BUFFERSIZE, 256 * 1024} -> defaults is 64 kB. apparently no performance improvement for larger buffers like 256 kB + + //{CURLOPT_INFILESIZE_LARGE, static_cast(inputBuffer.size())}, + //=> CURLOPT_INFILESIZE_LARGE does not issue a specific FTP command, but is used by libcurl only! + + //{CURLOPT_PREQUOTE, quote}, + //{CURLOPT_POSTQUOTE, quote}, + }, true /*requestUtf8*/); //throw SysError, SysErrorPassword, SysErrorFtpProtocol + }); + } + catch (const SysError& e) + { + if (exception) + std::rethrow_exception(exception); + + throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(getCurlDisplayPath(login, afsFilePath))), e.toString()); + } +} + +//=========================================================================================================================== + +struct InputStreamFtp : public AFS::InputStream +{ + InputStreamFtp(const FtpLogin& login, const AfsPath& filePath) + { + worker_ = InterruptibleThread([asyncStreamOut = this->asyncStreamIn_, login, filePath] + { + setCurrentThreadName(Zstr("Istream ") + utfTo(getCurlDisplayPath(login, filePath))); + try + { + auto writeBlock = [&](const void* buffer, size_t bytesToWrite) + { + asyncStreamOut->write(buffer, bytesToWrite); //throw ThreadStopRequest + }; + ftpFileDownload(login, filePath, writeBlock); //throw FileError, ThreadStopRequest + + asyncStreamOut->closeStream(); + } + catch (FileError&) { asyncStreamOut->setWriteError(std::current_exception()); } //let ThreadStopRequest pass through! + }); + } + + ~InputStreamFtp() + { + asyncStreamIn_->setReadError(std::make_exception_ptr(ThreadStopRequest())); + } + + size_t getBlockSize() override { return FTP_BLOCK_SIZE_DOWNLOAD; } //throw (FileError) + + //may return short; only 0 means EOF! CONTRACT: bytesToRead > 0! + size_t tryRead(void* buffer, size_t bytesToRead, const IoCallback& notifyUnbufferedIO /*throw X*/) override //throw FileError, (ErrorFileLocked), X + { + const size_t bytesRead = asyncStreamIn_->tryRead(buffer, bytesToRead); //throw FileError + reportBytesProcessed(notifyUnbufferedIO); //throw X + return bytesRead; + //no need for asyncStreamIn_->checkWriteErrors(): once end of stream is reached, asyncStreamOut->closeStream() was called => no errors occured + } + + std::optional tryGetAttributesFast() override { return {}; }//throw FileError + //there is no stream handle => no buffered attribute access! + //PERF: get attributes during file download? + // CURLOPT_FILETIME: test case 77 files, 4MB: overall copy time increases by 12% + // CURLOPT_PREQUOTE/CURLOPT_PREQUOTE/CURLOPT_POSTQUOTE + MDTM: test case 77 files, 4MB: overall copy time increases by 12% + +private: + void reportBytesProcessed(const IoCallback& notifyUnbufferedIO /*throw X*/) //throw X + { + const int64_t bytesDelta = makeSigned(asyncStreamIn_->getTotalBytesWritten()) - totalBytesReported_; + totalBytesReported_ += bytesDelta; + if (notifyUnbufferedIO) notifyUnbufferedIO(bytesDelta); //throw X + } + + int64_t totalBytesReported_ = 0; + std::shared_ptr asyncStreamIn_ = std::make_shared(FTP_STREAM_BUFFER_SIZE); + InterruptibleThread worker_; +}; + +//=========================================================================================================================== + +//CAVEAT: if upload fails due to already existing, OutputStreamFtp constructor does not fail, but OutputStreamFtp::write() does! +// => ~OutputStreamImpl() will delete the already existing file! +struct OutputStreamFtp : public AFS::OutputStreamImpl +{ + OutputStreamFtp(const FtpLogin& login, + const AfsPath& filePath, + std::optional modTime) : + login_(login), + filePath_(filePath), + modTime_(modTime) + { + std::promise promUploadDone; + futUploadDone_ = promUploadDone.get_future(); + + worker_ = InterruptibleThread([login, filePath, + asyncStreamIn = this->asyncStreamOut_, + pUploadDone = std::move(promUploadDone)]() mutable + { + setCurrentThreadName(Zstr("Ostream ") + utfTo(getCurlDisplayPath(login, filePath))); + try + { + auto readBlock = [&](void* buffer, size_t bytesToRead) + { + return asyncStreamIn->read(buffer, bytesToRead); //throw ThreadStopRequest + }; + ftpFileUpload(login, filePath, readBlock); //throw FileError, ThreadStopRequest + assert(asyncStreamIn->getTotalBytesRead() == asyncStreamIn->getTotalBytesWritten()); + + pUploadDone.set_value(); + } + catch (FileError&) + { + const std::exception_ptr exptr = std::current_exception(); + asyncStreamIn->setReadError(exptr); //set both! + pUploadDone.set_exception(exptr); // + } + //let ThreadStopRequest pass through! + }); + } + + ~OutputStreamFtp() + { + if (asyncStreamOut_) //=> cleanup non-finalized output file + { + asyncStreamOut_->setWriteError(std::make_exception_ptr(ThreadStopRequest())); + worker_.join(); + + try //see removeFilePlain() + { + accessFtpSession(login_, [&](FtpSession& session) //throw SysError + { + session.runSingleFtpCommand("DELE " + session.getServerPathInternal(filePath_), true /*requestUtf8*/); //throw SysError, SysErrorFtpProtocol + }); + } + catch (const SysError& e) + { + logExtraError(replaceCpy(_("Cannot delete file %x."), L"%x", fmtPath(getCurlDisplayPath(login_, filePath_))) + L"\n\n" + e.toString()); + } + } + } + + size_t getBlockSize() override { return FTP_BLOCK_SIZE_UPLOAD; } //throw (FileError) + + size_t tryWrite(const void* buffer, size_t bytesToWrite, const IoCallback& notifyUnbufferedIO /*throw X*/) override //throw FileError, X; may return short! CONTRACT: bytesToWrite > 0 + { + const size_t bytesWritten = asyncStreamOut_->tryWrite(buffer, bytesToWrite); //throw FileError + reportBytesProcessed(notifyUnbufferedIO); //throw X + return bytesWritten; + } + + AFS::FinalizeResult finalize(const IoCallback& notifyUnbufferedIO /*throw X*/) override //throw FileError, X + { + if (!asyncStreamOut_) + throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); + + asyncStreamOut_->closeStream(); + + while (futUploadDone_.wait_for(std::chrono::milliseconds(25)) == std::future_status::timeout) + reportBytesProcessed(notifyUnbufferedIO); //throw X + reportBytesProcessed(notifyUnbufferedIO); //[!] once more, now that *all* bytes were written + + assert(isReady(futUploadDone_)); + futUploadDone_.get(); //throw FileError + + //asyncStreamOut_->checkReadErrors(); //throw FileError -> not needed after *successful* upload + asyncStreamOut_.reset(); //output finalized => no more exceptions from here on! + //-------------------------------------------------------------------- + + AFS::FinalizeResult result; + //result.filePrint = ... -> yet unknown at this point + try + { + setModTimeIfAvailable(); //throw FileError, follows symlinks + /* is setting modtime after closing the file handle a pessimization? + FTP: no: could set modtime via CURLOPT_POSTQUOTE (but this would internally trigger an extra round-trip anyway!) */ + } + catch (const FileError& e) { result.errorModTime = e; /*might slice derived class?*/ } + + return result; + } + +private: + void reportBytesProcessed(const IoCallback& notifyUnbufferedIO /*throw X*/) //throw X + { + const int64_t bytesDelta = makeSigned(asyncStreamOut_->getTotalBytesRead()) - totalBytesReported_; + totalBytesReported_ += bytesDelta; + if (notifyUnbufferedIO) notifyUnbufferedIO(bytesDelta); //throw X + } + + void setModTimeIfAvailable() const //throw FileError, follows symlinks + { + //assert(isReady(futUploadDone_)); => MUST NOT CALL *after* std::future<>::get()! + if (modTime_) + try + { + const std::string isoTime = utfTo(formatTime(Zstr("%Y%m%d%H%M%S"), getUtcTime(*modTime_))); //returns empty string on error + if (isoTime.empty()) + throw SysError(L"Invalid modification time (time_t: " + numberTo(*modTime_) + L')'); + + accessFtpSession(login_, [&](FtpSession& session) //throw SysError + { + if (!session.supportsMfmt()) //throw SysError + throw SysError(L"Server does not support the MFMT command."); + + session.runSingleFtpCommand("MFMT " + isoTime + ' ' + session.getServerPathInternal(filePath_), + true /*requestUtf8*/); //throw SysError, SysErrorFtpProtocol + //not relevant for OutputStreamFtp, but: does MFMT follow symlinks? for Linux FTP server (using utime) it does + }); + } + catch (const SysError& e) + { + throw FileError(replaceCpy(_("Cannot write modification time of %x."), L"%x", fmtPath(getCurlDisplayPath(login_, filePath_))), e.toString()); + } + } + + const FtpLogin login_; + const AfsPath filePath_; + const std::optional modTime_; + int64_t totalBytesReported_ = 0; + std::shared_ptr asyncStreamOut_ = std::make_shared(FTP_STREAM_BUFFER_SIZE); + InterruptibleThread worker_; + std::future futUploadDone_; +}; + +//--------------------------------------------------------------------------------------------------------------------------- +//=========================================================================================================================== + +class FtpFileSystem : public AbstractFileSystem +{ +public: + explicit FtpFileSystem(const FtpLogin& login) : login_(login) {} + + const FtpLogin& getLogin() const { return login_; } + +private: + Zstring getInitPathPhrase(const AfsPath& itemPath) const override { return concatenateFtpFolderPathPhrase(login_, itemPath); } + + std::vector getPathPhraseAliases(const AfsPath& itemPath) const override { return {getInitPathPhrase(itemPath)}; } + + std::wstring getDisplayPath(const AfsPath& itemPath) const override { return getCurlDisplayPath(login_, itemPath); } + + bool isNullFileSystem() const override { return login_.server.empty(); } + + std::weak_ordering compareDeviceSameAfsType(const AbstractFileSystem& afsRhs) const override + { + const FtpLogin& lhs = login_; + const FtpLogin& rhs = static_cast(afsRhs).login_; + + return FtpDeviceId(lhs) <=> FtpDeviceId(rhs); + } + + //---------------------------------------------------------------------------------------------------------------- + + ItemType getItemType(const AfsPath& itemPath) const override //throw FileError + { + try + { + const std::optional parentPath = getParentPath(itemPath); + if (!parentPath) //device root => quick access test + { + try { accessFtpSession(login_, [](FtpSession& session) { session.testConnection(); }); /*throw SysError*/ } + catch (const SysError& e) { throw SysError(replaceCpy(_("Unable to connect to %x."), L"%x", fmtPath(login_.server)) + L'\n' + e.toString()); } + + return ItemType::folder; + } + + const std::vector items = [&] + { + try + { + //don't use MLST: broken for Pure-FTPd: https://freefilesync.org/forum/viewtopic.php?t=4287 + return FtpDirectoryReader::execute(login_, *parentPath); //throw SysError, SysErrorFtpProtocol + } + catch (const SysError& e) //add context: error might be folder-specific + { throw SysError(replaceCpy(_("Cannot read directory %x."), L"%x", fmtPath(parentPath->value.empty() ? Zstr("/") : getItemName(*parentPath))) + L'\n' + e.toString()); } + }(); + + const Zstring itemName = getItemName(itemPath); + assert(!itemName.empty()); + //is the underlying file system case-sensitive? we don't know => assume "case-sensitive" + //all path components (except the base folder part!) can be expected to have the right case anyway after directory traversal + for (const FtpItem& item : items) + if (item.itemName == itemName) + return item.type; + + throw SysError(replaceCpy(_("%x does not exist."), L"%x", fmtPath(itemName))); + } + catch (const SysError& e) + { + throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(getDisplayPath(itemPath))), e.toString()); + } + } + + std::optional getItemTypeIfExistsImpl(const AfsPath& itemPath) const //throw SysError + { + const std::optional parentPath = getParentPath(itemPath); + if (!parentPath) //device root => quick access test + { + try { accessFtpSession(login_, [](FtpSession& session) { session.testConnection(); }); /*throw SysError*/ } + catch (const SysError& e) { throw SysError(replaceCpy(_("Unable to connect to %x."), L"%x", fmtPath(login_.server)) + L'\n' + e.toString()); } + + return ItemType::folder; + } + + std::optional lastFtpError; + try + { + try + { + const Zstring itemName = getItemName(itemPath); + assert(!itemName.empty()); + + for (const FtpItem& item : FtpDirectoryReader::execute(login_, *parentPath)) //throw SysError, SysErrorFtpProtocol + if (item.itemName == itemName) //case-sensitive comparison! itemPath must be normalized! + return item.type; + + return std::nullopt; + } + catch (const SysErrorFtpProtocol& e) + { + //let's dig deeper, but *only* for SysErrorFtpProtocol, not for general connection issues + //+ check if FTP error code sounds like "not existing" + if (e.ftpErrorCode == 550) //FTP 550 No such file or directory + //501? "pathname that exists but is not a directory to a MLSD command generates a 501 reply": https://www.rfc-editor.org/rfc/rfc3659 + //=> really? cannot reproduce, getting: "550 '/filename.txt' is not a directory" or "550 Can't check for file existence" + lastFtpError = e; //-> get out of catch clause + else + throw; + } + } + catch (const SysError& e) //add context: error might be folder-specific + { throw SysError(replaceCpy(_("Cannot read directory %x."), L"%x", fmtPath(parentPath->value.empty() ? Zstr("/") : getItemName(*parentPath))) + L'\n' + e.toString()); } + + //---------------------------------------------------------------- + if (const std::optional parentType = getItemTypeIfExistsImpl(*parentPath)) //throw SysError + { + if (*parentType == ItemType::file /*obscure, but possible*/) + throw SysError(replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(getItemName(*parentPath)))); + + throw* lastFtpError; //throw SysError; parent path existing, so traversal should not have failed! + } + else + return std::nullopt; + } + + std::optional getItemTypeIfExists(const AfsPath& itemPath) const override //throw FileError + { + try + { + return getItemTypeIfExistsImpl(itemPath); //throw SysError + } + catch (const SysError& e) + { + throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(getDisplayPath(itemPath))), e.toString()); + } + } + + //---------------------------------------------------------------------------------------------------------------- + //already existing: fail + //=> FTP will (most likely) fail and give a clear error message: + // freefilesync.org: "550 Can't create directory: File exists" + // FileZilla Server: "550 Directory already exists" + // Windows IIS: "550 Cannot create a file when that file already exists" + void createFolderPlain(const AfsPath& folderPath) const override //throw FileError + { + try + { + accessFtpSession(login_, [&](FtpSession& session) //throw SysError + { + session.runSingleFtpCommand("MKD " + session.getServerPathInternal(folderPath), true /*requestUtf8*/); //throw SysError, SysErrorFtpProtocol + }); + } + catch (const SysError& e) + { + throw FileError(replaceCpy(_("Cannot create directory %x."), L"%x", fmtPath(getDisplayPath(folderPath))), e.toString()); + } + } + + void removeFilePlain(const AfsPath& filePath) const override //throw FileError + { + try + { + accessFtpSession(login_, [&](FtpSession& session) //throw SysError + { + session.runSingleFtpCommand("DELE " + session.getServerPathInternal(filePath), true /*requestUtf8*/); //throw SysError, SysErrorFtpProtocol + }); + } + catch (const SysError& e) + { + throw FileError(replaceCpy(_("Cannot delete file %x."), L"%x", fmtPath(getDisplayPath(filePath))), e.toString()); + } + } + + void removeSymlinkPlain(const AfsPath& linkPath) const override //throw FileError + { + try + { + accessFtpSession(login_, [&](FtpSession& session) //throw SysError + { + //works fine for Linux hosts, but what about Windows-hosted FTP??? Distinguish DELE/RMD? + //Windows test, FileZilla Server and Windows IIS FTP: all symlinks are reported as regular folders + session.runSingleFtpCommand("DELE " + session.getServerPathInternal(linkPath), true /*requestUtf8*/); //throw SysError, SysErrorFtpProtocol + }); + } + catch (const SysError& e) + { + throw FileError(replaceCpy(_("Cannot delete symbolic link %x."), L"%x", fmtPath(getDisplayPath(linkPath))), e.toString()); + } + } + + void removeFolderPlain(const AfsPath& folderPath) const override //throw FileError + { + try + { + accessFtpSession(login_, [&](FtpSession& session) //throw SysError + { + //Windows server: FileZilla Server and Windows IIS FTP: all symlinks are reported as regular folders + //Linux server (freefilesync.org): RMD will fail for symlinks! + session.runSingleFtpCommand("RMD " + session.getServerPathInternal(folderPath), true /*requestUtf8*/); //throw SysError, SysErrorFtpProtocol + }); + } + catch (const SysError& e) + { + throw FileError(replaceCpy(_("Cannot delete directory %x."), L"%x", fmtPath(getDisplayPath(folderPath))), e.toString()); + } + } + + void removeFolderIfExistsRecursion(const AfsPath& folderPath, //throw FileError + const std::function& onBeforeFileDeletion /*throw X*/, + const std::function& onBeforeSymlinkDeletion/*throw X*/, + const std::function& onBeforeFolderDeletion /*throw X*/) const override + { + //default implementation: folder traversal + AFS::removeFolderIfExistsRecursion(folderPath, onBeforeFileDeletion, onBeforeSymlinkDeletion, onBeforeFolderDeletion); //throw FileError, X + } + + //---------------------------------------------------------------------------------------------------------------- + AbstractPath getSymlinkResolvedPath(const AfsPath& linkPath) const override //throw FileError + { + throw FileError(replaceCpy(_("Cannot determine final path for %x."), L"%x", fmtPath(getDisplayPath(linkPath))), _("Operation not supported by device.")); + } + + bool equalSymlinkContentForSameAfsType(const AfsPath& linkPathL, const AbstractPath& linkPathR) const override //throw FileError + { + throw FileError(replaceCpy(_("Cannot resolve symbolic link %x."), L"%x", fmtPath(getDisplayPath(linkPathL))), _("Operation not supported by device.")); + } + //---------------------------------------------------------------------------------------------------------------- + + //return value always bound: + std::unique_ptr getInputStream(const AfsPath& filePath) const override //throw FileError, (ErrorFileLocked) + { + return std::make_unique(login_, filePath); + } + + //already existing: undefined behavior! (e.g. fail/overwrite/auto-rename) + //=> actual behavior: fail(+delete!)/overwrite/auto-rename + std::unique_ptr getOutputStream(const AfsPath& filePath, //throw FileError + std::optional streamSize, + std::optional modTime) const override + { + /* most FTP servers overwrite, but some (e.g. IIS) can be configured to fail, others (pureFTP) can be configured to auto-rename: + https://download.pureftpd.org/pub/pure-ftpd/doc/README + '-r': Never overwrite existing files. Uploading a file whose name already exists causes an automatic rename. Files are called xyz, xyz.1, xyz.2, xyz.3, etc. */ + + //already existing: fail (+ delete!!!) + return std::make_unique(login_, filePath, modTime); + } + + //---------------------------------------------------------------------------------------------------------------- + void traverseFolderRecursive(const TraverserWorkload& workload /*throw X*/, size_t parallelOps) const override + { + traverseFolderRecursiveFTP(login_, workload, parallelOps); //throw X + } + //---------------------------------------------------------------------------------------------------------------- + + //symlink handling: follow + //already existing: undefined behavior! (e.g. fail/overwrite/auto-rename) + FileCopyResult copyFileForSameAfsType(const AfsPath& sourcePath, const StreamAttributes& attrSource, //throw FileError, (ErrorFileLocked), X + const AbstractPath& targetPath, bool copyFilePermissions, const IoCallback& notifyUnbufferedIO /*throw X*/) const override + { + //no native FTP file copy => use stream-based file copy: + if (copyFilePermissions) + throw FileError(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(AFS::getDisplayPath(targetPath))), _("Operation not supported by device.")); + + //already existing: undefined behavior! (e.g. fail/overwrite/auto-rename) + return copyFileAsStream(sourcePath, attrSource, targetPath, notifyUnbufferedIO); //throw FileError, (ErrorFileLocked), X + } + + //symlink handling: follow + //already existing: fail + void copyNewFolderForSameAfsType(const AfsPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions) const override //throw FileError + { + //already existing: fail + AFS::createFolderPlain(targetPath); //throw FileError + + if (copyFilePermissions) + throw FileError(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(AFS::getDisplayPath(targetPath))), _("Operation not supported by device.")); + } + + //already existing: fail + void copySymlinkForSameAfsType(const AfsPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions) const override + { + throw FileError(replaceCpy(replaceCpy(_("Cannot copy symbolic link %x to %y."), + L"%x", L'\n' + fmtPath(getDisplayPath(sourcePath))), + L"%y", L'\n' + fmtPath(AFS::getDisplayPath(targetPath))), _("Operation not supported by device.")); + } + + //already existing: undefined behavior! (e.g. fail/overwrite) + //=> actual behavior: most linux-based FTP servers overwrite, Windows-based servers fail (but most can be configured to behave differently) + // freefilesync.org: silent overwrite + // Windows IIS: CURLE_QUOTE_ERROR: QUOT command failed with 550 Cannot create a file when that file already exists. + // FileZilla Server: CURLE_QUOTE_ERROR: QUOT command failed with 553 file exists + void moveAndRenameItemForSameAfsType(const AfsPath& pathFrom, const AbstractPath& pathTo) const override //throw FileError, ErrorMoveUnsupported + { + if (compareDeviceSameAfsType(pathTo.afsDevice.ref()) != std::weak_ordering::equivalent) + throw ErrorMoveUnsupported(generateMoveErrorMsg(pathFrom, pathTo), _("Operation not supported between different devices.")); + + try + { + accessFtpSession(login_, [&](FtpSession& session) //throw SysError + { + curl_slist* quote = nullptr; + ZEN_ON_SCOPE_EXIT(::curl_slist_free_all(quote)); + quote = ::curl_slist_append(quote, ("RNFR " + session.getServerPathInternal(pathFrom )).c_str()); //throw SysError + quote = ::curl_slist_append(quote, ("RNTO " + session.getServerPathInternal(pathTo.afsPath)).c_str()); // + + session.perform(AfsPath(), true /*isDir*/, CURLFTPMETHOD_NOCWD, //avoid needless CWDs + { + {CURLOPT_NOBODY, 1L}, + {CURLOPT_QUOTE, quote}, + }, true /*requestUtf8*/); //throw SysError, SysErrorPassword, SysErrorFtpProtocol + }); + } + catch (const SysError& e) + { + throw FileError(generateMoveErrorMsg(pathFrom, pathTo), e.toString()); + } + } + + bool supportsPermissions(const AfsPath& folderPath) const override { return false; } //throw FileError + //wait until there is real demand for copying from and to FTP with permissions => use stream-based file copy: + + //---------------------------------------------------------------------------------------------------------------- + FileIconHolder getFileIcon (const AfsPath& filePath, int pixelSize) const override { return {}; } //throw FileError; optional return value + ImageHolder getThumbnailImage(const AfsPath& filePath, int pixelSize) const override { return {}; } //throw FileError; optional return value + + void authenticateAccess(const RequestPasswordFun& requestPassword /*throw X*/) const override //throw FileError, X + { + auto connectServer = [&] //throw SysError, SysErrorPassword + { + accessFtpSession(login_, [](FtpSession& session) //connect with FTP server, *unless* already connected (in which case *nothing* is sent) + { + session.perform(AfsPath(), true /*isDir*/, CURLFTPMETHOD_NOCWD, + {{CURLOPT_NOBODY, 1L}, {CURLOPT_SERVER_RESPONSE_TIMEOUT, 0}}, false /*requestUtf8*/); + //caveat: connection phase only, so disable CURLOPT_SERVER_RESPONSE_TIMEOUT, or next access may fail with CURLE_OPERATION_TIMEDOUT! + }); //throw SysError, SysErrorPassword, SysErrorFtpProtocol + }; + + try + { + const std::shared_ptr mgr = globalFtpSessionManager.get(); + if (!mgr) + throw SysError(formatSystemError("getSessionPassword", L"", L"Function call not allowed during init/shutdown.")); + + mgr->setActiveConfig(login_); + + if (!login_.password) + { + try //1. test for connection error *before* bothering user to enter a password + { + connectServer(); //throw SysError, SysErrorPassword + return; //got new FtpSession (connected in constructor) or already connected session from cache + } + catch (const SysErrorPassword& e) + { + if (!requestPassword) + throw SysError(e.toString() + L'\n' + _("Password prompt not permitted by current settings.")); + } + + std::wstring lastErrorMsg; + for (;;) + { + //2. request (new) password + std::wstring msg = replaceCpy(_("Please enter your password to connect to %x."), L"%x", fmtPath(getDisplayPath(AfsPath()))); + if (lastErrorMsg.empty()) + msg += L"\n" + _("The password will only be remembered until FreeFileSync is closed."); + + const Zstring password = requestPassword(msg, lastErrorMsg); //throw X + mgr->setSessionPassword(login_, password); + + try //3. test access: + { + connectServer(); //throw SysError, SysErrorPassword + return; + } + catch (const SysErrorPassword& e) { lastErrorMsg = e.toString(); } + } + } + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Unable to connect to %x."), L"%x", fmtPath(getDisplayPath(AfsPath()))), e.toString()); } + } + + bool hasNativeTransactionalCopy() const override { return false; } + //---------------------------------------------------------------------------------------------------------------- + + int64_t getFreeDiskSpace(const AfsPath& folderPath) const override { return -1; } //throw FileError, returns < 0 if not available + + std::unique_ptr createRecyclerSession(const AfsPath& folderPath) const override //throw FileError, RecycleBinUnavailable + { + throw RecycleBinUnavailable(replaceCpy(_("The recycle bin is not available for %x."), L"%x", fmtPath(getDisplayPath(folderPath)))); + } + + void moveToRecycleBin(const AfsPath& itemPath) const override //throw FileError, RecycleBinUnavailable + { + throw RecycleBinUnavailable(replaceCpy(_("The recycle bin is not available for %x."), L"%x", fmtPath(getDisplayPath(itemPath)))); + } + + const FtpLogin login_; +}; + +//=========================================================================================================================== + +//expects "clean" login data +Zstring concatenateFtpFolderPathPhrase(const FtpLogin& login, const AfsPath& folderPath) //noexcept +{ + Zstring username; + if (!login.username.empty()) + username = encodeFtpUsername(login.username) + Zstr("@"); + + Zstring server = login.server; + if (parseIpv6Address(server) && login.portCfg > 0) + server = Zstr('[') + server + Zstr(']'); //e.g. [::1]:80 + + Zstring port; + if (login.portCfg > 0) + port = Zstr(':') + numberTo(login.portCfg); + + Zstring relPath = getServerRelPath(folderPath); + if (relPath == Zstr("/")) + relPath.clear(); + + Zstring options; + if (login.timeoutSec != FtpLogin().timeoutSec) + options += Zstr("|timeout=") + numberTo(login.timeoutSec); + + if (login.useTls) + options += Zstr("|ssl"); + + if (login.password) + { + if (!login.password->empty()) //password always last => visually truncated by folder input field + options += Zstr("|pass64=") + encodePasswordBase64(*login.password); + } + else + options += Zstr("|pwprompt"); + + return Zstring(ftpPrefix) + Zstr("//") + username + server + port + relPath + options; +} +} + + +void fff::ftpInit() +{ + assert(!globalFtpSessionManager.get()); + globalFtpSessionManager.set(std::make_unique()); +} + + +void fff::ftpTeardown() +{ + assert(globalFtpSessionManager.get()); + globalFtpSessionManager.set(nullptr); +} + + +AfsPath fff::getFtpHomePath(const FtpLogin& login) //throw FileError +{ + try + { + AfsPath homePath; + + accessFtpSession(login, [&](FtpSession& session) //throw SysError + { + homePath = session.getHomePath(); //throw SysError + }); + return homePath; + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot determine final path for %x."), L"%x", fmtPath(getCurlDisplayPath(login, AfsPath(Zstr("~"))))), e.toString()); } +} + + +AfsDevice fff::condenseToFtpDevice(const FtpLogin& login) //noexcept +{ + //clean up input: + FtpLogin loginTmp = login; + trim(loginTmp.server); + trim(loginTmp.username); + + loginTmp.timeoutSec = std::max(1, loginTmp.timeoutSec); + + if (startsWithAsciiNoCase(loginTmp.server, "http:" ) || + startsWithAsciiNoCase(loginTmp.server, "https:") || + startsWithAsciiNoCase(loginTmp.server, "ftp:" ) || + startsWithAsciiNoCase(loginTmp.server, "ftps:" ) || + startsWithAsciiNoCase(loginTmp.server, "sftp:" )) + loginTmp.server = afterFirst(loginTmp.server, Zstr(':'), IfNotFoundReturn::none); + trim(loginTmp.server, TrimSide::both, [](Zchar c) { return c == Zstr('/') || c == Zstr('\\'); }); + + if (std::optional> ip6AndPort = parseIpv6Address(loginTmp.server)) + loginTmp.server = ip6AndPort->first; //remove leading/trailing brackets + + return makeSharedRef(loginTmp); +} + + +FtpLogin fff::extractFtpLogin(const AfsDevice& afsDevice) //noexcept +{ + if (const auto ftpDevice = dynamic_cast(&afsDevice.ref())) + return ftpDevice->getLogin(); + + assert(false); + return {}; +} + + +bool fff::acceptsItemPathPhraseFtp(const Zstring& itemPathPhrase) //noexcept +{ + Zstring path = expandMacros(itemPathPhrase); //expand before trimming! + trim(path); + return startsWithAsciiNoCase(path, ftpPrefix); //check for explicit FTP path +} + + +/* syntax: ftp://[[:]@][:port]/[|option_name=value] + + e.g. ftp://user001:secretpassword@private.example.com:222/mydirectory/ + ftp://user001:secretpassword@[::1]:80/ipv6folder/ + ftp://user001:secretpassword@::1/ipv6withoutPort/ + ftp://user001@private.example.com/mydirectory|pass64=c2VjcmV0cGFzc3dvcmQ */ +AbstractPath fff::createItemPathFtp(const Zstring& itemPathPhrase) //noexcept +{ + Zstring pathPhrase = expandMacros(itemPathPhrase); //expand before trimming! + trim(pathPhrase); + + if (startsWithAsciiNoCase(pathPhrase, ftpPrefix)) + pathPhrase = pathPhrase.c_str() + strLength(ftpPrefix); + trim(pathPhrase, TrimSide::left, [](Zchar c) { return c == Zstr('/') || c == Zstr('\\'); }); + + const ZstringView credentials = beforeFirst(pathPhrase, Zstr('@'), IfNotFoundReturn::none); + const ZstringView fullPathOpt = afterFirst(pathPhrase, Zstr('@'), IfNotFoundReturn::all); + + FtpLogin login; + login.username = decodeFtpUsername(Zstring(beforeFirst(credentials, Zstr(':'), IfNotFoundReturn::all))); //support standard FTP syntax, even though + login.password = Zstring( afterFirst(credentials, Zstr(':'), IfNotFoundReturn::none)); //concatenateFtpFolderPathPhrase() uses "pass64" instead + + const ZstringView fullPath = beforeFirst(fullPathOpt, Zstr('|'), IfNotFoundReturn::all); + const ZstringView options = afterFirst(fullPathOpt, Zstr('|'), IfNotFoundReturn::none); + + auto it = std::find_if(fullPath.begin(), fullPath.end(), [](Zchar c) { return c == '/' || c == '\\'; }); + const ZstringView serverPort = makeStringView(fullPath.begin(), it); + const AfsPath serverRelPath = sanitizeDeviceRelativePath({it, fullPath.end()}); + + if (std::optional> ip6AndPort = parseIpv6Address(serverPort)) //e.g. 2001:db8::ff00:42:8329 or [::1]:80 + { + login.server = ip6AndPort->first; + login.portCfg = ip6AndPort->second; //0 if empty + } + else + { + login.server = Zstring(beforeLast(serverPort, Zstr(':'), IfNotFoundReturn::all)); + const ZstringView port = afterLast(serverPort, Zstr(':'), IfNotFoundReturn::none); + login.portCfg = stringTo(port); //0 if empty + } + + split(options, Zstr('|'), [&](ZstringView optPhrase) + { + optPhrase = trimCpy(optPhrase); + if (!optPhrase.empty()) + { + if (startsWith(optPhrase, Zstr("timeout="))) + login.timeoutSec = stringTo(afterFirst(optPhrase, Zstr('='), IfNotFoundReturn::none)); + else if (optPhrase == Zstr("ssl")) + login.useTls = true; + else if (startsWith(optPhrase, Zstr("pass64="))) + login.password = decodePasswordBase64(afterFirst(optPhrase, Zstr('='), IfNotFoundReturn::none)); + else if (optPhrase == Zstr("pwprompt")) + login.password = std::nullopt; + else + assert(false); + } + }); + return AbstractPath(makeSharedRef(login), serverRelPath); +} diff --git a/FreeFileSync/Source/afs/ftp.h b/FreeFileSync/Source/afs/ftp.h new file mode 100644 index 0000000..f8df01f --- /dev/null +++ b/FreeFileSync/Source/afs/ftp.h @@ -0,0 +1,41 @@ +// ***************************************************************************** +// * 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 FTP_H_745895742383425326568678 +#define FTP_H_745895742383425326568678 + +#include "abstract.h" + + +namespace fff +{ +bool acceptsItemPathPhraseFtp(const Zstring& itemPathPhrase); //noexcept +AbstractPath createItemPathFtp(const Zstring& itemPathPhrase); //noexcept + +void ftpInit(); +void ftpTeardown(); + +//------------------------------------------------------- + +const int DEFAULT_PORT_FTP = 21; //TLS enabled? => same for explicit FTP, but *implicit* FTP uses port 990 + +struct FtpLogin +{ + Zstring server; + int portCfg = 0; //use if > 0, DEFAULT_PORT_FTP otherwise + Zstring username; + std::optional password = Zstr(""); //none given => prompt during AFS::authenticateAccess() + bool useTls = false; + //other settings not specific to FTP session: + int timeoutSec = 10; +}; +AfsDevice condenseToFtpDevice(const FtpLogin& login); //noexcept; potentially messy user input +FtpLogin extractFtpLogin(const AfsDevice& afsDevice); //noexcept + +AfsPath getFtpHomePath(const FtpLogin& login); //throw FileError +} + +#endif //FTP_H_745895742383425326568678 diff --git a/FreeFileSync/Source/afs/ftp_common.h b/FreeFileSync/Source/afs/ftp_common.h new file mode 100644 index 0000000..5df0054 --- /dev/null +++ b/FreeFileSync/Source/afs/ftp_common.h @@ -0,0 +1,113 @@ +// ***************************************************************************** +// * 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 FTP_COMMON_H_92889457091324321454 +#define FTP_COMMON_H_92889457091324321454 + +#include +#include +#include "abstract.h" + + +namespace fff +{ +inline +Zstring encodePasswordBase64(const ZstringView pass) +{ + using namespace zen; + return utfTo(stringEncodeBase64(utfTo(pass))); //nothrow +} + + +inline +Zstring decodePasswordBase64(const ZstringView pass) +{ + using namespace zen; + return utfTo(stringDecodeBase64(utfTo(pass))); //nothrow +} + + +//according to the SFTP path syntax, the username must not contain raw @ and : +//-> we don't need a full urlencode! +inline +Zstring encodeFtpUsername(Zstring name) +{ + using namespace zen; + replace(name, Zstr('%'), Zstr("%25")); //first! + replace(name, Zstr('@'), Zstr("%40")); + replace(name, Zstr(':'), Zstr("%3A")); + return name; +} + + +inline +Zstring decodeFtpUsername(Zstring name) +{ + using namespace zen; + replace(name, Zstr("%40"), Zstr('@')); + replace(name, Zstr("%3A"), Zstr(':')); + replace(name, Zstr("%3a"), Zstr(':')); + replace(name, Zstr("%25"), Zstr('%')); //last! + return name; +} + + +inline +std::optional> parseIpv6Address(ZstringView str) +{ + using namespace zen; + + str = trimCpy(str); + + int port = 0; + + //https://en.wikipedia.org/wiki/IPv6#Address_representation + if (startsWith(str, Zstr('['))) + { + str = str.substr(1); + if (!contains(str, Zstr(']'))) + return std::nullopt; + + ZstringView portStr = afterLast (str, Zstr(']'), IfNotFoundReturn::none); + str = beforeLast(str, Zstr(']'), IfNotFoundReturn::none); + + if (!portStr.empty()) + { + if (!startsWith(portStr, Zstr(':'))) + return std::nullopt; + portStr = portStr.substr(1); + + if (!std::all_of(portStr.begin(), portStr.end(), &isDigit)) + return std::nullopt; + + port = stringTo(portStr); //valid range: [0, 65535] + } + } + + if (!contains(str, Zstr(':')) || + !std::all_of(str.begin(), str.end(), [](Zchar c) +{ + return isHexDigit(c) || c == Zstr(':'); + })) + return std::nullopt; + + return std::make_pair(Zstring(str), port); +} + + +//(S)FTP path relative to server root using Unix path separators and with leading slash +inline +Zstring getServerRelPath(const AfsPath& itemPath) +{ + using namespace zen; + if constexpr (FILE_NAME_SEPARATOR != Zstr('/' )) + return Zstr('/') + replaceCpy(itemPath.value, FILE_NAME_SEPARATOR, Zstr('/')); + else + return Zstr('/') + itemPath.value; +} +} + +#endif //FTP_COMMON_H_92889457091324321454 diff --git a/FreeFileSync/Source/afs/gdrive.cpp b/FreeFileSync/Source/afs/gdrive.cpp new file mode 100644 index 0000000..8cc23f0 --- /dev/null +++ b/FreeFileSync/Source/afs/gdrive.cpp @@ -0,0 +1,4118 @@ +// ***************************************************************************** +// * 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 "gdrive.h" +#include +#include //needed by clang +#include // +#include //DON'T include directly! +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "abstract_impl.h" +#include "init_curl_libssh2.h" + #include + +using namespace zen; +using namespace fff; +using AFS = AbstractFileSystem; + + +namespace fff +{ +struct GdrivePath +{ + GdriveLogin gdriveLogin; + AfsPath itemPath; //path relative to drive root +}; + +struct GdriveRawPath +{ + std::string parentId; //Google Drive item IDs are *globally* unique! + Zstring itemName; +}; +inline +std::weak_ordering operator<=>(const GdriveRawPath& lhs, const GdriveRawPath& rhs) +{ + if (const std::strong_ordering cmp = lhs.parentId <=> rhs.parentId; + cmp != std::strong_ordering::equal) + return cmp; + + return compareNativePath(lhs.itemName, rhs.itemName); +} + +constinit Global> globalGdrivePathAccessLocker; +GLOBAL_RUN_ONCE(globalGdrivePathAccessLocker.set(std::make_unique>())); + +template <> std::shared_ptr> PathAccessLocker::getGlobalInstance() { return globalGdrivePathAccessLocker.get(); } +template <> Zstring PathAccessLocker::getItemName(const GdriveRawPath& nativePath) { return nativePath.itemName; } + +using PathAccessLock = PathAccessLocker::Lock; //throw SysError +using PathBlockType = PathAccessLocker::BlockType; +} + + +namespace +{ +//Google Drive REST API Overview: https://developers.google.com/drive/api/v3/about-sdk +//Google Drive REST API Reference: https://developers.google.com/drive/api/v3/reference +const Zchar* GOOGLE_REST_API_SERVER = Zstr("www.googleapis.com"); + +constexpr std::chrono::seconds HTTP_SESSION_MAX_IDLE_TIME (20); +constexpr std::chrono::seconds HTTP_SESSION_CLEANUP_INTERVAL(4); +constexpr std::chrono::seconds GDRIVE_SYNC_INTERVAL (5); + +const size_t GDRIVE_BLOCK_SIZE_DOWNLOAD = 64 * 1024; //libcurl returns blocks of only 16 kB as returned by recv() even if we request larger blocks via CURLOPT_BUFFERSIZE +const size_t GDRIVE_BLOCK_SIZE_UPLOAD = 64 * 1024; //libcurl requests blocks of 64 kB. larger blocksizes set via CURLOPT_UPLOAD_BUFFERSIZE do not seem to make a difference +const size_t GDRIVE_STREAM_BUFFER_SIZE = 1024 * 1024; //unit: [byte] +//stream buffer should be big enough to facilitate prefetching during alternating read/write operations => e.g. see serialize.h::unbufferedStreamCopy() + +constexpr ZstringView gdrivePrefix = Zstr("gdrive:"); +const char gdriveFolderMimeType [] = "application/vnd.google-apps.folder"; +const char gdriveShortcutMimeType[] = "application/vnd.google-apps.shortcut"; //= symbolic link! + +const char DB_FILE_DESCR[] = "FreeFileSync"; +const int DB_FILE_VERSION = 5; //2021-05-15 + +std::string getGdriveClientId () { return ""; } // => replace with live credentials +std::string getGdriveClientSecret() { return ""; } // + + + + +struct HttpSessionId +{ + explicit HttpSessionId(const Zstring& serverName) : + server(serverName) {} + + Zstring server; +}; + +inline +bool operator==(const HttpSessionId& lhs, const HttpSessionId& rhs) { return equalAsciiNoCase(lhs.server, rhs.server); } +} + +//exactly the type of case insensitive comparison we need for server names! +//https://docs.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfow#IDNs +template<> struct std::hash { size_t operator()(const HttpSessionId& sessionId) const { return StringHashAsciiNoCase()(sessionId.server); } }; + + +namespace +{ +Zstring concatenateGdriveFolderPathPhrase(const GdrivePath& gdrivePath); //noexcept + + +//e.g.: gdrive:/john@gmail.com:SharedDrive/folder/file.txt +std::wstring getGdriveDisplayPath(const GdrivePath& gdrivePath) +{ + Zstring displayPath = Zstring(gdrivePrefix) + FILE_NAME_SEPARATOR; + + displayPath += utfTo(gdrivePath.gdriveLogin.email); + + if (!gdrivePath.gdriveLogin.locationName.empty()) + displayPath += Zstr(':') + gdrivePath.gdriveLogin.locationName; + + if (!gdrivePath.itemPath.value.empty()) + displayPath += FILE_NAME_SEPARATOR + gdrivePath.itemPath.value; + + return utfTo(displayPath); +} + + +std::wstring formatGdriveErrorRaw(std::string serverResponse) +{ + /* e.g.: { "error": { "errors": [{ "domain": "global", + "reason": "invalidSharingRequest", + "message": "Bad Request. User message: \"ACL change not allowed.\"" }], + "code": 400, + "message": "Bad Request" }} + + or: { "error": "invalid_client", + "error_description": "Unauthorized" } + + or merely: { "error": "invalid_token" } */ + trim(serverResponse); + + assert(!serverResponse.empty()); + if (serverResponse.empty()) + return L"<" + _("empty") + L">"; //at least give some indication + + try + { + const JsonValue jresponse = parseJson(serverResponse); //throw JsonParsingError + + if (const JsonValue* error = getChildFromJsonObject(jresponse, "error")) + { + if (error->type == JsonValue::Type::string) + return utfTo(error->primVal); + //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 (message->type == JsonValue::Type::string) + return utfTo(message->primVal); + } + } + catch (JsonParsingError&) {} //not JSON? + + return utfTo(serverResponse); +} + + +AFS::FingerPrint getGdriveFilePrint(const std::string& itemId) +{ + assert(!itemId.empty()); + //Google Drive item ID is persistent and globally unique! :) + return hashString(itemId); +} + +//---------------------------------------------------------------------------------------------------------------- + +constinit Global httpSessionCount; +GLOBAL_RUN_ONCE(httpSessionCount.set(createUniSessionCounter())); +UniInitializer globalInitHttp(*httpSessionCount.get()); + +//---------------------------------------------------------------------------------------------------------------- + +class HttpSessionManager //reuse (healthy) HTTP sessions globally +{ +public: + explicit HttpSessionManager(const Zstring& caCertFilePath) : + caCertFilePath_(caCertFilePath), + sessionCleaner_([this] + { + setCurrentThreadName(Zstr("Session Cleaner[HTTP]")); + runGlobalSessionCleanUp(); //throw ThreadStopRequest + }) {} + + void access(const HttpSessionId& sessionId, const std::function& useHttpSession /*throw X*/) //throw SysError, X + { + Protected& sessionCache = getSessionCache(sessionId); + + std::unique_ptr httpSession; + + sessionCache.access([&](HttpSessionManager::HttpSessionCache& sessions) + { + //assume "isHealthy()" to avoid hitting server connection limits: (clean up of !isHealthy() after use, idle sessions via worker thread) + if (!sessions.empty()) + { + httpSession = std::move(sessions.back ()); + /**/ sessions.pop_back(); + } + }); + + //create new HTTP session outside the lock: 1. don't block other threads 2. non-atomic regarding "sessionCache"! => one session too many is not a problem! + if (!httpSession) + httpSession = std::make_unique(sessionId.server, caCertFilePath_); //throw SysError + + ZEN_ON_SCOPE_EXIT( + if (isHealthy(httpSession->session)) //thread that created the "!isHealthy()" session is responsible for clean up (avoid hitting server connection limits!) + sessionCache.access([&](HttpSessionManager::HttpSessionCache& sessions) { sessions.push_back(std::move(httpSession)); }); ); + + useHttpSession(httpSession->session); //throw X + } + +private: + HttpSessionManager (const HttpSessionManager&) = delete; + HttpSessionManager& operator=(const HttpSessionManager&) = delete; + + //associate session counting (for initialization/teardown) + struct HttpInitSession + { + HttpInitSession(const Zstring& server, const Zstring& caCertFilePath) : + session(server, true /*useTls*/, caCertFilePath) {} + + const std::shared_ptr cookie{getLibsshCurlUnifiedInitCookie(httpSessionCount)}; //throw SysError + HttpSession session; //life time must be subset of UniCounterCookie + }; + static bool isHealthy(const HttpSession& s) { return std::chrono::steady_clock::now() - s.getLastUseTime() <= HTTP_SESSION_MAX_IDLE_TIME; } + + using HttpSessionCache = std::vector>; + + Protected& getSessionCache(const HttpSessionId& sessionId) + { + //single global session store per sessionId; life-time bound to globalInstance => never remove a sessionCache!!! + Protected* sessionCache = nullptr; + + globalSessionCache_.access([&](GlobalHttpSessions& sessionsById) + { + sessionCache = &sessionsById[sessionId]; //get or create + }); + static_assert(std::is_same_v>>, "require std::unordered_map so that the pointers we return remain stable"); + + return *sessionCache; + } + + //run a dedicated clean-up thread => it's unclear when the server let's a connection time out, so we do it preemptively + //context of worker thread: + void runGlobalSessionCleanUp() //throw ThreadStopRequest + { + std::chrono::steady_clock::time_point lastCleanupTime; + for (;;) + { + const auto now = std::chrono::steady_clock::now(); + + if (now < lastCleanupTime + HTTP_SESSION_CLEANUP_INTERVAL) + interruptibleSleep(lastCleanupTime + HTTP_SESSION_CLEANUP_INTERVAL - now); //throw ThreadStopRequest + + lastCleanupTime = std::chrono::steady_clock::now(); + + std::vector*> sessionCaches; //pointers remain stable, thanks to std::unordered_map<> + + globalSessionCache_.access([&](GlobalHttpSessions& sessionsByCfg) + { + for (auto& [sessionCfg, idleSession] : sessionsByCfg) + sessionCaches.push_back(&idleSession); + }); + + for (Protected* sessionCache : sessionCaches) + for (;;) + { + bool done = false; + sessionCache->access([&](HttpSessionCache& sessions) + { + for (std::unique_ptr& 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! + return; //don't hold lock for too long: delete only one session at a time, then yield... + } + done = true; + }); + if (done) + break; + std::this_thread::yield(); + } + } + } + + using GlobalHttpSessions = std::unordered_map>; + + Protected globalSessionCache_; + const Zstring caCertFilePath_; + InterruptibleThread sessionCleaner_; +}; + +//-------------------------------------------------------------------------------------- +constinit Global globalHttpSessionManager; //caveat: life time must be subset of static UniInitializer! +//-------------------------------------------------------------------------------------- + +struct GdriveAccess +{ + std::string token; + int timeoutSec = 0; +}; + +//=========================================================================================================================== + +HttpSession::Result googleHttpsRequest(const Zstring& serverName, const std::string& serverRelPath, //throw SysError, X + const std::vector& extraHeaders, + std::vector extraOptions, + const std::function buf)>& writeResponse /*throw X*/, //optional + const std::function buf)>& readRequest /*throw X*/, //optional; return "bytesToRead" bytes unless end of stream! + const std::function& receiveHeader /*throw X*/, //optional + int timeoutSec) +{ + //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, ["gzip" automatically set by HttpSession] + extraOptions.emplace_back(CURLOPT_USERAGENT, "FreeFileSync (gzip)"); //and modify your user agent to contain the string gzip." + + const std::shared_ptr mgr = globalHttpSessionManager.get(); + if (!mgr) + throw SysError(formatSystemError("googleHttpsRequest", L"", L"Function call not allowed during init/shutdown.")); + + HttpSession::Result httpResult; + + mgr->access(HttpSessionId(serverName), [&](HttpSession& session) //throw SysError + { + httpResult = session.perform(serverRelPath, extraHeaders, extraOptions, writeResponse, readRequest, receiveHeader, timeoutSec); //throw SysError, X + }); + return httpResult; +} + + +//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::Result gdriveHttpsRequest(const std::string& serverRelPath, //throw SysError, X + std::vector extraHeaders, + const std::vector& extraOptions, + const std::function buf)>& writeResponse /*throw X*/, //optional + const std::function buf)>& readRequest /*throw X*/, //optional; return "bytesToRead" bytes unless end of stream! + const std::function& receiveHeader /*throw X*/, //optional + const GdriveAccess& access) +{ + extraHeaders.push_back("Authorization: Bearer " + access.token); + + return googleHttpsRequest(GOOGLE_REST_API_SERVER, serverRelPath, + extraHeaders, + extraOptions, + writeResponse /*throw X*/, + readRequest /*throw X*/, + receiveHeader /*throw X*/, access.timeoutSec); //throw SysError, X +} + +//======================================================================================================== + +struct GdriveUser +{ + std::wstring displayName; + std::string email; +}; +GdriveUser getGdriveUser(const GdriveAccess& access) //throw SysError +{ + //https://developers.google.com/drive/api/v3/reference/about + const std::string& queryParams = xWwwFormUrlEncode( + { + {"fields", "user/displayName,user/emailAddress"}, + }); + std::string response; + gdriveHttpsRequest("/drive/v3/about?" + queryParams, {} /*extraHeaders*/, {} /*extraOptions*/, + [&](std::span buf) { response.append(buf.data(), buf.size()); }, nullptr /*readRequest*/, nullptr /*receiveHeader*/, access); //throw SysError + + JsonValue jresponse; + try { jresponse = parseJson(response); } + catch (JsonParsingError&) {} + + if (const JsonValue* user = getChildFromJsonObject(jresponse, "user")) + { + const std::optional displayName = getPrimitiveFromJsonObject(*user, "displayName"); + const std::optional email = getPrimitiveFromJsonObject(*user, "emailAddress"); + if (displayName && email) + return {utfTo(*displayName), *email}; + } + + throw SysError(formatGdriveErrorRaw(response)); +} + + +struct GdriveAuthCode +{ + std::string code; + std::string redirectUrl; + std::string codeChallenge; +}; + +struct GdriveAccessToken +{ + std::string value; + time_t validUntil = 0; //remaining lifetime of the access token +}; + +struct GdriveAccessInfo +{ + GdriveAccessToken accessToken; + std::string refreshToken; + GdriveUser userInfo; +}; + +GdriveAccessInfo gdriveExchangeAuthCode(const GdriveAuthCode& authCode, int timeoutSec) //throw SysError +{ + //https://developers.google.com/identity/protocols/OAuth2InstalledApp#exchange-authorization-code + const std::string postBuf = xWwwFormUrlEncode( + { + {"code", authCode.code}, + {"client_id", getGdriveClientId()}, + {"client_secret", getGdriveClientSecret()}, + {"redirect_uri", authCode.redirectUrl}, + {"grant_type", "authorization_code"}, + {"code_verifier", authCode.codeChallenge}, + }); + std::string response; + googleHttpsRequest(Zstr("oauth2.googleapis.com"), "/token", {} /*extraHeaders*/, {{CURLOPT_POSTFIELDS, postBuf.c_str()}}, + [&](std::span buf) { response.append(buf.data(), buf.size()); }, + nullptr /*readRequest*/, nullptr /*receiveHeader*/, timeoutSec); //throw SysError + + JsonValue jresponse; + try { jresponse = parseJson(response); } + catch (JsonParsingError&) {} + + const std::optional accessToken = getPrimitiveFromJsonObject(jresponse, "access_token"); + const std::optional refreshToken = getPrimitiveFromJsonObject(jresponse, "refresh_token"); + const std::optional expiresIn = getPrimitiveFromJsonObject(jresponse, "expires_in"); //e.g. 3600 seconds + if (!accessToken || !refreshToken || !expiresIn) + throw SysError(formatGdriveErrorRaw(response)); + + const GdriveUser userInfo = getGdriveUser({*accessToken, timeoutSec}); //throw SysError + + return {{*accessToken, std::time(nullptr) + stringTo(*expiresIn)}, *refreshToken, userInfo}; +} + + +GdriveAccessInfo gdriveAuthorizeAccess(const std::string& gdriveLoginHint, const std::function& updateGui /*throw X*/, int timeoutSec) //throw SysError, X +{ + //spin up a web server to wait for the HTTP GET after Google authentication + const addrinfo hints + { + .ai_flags = + AI_ADDRCONFIG | //no such issue on Linux: https://bugs.chromium.org/p/chromium/issues/detail?id=5234 + AI_PASSIVE, //the returned socket addresses will be suitable for bind(2)ing a socket that will accept(2) connections. + .ai_family = AF_UNSPEC, //don't care if AF_INET or AF_INET6 + .ai_socktype = SOCK_STREAM, //we *do* care about this one! + }; + addrinfo* servinfo = nullptr; + ZEN_ON_SCOPE_EXIT(if (servinfo) ::freeaddrinfo(servinfo)); + + //ServiceName == "0": open the next best free port + const int rcGai = ::getaddrinfo(nullptr, //_In_opt_ PCSTR pNodeName + "0", //_In_opt_ PCSTR pServiceName + &hints, //_In_opt_ const ADDRINFOA* pHints + &servinfo); //_Outptr_ PADDRINFOA* ppResult + if (rcGai != 0) + THROW_LAST_SYS_ERROR_GAI(rcGai); + if (!servinfo) + throw SysError(formatSystemError("getaddrinfo", L"" /*errorCode*/, L"No local IP address available")); + + + const auto getBoundSocket = [](const auto& /*::addrinfo*/ ai) + { + SocketType testSocket = ::socket(ai.ai_family, //int socket_family + SOCK_CLOEXEC | + ai.ai_socktype, //int socket_type + ai.ai_protocol); //int protocol + if (testSocket == invalidSocket) + THROW_LAST_SYS_ERROR_WSA("socket"); + ZEN_ON_SCOPE_FAIL(closeSocket(testSocket)); + + if (::bind(testSocket, ai.ai_addr, static_cast(ai.ai_addrlen)) != 0) + THROW_LAST_SYS_ERROR_WSA("bind"); + + return testSocket; + }; + + + SocketType socket = invalidSocket; + std::optional firstError; + + for (const auto* /*::addrinfo*/ si = servinfo; si; si = si->ai_next) + if (si->ai_family == AF_INET || + si->ai_family == AF_INET6) + try + { + socket = getBoundSocket(*si); //throw SysError; pass ownership + break; + } + catch (const SysError& e) { if (!firstError) firstError = e; } + + if (socket == invalidSocket) + { + if (firstError) + throw* firstError; + throw SysError(formatSystemError("getaddrinfo", L"" /*errorCode*/, L"No local IPv4 or IPv6 address available")); + } + ZEN_ON_SCOPE_EXIT(closeSocket(socket)); + + + sockaddr_storage addr = {}; //"sufficiently large to store address information for IPv4 (AF_INET/sockaddr_in) or IPv6 (AF_INET6/sockaddr_in6)" + socklen_t addrLen = sizeof(addr); + if (::getsockname(socket, reinterpret_cast(&addr), &addrLen) != 0) + THROW_LAST_SYS_ERROR_WSA("getsockname"); + + std::string redirectUrl; + if (addr.ss_family == AF_INET) + { + //the socket is not bound to a specific local IP: + // char buf[INET_ADDRSTRLEN] = {}; //inet_ntop -> "0.0.0.0" + // inet_ntop(AF_INET, &reinterpret_cast(addr).sin_addr, buf, std::size(buf)); + const int port = ntohs(reinterpret_cast(addr).sin_port); + redirectUrl = "http://127.0.0.1:" + numberTo(port); + } + else if (addr.ss_family == AF_INET6) //inet_ntop() == "::" + { + const int port = ntohs(reinterpret_cast(addr).sin6_port); + redirectUrl = "http://[::1]:" + numberTo(port); + } + else + throw SysError(formatSystemError("getsockname", L"", L"Unexpected protocol family: " + numberTo(addr.ss_family))); + + if (::listen(socket, SOMAXCONN) != 0) + THROW_LAST_SYS_ERROR_WSA("listen"); + + + //"A code_verifier is a high-entropy cryptographic random string using the unreserved characters:" + //[A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~", with a minimum length of 43 characters and a maximum length of 128 characters. + std::string codeChallenge = stringEncodeBase64(generateGUID() + generateGUID()); + replace(codeChallenge, '+', '-'); // + replace(codeChallenge, '/', '.'); //base64 is almost a perfect fit for code_verifier! + replace(codeChallenge, '=', '_'); // + assert(codeChallenge.size() == 44); + + //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", getGdriveClientId()}, + {"redirect_uri", redirectUrl}, + {"response_type", "code"}, + {"scope", "https://www.googleapis.com/auth/drive"}, + {"code_challenge", codeChallenge}, + {"code_challenge_method", "plain"}, + {"login_hint", gdriveLoginHint}, + }); + try + { + openWithDefaultApp(utfTo(oauthUrl)); //throw FileError + } + catch (const FileError& e) { throw SysError(replaceCpy(e.toString(), L"\n\n", L'\n')); } //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() + { + if (updateGui) updateGui(); //throw X + + const int waitTimeMs = 100; + pollfd fds[] = {{socket, POLLIN}}; + + const char* functionName = "poll"; + const int rv = ::poll(fds, std::size(fds), waitTimeMs); //int timeout + if (rv < 0) + THROW_LAST_SYS_ERROR_WSA(functionName); + else if (rv != 0) + break; + //else: time-out! + } + //potential race! if the connection is gone right after ::select() and before ::accept(), latter will hang + const int clientSocket = ::accept4(socket, //int sockfd + nullptr, //sockaddr* addr + nullptr, //socklen_t* addrlen + SOCK_CLOEXEC); //int flags + if (clientSocket == invalidSocket) + THROW_LAST_SYS_ERROR_WSA("accept"); + + + //receive first line of HTTP request + std::string reqLine; + for (;;) + { + 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", IfNotFoundReturn::none); + break; + } + if (bytesReceived == 0 || reqLine.size() >= 100'000 /*bogus line length*/) + break; + } + + //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 statusItems = splitCpy(reqLine, ' ', SplitOnEmpty::allow); //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], "?", IfNotFoundReturn::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::variant 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 + { + std::string htmlMsg = R"( + + + + + TITLE_PLACEHOLDER + + + +

TITLE_PLACEHOLDER

+
MESSAGE_PLACEHOLDER
+ + + )"; + try + { + if (!error.empty()) + throw SysError(replaceCpy(_("Error code %x"), L"%x", + L"\"" + utfTo(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 = gdriveExchangeAuthCode({code, redirectUrl, codeChallenge}, timeoutSec); //throw SysError + replace(htmlMsg, "TITLE_PLACEHOLDER", utfTo(_("Authentication completed."))); + replace(htmlMsg, "MESSAGE_PLACEHOLDER", utfTo(_("You may close this page now and continue with FreeFileSync."))); + } + catch (const SysError& e) + { + authResult = e; + replace(htmlMsg, "TITLE_PLACEHOLDER", utfTo(_("Authentication failed."))); + replace(htmlMsg, "MESSAGE_PLACEHOLDER", utfTo(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(strLength(htmlMsg)) + "\r\n" + "\r\n" + htmlMsg; + } + + for (size_t bytesToSend = httpResponse.size(); bytesToSend > 0;) + bytesToSend -= tryWriteSocket(clientSocket, &*(httpResponse.end() - bytesToSend), bytesToSend); //throw SysError + + shutdownSocketSend(clientSocket); //throw SysError + //--------------------------------------------------------------- + + if (const SysError* e = std::get_if(&authResult)) + throw *e; + if (const GdriveAccessInfo* res = std::get_if(&authResult)) + return *res; + } +} + + +GdriveAccessToken gdriveRefreshAccess(const std::string& refreshToken, int timeoutSec) //throw SysError +{ + //https://developers.google.com/identity/protocols/OAuth2InstalledApp#offline + const std::string postBuf = xWwwFormUrlEncode( + { + {"refresh_token", refreshToken}, + {"client_id", getGdriveClientId()}, + {"client_secret", getGdriveClientSecret()}, + {"grant_type", "refresh_token"}, + }); + std::string response; + googleHttpsRequest(Zstr("oauth2.googleapis.com"), "/token", {} /*extraHeaders*/, {{CURLOPT_POSTFIELDS, postBuf.c_str()}}, + [&](std::span buf) { response.append(buf.data(), buf.size()); }, + nullptr /*readRequest*/, nullptr /*receiveHeader*/, timeoutSec); //throw SysError + + JsonValue jresponse; + try { jresponse = parseJson(response); } + catch (JsonParsingError&) {} + + const std::optional accessToken = getPrimitiveFromJsonObject(jresponse, "access_token"); + const std::optional expiresIn = getPrimitiveFromJsonObject(jresponse, "expires_in"); //e.g. 3600 seconds + if (!accessToken || !expiresIn) + throw SysError(formatGdriveErrorRaw(response)); + + return {*accessToken, std::time(nullptr) + stringTo(*expiresIn)}; +} + + +void gdriveRevokeAccess(const GdriveAccess& access) //throw SysError +{ + //https://developers.google.com/identity/protocols/OAuth2InstalledApp#tokenrevoke + std::string response; + const HttpSession::Result httpResult = googleHttpsRequest(Zstr("oauth2.googleapis.com"), "/revoke?token=" + access.token, + {"Content-Type: application/x-www-form-urlencoded"}, {{ CURLOPT_POSTFIELDS, ""}}, + [&](std::span buf) { response.append(buf.data(), buf.size()); }, + nullptr /*readRequest*/, nullptr /*receiveHeader*/, access.timeoutSec); //throw SysError + + if (httpResult.statusCode != 200) + throw SysError(formatGdriveErrorRaw(response)); +} + + +int64_t gdriveGetMyDriveFreeSpace(const GdriveAccess& access) //throw SysError +{ + //https://developers.google.com/drive/api/v3/reference/about + std::string response; + gdriveHttpsRequest("/drive/v3/about?fields=storageQuota", {} /*extraHeaders*/, {} /*extraOptions*/, + [&](std::span buf) { response.append(buf.data(), buf.size()); }, + nullptr /*readRequest*/, nullptr /*receiveHeader*/, access); //throw SysError + + JsonValue jresponse; + try { jresponse = parseJson(response); } + catch (JsonParsingError&) {} + + if (const JsonValue* storageQuota = getChildFromJsonObject(jresponse, "storageQuota")) + { + const std::optional usage = getPrimitiveFromJsonObject(*storageQuota, "usage"); + const std::optional limit = getPrimitiveFromJsonObject(*storageQuota, "limit"); + if (usage) + { + if (!limit) //"will not be present if the user has unlimited storage." + return std::numeric_limits::max(); + + const auto bytesUsed = stringTo(*usage); + const auto bytesLimit = stringTo(*limit); + + if (0 <= bytesUsed && bytesUsed <= bytesLimit) + return bytesLimit - bytesUsed; + } + } + throw SysError(formatGdriveErrorRaw(response)); +} + + +//instead of the "root" alias Google uses an actual ID in file metadata +std::string /*itemId*/ getMyDriveId(const GdriveAccess& access) //throw SysError +{ + //https://developers.google.com/drive/api/v3/reference/files/get + const std::string& queryParams = xWwwFormUrlEncode( + { + {"supportsAllDrives", "true"}, + {"fields", "id"}, + }); + std::string response; + gdriveHttpsRequest("/drive/v3/files/root?" + queryParams, {} /*extraHeaders*/, {} /*extraOptions*/, + [&](std::span buf) { response.append(buf.data(), buf.size()); }, + nullptr /*readRequest*/, nullptr /*receiveHeader*/, access); //throw SysError + + JsonValue jresponse; + try { jresponse = parseJson(response); } + catch (JsonParsingError&) {} + + const std::optional itemId = getPrimitiveFromJsonObject(jresponse, "id"); + if (!itemId) + throw SysError(formatGdriveErrorRaw(response)); + + return *itemId; +} + + +struct DriveDetails +{ + std::string driveId; + Zstring driveName; +}; +std::vector getSharedDrives(const GdriveAccess& access) //throw SysError +{ + //https://developers.google.com/drive/api/v3/reference/drives/list + std::vector sharedDrives; + { + std::optional nextPageToken; + do + { + std::string queryParams = xWwwFormUrlEncode( + { + {"pageSize", "100"}, //"[1, 100] Default: 10" + {"fields", "nextPageToken,drives(id,name)"}, + }); + if (nextPageToken) + queryParams += '&' + xWwwFormUrlEncode({{"pageToken", *nextPageToken}}); + + std::string response; + gdriveHttpsRequest("/drive/v3/drives?" + queryParams, {} /*extraHeaders*/, {} /*extraOptions*/, + [&](std::span buf) { response.append(buf.data(), buf.size()); }, + nullptr /*readRequest*/, nullptr /*receiveHeader*/, access); //throw SysError + + JsonValue jresponse; + try { jresponse = parseJson(response); } + catch (JsonParsingError&) {} + + /**/ nextPageToken = getPrimitiveFromJsonObject(jresponse, "nextPageToken"); + const JsonValue* drives = getChildFromJsonObject (jresponse, "drives"); + if (!drives || drives->type != JsonValue::Type::array) + throw SysError(formatGdriveErrorRaw(response)); + + for (const JsonValue& driveVal : drives->arrayVal) + { + std::optional driveId = getPrimitiveFromJsonObject(driveVal, "id"); + std::optional driveName = getPrimitiveFromJsonObject(driveVal, "name"); + if (!driveId || !driveName || driveName->empty()) + throw SysError(formatGdriveErrorRaw(serializeJson(driveVal))); + + sharedDrives.push_back({std::move(*driveId), utfTo(*driveName)}); + } + } + while (nextPageToken); + } + return sharedDrives; +} + + +struct StarredFolderDetails +{ + std::string folderId; + Zstring folderName; + std::string sharedDriveId; //empty if on "My Drive" +}; +std::vector getStarredFolders(const GdriveAccess& access) //throw SysError +{ + //https://developers.google.com/drive/api/v3/reference/files/list + std::vector starredFolders; + { + std::optional nextPageToken; + do + { + std::string queryParams = xWwwFormUrlEncode( + { + {"corpora", "allDrives"}, //"The 'user' corpus includes all files in "My Drive" and "Shared with me" https://developers.google.com/drive/api/v3/reference/files/list + {"includeItemsFromAllDrives", "true"}, + {"pageSize", "1000"}, //"[1, 1000] Default: 100" + {"q", std::string("starred and mimeType = '") + gdriveFolderMimeType + "' and not trashed"}, + {"spaces", "drive"}, + {"supportsAllDrives", "true"}, + {"fields", "nextPageToken,incompleteSearch,files(id,name,driveId)"}, //https://developers.google.com/drive/api/v3/reference/files + }); + if (nextPageToken) + queryParams += '&' + xWwwFormUrlEncode({{"pageToken", *nextPageToken}}); + + std::string response; + gdriveHttpsRequest("/drive/v3/files?" + queryParams, {} /*extraHeaders*/, {} /*extraOptions*/, + [&](std::span buf) { response.append(buf.data(), buf.size()); }, + nullptr /*readRequest*/, nullptr /*receiveHeader*/, access); //throw SysError + + JsonValue jresponse; + try { jresponse = parseJson(response); } + catch (JsonParsingError&) {} + + /**/ nextPageToken = getPrimitiveFromJsonObject(jresponse, "nextPageToken"); + const std::optional incompleteSearch = getPrimitiveFromJsonObject(jresponse, "incompleteSearch"); + const JsonValue* files = getChildFromJsonObject (jresponse, "files"); + if (!incompleteSearch || *incompleteSearch != "false" || !files || files->type != JsonValue::Type::array) + throw SysError(formatGdriveErrorRaw(response)); + + for (const JsonValue& childVal : files->arrayVal) + { + assert(childVal.type == JsonValue::Type::object); + const std::optional itemId = getPrimitiveFromJsonObject(childVal, "id"); + const std::optional itemName = getPrimitiveFromJsonObject(childVal, "name"); + const std::optional driveId = getPrimitiveFromJsonObject(childVal, "driveId"); + + if (!itemId || itemId->empty() || !itemName || itemName->empty()) + throw SysError(formatGdriveErrorRaw(serializeJson(childVal))); + + starredFolders.push_back({*itemId, + utfTo(*itemName), + driveId ? *driveId : ""}); + } + } + while (nextPageToken); + } + return starredFolders; +} + + +enum class GdriveItemType : unsigned char +{ + file, + folder, + shortcut, +}; +enum class FileOwner : unsigned char +{ + none, //"ownedByMe" not populated for items in Shared Drives. + me, + other, +}; +struct GdriveItemDetails +{ + Zstring itemName; + uint64_t fileSize = 0; + time_t modTime = 0; + //--- minimize padding --- + GdriveItemType type = GdriveItemType::file; + FileOwner owner = FileOwner::none; + //------------------------ + std::string targetId; //for GdriveItemType::shortcut: https://developers.google.com/drive/api/v3/shortcuts + std::vector parentIds; + + bool operator==(const GdriveItemDetails&) const = default; +}; + + +GdriveItemDetails extractItemDetails(const JsonValue& jvalue) //throw SysError +{ + assert(jvalue.type == JsonValue::Type::object); + + /**/ std::optional itemName = getPrimitiveFromJsonObject(jvalue, "name"); + const std::optional mimeType = getPrimitiveFromJsonObject(jvalue, "mimeType"); + const std::optional ownedByMe = getPrimitiveFromJsonObject(jvalue, "ownedByMe"); + const std::optional size = getPrimitiveFromJsonObject(jvalue, "size"); + const std::optional modifiedTime = getPrimitiveFromJsonObject(jvalue, "modifiedTime"); + const JsonValue* parents = getChildFromJsonObject (jvalue, "parents"); + const JsonValue* shortcut = getChildFromJsonObject (jvalue, "shortcutDetails"); + + if (!itemName || itemName->empty() || !mimeType || !modifiedTime) + throw SysError(formatGdriveErrorRaw(serializeJson(jvalue))); + + const GdriveItemType type = *mimeType == gdriveFolderMimeType ? GdriveItemType::folder : + *mimeType == gdriveShortcutMimeType ? GdriveItemType::shortcut : + GdriveItemType::file; + + const FileOwner owner = ownedByMe ? (*ownedByMe == "true" ? FileOwner::me : FileOwner::other) : FileOwner::none; //"Not populated for items in Shared Drives" + const uint64_t fileSize = size ? stringTo(*size) : 0; //not available for folders and shortcuts + + //RFC 3339 date-time: e.g. "2018-09-29T08:39:12.053Z" + const TimeComp tc = parseTime("%Y-%m-%dT%H:%M:%S", beforeLast(*modifiedTime, '.', IfNotFoundReturn::all)); + if (tc == TimeComp() || !endsWith(*modifiedTime, 'Z')) //'Z' means "UTC" => it seems Google doesn't use the time-zone offset postfix + throw SysError(L"Modification time is invalid. (" + utfTo(*modifiedTime) + L')'); + + const auto [modTime, timeValid] = utcToTimeT(tc); + if (!timeValid) + throw SysError(L"Modification time is invalid. (" + utfTo(*modifiedTime) + L')'); + + std::vector parentIds; + if (parents) //item without "parents" array is possible! e.g. 1. shared item located in "Shared with me", referenced via a Shortcut 2. root folder under "Computers" + for (const JsonValue& parentVal : parents->arrayVal) + { + if (parentVal.type != JsonValue::Type::string) + throw SysError(formatGdriveErrorRaw(serializeJson(jvalue))); + parentIds.emplace_back(parentVal.primVal); + } + + if (!!shortcut != (type == GdriveItemType::shortcut)) + throw SysError(formatGdriveErrorRaw(serializeJson(jvalue))); + + std::string targetId; + if (shortcut) + { + std::optional targetItemId = getPrimitiveFromJsonObject(*shortcut, "targetId"); + if (!targetItemId || targetItemId->empty()) + throw SysError(formatGdriveErrorRaw(serializeJson(jvalue))); + + targetId = std::move(*targetItemId); + //evaluate "targetMimeType" ? don't bother: "The MIME type of a shortcut can become stale"! + } + + return {utfTo(*itemName), fileSize, modTime, type, owner, std::move(targetId), std::move(parentIds)}; +} + + +GdriveItemDetails getItemDetails(const std::string& itemId, const GdriveAccess& access) //throw SysError +{ + //https://developers.google.com/drive/api/v3/reference/files/get + const std::string& queryParams = xWwwFormUrlEncode( + { + {"fields", "trashed,name,mimeType,ownedByMe,size,modifiedTime,parents,shortcutDetails(targetId)"}, + {"supportsAllDrives", "true"}, + }); + std::string response; + gdriveHttpsRequest("/drive/v3/files/" + itemId + '?' + queryParams, {} /*extraHeaders*/, {} /*extraOptions*/, + [&](std::span buf) { response.append(buf.data(), buf.size()); }, + nullptr /*readRequest*/, nullptr /*receiveHeader*/, access); //throw SysError + try + { + const JsonValue jvalue = parseJson(response); //throw JsonParsingError + + //careful: do NOT return details about trashed items! they don't exist as far as FFS is concerned!!! + const std::optional trashed = getPrimitiveFromJsonObject(jvalue, "trashed"); + if (!trashed) + throw SysError(formatGdriveErrorRaw(response)); + else if (*trashed == "true") + throw SysError(L"Item has been trashed."); + + return extractItemDetails(jvalue); //throw SysError + } + catch (JsonParsingError&) { throw SysError(formatGdriveErrorRaw(response)); } +} + + +struct GdriveItem +{ + std::string itemId; + GdriveItemDetails details; +}; +std::vector readFolderContent(const std::string& folderId, const GdriveAccess& access) //throw SysError +{ + //https://developers.google.com/drive/api/v3/reference/files/list + std::vector childItems; + { + std::optional nextPageToken; + do + { + std::string queryParams = xWwwFormUrlEncode( + { + {"corpora", "allDrives"}, //"The 'user' corpus includes all files in "My Drive" and "Shared with me" https://developers.google.com/drive/api/v3/reference/files/list + {"includeItemsFromAllDrives", "true"}, + {"pageSize", "1000"}, //"[1, 1000] Default: 100" + {"q", "'" + folderId + "' in parents and not trashed"}, + {"spaces", "drive"}, + {"supportsAllDrives", "true"}, + {"fields", "nextPageToken,incompleteSearch,files(id,name,mimeType,ownedByMe,size,modifiedTime,parents,shortcutDetails(targetId))"}, //https://developers.google.com/drive/api/v3/reference/files + }); + if (nextPageToken) + queryParams += '&' + xWwwFormUrlEncode({{"pageToken", *nextPageToken}}); + + std::string response; + gdriveHttpsRequest("/drive/v3/files?" + queryParams, {} /*extraHeaders*/, {} /*extraOptions*/, + [&](std::span buf) { response.append(buf.data(), buf.size()); }, + nullptr /*readRequest*/, nullptr /*receiveHeader*/, access); //throw SysError + + JsonValue jresponse; + try { jresponse = parseJson(response); } + catch (JsonParsingError&) {} + + /**/ nextPageToken = getPrimitiveFromJsonObject(jresponse, "nextPageToken"); + const std::optional incompleteSearch = getPrimitiveFromJsonObject(jresponse, "incompleteSearch"); + const JsonValue* files = getChildFromJsonObject (jresponse, "files"); + if (!incompleteSearch || *incompleteSearch != "false" || !files || files->type != JsonValue::Type::array) + throw SysError(formatGdriveErrorRaw(response)); + + for (const JsonValue& childVal : files->arrayVal) + { + std::optional itemId = getPrimitiveFromJsonObject(childVal, "id"); + if (!itemId || itemId->empty()) + throw SysError(formatGdriveErrorRaw(serializeJson(childVal))); + + GdriveItemDetails itemDetails(extractItemDetails(childVal)); //throw SysError + assert(std::find(itemDetails.parentIds.begin(), itemDetails.parentIds.end(), folderId) != itemDetails.parentIds.end()); + + childItems.push_back({std::move(*itemId), std::move(itemDetails)}); + } + } + while (nextPageToken); + } + return childItems; +} + + +struct FileChange +{ + std::string itemId; + std::optional details; //empty if item was deleted/trashed +}; +struct DriveChange +{ + std::string driveId; + Zstring driveName; //empty if shared drive was deleted +}; +struct ChangesDelta +{ + std::string newStartPageToken; + std::vector fileChanges; + std::vector driveChanges; +}; +ChangesDelta getChangesDelta(const std::string& sharedDriveId /*empty for "My Drive"*/, const std::string& startPageToken, const GdriveAccess& access) //throw SysError +{ + //https://developers.google.com/drive/api/v3/reference/changes/list + ChangesDelta delta; + std::optional nextPageToken = startPageToken; + for (;;) + { + std::string queryParams = xWwwFormUrlEncode( + { + {"pageToken", *nextPageToken}, + {"fields", "kind,nextPageToken,newStartPageToken,changes(kind,changeType,removed,fileId,file(trashed,name,mimeType,ownedByMe,size,modifiedTime,parents,shortcutDetails(targetId)),driveId,drive(name))"}, + {"includeItemsFromAllDrives", "true"}, //semantics are a mess https://developers.google.com/drive/api/v3/enable-shareddrives https://freefilesync.org/forum/viewtopic.php?t=7827&start=30#p29712 + //in short: if driveId is set: required, but blatant lie; only drive-specific file changes returned + // if no driveId set: optional, but blatant lie; only changes to drive objects are returned, but not contained files (with a few exceptions) + {"pageSize", "1000"}, //"[1, 1000] Default: 100" + {"spaces", "drive"}, + {"supportsAllDrives", "true"}, + //do NOT "restrictToMyDrive": we're also interested in "Shared with me" items, which might be referenced by a shortcut in "My Drive" + }); + if (!sharedDriveId.empty()) + queryParams += '&' + xWwwFormUrlEncode({{"driveId", sharedDriveId}}); //only allowed for shared drives! + + std::string response; + gdriveHttpsRequest("/drive/v3/changes?" + queryParams, {} /*extraHeaders*/, {} /*extraOptions*/, + [&](std::span buf) { response.append(buf.data(), buf.size()); }, + nullptr /*readRequest*/, nullptr /*receiveHeader*/, access); //throw SysError + + JsonValue jresponse; + try { jresponse = parseJson(response); } + catch (JsonParsingError&) {} + + /**/ nextPageToken = getPrimitiveFromJsonObject(jresponse, "nextPageToken"); + const std::optional newStartPageToken = getPrimitiveFromJsonObject(jresponse, "newStartPageToken"); + const std::optional listKind = getPrimitiveFromJsonObject(jresponse, "kind"); + const JsonValue* changes = getChildFromJsonObject (jresponse, "changes"); + + if (!!nextPageToken == !!newStartPageToken || //there can be only one + !listKind || *listKind != "drive#changeList" || + !changes || changes->type != JsonValue::Type::array) + throw SysError(formatGdriveErrorRaw(response)); + + for (const JsonValue& childVal : changes->arrayVal) + { + const std::optional kind = getPrimitiveFromJsonObject(childVal, "kind"); + const std::optional changeType = getPrimitiveFromJsonObject(childVal, "changeType"); + const std::optional removed = getPrimitiveFromJsonObject(childVal, "removed"); + if (!kind || *kind != "drive#change" || !changeType || !removed) + throw SysError(formatGdriveErrorRaw(serializeJson(childVal))); + + if (*changeType == "file") + { + std::optional fileId = getPrimitiveFromJsonObject(childVal, "fileId"); + if (!fileId || fileId->empty()) + throw SysError(formatGdriveErrorRaw(serializeJson(childVal))); + + FileChange change; + change.itemId = std::move(*fileId); + if (*removed != "true") + { + const JsonValue* file = getChildFromJsonObject(childVal, "file"); + if (!file) + throw SysError(formatGdriveErrorRaw(serializeJson(childVal))); + + const std::optional trashed = getPrimitiveFromJsonObject(*file, "trashed"); + if (!trashed) + throw SysError(formatGdriveErrorRaw(serializeJson(childVal))); + + if (*trashed != "true") + change.details = extractItemDetails(*file); //throw SysError + } + delta.fileChanges.push_back(std::move(change)); + } + else if (*changeType == "drive") + { + std::optional driveId = getPrimitiveFromJsonObject(childVal, "driveId"); + if (!driveId || driveId->empty()) + throw SysError(formatGdriveErrorRaw(serializeJson(childVal))); + + DriveChange change; + change.driveId = std::move(*driveId); + if (*removed != "true") + { + const JsonValue* drive = getChildFromJsonObject(childVal, "drive"); + if (!drive) + throw SysError(formatGdriveErrorRaw(serializeJson(childVal))); + + const std::optional name = getPrimitiveFromJsonObject(*drive, "name"); + if (!name || name->empty()) + throw SysError(formatGdriveErrorRaw(serializeJson(childVal))); + + change.driveName = utfTo(*name); + } + delta.driveChanges.push_back(std::move(change)); + } + else assert(false); //no other types (yet!) + } + + if (!nextPageToken) + { + delta.newStartPageToken = *newStartPageToken; + return delta; + } + } +} + + +std::string /*startPageToken*/ getChangesCurrentToken(const std::string& sharedDriveId /*empty for "My Drive"*/, const GdriveAccess& access) //throw SysError +{ + //https://developers.google.com/drive/api/v3/reference/changes/getStartPageToken + std::string queryParams = xWwwFormUrlEncode( + { + {"supportsAllDrives", "true"}, + }); + if (!sharedDriveId.empty()) + queryParams += '&' + xWwwFormUrlEncode({{"driveId", sharedDriveId}}); //only allowed for shared drives! + + std::string response; + gdriveHttpsRequest("/drive/v3/changes/startPageToken?" + queryParams, {} /*extraHeaders*/, {} /*extraOptions*/, + [&](std::span buf) { response.append(buf.data(), buf.size()); }, + nullptr /*readRequest*/, nullptr /*receiveHeader*/, access); //throw SysError + + JsonValue jresponse; + try { jresponse = parseJson(response); } + catch (JsonParsingError&) {} + + const std::optional startPageToken = getPrimitiveFromJsonObject(jresponse, "startPageToken"); + if (!startPageToken) + throw SysError(formatGdriveErrorRaw(response)); + + return *startPageToken; +} + + +//- if item is a folder: deletes recursively!!! +//- even deletes a hardlink with multiple parents => use gdriveUnlinkParent() first +void gdriveDeleteItem(const std::string& itemId, const GdriveAccess& access) //throw SysError +{ + //https://developers.google.com/drive/api/v3/reference/files/delete + const std::string& queryParams = xWwwFormUrlEncode( + { + {"supportsAllDrives", "true"}, + }); + std::string response; + const HttpSession::Result httpResult = gdriveHttpsRequest("/drive/v3/files/" + itemId + '?' + queryParams, + {} /*extraHeaders*/, {{CURLOPT_CUSTOMREQUEST, "DELETE"}}, [&](std::span buf) { response.append(buf.data(), buf.size()); }, + nullptr /*readRequest*/, nullptr /*receiveHeader*/, access); //throw SysError + + if (response.empty() && httpResult.statusCode == 204) + return; //"If successful, this method returns an empty response body" + + throw SysError(formatGdriveErrorRaw(response)); +} + + +//item is NOT deleted when last parent is removed: it is just not accessible via the "My Drive" hierarchy but still adds to quota! => use for hard links only! +void gdriveUnlinkParent(const std::string& itemId, const std::string& parentId, const GdriveAccess& access) //throw SysError +{ + //https://developers.google.com/drive/api/v3/reference/files/update + const std::string& queryParams = xWwwFormUrlEncode( + { + {"removeParents", parentId}, + {"supportsAllDrives", "true"}, + {"fields", "id,parents"}, //for test if operation was successful + }); + std::string response; + const HttpSession::Result httpResult = gdriveHttpsRequest("/drive/v3/files/" + itemId + '?' + queryParams, + {"Content-Type: application/json; charset=UTF-8"}, {{CURLOPT_CUSTOMREQUEST, "PATCH"}, { CURLOPT_POSTFIELDS, "{}"}}, + [&](std::span buf) { response.append(buf.data(), buf.size()); }, nullptr /*readRequest*/, nullptr /*receiveHeader*/, access); //throw SysError + + if (response.empty() && httpResult.statusCode == 204) + return; //removing last parent of item not owned by us returns "204 No Content" (instead of 200 + file body) + + JsonValue jresponse; + try { jresponse = parseJson(response); /*throw JsonParsingError*/ } + catch (const JsonParsingError&) {} + + const std::optional id = getPrimitiveFromJsonObject(jresponse, "id"); //id is returned on "success", unlike "parents", see below... + const JsonValue* parents = getChildFromJsonObject(jresponse, "parents"); + if (!id || *id != itemId) + throw SysError(formatGdriveErrorRaw(response)); + + if (parents) //when last parent is removed, 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 JsonValue& jval) { return jval.type == JsonValue::Type::string && jval.primVal == parentId; })) + throw SysError(L"gdriveUnlinkParent: Google Drive internal failure"); //user should never see this... +} + + +//- if item is a folder: trashes recursively!!! +//- a hardlink with multiple parents will NOT be accessible anymore via any of its path aliases! +void gdriveMoveToTrash(const std::string& itemId, const GdriveAccess& access) //throw SysError +{ + //https://developers.google.com/drive/api/v3/reference/files/update + const std::string& queryParams = xWwwFormUrlEncode( + { + {"supportsAllDrives", "true"}, + {"fields", "trashed"}, + }); + const std::string postBuf = R"({ "trashed": true })"; + + std::string response; + gdriveHttpsRequest("/drive/v3/files/" + itemId + '?' + queryParams, + {"Content-Type: application/json; charset=UTF-8"}, {{CURLOPT_CUSTOMREQUEST, "PATCH"}, {CURLOPT_POSTFIELDS, postBuf.c_str()}}, + [&](std::span buf) { response.append(buf.data(), buf.size()); }, nullptr /*readRequest*/, nullptr /*receiveHeader*/, access); //throw SysError + + JsonValue jresponse; + try { jresponse = parseJson(response); /*throw JsonParsingError*/ } + catch (const JsonParsingError&) {} + + const std::optional trashed = getPrimitiveFromJsonObject(jresponse, "trashed"); + if (!trashed || *trashed != "true") + throw SysError(formatGdriveErrorRaw(response)); +} + + +//folder name already existing? will (happily) create duplicate => caller must check! +std::string /*folderId*/ gdriveCreateFolderPlain(const Zstring& folderName, const std::string& parentId, const GdriveAccess& access) //throw SysError +{ + //https://developers.google.com/drive/api/v3/folder#creating_a_folder + const std::string& queryParams = xWwwFormUrlEncode( + { + {"supportsAllDrives", "true"}, + {"fields", "id"}, + }); + JsonValue postParams(JsonValue::Type::object); + postParams.objectVal.set("mimeType", gdriveFolderMimeType); + postParams.objectVal.set("name", utfTo(folderName)); + postParams.objectVal.set("parents", std::vector {JsonValue(parentId)}); + const std::string& postBuf = serializeJson(postParams, "" /*lineBreak*/, "" /*indent*/); + + std::string response; + gdriveHttpsRequest("/drive/v3/files?" + queryParams, + {"Content-Type: application/json; charset=UTF-8"}, {{CURLOPT_POSTFIELDS, postBuf.c_str()}}, + [&](std::span buf) { response.append(buf.data(), buf.size()); }, nullptr /*readRequest*/, nullptr /*receiveHeader*/, access); //throw SysError + + JsonValue jresponse; + try { jresponse = parseJson(response); } + catch (JsonParsingError&) {} + + const std::optional itemId = getPrimitiveFromJsonObject(jresponse, "id"); + if (!itemId) + throw SysError(formatGdriveErrorRaw(response)); + return *itemId; +} + + +//shortcut name already existing? will (happily) create duplicate => caller must check! +std::string /*shortcutId*/ gdriveCreateShortcutPlain(const Zstring& shortcutName, const std::string& parentId, const std::string& targetId, const GdriveAccess& access) //throw SysError +{ + /* https://developers.google.com/drive/api/v3/shortcuts + - targetMimeType is determined automatically (ignored if passed) + - creating shortcuts to shortcuts fails with "Internal Error" */ + const std::string& queryParams = xWwwFormUrlEncode( + { + {"supportsAllDrives", "true"}, + {"fields", "id"}, + }); + JsonValue shortcutDetails(JsonValue::Type::object); + shortcutDetails.objectVal.set("targetId", targetId); + + JsonValue postParams(JsonValue::Type::object); + postParams.objectVal.set("mimeType", gdriveShortcutMimeType); + postParams.objectVal.set("name", utfTo(shortcutName)); + postParams.objectVal.set("parents", std::vector {JsonValue(parentId)}); + postParams.objectVal.set("shortcutDetails", std::move(shortcutDetails)); + const std::string& postBuf = serializeJson(postParams, "" /*lineBreak*/, "" /*indent*/); + + std::string response; + gdriveHttpsRequest("/drive/v3/files?" + queryParams, {"Content-Type: application/json; charset=UTF-8"}, + {{CURLOPT_POSTFIELDS, postBuf.c_str()}}, [&](std::span buf) { response.append(buf.data(), buf.size()); }, + nullptr /*readRequest*/, nullptr /*receiveHeader*/, access); //throw SysError + + JsonValue jresponse; + try { jresponse = parseJson(response); } + catch (JsonParsingError&) {} + + const std::optional itemId = getPrimitiveFromJsonObject(jresponse, "id"); + if (!itemId) + throw SysError(formatGdriveErrorRaw(response)); + return *itemId; +} + + +//target name already existing? will (happily) create duplicate items => caller must check! +//can copy files + shortcuts (but fails for folders) + Google-specific file types (.gdoc, .gsheet, .gslides) +std::string /*fileId*/ gdriveCopyFile(const std::string& fileId, const std::string& parentIdTo, const Zstring& newName, time_t newModTime, const GdriveAccess& access) //throw SysError +{ + //https://developers.google.com/drive/api/v3/reference/files/copy + const std::string queryParams = xWwwFormUrlEncode( + { + {"supportsAllDrives", "true"}, + {"fields", "id"}, + }); + + //more Google Drive peculiarities: changing the file name changes modifiedTime!!! => workaround: + + //RFC 3339 date-time: e.g. "2018-09-29T08:39:12.053Z" + const std::string modTimeRfc = utfTo(formatTime(Zstr("%Y-%m-%dT%H:%M:%S.000Z"), getUtcTime(newModTime))); //returns empty string on error + if (modTimeRfc.empty()) + throw SysError(L"Invalid modification time (time_t: " + numberTo(newModTime) + L')'); + + JsonValue postParams(JsonValue::Type::object); + postParams.objectVal.set("name", utfTo(newName)); + postParams.objectVal.set("parents", std::vector {JsonValue(parentIdTo)}); + postParams.objectVal.set("modifiedTime", modTimeRfc); + const std::string& postBuf = serializeJson(postParams, "" /*lineBreak*/, "" /*indent*/); + + std::string response; + gdriveHttpsRequest("/drive/v3/files/" + fileId + "/copy?" + queryParams, + {"Content-Type: application/json; charset=UTF-8"}, {{CURLOPT_POSTFIELDS, postBuf.c_str()}}, + [&](std::span buf) { response.append(buf.data(), buf.size()); }, + nullptr /*readRequest*/, nullptr /*receiveHeader*/, access); //throw SysError + + JsonValue jresponse; + try { jresponse = parseJson(response); /*throw JsonParsingError*/ } + catch (const JsonParsingError&) {} + + const std::optional itemId = getPrimitiveFromJsonObject(jresponse, "id"); + if (!itemId) + throw SysError(formatGdriveErrorRaw(response)); + + return *itemId; + +} + + +//target name already existing? will (happily) create duplicate items => caller must check! +void gdriveMoveAndRenameItem(const std::string& itemId, const std::string& parentIdFrom, const std::string& parentIdTo, + const Zstring& newName, time_t newModTime, const GdriveAccess& access) //throw SysError +{ + //https://developers.google.com/drive/api/v3/folder#moving_files_between_folders + std::string queryParams = xWwwFormUrlEncode( + { + {"supportsAllDrives", "true"}, + {"fields", "name,parents"}, //for test if operation was successful + }); + + if (parentIdFrom != parentIdTo) + queryParams += '&' + xWwwFormUrlEncode( + { + {"removeParents", parentIdFrom}, + {"addParents", parentIdTo}, + }); + + //more Google Drive peculiarities: changing the file name changes modifiedTime!!! => workaround: + + //RFC 3339 date-time: e.g. "2018-09-29T08:39:12.053Z" + const std::string modTimeRfc = utfTo(formatTime(Zstr("%Y-%m-%dT%H:%M:%S.000Z"), getUtcTime(newModTime))); //returns empty string on error + if (modTimeRfc.empty()) + throw SysError(L"Invalid modification time (time_t: " + numberTo(newModTime) + L')'); + + JsonValue postParams(JsonValue::Type::object); + postParams.objectVal.set("name", utfTo(newName)); + postParams.objectVal.set("modifiedTime", modTimeRfc); + const std::string& postBuf = serializeJson(postParams, "" /*lineBreak*/, "" /*indent*/); + + std::string response; + gdriveHttpsRequest("/drive/v3/files/" + itemId + '?' + queryParams, + {"Content-Type: application/json; charset=UTF-8"}, {{CURLOPT_CUSTOMREQUEST, "PATCH"}, {CURLOPT_POSTFIELDS, postBuf.c_str()}}, + [&](std::span buf) { response.append(buf.data(), buf.size()); }, + nullptr /*readRequest*/, nullptr /*receiveHeader*/, access); //throw SysError + + JsonValue jresponse; + try { jresponse = parseJson(response); /*throw JsonParsingError*/ } + catch (const JsonParsingError&) {} + + const std::optional name = getPrimitiveFromJsonObject(jresponse, "name"); + const JsonValue* parents = getChildFromJsonObject(jresponse, "parents"); + if (!name || *name != utfTo(newName) || + !parents || parents->type != JsonValue::Type::array) + throw SysError(formatGdriveErrorRaw(response)); + + if (!std::any_of(parents->arrayVal.begin(), parents->arrayVal.end(), + [&](const JsonValue& jval) { return jval.type == JsonValue::Type::string && jval.primVal == parentIdTo; })) + throw SysError(formatSystemError("gdriveMoveAndRenameItem", L"", L"Google Drive internal failure.")); //user should never see this... +} + + +#if 0 +void setModTime(const std::string& itemId, time_t modTime, const GdriveAccess& access) //throw SysError +{ + //https://developers.google.com/drive/api/v3/reference/files/update + //RFC 3339 date-time: e.g. "2018-09-29T08:39:12.053Z" + const std::string& modTimeRfc = formatTime("%Y-%m-%dT%H:%M:%S.000Z", getUtcTime2(modTime)); //returns empty string on error + if (modTimeRfc.empty()) + throw SysError(L"Invalid modification time (time_t: " + numberTo(modTime) + L')'); + + const std::string& queryParams = xWwwFormUrlEncode( + { + {"supportsAllDrives", "true"}, + {"fields", "modifiedTime"}, + }); + const std::string postBuf = R"({ "modifiedTime": ")" + modTimeRfc + "\" }"; + + std::string response; + gdriveHttpsRequest("/drive/v3/files/" + itemId + '?' + queryParams, + {"Content-Type: application/json; charset=UTF-8"}, {{CURLOPT_CUSTOMREQUEST, "PATCH"}, {CURLOPT_POSTFIELDS, postBuf.c_str()}}, + [&](std::span buf) { response.append(buf.data(), buf.size()); }, + nullptr /*readRequest*/, nullptr /*receiveHeader*/, access); //throw SysError + + JsonValue jresponse; + try { jresponse = parseJson(response); /*throw JsonParsingError*/ } + catch (const JsonParsingError&) {} + + const std::optional modifiedTime = getPrimitiveFromJsonObject(jresponse, "modifiedTime"); + if (!modifiedTime || *modifiedTime != modTimeRfc) + throw SysError(formatGdriveErrorRaw(response)); +} +#endif + + +DEFINE_NEW_SYS_ERROR(SysErrorAbusiveFile) +void gdriveDownloadFileImpl(const std::string& fileId, const std::function& writeBlock /*throw X*/, //throw SysError, SysErrorAbusiveFile, X + bool acknowledgeAbuse, const GdriveAccess& access) +{ + /* https://developers.google.com/drive/api/v3/manage-downloads + doesn't work for Google-specific file types, but Google Backup & Sync still "downloads" them: + - in some JSON-like file format: + {"url": "https://docs.google.com/open?id=FILE_ID", "doc_id": "FILE_ID", "email": "ACCOUNT_EMAIL"} + + - adds artificial file extensions: .gdoc, .gsheet, .gslides, ... + + - 2022-10-10: In "Google Drive for Desktop" the file content now looks like: + {"":"WARNING! DO NOT EDIT THIS FILE! ANY CHANGES MADE WILL BE LOST!","doc_id":"FILE_ID","resource_key":"","email":"ACCOUNT_EMAIL"} */ + + std::string queryParams = xWwwFormUrlEncode( + { + {"supportsAllDrives", "true"}, + {"alt", "media"}, + }); + if (acknowledgeAbuse) //apply on demand only! https://freefilesync.org/forum/viewtopic.php?t=7520") + queryParams += '&' + xWwwFormUrlEncode({{"acknowledgeAbuse", "true"}}); + + std::string headBytes; + bool headBytesWritten = false; + + const HttpSession::Result httpResult = gdriveHttpsRequest("/drive/v3/files/" + fileId + '?' + queryParams, {} /*extraHeaders*/, {} /*extraOptions*/, + [&](std::span buf) + /* libcurl feeds us a shitload of tiny kB-sized zlib-decompressed pieces of data! + libcurl's zlib buffer is sized at ridiculous 16 kB! + => if this ever becomes a perf issue: roll our own zlib decompression! */ + { + if (headBytes.size() < 16 * 1024) //don't access writeBlock() yet in case of error! (=> support acknowledgeAbuse retry handling) + headBytes.append(buf.data(), buf.size()); + else + { + if (!headBytesWritten) + { + headBytesWritten = true; + writeBlock(headBytes.c_str(), headBytes.size()); //throw X + } + + writeBlock(buf.data(), buf.size()); //throw X + } + }, nullptr /*tryReadRequest*/, nullptr /*receiveHeader*/, access); //throw SysError, X + + if (httpResult.statusCode / 100 != 2) + { + /* https://freefilesync.org/forum/viewtopic.php?t=7463 => HTTP status code 403 + body: + { "error": { "errors": [{ "domain": "global", + "reason": "cannotDownloadAbusiveFile", + "message": "This file has been identified as malware or spam and cannot be downloaded." }], + "code": 403, + "message": "This file has been identified as malware or spam and cannot be downloaded." }} */ + if (!headBytesWritten && httpResult.statusCode == 403 && contains(headBytes, "\"cannotDownloadAbusiveFile\"")) + throw SysErrorAbusiveFile(formatGdriveErrorRaw(headBytes)); + + throw SysError(formatGdriveErrorRaw(headBytes)); + } + + if (!headBytesWritten && !headBytes.empty()) + writeBlock(headBytes.c_str(), headBytes.size()); //throw X +} + + +void gdriveDownloadFile(const std::string& fileId, const std::function& writeBlock /*throw X*/, //throw SysError, X + const GdriveAccess& access) +{ + try + { + gdriveDownloadFileImpl(fileId, writeBlock /*throw X*/, false /*acknowledgeAbuse*/, access); //throw SysError, SysErrorAbusiveFile, X + } + catch (SysErrorAbusiveFile&) + { + gdriveDownloadFileImpl(fileId, writeBlock /*throw X*/, true /*acknowledgeAbuse*/, access); //throw SysError, (SysErrorAbusiveFile), X + } +} + + +#if 0 +//file name already existing? => duplicate file created! +//note: Google Drive upload is already transactional! +//upload "small files" (5 MB or less; enforced by Google?) in a single round-trip +std::string /*itemId*/ gdriveUploadSmallFile(const Zstring& fileName, const std::string& parentId, uint64_t streamSize, std::optional modTime, //throw SysError, X + const std::function& readBlock /*throw X; return "bytesToRead" bytes unless end of stream*/, + const GdriveAccess& access) +{ + //https://developers.google.com/drive/api/v3/folder#inserting_a_file_in_a_folder + //https://developers.google.com/drive/api/v3/manage-uploads#http_1 + + JsonValue postParams(JsonValue::Type::object); + postParams.objectVal.emplace("name", utfTo(fileName)); + postParams.objectVal.emplace("parents", std::vector {JsonValue(parentId)}); + if (modTime) //convert to RFC 3339 date-time: e.g. "2018-09-29T08:39:12.053Z" + { + const std::string& modTimeRfc = utfTo(formatTime(Zstr("%Y-%m-%dT%H:%M:%S.000Z"), getUtcTime2(*modTime))); //returns empty string on error + if (modTimeRfc.empty()) + throw SysError(L"Invalid modification time (time_t: " + numberTo(*modTime) + L')'); + + postParams.objectVal.emplace("modifiedTime", modTimeRfc); + } + const std::string& metaDataBuf = serializeJson(postParams, "" /*lineBreak*/, "" /*indent*/); + + //allowed chars for border: DIGIT ALPHA ' ( ) + _ , - . / : = ? + const std::string boundaryString = stringEncodeBase64(generateGUID() + generateGUID()); + + const std::string postBufHead = "--" + boundaryString + "\r\n" + "Content-Type: application/json; charset=UTF-8" "\r\n" + /**/ "\r\n" + + metaDataBuf + "\r\n" + "--" + boundaryString + "\r\n" + "Content-Type: application/octet-stream" "\r\n" + /**/ "\r\n"; + + const std::string postBufTail = "\r\n--" + boundaryString + "--"; + + auto readMultipartBlock = [&, headPos = size_t(0), eof = false, tailPos = size_t(0)](void* buffer, size_t bytesToRead) mutable -> size_t + { + const auto bufStart = buffer; + + if (headPos < postBufHead.size()) + { + const size_t junkSize = std::min(postBufHead.size() - headPos, bytesToRead); + std::memcpy(buffer, postBufHead.c_str() + headPos, junkSize); + headPos += junkSize; + buffer = static_cast(buffer) + junkSize; + bytesToRead -= junkSize; + } + if (bytesToRead > 0) + { + if (!eof) //don't assume readBlock() will return streamSize bytes as promised => exhaust and let Google Drive fail if there is a mismatch in Content-Length! + { + const size_t bytesRead = readBlock(buffer, bytesToRead); //throw X; return "bytesToRead" bytes unless end of stream + buffer = static_cast(buffer) + bytesRead; + bytesToRead -= bytesRead; + + if (bytesToRead > 0) + eof = true; + } + if (bytesToRead > 0) + if (tailPos < postBufTail.size()) + { + const size_t junkSize = std::min(postBufTail.size() - tailPos, bytesToRead); + std::memcpy(buffer, postBufTail.c_str() + tailPos, junkSize); + tailPos += junkSize; + buffer = static_cast(buffer) + junkSize; + bytesToRead -= junkSize; + } + } + return static_cast(buffer) - + static_cast(bufStart); + }; + +TODO: + gzip-compress HTTP request body! + + const std::string& queryParams = xWwwFormUrlEncode( + { + {"supportsAllDrives", "true"}, + {"uploadType", "multipart"}, + }); + std::string response; + const HttpSession::Result httpResult = gdriveHttpsRequest("/upload/drive/v3/files?" + queryParams, + { + "Content-Type: multipart/related; boundary=" + boundaryString, + "Content-Length: " + numberTo(postBufHead.size() + streamSize + postBufTail.size()) + }, + {{CURLOPT_POST, 1}}, //otherwise HttpSession::perform() will PUT + [&](std::span buf) { response.append(buf.data(), buf.size()); }, + readMultipartBlock, nullptr /*receiveHeader*/, access); //throw SysError, X + + JsonValue jresponse; + try { jresponse = parseJson(response); } + catch (JsonParsingError&) {} + + const std::optional itemId = getPrimitiveFromJsonObject(jresponse, "id"); + if (!itemId) + throw SysError(formatGdriveErrorRaw(response)); + + return *itemId; +} +#endif + + +//file name already existing? => duplicate file created! +//note: Google Drive upload is already transactional! +std::string /*itemId*/ gdriveUploadFile(const Zstring& fileName, const std::string& parentId, std::optional modTime, //throw SysError, X + const std::function& tryReadBlock /*throw X*/, //returning 0 signals EOF: Posix read() semantics + const GdriveAccess& access) +{ + //https://developers.google.com/drive/api/v3/folder#inserting_a_file_in_a_folder + //https://developers.google.com/drive/api/v3/manage-uploads#resumable + + //step 1: initiate resumable upload session + std::string uploadUrlRelative; + { + const std::string& queryParams = xWwwFormUrlEncode( + { + {"supportsAllDrives", "true"}, + {"uploadType", "resumable"}, + }); + JsonValue postParams(JsonValue::Type::object); + postParams.objectVal.set("name", utfTo(fileName)); + postParams.objectVal.set("parents", std::vector {JsonValue(parentId)}); + if (modTime) //convert to RFC 3339 date-time: e.g. "2018-09-29T08:39:12.053Z" + { + const std::string& modTimeRfc = utfTo(formatTime(Zstr("%Y-%m-%dT%H:%M:%S.000Z"), getUtcTime(*modTime))); //returns empty string on error + if (modTimeRfc.empty()) + throw SysError(L"Invalid modification time (time_t: " + numberTo(*modTime) + L')'); + + postParams.objectVal.set("modifiedTime", modTimeRfc); + } + const std::string& postBuf = serializeJson(postParams, "" /*lineBreak*/, "" /*indent*/); + //--------------------------------------------------- + + std::string uploadUrl; + + auto onHeaderData = [&](const std::string_view& header) + { + //"The callback will be called once for each header and only complete header lines are passed on to the callback" (including \r\n at the end) + if (startsWithAsciiNoCase(header, "Location:")) + { + uploadUrl = header; + uploadUrl = afterFirst(uploadUrl, ':', IfNotFoundReturn::none); + trim(uploadUrl); + } + }; + + std::string response; + const HttpSession::Result httpResult = gdriveHttpsRequest("/upload/drive/v3/files?" + queryParams, + {"Content-Type: application/json; charset=UTF-8"}, {{CURLOPT_POSTFIELDS, postBuf.c_str()}}, + [&](std::span buf) { response.append(buf.data(), buf.size()); }, + nullptr /*readRequest*/, onHeaderData, access); //throw SysError + + if (httpResult.statusCode != 200) + throw SysError(formatGdriveErrorRaw(response)); + + if (!startsWith(uploadUrl, "https://www.googleapis.com/")) + throw SysError(L"Invalid upload URL: " + utfTo(uploadUrl)); //user should never see this + + uploadUrlRelative = afterFirst(uploadUrl, "googleapis.com", IfNotFoundReturn::none); + } + //--------------------------------------------------- + //step 2: upload file content + + //not officially documented, but Google Drive supports compressed file upload when "Content-Encoding: gzip" is set! :))) + InputStreamAsGzip gzipStream(tryReadBlock, GDRIVE_BLOCK_SIZE_UPLOAD); //throw SysError + + auto readRequest = [&](std::span buf) { return gzipStream.read(buf.data(), buf.size()); }; //throw SysError, X + + std::string response; //don't need "Authorization: Bearer": + googleHttpsRequest(GOOGLE_REST_API_SERVER, uploadUrlRelative, { "Content-Encoding: gzip" }, {} /*extraOptions*/, + [&](std::span buf) { response.append(buf.data(), buf.size()); }, readRequest, + nullptr /*receiveHeader*/, access.timeoutSec); //throw SysError, X + + JsonValue jresponse; + try { jresponse = parseJson(response); } + catch (JsonParsingError&) {} + + const std::optional itemId = getPrimitiveFromJsonObject(jresponse, "id"); + if (!itemId) + throw SysError(formatGdriveErrorRaw(response)); + + return *itemId; +} + + +class GdriveAccessBuffer //per-user-session & drive! => serialize access (perf: amortized fully buffered!) +{ +public: + //GdriveDrivesBuffer constructor calls GdriveAccessBuffer::getAccessToken() + explicit GdriveAccessBuffer(const GdriveAccessInfo& accessInfo) : + accessInfo_(accessInfo) {} + + GdriveAccessBuffer(MemoryStreamIn& stream) //throw SysError + { + accessInfo_.accessToken.validUntil = readNumber(stream); // + accessInfo_.accessToken.value = readContainer(stream); // + accessInfo_.refreshToken = readContainer(stream); //SysErrorUnexpectedEos + accessInfo_.userInfo.displayName = utfTo(readContainer(stream)); // + accessInfo_.userInfo.email = readContainer(stream); // + } + + void serialize(MemoryStreamOut& stream) const + { + writeNumber(stream, accessInfo_.accessToken.validUntil); + static_assert(sizeof(accessInfo_.accessToken.validUntil) <= sizeof(int64_t)); //ensure cross-platform compatibility! + writeContainer(stream, accessInfo_.accessToken.value); + writeContainer(stream, accessInfo_.refreshToken); + writeContainer(stream, utfTo(accessInfo_.userInfo.displayName)); + writeContainer(stream, accessInfo_.userInfo.email); + } + + //set *before* calling any of the subsequent functions; see GdrivePersistentSessions::accessUserSession() + void setContextTimeout(const std::weak_ptr& timeoutSec) { timeoutSec_ = timeoutSec; } + + GdriveAccess getAccessToken() //throw SysError + { + const int timeoutSec = getTimeoutSec(); + + if (accessInfo_.accessToken.validUntil <= std::time(nullptr) + timeoutSec + 5 /*some leeway*/) //expired/will expire + { + GdriveAccessToken token = gdriveRefreshAccess(accessInfo_.refreshToken, timeoutSec); //throw SysError + + //"there are limits on the number of refresh tokens that will be issued" + //Google Drive access token is usually valid for one hour => fail on pathologic user-defined time out: + if (token.validUntil <= std::time(nullptr) + 2 * timeoutSec) + throw SysError(_("Please set up a shorter time out for Google Drive.") + L" [" + _P("1 sec", "%x sec", timeoutSec) + L']'); + + accessInfo_.accessToken = std::move(token); + } + + return {accessInfo_.accessToken.value, timeoutSec}; + } + + const std::string& getUserEmail() const { return accessInfo_.userInfo.email; } + + void update(const GdriveAccessInfo& accessInfo) + { + if (!equalAsciiNoCase(accessInfo.userInfo.email, accessInfo_.userInfo.email)) + throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); + accessInfo_ = accessInfo; + } + +private: + GdriveAccessBuffer (const GdriveAccessBuffer&) = delete; + GdriveAccessBuffer& operator=(const GdriveAccessBuffer&) = delete; + + int getTimeoutSec() const + { + const std::shared_ptr timeoutSec = timeoutSec_.lock(); + assert(timeoutSec); + if (!timeoutSec) + throw std::runtime_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] GdriveAccessBuffer: Timeout duration was not set."); + + return *timeoutSec; + } + + GdriveAccessInfo accessInfo_; + std::weak_ptr timeoutSec_; +}; + + +class GdriveDrivesBuffer; + + +class GdriveFileState //per-user-session! => serialize access (perf: amortized fully buffered!) +{ +public: + GdriveFileState(const std::string& driveId, //ID of shared drive or "My Drive": never empty! + const Zstring& sharedDriveName, //*empty* for "My Drive" + GdriveAccessBuffer& accessBuf) : //throw SysError + /* issue getChangesCurrentToken() as the very first Google Drive query! */ + lastSyncToken_(getChangesCurrentToken(sharedDriveName.empty() ? std::string() : driveId, accessBuf.getAccessToken())), //throw SysError + driveId_(driveId), + sharedDriveName_(sharedDriveName), + accessBuf_(accessBuf) { assert(!driveId.empty() && sharedDriveName != Zstr("My Drive")); } + + GdriveFileState(MemoryStreamIn& stream, GdriveAccessBuffer& accessBuf) : //throw SysError + accessBuf_(accessBuf) + { + lastSyncToken_ = readContainer(stream); // + driveId_ = readContainer(stream); //SysErrorUnexpectedEos + sharedDriveName_ = utfTo(readContainer(stream)); // + + for (;;) + { + const std::string folderId = readContainer(stream); //SysErrorUnexpectedEos + if (folderId.empty()) + break; + folderContents_[folderId].isKnownFolder = true; + } + + for (;;) + { + const std::string itemId = readContainer(stream); //SysErrorUnexpectedEos + if (itemId.empty()) + break; + + GdriveItemDetails details = {}; //read in correct sequence! + details.itemName = utfTo(readContainer(stream)); // + details.type = readNumber(stream); // + details.owner = readNumber (stream); // + details.fileSize = readNumber (stream); //SysErrorUnexpectedEos + details.modTime = static_cast(readNumber(stream)); // + details.targetId = readContainer(stream); // + + size_t parentsCount = readNumber(stream); //SysErrorUnexpectedEos + while (parentsCount-- != 0) + details.parentIds.push_back(readContainer(stream)); //SysErrorUnexpectedEos + + updateItemState(itemId, &details); + } + } + + void serialize(MemoryStreamOut& stream) const + { + writeContainer(stream, lastSyncToken_); + writeContainer(stream, driveId_); + writeContainer(stream, utfTo(sharedDriveName_)); + + for (const auto& [folderId, content] : folderContents_) + if (folderId.empty()) + throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); + else if (content.isKnownFolder) + writeContainer(stream, folderId); + writeContainer(stream, std::string()); //sentinel + + auto serializeItem = [&](const std::string& itemId, const GdriveItemDetails& details) + { + writeContainer (stream, itemId); + writeContainer (stream, utfTo(details.itemName)); + writeNumber(stream, details.type); + writeNumber (stream, details.owner); + writeNumber (stream, details.fileSize); + writeNumber (stream, details.modTime); + static_assert(sizeof(details.modTime) <= sizeof(int64_t)); //ensure cross-platform compatibility! + writeContainer(stream, details.targetId); + + writeNumber(stream, static_cast(details.parentIds.size())); + for (const std::string& parentId : details.parentIds) + writeContainer(stream, parentId); + }; + + //serialize + clean up: only save items in "known folders" + items referenced by shortcuts + for (const auto& [folderId, content] : folderContents_) + if (content.isKnownFolder) + for (const auto& itItem : content.childItems) + { + const auto& [itemId, details] = *itItem; + if (itemId.empty()) + throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); + serializeItem(itemId, details); + + if (details.type == GdriveItemType::shortcut) + { + if (details.targetId.empty()) + throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); + + if (auto it = itemDetails_.find(details.targetId); + it != itemDetails_.end()) + serializeItem(details.targetId, it->second); + } + } + writeContainer(stream, std::string()); //sentinel + } + + std::string getDriveId() const { return driveId_; } + + Zstring getSharedDriveName() const { return sharedDriveName_; } //*empty* for "My Drive" + + void setSharedDriveName(const Zstring& sharedDriveName) { sharedDriveName_ = sharedDriveName; } + + struct PathStatus + { + std::string existingItemId; + GdriveItemType existingType = GdriveItemType::file; + AfsPath existingPath; //input path =: existingPath + relPath + std::vector relPath; // + }; + PathStatus getPathStatus(const std::string& locationRootId, const AfsPath& itemPath, bool followLeafShortcut) //throw SysError + { + const std::vector relPath = splitCpy(itemPath.value, FILE_NAME_SEPARATOR, SplitOnEmpty::skip); + if (relPath.empty()) + return {locationRootId, GdriveItemType::folder, AfsPath(), {}}; + else + return getPathStatusSub(locationRootId, AfsPath(), relPath, followLeafShortcut); //throw SysError + } + + std::string /*itemId*/ getItemId(const std::string& locationRootId, const AfsPath& itemPath, bool followLeafShortcut) //throw SysError + { + const GdriveFileState::PathStatus& ps = getPathStatus(locationRootId, itemPath, followLeafShortcut); //throw SysError + if (ps.relPath.empty()) + return ps.existingItemId; + + throw SysError(replaceCpy(_("%x does not exist."), L"%x", fmtPath(ps.relPath.front()))); + } + + std::pair getFileAttributes(const std::string& locationRootId, const AfsPath& itemPath, bool followLeafShortcut) //throw SysError + { + if (itemPath.value.empty()) //location root not covered by itemDetails_ + { + GdriveItemDetails rootDetails + { + .type = GdriveItemType::folder, + //.itemName =... => better leave empty for a root item! + .owner = sharedDriveName_.empty() ? FileOwner::me : FileOwner::none, + }; + return {locationRootId, std::move(rootDetails)}; + } + + const std::string itemId = getItemId(locationRootId, itemPath, followLeafShortcut); //throw SysError + if (auto it = itemDetails_.find(itemId); + it != itemDetails_.end()) + return *it; + + //itemId was already found! => (must either be a location root) or buffered in itemDetails_ + throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); + } + + std::optional tryGetBufferedItemDetails(const std::string& itemId) const + { + if (auto it = itemDetails_.find(itemId); + it != itemDetails_.end()) + return it->second; + return {}; + } + + std::optional> tryGetBufferedFolderContent(const std::string& folderId) const + { + auto it = folderContents_.find(folderId); + if (it == folderContents_.end() || !it->second.isKnownFolder) + return std::nullopt; + + std::vector childItems; + for (auto itChild : it->second.childItems) + { + const auto& [childId, childDetails] = *itChild; + childItems.push_back({childId, childDetails}); + } + return std::move(childItems); //[!] need std::move! + } + + //-------------- notifications -------------- + using ItemIdDelta = std::unordered_set; + + struct FileStateDelta //as long as instance exists, GdriveItem will log all changed items + { + FileStateDelta() {} + private: + explicit FileStateDelta(const std::shared_ptr& cids) : changedIds(cids) {} + friend class GdriveFileState; + std::shared_ptr changedIds; //lifetime is managed by caller; access *only* by GdriveFileState! + }; + + void notifyFolderContent(const FileStateDelta& stateDelta, const std::string& folderId, const std::vector& childItems) + { + folderContents_[folderId].isKnownFolder = true; + + for (const GdriveItem& item : childItems) + notifyItemUpdated(stateDelta, item.itemId, &item.details); + + //- should we remove parent links for items that are not children of folderId anymore (as of this update)?? => fringe case during first update! (still: maybe trigger sync?) + //- what if there are multiple folder state updates incoming in wrong order!? => notifyItemUpdated() will sort it out! + } + + void notifyItemCreated(const FileStateDelta& stateDelta, const GdriveItem& item) + { + notifyItemUpdated(stateDelta, item.itemId, &item.details); + } + + void notifyItemUpdated(const FileStateDelta& stateDelta, const GdriveItem& item) + { + notifyItemUpdated(stateDelta, item.itemId, &item.details); + } + + void notifyFolderCreated(const FileStateDelta& stateDelta, const std::string& folderId, const Zstring& folderName, const std::string& parentId) + { + GdriveItemDetails details + { + .itemName = folderName, + .modTime = std::time(nullptr), + .type = GdriveItemType::folder, + .owner = FileOwner::me, + .parentIds{parentId}, + }; + + //avoid needless conflicts due to different Google Drive folder modTime! + if (auto it = itemDetails_.find(folderId); it != itemDetails_.end()) + details.modTime = it->second.modTime; + + notifyItemUpdated(stateDelta, folderId, &details); + } + + void notifyShortcutCreated(const FileStateDelta& stateDelta, const std::string& shortcutId, const Zstring& shortcutName, const std::string& parentId, const std::string& targetId) + { + GdriveItemDetails details + { + .itemName = shortcutName, + .modTime = std::time(nullptr), + .type = GdriveItemType::shortcut, + .owner = FileOwner::me, + .targetId = targetId, + .parentIds{parentId}, + }; + + //avoid needless conflicts due to different Google Drive folder modTime! + if (auto it = itemDetails_.find(shortcutId); it != itemDetails_.end()) + details.modTime = it->second.modTime; + + notifyItemUpdated(stateDelta, shortcutId, &details); + } + + + void notifyItemDeleted(const FileStateDelta& stateDelta, const std::string& itemId) + { + notifyItemUpdated(stateDelta, itemId, nullptr); + } + + void notifyParentRemoved(const FileStateDelta& stateDelta, const std::string& itemId, const std::string& parentIdOld) + { + if (auto it = itemDetails_.find(itemId); it != itemDetails_.end()) + { + GdriveItemDetails detailsNew = it->second; + std::erase(detailsNew.parentIds, parentIdOld); + notifyItemUpdated(stateDelta, itemId, &detailsNew); + } + else //conflict!!! + markSyncDue(); + } + + void notifyMoveAndRename(const FileStateDelta& stateDelta, const std::string& itemId, const std::string& parentIdFrom, const std::string& parentIdTo, const Zstring& newName) + { + if (auto it = itemDetails_.find(itemId); it != itemDetails_.end()) + { + GdriveItemDetails detailsNew = it->second; + detailsNew.itemName = newName; + + std::erase_if(detailsNew.parentIds, [&](const std::string& id) { return id == parentIdFrom || id == parentIdTo; }); // + detailsNew.parentIds.push_back(parentIdTo); //not a duplicate + + notifyItemUpdated(stateDelta, itemId, &detailsNew); + } + else //conflict!!! + markSyncDue(); + } + +private: + GdriveFileState (const GdriveFileState&) = delete; + GdriveFileState& operator=(const GdriveFileState&) = delete; + + friend class GdriveDrivesBuffer; + + void notifyItemUpdated(const FileStateDelta& stateDelta, const std::string& itemId, const GdriveItemDetails* details) + { + if (!stateDelta.changedIds->contains(itemId)) //no conflicting changes in the meantime? + updateItemState(itemId, details); //=> accept new state data + else //conflict? + { + auto it = itemDetails_.find(itemId); + if (!details == (it == itemDetails_.end())) + if (!details || *details == it->second) + return; //notified changes match our current file state + //else: conflict!!! unclear which has the more recent data! + markSyncDue(); + } + } + + FileStateDelta registerFileStateDelta() + { + auto deltaPtr = std::make_shared(); + changeLog_.push_back(deltaPtr); + return FileStateDelta(deltaPtr); + } + + bool syncIsDue() const { return std::chrono::steady_clock::now() >= lastSyncTime_ + GDRIVE_SYNC_INTERVAL; } + + void markSyncDue() { lastSyncTime_ = std::chrono::steady_clock::now() - GDRIVE_SYNC_INTERVAL; } + + void syncWithGoogle() //throw SysError + { + const ChangesDelta delta = getChangesDelta(sharedDriveName_.empty() ? std::string() : driveId_, lastSyncToken_, accessBuf_.getAccessToken()); //throw SysError + + for (const FileChange& change : delta.fileChanges) + updateItemState(change.itemId, get(change.details)); + + lastSyncToken_ = delta.newStartPageToken; + lastSyncTime_ = std::chrono::steady_clock::now(); + + //good to know: if item is created and deleted between polling for changes it is still reported as deleted by Google! + //Same goes for any other change that is undone in between change notification syncs. + } + + PathStatus getPathStatusSub(const std::string& folderId, const AfsPath& folderPath, const std::vector& relPath, bool followLeafShortcut) //throw SysError + { + assert(!relPath.empty()); + + auto itKnown = folderContents_.find(folderId); + if (itKnown == folderContents_.end() || !itKnown->second.isKnownFolder) + { + notifyFolderContent(registerFileStateDelta(), folderId, readFolderContent(folderId, accessBuf_.getAccessToken())); //throw SysError + //perf: always buffered, except for direct, first-time folder access! + itKnown = folderContents_.find(folderId); + assert(itKnown != folderContents_.end()); + if (!itKnown->second.isKnownFolder) + throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); + } + + auto itFound = itemDetails_.cend(); + for (const DetailsIterator& itChild : itKnown->second.childItems) + //Since Google Drive has no concept of a file path, we have to roll our own "path to ID" mapping => let's use the platform-native style + if (equalNativePath(itChild->second.itemName, relPath.front())) + { + if (itFound != itemDetails_.end()) + throw SysError(replaceCpy(_("The name %x is used by more than one item in the folder."), L"%x", fmtPath(relPath.front()))); + + itFound = itChild; + } + + if (itFound == itemDetails_.end()) + return {folderId, GdriveItemType::folder, folderPath, relPath}; //always a folder, see check before recursion above + else + { + auto getItemDetailsBuffered = [&](const std::string& itemId) -> const GdriveItemDetails& + { + auto it = itemDetails_.find(itemId); + if (it == itemDetails_.end()) + { + notifyItemUpdated(registerFileStateDelta(), {itemId, getItemDetails(itemId, accessBuf_.getAccessToken())}); //throw SysError + //perf: always buffered, except for direct, first-time folder access! + it = itemDetails_.find(itemId); + assert(it != itemDetails_.end()); + } + return it->second; + }; + + const auto& [childId, childDetails] = *itFound; + const AfsPath childItemPath(appendPath(folderPath.value, relPath.front())); + const std::vector childRelPath(relPath.begin() + 1, relPath.end()); + + if (childRelPath.empty()) + { + if (childDetails.type == GdriveItemType::shortcut && followLeafShortcut) + return {childDetails.targetId, getItemDetailsBuffered(childDetails.targetId).type, childItemPath, childRelPath}; + else + return {childId, childDetails.type, childItemPath, childRelPath}; + } + + switch (childDetails.type) + { + case GdriveItemType::file: //parent/file/child-rel-path... => obscure, but possible + throw SysError(replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(AFS::getItemName(childItemPath)))); + + case GdriveItemType::folder: + return getPathStatusSub(childId, childItemPath, childRelPath, followLeafShortcut); //throw SysError + + case GdriveItemType::shortcut: + switch (getItemDetailsBuffered(childDetails.targetId).type) + { + case GdriveItemType::file: //parent/file-symlink/child-rel-path... => obscure, but possible + throw SysError(replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(AFS::getItemName(childItemPath)))); + + case GdriveItemType::folder: //parent/folder-symlink/child-rel-path... => always follow + return getPathStatusSub(childDetails.targetId, childItemPath, childRelPath, followLeafShortcut); //throw SysError + + case GdriveItemType::shortcut: //should never happen: creating shortcuts to shortcuts fails with "Internal Error" + throw SysError(replaceCpy(L"Google Drive Shortcut %x is pointing to another Shortcut.", L"%x", fmtPath(AFS::getItemName(childItemPath)))); + } + break; + } + throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); + } + } + + void updateItemState(const std::string& itemId, const GdriveItemDetails* details) + { + auto it = itemDetails_.find(itemId); + if (!details == (it == itemDetails_.end())) + if (!details || *details == it->second) //notified changes match our current file state + return; //=> avoid misleading changeLog_ entries after Google Drive sync!!! + + //update change logs (and clean up obsolete entries) + std::erase_if(changeLog_, [&](std::weak_ptr& weakPtr) + { + if (std::shared_ptr iid = weakPtr.lock()) + { + (*iid).insert(itemId); + return false; + } + else + return true; + }); + + //update file state + if (details) + { + if (it != itemDetails_.end()) //update + { + if (it->second.type != details->type) + throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); //WTF!? + + std::vector parentIdsNew = details->parentIds; + std::vector parentIdsRemoved = it->second.parentIds; + std::erase_if(parentIdsNew, [&](const std::string& id) { return std::find(it->second.parentIds.begin(), it->second.parentIds.end(), id) != it->second.parentIds.end(); }); + std::erase_if(parentIdsRemoved, [&](const std::string& id) { return std::find(details->parentIds.begin(), details->parentIds.end(), id) != details->parentIds.end(); }); + + for (const std::string& parentId : parentIdsNew) + folderContents_[parentId].childItems.push_back(it); //new insert => no need for duplicate check + + for (const std::string& parentId : parentIdsRemoved) + if (auto itP = folderContents_.find(parentId); itP != folderContents_.end()) + std::erase(itP->second.childItems, it); + //if all parents are removed, Google Drive will (recursively) delete the item => don't prematurely do this now: wait for change notifications! + //OR: item without parents located in "Shared with me", but referenced via Shortcut => don't remove!!! + + it->second = *details; + } + else //create + { + auto itNew = itemDetails_.emplace(itemId, *details).first; + + for (const std::string& parentId : details->parentIds) + folderContents_[parentId].childItems.push_back(itNew); //new insert => no need for duplicate check + } + } + else //delete + { + if (it != itemDetails_.end()) + { + for (const std::string& parentId : it->second.parentIds) //1. delete from parent folders + if (auto itP = folderContents_.find(parentId); itP != folderContents_.end()) + std::erase(itP->second.childItems, it); + + itemDetails_.erase(it); + } + + if (auto itP = folderContents_.find(itemId); itP != folderContents_.end()) + { + //2. delete as parent from child items (don't wait for change notifications of children) + // what if e.g. single change notification "folder removed", then folder reapears, + // and no notifications for child items: possible with Google drive!? + // => no problem: FolderContent::isKnownFolder will be false for this restored folder => only a rescan needed + for (auto itChild : itP->second.childItems) + std::erase(itChild->second.parentIds, itemId); + folderContents_.erase(itP); + } + } + } + + using DetailsIterator = std::unordered_map::iterator; + + struct FolderContent + { + bool isKnownFolder = false; //:= we've seen its full content at least once; further changes are calculated via change notifications + std::vector childItems; + }; + std::unordered_map folderContents_; + std::unordered_map itemDetails_; //contains ALL known, existing items! + + std::string lastSyncToken_; //drive-specific(!) marker corresponding to last sync with Google's change notifications + std::chrono::steady_clock::time_point lastSyncTime_ = std::chrono::steady_clock::now() - GDRIVE_SYNC_INTERVAL; //... with Google Drive (default: sync is due) + + std::vector> changeLog_; //track changed items since FileStateDelta was created (includes sync with Google + our own intermediate change notifications) + + std::string driveId_; //ID of shared drive or "My Drive": never empty! + Zstring sharedDriveName_; //name of shared drive: empty for "My Drive"! + + GdriveAccessBuffer& accessBuf_; +}; + + +class GdriveFileStateAtLocation +{ +public: + GdriveFileStateAtLocation(GdriveFileState& fileState, const std::string& locationRootId) : fileState_(fileState), locationRootId_(locationRootId) {} + + GdriveFileState::PathStatus getPathStatus(const AfsPath& itemPath, bool followLeafShortcut) //throw SysError + { + return fileState_.getPathStatus(locationRootId_, itemPath, followLeafShortcut); //throw SysError + } + + std::string /*itemId*/ getItemId(const AfsPath& itemPath, bool followLeafShortcut) //throw SysError + { + return fileState_.getItemId(locationRootId_, itemPath, followLeafShortcut); //throw SysError + } + + std::pair getFileAttributes(const AfsPath& itemPath, bool followLeafShortcut) //throw SysError + { + return fileState_.getFileAttributes(locationRootId_, itemPath, followLeafShortcut); //throw SysError + } + + GdriveFileState& all() { return fileState_; } + +private: + GdriveFileState& fileState_; + const std::string locationRootId_; +}; + + +class GdriveDrivesBuffer +{ +public: + explicit GdriveDrivesBuffer(GdriveAccessBuffer& accessBuf) : + accessBuf_(accessBuf), + myDrive_(getMyDriveId(accessBuf.getAccessToken()), Zstring() /*sharedDriveName*/, accessBuf) {} //throw SysError + + GdriveDrivesBuffer(MemoryStreamIn& stream, GdriveAccessBuffer& accessBuf) : //throw SysError + accessBuf_(accessBuf), + myDrive_(stream, accessBuf) //throw SysError + { + size_t sharedDrivesCount = readNumber(stream); //SysErrorUnexpectedEos + while (sharedDrivesCount-- != 0) + { + auto fileState = makeSharedRef(stream, accessBuf); //throw SysError + sharedDrives_.emplace(fileState.ref().getDriveId(), fileState); + } + } + + void serialize(MemoryStreamOut& stream) const + { + myDrive_.serialize(stream); + + writeNumber(stream, static_cast(sharedDrives_.size())); + for (const auto& [driveId, fileState] : sharedDrives_) + fileState.ref().serialize(stream); + + //starredFolders_? no, will be fully restored by syncWithGoogle() + } + + std::vector listLocations() //throw SysError + { + if (syncIsDue()) + syncWithGoogle(); //throw SysError + + std::vector locationNames; + + for (const auto& [driveId, fileState] : sharedDrives_) + locationNames.push_back(fileState.ref().getSharedDriveName()); + + for (const StarredFolderDetails& sfd : starredFolders_) + locationNames.push_back(sfd.folderName); + + return locationNames; + } + + std::pair prepareAccess(const Zstring& locationName) //throw SysError + { + //checking for added/renamed/deleted shared drives *every* GDRIVE_SYNC_INTERVAL is needlessly excessive! + // => check 1. once per FFS run + // 2. on drive access error + if (lastSyncTime_ == std::chrono::steady_clock::time_point()) + syncWithGoogle(); //throw SysError + + GdriveFileStateAtLocation fileState = [&] + { + try + { + return getFileState(locationName); //throw SysError + } + catch (SysError&) + { + if (syncIsDue()) + syncWithGoogle(); //throw SysError + + return getFileState(locationName); //throw SysError + } + }(); + + //manage last sync time here so that "lastSyncToken" remains stable while accessing GdriveFileState in the callback + if (fileState.all().syncIsDue()) + fileState.all().syncWithGoogle(); //throw SysError + + return {fileState, fileState.all().registerFileStateDelta()}; + } + +private: + bool syncIsDue() const { return std::chrono::steady_clock::now() >= lastSyncTime_ + GDRIVE_SYNC_INTERVAL; } + + void syncWithGoogle() //throw SysError + { + //run in parallel with getSharedDrives() + auto ftStarredFolders = runAsync([access = accessBuf_.getAccessToken() /*throw SysError*/] { return getStarredFolders(access); /*throw SysError*/ }); + + decltype(sharedDrives_) currentDrives; + + //getSharedDrives() should be fast enough to avoid the unjustified complexity of change notifications: https://freefilesync.org/forum/viewtopic.php?t=7827&start=30#p29712 + for (const auto& [driveId, driveName] : getSharedDrives(accessBuf_.getAccessToken())) //throw SysError + { + auto fileState = [&, &driveId /*clang bug*/= driveId, &driveName /*clang bug*/= driveName] + { + if (auto it = sharedDrives_.find(driveId); + it != sharedDrives_.end()) + { + it->second.ref().setSharedDriveName(driveName); + return it->second; + } + else + return makeSharedRef(driveId, driveName, accessBuf_); //throw SysError + }(); + currentDrives.emplace(driveId, fileState); + } + + starredFolders_ = ftStarredFolders.get(); //throw SysError // + sharedDrives_.swap(currentDrives); //transaction! + lastSyncTime_ = std::chrono::steady_clock::now(); //...(uhm, mostly, except for setSharedDriveName()) + } + + GdriveFileStateAtLocation getFileState(const Zstring& locationName) //throw SysError + { + if (locationName.empty()) + return {myDrive_, myDrive_.getDriveId()}; + + GdriveFileState* fileState = nullptr; + std::string locationRootId; + + for (auto& [driveId, fileStateRef] : sharedDrives_) + if (equalNativePath(fileStateRef.ref().getSharedDriveName(), locationName)) + { + if (fileState) + throw SysError(replaceCpy(_("The name %x is used by more than one item in the folder."), L"%x", fmtPath(locationName))); + + fileState = &fileStateRef.ref(); + locationRootId = driveId; + } + + for (const StarredFolderDetails& sfd : starredFolders_) + if (equalNativePath(sfd.folderName, locationName)) + { + if (fileState) + throw SysError(replaceCpy(_("The name %x is used by more than one item in the folder."), L"%x", fmtPath(locationName))); + + if (sfd.sharedDriveId.empty()) //=> My Drive + fileState = &myDrive_; + else + { + auto it = sharedDrives_.find(sfd.sharedDriveId); + if (it == sharedDrives_.end()) + break; + + fileState = &it->second.ref(); + } + locationRootId = sfd.folderId; + } + + if (!fileState) + throw SysError(replaceCpy(_("%x does not exist."), L"%x", fmtPath(locationName))); + + return {*fileState, locationRootId}; + } + + GdriveAccessBuffer& accessBuf_; + std::chrono::steady_clock::time_point lastSyncTime_; //... with Google Drive (default: sync is due) + + GdriveFileState myDrive_; + std::unordered_map> sharedDrives_; + + std::vector starredFolders_; +}; + +//========================================================================================== +//========================================================================================== + +class GdrivePersistentSessions +{ +public: + explicit GdrivePersistentSessions(const Zstring& configDirPath) : configDirPath_(configDirPath) + { + onSystemShutdownRegister(onBeforeSystemShutdownCookie_); + } + + void saveActiveSessions() //throw FileError + { + std::vector*> protectedSessions; //pointers remain stable, thanks to std::unordered_map<> + globalSessions_.access([&](GlobalSessions& sessions) + { + for (auto& [accountEmail, protectedSession] : sessions) + protectedSessions.push_back(&protectedSession); + }); + + if (!protectedSessions.empty()) + { + createDirectoryIfMissingRecursion(configDirPath_); //throw FileError + + std::exception_ptr firstError; + + //access each session outside the globalSessions_ lock! + for (Protected* protectedSession : protectedSessions) + protectedSession->access([&](SessionHolder& holder) + { + if (holder.session) + try + { + const Zstring dbFilePath = getDbFilePath(holder.session->accessBuf.ref().getUserEmail()); + saveSession(dbFilePath, *holder.session); //throw FileError + } + catch (FileError&) { if (!firstError) firstError = std::current_exception(); } + }); + + if (firstError) + std::rethrow_exception(firstError); //throw FileError + } + } + + std::string addUserSession(const std::string& gdriveLoginHint, const std::function& updateGui /*throw X*/, int timeoutSec) //throw SysError, X + { + const GdriveAccessInfo accessInfo = gdriveAuthorizeAccess(gdriveLoginHint, updateGui, timeoutSec); //throw SysError, X + + accessUserSession(accessInfo.userInfo.email, timeoutSec, [&](std::optional& userSession) //throw SysError + { + if (userSession) + userSession->accessBuf.ref().update(accessInfo); //redundant? + else + { + const std::shared_ptr timeoutSec2 = std::make_shared(timeoutSec); //context option: valid only for duration of this call! + auto accessBuf = makeSharedRef(accessInfo); + accessBuf.ref().setContextTimeout(timeoutSec2); //[!] used by GdriveDrivesBuffer()! + auto drivesBuf = makeSharedRef(accessBuf.ref()); //throw SysError + userSession = {accessBuf, drivesBuf}; + } + }); + + return accessInfo.userInfo.email; + } + + void removeUserSession(const std::string& accountEmail, int timeoutSec) //throw SysError + { + try + { + accessUserSession(accountEmail, timeoutSec, [&](std::optional& userSession) //throw SysError + { + if (userSession) + gdriveRevokeAccess(userSession->accessBuf.ref().getAccessToken()); //throw SysError + }); + } + catch ([[maybe_unused]] const SysError& e) { assert(false); } //best effort: try to invalidate the access token + //=> expected to fail 1. if offline => not worse than removing FFS via "Uninstall Programs" 2. already revoked 3. if DB is corrupted + + try + { + //start with deleting the DB file (1. maybe it's corrupted? 2. skip unnecessary lazy-load) + const Zstring dbFilePath = getDbFilePath(accountEmail); + try + { + removeFilePlain(dbFilePath); //throw FileError + } + catch (FileError&) + { + if (itemExists(dbFilePath)) //throw FileError + throw; + } + } + catch (const FileError& e) { throw SysError(replaceCpy(e.toString(), L"\n\n", L'\n')); } //file access errors should be further enriched by context info => SysError + + + accessUserSession(accountEmail, timeoutSec, [&](std::optional& userSession) //throw SysError + { + userSession.reset(); + }); + } + + std::vector listAccounts() //throw SysError + { + std::vector emails; + + std::vector*> protectedSessions; //pointers remain stable, thanks to std::unordered_map<> + globalSessions_.access([&](GlobalSessions& sessions) + { + for (auto& [accountEmail, protectedSession] : sessions) + protectedSessions.push_back(&protectedSession); + }); + + //access each session outside the globalSessions_ lock! + for (Protected* protectedSession : protectedSessions) + protectedSession->access([&](SessionHolder& holder) + { + if (holder.session) + emails.push_back(holder.session->accessBuf.ref().getUserEmail()); + }); + + //also include available, but not-yet-loaded sessions + try + { + traverseFolder(configDirPath_, + [&](const FileInfo& fi) { if (endsWith(fi.itemName, Zstr(".db"))) emails.push_back(utfTo(beforeLast(fi.itemName, Zstr('.'), IfNotFoundReturn::none))); }, + [&](const FolderInfo& fi) {}, + [&](const SymlinkInfo& si) {}); //throw FileError + } + catch (FileError&) + { + try + { + if (itemExists(configDirPath_)) //throw FileError + throw; + } + catch (const FileError& e) { throw SysError(replaceCpy(e.toString(), L"\n\n", L'\n')); } //file access errors should be further enriched by context info => SysError + } + + removeDuplicates(emails, LessAsciiNoCase()); + return emails; + } + + std::vector listLocations(const std::string& accountEmail, int timeoutSec) //throw SysError + { + std::vector locationNames; + + accessUserSession(accountEmail, timeoutSec, [&](std::optional& userSession) //throw SysError + { + if (!userSession) + throw SysError(replaceCpy(_("Please add a connection to user account %x first."), L"%x", utfTo(accountEmail))); + + locationNames = userSession->drivesBuf.ref().listLocations(); //throw SysError + }); + return locationNames; + } + + struct AsyncAccessInfo + { + GdriveAccess access; //don't allow (long-running) web requests while holding the global session lock! + GdriveFileState::FileStateDelta stateDelta; + }; + //perf: amortized fully buffered! + AsyncAccessInfo accessGlobalFileState(const GdriveLogin& login, const std::function& useFileState /*throw X*/) //throw SysError, X + { + GdriveAccess access; + GdriveFileState::FileStateDelta stateDelta; + + accessUserSession(login.email, login.timeoutSec, [&](std::optional& userSession) //throw SysError + { + if (!userSession) + throw SysError(replaceCpy(_("Please add a connection to user account %x first."), L"%x", utfTo(login.email))); + + access = userSession->accessBuf.ref().getAccessToken(); //throw SysError + auto [fileState, stateDelta2] = userSession->drivesBuf.ref().prepareAccess(login.locationName); //throw SysError + stateDelta = std::move(stateDelta2); + + useFileState(fileState); //throw X + }); + return {access, stateDelta}; + } + +private: + GdrivePersistentSessions (const GdrivePersistentSessions&) = delete; + GdrivePersistentSessions& operator=(const GdrivePersistentSessions&) = delete; + + struct UserSession; + + Zstring getDbFilePath(std::string accountEmail) const + { + for (char& c : accountEmail) + c = asciiToLower(c); + //return appendPath(configDirPath_, utfTo(formatAsHexString(getMd5(utfTo(accountEmail)))) + Zstr(".db")); + return appendPath(configDirPath_, utfTo(accountEmail) + Zstr(".db")); + } + + void accessUserSession(const std::string& accountEmail, int timeoutSec, const std::function& userSession)>& useSession /*throw X*/) //throw SysError, X + { + Protected* protectedSession = nullptr; //pointers remain stable, thanks to std::unordered_map<> + globalSessions_.access([&](GlobalSessions& sessions) { protectedSession = &sessions[accountEmail]; }); + + protectedSession->access([&](SessionHolder& holder) + { + if (!holder.dbWasLoaded) //let's NOT load the DB files under the globalSessions_ lock, but the session-specific one! + try + { + holder.session = loadSession(getDbFilePath(accountEmail), timeoutSec); //throw SysError + } + catch (const FileError& e) { throw SysError(replaceCpy(e.toString(), L"\n\n", L'\n')); } //GdrivePersistentSessions errors should be further enriched with context info => SysError + holder.dbWasLoaded = true; + + const std::shared_ptr timeoutSec2 = std::make_shared(timeoutSec); //context option: valid only for duration of this call! + if (holder.session) + holder.session->accessBuf.ref().setContextTimeout(timeoutSec2); + + useSession(holder.session); //throw X + }); + } + + static void saveSession(const Zstring& dbFilePath, const UserSession& userSession) //throw FileError + { + MemoryStreamOut streamOut; + writeArray(streamOut, DB_FILE_DESCR, sizeof(DB_FILE_DESCR)); + writeNumber(streamOut, DB_FILE_VERSION); + + MemoryStreamOut streamOutBody; + userSession.accessBuf.ref().serialize(streamOutBody); + userSession.drivesBuf.ref().serialize(streamOutBody); + + try + { + streamOut.ref() += compress(streamOutBody.ref(), 3 /*best compression level: see db_file.cpp*/); //throw SysError + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(dbFilePath)), e.toString()); } + + setFileContent(dbFilePath, streamOut.ref(), nullptr /*notifyUnbufferedIO*/); //throw FileError + } + + static std::optional loadSession(const Zstring& dbFilePath, int timeoutSec) //throw FileError + { + std::string byteStream; + try + { + byteStream = getFileContent(dbFilePath, nullptr /*notifyUnbufferedIO*/); //throw FileError + } + catch (FileError&) + { + if (itemExists(dbFilePath)) //throw FileError + throw; + + return std::nullopt; + } + + try + { + MemoryStreamIn streamIn(byteStream); + //-------- file format header -------- + char tmp[sizeof(DB_FILE_DESCR)] = {}; + readArray(streamIn, &tmp, sizeof(tmp)); //throw SysErrorUnexpectedEos + + const std::shared_ptr timeoutSec2 = std::make_shared(timeoutSec); //context option: valid only for duration of this call! + + //TODO: remove migration code at some time! 2020-07-03 + if (!std::equal(std::begin(tmp), std::end(tmp), std::begin(DB_FILE_DESCR))) + { + const std::string& uncompressedStream = decompress(byteStream); //throw SysError + MemoryStreamIn streamIn2(uncompressedStream); + //-------- file format header -------- + const char DB_FILE_DESCR_OLD[] = "FreeFileSync: Google Drive Database"; + char tmp2[sizeof(DB_FILE_DESCR_OLD)] = {}; + readArray(streamIn2, &tmp2, sizeof(tmp2)); //throw SysErrorUnexpectedEos + + if (!std::equal(std::begin(tmp2), std::end(tmp2), std::begin(DB_FILE_DESCR_OLD))) + throw SysError(_("File content is corrupted.") + L" (invalid header)"); + + const int version = readNumber(streamIn2); //throw SysErrorUnexpectedEos + if (version != 1 && //TODO: remove migration code at some time! 2019-12-05 + version != 2 && //TODO: remove migration code at some time! 2020-06-11 + version != 3) //TODO: remove migration code at some time! 2020-07-03 + throw SysError(_("Unsupported data format.") + L' ' + replaceCpy(_("Version: %x"), L"%x", numberTo(version))); + + //version 1 + 2: fully discard old state due to missing "ownedByMe" attribute + shortcut support + //version 3: fully discard old state due to revamped shared drive handling + auto accessBuf = makeSharedRef(streamIn2); //throw SysError + accessBuf.ref().setContextTimeout(timeoutSec2); //not used by GdriveDrivesBuffer(), but let's be consistent + auto drivesBuf = makeSharedRef(accessBuf.ref()); //throw SysError + return UserSession{accessBuf, drivesBuf}; + } + else + { + if (!std::equal(std::begin(tmp), std::end(tmp), std::begin(DB_FILE_DESCR))) + throw SysError(_("File content is corrupted.") + L" (invalid header)"); + + const int version = readNumber(streamIn); //throw SysErrorUnexpectedEos + if (version != 4 && + version != DB_FILE_VERSION) + throw SysError(_("Unsupported data format.") + L' ' + replaceCpy(_("Version: %x"), L"%x", numberTo(version))); + + const std::string& uncompressedStream = decompress(makeStringView(byteStream.begin() + streamIn.pos(), byteStream.end())); //throw SysError + MemoryStreamIn streamInBody(uncompressedStream); + + auto accessBuf = makeSharedRef(streamInBody); //throw SysError + accessBuf.ref().setContextTimeout(timeoutSec2); //not used by GdriveDrivesBuffer(), but let's be consistent + auto drivesBuf = [&] + { + //TODO: remove migration code at some time! 2021-05-15 + if (version <= 4) //fully discard old state due to revamped shared drive handling + return makeSharedRef(accessBuf.ref()); //throw SysError + else + return makeSharedRef(streamInBody, accessBuf.ref()); //throw SysError + }(); + + return UserSession{accessBuf, drivesBuf}; + } + } + catch (const SysError& e) + { + throw FileError(replaceCpy(_("Cannot read database file %x."), L"%x", fmtPath(dbFilePath)), e.toString()); + } + } + + struct UserSession + { + SharedRef accessBuf; + SharedRef drivesBuf; + }; + + struct SessionHolder + { + bool dbWasLoaded = false; + std::optional session; + }; + using GlobalSessions = std::unordered_map, StringHashAsciiNoCase, StringEqualAsciiNoCase>; + + Protected globalSessions_; + const Zstring configDirPath_; + + const SharedRef> onBeforeSystemShutdownCookie_ = makeSharedRef>([this] + { + try //let's not lose Google Drive data due to unexpected system shutdown: + { saveActiveSessions(); } //throw FileError + catch (const FileError& e) { logExtraError(e.toString()); } + }); +}; +//========================================================================================== +constinit Global globalGdriveSessions; +//========================================================================================== + +GdrivePersistentSessions::AsyncAccessInfo accessGlobalFileState(const GdriveLogin& login, const std::function& useFileState /*throw X*/) //throw SysError, X +{ + if (const std::shared_ptr gps = globalGdriveSessions.get()) + return gps->accessGlobalFileState(login, useFileState); //throw SysError, X + + throw SysError(formatSystemError("accessGlobalFileState", L"", L"Function call not allowed during init/shutdown.")); +} + +//========================================================================================== +//========================================================================================== + +struct GetDirDetails +{ + GetDirDetails(const GdrivePath& folderPath) : folderPath_(folderPath) {} + + struct Result + { + std::vector childItems; + GdrivePath folderPath; + }; + Result operator()() const + { + try + { + std::string folderId; + std::optional> childItemsBuf; + const GdrivePersistentSessions::AsyncAccessInfo aai = accessGlobalFileState(folderPath_.gdriveLogin, [&](GdriveFileStateAtLocation& fileState) //throw SysError + { + const auto& [itemId, itemDetails] = fileState.getFileAttributes(folderPath_.itemPath, true /*followLeafShortcut*/); //throw SysError + + if (itemDetails.type != GdriveItemType::folder) //check(!) or readFolderContent() will return empty (without failing!) + throw SysError(replaceCpy(L"%x is not a directory.", L"%x", fmtPath(utfTo(itemDetails.itemName)))); + + folderId = itemId; + childItemsBuf = fileState.all().tryGetBufferedFolderContent(folderId); + }); + + if (!childItemsBuf) + { + childItemsBuf = readFolderContent(folderId, aai.access); //throw SysError + + //buffer new file state ASAP => make sure accessGlobalFileState() has amortized constant access (despite the occasional internal readFolderContent() on non-leaf folders) + accessGlobalFileState(folderPath_.gdriveLogin, [&](GdriveFileStateAtLocation& fileState) //throw SysError + { + fileState.all().notifyFolderContent(aai.stateDelta, folderId, *childItemsBuf); + }); + } + + for (const GdriveItem& item : *childItemsBuf) + if (item.details.itemName.empty()) + throw SysError(L"Folder contains an item without name."); //mostly an issue for FFS's folder traversal, but NOT for globalGdriveSessions! + + return {std::move(*childItemsBuf), folderPath_}; + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read directory %x."), L"%x", fmtPath(getGdriveDisplayPath(folderPath_))), e.toString()); } + } + +private: + GdrivePath folderPath_; +}; + + +struct GetShortcutTargetDetails +{ + GetShortcutTargetDetails(const GdrivePath& shortcutPath, const GdriveItemDetails& shortcutDetails) : shortcutPath_(shortcutPath), shortcutDetails_(shortcutDetails) {} + + struct Result + { + GdriveItemDetails target; + GdriveItemDetails shortcut; + GdrivePath shortcutPath; + }; + Result operator()() const + { + try + { + std::optional targetDetailsBuf; + const GdrivePersistentSessions::AsyncAccessInfo aai = accessGlobalFileState(shortcutPath_.gdriveLogin, [&](GdriveFileStateAtLocation& fileState) //throw SysError + { + targetDetailsBuf = fileState.all().tryGetBufferedItemDetails(shortcutDetails_.targetId); + }); + if (!targetDetailsBuf) + { + targetDetailsBuf = getItemDetails(shortcutDetails_.targetId, aai.access); //throw SysError + + //buffer new file state ASAP + accessGlobalFileState(shortcutPath_.gdriveLogin, [&](GdriveFileStateAtLocation& fileState) //throw SysError + { + fileState.all().notifyItemUpdated(aai.stateDelta, {shortcutDetails_.targetId, *targetDetailsBuf}); + }); + } + + assert(targetDetailsBuf->targetId.empty()); + if (targetDetailsBuf->type == GdriveItemType::shortcut) //should never happen: creating shortcuts to shortcuts fails with "Internal Error" + throw SysError(L"Google Drive Shortcut points to another Shortcut."); + + return {std::move(*targetDetailsBuf), shortcutDetails_, shortcutPath_}; + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot resolve symbolic link %x."), L"%x", fmtPath(getGdriveDisplayPath(shortcutPath_))), e.toString()); } + } + +private: + GdrivePath shortcutPath_; + GdriveItemDetails shortcutDetails_; +}; + + +class SingleFolderTraverser +{ +public: + SingleFolderTraverser(const GdriveLogin& gdriveLogin, const std::vector>>& workload /*throw X*/) : + gdriveLogin_(gdriveLogin), workload_(workload) + { + while (!workload_.empty()) + { + auto wi = std::move(workload_. back()); //yes, no strong exception guarantee (std::bad_alloc) + /**/ workload_.pop_back(); // + const auto& [folderPath, cb] = wi; + + tryReportingDirError([&] //throw X + { + traverseWithException(folderPath, *cb); //throw FileError, X + }, *cb); + } + } + +private: + SingleFolderTraverser (const SingleFolderTraverser&) = delete; + SingleFolderTraverser& operator=(const SingleFolderTraverser&) = delete; + + void traverseWithException(const AfsPath& folderPath, AFS::TraverserCallback& cb) //throw FileError, X + { + const std::vector& childItems = GetDirDetails({gdriveLogin_, folderPath})().childItems; //throw FileError + + for (const GdriveItem& item : childItems) + { + const Zstring itemName = utfTo(item.details.itemName); + + switch (item.details.type) + { + case GdriveItemType::file: + cb.onFile({itemName, item.details.fileSize, item.details.modTime, getGdriveFilePrint(item.itemId), false /*isFollowedSymlink*/}); //throw X + break; + + case GdriveItemType::folder: + if (std::shared_ptr cbSub = cb.onFolder({itemName, false /*isFollowedSymlink*/})) //throw X + { + const AfsPath afsItemPath(appendPath(folderPath.value, itemName)); + workload_.push_back({afsItemPath, std::move(cbSub)}); + } + break; + + case GdriveItemType::shortcut: + switch (cb.onSymlink({itemName, item.details.modTime})) //throw X + { + case AFS::TraverserCallback::HandleLink::follow: + { + const AfsPath afsItemPath(appendPath(folderPath.value, itemName)); + + GdriveItemDetails targetDetails = {}; + if (!tryReportingItemError([&] //throw X + { + targetDetails = GetShortcutTargetDetails({gdriveLogin_, afsItemPath}, item.details)().target; //throw FileError + }, cb, itemName)) + continue; + + if (targetDetails.type == GdriveItemType::folder) + { + if (std::shared_ptr cbSub = cb.onFolder({itemName, true /*isFollowedSymlink*/})) //throw X + workload_.push_back({afsItemPath, std::move(cbSub)}); + } + else //a file or named pipe, etc. + cb.onFile({itemName, targetDetails.fileSize, targetDetails.modTime, getGdriveFilePrint(item.details.targetId), true /*isFollowedSymlink*/}); //throw X + } + break; + + case AFS::TraverserCallback::HandleLink::skip: + break; + } + break; + } + } + } + + const GdriveLogin gdriveLogin_; + std::vector>> workload_; +}; + + +void gdriveTraverseFolderRecursive(const GdriveLogin& gdriveLogin, const std::vector>>& workload /*throw X*/, size_t) //throw X +{ + SingleFolderTraverser dummy(gdriveLogin, workload); //throw X +} +//========================================================================================== +//========================================================================================== + +struct InputStreamGdrive : public AFS::InputStream +{ + explicit InputStreamGdrive(const GdrivePath& gdrivePath) : + gdrivePath_(gdrivePath) + { + worker_ = InterruptibleThread([asyncStreamOut = this->asyncStreamIn_, gdrivePath] + { + setCurrentThreadName(Zstr("Istream ") + utfTo(getGdriveDisplayPath(gdrivePath))); + try + { + GdriveAccess access; + std::string fileId; + try + { + access = accessGlobalFileState(gdrivePath.gdriveLogin, [&](GdriveFileStateAtLocation& fileState) //throw SysError + { + fileId = fileState.getItemId(gdrivePath.itemPath, true /*followLeafShortcut*/); //throw SysError + }).access; + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot open file %x."), L"%x", fmtPath(getGdriveDisplayPath(gdrivePath))), e.toString()); } + + try + { + auto writeBlock = [&](const void* buffer, size_t bytesToWrite) + { + asyncStreamOut->write(buffer, bytesToWrite); //throw ThreadStopRequest + }; + gdriveDownloadFile(fileId, writeBlock, access); //throw SysError, ThreadStopRequest + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(getGdriveDisplayPath(gdrivePath))), e.toString()); } + + asyncStreamOut->closeStream(); + } + catch (FileError&) { asyncStreamOut->setWriteError(std::current_exception()); } //let ThreadStopRequest pass through! + }); + } + + ~InputStreamGdrive() + { + asyncStreamIn_->setReadError(std::make_exception_ptr(ThreadStopRequest())); + } + + size_t getBlockSize() override { return GDRIVE_BLOCK_SIZE_DOWNLOAD; } //throw (FileError) + + //may return short; only 0 means EOF! CONTRACT: bytesToRead > 0! + size_t tryRead(void* buffer, size_t bytesToRead, const IoCallback& notifyUnbufferedIO /*throw X*/) override //throw FileError, (ErrorFileLocked), X + { + const size_t bytesRead = asyncStreamIn_->tryRead(buffer, bytesToRead); //throw FileError + reportBytesProcessed(notifyUnbufferedIO); //throw X + return bytesRead; + //no need for asyncStreamIn_->checkWriteErrors(): once end of stream is reached, asyncStreamOut->closeStream() was called => no errors occured + } + + std::optional tryGetAttributesFast() override //throw FileError + { + AFS::StreamAttributes attr = {}; + try + { + accessGlobalFileState(gdrivePath_.gdriveLogin, [&](GdriveFileStateAtLocation& fileState) //throw SysError + { + const auto& [itemId, itemDetails] = fileState.getFileAttributes(gdrivePath_.itemPath, true /*followLeafShortcut*/); //throw SysError + attr.modTime = itemDetails.modTime; + attr.fileSize = itemDetails.fileSize; + attr.filePrint = getGdriveFilePrint(itemId); + }); + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(getGdriveDisplayPath(gdrivePath_))), e.toString()); } + return std::move(attr); //[!] + } + +private: + void reportBytesProcessed(const IoCallback& notifyUnbufferedIO /*throw X*/) //throw X + { + const int64_t bytesDelta = makeSigned(asyncStreamIn_->getTotalBytesWritten()) - totalBytesReported_; + totalBytesReported_ += bytesDelta; + if (notifyUnbufferedIO) notifyUnbufferedIO(bytesDelta); //throw X + } + + const GdrivePath gdrivePath_; + int64_t totalBytesReported_ = 0; + std::shared_ptr asyncStreamIn_ = std::make_shared(GDRIVE_STREAM_BUFFER_SIZE); + InterruptibleThread worker_; +}; + +//========================================================================================== + +//already existing: 1. fails or 2. creates duplicate +struct OutputStreamGdrive : public AFS::OutputStreamImpl +{ + OutputStreamGdrive(const GdrivePath& gdrivePath, + std::optional /*streamSize*/, + std::optional modTime, + std::unique_ptr&& pal) : //throw SysError + gdrivePath_(gdrivePath) + { + std::promise promFilePrint; + futFilePrint_ = promFilePrint.get_future(); + + //CAVEAT: if file is already existing, OutputStreamGdrive *constructor* must fail, not OutputStreamGdrive::write(), + // otherwise ~OutputStreamImpl() will delete the already existing file! => don't check asynchronously! + const Zstring fileName = AFS::getItemName(gdrivePath.itemPath); + std::string parentId; + /*const*/ GdrivePersistentSessions::AsyncAccessInfo aai = accessGlobalFileState(gdrivePath.gdriveLogin, [&](GdriveFileStateAtLocation& fileState) //throw SysError + { + const GdriveFileState::PathStatus& ps = fileState.getPathStatus(gdrivePath.itemPath, false /*followLeafShortcut*/); //throw SysError + if (ps.relPath.empty()) + throw SysError(replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(fileName))); + + if (ps.relPath.size() > 1) //parent folder missing + throw SysError(replaceCpy(_("%x does not exist."), L"%x", fmtPath(ps.relPath.front()))); + + parentId = ps.existingItemId; + }); + + worker_ = InterruptibleThread([gdrivePath, modTime, fileName, asyncStreamIn = this->asyncStreamOut_, + pFilePrint = std::move(promFilePrint), + parentId = std::move(parentId), + aai = std::move(aai), + pal = std::move(pal)]() mutable + { + assert(pal); //bind life time to worker thread! + setCurrentThreadName(Zstr("Ostream ") + utfTo(getGdriveDisplayPath(gdrivePath))); + try + { + auto tryReadBlock = [&](void* buffer, size_t bytesToRead) //may return short, only 0 means EOF! + { + return asyncStreamIn->tryRead(buffer, bytesToRead); //throw ThreadStopRequest + }; + //for whatever reason, gdriveUploadFile() is slightly faster than gdriveUploadSmallFile()! despite its two roundtrips! even when file sizes are 0! + //=> 1. issue likely on Google's side => 2. persists even after having fixed "Expect: 100-continue" + const std::string fileIdNew = //streamSize && *streamSize < 5 * 1024 * 1024 ? + //gdriveUploadSmallFile(fileName, parentId, *streamSize, modTime, readBlock, aai.access) : //throw SysError, ThreadStopRequest + gdriveUploadFile (fileName, parentId, modTime, tryReadBlock, aai.access); //throw SysError, ThreadStopRequest + assert(asyncStreamIn->getTotalBytesRead() == asyncStreamIn->getTotalBytesWritten()); + //already existing: creates duplicate + + //buffer new file state ASAP (don't wait GDRIVE_SYNC_INTERVAL) + GdriveItem newFileItem + { + .itemId = fileIdNew, + .details{ + .itemName = fileName, + .fileSize = asyncStreamIn->getTotalBytesRead(), + .type = GdriveItemType::file, + .owner = FileOwner::me, + } + }; + if (modTime) //else: whatever modTime Google Drive selects will be notified after GDRIVE_SYNC_INTERVAL + newFileItem.details.modTime = *modTime; + newFileItem.details.parentIds.push_back(parentId); + + accessGlobalFileState(gdrivePath.gdriveLogin, [&](GdriveFileStateAtLocation& fileState) //throw SysError + { + fileState.all().notifyItemCreated(aai.stateDelta, newFileItem); + }); + + pFilePrint.set_value(getGdriveFilePrint(fileIdNew)); + } + catch (const SysError& e) + { + FileError fe(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(getGdriveDisplayPath(gdrivePath))), e.toString()); + const std::exception_ptr exptr = std::make_exception_ptr(std::move(fe)); + asyncStreamIn->setReadError(exptr); //set both! + pFilePrint.set_exception(exptr); // + } + //let ThreadStopRequest pass through! + }); + } + + ~OutputStreamGdrive() + { + if (asyncStreamOut_) //=> cleanup non-finalized output file + { + asyncStreamOut_->setWriteError(std::make_exception_ptr(ThreadStopRequest())); + worker_.join(); + + try //see removeFilePlain() + { + std::optional itemId; + const GdrivePersistentSessions::AsyncAccessInfo aai = accessGlobalFileState(gdrivePath_.gdriveLogin, [&](GdriveFileStateAtLocation& fileState) //throw SysError + { + const GdriveFileState::PathStatus ps = fileState.getPathStatus(gdrivePath_.itemPath, false /*followLeafShortcut*/); //throw SysError + if (ps.relPath.empty()) + itemId = ps.existingItemId; + }); + if (itemId) + { + gdriveDeleteItem(*itemId, aai.access); //throw SysError + + //buffer new file state ASAP (don't wait GDRIVE_SYNC_INTERVAL) + accessGlobalFileState(gdrivePath_.gdriveLogin, [&](GdriveFileStateAtLocation& fileState) //throw SysError + { + fileState.all().notifyItemDeleted(aai.stateDelta, *itemId); + }); + } + } + catch (const SysError& e) + { + logExtraError(replaceCpy(_("Cannot delete file %x."), L"%x", fmtPath(getGdriveDisplayPath(gdrivePath_))) + L"\n\n" + e.toString()); + } + } + } + + size_t getBlockSize() override { return GDRIVE_BLOCK_SIZE_UPLOAD; } //throw (FileError) + + size_t tryWrite(const void* buffer, size_t bytesToWrite, const IoCallback& notifyUnbufferedIO /*throw X*/) override //throw FileError, X; may return short! CONTRACT: bytesToWrite > 0 + { + const size_t bytesWritten = asyncStreamOut_->tryWrite(buffer, bytesToWrite); //throw FileError + reportBytesProcessed(notifyUnbufferedIO); //throw X + return bytesWritten; + } + + AFS::FinalizeResult finalize(const IoCallback& notifyUnbufferedIO /*throw X*/) override //throw FileError, X + { + if (!asyncStreamOut_) + throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); + + asyncStreamOut_->closeStream(); + + while (futFilePrint_.wait_for(std::chrono::milliseconds(25)) == std::future_status::timeout) + reportBytesProcessed(notifyUnbufferedIO); //throw X + reportBytesProcessed(notifyUnbufferedIO); //[!] once more, now that *all* bytes were written + + AFS::FinalizeResult result; + assert(isReady(futFilePrint_)); + result.filePrint = futFilePrint_.get(); //throw FileError + + //asyncStreamOut_->checkReadErrors(); //throw FileError -> not needed after *successful* upload + asyncStreamOut_.reset(); //output finalized => no more exceptions from here on! + //-------------------------------------------------------------------- + + //result.errorModTime -> already (successfully) set during file creation + return result; + } + +private: + void reportBytesProcessed(const IoCallback& notifyUnbufferedIO /*throw X*/) //throw X + { + const int64_t bytesDelta = makeSigned(asyncStreamOut_->getTotalBytesRead()) - totalBytesReported_; + totalBytesReported_ += bytesDelta; + if (notifyUnbufferedIO) notifyUnbufferedIO(bytesDelta); //throw X + } + + const GdrivePath gdrivePath_; + int64_t totalBytesReported_ = 0; + std::shared_ptr asyncStreamOut_ = std::make_shared(GDRIVE_STREAM_BUFFER_SIZE); + InterruptibleThread worker_; + std::future futFilePrint_; +}; + +//========================================================================================== + +class GdriveFileSystem : public AbstractFileSystem +{ +public: + explicit GdriveFileSystem(const GdriveLogin& gdriveLogin) : gdriveLogin_(gdriveLogin) {} + + const GdriveLogin& getGdriveLogin() const { return gdriveLogin_; } + + Zstring getFolderUrl(const AfsPath& folderPath) const //throw FileError + { + try + { + GdriveFileState::PathStatus ps; + accessGlobalFileState(gdriveLogin_, [&](GdriveFileStateAtLocation& fileState) //throw SysError + { + ps = fileState.getPathStatus(folderPath, true /*followLeafShortcut*/); //throw SysError + }); + + if (!ps.relPath.empty()) + throw SysError(replaceCpy(_("%x does not exist."), L"%x", fmtPath(ps.relPath.front()))); + + if (ps.existingType != GdriveItemType::folder) + throw SysError(replaceCpy(L"%x is not a folder.", L"%x", fmtPath(getItemName(folderPath)))); + + return Zstr("https://drive.google.com/drive/folders/") + utfTo(ps.existingItemId); + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read directory %x."), L"%x", fmtPath(getDisplayPath(folderPath))), e.toString()); } + } + +private: + GdrivePath getGdrivePath(const AfsPath& itemPath) const { return {gdriveLogin_, itemPath}; } + + GdriveRawPath getGdriveRawPath(const AfsPath& itemPath) const //throw SysError + { + const std::optional parentPath = getParentPath(itemPath); + if (!parentPath) + throw SysError(L"Item is device root"); + + std::string parentId; + accessGlobalFileState(gdriveLogin_, [&](GdriveFileStateAtLocation& fileState) //throw SysError + { + parentId = fileState.getItemId(*parentPath, true /*followLeafShortcut*/); //throw SysError + }); + return { std::move(parentId), getItemName(itemPath)}; + } + + Zstring getInitPathPhrase(const AfsPath& itemPath) const override { return concatenateGdriveFolderPathPhrase(getGdrivePath(itemPath)); } + + std::vector getPathPhraseAliases(const AfsPath& itemPath) const override { return {getInitPathPhrase(itemPath)}; } + + std::wstring getDisplayPath(const AfsPath& itemPath) const override { return getGdriveDisplayPath(getGdrivePath(itemPath)); } + + bool isNullFileSystem() const override { return gdriveLogin_.email.empty(); } + + std::weak_ordering compareDeviceSameAfsType(const AbstractFileSystem& afsRhs) const override + { + const GdriveLogin& lhs = gdriveLogin_; + const GdriveLogin& rhs = static_cast(afsRhs).gdriveLogin_; + + if (const std::weak_ordering cmp = compareAsciiNoCase(lhs.email, rhs.email); + cmp != std::weak_ordering::equivalent) + return cmp; + + return compareNativePath(lhs.locationName, rhs.locationName); + } + + //---------------------------------------------------------------------------------------------------------------- + ItemType getItemType(const AfsPath& itemPath) const override //throw FileError + { + try + { + GdriveFileState::PathStatus ps; + accessGlobalFileState(gdriveLogin_, [&](GdriveFileStateAtLocation& fileState) //throw SysError + { + ps = fileState.getPathStatus(itemPath, false /*followLeafShortcut*/); //throw SysError + }); + if (ps.relPath.empty()) + switch (ps.existingType) + { + case GdriveItemType::file: return ItemType::file; + case GdriveItemType::folder: return ItemType::folder; + case GdriveItemType::shortcut: return ItemType::symlink; + } + + throw SysError(replaceCpy(_("%x does not exist."), L"%x", fmtPath(Zstring(ps.relPath.front())))); + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(getDisplayPath(itemPath))), e.toString()); } + } + + std::optional getItemTypeIfExists(const AfsPath& itemPath) const override //throw FileError + { + try + { + GdriveFileState::PathStatus ps; + accessGlobalFileState(gdriveLogin_, [&](GdriveFileStateAtLocation& fileState) //throw SysError + { + ps = fileState.getPathStatus(itemPath, false /*followLeafShortcut*/); //throw SysError + }); + if (ps.relPath.empty()) + switch (ps.existingType) + { + case GdriveItemType::file: return ItemType::file; + case GdriveItemType::folder: return ItemType::folder; + case GdriveItemType::shortcut: return ItemType::symlink; + } + return std::nullopt; + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(getDisplayPath(itemPath))), e.toString()); } + } + + //---------------------------------------------------------------------------------------------------------------- + //already existing: 1. fails or 2. creates duplicate (unlikely) + void createFolderPlain(const AfsPath& folderPath) const override //throw FileError + { + try + { + //avoid duplicate Google Drive item creation by multiple threads + PathAccessLock pal(getGdriveRawPath(folderPath), PathBlockType::otherWait); //throw SysError + + const Zstring folderName = getItemName(folderPath); + std::string parentId; + const GdrivePersistentSessions::AsyncAccessInfo aai = accessGlobalFileState(gdriveLogin_, [&](GdriveFileStateAtLocation& fileState) //throw SysError + { + const GdriveFileState::PathStatus& ps = fileState.getPathStatus(folderPath, false /*followLeafShortcut*/); //throw SysError + if (ps.relPath.empty()) + throw SysError(replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(folderName))); + + if (ps.relPath.size() > 1) //parent folder missing + throw SysError(replaceCpy(_("%x does not exist."), L"%x", fmtPath(ps.relPath.front()))); + + parentId = ps.existingItemId; + }); + + //already existing: creates duplicate + const std::string folderIdNew = gdriveCreateFolderPlain(folderName, parentId, aai.access); //throw SysError + + //buffer new file state ASAP (don't wait GDRIVE_SYNC_INTERVAL) + accessGlobalFileState(gdriveLogin_, [&](GdriveFileStateAtLocation& fileState) //throw SysError + { + fileState.all().notifyFolderCreated(aai.stateDelta, folderIdNew, folderName, parentId); + }); + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot create directory %x."), L"%x", fmtPath(getDisplayPath(folderPath))), e.toString()); } + } + + void removeItemPlainImpl(const AfsPath& itemPath, std::optional expectedType, bool permanent /*...or move to trash*/, bool failIfNotExist) const //throw SysError + { + const std::optional parentPath = getParentPath(itemPath); + if (!parentPath) throw SysError(L"Item is device root"); + + std::string itemId; + std::optional parentIdToUnlink; + const GdrivePersistentSessions::AsyncAccessInfo aai = accessGlobalFileState(gdriveLogin_, [&](GdriveFileStateAtLocation& fileState) //throw SysError + { + const GdriveFileState::PathStatus ps = fileState.getPathStatus(itemPath, false /*followLeafShortcut*/); //throw SysError + if (!ps.relPath.empty()) + { + if (failIfNotExist) + throw SysError(replaceCpy(_("%x does not exist."), L"%x", fmtPath(ps.relPath.front()))); + else + return; + } + + GdriveItemDetails itemDetails; + std::tie(itemId, itemDetails) = fileState.getFileAttributes(itemPath, false /*followLeafShortcut*/); //throw SysError + assert(std::find(itemDetails.parentIds.begin(), itemDetails.parentIds.end(), fileState.getItemId(*parentPath, true /*followLeafShortcut*/)) != itemDetails.parentIds.end()); + + if (expectedType && itemDetails.type != *expectedType) + switch (*expectedType) + { + case GdriveItemType::file: throw SysError(L"Item is not a file"); + case GdriveItemType::folder: throw SysError(L"Item is not a folder"); + case GdriveItemType::shortcut: throw SysError(L"Item is not a shortcut"); + } + + //hard-link handling applies to shared files as well: 1. it's the right thing (TM) 2. if we're not the owner: deleting would fail + if (itemDetails.parentIds.size() > 1 || itemDetails.owner == FileOwner::other) //FileOwner::other behaves like a followed symlink! i.e. vanishes if owner deletes it! + parentIdToUnlink = fileState.getItemId(*parentPath, true /*followLeafShortcut*/); //throw SysError + }); + if (itemId.empty()) + return; + + if (parentIdToUnlink) + { + gdriveUnlinkParent(itemId, *parentIdToUnlink, aai.access); //throw SysError + + //buffer new file state ASAP (don't wait GDRIVE_SYNC_INTERVAL) + accessGlobalFileState(gdriveLogin_, [&](GdriveFileStateAtLocation& fileState) //throw SysError + { + fileState.all().notifyParentRemoved(aai.stateDelta, itemId, *parentIdToUnlink); + }); + } + else + { + if (permanent) + gdriveDeleteItem(itemId, aai.access); //throw SysError + else + gdriveMoveToTrash(itemId, aai.access); //throw SysError + + //buffer new file state ASAP (don't wait GDRIVE_SYNC_INTERVAL) + accessGlobalFileState(gdriveLogin_, [&](GdriveFileStateAtLocation& fileState) //throw SysError + { + fileState.all().notifyItemDeleted(aai.stateDelta, itemId); + }); + } + } + + void removeFilePlain(const AfsPath& filePath) const override //throw FileError + { + try { removeItemPlainImpl(filePath, GdriveItemType::file, true /*permanent*/, false /*failIfNotExist*/); /*throw SysError*/ } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot delete file %x."), L"%x", fmtPath(getDisplayPath(filePath))), e.toString()); } + } + + void removeSymlinkPlain(const AfsPath& linkPath) const override //throw FileError + { + try { removeItemPlainImpl(linkPath, GdriveItemType::shortcut, true /*permanent*/, false /*failIfNotExist*/); /*throw SysError*/ } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot delete symbolic link %x."), L"%x", fmtPath(getDisplayPath(linkPath))), e.toString()); } + } + + void removeFolderPlain(const AfsPath& folderPath) const override //throw FileError + { + try { removeItemPlainImpl(folderPath, GdriveItemType::folder, true /*permanent*/, false /*failIfNotExist*/); /*throw SysError*/ } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot delete directory %x."), L"%x", fmtPath(getDisplayPath(folderPath))), e.toString()); } + } + + void removeFolderIfExistsRecursion(const AfsPath& folderPath, //throw FileError + const std::function& onBeforeFileDeletion /*throw X*/, + const std::function& onBeforeSymlinkDeletion/*throw X*/, + const std::function& onBeforeFolderDeletion /*throw X*/) const override + { + if (onBeforeFolderDeletion) onBeforeFolderDeletion(getDisplayPath(folderPath)); //throw X + + try { removeItemPlainImpl(folderPath, GdriveItemType::folder, true /*permanent*/, false /*failIfNotExist*/); /*throw SysError*/ } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot delete directory %x."), L"%x", fmtPath(getDisplayPath(folderPath))), e.toString()); } + } + + //---------------------------------------------------------------------------------------------------------------- + AbstractPath getSymlinkResolvedPath(const AfsPath& linkPath) const override //throw FileError + { + //this function doesn't make sense for Google Drive: Shortcuts do not refer by path, but ID! + //even if it were possible to determine a path, doing anything with the target file (e.g. delete + recreate) would break other Shortcuts! + throw FileError(replaceCpy(_("Cannot determine final path for %x."), L"%x", fmtPath(getDisplayPath(linkPath))), _("Operation not supported by device.")); + } + + bool equalSymlinkContentForSameAfsType(const AfsPath& linkPathL, const AbstractPath& linkPathR) const override //throw FileError + { + auto getTargetId = [](const GdriveFileSystem& gdriveFs, const AfsPath& linkPath) + { + try + { + std::string targetId; + const GdrivePersistentSessions::AsyncAccessInfo aai = accessGlobalFileState(gdriveFs.gdriveLogin_, [&](GdriveFileStateAtLocation& fileState) //throw SysError + { + const GdriveItemDetails& itemDetails = fileState.getFileAttributes(linkPath, false /*followLeafShortcut*/).second; //throw SysError + if (itemDetails.type != GdriveItemType::shortcut) + throw SysError(L"Not a Google Drive Shortcut."); + + targetId = itemDetails.targetId; + }); + return targetId; + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot resolve symbolic link %x."), L"%x", fmtPath(gdriveFs.getDisplayPath(linkPath))), e.toString()); } + }; + + return getTargetId(*this, linkPathL) == getTargetId(static_cast(linkPathR.afsDevice.ref()), linkPathR.afsPath); + } + + //---------------------------------------------------------------------------------------------------------------- + + //return value always bound: + std::unique_ptr getInputStream(const AfsPath& filePath) const override //throw FileError, (ErrorFileLocked) + { + return std::make_unique(getGdrivePath(filePath)); + } + + //already existing: undefined behavior! (e.g. fail/overwrite/auto-rename) + //=> actual behavior: 1. fails or 2. creates duplicate (unlikely) + std::unique_ptr getOutputStream(const AfsPath& filePath, //throw FileError + std::optional streamSize, + std::optional modTime) const override + { + try + { + //avoid duplicate item creation by multiple threads + auto pal = std::make_unique(getGdriveRawPath(filePath), PathBlockType::otherFail); //throw SysError + //don't block during a potentially long-running file upload! + + //already existing: 1. fails or 2. creates duplicate + return std::make_unique(getGdrivePath(filePath), streamSize, modTime, std::move(pal)); //throw SysError + } + catch (const SysError& e) + { + throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(getDisplayPath(filePath))), e.toString()); + } + } + + //---------------------------------------------------------------------------------------------------------------- + void traverseFolderRecursive(const TraverserWorkload& workload /*throw X*/, size_t parallelOps) const override + { + gdriveTraverseFolderRecursive(gdriveLogin_, workload, parallelOps); //throw X + } + //---------------------------------------------------------------------------------------------------------------- + + //symlink handling: follow + //already existing: undefined behavior! (e.g. fail/overwrite/auto-rename) + //=> actual behavior: 1. fails or 2. creates duplicate (unlikely) + FileCopyResult copyFileForSameAfsType(const AfsPath& sourcePath, const StreamAttributes& attrSource, //throw FileError, (ErrorFileLocked), (X) + const AbstractPath& targetPath, bool copyFilePermissions, const IoCallback& notifyUnbufferedIO /*throw X*/) const override + { + //no native Google Drive file copy => use stream-based file copy: + if (copyFilePermissions) + throw FileError(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(AFS::getDisplayPath(targetPath))), _("Operation not supported by device.")); + + const GdriveFileSystem& fsTarget = static_cast(targetPath.afsDevice.ref()); + + if (!equalAsciiNoCase(gdriveLogin_.email, fsTarget.gdriveLogin_.email)) + //already existing: undefined behavior! (e.g. fail/overwrite/auto-rename) + //=> actual behavior: 1. fails or 2. creates duplicate (unlikely) + return copyFileAsStream(sourcePath, attrSource, targetPath, notifyUnbufferedIO); //throw FileError, (ErrorFileLocked), X + //else: copying files within account works, e.g. between My Drive <-> shared drives + + try + { + //avoid duplicate Google Drive item creation by multiple threads (blocking is okay: gdriveCopyFile() should complete instantly!) + PathAccessLock pal(fsTarget.getGdriveRawPath(targetPath.afsPath), PathBlockType::otherWait); //throw SysError + + const Zstring itemNameNew = getItemName(targetPath); + std::string itemIdSrc; + GdriveItemDetails itemDetailsSrc; + /*const GdrivePersistentSessions::AsyncAccessInfo aaiSrc =*/ accessGlobalFileState(gdriveLogin_, [&](GdriveFileStateAtLocation& fileState) //throw SysError + { + std::tie(itemIdSrc, itemDetailsSrc) = fileState.getFileAttributes(sourcePath, true /*followLeafShortcut*/); //throw SysError + + assert(itemDetailsSrc.type == GdriveItemType::file); //Google Drive *should* fail trying to copy folder: "This file cannot be copied by the user." + if (itemDetailsSrc.type != GdriveItemType::file) //=> don't trust + improve error message + throw SysError(replaceCpy(L"%x is not a file.", L"%x", fmtPath(getItemName(sourcePath)))); + }); + + std::string parentIdTrg; + const GdrivePersistentSessions::AsyncAccessInfo aaiTrg = accessGlobalFileState(fsTarget.gdriveLogin_, [&](GdriveFileStateAtLocation& fileState) //throw SysError + { + const GdriveFileState::PathStatus psTo = fileState.getPathStatus(targetPath.afsPath, false /*followLeafShortcut*/); //throw SysError + if (psTo.relPath.empty()) + throw SysError(replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(itemNameNew))); + + if (psTo.relPath.size() > 1) //parent folder missing + throw SysError(replaceCpy(_("%x does not exist."), L"%x", fmtPath(psTo.relPath.front()))); + + parentIdTrg = psTo.existingItemId; + }); + + //already existing: creates duplicate + const std::string fileIdTrg = gdriveCopyFile(itemIdSrc, parentIdTrg, itemNameNew, itemDetailsSrc.modTime, aaiTrg.access); //throw SysError + + //buffer new file state ASAP (don't wait GDRIVE_SYNC_INTERVAL) + accessGlobalFileState(fsTarget.gdriveLogin_, [&](GdriveFileStateAtLocation& fileState) //throw SysError + { + const GdriveItem newFileItem + { + .itemId = fileIdTrg, + .details{ + .itemName = itemNameNew, + .fileSize = itemDetailsSrc.fileSize, + .modTime = itemDetailsSrc.modTime, + .type = GdriveItemType::file, + .owner = fileState.all().getSharedDriveName().empty() ? FileOwner::me : FileOwner::none, + .parentIds{parentIdTrg}, + } + }; + fileState.all().notifyItemCreated(aaiTrg.stateDelta, newFileItem); + }); + + return + { + .fileSize = itemDetailsSrc.fileSize, + .modTime = itemDetailsSrc.modTime, + .sourceFilePrint = getGdriveFilePrint(itemIdSrc), + .targetFilePrint = getGdriveFilePrint(fileIdTrg), + /*.errorModTime = */ + }; + } + catch (const SysError& e) + { + throw FileError(replaceCpy(replaceCpy(_("Cannot copy file %x to %y."), + L"%x", L'\n' + fmtPath(getDisplayPath(sourcePath))), + L"%y", L'\n' + fmtPath(AFS::getDisplayPath(targetPath))), e.toString()); + } + } + + //symlink handling: follow + //already existing: fail + void copyNewFolderForSameAfsType(const AfsPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions) const override //throw FileError + { + //already existing: 1. fails or 2. creates duplicate (unlikely) + AFS::createFolderPlain(targetPath); //throw FileError + + if (copyFilePermissions) + throw FileError(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(AFS::getDisplayPath(targetPath))), _("Operation not supported by device.")); + } + + //already existing: fail + void copySymlinkForSameAfsType(const AfsPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions) const override //throw FileError + { + try + { + std::string targetId; + accessGlobalFileState(gdriveLogin_, [&](GdriveFileStateAtLocation& fileState) //throw SysError + { + const GdriveItemDetails& itemDetails = fileState.getFileAttributes(sourcePath, false /*followLeafShortcut*/).second; //throw SysError + if (itemDetails.type != GdriveItemType::shortcut) + throw SysError(L"Not a Google Drive Shortcut."); + + targetId = itemDetails.targetId; + }); + + const GdriveFileSystem& fsTarget = static_cast(targetPath.afsDevice.ref()); + + //avoid duplicate Google Drive item creation by multiple threads + PathAccessLock pal(fsTarget.getGdriveRawPath(targetPath.afsPath), PathBlockType::otherWait); //throw SysError + + const Zstring shortcutName = getItemName(targetPath.afsPath); + std::string parentId; + const GdrivePersistentSessions::AsyncAccessInfo aaiTrg = accessGlobalFileState(fsTarget.gdriveLogin_, [&](GdriveFileStateAtLocation& fileState) //throw SysError + { + const GdriveFileState::PathStatus& ps = fileState.getPathStatus(targetPath.afsPath, false /*followLeafShortcut*/); //throw SysError + if (ps.relPath.empty()) + throw SysError(replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(shortcutName))); + + if (ps.relPath.size() > 1) //parent folder missing + throw SysError(replaceCpy(_("%x does not exist."), L"%x", fmtPath(ps.relPath.front()))); + + parentId = ps.existingItemId; + }); + + //already existing: creates duplicate + const std::string shortcutIdNew = gdriveCreateShortcutPlain(shortcutName, parentId, targetId, aaiTrg.access); //throw SysError + + //buffer new file state ASAP (don't wait GDRIVE_SYNC_INTERVAL) + accessGlobalFileState(fsTarget.gdriveLogin_, [&](GdriveFileStateAtLocation& fileState) //throw SysError + { + fileState.all().notifyShortcutCreated(aaiTrg.stateDelta, shortcutIdNew, shortcutName, parentId, targetId); + }); + } + catch (const SysError& e) + { + throw FileError(replaceCpy(replaceCpy(_("Cannot copy symbolic link %x to %y."), + L"%x", L'\n' + fmtPath(getDisplayPath(sourcePath))), + L"%y", L'\n' + fmtPath(AFS::getDisplayPath(targetPath))), e.toString()); + } + } + + //already existing: undefined behavior! (e.g. fail/overwrite) + //=> actual behavior: 1. fails or 2. creates duplicate (unlikely) + void moveAndRenameItemForSameAfsType(const AfsPath& pathFrom, const AbstractPath& pathTo) const override //throw FileError, ErrorMoveUnsupported + { + if (compareDeviceSameAfsType(pathTo.afsDevice.ref()) != std::weak_ordering::equivalent) + throw ErrorMoveUnsupported(generateMoveErrorMsg(pathFrom, pathTo), _("Operation not supported between different devices.")); + //note: moving files within account works, e.g. between My Drive <-> shared drives + // BUT: not supported by our model with separate GdriveFileStates; e.g. how to handle complexity of a moved folder (tree)? + try + { + const GdriveFileSystem& fsTarget = static_cast(pathTo.afsDevice.ref()); + + //avoid duplicate Google Drive item creation by multiple threads + PathAccessLock pal(fsTarget.getGdriveRawPath(pathTo.afsPath), PathBlockType::otherWait); //throw SysError + + const Zstring itemNameOld = getItemName(pathFrom); + const Zstring itemNameNew = getItemName(pathTo); + const std::optional parentPathFrom = getParentPath(pathFrom); + const std::optional parentPathTo = getParentPath(pathTo.afsPath); + if (!parentPathFrom) throw SysError(L"Source is device root"); + if (!parentPathTo ) throw SysError(L"Target is device root"); + + std::string itemId; + GdriveItemDetails itemDetails; + std::string parentIdFrom; + std::string parentIdTo; + const GdrivePersistentSessions::AsyncAccessInfo aai = accessGlobalFileState(gdriveLogin_, [&](GdriveFileStateAtLocation& fileState) //throw SysError + { + std::tie(itemId, itemDetails) = fileState.getFileAttributes(pathFrom, false /*followLeafShortcut*/); //throw SysError + + parentIdFrom = fileState.getItemId(*parentPathFrom, true /*followLeafShortcut*/); //throw SysError + + const GdriveFileState::PathStatus psTo = fileState.getPathStatus(pathTo.afsPath, false /*followLeafShortcut*/); //throw SysError + + //e.g. changing file name case only => this is not an "already exists" situation! + //also: hardlink referenced by two different paths, the source one will be unlinked + if (psTo.relPath.empty() && psTo.existingItemId == itemId) + parentIdTo = fileState.getItemId(*parentPathTo, true /*followLeafShortcut*/); //throw SysError + else + { + if (psTo.relPath.empty()) + throw SysError(replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(itemNameNew))); + + if (psTo.relPath.size() > 1) //parent folder missing + throw SysError(replaceCpy(_("%x does not exist."), L"%x", fmtPath(psTo.relPath.front()))); + + parentIdTo = psTo.existingItemId; + } + }); + + if (parentIdFrom == parentIdTo && itemNameOld == itemNameNew) + return; //nothing to do + + //already existing: creates duplicate + gdriveMoveAndRenameItem(itemId, parentIdFrom, parentIdTo, itemNameNew, itemDetails.modTime, aai.access); //throw SysError + + //buffer new file state ASAP (don't wait GDRIVE_SYNC_INTERVAL) + accessGlobalFileState(gdriveLogin_, [&](GdriveFileStateAtLocation& fileState) //throw SysError + { + fileState.all().notifyMoveAndRename(aai.stateDelta, itemId, parentIdFrom, parentIdTo, itemNameNew); + }); + } + catch (const SysError& e) { throw FileError(generateMoveErrorMsg(pathFrom, pathTo), e.toString()); } + } + + bool supportsPermissions(const AfsPath& folderPath) const override { return false; } //throw FileError + + //---------------------------------------------------------------------------------------------------------------- + FileIconHolder getFileIcon (const AfsPath& filePath, int pixelSize) const override { return {}; } //throw FileError; optional return value + ImageHolder getThumbnailImage(const AfsPath& filePath, int pixelSize) const override { return {}; } //throw FileError; optional return value + + void authenticateAccess(const RequestPasswordFun& requestPassword /*throw X*/) const override //throw FileError, (X) + { + try + { + const std::shared_ptr gps = globalGdriveSessions.get(); + if (!gps) + throw SysError(formatSystemError("GdriveFileSystem::authenticateAccess", L"", L"Function call not allowed during init/shutdown.")); + + for (const std::string& accountEmail : gps->listAccounts()) //throw SysError + if (equalAsciiNoCase(accountEmail, gdriveLogin_.email)) + return; + + const bool allowUserInteraction = static_cast(requestPassword); + if (allowUserInteraction) + gps->addUserSession(gdriveLogin_.email /*gdriveLoginHint*/, nullptr /*updateGui*/, gdriveLogin_.timeoutSec); //throw SysError + //error messages will be lost if user cancels in dir_exist_async.h! However: + //The most-likely-to-fail parts (web access) are reported by gdriveAuthorizeAccess() via the browser! + else + throw SysError(replaceCpy(_("Please add a connection to user account %x first."), L"%x", utfTo(gdriveLogin_.email))); + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Unable to connect to %x."), L"%x", fmtPath(getDisplayPath(AfsPath()))), e.toString()); } + } + + bool hasNativeTransactionalCopy() const override { return true; } + //---------------------------------------------------------------------------------------------------------------- + + int64_t getFreeDiskSpace(const AfsPath& folderPath) const override //throw FileError, returns < 0 if not available + { + bool onMyDrive = false; + try + { + const GdriveAccess& access = accessGlobalFileState(gdriveLogin_, [&](GdriveFileStateAtLocation& fileState) + { onMyDrive = fileState.all().getSharedDriveName().empty(); }).access; //throw SysError + + if (onMyDrive) + return gdriveGetMyDriveFreeSpace(access); //throw SysError + else + return -1; + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot determine free disk space for %x."), L"%x", fmtPath(getDisplayPath(folderPath))), e.toString()); } + } + + std::unique_ptr createRecyclerSession(const AfsPath& folderPath) const override //throw FileError, (RecycleBinUnavailable) + { + struct RecycleSessionGdrive : public RecycleSession + { + //fails if item is not existing + void moveToRecycleBin(const AbstractPath& itemPath, const Zstring& logicalRelPath) override { AFS::moveToRecycleBin(itemPath); } //throw FileError, (RecycleBinUnavailable) + void tryCleanup(const std::function& notifyDeletionStatus) override {}; //throw FileError + }; + + return std::make_unique(); + } + + //fails if item is not existing + void moveToRecycleBin(const AfsPath& itemPath) const override //throw FileError, (RecycleBinUnavailable) + { + try + { + removeItemPlainImpl(itemPath, std::nullopt /*expectedType*/, false /*permanent*/, true /*failIfNotExist*/); //throw SysError + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Unable to move %x to the recycle bin."), L"%x", fmtPath(getDisplayPath(itemPath))), e.toString()); } + } + + const GdriveLogin gdriveLogin_; +}; +//=========================================================================================================================== + +//expects "clean" input data +Zstring concatenateGdriveFolderPathPhrase(const GdrivePath& gdrivePath) //noexcept +{ + Zstring emailAndDrive = utfTo(gdrivePath.gdriveLogin.email); + if (!gdrivePath.gdriveLogin.locationName.empty()) + emailAndDrive += Zstr(':') + gdrivePath.gdriveLogin.locationName; + + Zstring options; + if (gdrivePath.gdriveLogin.timeoutSec != GdriveLogin().timeoutSec) + options += Zstr("|timeout=") + numberTo(gdrivePath.gdriveLogin.timeoutSec); + + Zstring itemPath; + if (!gdrivePath.itemPath.value.empty()) + itemPath += FILE_NAME_SEPARATOR + gdrivePath.itemPath.value; + + if (endsWith(itemPath, Zstr(' ')) && options.empty()) //path phrase concept must survive trimming! + itemPath += FILE_NAME_SEPARATOR; + + return Zstring(gdrivePrefix) + FILE_NAME_SEPARATOR + emailAndDrive + itemPath + options; +} +} + + +void fff::gdriveInit(const Zstring& configDirPath, const Zstring& caCertFilePath) +{ + assert(!globalHttpSessionManager.get()); + globalHttpSessionManager.set(std::make_unique(caCertFilePath)); + + assert(!globalGdriveSessions.get()); + globalGdriveSessions.set(std::make_unique(configDirPath)); +} + + +void fff::gdriveTeardown() +{ + try //don't use ~GdrivePersistentSessions() to save! Might never happen, e.g. detached thread waiting for Google Drive authentication; terminated on exit! + { + if (const std::shared_ptr gps = globalGdriveSessions.get()) + gps->saveActiveSessions(); //throw FileError + } + catch (const FileError& e) { logExtraError(e.toString()); } + + assert(globalGdriveSessions.get()); + globalGdriveSessions.set(nullptr); + + assert(globalHttpSessionManager.get()); + globalHttpSessionManager.set(nullptr); +} + + +std::string fff::gdriveAddUser(const std::function& updateGui /*throw X*/, int timeoutSec) //throw FileError, X +{ + try + { + if (const std::shared_ptr gps = globalGdriveSessions.get()) + return gps->addUserSession("" /*gdriveLoginHint*/, updateGui, timeoutSec); //throw SysError, X + + throw SysError(formatSystemError("gdriveAddUser", L"", L"Function call not allowed during init/shutdown.")); + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Unable to connect to %x."), L"%x", L"Google Drive"), e.toString()); } +} + + +void fff::gdriveRemoveUser(const std::string& accountEmail, int timeoutSec) //throw FileError +{ + try + { + if (const std::shared_ptr gps = globalGdriveSessions.get()) + return gps->removeUserSession(accountEmail, timeoutSec); //throw SysError + + throw SysError(formatSystemError("gdriveRemoveUser", L"", L"Function call not allowed during init/shutdown.")); + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Unable to disconnect from %x."), L"%x", fmtPath(getGdriveDisplayPath({{accountEmail, Zstr("")}, AfsPath()}))), e.toString()); } +} + + +std::vector fff::gdriveListAccounts() //throw FileError +{ + try + { + if (const std::shared_ptr gps = globalGdriveSessions.get()) + return gps->listAccounts(); //throw SysError + + throw SysError(formatSystemError("gdriveListAccounts", L"", L"Function call not allowed during init/shutdown.")); + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Unable to connect to %x."), L"%x", L"Google Drive"), e.toString()); } +} + + +std::vector fff::gdriveListLocations(const std::string& accountEmail, int timeoutSec) //throw FileError +{ + try + { + if (const std::shared_ptr gps = globalGdriveSessions.get()) + return gps->listLocations(accountEmail, timeoutSec); //throw SysError + + throw SysError(formatSystemError("gdriveListLocations", L"", L"Function call not allowed during init/shutdown.")); + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Unable to connect to %x."), L"%x", fmtPath(getGdriveDisplayPath({{accountEmail, Zstr("")}, AfsPath()}))), e.toString()); } +} + + +AfsDevice fff::condenseToGdriveDevice(const GdriveLogin& login) //noexcept +{ + //clean up input: + GdriveLogin loginTmp = login; + trim(loginTmp.email); + + loginTmp.timeoutSec = std::max(1, loginTmp.timeoutSec); + + return makeSharedRef(loginTmp); +} + + +GdriveLogin fff::extractGdriveLogin(const AfsDevice& afsDevice) //noexcept +{ + if (const auto gdriveDevice = dynamic_cast(&afsDevice.ref())) + return gdriveDevice ->getGdriveLogin(); + + assert(false); + return {}; +} + + +Zstring fff::getGoogleDriveFolderUrl(const AbstractPath& folderPath) //throw FileError +{ + if (const auto gdriveDevice = dynamic_cast(&folderPath.afsDevice.ref())) + return gdriveDevice->getFolderUrl(folderPath.afsPath); //throw FileError + //assert(false); + return {}; +} + + +bool fff::acceptsItemPathPhraseGdrive(const Zstring& itemPathPhrase) //noexcept +{ + Zstring path = expandMacros(itemPathPhrase); //expand before trimming! + trim(path); + return startsWithAsciiNoCase(path, gdrivePrefix); +} + + +/* syntax: gdrive:\[:]\[|option_name=value] + + e.g.: gdrive:\john@gmail.com\folder\file.txt + gdrive:\john@gmail.com:location\folder\file.txt|option_name=value */ +AbstractPath fff::createItemPathGdrive(const Zstring& itemPathPhrase) //noexcept +{ + Zstring pathPhrase = expandMacros(itemPathPhrase); //expand before trimming! + trim(pathPhrase); + + if (startsWithAsciiNoCase(pathPhrase, gdrivePrefix)) + pathPhrase = pathPhrase.c_str() + strLength(gdrivePrefix); + trim(pathPhrase, TrimSide::left, [](Zchar c) { return c == Zstr('/') || c == Zstr('\\'); }); + + const ZstringView fullPath = beforeFirst(pathPhrase, Zstr('|'), IfNotFoundReturn::all); + const ZstringView options = afterFirst(pathPhrase, Zstr('|'), IfNotFoundReturn::none); + + auto it = std::find_if(fullPath.begin(), fullPath.end(), [](Zchar c) { return c == '/' || c == '\\'; }); + const ZstringView emailAndDrive = makeStringView(fullPath.begin(), it); + const AfsPath itemPath = sanitizeDeviceRelativePath({it, fullPath.end()}); + + GdriveLogin login + { + .email = utfTo(beforeFirst(emailAndDrive, Zstr(':'), IfNotFoundReturn::all)), + .locationName = Zstring(afterFirst (emailAndDrive, Zstr(':'), IfNotFoundReturn::none)), + }; + + split(options, Zstr('|'), [&](ZstringView optPhrase) + { + optPhrase = trimCpy(optPhrase); + if (!optPhrase.empty()) + { + if (startsWith(optPhrase, Zstr("timeout="))) + login.timeoutSec = stringTo(afterFirst(optPhrase, Zstr('='), IfNotFoundReturn::none)); + else + assert(false); + } + }); + return AbstractPath(makeSharedRef(login), itemPath); +} diff --git a/FreeFileSync/Source/afs/gdrive.h b/FreeFileSync/Source/afs/gdrive.h new file mode 100644 index 0000000..78f7d31 --- /dev/null +++ b/FreeFileSync/Source/afs/gdrive.h @@ -0,0 +1,44 @@ +// ***************************************************************************** +// * 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 FS_GDRIVE_9238425018342701356 +#define FS_GDRIVE_9238425018342701356 + +#include "abstract.h" + +namespace fff +{ +bool acceptsItemPathPhraseGdrive(const Zstring& itemPathPhrase); //noexcept +AbstractPath createItemPathGdrive (const Zstring& itemPathPhrase); //noexcept + +void gdriveInit(const Zstring& configDirPath, //directory to store Google-Drive-specific files + const Zstring& caCertFilePath); //cacert.pem +void gdriveTeardown(); + +//------------------------------------------------------- + +//caveat: gdriveAddUser() blocks indefinitely if user doesn't log in with Google! timeoutSec is only regarding HTTP requests +std::string /*account email*/ gdriveAddUser(const std::function& updateGui /*throw X*/, int timeoutSec); //throw FileError, X +void gdriveRemoveUser(const std::string& accountEmail, int timeoutSec); //throw FileError + +std::vector gdriveListAccounts(); //throw FileError +std::vector gdriveListLocations(const std::string& accountEmail, int timeoutSec); //throw FileError + +struct GdriveLogin +{ + std::string email; + Zstring locationName; //empty for "My Drive"; can be a shared drive or starred folder name + int timeoutSec = 10; //Gdrive can "hang" for 20 seconds when "scanning for viruses": https://freefilesync.org/forum/viewtopic.php?t=9116 +}; + +AfsDevice condenseToGdriveDevice(const GdriveLogin& login); //noexcept; potentially messy user input +GdriveLogin extractGdriveLogin(const AfsDevice& afsDevice); //noexcept + +//return empty, if not a Google Drive path +Zstring getGoogleDriveFolderUrl(const AbstractPath& folderPath); //throw FileError +} + +#endif //FS_GDRIVE_9238425018342701356 diff --git a/FreeFileSync/Source/afs/init_curl_libssh2.cpp b/FreeFileSync/Source/afs/init_curl_libssh2.cpp new file mode 100644 index 0000000..40026a9 --- /dev/null +++ b/FreeFileSync/Source/afs/init_curl_libssh2.cpp @@ -0,0 +1,155 @@ +// ***************************************************************************** +// * 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 "init_curl_libssh2.h" +#include +#include //DON'T include directly! +#include //DON'T include directly! + +using namespace zen; + + +namespace +{ +int uniInitLevel = 0; //support interleaving initialization calls! (e.g. use for libssh2 and libcurl) +//zero-initialized POD => not subject to static initialization order fiasco + +void libsshCurlUnifiedInit() +{ + assert(runningOnMainThread()); + assert(uniInitLevel >= 0); + if (++uniInitLevel != 1) //non-atomic => require call from main thread + return; + + libcurlInit(); //includes WSAStartup() also needed by libssh2 + + [[maybe_unused]] const int rc = ::libssh2_init(0); //includes OpenSSL-related initialization which might be needed (and hopefully won't hurt...) + assert(rc == 0); //libssh2 unconditionally returns 0 => why then have a return value in first place??? +} + + +void libsshCurlUnifiedTearDown() +{ + assert(runningOnMainThread()); + assert(uniInitLevel >= 1); + if (--uniInitLevel != 0) + return; + + ::libssh2_exit(); + libcurlTearDown(); +} +} + + +class zen::UniSessionCounter::Impl +{ +public: + void inc() //throw SysError + { + { + std::unique_lock dummy(lockCount_); + assert(sessionCount_ >= 0); + + if (!newSessionsAllowed_) + throw SysError(formatSystemError("UniSessionCounter::inc", L"", L"Function call not allowed during init/shutdown.")); + + ++sessionCount_; + } + conditionCountChanged_.notify_all(); + } + + void dec() //noexcept + { + { + std::unique_lock dummy(lockCount_); + assert(sessionCount_ >= 1); + --sessionCount_; + } + conditionCountChanged_.notify_all(); + } + + void onInitCompleted() //noexcept + { + std::unique_lock dummy(lockCount_); + newSessionsAllowed_ = true; + } + + void onBeforeTearDown() //noexcept + { + std::unique_lock dummy(lockCount_); + newSessionsAllowed_ = false; + conditionCountChanged_.wait(dummy, [this] { return sessionCount_ == 0; }); + } + + Impl() {} + ~Impl() + { + } + +private: + Impl (const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + + std::mutex lockCount_; + int sessionCount_ = 0; + std::condition_variable conditionCountChanged_; + + bool newSessionsAllowed_ = false; +}; + + +UniSessionCounter::UniSessionCounter() : pimpl(std::make_unique()) {} +UniSessionCounter::~UniSessionCounter() {} + + +std::unique_ptr zen::createUniSessionCounter() +{ + return std::make_unique(); +} + + +class zen::UniCounterCookie +{ +public: + UniCounterCookie(const std::shared_ptr& sessionCounter) : sessionCounter_(sessionCounter) {} + ~UniCounterCookie() { sessionCounter_->pimpl->dec(); } + +private: + UniCounterCookie (const UniCounterCookie&) = delete; + UniCounterCookie& operator=(const UniCounterCookie&) = delete; + + const std::shared_ptr sessionCounter_; +}; + + +std::shared_ptr zen::getLibsshCurlUnifiedInitCookie(Global& globalSftpSessionCount) //throw SysError +{ + std::shared_ptr sessionCounter = globalSftpSessionCount.get(); + if (!sessionCounter) + throw SysError(formatSystemError("getLibsshCurlUnifiedInitCookie", L"", L"Function call not allowed during init/shutdown.")); //=> ~UniCounterCookie() *not* called! + sessionCounter->pimpl->inc(); //throw SysError // + + //pass "ownership" of having to call UniSessionCounter::dec() + return std::make_shared(sessionCounter); //throw SysError +} + + +UniInitializer::UniInitializer(UniSessionCounter& sessionCount) : sessionCount_(sessionCount) +{ + libsshCurlUnifiedInit(); + sessionCount_.pimpl->onInitCompleted(); +} + + +UniInitializer::~UniInitializer() +{ + //wait until all (S)FTP sessions running on detached threads have ended! otherwise they'll crash during ::WSACleanup()! + sessionCount_.pimpl->onBeforeTearDown(); + /* alternatively we could use a Global and have each session own a shared_ptr: + drawback 1: SFTP clean-up may happen on worker thread => probably not supported!!! + drawback 2: cleanup will not happen when the C++ runtime on Windows kills all worker threads during shutdown */ + libsshCurlUnifiedTearDown(); +} diff --git a/FreeFileSync/Source/afs/init_curl_libssh2.h b/FreeFileSync/Source/afs/init_curl_libssh2.h new file mode 100644 index 0000000..93efb9f --- /dev/null +++ b/FreeFileSync/Source/afs/init_curl_libssh2.h @@ -0,0 +1,51 @@ +// ***************************************************************************** +// * 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 INIT_CURL_LIBSSH2_H_4570285702375915765 +#define INIT_CURL_LIBSSH2_H_4570285702375915765 + +#include +#include + + +namespace zen +{ +//(S)FTP initialization/shutdown dance: + +//1. create "Global globalSftpSessionCount(createUniSessionCounter());" to have a waitable counter of existing (S)FTP sessions +struct UniSessionCounter +{ + UniSessionCounter(); + ~UniSessionCounter(); + + class Impl; + const std::unique_ptr pimpl; +}; +std::unique_ptr createUniSessionCounter(); + + +//2. count number of existing (S)FTP sessions => tie to (S)FTP session instances! +class UniCounterCookie; +std::shared_ptr getLibsshCurlUnifiedInitCookie(Global& globalSftpSessionCount); //throw SysError + + +//3. Create static "UniInitializer globalInitSftp(*globalSftpSessionCount.get());" instance *before* constructing objects like "SftpSessionManager" +// => ~SftpSessionManager will run first and all remaining sessions are on non-main threads => can be waited on in ~UniInitializer +class UniInitializer +{ +public: + UniInitializer(UniSessionCounter& sessionCount); + ~UniInitializer(); + +private: + UniInitializer (const UniInitializer&) = delete; + UniInitializer& operator=(const UniInitializer&) = delete; + + UniSessionCounter& sessionCount_; +}; +} + +#endif //INIT_CURL_LIBSSH2_H_4570285702375915765 diff --git a/FreeFileSync/Source/afs/native.cpp b/FreeFileSync/Source/afs/native.cpp new file mode 100644 index 0000000..9e5e8f6 --- /dev/null +++ b/FreeFileSync/Source/afs/native.cpp @@ -0,0 +1,758 @@ +// ***************************************************************************** +// * 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 "native.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "abstract_impl.h" +#include "../base/icon_loader.h" + + #include //statfs + + #include + #include + #include //fallocate, fcntl + +using namespace zen; +using namespace fff; +using AFS = AbstractFileSystem; + + +namespace +{ + +void initComForThread() //throw FileError +{ +} + +//==================================================================================================== +//==================================================================================================== + +//persistent + unique (relative to volume) or 0! +inline +AFS::FingerPrint getFileFingerprint(FileIndex fileIndex) +{ + static_assert(sizeof(fileIndex) == sizeof(AFS::FingerPrint)); + return fileIndex; //== 0 if not supported + /* File details + ------------ + st_mtim (Linux) + st_mtimespec (macOS): nanosecond-precision for improved uniqueness? + => essentially unknown after file copy (e.g. to FAT) without extra directory traversal :( + + macOS st_birthtimespec: "if not supported by file system, holds the ctime instead" + ctime: inode modification time => changed on* rename*! => FU... + + Volume details + -------------- + st_dev: "st_dev value is not necessarily consistent across reboots or system crashes" https://freefilesync.org/forum/viewtopic.php?t=8054 + only locally unique and depends on device mount point! => FU... + + f_fsid: "Some operating systems use the device number..." => fuck! + "Several OSes restrict giving out the f_fsid field to the superuser only" + + f_bsize macOS: "fundamental file system block size" + Linux: "optimal transfer block size" -> no! for all intents and purposes this *is* the "fundamental file system block size": https://stackoverflow.com/a/54835515 + f_blocks => meh... + + f_type Linux: documented values, nice! https://linux.die.net/man/2/statfs + macOS: - not stable between macOS releases: https://developer.apple.com/forums/thread/87745 + - Apple docs say: "generally not a useful value" + - f_fstypename can be used as alternative + + DADiskGetBSDName(): macOS only */ +} + + +struct NativeFileInfo +{ + FileTimeNative modTime; + uint64_t fileSize; + AFS::FingerPrint filePrint; +}; +NativeFileInfo getNativeFileInfo(FileBase& file) //throw FileError +{ + const struct stat& fileInfo = file.getStatBuffered(); //throw FileError + return + { + fileInfo.st_mtim, + makeUnsigned(fileInfo.st_size), + getFileFingerprint(fileInfo.st_ino) + }; +} + + +struct FsItem +{ + Zstring itemName; +}; +std::vector getDirContentFlat(const Zstring& dirPath) //throw FileError +{ + //no need to check for endless recursion: + //1. Linux has a fixed limit on the number of symbolic links in a path + //2. fails with "too many open files" or "path too long" before reaching stack overflow + + DIR* folder = ::opendir(dirPath.c_str()); //directory must NOT end with path separator, except "/" + if (!folder) + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot open directory %x."), L"%x", fmtPath(dirPath)), "opendir"); + ZEN_ON_SCOPE_EXIT(::closedir(folder)); //never close nullptr handles! -> crash + + std::vector output; + for (;;) + { + /* Linux: https://man7.org/linux/man-pages/man3/readdir_r.3.html + "It is recommended that applications use readdir(3) instead of readdir_r" + "... in modern implementations (including the glibc implementation), concurrent calls to readdir(3) that specify different directory streams are thread-safe" + + macOS: - libc: readdir thread-safe already in code from 2000: https://opensource.apple.com/source/Libc/Libc-166/gen.subproj/readdir.c.auto.html + - and in the latest version from 2017: https://opensource.apple.com/source/Libc/Libc-1244.30.3/gen/FreeBSD/readdir.c.auto.html */ + errno = 0; + const dirent* dirEntry = ::readdir(folder); + if (!dirEntry) + { + if (errno == 0) //errno left unchanged => no more items + return output; + + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read directory %x."), L"%x", fmtPath(dirPath)), "readdir"); + //don't retry but restart dir traversal on error! https://devblogs.microsoft.com/oldnewthing/20140612-00/?p=753 + } + + const char* itemNameRaw = dirEntry->d_name; + + //skip "." and ".." + if (itemNameRaw[0] == '.' && + (itemNameRaw[1] == 0 || (itemNameRaw[1] == '.' && itemNameRaw[2] == 0))) + continue; + + if (itemNameRaw[0] == 0) //show error instead of endless recursion!!! + throw FileError(replaceCpy(_("Cannot read directory %x."), L"%x", fmtPath(dirPath)), formatSystemError("readdir", L"", L"Folder contains an item without name.")); + + output.push_back({itemNameRaw}); + + /* Unicode normalization is file-system-dependent: + + OS Accepts Gives back + ---------- ------- ---------- + macOS (HFS+) all NFD + Linux all + Windows (NTFS, FAT) all + + some file systems return precomposed others decomposed UTF8: https://developer.apple.com/library/archive/qa/qa1173/_index.html + - OS X edit controls and text fields may return precomposed UTF as directly received by keyboard or decomposed UTF that was copy & pasted! + - Posix APIs require decomposed form: https://freefilesync.org/forum/viewtopic.php?t=2480 + + => General recommendation: always preserve input UNCHANGED (both unicode normalization and case sensitivity) + => normalize only when needed during string comparison + + Create sample files on Linux: touch decomposed-$'\x6f\xcc\x81'.txt + touch precomposed-$'\xc3\xb3'.txt + + - list file name hex chars in terminal: ls | od -c -t x1 + + - SMB sharing case-sensitive or NFD file names is fundamentally broken on macOS: + => the macOS SMB manager internally buffers file names as case-insensitive and NFC (= just like NTFS on Windows) + => test: create SMB share from Linux => *boom* on macOS: "Error Code 2: No such file or directory [lstat]" + or WORSE: folders "test" and "Test" *both* incorrectly return the content of one of the two + => Update 2020-04-24: converting to NFC doesn't help: both NFD/NFC forms fail(ENOENT) lstat in FFS, AS WELL AS IN FINDER (silently skipped!) => macOS bug! */ + } +} + + +struct FsItemDetails +{ + ItemType type; + time_t modTime; //number of seconds since Jan. 1st 1970 GMT + uint64_t fileSize; //unit: bytes! + AFS::FingerPrint filePrint; +}; +FsItemDetails getItemDetails(const Zstring& itemPath) //throw FileError +{ + struct stat itemInfo = {}; + if (::lstat(itemPath.c_str(), &itemInfo) != 0) //lstat() does not resolve symlinks + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(itemPath)), "lstat"); + + return {S_ISLNK(itemInfo.st_mode) ? ItemType::symlink : //on Linux there is no distinction between file and directory symlinks! + /**/ (S_ISDIR(itemInfo.st_mode) ? ItemType::folder : ItemType::file), //a file or named pipe, etc. S_ISREG, S_ISCHR, S_ISBLK, S_ISFIFO, S_ISSOCK + //=> dont't check using S_ISREG(): see comment in file_traverser.cpp + itemInfo.st_mtime, + makeUnsigned(itemInfo.st_size), + getFileFingerprint(itemInfo.st_ino)}; +} + + +FsItemDetails getSymlinkTargetDetails(const Zstring& linkPath) //throw FileError +{ + try + { + struct stat itemInfo = {}; + if (::stat(linkPath.c_str(), &itemInfo) != 0) + THROW_LAST_SYS_ERROR("stat"); + + const ItemType targetType = S_ISDIR(itemInfo.st_mode) ? ItemType::folder : ItemType::file; + + const AFS::FingerPrint filePrint = targetType == ItemType::folder ? 0 : getFileFingerprint(itemInfo.st_ino); + return {targetType, + itemInfo.st_mtime, + makeUnsigned(itemInfo.st_size), + filePrint}; + } + catch (const SysError& e) + { + throw FileError(replaceCpy(_("Cannot resolve symbolic link %x."), L"%x", fmtPath(linkPath)), e.toString()); + } +} + + +class SingleFolderTraverser +{ +public: + SingleFolderTraverser(const std::vector>>& workload /*throw X*/) + { + for (const auto& [folderPath, cb] : workload) + workload_.push_back({folderPath, cb}); + + while (!workload_.empty()) + { + WorkItem wi = std::move(workload_. back()); //yes, no strong exception guarantee (std::bad_alloc) + /**/ workload_.pop_back(); // + + tryReportingDirError([&] //throw X + { + traverseWithException(wi.dirPath, *wi.cb); //throw FileError, X + }, *wi.cb); + } + } + +private: + SingleFolderTraverser (const SingleFolderTraverser&) = delete; + SingleFolderTraverser& operator=(const SingleFolderTraverser&) = delete; + + void traverseWithException(const Zstring& dirPath, AFS::TraverserCallback& cb) //throw FileError, X + { + for (const auto& [itemName] : getDirContentFlat(dirPath)) //throw FileError + { + const Zstring itemPath = appendPath(dirPath, itemName); + + FsItemDetails itemDetails = {}; + if (!tryReportingItemError([&] //throw X + { + itemDetails = getItemDetails(itemPath); //throw FileError + }, cb, itemName)) + continue; //ignore error: skip file + + switch (itemDetails.type) + { + case ItemType::file: + cb.onFile({itemName, itemDetails.fileSize, itemDetails.modTime, itemDetails.filePrint, false /*isFollowedSymlink*/}); //throw X + break; + + case ItemType::folder: + if (std::shared_ptr cbSub = cb.onFolder({itemName, false /*isFollowedSymlink*/})) //throw X + workload_.push_back({itemPath, std::move(cbSub)}); + break; + + case ItemType::symlink: + switch (cb.onSymlink({itemName, itemDetails.modTime})) //throw X + { + case AFS::TraverserCallback::HandleLink::follow: + { + FsItemDetails targetDetails = {}; + if (!tryReportingItemError([&] //throw X + { + targetDetails = getSymlinkTargetDetails(itemPath); //throw FileError + }, cb, itemName)) + continue; + + if (targetDetails.type == ItemType::folder) + { + if (std::shared_ptr cbSub = cb.onFolder({itemName, true /*isFollowedSymlink*/})) //throw X + workload_.push_back({itemPath, std::move(cbSub)}); //symlink may link to different volume! + } + else //a file or named pipe, etc. + cb.onFile({itemName, targetDetails.fileSize, targetDetails.modTime, targetDetails.filePrint, true /*isFollowedSymlink*/}); //throw X + } + break; + + case AFS::TraverserCallback::HandleLink::skip: + break; + } + break; + } + } + } + + struct WorkItem + { + Zstring dirPath; + std::shared_ptr cb; + }; + std::vector workload_; +}; + + +void traverseFolderRecursiveNative(const std::vector>>& workload /*throw X*/, size_t) //throw X +{ + SingleFolderTraverser dummy(workload); //throw X +} +//==================================================================================================== +//==================================================================================================== + +class RecycleSessionNative : public AFS::RecycleSession +{ +public: + explicit RecycleSessionNative(const Zstring& baseFolderPath) : baseFolderPath_(baseFolderPath) {} + //constructor will be running on main thread => keep trivial and defer work to getRecyclerTempPath()! + + void moveToRecycleBin(const AbstractPath& itemPath, const Zstring& logicalRelPath) override; //throw FileError, RecycleBinUnavailable + void tryCleanup(const std::function& notifyDeletionStatus /*throw X*/) override; //throw FileError, X + +private: + const Zstring baseFolderPath_; +}; + +//=========================================================================================================================== + +struct InputStreamNative : public AFS::InputStream +{ + explicit InputStreamNative(const Zstring& filePath) : fileIn_(filePath) {} //throw FileError, ErrorFileLocked + + size_t getBlockSize() override { return fileIn_.getBlockSize(); } //throw FileError; non-zero block size is AFS contract! + + //may return short; only 0 means EOF! CONTRACT: bytesToRead > 0! + size_t tryRead(void* buffer, size_t bytesToRead, const IoCallback& notifyUnbufferedIO /*throw X*/) override //throw FileError, ErrorFileLocked, X + { + const size_t bytesRead = fileIn_.tryRead(buffer, bytesToRead); //throw FileError, ErrorFileLocked + if (notifyUnbufferedIO) notifyUnbufferedIO(bytesRead); //throw X + return bytesRead; + } + + std::optional tryGetAttributesFast() override //throw FileError + { + const NativeFileInfo& fileInfo = getNativeFileInfo(fileIn_); //throw FileError + + return AFS::StreamAttributes({nativeFileTimeToTimeT(fileInfo.modTime), + fileInfo.fileSize, + fileInfo.filePrint}); + } + +private: + FileInputPlain fileIn_; +}; + +//=========================================================================================================================== + +struct OutputStreamNative : public AFS::OutputStreamImpl +{ + OutputStreamNative(const Zstring& filePath, + std::optional streamSize, + std::optional modTime) : + fileOut_(filePath), //throw FileError, ErrorTargetExisting + modTime_(modTime) + { + if (streamSize) //preallocate disk space + reduce fragmentation + fileOut_.reserveSpace(*streamSize); //throw FileError + } + + size_t getBlockSize() override { return fileOut_.getBlockSize(); } //throw FileError + + size_t tryWrite(const void* buffer, size_t bytesToWrite, const IoCallback& notifyUnbufferedIO /*throw X*/) override //throw FileError, X; may return short! CONTRACT: bytesToWrite > 0 + { + const size_t bytesWritten = fileOut_.tryWrite(buffer, bytesToWrite); //throw FileError + if (notifyUnbufferedIO) notifyUnbufferedIO(bytesWritten); //throw X + return bytesWritten; + } + + AFS::FinalizeResult finalize(const IoCallback& notifyUnbufferedIO /*throw X*/) override //throw FileError, X + { + AFS::FinalizeResult result; + + result.filePrint = getNativeFileInfo(fileOut_).filePrint; //throw FileError + + fileOut_.close(); //throw FileError + //output finalized => no more exceptions from here on! + //-------------------------------------------------------------------- + + /* is setting modtime after closing the file handle a pessimization? + no, needed for functional correctness, see file_access.cpp::copyNewFile() for macOS/Linux + even required on Windows: https://freefilesync.org/forum/viewtopic.php?t=10781 */ + try + { + if (modTime_) + setFileTime(fileOut_.getFilePath(), *modTime_, ProcSymlink::follow); //throw FileError + } + catch (const FileError& e) { result.errorModTime = e; /*might slice derived class?*/ } + + return result; + } + +private: + FileOutputPlain fileOut_; + const std::optional modTime_; +}; + +//=========================================================================================================================== + +class NativeFileSystem : public AbstractFileSystem +{ +public: + explicit NativeFileSystem(const Zstring& rootPath) : rootPath_(rootPath) {} + + Zstring getNativePath(const AfsPath& itemPath) const { return isNullFileSystem() ? Zstring{} : appendPath(rootPath_, itemPath.value); } + +private: + Zstring getInitPathPhrase(const AfsPath& itemPath) const override { return makePathPhrase(getNativePath(itemPath)); } + + std::vector getPathPhraseAliases(const AfsPath& itemPath) const override + { + if (isNullFileSystem()) + return {}; + + return ::getPathPhraseAliases(getNativePath(itemPath)); + } + + std::wstring getDisplayPath(const AfsPath& itemPath) const override { return utfTo(getNativePath(itemPath)); } + + bool isNullFileSystem() const override { return rootPath_.empty(); } + + std::weak_ordering compareDeviceSameAfsType(const AbstractFileSystem& afsRhs) const override + { + return compareNativePath(rootPath_, static_cast(afsRhs).rootPath_); + } + + //---------------------------------------------------------------------------------------------------------------- + static ItemType zenToAfsItemType(zen::ItemType type) + { + switch (type) + { + case zen::ItemType::file: + return AFS::ItemType::file; + case zen::ItemType::folder: + return AFS::ItemType::folder; + case zen::ItemType::symlink: + return AFS::ItemType::symlink; + } + assert(false); + return static_cast(type); + } + + ItemType getItemType(const AfsPath& itemPath) const override //throw FileError + { + initComForThread(); //throw FileError + return zenToAfsItemType(zen::getItemType(getNativePath(itemPath))); //throw FileError + } + + std::optional getItemTypeIfExists(const AfsPath& itemPath) const override //throw FileError + { + initComForThread(); //throw FileError + if (const std::optional type = zen::getItemTypeIfExists(getNativePath(itemPath))) //throw FileError + return zenToAfsItemType(*type); + return std::nullopt; + } + + //---------------------------------------------------------------------------------------------------------------- + //already existing: fail + void createFolderPlain(const AfsPath& folderPath) const override //throw FileError + { + initComForThread(); //throw FileError + createDirectory(getNativePath(folderPath)); //throw FileError, ErrorTargetExisting + } + + void removeFilePlain(const AfsPath& filePath) const override //throw FileError + { + initComForThread(); //throw FileError + zen::removeFilePlain(getNativePath(filePath)); //throw FileError + } + + void removeSymlinkPlain(const AfsPath& linkPath) const override //throw FileError + { + initComForThread(); //throw FileError + zen::removeSymlinkPlain(getNativePath(linkPath)); //throw FileError + } + + void removeFolderPlain(const AfsPath& folderPath) const override //throw FileError + { + initComForThread(); //throw FileError + zen::removeDirectoryPlain(getNativePath(folderPath)); //throw FileError + } + + void removeFolderIfExistsRecursion(const AfsPath& folderPath, //throw FileError + const std::function& onBeforeFileDeletion /*throw X*/, + const std::function& onBeforeSymlinkDeletion/*throw X*/, + const std::function& onBeforeFolderDeletion /*throw X*/) const override + { + //default implementation: folder traversal + AFS::removeFolderIfExistsRecursion(folderPath, onBeforeFileDeletion, onBeforeSymlinkDeletion, onBeforeFolderDeletion); //throw FileError, X + } + + //---------------------------------------------------------------------------------------------------------------- + AbstractPath getSymlinkResolvedPath(const AfsPath& linkPath) const override //throw FileError + { + initComForThread(); //throw FileError + const Zstring nativePath = getNativePath(linkPath); + + const Zstring resolvedPath = zen::getSymlinkResolvedPath(nativePath); //throw FileError + const std::optional comp = parsePathComponents(resolvedPath); + if (!comp) + throw FileError(replaceCpy(_("Cannot determine final path for %x."), L"%x", fmtPath(nativePath)), + replaceCpy(L"Invalid path %x.", L"%x", fmtPath(resolvedPath))); + + return AbstractPath(makeSharedRef(comp->rootPath), AfsPath(comp->relPath)); + } + + bool equalSymlinkContentForSameAfsType(const AfsPath& linkPathL, const AbstractPath& linkPathR) const override //throw FileError + { + initComForThread(); //throw FileError + + const NativeFileSystem& nativeFsR = static_cast(linkPathR.afsDevice.ref()); + + const SymlinkRawContent linkContentL = getSymlinkRawContent(getNativePath(linkPathL)); //throw FileError + const SymlinkRawContent linkContentR = getSymlinkRawContent(nativeFsR.getNativePath(linkPathR.afsPath)); //throw FileError + + if (linkContentL.targetPath != linkContentR.targetPath) + return false; + + return true; + } + //---------------------------------------------------------------------------------------------------------------- + + //return value always bound: + std::unique_ptr getInputStream(const AfsPath& filePath) const override //throw FileError, ErrorFileLocked + { + initComForThread(); //throw FileError + return std::make_unique(getNativePath(filePath)); //throw FileError, ErrorFileLocked + } + + //already existing: undefined behavior! (e.g. fail/overwrite/auto-rename) + //=> actual behavior: fail with clear error message + std::unique_ptr getOutputStream(const AfsPath& filePath, //throw FileError + std::optional streamSize, + std::optional modTime) const override + { + initComForThread(); //throw FileError + return std::make_unique(getNativePath(filePath), streamSize, modTime); //throw FileError, ErrorTargetExisting + } + + //---------------------------------------------------------------------------------------------------------------- + void traverseFolderRecursive(const TraverserWorkload& workload /*throw X*/, size_t parallelOps) const override + { + //initComForThread() -> done on traverser worker threads + + std::vector>> initialWorkItems; + for (const auto& [folderPath, cb] : workload) + initialWorkItems.emplace_back(getNativePath(folderPath), cb); + + traverseFolderRecursiveNative(initialWorkItems, parallelOps); //throw X + } + //---------------------------------------------------------------------------------------------------------------- + + //symlink handling: follow + //already existing: undefined behavior! (e.g. fail/overwrite/auto-rename) + //=> actual behavior: fail with clear error message + FileCopyResult copyFileForSameAfsType(const AfsPath& sourcePath, const StreamAttributes& attrSource, //throw FileError, ErrorFileLocked, X + const AbstractPath& targetPath, bool copyFilePermissions, const IoCallback& notifyUnbufferedIO /*throw X*/) const override + { + const Zstring nativePathTarget = static_cast(targetPath.afsDevice.ref()).getNativePath(targetPath.afsPath); + + initComForThread(); //throw FileError + + const zen::FileCopyResult nativeResult = copyNewFile(getNativePath(sourcePath), nativePathTarget, notifyUnbufferedIO); //throw FileError, ErrorTargetExisting, ErrorFileLocked, X + + //at this point we know we created a new file, so it's fine to delete it for cleanup! + ZEN_ON_SCOPE_FAIL(try { zen::removeFilePlain(nativePathTarget); } + catch (const FileError& e) { logExtraError(e.toString()); }); + + if (copyFilePermissions) + copyItemPermissions(getNativePath(sourcePath), nativePathTarget, ProcSymlink::follow); //throw FileError + + FileCopyResult result; + result.fileSize = nativeResult.fileSize; + //caveat: modTime will be incorrect for file systems with imprecise file times, e.g. see FAT_FILE_TIME_PRECISION_SEC + result.modTime = nativeFileTimeToTimeT(nativeResult.sourceModTime); + result.sourceFilePrint = getFileFingerprint(nativeResult.sourceFileIdx); + result.targetFilePrint = getFileFingerprint(nativeResult.targetFileIdx); + result.errorModTime = nativeResult.errorModTime; + return result; + } + + //symlink handling: follow + //already existing: fail + void copyNewFolderForSameAfsType(const AfsPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions) const override //throw FileError + { + initComForThread(); //throw FileError + + const Zstring& sourcePathNative = getNativePath(sourcePath); + const Zstring& targetPathNative = static_cast(targetPath.afsDevice.ref()).getNativePath(targetPath.afsPath); + + zen::createDirectory(targetPathNative); //throw FileError, ErrorTargetExisting + + try + { + copyDirectoryAttributes(sourcePathNative, targetPathNative); //throw FileError + } + catch (FileError&) {} //[!] too unimportant + too frequent for external devices, e.g. "ERROR_INVALID_PARAMETER [SetFileInformationByHandle(FileBasicInfo)]" on Samba share + + if (copyFilePermissions) + copyItemPermissions(sourcePathNative, targetPathNative, ProcSymlink::follow); //throw FileError + } + + //already existing: fail + void copySymlinkForSameAfsType(const AfsPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions) const override //throw FileError + { + const Zstring targetPathNative = static_cast(targetPath.afsDevice.ref()).getNativePath(targetPath.afsPath); + + initComForThread(); //throw FileError + zen::copySymlink(getNativePath(sourcePath), targetPathNative); //throw FileError + + ZEN_ON_SCOPE_FAIL(try { zen::removeSymlinkPlain(targetPathNative); } + catch (const FileError& e) { logExtraError(e.toString()); }); + + if (copyFilePermissions) + copyItemPermissions(getNativePath(sourcePath), targetPathNative, ProcSymlink::asLink); //throw FileError + } + + //already existing: undefined behavior! (e.g. fail/overwrite) + //=> actual behavior: fail with clear error message + void moveAndRenameItemForSameAfsType(const AfsPath& pathFrom, const AbstractPath& pathTo) const override //throw FileError, ErrorMoveUnsupported + { + //perf test: detecting different volumes by path is ~30 times faster than having ::MoveFileEx() fail with ERROR_NOT_SAME_DEVICE (6µs vs 190µs) + //=> maybe we can even save some actual I/O in some cases? + if (compareDeviceSameAfsType(pathTo.afsDevice.ref()) != std::weak_ordering::equivalent) + throw ErrorMoveUnsupported(generateMoveErrorMsg(pathFrom, pathTo), _("Operation not supported between different devices.")); + + initComForThread(); //throw FileError + const Zstring nativePathTarget = static_cast(pathTo.afsDevice.ref()).getNativePath(pathTo.afsPath); + + zen::moveAndRenameItem(getNativePath(pathFrom), nativePathTarget, false /*replaceExisting*/); //throw FileError, ErrorTargetExisting, ErrorMoveUnsupported + //may fail with ERROR_ALREADY_EXISTS despite previously existing file already deleted + //=> reason: corrupted disk, fixable via Windows error checking! https://freefilesync.org/forum/viewtopic.php?t=9776 + } + + bool supportsPermissions(const AfsPath& folderPath) const override //throw FileError + { + initComForThread(); //throw FileError + return zen::supportsPermissions(getNativePath(folderPath)); + } + + //---------------------------------------------------------------------------------------------------------------- + FileIconHolder getFileIcon(const AfsPath& filePath, int pixelSize) const override //throw FileError; (optional return value) + { + initComForThread(); //throw FileError + try + { + return fff::getFileIcon(getNativePath(filePath), pixelSize); //throw SysError + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(getDisplayPath(filePath))), e.toString()); } + } + + ImageHolder getThumbnailImage(const AfsPath& filePath, int pixelSize) const override //throw FileError; (optional return value) + { + initComForThread(); //throw FileError + try + { + return fff::getThumbnailImage(getNativePath(filePath), pixelSize); //throw SysError + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(getDisplayPath(filePath))), e.toString()); } + } + + void authenticateAccess(const RequestPasswordFun& requestPassword /*throw X*/) const override //throw FileError, (X) + { + } + + bool hasNativeTransactionalCopy() const override { return false; } + //---------------------------------------------------------------------------------------------------------------- + + int64_t getFreeDiskSpace(const AfsPath& folderPath) const override //throw FileError, returns < 0 if not available + { + initComForThread(); //throw FileError + return zen::getFreeDiskSpace(getNativePath(folderPath)); //throw FileError + } + + std::unique_ptr createRecyclerSession(const AfsPath& folderPath) const override //throw FileError, (RecycleBinUnavailable) + { + initComForThread(); //throw FileError + return std::make_unique(getNativePath(folderPath)); + } + + //fails if item is not existing + void moveToRecycleBin(const AfsPath& itemPath) const override //throw FileError, RecycleBinUnavailable + { + initComForThread(); //throw FileError + zen::moveToRecycleBin(getNativePath(itemPath)); //throw FileError, RecycleBinUnavailable + } + + const Zstring rootPath_; +}; + +//=========================================================================================================================== + + + +//- fails if item is not existing +//- multi-threaded access: internally synchronized! +void RecycleSessionNative::moveToRecycleBin(const AbstractPath& itemPath, const Zstring& logicalRelPath) //throw FileError, RecycleBinUnavailable +{ + const Zstring& itemPathNative = getNativeItemPath(itemPath); + if (itemPathNative.empty()) + throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); + + zen::moveToRecycleBin(itemPathNative); //throw FileError, RecycleBinUnavailable +} + + +void RecycleSessionNative::tryCleanup(const std::function& notifyDeletionStatus /*throw X*/) //throw FileError, X +{ +} +} + + +//coordinate changes with getResolvedFilePath()! +bool fff::acceptsItemPathPhraseNative(const Zstring& itemPathPhrase) //noexcept +{ + Zstring path = expandMacros(itemPathPhrase); //expand before trimming! + trim(path); + + if (path.empty()) //eat up empty paths before other AFS implementations get a chance! + return true; + + + if (startsWith(path, Zstr('['))) //drive letter by volume name syntax + return true; + + //don't accept relative paths!!! indistinguishable from MTP paths as shown in Explorer's address bar! + return static_cast(parsePathComponents(path)); +} + + +AbstractPath fff::createItemPathNative(const Zstring& itemPathPhrase) //noexcept +{ + const Zstring& itemPath = getResolvedFilePath(itemPathPhrase); + return createItemPathNativeNoFormatting(itemPath); +} + + +AbstractPath fff::createItemPathNativeNoFormatting(const Zstring& nativePath) //noexcept +{ + if (const std::optional pc = parsePathComponents(nativePath)) + return AbstractPath(makeSharedRef(pc->rootPath), AfsPath(pc->relPath)); + + assert(nativePath.empty()); //broken path syntax + return AbstractPath(makeSharedRef(nativePath), AfsPath()); +} + + +Zstring fff::getNativeItemPath(const AbstractPath& itemPath) +{ + if (const auto nativeDevice = dynamic_cast(&itemPath.afsDevice.ref())) + return nativeDevice->getNativePath(itemPath.afsPath); + return {}; +} diff --git a/FreeFileSync/Source/afs/native.h b/FreeFileSync/Source/afs/native.h new file mode 100644 index 0000000..905edd7 --- /dev/null +++ b/FreeFileSync/Source/afs/native.h @@ -0,0 +1,25 @@ +// ***************************************************************************** +// * 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 FS_NATIVE_183247018532434563465 +#define FS_NATIVE_183247018532434563465 + +#include "abstract.h" + +namespace fff +{ +bool acceptsItemPathPhraseNative(const Zstring& itemPathPhrase); //noexcept +AbstractPath createItemPathNative(const Zstring& itemPathPhrase); //noexcept + +//------------------------------------------------------- + +AbstractPath createItemPathNativeNoFormatting(const Zstring& nativePath); //noexcept + +//return empty, if not a native path +Zstring getNativeItemPath(const AbstractPath& itemPath); +} + +#endif //FS_NATIVE_183247018532434563465 diff --git a/FreeFileSync/Source/afs/sftp.cpp b/FreeFileSync/Source/afs/sftp.cpp new file mode 100644 index 0000000..dc32d3a --- /dev/null +++ b/FreeFileSync/Source/afs/sftp.cpp @@ -0,0 +1,2209 @@ +// ***************************************************************************** +// * 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 "sftp.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include //DON'T include directly! +#include "init_curl_libssh2.h" +#include "ftp_common.h" +#include "abstract_impl.h" + #include + +using namespace zen; +using namespace fff; +using AFS = AbstractFileSystem; + + +namespace +{ +/* +SFTP specification version 3 (implemented by libssh2): https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt + +libssh2: prefer OpenSSL over WinCNG backend: + +WinCNG supports the following ciphers: + rijndael-cbc@lysator.liu.se + aes256-cbc + aes192-cbc + aes128-cbc + arcfour128 + arcfour + 3des-cbc + +OpenSSL supports the same ciphers like WinCNG plus the following: + aes256-ctr + aes192-ctr + aes128-ctr + cast128-cbc + blowfish-cbc */ + +constexpr ZstringView sftpPrefix = Zstr("sftp:"); + +constexpr std::chrono::seconds SFTP_SESSION_MAX_IDLE_TIME (20); +constexpr std::chrono::seconds SFTP_SESSION_CLEANUP_INTERVAL (4); //facilitate default of 5-seconds delay for error retry +constexpr std::chrono::seconds SFTP_CHANNEL_LIMIT_DETECTION_TIME_OUT(30); + +//permissions for new files: rw- rw- rw- [0666] => consider umask! (e.g. 0022 for ffs.org) +const long SFTP_DEFAULT_PERMISSION_FILE = LIBSSH2_SFTP_S_IRUSR | LIBSSH2_SFTP_S_IWUSR | + LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH | LIBSSH2_SFTP_S_IWOTH; + +//permissions for new folders: rwx rwx rwx [0777] => consider umask! (e.g. 0022 for ffs.org) +const long SFTP_DEFAULT_PERMISSION_FOLDER = LIBSSH2_SFTP_S_IRWXU | + LIBSSH2_SFTP_S_IRWXG | + LIBSSH2_SFTP_S_IRWXO; + +//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 = 16 * MAX_SFTP_READ_SIZE; //https://github.com/libssh2/libssh2/issues/90 +const size_t SFTP_OPTIMAL_BLOCK_SIZE_WRITE = 16 * MAX_SFTP_OUTGOING_SIZE; //need large buffer to mitigate libssh2 stupidly waiting on "acks": https://www.libssh2.org/libssh2_sftp_write.html +static_assert(MAX_SFTP_READ_SIZE == 30000 && MAX_SFTP_OUTGOING_SIZE == 30000, "reevaluate optimal block sizes if these constants change!"); + +/* Perf Test, Sourceforge frs, SFTP upload, compressed 25 MB test file: + +SFTP_OPTIMAL_BLOCK_SIZE_READ: SFTP_OPTIMAL_BLOCK_SIZE_WRITE: + multiples of multiples of + MAX_SFTP_READ_SIZE KB/s MAX_SFTP_OUTGOING_SIZE KB/s + 1 650 1 140 + 2 1000 2 280 + 4 1800 4 320 + 8 1800 8 320 + 16 1800 16 320 + 32 1800 32 320 + Filezilla download speed: 1800 KB/s Filezilla upload speed: 560 KB/s + DSL maximum download speed: 3060 KB/s DSL maximum upload speed: 620 KB/s + + +Perf Test 2: FFS hompage (2022-09-22) + +SFTP_OPTIMAL_BLOCK_SIZE_READ: SFTP_OPTIMAL_BLOCK_SIZE_WRITE: + multiples of multiples of + MAX_SFTP_READ_SIZE MB/s MAX_SFTP_OUTGOING_SIZE MB/s + 1 0,77 1 0.25 + 2 1,63 2 0.50 + 4 3,43 4 0.97 + 8 6,93 8 1.86 + 16 9,41 16 3.60 + 32 9,58 32 3.83 + Filezilla download speed: 12,2 MB/s Filezilla upload speed: 4.4 MB/s -> unfair comparison: FFS seems slower because it includes setup work, e.g. open file handle + DSL maximum download speed: 12,9 MB/s DSL maximum upload speed: 4,7 MB/s + +=> libssh2_sftp_read/libssh2_sftp_write may take quite long for 16x and larger => use smallest multiple that fills bandwidth! */ + + +inline +uint16_t getEffectivePort(int portOption) +{ + if (portOption > 0) + return static_cast(portOption); + return DEFAULT_PORT_SFTP; +} + + +struct SshDeviceId //= what defines a unique SFTP location +{ + /*explicit*/ SshDeviceId(const SftpLogin& login) : + server(login.server), + port(getEffectivePort(login.portCfg)), + username(login.username) {} + + Zstring server; + uint16_t port; //must be valid port! + Zstring username; +}; +std::weak_ordering operator<=>(const SshDeviceId& lhs, const SshDeviceId& rhs) +{ + //exactly the type of case insensitive comparison we need for server names! https://docs.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfow#IDNs + if (const std::weak_ordering cmp = compareAsciiNoCase(lhs.server, rhs.server); + cmp != std::weak_ordering::equivalent) + return cmp; + + return std::tie(lhs.port, lhs.username) <=> //username: case sensitive! + std::tie(rhs.port, rhs.username); +} +//also needed by compareDeviceSameAfsType(), so can't just replace with hash and use std::unordered_map + + +struct SshSessionCfg //= config for buffered SFTP session +{ + SshDeviceId deviceId; + SftpAuthType authType = SftpAuthType::password; + Zstring password; //authType == password or keyFile + Zstring privateKeyFilePath; //authType == keyFile: use PEM-encoded private key (protected by password) for authentication + bool allowZlib = false; +}; +bool operator==(const SshSessionCfg& lhs, const SshSessionCfg& rhs) +{ + if (lhs.deviceId <=> rhs.deviceId != std::weak_ordering::equivalent) + return false; + + if (std::tie(lhs.authType, lhs.allowZlib) != + std::tie(rhs.authType, rhs.allowZlib)) + return false; + + switch (lhs.authType) + { + case SftpAuthType::password: + return lhs.password == rhs.password; //case sensitive! + + case SftpAuthType::keyFile: + return std::tie(lhs.password, lhs.privateKeyFilePath) == //case sensitive! + std::tie(rhs.password, rhs.privateKeyFilePath); // + + case SftpAuthType::agent: + return true; + } + assert(false); + return true; +} + + +Zstring concatenateSftpFolderPathPhrase(const SftpLogin& login, const AfsPath& itemPath); //noexcept + + +std::string getLibssh2Path(const AfsPath& itemPath) +{ + return utfTo(getServerRelPath(itemPath)); +} + + +std::wstring getSftpDisplayPath(const SshDeviceId& deviceId, const AfsPath& itemPath) +{ + Zstring displayPath = Zstring(sftpPrefix) + Zstr("//"); + + if (!deviceId.username.empty()) //show username! consider AFS::compareDeviceSameAfsType() + displayPath += deviceId.username + Zstr('@'); + + //if (parseIpv6Address(deviceId.server) && deviceId.port != DEFAULT_PORT_SFTP) + // displayPath += Zstr('[') + deviceId.server + Zstr(']'); + //else + displayPath += deviceId.server; + + //if (deviceId.port != DEFAULT_PORT_SFTP) + // displayPath += Zstr(':') + numberTo(deviceId.port); + + const Zstring& relPath = getServerRelPath(itemPath); + if (relPath != Zstr("/")) + displayPath += relPath; + + return utfTo(displayPath); +} + +//=========================================================================================================================== + +//=> most likely *not* a connection issue +struct SysErrorSftpProtocol : public zen::SysError +{ + SysErrorSftpProtocol(const std::wstring& msg, unsigned long sftpError) : SysError(msg), sftpErrorCode(sftpError) {} + + const unsigned long sftpErrorCode; +}; + +DEFINE_NEW_SYS_ERROR(SysErrorPassword) + + +constinit Global globalSftpSessionCount; +GLOBAL_RUN_ONCE(globalSftpSessionCount.set(createUniSessionCounter())); + + +class SshSession +{ +public: + SshSession(const SshSessionCfg& sessionCfg, int timeoutSec) : //throw SysError, SysErrorPassword + sessionCfg_(sessionCfg) + { + ZEN_ON_SCOPE_FAIL(cleanup()); //destructor call would lead to member double clean-up!!! + + const Zstring& serviceName = numberTo(sessionCfg_.deviceId.port); + + socket_.emplace(sessionCfg_.deviceId.server, serviceName, timeoutSec); //throw SysError + + sshSession_ = ::libssh2_session_init(); + if (!sshSession_) //does not set ssh last error; source: only memory allocation may fail + throw SysError(formatSystemError("libssh2_session_init", formatSshStatusCode(LIBSSH2_ERROR_ALLOC), L"")); + + //if zlib compression causes trouble, make it a user setting: https://freefilesync.org/forum/viewtopic.php?t=6663 + //=> surprise: it IS causing trouble: slow-down in local syncs: https://freefilesync.org/forum/viewtopic.php?t=7244#p24250 + if (sessionCfg_.allowZlib) + if (const int rc = ::libssh2_session_flag(sshSession_, LIBSSH2_FLAG_COMPRESS, 1); + rc != 0) //does not set SSH last error + throw SysError(formatSystemError("libssh2_session_flag", formatSshStatusCode(rc), L"")); + + ::libssh2_session_set_blocking(sshSession_, 1); + + //we don't consider the timeout part of the session when it comes to reuse! but we already require it during initialization + ::libssh2_session_set_timeout(sshSession_, timeoutSec * 1000 /*ms*/); + + + if (::libssh2_session_handshake(sshSession_, socket_->get()) != 0) + throw SysError(formatLastSshError("libssh2_session_handshake", nullptr)); + + //evaluate fingerprint = libssh2_hostkey_hash(sshSession_, LIBSSH2_HOSTKEY_HASH_SHA1) ??? + + const auto usernameUtf8 = utfTo(sessionCfg_.deviceId.username); + const auto passwordUtf8 = utfTo(sessionCfg_.password); + + const char* authList = ::libssh2_userauth_list(sshSession_, usernameUtf8); + if (!authList) + { + if (::libssh2_userauth_authenticated(sshSession_) != 1) + throw SysError(formatLastSshError("libssh2_userauth_list", nullptr)); + //else: SSH_USERAUTH_NONE has authenticated successfully => we're already done + } + else + { + bool supportAuthPassword = false; + bool supportAuthKeyfile = false; + bool supportAuthInteractive = false; + split(authList, ',', [&](std::string_view authMethod) + { + authMethod = trimCpy(authMethod); + if (!authMethod.empty()) + { + if (authMethod == "password") + supportAuthPassword = true; + else if (authMethod == "publickey") + supportAuthKeyfile = true; + else if (authMethod == "keyboard-interactive") + supportAuthInteractive = true; + } + }); + + switch (sessionCfg_.authType) + { + case SftpAuthType::password: + { + if (supportAuthPassword) + { + if (::libssh2_userauth_password(sshSession_, usernameUtf8, passwordUtf8) != 0) + throw SysErrorPassword(formatLastSshError("libssh2_userauth_password", nullptr)); + } + else if (supportAuthInteractive) //some servers, e.g. web.sourceforge.net, support "keyboard-interactive", but not "password" + { + std::wstring unexpectedPrompts; + + auto authCallback = [&](int num_prompts, const LIBSSH2_USERAUTH_KBDINT_PROMPT* prompts, LIBSSH2_USERAUTH_KBDINT_RESPONSE* responses) + { + //note: FileZilla assumes password requests when it finds "num_prompts == 1" and "!echo" -> prompt may be localized! + //test case: sourceforge.net sends a single "Password: " prompt with "!echo" + if (num_prompts == 1 && prompts[0].echo == 0) + { + responses[0].text = //pass ownership; will be ::free()d + ::strdup(passwordUtf8.c_str()); + responses[0].length = static_cast(passwordUtf8.size()); + } + else + for (int i = 0; i < num_prompts; ++i) + unexpectedPrompts += (unexpectedPrompts.empty() ? L"" : L"|") + utfTo(makeStringView(reinterpret_cast(prompts[i].text), prompts[i].length)); + }; + using AuthCbType = decltype(authCallback); + + auto authCallbackWrapper = [](const char* name, int name_len, const char* instruction, int instruction_len, + int num_prompts, const LIBSSH2_USERAUTH_KBDINT_PROMPT* prompts, LIBSSH2_USERAUTH_KBDINT_RESPONSE* responses, void** abstract) + { + try + { + AuthCbType* callback = *reinterpret_cast(abstract); //free this poor little C-API from its shackles and redirect to a proper lambda + (*callback)(num_prompts, prompts, responses); //name, instruction are nullptr for sourceforge.net + } + catch (...) { assert(false); } + }; + + if (*::libssh2_session_abstract(sshSession_)) + throw SysError(L"libssh2_session_abstract: non-null value"); + + *reinterpret_cast(::libssh2_session_abstract(sshSession_)) = &authCallback; + ZEN_ON_SCOPE_EXIT(*::libssh2_session_abstract(sshSession_) = nullptr); + + if (::libssh2_userauth_keyboard_interactive(sshSession_, usernameUtf8, authCallbackWrapper) != 0) + throw SysErrorPassword(formatLastSshError("libssh2_userauth_keyboard_interactive", nullptr) + + (unexpectedPrompts.empty() ? L"" : L"\nUnexpected prompts: " + unexpectedPrompts)); + } + else + throw SysError(replaceCpy(_("The server does not support authentication via %x."), L"%x", L"\"username/password\"") + + L'\n' +_("Required:") + L' ' + utfTo(authList)); + } + break; + + case SftpAuthType::keyFile: + { + if (!supportAuthKeyfile) + throw SysError(replaceCpy(_("The server does not support authentication via %x."), L"%x", L"\"key file\"") + + L'\n' +_("Required:") + L' ' + utfTo(authList)); + + std::string passphrase = passwordUtf8; + std::string pkStream; + try + { + pkStream = getFileContent(sessionCfg_.privateKeyFilePath, nullptr /*notifyUnbufferedIO*/); //throw FileError + trim(pkStream); + } + catch (const FileError& e) { throw SysError(replaceCpy(e.toString(), L"\n\n", L'\n')); } //errors should be further enriched by context info => SysError + + //libssh2 doesn't support the PuTTY key file format, but we do! + if (isPuttyKeyStream(pkStream)) + try + { + pkStream = convertPuttyKeyToPkix(pkStream, passphrase); //throw SysError + passphrase.clear(); + } + catch (const SysError& e) //add context + { + throw SysErrorPassword(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(sessionCfg_.privateKeyFilePath)) + L' ' + e.toString()); + } + + if (::libssh2_userauth_publickey_frommemory(sshSession_, usernameUtf8, pkStream, passphrase) != 0) //const char* passphrase + { + //libssh2_userauth_publickey_frommemory()'s "Unable to extract public key from private key" isn't exactly *helpful* + //=> detect invalid key files and give better error message: + const wchar_t* invalidKeyFormat = [&]() -> const wchar_t* + { + //"-----BEGIN PUBLIC KEY-----" OpenSSH SSH-2 public key (X.509 SubjectPublicKeyInfo) = PKIX + //"-----BEGIN RSA PUBLIC KEY-----" OpenSSH SSH-2 public key (PKCS#1 RSAPublicKey) + //"---- BEGIN SSH2 PUBLIC KEY ----" SSH-2 public key (RFC 4716 format) + const std::string_view firstLine = makeStringView(pkStream.begin(), std::find_if(pkStream.begin(), pkStream.end(), isLineBreak)); + if (contains(firstLine, "PUBLIC KEY")) + return L"OpenSSH public key"; + + if (startsWith(pkStream, "rsa-") || //rsa-sha2-256, rsa-sha2-512 + startsWith(pkStream, "ssh-") || //ssh-rsa, ssh-dss, ssh-ed25519, ssh-ed448 + startsWith(pkStream, "ecdsa-")) //ecdsa-sha2-nistp256, ecdsa-sha2-nistp384, ecdsa-sha2-nistp521 + return L"OpenSSH public key"; //OpenSSH SSH-2 public key + + if (std::count(pkStream.begin(), pkStream.end(), ' ') == 2 && + /**/std::all_of(pkStream.begin(), pkStream.end(), [](const char c) { return isDigit(c) || c == ' '; })) + return L"SSH-1 public key"; + + //"-----BEGIN PRIVATE KEY-----" => OpenSSH SSH-2 private key (PKCS#8 PrivateKeyInfo) => should work + //"-----BEGIN ENCRYPTED PRIVATE KEY-----" => OpenSSH SSH-2 private key (PKCS#8 EncryptedPrivateKeyInfo) => should work + //"-----BEGIN RSA PRIVATE KEY-----" => OpenSSH SSH-2 private key (PKCS#1 RSAPrivateKey) => should work + //"-----BEGIN DSA PRIVATE KEY-----" => OpenSSH SSH-2 private key (PKCS#1 DSAPrivateKey) => should work + //"-----BEGIN EC PRIVATE KEY-----" => OpenSSH SSH-2 private key (PKCS#1 ECPrivateKey) => should work + //"-----BEGIN OPENSSH PRIVATE KEY-----" => OpenSSH SSH-2 private key (new format) => should work + //"---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----" => ssh.com SSH-2 private key => unclear + //"SSH PRIVATE KEY FILE FORMAT 1.1" => SSH-1 private key => unclear + return nullptr; //other: maybe invalid, maybe not + }(); + if (invalidKeyFormat) + throw SysError(_("Authentication failed.") + L' ' + + replaceCpy(L"%x is not an OpenSSH or PuTTY private key file.", L"%x", + fmtPath(sessionCfg_.privateKeyFilePath) + L" [" + invalidKeyFormat + L']')); + if (isPuttyKeyStream(pkStream)) + throw SysError(formatLastSshError("libssh2_userauth_publickey_frommemory", nullptr)); + else + //can't rely on LIBSSH2_ERROR_AUTHENTICATION_FAILED: https://github.com/libssh2/libssh2/pull/789 + throw SysErrorPassword(formatLastSshError("libssh2_userauth_publickey_frommemory", nullptr)); + } + } + break; + + case SftpAuthType::agent: + { + LIBSSH2_AGENT* sshAgent = ::libssh2_agent_init(sshSession_); + if (!sshAgent) + throw SysError(formatLastSshError("libssh2_agent_init", nullptr)); + ZEN_ON_SCOPE_EXIT(::libssh2_agent_free(sshAgent)); + + if (::libssh2_agent_connect(sshAgent) != 0) + throw SysError(formatLastSshError("libssh2_agent_connect", nullptr)); + ZEN_ON_SCOPE_EXIT(::libssh2_agent_disconnect(sshAgent)); + + if (::libssh2_agent_list_identities(sshAgent) != 0) + throw SysError(formatLastSshError("libssh2_agent_list_identities", nullptr)); + + for (libssh2_agent_publickey* prev = nullptr;;) + { + libssh2_agent_publickey* identity = nullptr; + const int rc = ::libssh2_agent_get_identity(sshAgent, &identity, prev); + if (rc == 0) //public key returned + ; + else if (rc == 1) //no more public keys + throw SysError(L"SSH agent contains no matching public key."); + else + throw SysError(formatLastSshError("libssh2_agent_get_identity", nullptr)); + + if (::libssh2_agent_userauth(sshAgent, usernameUtf8.c_str(), identity) == 0) + break; //authentication successful + + //else: failed => try next public key + prev = identity; + } + } + break; + } + } + + lastSuccessfulUseTime_ = std::chrono::steady_clock::now(); + } + + ~SshSession() { cleanup(); } + + const SshSessionCfg& getSessionCfg() const + { + static_assert(std::is_const_v, "keep this function thread-safe!"); + return sessionCfg_; + } + + bool isHealthy() const + { + for (const SftpChannelInfo& ci : sftpChannels_) + if (ci.nbInfo.commandPending) + return false; + + if (nbInfo_.commandPending) + return false; + + if (possiblyCorrupted_) + return false; + + if (std::chrono::steady_clock::now() > lastSuccessfulUseTime_ + SFTP_SESSION_MAX_IDLE_TIME) + return false; + + return true; + } + + void markAsCorrupted() { possiblyCorrupted_ = true; } + + struct Details + { + LIBSSH2_SESSION* sshSession; + LIBSSH2_SFTP* sftpChannel; + }; + + size_t getSftpChannelCount() const { return sftpChannels_.size(); } + + //return "false" if pending + bool tryNonBlocking(size_t channelNo, std::chrono::steady_clock::time_point commandStartTime, const char* functionName, + const std::function& sftpCommand /*noexcept!*/, int timeoutSec) //throw SysError, SysErrorSftpProtocol + { + assert(::libssh2_session_get_blocking(sshSession_)); + ::libssh2_session_set_blocking(sshSession_, 0); + ZEN_ON_SCOPE_EXIT(::libssh2_session_set_blocking(sshSession_, 1)); + + //yes, we're non-blocking, still won't hurt to set the timeout in case libssh2 decides to use it nevertheless + ::libssh2_session_set_timeout(sshSession_, timeoutSec * 1000 /*ms*/); + + LIBSSH2_SFTP* sftpChannel = channelNo < sftpChannels_.size() ? sftpChannels_[channelNo].sftpChannel : nullptr; + SftpNonBlockInfo& nbInfo = channelNo < sftpChannels_.size() ? sftpChannels_[channelNo].nbInfo : nbInfo_; + + if (!nbInfo.commandPending) + assert(nbInfo.commandStartTime != commandStartTime); + else if (nbInfo.commandStartTime == commandStartTime && nbInfo.functionName == functionName) + ; //continue pending SFTP call + else + { + assert(false); //pending sftp command is not completed by client: e.g. libssh2_sftp_close() cleaning up after a timed-out libssh2_sftp_read() + possiblyCorrupted_ = true; //=> start new command (with new start time), but remember to not trust this session anymore! + } + nbInfo.commandPending = true; + nbInfo.commandStartTime = commandStartTime; + nbInfo.functionName = functionName; + + int rc = LIBSSH2_ERROR_NONE; + try + { + rc = sftpCommand({sshSession_, sftpChannel}); //noexcept + } + catch (...) { assert(false); rc = LIBSSH2_ERROR_BAD_USE; } + + assert(rc >= 0 || ::libssh2_session_last_errno(sshSession_) == rc); + if (rc < 0 && ::libssh2_session_last_errno(sshSession_) != rc) //when libssh2 fails to properly set last error; e.g. https://github.com/libssh2/libssh2/pull/123 + ::libssh2_session_set_last_error(sshSession_, rc, nullptr); + + if (rc >= LIBSSH2_ERROR_NONE || + (rc == LIBSSH2_ERROR_SFTP_PROTOCOL && ::libssh2_sftp_last_error(sftpChannel) != LIBSSH2_FX_OK)) + //libssh2 source: LIBSSH2_ERROR_SFTP_PROTOCOL *without* setting LIBSSH2_SFTP::last_errno indicates a corrupted connection! + { + nbInfo.commandPending = false; // + lastSuccessfulUseTime_ = std::chrono::steady_clock::now(); //[!] LIBSSH2_ERROR_SFTP_PROTOCOL is NOT an SSH error => the SSH session is just fine! + + if (rc == LIBSSH2_ERROR_SFTP_PROTOCOL) + throw SysErrorSftpProtocol(formatLastSshError(functionName, sftpChannel), ::libssh2_sftp_last_error(sftpChannel)); + return true; + } + else if (rc == LIBSSH2_ERROR_EAGAIN) + { + if (std::chrono::steady_clock::now() > nbInfo.commandStartTime + std::chrono::seconds(timeoutSec)) + //consider SSH session corrupted! => isHealthy() will see pending command + throw SysError(formatSystemError(functionName, formatSshStatusCode(LIBSSH2_ERROR_TIMEOUT), + _P("Operation timed out after 1 second.", "Operation timed out after %x seconds.", timeoutSec))); + return false; + } + else //=> SSH session errors only (hopefully!) e.g. LIBSSH2_ERROR_SOCKET_RECV + //consider SSH session corrupted! => isHealthy() will see pending command + throw SysError(formatLastSshError(functionName, sftpChannel)); + } + + //returns when traffic is available or time out: both cases are handled by next tryNonBlocking() call + static void waitForTraffic(const std::vector& sshSessions, int timeoutSec) //throw SysError + { + //reference: session.c: _libssh2_wait_socket() + std::vector fds; + std::chrono::steady_clock::time_point startTimeMin = std::chrono::steady_clock::time_point::max(); + + for (SshSession* session : sshSessions) + { + assert(::libssh2_session_last_errno(session->sshSession_) == LIBSSH2_ERROR_EAGAIN); + assert(session->nbInfo_.commandPending || std::any_of(session->sftpChannels_.begin(), session->sftpChannels_.end(), [](SftpChannelInfo& ci) { return ci.nbInfo.commandPending; })); + + pollfd pfd{.fd = session->socket_->get()}; + + const int dir = ::libssh2_session_block_directions(session->sshSession_); + assert(dir != 0); //we assert a blocked direction after libssh2 returned LIBSSH2_ERROR_EAGAIN! + if (dir & LIBSSH2_SESSION_BLOCK_INBOUND) + pfd.events |= POLLIN; + if (dir & LIBSSH2_SESSION_BLOCK_OUTBOUND) + pfd.events |= POLLOUT; + + if (pfd.events != 0) + fds.push_back(pfd); + + for (const SftpChannelInfo& ci : session->sftpChannels_) + if (ci.nbInfo.commandPending) + startTimeMin = std::min(startTimeMin, ci.nbInfo.commandStartTime); + if (session->nbInfo_.commandPending) + startTimeMin = std::min(startTimeMin, session->nbInfo_.commandStartTime); + } + + if (!fds.empty()) + { + assert(startTimeMin != std::chrono::steady_clock::time_point::max()); + const auto now = std::chrono::steady_clock::now(); + const auto stopTime = startTimeMin + std::chrono::seconds(timeoutSec); + if (now >= stopTime) + return; //time-out! => let next tryNonBlocking() call fail with detailed error! + const auto waitTimeMs = std::chrono::duration_cast(stopTime - now).count(); + + //is poll() on macOS broken? https://daniel.haxx.se/blog/2016/10/11/poll-on-mac-10-12-is-broken/ + // it seems Daniel only takes issue with "empty" input handling!? => not an issue for us + const char* functionName = "poll"; + const int rv = ::poll(fds.data(), //struct pollfd* fds + fds.size(), //nfds_t nfds + waitTimeMs); //int timeout [ms] + if (rv < 0) //consider SSH sessions corrupted! => isHealthy() will see pending commands + throw SysError(formatSystemError(functionName, getLastError())); + + if (rv == 0) //time-out! => let next tryNonBlocking() call fail with detailed error! + return; + } + else assert(false); + } + + static void addSftpChannel(const std::vector& sshSessions, int timeoutSec) //throw SysError + { + auto addChannelDetails = [](const std::wstring& msg, SshSession& sshSession) //when hitting the server's SFTP channel limit, inform user about channel number + { + if (sshSession.sftpChannels_.empty()) + return msg; + return msg + L' ' + replaceCpy(_("Failed to open SFTP channel number %x."), L"%x", formatNumber(sshSession.sftpChannels_.size() + 1)); + }; + + std::optional firstSysError; + + std::vector pendingSessions = sshSessions; + const auto sftpCommandStartTime = std::chrono::steady_clock::now(); + + for (;;) + { + //create all SFTP sessions in parallel => non-blocking + //note: each libssh2_sftp_init() consists of multiple round-trips => poll until all sessions are finished, don't just init and then block on each! + for (size_t pos = pendingSessions.size(); pos-- > 0 ; ) //CAREFUL WITH THESE ERASEs (invalidate positions!!!) + try + { + if (pendingSessions[pos]->tryNonBlocking(static_cast(-1), sftpCommandStartTime, "libssh2_sftp_init", + [&](const SshSession::Details& sd) //noexcept! + { + LIBSSH2_SFTP* sftpChannelNew = ::libssh2_sftp_init(sd.sshSession); + if (!sftpChannelNew) + return std::min(::libssh2_session_last_errno(sd.sshSession), LIBSSH2_ERROR_SOCKET_NONE); + //just in case libssh2 failed to properly set last error; e.g. https://github.com/libssh2/libssh2/pull/123 + + pendingSessions[pos]->sftpChannels_.emplace_back(sftpChannelNew); + return LIBSSH2_ERROR_NONE; + }, timeoutSec)) //throw SysError, (SysErrorSftpProtocol) + pendingSessions.erase(pendingSessions.begin() + pos); //= not pending + } + catch (const SysError& e) + { + if (!firstSysError) //don't throw yet and corrupt other valid, but pending SshSessions! We also don't want to leak LIBSSH2_SFTP* waiting in libssh2 code + firstSysError = SysError(addChannelDetails(e.toString(), *pendingSessions[pos])); + //SysErrorSftpProtocol? unexpected during libssh2_sftp_init() + //-> still occuring for whatever reason!? => "slice" down to SysError + pendingSessions.erase(pendingSessions.begin() + pos); + } + + if (pendingSessions.empty()) + { + if (firstSysError) + throw* firstSysError; + return; + } + + waitForTraffic(pendingSessions, timeoutSec); //throw SysError + } + } + +private: + SshSession (const SshSession&) = delete; + SshSession& operator=(const SshSession&) = delete; + + void cleanup() //attention: may block heavily after error! + { + for (SftpChannelInfo& ci : sftpChannels_) + //ci.nbInfo.commandPending? => may "legitimately" happen when an SFTP command times out + if (::libssh2_sftp_shutdown(ci.sftpChannel) != LIBSSH2_ERROR_NONE) + assert(false); + + if (sshSession_) + { + //*INDENT-OFF* + if (!nbInfo_.commandPending && std::all_of(sftpChannels_.begin(), sftpChannels_.end(), + [](const SftpChannelInfo& ci) { return !ci.nbInfo.commandPending; })) + if (::libssh2_session_disconnect(sshSession_, "FreeFileSync says \"bye\"!") != LIBSSH2_ERROR_NONE) //= server notification only! no local cleanup apparently + assert(false); + //else: avoid further stress on the broken SSH session and take French leave + + //nbInfo_.commandPending? => have to clean up, no matter what! + if (::libssh2_session_free(sshSession_) != LIBSSH2_ERROR_NONE) + assert(false); + //*INDENT-ON* + } + } + + std::wstring formatLastSshError(const char* 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(lastErrorMsg)); + + //LIBSSH2_ERROR_SFTP_PROTOCOL does *not* mean libssh2_sftp_last_error() is also available! + //But if it's not, we have a broken connection, and lastErrorMsg contains meaningful details! + if (sshStatusCode == LIBSSH2_ERROR_SFTP_PROTOCOL && ::libssh2_sftp_last_error(sftpChannel) != LIBSSH2_FX_OK) + { + if (errorMsg == L"SFTP Protocol Error") //that's trite! + errorMsg.clear(); + return formatSystemError(functionName, formatSftpStatusCode(::libssh2_sftp_last_error(sftpChannel)), errorMsg); + } + + return formatSystemError(functionName, formatSshStatusCode(sshStatusCode), errorMsg); + } + + struct SftpNonBlockInfo + { + bool commandPending = false; + std::chrono::steady_clock::time_point commandStartTime; //specified by client, try to detect libssh2 usage errors + std::string functionName; + }; + + struct SftpChannelInfo + { + explicit SftpChannelInfo(LIBSSH2_SFTP* sc) : sftpChannel(sc) {} + + LIBSSH2_SFTP* sftpChannel = nullptr; + SftpNonBlockInfo nbInfo; + }; + + std::optional socket_; //*bound* after constructor has run + LIBSSH2_SESSION* sshSession_ = nullptr; + std::vector sftpChannels_; + bool possiblyCorrupted_ = false; + + SftpNonBlockInfo nbInfo_; //for SSH session, e.g. libssh2_sftp_init() + + const SshSessionCfg sessionCfg_; + const std::shared_ptr libsshCurlUnifiedInitCookie_{(getLibsshCurlUnifiedInitCookie(globalSftpSessionCount))}; //throw SysError + std::chrono::steady_clock::time_point lastSuccessfulUseTime_; //...of the SSH session (but not necessarily the SFTP functionality!) +}; + +//=========================================================================================================================== +//=========================================================================================================================== + +class SftpSessionManager //reuse (healthy) SFTP sessions globally +{ + struct SshSessionCache; + +public: + SftpSessionManager() : sessionCleaner_([this] + { + setCurrentThreadName(Zstr("Session Cleaner[SFTP]")); + runGlobalSessionCleanUp(); /*throw ThreadStopRequest*/ + }) {} + + struct ReUseOnDelete + { + void operator()(SshSession* s) const; + }; + + class SshSessionShared + { + public: + SshSessionShared(std::unique_ptr&& idleSession, int timeoutSec) : + session_(std::move(idleSession)) /*bound!*/, timeoutSec_(timeoutSec) { /*assert(session_->isHealthy());*/ } + + //we need two-step initialization: 1. constructor is FAST and noexcept 2. init() is SLOW and throws + void initSftpChannel() //throw SysError + { + if (session_->getSftpChannelCount() == 0) //make sure the SSH session contains at least one SFTP channel + SshSession::addSftpChannel({session_.get()}, timeoutSec_); //throw SysError + } + + void executeBlocking(const char* functionName, const std::function& sftpCommand /*noexcept!*/) //throw SysError, SysErrorSftpProtocol + { + assert(threadId_ == std::this_thread::get_id()); + assert(session_->getSftpChannelCount() > 0); + const auto sftpCommandStartTime = std::chrono::steady_clock::now(); + + for (;;) + if (session_->tryNonBlocking(0 /*channelNo*/, sftpCommandStartTime, functionName, sftpCommand, timeoutSec_)) //throw SysError, SysErrorSftpProtocol + return; + else //pending + SshSession::waitForTraffic({session_.get()}, timeoutSec_); //throw SysError + } + + const SshSessionCfg& getSessionCfg() const { return session_->getSessionCfg(); } //thread-safe + + private: + std::unique_ptr session_; //bound! + const std::thread::id threadId_ = std::this_thread::get_id(); + const int timeoutSec_; + }; + + class SshSessionExclusive + { + public: + SshSessionExclusive(std::unique_ptr&& idleSession, int timeoutSec) : + session_(std::move(idleSession)) /*bound!*/, timeoutSec_(timeoutSec) { /*assert(session_->isHealthy());*/ } + + bool tryNonBlocking(size_t channelNo, std::chrono::steady_clock::time_point commandStartTime, const char* functionName, //throw SysError, SysErrorSftpProtocol + const std::function& sftpCommand /*noexcept!*/) + { + return session_->tryNonBlocking(channelNo, commandStartTime, functionName, sftpCommand, timeoutSec_); //throw SysError, SysErrorSftpProtocol + } + + void waitForTraffic() //throw SysError + { + SshSession::waitForTraffic({session_.get()}, timeoutSec_); //throw SysError + } + + size_t getSftpChannelCount() const { return session_->getSftpChannelCount(); } + + void markAsCorrupted() { session_->markAsCorrupted(); } + + static void addSftpChannel(const std::vector& exSessions) //throw SysError + { + std::vector sshSessions; + for (SshSessionExclusive* exSession : exSessions) + sshSessions.push_back(exSession->session_.get()); + + int timeoutSec = 0; + for (SshSessionExclusive* exSession : exSessions) + timeoutSec = std::max(timeoutSec, exSession->timeoutSec_); + + SshSession::addSftpChannel(sshSessions, timeoutSec); //throw SysError + } + + static void waitForTraffic(const std::vector& exSessions) //throw SysError + { + std::vector sshSessions; + for (SshSessionExclusive* exSession : exSessions) + sshSessions.push_back(exSession->session_.get()); + + int timeoutSec = 0; + for (SshSessionExclusive* exSession : exSessions) + timeoutSec = std::max(timeoutSec, exSession->timeoutSec_); + + SshSession::waitForTraffic(sshSessions, timeoutSec); //throw SysError + } + + const SshSessionCfg& getSessionCfg() const { return session_->getSessionCfg(); } //thread-safe + + private: + std::unique_ptr session_; //bound! + const int timeoutSec_; + }; + + + std::shared_ptr getSharedSession(const SftpLogin& login) //throw SysError, SysErrorPassword + { + Protected& sessionCache = getSessionCache(login); + + const std::thread::id threadId = std::this_thread::get_id(); + std::shared_ptr sharedSession; //either or + std::optional sessionCfg; // + + sessionCache.access([&](SshSessionCache& cache) + { + if (!cache.activeCfg) //AFS::authenticateAccess() not called => authenticate implicitly! + setActiveConfig(cache, login); + + std::weak_ptr& sharedSessionWeak = cache.sshSessionsWithThreadAffinity[threadId]; //get or create + if (auto session = sharedSessionWeak.lock()) + //dereference session ONLY after affinity to THIS thread was confirmed!!! + //assume "isHealthy()" to avoid hitting server connection limits: (clean up of !isHealthy() after use; idle sessions via worker thread) + sharedSession = session; + + if (!sharedSession) + //assume "isHealthy()" to avoid hitting server connection limits: (clean up of !isHealthy() after use; idle sessions via worker thread) + if (!cache.idleSshSessions.empty()) + { + std::unique_ptr sshSession(cache.idleSshSessions.back().release()); + /**/ cache.idleSshSessions.pop_back(); + sharedSessionWeak = sharedSession = std::make_shared(std::move(sshSession), login.timeoutSec); //still holding lock => constructor must be *fast*! + } + if (!sharedSession) + sessionCfg = *cache.activeCfg; + }); + + //create new SFTP session outside the lock: 1. don't block other threads 2. non-atomic regarding "sessionCache"! => one session too many is not a problem! + if (!sharedSession) + { + sharedSession = std::make_shared(std::unique_ptr(new SshSession(*sessionCfg, login.timeoutSec)), login.timeoutSec); //throw SysError, SysErrorPassword + + sessionCache.access([&](SshSessionCache& cache) + { + if (sharedSession->getSessionCfg() == *cache.activeCfg) //created outside the lock => check *again* + cache.sshSessionsWithThreadAffinity[threadId] = sharedSession; + }); + } + + //finish two-step initialization outside the lock: BLOCKING! + sharedSession->initSftpChannel(); //throw SysError + + return sharedSession; + } + + + std::unique_ptr getExclusiveSession(const SftpLogin& login) //throw SysError + { + std::unique_ptr sshSession; //either or + std::optional sessionCfg; // + + getSessionCache(login).access([&](SshSessionCache& cache) + { + if (!cache.activeCfg) //AFS::authenticateAccess() not called => authenticate implicitly! + setActiveConfig(cache, login); + + //assume "isHealthy()" to avoid hitting server connection limits: (clean up of !isHealthy() after use, idle sessions via worker thread) + if (!cache.idleSshSessions.empty()) + { + sshSession.reset(cache.idleSshSessions.back().release()); + /**/ cache.idleSshSessions.pop_back(); + } + else + sessionCfg = *cache.activeCfg; + }); + + //create new SFTP session outside the lock: 1. don't block other threads 2. non-atomic regarding "sessionCache"! => one session too many is not a problem! + if (!sshSession) + sshSession.reset(new SshSession(*sessionCfg, login.timeoutSec)); //throw SysError, SysErrorPassword + + return std::make_unique(std::move(sshSession), login.timeoutSec); + } + + void setActiveConfig(const SftpLogin& login) + { + getSessionCache(login).access([&](SshSessionCache& cache) { setActiveConfig(cache, login); }); + } + + void setSessionPassword(const SftpLogin& login, const Zstring& password, SftpAuthType authType) + { + getSessionCache(login).access([&](SshSessionCache& cache) + { + (authType == SftpAuthType::password ? cache.sessionPassword : cache.sessionPassphrase) = password; + setActiveConfig(cache, login); + }); + } + +private: + SftpSessionManager (const SftpSessionManager&) = delete; + SftpSessionManager& operator=(const SftpSessionManager&) = delete; + + Protected& getSessionCache(const SshDeviceId& deviceId) + { + //single global session store per login; life-time bound to globalInstance => never remove a sessionCache!!! + Protected* sessionCache = nullptr; + + globalSessionCache_.access([&](GlobalSshSessions& sessionsById) + { + sessionCache = &sessionsById[deviceId]; //get or create + }); + static_assert(std::is_same_v>>, "require std::map so that the pointers we return remain stable"); + + return *sessionCache; + } + + void setActiveConfig(SshSessionCache& cache, const SftpLogin& login) + { + const Zstring password = [&] + { + if (login.authType == SftpAuthType::password || + login.authType == SftpAuthType::keyFile) + { + if (login.password) + return *login.password; + + return login.authType == SftpAuthType::password ? cache.sessionPassword : cache.sessionPassphrase; + } + return Zstring(); + }(); + + if (cache.activeCfg) + { + assert(std::all_of(cache.idleSshSessions.begin(), cache.idleSshSessions.end(), + [&](const std::unique_ptr& session) { return session->getSessionCfg() == cache.activeCfg; })); + + assert(std::all_of(cache.sshSessionsWithThreadAffinity.begin(), cache.sshSessionsWithThreadAffinity.end(), [&](const auto& v) + { + if (std::shared_ptr sharedSession = v.second.lock()) + return sharedSession->getSessionCfg() /*thread-safe!*/ == cache.activeCfg; + return true; + })); + } + else + assert(cache.idleSshSessions.empty() && cache.sshSessionsWithThreadAffinity.empty()); + + const std::optional prevCfg = cache.activeCfg; + + cache.activeCfg = + { + .deviceId{login}, + .authType = login.authType, + .password = password, + .privateKeyFilePath = login.privateKeyFilePath, + .allowZlib = login.allowZlib, + }; + + /* remove incompatible sessions: + - avoid hitting FTP connection limit if some config uses TLS, but not the other: https://freefilesync.org/forum/viewtopic.php?t=8532 + - logically consistent with AFS::compareDevice() + - don't allow different authentication methods, when authenticateAccess() is called *once* per device in getFolderStatusParallel() + - what user expects, e.g. when tesing changed settings in SFTP login dialog */ + if (cache.activeCfg != prevCfg) + { + cache.idleSshSessions .clear(); //run ~SshSession *inside* the lock! => avoid hitting server limits! + cache.sshSessionsWithThreadAffinity.clear(); // + //=> incompatible sessions will be deleted by ReUseOnDelete(); until then: additionally counts towards SFTP connection limit :( + } + } + + //run a dedicated clean-up thread => it's unclear when the server let's a connection time out, so we do it preemptively + //context of worker thread: + void runGlobalSessionCleanUp() //throw ThreadStopRequest + { + std::chrono::steady_clock::time_point lastCleanupTime; + for (;;) + { + const auto now = std::chrono::steady_clock::now(); + + if (now < lastCleanupTime + SFTP_SESSION_CLEANUP_INTERVAL) + interruptibleSleep(lastCleanupTime + SFTP_SESSION_CLEANUP_INTERVAL - now); //throw ThreadStopRequest + + lastCleanupTime = std::chrono::steady_clock::now(); + + std::vector*> sessionCaches; //pointers remain stable, thanks to std::map<> + + globalSessionCache_.access([&](GlobalSshSessions& sessionsById) + { + for (auto& [sessionId, idleSession] : sessionsById) + sessionCaches.push_back(&idleSession); + }); + for (Protected* sessionCache : sessionCaches) + for (;;) + { + bool done = false; + sessionCache->access([&](SshSessionCache& cache) + { + for (std::unique_ptr& sshSession : cache.idleSshSessions) + if (!sshSession->isHealthy()) //!isHealthy() sessions are destroyed after use => in this context this means they have been idle for too long + { + sshSession.swap(cache.idleSshSessions.back()); + /**/ cache.idleSshSessions.pop_back(); //run ~SshSession *inside* the lock! => avoid hitting server limits! + return; //don't hold lock for too long: delete only one session at a time, then yield... + } + std::erase_if(cache.sshSessionsWithThreadAffinity, [](const auto& v) { return v.second.expired(); }); //clean up dangling weak pointer + done = true; + }); + if (done) + break; + std::this_thread::yield(); //outside the lock + } + } + } + + struct SshSessionCache + { + //invariant: all cached sessions correspond to activeCfg at any time! + std::vector> idleSshSessions; //extract *temporarily* from this list during use + std::unordered_map> sshSessionsWithThreadAffinity; //Win32 thread IDs may be REUSED! still, shouldn't be a problem... + + std::optional activeCfg; + + Zstring sessionPassword; //user/password + Zstring sessionPassphrase; //keyfile/passphrase + }; + + using GlobalSshSessions = std::map>; + Protected globalSessionCache_; + + InterruptibleThread sessionCleaner_; +}; + +//-------------------------------------------------------------------------------------- +UniInitializer globalInitSftp(*globalSftpSessionCount.get()); + +constinit Global globalSftpSessionManager; //caveat: life time must be subset of static UniInitializer! +//-------------------------------------------------------------------------------------- + + +void SftpSessionManager::ReUseOnDelete::operator()(SshSession* session) const +{ + //assert(session); -> custom deleter is only called on non-null pointer + if (session->isHealthy()) //thread that created the "!isHealthy()" session is responsible for clean up (avoid hitting server connection limits!) + if (std::shared_ptr mgr = globalSftpSessionManager.get()) + mgr->getSessionCache(session->getSessionCfg().deviceId).access([&](SshSessionCache& cache) + { + assert(cache.activeCfg); + if (cache.activeCfg && session->getSessionCfg() == *cache.activeCfg) + cache.idleSshSessions.emplace_back(std::exchange(session, nullptr)); //pass ownership + }); + delete session; +} + + +std::shared_ptr getSharedSftpSession(const SftpLogin& login) //throw SysError +{ + if (const std::shared_ptr mgr = globalSftpSessionManager.get()) + return mgr->getSharedSession(login); //throw SysError, SysErrorPassword + + throw SysError(formatSystemError("getSharedSftpSession", L"", L"Function call not allowed during init/shutdown.")); +} + + +std::unique_ptr getExclusiveSftpSession(const SftpLogin& login) //throw SysError +{ + if (const std::shared_ptr mgr = globalSftpSessionManager.get()) + return mgr->getExclusiveSession(login); //throw SysError + + throw SysError(formatSystemError("getExclusiveSftpSession", L"", L"Function call not allowed during init/shutdown.")); +} + + +void runSftpCommand(const SftpLogin& login, const char* functionName, + const std::function& sftpCommand /*noexcept!*/) //throw SysError, SysErrorSftpProtocol +{ + std::shared_ptr asyncSession = getSharedSftpSession(login); //throw SysError + //no need to protect against concurrency: shared session is (temporarily) bound to current thread + + asyncSession->executeBlocking(functionName, sftpCommand); //throw SysError, SysErrorSftpProtocol +} + +//=========================================================================================================================== +//=========================================================================================================================== +struct SftpItemDetails +{ + AFS::ItemType type; + uint64_t fileSize; + time_t modTime; +}; +struct SftpItem +{ + Zstring itemName; + SftpItemDetails details; +}; +std::vector getDirContentFlat(const SftpLogin& login, const AfsPath& dirPath) //throw FileError +{ + LIBSSH2_SFTP_HANDLE* dirHandle = nullptr; + try + { + runSftpCommand(login, "libssh2_sftp_opendir", //throw SysError, SysErrorSftpProtocol + [&](const SshSession::Details& sd) //noexcept! + { + dirHandle = ::libssh2_sftp_opendir(sd.sftpChannel, getLibssh2Path(dirPath)); + if (!dirHandle) + return std::min(::libssh2_session_last_errno(sd.sshSession), LIBSSH2_ERROR_SOCKET_NONE); + return LIBSSH2_ERROR_NONE; + }); + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot open directory %x."), L"%x", fmtPath(getSftpDisplayPath(login, dirPath))), e.toString()); } + + ZEN_ON_SCOPE_EXIT(try + { + runSftpCommand(login, "libssh2_sftp_closedir", //throw SysError, SysErrorSftpProtocol + [&](const SshSession::Details& sd) { return ::libssh2_sftp_closedir(dirHandle); }); //noexcept! + } + catch (const SysError& e) { logExtraError(replaceCpy(_("Cannot read directory %x."), L"%x", fmtPath(getSftpDisplayPath(login, dirPath))) + L"\n\n" + e.toString()); }); + + std::vector output; + for (;;) + { + std::array buf; //libssh2 sample code uses 512; in practice NAME_MAX(255)+1 should suffice: https://serverfault.com/questions/9546/filename-length-limits-on-linux + LIBSSH2_SFTP_ATTRIBUTES attribs = {}; + int rc = 0; + try + { + runSftpCommand(login, "libssh2_sftp_readdir", //throw SysError, SysErrorSftpProtocol + [&](const SshSession::Details& sd) { return rc = ::libssh2_sftp_readdir(dirHandle, buf.data(), buf.size(), &attribs); }); //noexcept! + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read directory %x."), L"%x", fmtPath(getSftpDisplayPath(login, dirPath))), e.toString()); } + + if (rc == 0) //no more items + return output; + + const std::string_view sftpItemName = makeStringView(buf.data(), rc); + + if (sftpItemName == "." || sftpItemName == "..") //check needed for SFTP, too! + continue; + + const Zstring& itemName = utfTo(sftpItemName); + const AfsPath itemPath(appendPath(dirPath.value, itemName)); + + if ((attribs.flags & LIBSSH2_SFTP_ATTR_PERMISSIONS) == 0) //server probably does not support these attributes => fail at folder level + throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(getSftpDisplayPath(login, itemPath))), L"File attributes not available."); + + if (LIBSSH2_SFTP_S_ISLNK(attribs.permissions)) + { + if ((attribs.flags & LIBSSH2_SFTP_ATTR_ACMODTIME) == 0) //server probably does not support these attributes => fail at folder level + throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(getSftpDisplayPath(login, itemPath))), L"Modification time not supported."); + output.push_back({itemName, {AFS::ItemType::symlink, 0, static_cast(attribs.mtime)}}); + } + else if (LIBSSH2_SFTP_S_ISDIR(attribs.permissions)) + output.push_back({itemName, {AFS::ItemType::folder, 0, static_cast(attribs.mtime)}}); + else //a file or named pipe, ect: LIBSSH2_SFTP_S_ISREG, LIBSSH2_SFTP_S_ISCHR, LIBSSH2_SFTP_S_ISBLK, LIBSSH2_SFTP_S_ISFIFO, LIBSSH2_SFTP_S_ISSOCK + { + if ((attribs.flags & LIBSSH2_SFTP_ATTR_ACMODTIME) == 0) //server probably does not support these attributes => fail at folder level + throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(getSftpDisplayPath(login, itemPath))), L"Modification time not supported."); + if ((attribs.flags & LIBSSH2_SFTP_ATTR_SIZE) == 0) + throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(getSftpDisplayPath(login, itemPath))), L"File size not supported."); + output.push_back({itemName, {AFS::ItemType::file, attribs.filesize, static_cast(attribs.mtime)}}); + } + } +} + + +SftpItemDetails getSymlinkTargetDetails(const SftpLogin& login, const AfsPath& linkPath) //throw FileError +{ + LIBSSH2_SFTP_ATTRIBUTES attribsTrg = {}; + try + { + runSftpCommand(login, "libssh2_sftp_stat", //throw SysError, SysErrorSftpProtocol + [&](const SshSession::Details& sd) { return ::libssh2_sftp_stat(sd.sftpChannel, getLibssh2Path(linkPath), &attribsTrg); }); //noexcept! + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot resolve symbolic link %x."), L"%x", fmtPath(getSftpDisplayPath(login, linkPath))), e.toString()); } + + if ((attribsTrg.flags & LIBSSH2_SFTP_ATTR_PERMISSIONS) == 0) + throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(getSftpDisplayPath(login, linkPath))), L"File attributes not available."); + + if (LIBSSH2_SFTP_S_ISDIR(attribsTrg.permissions)) + return {AFS::ItemType::folder, 0, static_cast(attribsTrg.mtime)}; + else + { + if ((attribsTrg.flags & LIBSSH2_SFTP_ATTR_ACMODTIME) == 0) //server probably does not support these attributes => should fail at folder level! + throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(getSftpDisplayPath(login, linkPath))), L"Modification time not supported."); + if ((attribsTrg.flags & LIBSSH2_SFTP_ATTR_SIZE) == 0) + throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(getSftpDisplayPath(login, linkPath))), L"File size not supported."); + + return {AFS::ItemType::file, attribsTrg.filesize, static_cast(attribsTrg.mtime)}; + } +} + + +class SingleFolderTraverser +{ +public: + using WorkItem = std::pair>; + + SingleFolderTraverser(const SftpLogin& login, const std::vector>>& workload /*throw X*/) : + login_(login) + { + for (const auto& [folderPath, cb] : workload) + workload_.push_back(WorkItem{folderPath, cb}); + + while (!workload_.empty()) + { + auto wi = std::move(workload_. front()); //yes, no strong exception guarantee (std::bad_alloc) + /**/ workload_.pop_front(); // + const auto& [folderPath, cb] = wi; + + tryReportingDirError([&] //throw X + { + traverseWithException(folderPath, *cb); //throw FileError, X + }, *cb); + } + } + +private: + SingleFolderTraverser (const SingleFolderTraverser&) = delete; + SingleFolderTraverser& operator=(const SingleFolderTraverser&) = delete; + + void traverseWithException(const AfsPath& dirPath, AFS::TraverserCallback& cb) //throw FileError, X + { + for (const SftpItem& item : getDirContentFlat(login_, dirPath)) //throw FileError + { + const AfsPath itemPath(appendPath(dirPath.value, item.itemName)); + + switch (item.details.type) + { + case AFS::ItemType::file: + cb.onFile({item.itemName, item.details.fileSize, item.details.modTime, AFS::FingerPrint() /*not supported by SFTP*/, false /*isFollowedSymlink*/}); //throw X + break; + + case AFS::ItemType::folder: + if (std::shared_ptr cbSub = cb.onFolder({item.itemName, false /*isFollowedSymlink*/})) //throw X + workload_.push_back(WorkItem{itemPath, std::move(cbSub)}); + break; + + case AFS::ItemType::symlink: + switch (cb.onSymlink({item.itemName, item.details.modTime})) //throw X + { + case AFS::TraverserCallback::HandleLink::follow: + { + SftpItemDetails targetDetails = {}; + if (!tryReportingItemError([&] //throw X + { + targetDetails = getSymlinkTargetDetails(login_, itemPath); //throw FileError + }, cb, item.itemName)) + continue; + + if (targetDetails.type == AFS::ItemType::folder) + { + if (std::shared_ptr cbSub = cb.onFolder({item.itemName, true /*isFollowedSymlink*/})) //throw X + workload_.push_back(WorkItem{itemPath, std::move(cbSub)}); + } + else //a file or named pipe, etc. + cb.onFile({item.itemName, targetDetails.fileSize, targetDetails.modTime, AFS::FingerPrint() /*not supported by SFTP*/, true /*isFollowedSymlink*/}); //throw X + } + break; + + case AFS::TraverserCallback::HandleLink::skip: + break; + } + break; + } + } + } + + const SftpLogin login_; + RingBuffer workload_; +}; + + +void traverseFolderRecursiveSftp(const SftpLogin& login, const std::vector>>& workload /*throw X*/, size_t) //throw X +{ + SingleFolderTraverser dummy(login, workload); //throw X +} + +//=========================================================================================================================== + +struct InputStreamSftp : public AFS::InputStream +{ + InputStreamSftp(const SftpLogin& login, const AfsPath& filePath) : //throw FileError + displayPath_(getSftpDisplayPath(login, filePath)) + { + try + { + session_ = getSharedSftpSession(login); //throw SysError + + session_->executeBlocking("libssh2_sftp_open", //throw SysError, SysErrorSftpProtocol + [&](const SshSession::Details& sd) //noexcept! + { + fileHandle_ = ::libssh2_sftp_open(sd.sftpChannel, getLibssh2Path(filePath), LIBSSH2_FXF_READ, 0); + if (!fileHandle_) + return std::min(::libssh2_session_last_errno(sd.sshSession), LIBSSH2_ERROR_SOCKET_NONE); + return LIBSSH2_ERROR_NONE; + }); + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot open file %x."), L"%x", fmtPath(displayPath_)), e.toString()); } + } + + ~InputStreamSftp() + { + try + { + session_->executeBlocking("libssh2_sftp_close", //throw SysError, SysErrorSftpProtocol + [&](const SshSession::Details& sd) { return ::libssh2_sftp_close(fileHandle_); }); //noexcept! + } + catch (const SysError& e) { logExtraError(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(displayPath_)) + L"\n\n" + e.toString()); } + } + + size_t getBlockSize() override { return SFTP_OPTIMAL_BLOCK_SIZE_READ; } //throw (FileError); non-zero block size is AFS contract! + + //may return short; only 0 means EOF! CONTRACT: bytesToRead > 0! + size_t tryRead(void* buffer, size_t bytesToRead, const IoCallback& notifyUnbufferedIO /*throw X*/) override //throw FileError, (ErrorFileLocked), X + { + //libssh2_sftp_read has same semantics as Posix read: + if (bytesToRead == 0) //"read() with a count of 0 returns zero" => indistinguishable from end of file! => check! + throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); + assert(bytesToRead % getBlockSize() == 0); + + ssize_t bytesRead = 0; + try + { + session_->executeBlocking("libssh2_sftp_read", //throw SysError, SysErrorSftpProtocol + [&](const SshSession::Details& sd) //noexcept! + { + bytesRead = ::libssh2_sftp_read(fileHandle_, static_cast(buffer), bytesToRead); + return static_cast(bytesRead); + }); + + ASSERT_SYSERROR(makeUnsigned(bytesRead) <= bytesToRead); //better safe than sorry (user should never see this) + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(displayPath_)), e.toString()); } + + if (notifyUnbufferedIO) notifyUnbufferedIO(bytesRead); //throw X + return bytesRead; //"zero indicates end of file" + } + + std::optional tryGetAttributesFast() override { return {}; }//throw FileError + //although we have an SFTP stream handle, attribute access requires an extra (expensive) round-trip! + //PERF: test case 148 files, 1MB: overall copy time increases by 20% if libssh2_sftp_fstat() gets called per each file + +private: + const std::wstring displayPath_; + LIBSSH2_SFTP_HANDLE* fileHandle_ = nullptr; + std::shared_ptr session_; +}; + +//=========================================================================================================================== + +//libssh2_sftp_open fails with generic LIBSSH2_FX_FAILURE if already existing +struct OutputStreamSftp : public AFS::OutputStreamImpl +{ + OutputStreamSftp(const SftpLogin& login, //throw FileError + const AfsPath& filePath, + std::optional modTime) : + login_(login), + filePath_(filePath), + modTime_(modTime) + { + try + { + session_ = getSharedSftpSession(login); //throw SysError + + session_->executeBlocking("libssh2_sftp_open", //throw SysError, SysErrorSftpProtocol + [&](const SshSession::Details& sd) //noexcept! + { + fileHandle_ = ::libssh2_sftp_open(sd.sftpChannel, getLibssh2Path(filePath), + LIBSSH2_FXF_WRITE | LIBSSH2_FXF_CREAT | LIBSSH2_FXF_EXCL, + 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; + }); + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(getSftpDisplayPath(login_, filePath_))), e.toString()); } + + //NOTE: fileHandle_ still unowned until end of constructor!!! + + //pre-allocate file space? not supported + } + + ~OutputStreamSftp() + { + if (fileHandle_) //=> cleanup non-finalized output file + { + if (!closeFailed_) //otherwise there's no much point in calling libssh2_sftp_close() a second time => let it leak!? + try { close(); /*throw FileError*/ } + catch (const FileError& e) { logExtraError(e.toString()); } + + session_.reset(); //reset before file deletion to potentially get new session if !SshSession::isHealthy() + + try //see removeFilePlain() + { + runSftpCommand(login_, "libssh2_sftp_unlink", //throw SysError, SysErrorSftpProtocol + [&](const SshSession::Details& sd) { return ::libssh2_sftp_unlink(sd.sftpChannel, getLibssh2Path(filePath_)); }); //noexcept! + } + catch (const SysError& e) + { + logExtraError(replaceCpy(_("Cannot delete file %x."), L"%x", fmtPath(getSftpDisplayPath(login_, filePath_))) + L"\n\n" + e.toString()); + } + } + } + + size_t getBlockSize() override { return SFTP_OPTIMAL_BLOCK_SIZE_WRITE; } //throw (FileError) + + size_t tryWrite(const void* buffer, size_t bytesToWrite, const IoCallback& notifyUnbufferedIO /*throw X*/) override //throw FileError, X; may return short! CONTRACT: bytesToWrite > 0 + { + if (bytesToWrite == 0) + throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); + assert(bytesToWrite % getBlockSize() == 0 || bytesToWrite < getBlockSize()); + + ssize_t bytesWritten = 0; + try + { + session_->executeBlocking("libssh2_sftp_write", //throw SysError, SysErrorSftpProtocol + [&](const SshSession::Details& sd) //noexcept! + { + bytesWritten = ::libssh2_sftp_write(fileHandle_, static_cast(buffer), bytesToWrite); + /* "If this function returns zero it should not be considered an error, but simply that there was no error but yet no payload data got sent to the other end." + => sounds like BS, but is it really true!? + From the libssh2_sftp_write code it appears that the function always waits for at least one "ack", unless we give it so much data _libssh2_channel_write() can't sent it all! */ + assert(bytesWritten != 0); + return static_cast(bytesWritten); + }); + + ASSERT_SYSERROR(makeUnsigned(bytesWritten) <= bytesToWrite); //better safe than sorry + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(getSftpDisplayPath(login_, filePath_))), e.toString()); } + + if (notifyUnbufferedIO) notifyUnbufferedIO(bytesWritten); //throw X! + + return bytesWritten; + } + + AFS::FinalizeResult finalize(const IoCallback& notifyUnbufferedIO /*throw X*/) override //throw FileError, X + { + close(); //throw FileError + //output finalized => no more exceptions from here on! + //-------------------------------------------------------------------- + + AFS::FinalizeResult result; + //result.filePrint = ... -> not supported by SFTP + try + { + setModTimeIfAvailable(); //throw FileError, follows symlinks + /* is setting modtime after closing the file handle a pessimization? + SFTP: no, needed for functional correctness (synology server), same as for Native */ + } + catch (const FileError& e) { result.errorModTime = e; /*slicing?*/ } + + return result; + } + +private: + void close() //throw FileError + { + if (!fileHandle_) + throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); + + try + { + session_->executeBlocking("libssh2_sftp_close", //throw SysError, SysErrorSftpProtocol + [&](const SshSession::Details& sd) { return ::libssh2_sftp_close(fileHandle_); }); //noexcept! + + fileHandle_ = nullptr; + } + catch (const SysError& e) + { + closeFailed_ = true; + throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(getSftpDisplayPath(login_, filePath_))), e.toString()); + } + } + + void setModTimeIfAvailable() const //throw FileError, follows symlinks + { + assert(!fileHandle_); + if (modTime_) + { + LIBSSH2_SFTP_ATTRIBUTES attribNew = {}; + attribNew.flags = LIBSSH2_SFTP_ATTR_ACMODTIME; + attribNew.mtime = static_cast(*modTime_); //32-bit target! loss of data! + attribNew.atime = static_cast(::time(nullptr)); // + + //it seems libssh2_sftp_fsetstat() triggers bugs on synology server => set mtime by path! https://freefilesync.org/forum/viewtopic.php?t=1281 + try + { + session_->executeBlocking("libssh2_sftp_setstat", //throw SysError, SysErrorSftpProtocol + [&](const SshSession::Details& sd) { return ::libssh2_sftp_setstat(sd.sftpChannel, getLibssh2Path(filePath_), &attribNew); }); //noexcept! + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot write modification time of %x."), L"%x", fmtPath(getSftpDisplayPath(login_, filePath_))), e.toString()); } + } + } + + const SftpLogin login_; + const AfsPath filePath_; + const std::optional modTime_; + LIBSSH2_SFTP_HANDLE* fileHandle_ = nullptr; + bool closeFailed_ = false; + std::shared_ptr session_; +}; + +//=========================================================================================================================== + +class SftpFileSystem : public AbstractFileSystem +{ +public: + explicit SftpFileSystem(const SftpLogin& login) : login_(login) {} + + const SftpLogin& getLogin() const { return login_; } + + AfsPath getHomePath() const //throw FileError + { + try + { + //we never ever change the SFTP working directory, right? ...right? + return getServerRealPath("."); //throw SysError + //use "~" instead? NO: libssh2_sftp_realpath() fails with LIBSSH2_FX_NO_SUCH_FILE + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot determine final path for %x."), L"%x", fmtPath(getDisplayPath(AfsPath(Zstr("~"))))), e.toString()); } + } + +private: + Zstring getInitPathPhrase(const AfsPath& itemPath) const override { return concatenateSftpFolderPathPhrase(login_, itemPath); } + + std::vector getPathPhraseAliases(const AfsPath& itemPath) const override + { + std::vector pathAliases; + + if (login_.authType != SftpAuthType::keyFile || login_.privateKeyFilePath.empty()) + pathAliases.push_back(concatenateSftpFolderPathPhrase(login_, itemPath)); + else //why going crazy with key path aliases!? because we can... + for (const Zstring& pathPhrase : ::getPathPhraseAliases(login_.privateKeyFilePath)) + { + auto loginTmp = login_; + loginTmp.privateKeyFilePath = pathPhrase; + + pathAliases.push_back(concatenateSftpFolderPathPhrase(loginTmp, itemPath)); + } + return pathAliases; + } + + std::wstring getDisplayPath(const AfsPath& itemPath) const override { return getSftpDisplayPath(login_, itemPath); } + + bool isNullFileSystem() const override { return login_.server.empty(); } + + std::weak_ordering compareDeviceSameAfsType(const AbstractFileSystem& afsRhs) const override + { + const SftpLogin& lhs = login_; + const SftpLogin& rhs = static_cast(afsRhs).login_; + + return SshDeviceId(lhs) <=> SshDeviceId(rhs); + } + + //---------------------------------------------------------------------------------------------------------------- + ItemType getItemTypeImpl(const AfsPath& itemPath) const //throw SysError, SysErrorSftpProtocol + { + LIBSSH2_SFTP_ATTRIBUTES attr = {}; + runSftpCommand(login_, "libssh2_sftp_lstat", //throw SysError, SysErrorSftpProtocol + [&](const SshSession::Details& sd) { return ::libssh2_sftp_lstat(sd.sftpChannel, getLibssh2Path(itemPath), &attr); }); //noexcept! + + if ((attr.flags & LIBSSH2_SFTP_ATTR_PERMISSIONS) == 0) + throw SysError(formatSystemError("libssh2_sftp_lstat", L"", L"File attributes not available.")); + + if (LIBSSH2_SFTP_S_ISLNK(attr.permissions)) + return ItemType::symlink; + if (LIBSSH2_SFTP_S_ISDIR(attr.permissions)) + return ItemType::folder; + return ItemType::file; //LIBSSH2_SFTP_S_ISREG || LIBSSH2_SFTP_S_ISCHR || LIBSSH2_SFTP_S_ISBLK || LIBSSH2_SFTP_S_ISFIFO || LIBSSH2_SFTP_S_ISSOCK + } + + ItemType getItemType(const AfsPath& itemPath) const override //throw FileError + { + try + { + return getItemTypeImpl(itemPath); //throw SysError, SysErrorSftpProtocol + } + catch (const SysError& e) + { + throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(getDisplayPath(itemPath))), e.toString()); + } + } + + std::optional getItemTypeIfExists(const AfsPath& itemPath) const override //throw FileError + { + try + { + try + { + //fast check: 1. perf 2. expected by getFolderStatusNonBlocking() 3. traversing non-existing folder below MIGHT NOT FAIL (e.g. for SFTP on AWS) + return getItemTypeImpl(itemPath); //throw SysError, SysErrorSftpProtocol + } + catch (const SysErrorSftpProtocol& e) + { + const std::optional parentPath = getParentPath(itemPath); + if (!parentPath) //device root => quick access test + throw; + //let's dig deeper, but *only* for SysErrorSftpProtocol, not for general connection issues + //+ check if SFTP error code sounds like "not existing" + if (e.sftpErrorCode == LIBSSH2_FX_NO_SUCH_FILE || + e.sftpErrorCode == LIBSSH2_FX_NO_SUCH_PATH) //-> not seen yet, but sounds reasonable + { + if (const std::optional parentType = getItemTypeIfExists(*parentPath)) //throw FileError + { + if (*parentType == ItemType::file /*obscure, but possible*/) + throw SysError(replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(getItemName(*parentPath)))); + + const Zstring itemName = getItemName(itemPath); + assert(!itemName.empty()); + + traverseFolder(*parentPath, //throw FileError + [&](const FileInfo& fi) { if (fi.itemName == itemName) throw SysError(_("Temporary access error:") + L' ' + e.toString()); }, + [&](const FolderInfo& fi) { if (fi.itemName == itemName) throw SysError(_("Temporary access error:") + L' ' + e.toString()); }, + [&](const SymlinkInfo& si) { if (si.itemName == itemName) throw SysError(_("Temporary access error:") + L' ' + e.toString()); }); + //- case-sensitive comparison! itemPath must be normalized! + //- finding the item after getItemType() previously failed is exceptional + } + return std::nullopt; + } + else + throw; + } + } + catch (const SysError& e) + { + throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(getDisplayPath(itemPath))), e.toString()); + } + } + + //---------------------------------------------------------------------------------------------------------------- + //already existing: fail + void createFolderPlain(const AfsPath& folderPath) const override //throw FileError + { + try + { + //fails with obscure LIBSSH2_FX_FAILURE if already existing + runSftpCommand(login_, "libssh2_sftp_mkdir", //throw SysError, SysErrorSftpProtocol + [&](const SshSession::Details& sd) //noexcept! + { + return ::libssh2_sftp_mkdir(sd.sftpChannel, getLibssh2Path(folderPath), SFTP_DEFAULT_PERMISSION_FOLDER); + //less explicit variant: return ::libssh2_sftp_mkdir(sd.sftpChannel, getLibssh2Path(folderPath), LIBSSH2_SFTP_DEFAULT_MODE); + }); + } + catch (const SysError& e) //libssh2_sftp_mkdir reports generic LIBSSH2_FX_FAILURE if existing + { + throw FileError(replaceCpy(_("Cannot create directory %x."), L"%x", fmtPath(getDisplayPath(folderPath))), e.toString()); + } + } + + void removeFilePlain(const AfsPath& filePath) const override //throw FileError + { + try + { + runSftpCommand(login_, "libssh2_sftp_unlink", //throw SysError, SysErrorSftpProtocol + [&](const SshSession::Details& sd) { return ::libssh2_sftp_unlink(sd.sftpChannel, getLibssh2Path(filePath)); }); //noexcept! + } + catch (const SysError& e) + { + throw FileError(replaceCpy(_("Cannot delete file %x."), L"%x", fmtPath(getDisplayPath(filePath))), e.toString()); + } + } + + void removeSymlinkPlain(const AfsPath& linkPath) const override //throw FileError + { + try + { + runSftpCommand(login_, "libssh2_sftp_unlink", //throw SysError, SysErrorSftpProtocol + [&](const SshSession::Details& sd) { return ::libssh2_sftp_unlink(sd.sftpChannel, getLibssh2Path(linkPath)); }); //noexcept! + } + catch (const SysError& e) + { + throw FileError(replaceCpy(_("Cannot delete symbolic link %x."), L"%x", fmtPath(getDisplayPath(linkPath))), e.toString()); + } + } + + void removeFolderPlain(const AfsPath& folderPath) const override //throw FileError + { + try + { + //libssh2_sftp_rmdir fails for symlinks! (LIBSSH2_ERROR_SFTP_PROTOCOL: LIBSSH2_FX_NO_SUCH_FILE) + runSftpCommand(login_, "libssh2_sftp_rmdir", //throw SysError, SysErrorSftpProtocol + [&](const SshSession::Details& sd) { return ::libssh2_sftp_rmdir(sd.sftpChannel, getLibssh2Path(folderPath)); }); //noexcept! + } + catch (const SysError& e) + { + throw FileError(replaceCpy(_("Cannot delete directory %x."), L"%x", fmtPath(getDisplayPath(folderPath))), e.toString()); + } + } + + void removeFolderIfExistsRecursion(const AfsPath& folderPath, //throw FileError + const std::function& onBeforeFileDeletion /*throw X*/, + const std::function& onBeforeSymlinkDeletion/*throw X*/, + const std::function& onBeforeFolderDeletion /*throw X*/) const override + { + //default implementation: folder traversal + AFS::removeFolderIfExistsRecursion(folderPath, onBeforeFileDeletion, onBeforeSymlinkDeletion, onBeforeFolderDeletion); //throw FileError, X + } + + //---------------------------------------------------------------------------------------------------------------- + AfsPath getServerRealPath(const std::string& sftpPath) const //throw SysError + { + const size_t bufSize = 10000; + std::vector buf(bufSize + 1); //ensure buffer is always null-terminated since we don't evaluate the byte count returned by libssh2_sftp_realpath()! + + int rc = 0; + runSftpCommand(login_, "libssh2_sftp_realpath", //throw SysError, SysErrorSftpProtocol + [&](const SshSession::Details& sd) { return rc = ::libssh2_sftp_realpath(sd.sftpChannel, sftpPath, buf.data(), bufSize); }); //noexcept! + + const std::string_view sftpPathTrg = makeStringView(buf.data(), rc); + if (!startsWith(sftpPathTrg, '/')) + throw SysError(replaceCpy(L"Invalid path %x.", L"%x", fmtPath(utfTo(sftpPathTrg)))); + + return sanitizeDeviceRelativePath(utfTo(sftpPathTrg)); //code-reuse! but the sanitize part isn't really needed here... + } + + AbstractPath getSymlinkResolvedPath(const AfsPath& linkPath) const override //throw FileError + { + try + { + const AfsPath linkPathTrg = getServerRealPath(getLibssh2Path(linkPath)); //throw SysError + return AbstractPath(makeSharedRef(login_), linkPathTrg); + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot determine final path for %x."), L"%x", fmtPath(getDisplayPath(linkPath))), e.toString()); } + } + + static std::string getSymlinkContentImpl(const SftpFileSystem& sftpFs, const AfsPath& linkPath) //throw SysError + { + std::string buf(10000, '\0'); + int rc = 0; + + runSftpCommand(sftpFs.login_, "libssh2_sftp_readlink", //throw SysError, SysErrorSftpProtocol + [&](const SshSession::Details& sd) { return rc = ::libssh2_sftp_readlink(sd.sftpChannel, getLibssh2Path(linkPath), buf.data(), buf.size()); }); //noexcept! + + ASSERT_SYSERROR(makeUnsigned(rc) <= buf.size()); //better safe than sorry + + buf.resize(rc); + return buf; + } + + bool equalSymlinkContentForSameAfsType(const AfsPath& linkPathL, const AbstractPath& linkPathR) const override //throw FileError + { + auto getLinkContent = [](const SftpFileSystem& sftpFs, const AfsPath& linkPath) + { + try + { + return getSymlinkContentImpl(sftpFs, linkPath); //throw SysError + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot resolve symbolic link %x."), L"%x", fmtPath(sftpFs.getDisplayPath(linkPath))), e.toString()); } + }; + return getLinkContent(*this, linkPathL) == getLinkContent(static_cast(linkPathR.afsDevice.ref()), linkPathR.afsPath); //throw FileError + } + //---------------------------------------------------------------------------------------------------------------- + + //return value always bound: + std::unique_ptr getInputStream(const AfsPath& filePath) const override //throw FileError, (ErrorFileLocked) + { + return std::make_unique(login_, filePath); //throw FileError + } + + //already existing: undefined behavior! (e.g. fail/overwrite/auto-rename) + //=> actual behavior: fail with obscure LIBSSH2_FX_FAILURE error + std::unique_ptr getOutputStream(const AfsPath& filePath, //throw FileError + std::optional streamSize, + std::optional modTime) const override + { + return std::make_unique(login_, filePath, modTime); //throw FileError + } + + //---------------------------------------------------------------------------------------------------------------- + void traverseFolderRecursive(const TraverserWorkload& workload /*throw X*/, size_t parallelOps) const override + { + traverseFolderRecursiveSftp(login_, workload /*throw X*/, parallelOps); //throw X + } + //---------------------------------------------------------------------------------------------------------------- + + //symlink handling: follow + //already existing: undefined behavior! (e.g. fail/overwrite/auto-rename) + FileCopyResult copyFileForSameAfsType(const AfsPath& sourcePath, const StreamAttributes& attrSource, //throw FileError, (ErrorFileLocked), X + const AbstractPath& targetPath, bool copyFilePermissions, const IoCallback& notifyUnbufferedIO /*throw X*/) const override + { + //no native SFTP file copy => use stream-based file copy: + if (copyFilePermissions) + throw FileError(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(AFS::getDisplayPath(targetPath))), _("Operation not supported by device.")); + + //already existing: undefined behavior! (e.g. fail/overwrite/auto-rename) + return copyFileAsStream(sourcePath, attrSource, targetPath, notifyUnbufferedIO); //throw FileError, (ErrorFileLocked), X + } + + //symlink handling: follow + //already existing: fail + void copyNewFolderForSameAfsType(const AfsPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions) const override //throw FileError + { + //already existing: fail + AFS::createFolderPlain(targetPath); //throw FileError + + if (copyFilePermissions) + throw FileError(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(AFS::getDisplayPath(targetPath))), _("Operation not supported by device.")); + } + + //already existing: fail (SSH_FX_FAILURE) + void copySymlinkForSameAfsType(const AfsPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions) const override //throw FileError + { + try + { + const std::string buf = getSymlinkContentImpl(*this, sourcePath); //throw SysError + + runSftpCommand(static_cast(targetPath.afsDevice.ref()).login_, "libssh2_sftp_symlink", //throw SysError, SysErrorSftpProtocol + [&](const SshSession::Details& sd) //noexcept! + { + return ::libssh2_sftp_symlink(sd.sftpChannel, getLibssh2Path(targetPath.afsPath), buf); + }); + } + catch (const SysError& e) + { + throw FileError(replaceCpy(replaceCpy(_("Cannot copy symbolic link %x to %y."), + L"%x", L'\n' + fmtPath(getDisplayPath(sourcePath))), + L"%y", L'\n' + fmtPath(AFS::getDisplayPath(targetPath))), e.toString()); + } + } + + //already existing: undefined behavior! (e.g. fail/overwrite) + //=> actual behavior: fail with obscure LIBSSH2_FX_FAILURE error + void moveAndRenameItemForSameAfsType(const AfsPath& pathFrom, const AbstractPath& pathTo) const override //throw FileError, ErrorMoveUnsupported + { + if (compareDeviceSameAfsType(pathTo.afsDevice.ref()) != std::weak_ordering::equivalent) + throw ErrorMoveUnsupported(generateMoveErrorMsg(pathFrom, pathTo), _("Operation not supported between different devices.")); + + try + { + runSftpCommand(login_, "libssh2_sftp_rename", //throw SysError, SysErrorSftpProtocol + [&](const SshSession::Details& sd) //noexcept! + { + /* LIBSSH2_SFTP_RENAME_NATIVE: "The server is free to do the rename operation in whatever way it chooses. Any other set flags are to be taken as hints to the server." No, thanks! + LIBSSH2_SFTP_RENAME_OVERWRITE: "No overwriting rename in [SFTP] v3/v4" https://www.greenend.org.uk/rjk/sftp/sftpversions.html + + Test: LIBSSH2_SFTP_RENAME_OVERWRITE is not honored on freefilesync.org, no matter if LIBSSH2_SFTP_RENAME_NATIVE is set or not + => makes sense since SFTP v3 does not honor the additional flags that libssh2 sends! + + "... the most widespread SFTP server implementation, the OpenSSH, will fail the SSH_FXP_RENAME request if the target file already exists" + => incidentally this is just the behavior we want! */ + const std::string sftpPathOld = getLibssh2Path(pathFrom); + const std::string sftpPathNew = getLibssh2Path(pathTo.afsPath); + + return ::libssh2_sftp_rename(sd.sftpChannel, sftpPathOld, sftpPathNew, LIBSSH2_SFTP_RENAME_ATOMIC); + }); + } + catch (const SysError& e) //libssh2_sftp_rename_ex reports generic LIBSSH2_FX_FAILURE if target is already existing! + { + throw FileError(generateMoveErrorMsg(pathFrom, pathTo), e.toString()); + } + } + + bool supportsPermissions(const AfsPath& folderPath) const override { return false; } //throw FileError + //wait until there is real demand for copying from and to SFTP with permissions => use stream-based file copy: + + //---------------------------------------------------------------------------------------------------------------- + FileIconHolder getFileIcon (const AfsPath& filePath, int pixelSize) const override { return {}; } //throw FileError; optional return value + ImageHolder getThumbnailImage(const AfsPath& filePath, int pixelSize) const override { return {}; } //throw FileError; optional return value + + void authenticateAccess(const RequestPasswordFun& requestPassword /*throw X*/) const override //throw FileError, X + { + try + { + const std::shared_ptr mgr = globalSftpSessionManager.get(); + if (!mgr) + throw SysError(formatSystemError("getSessionPassword", L"", L"Function call not allowed during init/shutdown.")); + + mgr->setActiveConfig(login_); + + if (login_.authType == SftpAuthType::password || + login_.authType == SftpAuthType::keyFile) + if (!login_.password) + { + try //1. test for connection error *before* bothering user to enter a password + { + /*auto session =*/ mgr->getSharedSession(login_); //throw SysError, SysErrorPassword + return; //got new SshSession (connected in constructor) or already connected session from cache + } + catch (const SysErrorPassword& e) + { + if (!requestPassword) + throw SysError(e.toString() + L'\n' + _("Password prompt not permitted by current settings.")); + } + + std::wstring lastErrorMsg; + for (;;) + { + //2. request (new) password + std::wstring msg = replaceCpy(_("Please enter your password to connect to %x."), L"%x", fmtPath(getDisplayPath(AfsPath()))); + if (lastErrorMsg.empty()) + msg += L"\n" + _("The password will only be remembered until FreeFileSync is closed."); + + const Zstring password = requestPassword(msg, lastErrorMsg); //throw X + mgr->setSessionPassword(login_, password, login_.authType); + + try //3. test access: + { + /*auto session =*/ mgr->getSharedSession(login_); //throw SysError, SysErrorPassword + return; + } + catch (const SysErrorPassword& e) { lastErrorMsg = e.toString(); } + } + } + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Unable to connect to %x."), L"%x", fmtPath(getDisplayPath(AfsPath()))), e.toString()); } + } + + bool hasNativeTransactionalCopy() const override { return false; } + //---------------------------------------------------------------------------------------------------------------- + + int64_t getFreeDiskSpace(const AfsPath& folderPath) const override //throw FileError, returns < 0 if not available + { + //statvfs is an SFTP v3 extension and not supported by all server implementations + //Mikrotik SFTP server fails with LIBSSH2_FX_OP_UNSUPPORTED and corrupts session so that next SFTP call will hang + //(Server sends a duplicate SSH_FX_OP_UNSUPPORTED response with seemingly corrupt body and fails to respond from now on) + //https://freefilesync.org/forum/viewtopic.php?t=618 + //Just discarding the current session is not enough in all cases, e.g. 1. Open SFTP file handle 2. statvfs fails 3. must close file handle + return -1; +#if 0 + const std::string sftpPath = "/"; //::libssh2_sftp_statvfs will fail if path is not yet existing, OTOH root path should work, too? + //NO, for correctness we must check free space for the given folder!! + + //"It is unspecified whether all members of the returned struct have meaningful values on all file systems." + LIBSSH2_SFTP_STATVFS fsStats = {}; + try + { + runSftpCommand(login_, "libssh2_sftp_statvfs", //throw SysError, SysErrorSftpProtocol + [&](const SshSession::Details& sd) { return ::libssh2_sftp_statvfs(sd.sftpChannel, sftpPath.c_str(), sftpPath.size(), &fsStats); }); //noexcept! + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot determine free disk space for %x."), L"%x", fmtPath(getDisplayPath(L"/"))), e.toString()); } + + static_assert(sizeof(fsStats.f_bsize) >= 8); + return fsStats.f_bsize * fsStats.f_bavail; +#endif + } + + std::unique_ptr createRecyclerSession(const AfsPath& folderPath) const override //throw FileError, RecycleBinUnavailable + { + throw RecycleBinUnavailable(replaceCpy(_("The recycle bin is not available for %x."), L"%x", fmtPath(getDisplayPath(folderPath)))); + } + + void moveToRecycleBin(const AfsPath& itemPath) const override //throw FileError, RecycleBinUnavailable + { + throw RecycleBinUnavailable(replaceCpy(_("The recycle bin is not available for %x."), L"%x", fmtPath(getDisplayPath(itemPath)))); + } + + const SftpLogin login_; +}; + +//=========================================================================================================================== + +//expects "clean" login data +Zstring concatenateSftpFolderPathPhrase(const SftpLogin& login, const AfsPath& folderPath) //noexcept +{ + Zstring username; + if (!login.username.empty()) + username = encodeFtpUsername(login.username) + Zstr("@"); + + Zstring server = login.server; + if (parseIpv6Address(server) && login.portCfg > 0) + server = Zstr('[') + server + Zstr(']'); //e.g. [::1]:80 + + Zstring port; + if (login.portCfg > 0) + port = Zstr(':') + numberTo(login.portCfg); + + Zstring relPath = getServerRelPath(folderPath); + if (relPath == Zstr("/")) + relPath.clear(); + + const SftpLogin loginDefault; + + Zstring options; + if (login.timeoutSec != loginDefault.timeoutSec) + options += Zstr("|timeout=") + numberTo(login.timeoutSec); + + if (login.traverserChannelsPerConnection != loginDefault.traverserChannelsPerConnection) + options += Zstr("|chan=") + numberTo(login.traverserChannelsPerConnection); + + if (login.allowZlib) + options += Zstr("|zlib"); + + switch (login.authType) + { + case SftpAuthType::password: + break; + + case SftpAuthType::keyFile: + options += Zstr("|keyfile=") + login.privateKeyFilePath; + break; + + case SftpAuthType::agent: + options += Zstr("|agent"); + break; + } + + if (login.authType != SftpAuthType::agent) + { + if (login.password) + { + if (!login.password->empty()) //password always last => visually truncated by folder input field + options += Zstr("|pass64=") + encodePasswordBase64(*login.password); + } + else + options += Zstr("|pwprompt"); + } + + return Zstring(sftpPrefix) + Zstr("//") + username + server + port + relPath + options; +} +} + + +void fff::sftpInit() +{ + assert(!globalSftpSessionManager.get()); + globalSftpSessionManager.set(std::make_unique()); +} + + +void fff::sftpTeardown() +{ + assert(globalSftpSessionManager.get()); + globalSftpSessionManager.set(nullptr); +} + + +AfsPath fff::getSftpHomePath(const SftpLogin& login) //throw FileError +{ + return SftpFileSystem(login).getHomePath(); //throw FileError +} + + +AfsDevice fff::condenseToSftpDevice(const SftpLogin& login) //noexcept +{ + //clean up input: + SftpLogin loginTmp = login; + trim(loginTmp.server); + trim(loginTmp.username); + trim(loginTmp.privateKeyFilePath); + + loginTmp.timeoutSec = std::max(1, loginTmp.timeoutSec); + loginTmp.traverserChannelsPerConnection = std::max(1, loginTmp.traverserChannelsPerConnection); + + if (startsWithAsciiNoCase(loginTmp.server, "http:" ) || + startsWithAsciiNoCase(loginTmp.server, "https:") || + startsWithAsciiNoCase(loginTmp.server, "ftp:" ) || + startsWithAsciiNoCase(loginTmp.server, "ftps:" ) || + startsWithAsciiNoCase(loginTmp.server, "sftp:" )) + loginTmp.server = afterFirst(loginTmp.server, Zstr(':'), IfNotFoundReturn::none); + trim(loginTmp.server, TrimSide::both, [](Zchar c) { return c == Zstr('/') || c == Zstr('\\'); }); + + if (std::optional> ip6AndPort = parseIpv6Address(loginTmp.server)) + loginTmp.server = ip6AndPort->first; //remove IPv6 leading/trailing brackets + + return makeSharedRef(loginTmp); +} + + +SftpLogin fff::extractSftpLogin(const AfsDevice& afsDevice) //noexcept +{ + if (const auto sftpDevice = dynamic_cast(&afsDevice.ref())) + return sftpDevice->getLogin(); + + assert(false); + return {}; +} + + +int fff::getServerMaxChannelsPerConnection(const SftpLogin& login) //throw FileError +{ + try + { + const auto timeoutTime = std::chrono::steady_clock::now() + SFTP_CHANNEL_LIMIT_DETECTION_TIME_OUT; + + std::unique_ptr exSession = getExclusiveSftpSession(login); //throw SysError + + ZEN_ON_SCOPE_EXIT(exSession->markAsCorrupted()); //after hitting the server limits, the session might have gone bananas (e.g. server fails on all requests) + + for (;;) + { + try + { + SftpSessionManager::SshSessionExclusive::addSftpChannel({exSession.get()}); //throw SysError + } + catch (SysError&) { if (exSession->getSftpChannelCount() == 0) throw; return static_cast(exSession->getSftpChannelCount()); } + + if (std::chrono::steady_clock::now() > timeoutTime) + throw SysError(_P("Operation timed out after 1 second.", "Operation timed out after %x seconds.", + std::chrono::seconds(SFTP_CHANNEL_LIMIT_DETECTION_TIME_OUT).count()) + L' ' + + replaceCpy(_("Failed to open SFTP channel number %x."), L"%x", formatNumber(exSession->getSftpChannelCount() + 1))); + } + } + catch (const SysError& e) + { + throw FileError(replaceCpy(_("Unable to connect to %x."), L"%x", fmtPath(login.server)), e.toString()); + } +} + + +bool fff::acceptsItemPathPhraseSftp(const Zstring& itemPathPhrase) //noexcept +{ + Zstring path = expandMacros(itemPathPhrase); //expand before trimming! + trim(path); + return startsWithAsciiNoCase(path, sftpPrefix); //check for explicit SFTP path +} + + +/* syntax: sftp://[[:]@][:port]/[|option_name=value] + + e.g. sftp://user001:secretpassword@private.example.com:222/mydirectory/ + sftp://user001:secretpassword@[::1]:80/ipv6folder/ + sftp://user001:secretpassword@::1/ipv6withoutPort/ + sftp://user001@private.example.com/mydirectory|con=2|cpc=10|keyfile=%AppData%\id_rsa|pass64=c2VjcmV0cGFzc3dvcmQ */ +AbstractPath fff::createItemPathSftp(const Zstring& itemPathPhrase) //noexcept +{ + Zstring pathPhrase = expandMacros(itemPathPhrase); //expand before trimming! + trim(pathPhrase); + + if (startsWithAsciiNoCase(pathPhrase, sftpPrefix)) + pathPhrase = pathPhrase.c_str() + strLength(sftpPrefix); + trim(pathPhrase, TrimSide::left, [](Zchar c) { return c == Zstr('/') || c == Zstr('\\'); }); + + const ZstringView credentials = beforeFirst(pathPhrase, Zstr('@'), IfNotFoundReturn::none); + const ZstringView fullPathOpt = afterFirst(pathPhrase, Zstr('@'), IfNotFoundReturn::all); + + SftpLogin login; + login.username = decodeFtpUsername(Zstring(beforeFirst(credentials, Zstr(':'), IfNotFoundReturn::all))); //support standard FTP syntax, even though + login.password = Zstring( afterFirst(credentials, Zstr(':'), IfNotFoundReturn::none)); //concatenateSftpFolderPathPhrase() uses "pass64" instead + + const ZstringView fullPath = beforeFirst(fullPathOpt, Zstr('|'), IfNotFoundReturn::all); + const ZstringView options = afterFirst(fullPathOpt, Zstr('|'), IfNotFoundReturn::none); + + auto it = std::find_if(fullPath.begin(), fullPath.end(), [](Zchar c) { return c == '/' || c == '\\'; }); + const ZstringView serverPort = makeStringView(fullPath.begin(), it); + const AfsPath serverRelPath = sanitizeDeviceRelativePath({it, fullPath.end()}); + + if (std::optional> ip6AndPort = parseIpv6Address(serverPort)) //e.g. 2001:db8::ff00:42:8329 or [::1]:80 + { + login.server = ip6AndPort->first; + login.portCfg = ip6AndPort->second; //0 if empty + } + else + { + login.server = Zstring(beforeLast(serverPort, Zstr(':'), IfNotFoundReturn::all)); + const ZstringView port = afterLast(serverPort, Zstr(':'), IfNotFoundReturn::none); + login.portCfg = stringTo(port); //0 if empty + } + + assert(login.allowZlib == false); + + split(options, Zstr('|'), [&](ZstringView optPhrase) + { + optPhrase = trimCpy(optPhrase); + if (!optPhrase.empty()) + { + if (startsWith(optPhrase, Zstr("timeout="))) + login.timeoutSec = stringTo(afterFirst(optPhrase, Zstr('='), IfNotFoundReturn::none)); + else if (startsWith(optPhrase, Zstr("chan="))) + login.traverserChannelsPerConnection = stringTo(afterFirst(optPhrase, Zstr('='), IfNotFoundReturn::none)); + else if (startsWith(optPhrase, Zstr("keyfile="))) + { + login.authType = SftpAuthType::keyFile; + login.privateKeyFilePath = getResolvedFilePath(Zstring(afterFirst(optPhrase, Zstr('='), IfNotFoundReturn::none))); + } + else if (optPhrase == Zstr("agent")) + login.authType = SftpAuthType::agent; + else if (startsWith(optPhrase, Zstr("pass64="))) + login.password = decodePasswordBase64(afterFirst(optPhrase, Zstr('='), IfNotFoundReturn::none)); + else if (optPhrase == Zstr("pwprompt")) + login.password = std::nullopt; + else if (optPhrase == Zstr("zlib")) + login.allowZlib = true; + else + assert(false); + } + }); + return AbstractPath(makeSharedRef(login), serverRelPath); +} diff --git a/FreeFileSync/Source/afs/sftp.h b/FreeFileSync/Source/afs/sftp.h new file mode 100644 index 0000000..8d0c62d --- /dev/null +++ b/FreeFileSync/Source/afs/sftp.h @@ -0,0 +1,55 @@ +// ***************************************************************************** +// * 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 SFTP_H_5392187498172458215426 +#define SFTP_H_5392187498172458215426 + +#include "abstract.h" + + +namespace fff +{ +bool acceptsItemPathPhraseSftp(const Zstring& itemPathPhrase); //noexcept +AbstractPath createItemPathSftp(const Zstring& itemPathPhrase); //noexcept + +void sftpInit(); +void sftpTeardown(); + +//------------------------------------------------------- + +enum class SftpAuthType +{ + password, + keyFile, + agent, +}; + +const int DEFAULT_PORT_SFTP = 22; +//SFTP default port: 22, see %WINDIR%\system32\drivers\etc\services +//=> we could use the "ssh" alias, but let's be explicit + +struct SftpLogin +{ + Zstring server; + int portCfg = 0; //use if > 0, DEFAULT_PORT_SFTP otherwise + Zstring username; + SftpAuthType authType = SftpAuthType::password; + std::optional password = Zstr(""); //authType == password or keyFile: none given => prompt during AFS::authenticateAccess() + Zstring privateKeyFilePath; //authType == keyFile: use PEM-encoded private key (protected by password) for authentication + bool allowZlib = false; + //other settings not specific to SFTP session: + int timeoutSec = 10; //valid range: [1, inf) + int traverserChannelsPerConnection = 1; //valid range: [1, inf) +}; +AfsDevice condenseToSftpDevice(const SftpLogin& login); //noexcept; potentially messy user input +SftpLogin extractSftpLogin(const AfsDevice& afsDevice); //noexcept + +int getServerMaxChannelsPerConnection(const SftpLogin& login); //throw FileError + +AfsPath getSftpHomePath(const SftpLogin& login); //throw FileError +} + +#endif //SFTP_H_5392187498172458215426 diff --git a/FreeFileSync/Source/application.cpp b/FreeFileSync/Source/application.cpp new file mode 100644 index 0000000..a868de8 --- /dev/null +++ b/FreeFileSync/Source/application.cpp @@ -0,0 +1,768 @@ +// ***************************************************************************** +// * 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 "application.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "afs/concrete.h" +#include "base/comparison.h" +#include "base/synchronization.h" +#include "ui/batch_status_handler.h" +#include "ui/main_dlg.h" +#include "ui/small_dlgs.h" +#include "base_tools.h" +#include "ffs_paths.h" +#include "return_codes.h" + + #include + +using namespace zen; +using namespace fff; + + +#ifdef __WXGTK3__ + /* Wayland backend used by GTK3 does not allow to move windows! (no such issue on GTK2) + + "I'd really like to know if there is some deep technical reason for it or + if this is really as bloody stupid as it seems?" - vadz https://github.com/wxWidgets/wxWidgets/issues/18733#issuecomment-1011235902 + + Show all available GTK backends: run FreeFileSync with env variable: GDK_BACKEND=help + + => workaround: https://docs.gtk.org/gdk3/func.set_allowed_backends.html */ + GLOBAL_RUN_ONCE(::gdk_set_allowed_backends("x11,*")); //call *before* gtk_init() +#endif + +IMPLEMENT_APP(Application) + + +namespace +{ +std::vector getCommandlineArgs(const wxApp& app) +{ + std::vector args; + for (const wxString& arg : app.argv.GetArguments()) + args.push_back(utfTo(arg)); + //remove first argument which is exe path by convention: https://devblogs.microsoft.com/oldnewthing/20060515-07/?p=31203 + if (!args.empty()) + args.erase(args.begin()); + + return args; +} + + +void showSyntaxHelp() +{ + showNotificationDialog(nullptr, DialogInfoType::info, PopupDialogCfg(). + setTitle(_("Command line")). + setDetailInstructions(_("Syntax:") + L"\n\n" + + L"FreeFileSync" + L'\n' + + TAB_SPACE + L"[" + _("config files:") + L" *.ffs_gui/*.ffs_batch]" + L'\n' + + TAB_SPACE + L"[-DirPair " + _("directory") + L' ' + _("directory") + L"]" L"\n" + + TAB_SPACE + L"[-Edit]" + L'\n' + + TAB_SPACE + L"[" + _("global config file:") + L" GlobalSettings.xml]" + L"\n\n" + + + _("config files:") + L'\n' + + _("Any number of FreeFileSync \"ffs_gui\" and/or \"ffs_batch\" configuration files.") + L"\n\n" + + + L"-DirPair " + _("directory") + L' ' + _("directory") + L'\n' + + _("Any number of alternative directory pairs for at most one config file.") + L"\n\n" + + + L"-Edit" + L'\n' + + _("Open the selected configuration for editing only, without executing it.") + L"\n\n" + + + _("global config file:") + L'\n' + + _("Path to an alternate GlobalSettings.xml file."))); +} + + +void notifyAppError(const std::wstring& msg) +{ + std::cerr << utfTo(_("Error") + L": " + msg) << '\n'; + //alternative0: std::wcerr: cannot display non-ASCII at all, so why does it exist??? + //alternative1: wxSafeShowMessage => NO console output on Debian x86, WTF! + //alternative2: wxMessageBox() => works, but we probably shouldn't block during command line usage +} +} + +//################################################################################################################## + +bool Application::OnInit() +{ + //do not call wxApp::OnInit() to avoid using wxWidgets command line parser + + const auto now = std::chrono::system_clock::now(); //e.g. "ErrorLog 2023-07-05 105207.073.xml" + initExtraLog([logFilePath = appendPath(getConfigDirPath(), Zstr("ErrorLog ") + + formatTime(Zstr("%Y-%m-%d %H%M%S"), getLocalTime(std::chrono::system_clock::to_time_t(now))) + Zstr('.') + + printNumber(Zstr("%03d"), //[ms] should yield a fairly unique name + static_cast(std::chrono::duration_cast(now.time_since_epoch()).count() % 1000)) + + Zstr(".xml"))](const ErrorLog& log) + { + try //don't call functions depending on global state (which might be destroyed already!) + { + saveErrorLog(log, logFilePath); //throw FileError + } + catch (const FileError& e) { assert(false); notifyAppError(e.toString()); } + }); + + //tentatively set program language to OS default until GlobalSettings.xml is read later + try { localizationInit(appendPath(getResourceDirPath(), Zstr("Languages.zip"))); } //throw FileError + catch (const FileError& e) { logExtraError(e.toString()); } + + //parallel xBRZ-scaling! => run as early as possible + try { imageResourcesInit(appendPath(getResourceDirPath(), Zstr("Icons.zip"))); } + catch (const FileError& e) { logExtraError(e.toString()); } //not critical in this context + + //GTK should already have been initialized by wxWidgets (see \src\gtk\app.cpp:wxApp::Initialize) +#if GTK_MAJOR_VERSION == 2 + ::gtk_rc_parse(appendPath(getResourceDirPath(), "Gtk2Styles.rc").c_str()); + + //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 + // 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(formatSystemError("setenv(GIO_USE_VFS)", errno)) + '\n'; + // //BUGZ!?: "Modifications of environment variables are not allowed in multi-threaded programs" - https://rachelbythebay.com/w/2017/01/30/env/ + + //=> work around 2: + [[maybe_unused]] GVfs* defaultFs = ::g_vfs_get_default(); //not owned by us! + //no such issue on GTK3! + +#elif GTK_MAJOR_VERSION == 3 + auto loadCSS = [&](const char* fileName) + { + GtkCssProvider* provider = ::gtk_css_provider_new(); + ZEN_ON_SCOPE_EXIT(::g_object_unref(provider)); + + GError* error = nullptr; + ZEN_ON_SCOPE_EXIT(if (error) ::g_error_free(error)); + + ::gtk_css_provider_load_from_path(provider, //GtkCssProvider* css_provider + appendPath(getResourceDirPath(), fileName).c_str(), //const gchar* path + &error); //GError** error + if (error) + throw SysError(formatGlibError("gtk_css_provider_load_from_path", error)); + + ::gtk_style_context_add_provider_for_screen(::gdk_screen_get_default(), //GdkScreen* screen + GTK_STYLE_PROVIDER(provider), //GtkStyleProvider* provider + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); //guint priority + }; + try + { + loadCSS("Gtk3Styles.css"); //throw SysError + } + catch (const SysError& e) + { + std::cerr << "[FreeFileSync] " + utfTo(e.toString()) + "\n" "Loading GTK3\'s old CSS format instead..." "\n"; + try + { + loadCSS("Gtk3Styles.old.css"); //throw SysError + } + catch (const SysError& e2) { logExtraError(_("Failed to update the color theme.") + L"\n\n" + e2.toString()); } + } +#else +#error unknown GTK version! +#endif + + /* we're a GUI app: ignore SIGHUP when the parent terminal quits! (or process is killed!) + => the FFS launcher will still be killed => fine + => macOS: apparently not needed! interestingly the FFS launcher does receive SIGHUP and *is* killed */ + if (sighandler_t oldHandler = ::signal(SIGHUP, SIG_IGN); + oldHandler == SIG_ERR) + logExtraError(_("Error during process initialization.") + L"\n\n" + formatSystemError("signal(SIGHUP)", getLastError())); + else assert(!oldHandler); + + + //Windows User Experience Interaction Guidelines: tool tips should have 5s timeout, info tips no timeout => compromise: + wxToolTip::Enable(true); //wxWidgets screw-up: wxToolTip::SetAutoPop is no-op if global tooltip window is not yet constructed: wxToolTip::Enable creates it + wxToolTip::SetAutoPop(15'000); //https://docs.microsoft.com/en-us/windows/win32/uxguide/ctrl-tooltips-and-infotips + + SetAppName(L"FreeFileSync"); //if not set, defaults to executable name + + + initAfs({getResourceDirPath(), getConfigDirPath()}); //bonus: using FTP Gdrive implicitly inits OpenSSL (used in runSanityChecks() on Linux) already during globals init + + + auto onSystemShutdown = [](int /*unused*/ = 0) + { + onSystemShutdownRunTasks(); + + //- it's futile to try and clean up while the process is in full swing (CRASH!) => just terminate! + //- system sends close events to all open dialogs: If one of these calls wxCloseEvent::Veto(), + // e.g. user clicking cancel on save prompt, this would cancel the shutdown + terminateProcess(static_cast(FfsExitCode::cancelled)); + }; + Bind(wxEVT_QUERY_END_SESSION, [onSystemShutdown](wxCloseEvent& event) { onSystemShutdown(); }); //can veto + Bind(wxEVT_END_SESSION, [onSystemShutdown](wxCloseEvent& event) { onSystemShutdown(); }); //can *not* veto + //- log off: Windows/macOS generates wxEVT_QUERY_END_SESSION/wxEVT_END_SESSION + // Linux/macOS generates SIGTERM, which we handle below + //- Windows sends WM_QUERYENDSESSION, WM_ENDSESSION during log off, *not* WM_CLOSE https://devblogs.microsoft.com/oldnewthing/20080421-00/?p=22663 + // => "taskkill sending WM_CLOSE (without /f)" is a misguided app simulating a button-click on X + // -> should send WM_QUERYENDSESSION instead! + if (auto /*sighandler_t n.a. on macOS*/ oldHandler = ::signal(SIGTERM, onSystemShutdown);//"graceful" exit requested, unlike SIGKILL + oldHandler == SIG_ERR) + logExtraError(_("Error during process initialization.") + L"\n\n" + formatSystemError("signal(SIGTERM)", getLastError())); + else assert(!oldHandler); + + //Note: app start is deferred: batch mode requires the wxApp eventhandler to be established for UI update events. This is not the case at the time of OnInit()! + CallAfter([&] { onEnterEventLoop(); }); + + return true; //true: continue processing; false: exit immediately +} + + +int Application::OnExit() +{ + [[maybe_unused]] const bool rv = wxClipboard::Get()->Flush(); //see wx+/context_menu.h + //assert(rv); -> fails if clipboard wasn't used + localizationCleanup(); + imageResourcesCleanup(); + teardownAfs(); + colorThemeCleanup(); + return wxApp::OnExit(); +} + + +wxLayoutDirection Application::GetLayoutDirection() const { return languageLayoutIsRtl() ? wxLayout_RightToLeft : wxLayout_LeftToRight; } + + +int Application::OnRun() +{ +#if wxUSE_EXCEPTIONS +#error why is wxWidgets uncaught exception handling enabled!? +#endif + + //exception => Windows: let it crash and create mini dump!!! Linux/macOS: std::exception::what() logged to console + [[maybe_unused]] const int rc = wxApp::OnRun(); + return static_cast(exitCode_); +} + + + + +void Application::onEnterEventLoop() +{ + const std::vector& commandArgs = getCommandlineArgs(*this); + + //wxWidgets app exit handling is weird... we want to exit only if the logical main window is closed, not just *any* window! + wxTheApp->SetExitOnFrameDelete(false); //prevent popup-windows from becoming temporary top windows leading to program exit after closure + ZEN_ON_SCOPE_EXIT(if (!wxTheApp->GetExitOnFrameDelete()) wxTheApp->ExitMainLoop()); //quit application, if no main window was set (batch silent mode) + + try + { + //parse command line arguments + std::vector> dirPathPhrasePairs; + std::vector cfgFilePaths; + Zstring globalCfgPathAlt; + bool openForEdit = false; + { + const char* optionEdit = "-edit"; + const char* optionDirPair = "-dirpair"; + const char* optionSendTo = "-sendto"; //remaining arguments are unspecified number of folder paths; wonky syntax; let's keep it undocumented + + auto isHelpRequest = [](const Zstring& arg) + { + auto it = std::find_if(arg.begin(), arg.end(), [](Zchar c) { return c != Zstr('/') && c != Zstr('-'); }); + if (it == arg.begin()) return false; //require at least one prefix character + + const Zstring argTmp(it, arg.end()); + return equalAsciiNoCase(argTmp, "help") || + equalAsciiNoCase(argTmp, "h") || + argTmp == Zstr("?"); + }; + + auto isCommandLineOption = [&](const Zstring& arg) + { + return equalAsciiNoCase(arg, optionEdit ) || + equalAsciiNoCase(arg, optionDirPair) || + equalAsciiNoCase(arg, optionSendTo ) || + isHelpRequest(arg); + }; + + for (auto it = commandArgs.begin(); it != commandArgs.end(); ++it) + if (isHelpRequest(*it)) + return showSyntaxHelp(); + else if (equalAsciiNoCase(*it, optionEdit)) + openForEdit = true; + else if (equalAsciiNoCase(*it, optionDirPair)) + { + if (++it == commandArgs.end() || isCommandLineOption(*it)) + throw FileError(replaceCpy(_("A left and a right directory path are expected after %x."), L"%x", utfTo(optionDirPair))); + dirPathPhrasePairs.emplace_back(*it, Zstring()); + + if (++it == commandArgs.end() || isCommandLineOption(*it)) + throw FileError(replaceCpy(_("A left and a right directory path are expected after %x."), L"%x", utfTo(optionDirPair))); + dirPathPhrasePairs.back().second = *it; + } + else if (equalAsciiNoCase(*it, optionSendTo)) + { + for (size_t i = 0; ; ++i) + { + if (++it == commandArgs.end() || isCommandLineOption(*it)) + { + --it; + break; + } + + if (i < 2) //else: -SendTo with more than 2 paths? Doesn't make any sense, does it!? + { + //for -SendTo we expect a list of full native paths, not "phrases" that need to be resolved! + auto getFolderPath = [](Zstring itemPath) + { + try + { + if (getItemType(itemPath) == ItemType::file) //throw FileError + if (const std::optional& parentPath = getParentFolderPath(itemPath)) + return *parentPath; + } + catch (FileError&) {} + + return itemPath; + }; + + if (i % 2 == 0) + dirPathPhrasePairs.emplace_back(getFolderPath(*it), Zstring()); + else + { + const Zstring folderPath = getFolderPath(*it); + if (dirPathPhrasePairs.back().first != folderPath) //else: user accidentally sending to two files, which each time yield the same parent folder + dirPathPhrasePairs.back().second = folderPath; + } + } + } + } + else + { + const Zstring& filePath = getResolvedFilePath(*it); +#if 0 + if (!fileAvailable(filePath)) //...be a little tolerant + for (const Zchar* ext : {Zstr(".ffs_gui"), Zstr(".ffs_batch"), Zstr(".xml")}) + if (fileAvailable(filePath + ext)) + filePath += ext; +#endif + if (endsWithAsciiNoCase(filePath, Zstr(".ffs_gui")) || + endsWithAsciiNoCase(filePath, Zstr(".ffs_batch"))) + cfgFilePaths.push_back(filePath); + else if (endsWithAsciiNoCase(filePath, Zstr(".xml"))) + globalCfgPathAlt = filePath; + else + throw FileError(replaceCpy(_("Cannot open file %x."), L"%x", fmtPath(filePath)), + _("Unexpected file extension:") + L' ' + fmtPath(getFileExtension(filePath)) + L'\n' + + _("Expected:") + L" ffs_gui, ffs_batch, xml"); + } + } + //---------------------------------------------------------------------------------------------------- + + auto hasNonDefaultConfig = [](const LocalPairConfig& lpc) + { + return lpc != LocalPairConfig{lpc.folderPathPhraseLeft, + lpc.folderPathPhraseRight, + std::nullopt, std::nullopt, FilterConfig()}; + }; + + auto replaceDirectories = [&](MainConfiguration& mainCfg) //throw FileError + { + if (!dirPathPhrasePairs.empty()) + { + if (cfgFilePaths.size() > 1) + throw FileError(_("Directories cannot be set for more than one configuration file.")); + + //check if config at folder-pair level is present: this probably doesn't make sense when replacing/adding the user-specified directories + if (hasNonDefaultConfig(mainCfg.firstPair) || std::any_of(mainCfg.additionalPairs.begin(), mainCfg.additionalPairs.end(), hasNonDefaultConfig)) + throw FileError(_("The config file must not contain settings at directory pair level when directories are set via command line.")); + + mainCfg.additionalPairs.clear(); + for (size_t i = 0; i < dirPathPhrasePairs.size(); ++i) + if (i == 0) + { + mainCfg.firstPair.folderPathPhraseLeft = dirPathPhrasePairs[0].first; + mainCfg.firstPair.folderPathPhraseRight = dirPathPhrasePairs[0].second; + } + else + mainCfg.additionalPairs.push_back({dirPathPhrasePairs[i].first, dirPathPhrasePairs[i].second, + std::nullopt, std::nullopt, FilterConfig()}); + } + }; + + const Zstring globalCfgFilePath = !globalCfgPathAlt.empty() ? globalCfgPathAlt : getGlobalConfigDefaultPath(); + + GlobalConfig globalCfg; + try + { + std::wstring warningMsg; + std::tie(globalCfg, warningMsg) = readGlobalConfig(globalCfgFilePath); //throw FileError + assert(warningMsg.empty()); //ignore parsing errors: should be migration problems only *cross-fingers* + } + catch (const FileError& e) + { + try + { + bool cfgFileExists = true; + try { cfgFileExists = itemExists(globalCfgFilePath); /*throw FileError*/ } //=> unclear which exception is more relevant/useless: + catch (const FileError& e2) { throw FileError(replaceCpy(e.toString(), L"\n\n", L'\n'), replaceCpy(e2.toString(), L"\n\n", L'\n')); } + + if (cfgFileExists) + throw; + } + catch (const FileError& e3) { logExtraError(e3.toString()); } + } + + //late GlobalSettings.xml-dependent app initialization: + try { setLanguage(globalCfg.programLanguage); } //throw FileError + catch (const FileError& e) { logExtraError(e.toString()); } + + try { colorThemeInit(*this, globalCfg.appColorTheme); } //throw FileError + catch (const FileError& e) { logExtraError(e.toString()); } //not critical in this context + + + //----------------------------------------------------------- + //distinguish sync scenarios: + //----------------------------------------------------------- + if (cfgFilePaths.empty()) + { + //gui mode: default startup + if (dirPathPhrasePairs.empty()) + MainDialog::create(globalCfg, globalCfgFilePath); + //gui mode: default config with given directories + else + { + FfsGuiConfig guiCfg; + guiCfg.mainCfg.syncCfg.directionCfg = getDefaultSyncCfg(SyncVariant::mirror); + + replaceDirectories(guiCfg.mainCfg); //throw FileError + + MainDialog::create(guiCfg, {} /*cfgFilePaths*/, globalCfg, globalCfgFilePath, !openForEdit /*startComparison*/); + } + } + else if (const Zstring filePath0 = cfgFilePaths[0]; + //batch mode (single config) + cfgFilePaths.size() == 1 && endsWithAsciiNoCase(filePath0, Zstr(".ffs_batch")) && !openForEdit) + { + auto [batchCfg, warningMsg] = readBatchConfig(filePath0); //throw FileError + if (!warningMsg.empty()) + throw FileError(warningMsg); //batch mode: break on errors AND even warnings! + + replaceDirectories(batchCfg.guiCfg.mainCfg); //throw FileError + + runBatchMode(batchCfg, filePath0, globalCfg, globalCfgFilePath); + } + else //GUI mode: (ffs_gui *or* ffs_batch) + { + auto [guiCfg, warningMsg] = readAnyConfig(cfgFilePaths); //throw FileError + if (!warningMsg.empty()) + showNotificationDialog(nullptr, DialogInfoType::warning, PopupDialogCfg().setDetailInstructions(warningMsg)); + //what about simulating changed config on parsing errors? + + replaceDirectories(guiCfg.mainCfg); //throw FileError + //what about simulating changed config due to directory replacement? + //-> propably fine to not show as changed on GUI and not ask user to save on exit! + + MainDialog::create(guiCfg, cfgFilePaths, globalCfg, globalCfgFilePath, !openForEdit /*startComparison*/); + } + } + catch (const FileError& e) + { + raiseExitCode(exitCode_, FfsExitCode::exception); + notifyAppError(e.toString()); + } +} + + +void Application::runBatchMode(const FfsBatchConfig& batchCfg, const Zstring& cfgFilePath, GlobalConfig globalCfg, const Zstring& globalCfgFilePath) +{ + const bool allowUserInteraction = !batchCfg.batchExCfg.autoCloseSummary || + (!batchCfg.guiCfg.mainCfg.ignoreErrors && batchCfg.batchExCfg.batchErrorHandling == BatchErrorHandling::showPopup); + + + /* regular check for software updates -> disabled for batch + if (batchCfg.showProgress && manualProgramUpdateRequired()) + checkForUpdatePeriodically(globalCfg.lastUpdateCheck); + -> WinInet not working when FFS is running as a service!!! https://support.microsoft.com/en-us/help/238425/info-wininet-not-supported-for-use-in-services */ + + + const std::chrono::system_clock::time_point syncStartTime = std::chrono::system_clock::now(); + + const WindowLayout::Dimensions progressDim + { + globalCfg.dpiLayouts[getDpiScalePercent()].progressDlg.size, + std::nullopt /*pos*/, + globalCfg.dpiLayouts[getDpiScalePercent()].progressDlg.isMaximized + }; + + //class handling status updates and error messages + BatchStatusHandler statusHandler(!batchCfg.batchExCfg.runMinimized, + extractJobName(cfgFilePath), + syncStartTime, + batchCfg.guiCfg.mainCfg.ignoreErrors, + batchCfg.guiCfg.mainCfg.autoRetryCount, + batchCfg.guiCfg.mainCfg.autoRetryDelay, + globalCfg.soundFileSyncFinished, + globalCfg.soundFileAlertPending, + progressDim, + batchCfg.batchExCfg.autoCloseSummary, + batchCfg.batchExCfg.postBatchAction, + batchCfg.batchExCfg.batchErrorHandling); + + AFS::RequestPasswordFun requestPassword; //throw CancelProcess + if (allowUserInteraction) + requestPassword = [&, password = Zstring()](const std::wstring& msg, const std::wstring& lastErrorMsg) mutable + { + assert(runningOnMainThread()); + if (showPasswordPrompt(statusHandler.getWindowIfVisible(), msg, lastErrorMsg, password) != ConfirmationButton::accept) + statusHandler.cancelProcessNow(CancelReason::user); //throw CancelProcess + + return password; + }; + + try + { + //inform about (important) non-default global settings + logNonDefaultSettings(globalCfg, statusHandler); //throw CancelProcess + + //batch mode: place directory locks on directories during both comparison AND synchronization + std::unique_ptr dirLocks; + + FolderComparison cmpResult = compare(globalCfg.warnDlgs, + globalCfg.fileTimeTolerance, + requestPassword, + globalCfg.runWithBackgroundPriority, + globalCfg.createLockFile, + dirLocks, + extractCompareCfg(batchCfg.guiCfg.mainCfg), + statusHandler); //throw CancelProcess + if (!cmpResult.empty()) + synchronize(syncStartTime, + globalCfg.verifyFileCopy, + globalCfg.copyLockedFiles, + globalCfg.copyFilePermissions, + globalCfg.failSafeFileCopy, + globalCfg.runWithBackgroundPriority, + extractSyncCfg(batchCfg.guiCfg.mainCfg), + cmpResult, + globalCfg.warnDlgs, + statusHandler); //throw CancelProcess + } + catch (CancelProcess&) {} + + //------------------------------------------------------------------- + BatchStatusHandler::Result r = statusHandler.prepareResult(); + + + AbstractPath logFolderPath = createAbstractPath(batchCfg.guiCfg.mainCfg.altLogFolderPathPhrase); //optional + if (AFS::isNullPath(logFolderPath)) + logFolderPath = createAbstractPath(globalCfg.logFolderPhrase); + assert(!AFS::isNullPath(logFolderPath)); //mandatory! but still: let's include fall back + if (AFS::isNullPath(logFolderPath)) + logFolderPath = createAbstractPath(getLogFolderDefaultPath()); + + AbstractPath logFilePath = AFS::appendRelPath(logFolderPath, generateLogFileName(globalCfg.logFormat, r.summary)); + //e.g. %AppData%\FreeFileSync\Logs\Backup FreeFileSync 2013-09-15 015052.123 [Error].log + + auto notifyStatusNoThrow = [&](std::wstring&& msg) { try { statusHandler.updateStatus(std::move(msg)); /*throw CancelProcess*/ } catch (CancelProcess&) {} }; + + + if (statusHandler.taskCancelled() && *statusHandler.taskCancelled() == CancelReason::user) + ; /* user cancelled => don't run post sync command + => don't run post sync action + => don't send email notification + => don't play sound notification */ + else + { + //--------------------- post sync command ---------------------- + if (const Zstring cmdLine = trimCpy(expandMacros(batchCfg.guiCfg.mainCfg.postSyncCommand)); + !cmdLine.empty()) + if (batchCfg.guiCfg.mainCfg.postSyncCondition == PostSyncCondition::completion || + (batchCfg.guiCfg.mainCfg.postSyncCondition == PostSyncCondition::errors) == (r.summary.result == TaskResult::cancelled || + r.summary.result == TaskResult::error)) + try + { + //give consoleExecute() some "time to fail", but not too long to hang our process + const int DEFAULT_APP_TIMEOUT_MS = 100; + + if (const auto& [exitCode, output] = consoleExecute(cmdLine, DEFAULT_APP_TIMEOUT_MS); //throw SysError, SysErrorTimeOut + exitCode != 0) + throw SysError(formatSystemError("", replaceCpy(_("Exit code %x"), L"%x", numberTo(exitCode)), utfTo(output))); + + logMsg(r.errorLog.ref(), _("Executing command:") + L' ' + utfTo(cmdLine) + L" [" + replaceCpy(_("Exit code %x"), L"%x", L"0") + L']', MSG_TYPE_INFO); + } + catch (SysErrorTimeOut&) //child process not failed yet => probably fine :> + { + logMsg(r.errorLog.ref(), _("Executing command:") + L' ' + utfTo(cmdLine), MSG_TYPE_INFO); + } + catch (const SysError& e) + { + logMsg(r.errorLog.ref(), replaceCpy(_("Command %x failed."), L"%x", fmtPath(cmdLine)) + L"\n\n" + e.toString(), MSG_TYPE_ERROR); + } + + //--------------------- email notification ---------------------- + if (const std::string notifyEmail = trimCpy(batchCfg.guiCfg.mainCfg.emailNotifyAddress); + !notifyEmail.empty()) + if (batchCfg.guiCfg.mainCfg.emailNotifyCondition == ResultsNotification::always || + (batchCfg.guiCfg.mainCfg.emailNotifyCondition == ResultsNotification::errorWarning && (r.summary.result == TaskResult::cancelled || + r.summary.result == TaskResult::error || + r.summary.result == TaskResult::warning)) || + (batchCfg.guiCfg.mainCfg.emailNotifyCondition == ResultsNotification::errorOnly && (r.summary.result == TaskResult::cancelled || + r.summary.result == TaskResult::error))) + try + { + logMsg(r.errorLog.ref(), replaceCpy(_("Sending email notification to %x"), L"%x", utfTo(notifyEmail)), MSG_TYPE_INFO); + sendLogAsEmail(notifyEmail, r.summary, r.errorLog.ref(), logFilePath, notifyStatusNoThrow); //throw FileError + } + catch (const FileError& e) { logMsg(r.errorLog.ref(), e.toString(), MSG_TYPE_ERROR); } + } + + //--------------------- save log file ---------------------- + std::set logsToKeepPaths; + for (const ConfigFileItem& cfi : globalCfg.mainDlg.config.fileHistory) + if (!equalNativePath(cfi.cfgFilePath, cfgFilePath)) //exception: don't keep old log for the selected cfg file! + logsToKeepPaths.insert(cfi.lastRunStats.logFilePath); + + 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! + saveLogFile(logFilePath, r.summary, r.errorLog.ref(), globalCfg.logfilesMaxAgeDays, globalCfg.logFormat, logsToKeepPaths, notifyStatusNoThrow); //throw FileError + } + catch (const FileError& e) + { + try //fallback: log file *must* be saved no matter what! + { + const AbstractPath logFileDefaultPath = AFS::appendRelPath(createAbstractPath(getLogFolderDefaultPath()), generateLogFileName(globalCfg.logFormat, r.summary)); + if (logFilePath == logFileDefaultPath) + throw; + + logMsg(r.errorLog.ref(), e.toString(), MSG_TYPE_ERROR); + + logFilePath = logFileDefaultPath; + saveLogFile(logFileDefaultPath, r.summary, r.errorLog.ref(), globalCfg.logfilesMaxAgeDays, globalCfg.logFormat, logsToKeepPaths, notifyStatusNoThrow); //throw FileError + } + catch (const FileError& e2) { logMsg(r.errorLog.ref(), e2.toString(), MSG_TYPE_ERROR); logExtraError(e2.toString()); } //should never happen!!! + } + + //--------- update last sync stats for the selected cfg file --------- + const ErrorLogStats& logStats = getStats(r.errorLog.ref()); + + for (ConfigFileItem& cfi : globalCfg.mainDlg.config.fileHistory) + if (equalNativePath(cfi.cfgFilePath, cfgFilePath)) + { + assert(r.summary.startTime == syncStartTime); + assert(!AFS::isNullPath(logFilePath)); + + cfi.lastRunStats = + { + std::chrono::system_clock::to_time_t(r.summary.startTime), + logFilePath, + r.summary.result, + r.summary.statsProcessed.items, + r.summary.statsProcessed.bytes, + r.summary.totalTime, + logStats.errors, + logStats.warnings, + }; + break; + } + + //--------------------------------------------------------------------------- + const BatchStatusHandler::DlgOptions dlgOpt = statusHandler.showResult(); + + globalCfg.dpiLayouts[getDpiScalePercent()].progressDlg.size = dlgOpt.dim.size; //=> ignore dim.pos + globalCfg.dpiLayouts[getDpiScalePercent()].progressDlg.isMaximized = dlgOpt.dim.isMaximized; + + //---------------------------------------------------------------------- + switch (r.summary.result) + { + case TaskResult::success: raiseExitCode(exitCode_, FfsExitCode::success); break; + case TaskResult::warning: raiseExitCode(exitCode_, FfsExitCode::warning); break; + case TaskResult::error: raiseExitCode(exitCode_, FfsExitCode::error ); break; + case TaskResult::cancelled: raiseExitCode(exitCode_, FfsExitCode::cancelled); break; + } + + //email sending, or saving log file failed? at least this should affect the exit code: + if (logStats.errors > 0) + raiseExitCode(exitCode_, FfsExitCode::error); + else if (logStats.warnings > 0) + raiseExitCode(exitCode_, FfsExitCode::warning); + + //--------------------------------------------------------------------------- + //stream sync stats to STDOUT as JSON + JsonValue syncStats(JsonValue::Type::object); + switch (r.summary.result) + { + case TaskResult::success: syncStats.objectVal.set("syncResult", "success"); break; + case TaskResult::warning: syncStats.objectVal.set("syncResult", "warning"); break; + case TaskResult::error: syncStats.objectVal.set("syncResult", "error"); break; + case TaskResult::cancelled: syncStats.objectVal.set("syncResult", "cancelled"); break; + } + + std::string startTimeStr = utfTo(formatTime(Zstr("%Y-%m-%dT%H:%M:%S%z"), getLocalTime(std::chrono::system_clock::to_time_t(r.summary.startTime)))); + syncStats.objectVal.set("startTime", std::move(startTimeStr.insert(startTimeStr.size() - 2, ":"))); //ISO 8601 date/time with offset e.g. 2001-08-23T14:55:02+02:00 + + syncStats.objectVal.set("totalTimeSec", std::chrono::duration_cast(r.summary.totalTime).count()); + + syncStats.objectVal.set("errors", logStats.errors); + syncStats.objectVal.set("warnings", logStats.warnings); + + syncStats.objectVal.set("totalItems", r.summary.statsTotal.items); + syncStats.objectVal.set("totalBytes", r.summary.statsTotal.bytes); + + syncStats.objectVal.set("processedItems", r.summary.statsProcessed.items); + syncStats.objectVal.set("processedBytes", r.summary.statsProcessed.bytes); + + syncStats.objectVal.set("logFile", utfTo(AFS::getDisplayPath(logFilePath))); + + std::cout << serializeJson(syncStats); + + //--------------------------------------------------------------------------- + try //save global settings to XML: e.g. ignored warnings, last sync stats + { + writeConfig(globalCfg, globalCfgFilePath); //FileError + } + catch (const FileError& e) + { + //raiseExitCode(exitCode_, FfsExitCode::error); -> sync successful + if (allowUserInteraction) + showNotificationDialog(nullptr, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); + else + logExtraError(e.toString()); + } + + //--------------------------------------------------------------------------- + //run shutdown *after* saving global config! https://freefilesync.org/forum/viewtopic.php?t=5761 + using FinalRequest = BatchStatusHandler::FinalRequest; + switch (dlgOpt.finalRequest) + { + case FinalRequest::none: + break; + + case FinalRequest::switchGui: //open new top-level window *after* progress dialog is gone => run on main event loop + MainDialog::create(batchCfg.guiCfg, {cfgFilePath}, globalCfg, globalCfgFilePath, true /*startComparison*/); + break; + + case FinalRequest::shutdown: + try + { + shutdownSystem(); //throw FileError + terminateProcess(static_cast(exitCode_)); //better exit in a controlled manner rather than letting the OS kill us any time! + } + catch (const FileError& e) + { + //raiseExitCode(exitCode_, FfsExitCode::error); -> no! sync was successful + if (allowUserInteraction) + showNotificationDialog(nullptr, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); + else + logExtraError(e.toString()); + } + break; + } +} diff --git a/FreeFileSync/Source/application.h b/FreeFileSync/Source/application.h new file mode 100644 index 0000000..f2b0f0d --- /dev/null +++ b/FreeFileSync/Source/application.h @@ -0,0 +1,35 @@ +// ***************************************************************************** +// * 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 APPLICATION_H_081568741942010985702395 +#define APPLICATION_H_081568741942010985702395 + +//#include +#include +#include +#include "config.h" +#include "return_codes.h" + + +namespace fff //avoid name clash with "int ffs()" for fuck's sake! (maxOS, Linux issue only: internally includes , WTF!) +{ +class Application : public wxApp +{ +private: + bool OnInit() override; + int OnRun () override; + int OnExit() override; + wxLayoutDirection GetLayoutDirection() const override; + void onEnterEventLoop(); + + void runBatchMode(const FfsBatchConfig& batchCfg, const Zstring& cfgFilePath, + GlobalConfig globalCfg, const Zstring& globalCfgFilePath); + + FfsExitCode exitCode_ = FfsExitCode::success; +}; +} + +#endif //APPLICATION_H_081568741942010985702395 diff --git a/FreeFileSync/Source/base/algorithm.cpp b/FreeFileSync/Source/base/algorithm.cpp new file mode 100644 index 0000000..a53d0bc --- /dev/null +++ b/FreeFileSync/Source/base/algorithm.cpp @@ -0,0 +1,1886 @@ +// ***************************************************************************** +// * 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 "algorithm.h" +#include +#include +#include //needed for TempFileBuffer only +#include "norm_filter.h" +#include "db_file.h" +#include "cmp_filetime.h" +#include "status_handler_impl.h" +#include "../afs/concrete.h" +#include "../afs/native.h" + +using namespace zen; +using namespace fff; + + +void fff::swapGrids(const MainConfiguration& mainCfg, FolderComparison& folderCmp, + PhaseCallback& callback /*throw X*/) //throw X +{ + for (BaseFolderPair& baseFolder : asRange(folderCmp)) + baseFolder.flip(); + + redetermineSyncDirection(extractDirectionCfg(folderCmp, mainCfg), + callback); //throw FileError +} + +//---------------------------------------------------------------------------------------------- + +namespace +{ +//visitFSObjectRecursively? nope, see premature end of traversal in processFolder() +class SetSyncDirViaDifferences +{ +public: + static void execute(const DirectionByDiff& dirs, ContainerObject& conObj) { SetSyncDirViaDifferences(dirs).recurse(conObj); } + +private: + SetSyncDirViaDifferences(const DirectionByDiff& dirs) : dirs_(dirs) {} + + void recurse(ContainerObject& conObj) const + { + for (FilePair& file : conObj.files()) + processFile(file); + for (SymlinkPair& link : conObj.symlinks()) + processLink(link); + for (FolderPair& folder : conObj.subfolders()) + processFolder(folder); + } + + void processFile(FilePair& file) const + { + const CompareFileResult cat = file.getCategory(); + + //##################### schedule old temporary files for deletion #################### + if (cat == FILE_LEFT_ONLY && endsWith(file.getItemName(), AFS::TEMP_FILE_ENDING)) + return file.setSyncDir(SyncDirection::left); + else if (cat == FILE_RIGHT_ONLY && endsWith(file.getItemName(), AFS::TEMP_FILE_ENDING)) + return file.setSyncDir(SyncDirection::right); + //#################################################################################### + + switch (cat) + { + case FILE_EQUAL: + //file.setSyncDir(SyncDirection::none); + break; + case FILE_RENAMED: + if (dirs_.leftNewer == dirs_.rightNewer) + file.setSyncDir(dirs_.leftNewer); //treat "rename" like a "file update" + else + file.setSyncDirConflict(txtDiffName_); + break; + case FILE_LEFT_ONLY: + file.setSyncDir(dirs_.leftOnly); + break; + case FILE_RIGHT_ONLY: + file.setSyncDir(dirs_.rightOnly); + break; + case FILE_LEFT_NEWER: + file.setSyncDir(dirs_.leftNewer); + break; + case FILE_RIGHT_NEWER: + file.setSyncDir(dirs_.rightNewer); + break; + case FILE_TIME_INVALID: + if (dirs_.leftNewer == dirs_.rightNewer) //e.g. "Mirror" sync variant + file.setSyncDir(dirs_.leftNewer); + else + file.setSyncDirConflict(file.getCategoryCustomDescription()); + break; + case FILE_DIFFERENT_CONTENT: + if (dirs_.leftNewer == dirs_.rightNewer) + file.setSyncDir(dirs_.leftNewer); + else + file.setSyncDirConflict(txtDiffContent_); + break; + case FILE_CONFLICT: + file.setSyncDirConflict(file.getCategoryCustomDescription()); //take over category conflict: allow *manual* resolution only! + break; + } + } + + void processLink(SymlinkPair& symlink) const + { + switch (symlink.getLinkCategory()) + { + case SYMLINK_EQUAL: + //symlink.setSyncDir(SyncDirection::none); + break; + case SYMLINK_RENAMED: + if (dirs_.leftNewer == dirs_.rightNewer) + symlink.setSyncDir(dirs_.leftNewer); + else + symlink.setSyncDirConflict(txtDiffName_); + break; + case SYMLINK_LEFT_ONLY: + symlink.setSyncDir(dirs_.leftOnly); + break; + case SYMLINK_RIGHT_ONLY: + symlink.setSyncDir(dirs_.rightOnly); + break; + case SYMLINK_LEFT_NEWER: + symlink.setSyncDir(dirs_.leftNewer); + break; + case SYMLINK_RIGHT_NEWER: + symlink.setSyncDir(dirs_.rightNewer); + break; + case SYMLINK_TIME_INVALID: + if (dirs_.leftNewer == dirs_.rightNewer) + symlink.setSyncDir(dirs_.leftNewer); + else + symlink.setSyncDirConflict(symlink.getCategoryCustomDescription()); + break; + case SYMLINK_DIFFERENT_CONTENT: + if (dirs_.leftNewer == dirs_.rightNewer) + symlink.setSyncDir(dirs_.leftNewer); + else + symlink.setSyncDirConflict(txtDiffContent_); + break; + case SYMLINK_CONFLICT: + symlink.setSyncDirConflict(symlink.getCategoryCustomDescription()); //take over category conflict: allow *manual* resolution only! + break; + } + } + + void processFolder(FolderPair& folder) const + { + const CompareDirResult cat = folder.getDirCategory(); + + //########### schedule abandoned temporary recycle bin directory for deletion ########## + if (cat == DIR_LEFT_ONLY && endsWith(folder.getItemName(), AFS::TEMP_FILE_ENDING)) + return setSyncDirectionRec(SyncDirection::left, folder); // + else if (cat == DIR_RIGHT_ONLY && endsWith(folder.getItemName(), AFS::TEMP_FILE_ENDING)) + return setSyncDirectionRec(SyncDirection::right, folder); //don't recurse below! + //####################################################################################### + + switch (cat) + { + case DIR_EQUAL: + //folder.setSyncDir(SyncDirection::none); + break; + case DIR_RENAMED: + if (dirs_.leftNewer == dirs_.rightNewer) + folder.setSyncDir(dirs_.leftNewer); + else + folder.setSyncDirConflict(txtDiffName_); + break; + case DIR_LEFT_ONLY: + folder.setSyncDir(dirs_.leftOnly); + break; + case DIR_RIGHT_ONLY: + folder.setSyncDir(dirs_.rightOnly); + break; + case DIR_CONFLICT: + folder.setSyncDirConflict(folder.getCategoryCustomDescription()); //take over category conflict: allow *manual* resolution only! + break; + } + + recurse(folder); + } + + const DirectionByDiff dirs_; + const Zstringc txtDiffName_ = utfTo(_("Cannot determine sync-direction:") + L'\n' + TAB_SPACE + + _("The items have different names, but it's unknown which side was renamed.")); + const Zstringc txtDiffContent_ = utfTo(_("Cannot determine sync-direction:") + L'\n' + TAB_SPACE + + _("The items have different content, but it's unknown which side has changed.")); +}; + +//--------------------------------------------------------------------------------------------------------------- + +//test if non-equal items exist in scanned data +bool allItemsCategoryEqual(const ContainerObject& conObj) +{ + for (const FilePair& file : conObj.files()) + if (file.getCategory() != FILE_EQUAL) + return false; + + for (const SymlinkPair& symlink : conObj.symlinks()) + if (symlink.getLinkCategory() != SYMLINK_EQUAL) + return false; + + for (const FolderPair& folder : conObj.subfolders()) + if (folder.getDirCategory() != DIR_EQUAL || !allItemsCategoryEqual(folder)) //short-circuit behavior! + return false; + + return true; +} +} + +bool fff::allElementsEqual(const FolderComparison& folderCmp) +{ + for (const BaseFolderPair& baseFolder : asRange(folderCmp)) + if (!allItemsCategoryEqual(baseFolder)) + return false; + + return true; +} + +//--------------------------------------------------------------------------------------------------------------- + +namespace +{ +template inline +CudAction compareDbEntry(const FilePair& file, const InSyncFile* dbFile, unsigned int fileTimeTolerance, + const std::vector& ignoreTimeShiftMinutes, bool renamedOrMoved) +{ + if (file.isEmpty()) + return dbFile ? (renamedOrMoved ? CudAction::update: CudAction::delete_) : CudAction::noChange; + else if (!dbFile) + return (renamedOrMoved ? CudAction::update : CudAction::create); + + const InSyncDescrFile& descrDb = selectParam(dbFile->left, dbFile->right); + + return sameFileTime(file.getLastWriteTime(), descrDb.modTime, fileTimeTolerance, ignoreTimeShiftMinutes) && + //- we do *not* consider file ID, but only *user-visual* changes. E.g. user moving data to some other medium should not be considered a change! + file.getFileSize() == dbFile->fileSize ? + CudAction::noChange : CudAction::update; +} + + +//check whether database entry is in sync considering *current* comparison settings +inline +bool stillInSync(const InSyncFile& dbFile, CompareVariant compareVar, unsigned int fileTimeTolerance, const std::vector& ignoreTimeShiftMinutes) +{ + switch (compareVar) + { + case CompareVariant::timeSize: + if (dbFile.cmpVar == CompareVariant::content) return true; //special rule: this is certainly "good enough" for CompareVariant::timeSize! + + //case-sensitive file name match is a database invariant! + return sameFileTime(dbFile.left.modTime, dbFile.right.modTime, fileTimeTolerance, ignoreTimeShiftMinutes); + + case CompareVariant::content: + //case-sensitive file name match is a database invariant! + return dbFile.cmpVar == CompareVariant::content; + //in contrast to comparison, we don't care about modification time here! + + case CompareVariant::size: //file size/case-sensitive file name always matches on both sides for an "in-sync" database entry + return true; + } + assert(false); + return false; +} + +//-------------------------------------------------------------------- + +//check whether database entry and current item match: *irrespective* of current comparison settings +template inline +CudAction compareDbEntry(const SymlinkPair& symlink, const InSyncSymlink* dbSymlink, unsigned int fileTimeTolerance, + const std::vector& ignoreTimeShiftMinutes, bool renamedOrMoved) +{ + if (symlink.isEmpty()) + return dbSymlink ? (renamedOrMoved ? CudAction::update: CudAction::delete_) : CudAction::noChange; + else if (!dbSymlink) + return (renamedOrMoved ? CudAction::update : CudAction::create); + + const InSyncDescrLink& descrDb = selectParam(dbSymlink->left, dbSymlink->right); + + return sameFileTime(symlink.getLastWriteTime(), descrDb.modTime, fileTimeTolerance, ignoreTimeShiftMinutes) ? + CudAction::noChange : CudAction::update; +} + + +//check whether database entry is in sync considering *current* comparison settings +inline +bool stillInSync(const InSyncSymlink& dbLink, CompareVariant compareVar, unsigned int fileTimeTolerance, const std::vector& ignoreTimeShiftMinutes) +{ + switch (compareVar) + { + case CompareVariant::timeSize: + if (dbLink.cmpVar == CompareVariant::content || dbLink.cmpVar == CompareVariant::size) + return true; //special rule: this is already "good enough" for CompareVariant::timeSize! + + //case-sensitive symlink name match is a database invariant! + return sameFileTime(dbLink.left.modTime, dbLink.right.modTime, fileTimeTolerance, ignoreTimeShiftMinutes); + + case CompareVariant::content: + case CompareVariant::size: //== categorized by content! see comparison.cpp, ComparisonBuffer::compareBySize() + //case-sensitive symlink name match is a database invariant! + return dbLink.cmpVar == CompareVariant::content || dbLink.cmpVar == CompareVariant::size; + } + assert(false); + return false; +} + +//-------------------------------------------------------------------- + +//check whether database entry and current item match: *irrespective* of current comparison settings +template inline +CudAction compareDbEntry(const FolderPair& folder, const InSyncFolder* dbFolder, bool renamedOrMoved) +{ + if (folder.isEmpty()) + return dbFolder ? (renamedOrMoved ? CudAction::update: CudAction::delete_) : CudAction::noChange; + else if (!dbFolder) + return (renamedOrMoved ? CudAction::update : CudAction::create); + + return CudAction::noChange; +} + + +inline +bool stillInSync(const InSyncFolder& dbFolder) +{ + //case-sensitive folder name match is a database invariant! + return true; +} + +//---------------------------------------------------------------------------------------------- + +class DetectMovedFiles +{ +public: + static void execute(BaseFolderPair& baseFolder, const InSyncFolder& dbFolder) + { + DetectMovedFiles(baseFolder, dbFolder); + baseFolder.removeDoubleEmpty(); //see findAndSetMovePair() + } + +private: + DetectMovedFiles(BaseFolderPair& baseFolder, const InSyncFolder& dbFolder) : + cmpVar_ (baseFolder.getCompVariant()), + fileTimeTolerance_(baseFolder.getFileTimeTolerance()), + ignoreTimeShiftMinutes_(baseFolder.getIgnoredTimeShift()) + { + recurse(baseFolder, &dbFolder, &dbFolder); + + purgeDuplicates(filesL_, exLeftOnlyById_); + purgeDuplicates(filesR_, exRightOnlyById_); + + if ((!exLeftOnlyById_ .empty() || !exLeftOnlyByPath_ .empty()) && + (!exRightOnlyById_.empty() || !exRightOnlyByPath_.empty())) + detectMovePairs(dbFolder); + } + + void recurse(ContainerObject& conObj, const InSyncFolder* dbFolderL, const InSyncFolder* dbFolderR) + { + for (FilePair& file : conObj.files()) + { + file.setMovePair(nullptr); //discard remnants from previous move detection and start fresh (e.g. consider manual folder rename) + + const AFS::FingerPrint filePrintL = file.isEmpty() ? 0 : file.getFilePrint(); + const AFS::FingerPrint filePrintR = file.isEmpty() ? 0 : file.getFilePrint(); + + if (filePrintL != 0) filesL_.push_back(&file); //collect *all* prints for uniqueness check! + if (filePrintR != 0) filesR_.push_back(&file); // + + auto getDbEntry = [](const InSyncFolder* dbFolder, const Zstring& fileName) -> const InSyncFile* + { + if (dbFolder) + if (const auto it = dbFolder->files.find(fileName); + it != dbFolder->files.end()) + return &it->second; + return nullptr; + }; + + if (const CompareFileResult cat = file.getCategory(); + cat == FILE_LEFT_ONLY) + { + if (const InSyncFile* dbEntry = getDbEntry(dbFolderL, file.getItemName())) + exLeftOnlyByPath_.emplace(dbEntry, &file); + } + else if (cat == FILE_RIGHT_ONLY) + { + if (const InSyncFile* dbEntry = getDbEntry(dbFolderR, file.getItemName())) + exRightOnlyByPath_.emplace(dbEntry, &file); + } + } + + for (FolderPair& folder : conObj.subfolders()) + { + auto getDbEntry = [](const InSyncFolder* dbFolder, const ZstringNorm& folderName) -> const InSyncFolder* + { + if (dbFolder) + if (const auto it = dbFolder->folders.find(folderName); + it != dbFolder->folders.end()) + return &it->second; + return nullptr; + }; + const ZstringNorm itemNameL = folder.getItemName(); + const ZstringNorm itemNameR = folder.getItemName(); + + const InSyncFolder* dbEntryL = getDbEntry(dbFolderL, itemNameL); + const InSyncFolder* dbEntryR = dbFolderL == dbFolderR && itemNameL == itemNameR ? + dbEntryL : getDbEntry(dbFolderR, itemNameR); + + recurse(folder, dbEntryL, dbEntryR); + } + } + + template + static void purgeDuplicates(std::vector& files, + std::unordered_map& exOneSideById) + { + if (!files.empty()) + { + std::sort(files.begin(), files.end(), [](const FilePair* lhs, const FilePair* rhs) + { return lhs->getFilePrint() < rhs->getFilePrint(); }); + + AFS::FingerPrint prevPrint = files[0]->getFilePrint(); + + for (auto it = files.begin() + 1; it != files.end(); ++it) + if (const AFS::FingerPrint filePrint = (*it)->getFilePrint(); + prevPrint != filePrint) + prevPrint = filePrint; + else //duplicate file ID! NTFS hard link/symlink? + { + const auto dupFirst = it - 1; + const auto dupLast = std::find_if(it + 1, files.end(), [prevPrint](const FilePair* file) + { return file->getFilePrint() != prevPrint; }); + + //remove from model: do *not* store invalid file prints in sync.ffs_db! + std::for_each(dupFirst, dupLast, [](FilePair* file) { file->clearFilePrint(); }); + it = dupLast - 1; + } + + //collect unique file prints for files existing on one side only: + constexpr CompareFileResult oneSideOnlyTag = side == SelectSide::left ? FILE_LEFT_ONLY : FILE_RIGHT_ONLY; + + for (FilePair* file : files) + if (file->getCategory() == oneSideOnlyTag) + if (const AFS::FingerPrint filePrint = file->getFilePrint(); + filePrint != 0) //skip duplicates marked by clearFilePrint() + exOneSideById.emplace(filePrint, file); + } + } + + void detectMovePairs(const InSyncFolder& container) const + { + for (const auto& [fileName, dbAttrib] : container.files) + findAndSetMovePair(dbAttrib); + + for (const auto& [folderName, subFolder] : container.folders) + detectMovePairs(subFolder); + } + + template + static bool sameSizeAndDate(const FilePair& file, const InSyncFile& dbFile) + { + return file.getFileSize() == dbFile.fileSize && + file.getLastWriteTime() == selectParam(dbFile.left, dbFile.right).modTime; + /* do NOT consider FAT_FILE_TIME_PRECISION_SEC: + 1. if DB contains file metadata collected during folder comparison we can be as precise as we want here + 2. if DB contains file metadata *estimated* directly after file copy: + - most file systems store file times with sub-second precision... + - ...except for FAT, but FAT does not have stable file IDs after file copy anyway (see comment below) + => file time comparison with seconds precision is fine! + + PS: *never* allow a tolerance as container predicate!! + => no strict weak ordering relation! reason: no transitivity of equivalence! */ + } + + template + FilePair* getAssocFilePair(const InSyncFile& dbFile) const + { + const std::unordered_map& exOneSideByPath = selectParam(exLeftOnlyByPath_, exRightOnlyByPath_); + const std::unordered_map& exOneSideById = selectParam(exLeftOnlyById_, exRightOnlyById_); + + if (const auto it = exOneSideByPath.find(&dbFile); + it != exOneSideByPath.end()) + return it->second; //if there is an association by path, don't care if there is also an association by ID, + //even if the association by path doesn't match time and size while the association by ID does! + //there doesn't seem to be (any?) value in allowing this! + + if (const AFS::FingerPrint filePrint = selectParam(dbFile.left, dbFile.right).filePrint; + filePrint != 0) + if (const auto it = exOneSideById.find(filePrint); + it != exOneSideById.end()) + return it->second; + + return nullptr; + } + + void findAndSetMovePair(const InSyncFile& dbFile) const + { + if (stillInSync(dbFile, cmpVar_, fileTimeTolerance_, ignoreTimeShiftMinutes_)) + if (FilePair* fileLeftOnly = getAssocFilePair(dbFile)) + if (sameSizeAndDate(*fileLeftOnly, dbFile)) + if (FilePair* fileRightOnly = getAssocFilePair(dbFile)) + if (sameSizeAndDate(*fileRightOnly, dbFile)) + { + if (!fileLeftOnly ->getMovePair() && //needless checks? (file prints are unique in this context) + !fileRightOnly->getMovePair() && // + fileLeftOnly ->getCategory() == FILE_LEFT_ONLY && //is it possible we could get conflicting matches!? + fileRightOnly->getCategory() == FILE_RIGHT_ONLY) //=> likely 'yes', but only in obscure cases + //--------------- found a match --------------- + { + //move pair is just a 'rename' => combine: + if (&fileLeftOnly->parent() == &fileRightOnly->parent()) + { + fileLeftOnly->setSyncedTo(fileLeftOnly->getFileSize(), + fileRightOnly->getLastWriteTime(), //lastWriteTimeTrg + fileLeftOnly ->getLastWriteTime(), //lastWriteTimeSrc + + fileRightOnly->getFilePrint(), //filePrintTrg + fileLeftOnly ->getFilePrint(), //filePrintSrc + + fileRightOnly->isFollowedSymlink(), //isSymlinkTrg + fileLeftOnly ->isFollowedSymlink()); //isSymlinkSrc + + fileLeftOnly->setItemName(fileRightOnly->getItemName()); + + assert(fileLeftOnly->isActive() && fileRightOnly->isActive()); //can this fail? excluded files are not added during comparison... + if (fileLeftOnly->isActive() != fileRightOnly->isActive()) //just in case + fileLeftOnly->setActive(false); + + fileRightOnly->removeItem(); //=> call ContainerObject::removeDoubleEmpty() later! + } + else //regular move pair: mark it! + fileLeftOnly->setMovePair(fileRightOnly); + } + else + assert(fileLeftOnly->getMovePair() == fileRightOnly); + } + } + + const CompareVariant cmpVar_; + const unsigned int fileTimeTolerance_; + const std::vector ignoreTimeShiftMinutes_; + + std::vector filesL_; //collection of *all* file items (with non-null filePrint) + std::vector filesR_; // => detect duplicate file IDs + + std::unordered_map exLeftOnlyById_; + std::unordered_map exRightOnlyById_; + + std::unordered_map exLeftOnlyByPath_; + std::unordered_map exRightOnlyByPath_; + + /* Detect Renamed Files: + + X -> |_| Create right + |_| -> Y Delete right + + resolve as: Move/Rename Y to X on right + + Algorithm: + ---------- + DB-file left <--- (name, size, date) ---> DB-file right + | | + | (file ID, size, date) | (file ID, size, date) + | or | or + | (file path, size, date) | (file path, size, date) + \|/ \|/ + file left only file right only + + FAT caveat: file IDs are generally not stable when file is either moved or renamed! + 1. Move/rename operations on FAT cannot be detected reliably. + 2. database generally contains wrong file ID on FAT after renaming from .ffs_tmp files => correct file IDs in database only after next sync + 3. even exFAT screws up (but less than FAT) and changes IDs after file move. Did they learn nothing from the past? */ +}; + +//---------------------------------------------------------------------------------------------- + +class SetSyncDirViaChanges +{ +public: + static void execute(BaseFolderPair& baseFolder, const InSyncFolder& dbFolder, const DirectionByChange& dirs) + { SetSyncDirViaChanges(baseFolder, dbFolder, dirs); } + +private: + SetSyncDirViaChanges(BaseFolderPair& baseFolder, const InSyncFolder& dbFolder, const DirectionByChange& dirs) : + dirs_(dirs), + cmpVar_ (baseFolder.getCompVariant()), + fileTimeTolerance_ (baseFolder.getFileTimeTolerance()), + ignoreTimeShiftMinutes_(baseFolder.getIgnoredTimeShift()) + { + //-> considering filter not relevant: + // if stricter filter than last time: all ok; + // if less strict filter (if file ex on both sides -> conflict, fine; if file ex. on one side: copy to other side: fine) + recurse(baseFolder, &dbFolder); + } + + void recurse(ContainerObject& conObj, const InSyncFolder* dbFolder) const + { + for (FilePair& file : conObj.files()) + processFile(file, dbFolder); + for (SymlinkPair& symlink : conObj.symlinks()) + processSymlink(symlink, dbFolder); + for (FolderPair& folder : conObj.subfolders()) + processDir(folder, dbFolder); + } + + void processFile(FilePair& file, const InSyncFolder* dbFolder) const + { + const CompareFileResult cat = file.getCategory(); + if (cat == FILE_EQUAL) + return; + else if (cat == FILE_CONFLICT) //take over category conflict: allow *manual* resolution only! + return file.setSyncDirConflict(file.getCategoryCustomDescription()); + + //##################### schedule old temporary files for deletion #################### + if (cat == FILE_LEFT_ONLY && endsWith(file.getItemName(), AFS::TEMP_FILE_ENDING)) + return file.setSyncDir(SyncDirection::left); + else if (cat == FILE_RIGHT_ONLY && endsWith(file.getItemName(), AFS::TEMP_FILE_ENDING)) + return file.setSyncDir(SyncDirection::right); + //#################################################################################### + + //try to find corresponding database entry + auto getDbEntry = [dbFolder](const ZstringNorm& fileName) -> const InSyncFile* + { + if (dbFolder) + if (auto it = dbFolder->files.find(fileName); + it != dbFolder->files.end()) + return &it->second; + return nullptr; + }; + const ZstringNorm itemNameL = file.getItemName(); + const ZstringNorm itemNameR = file.getItemName(); + + const InSyncFile* dbEntryL = getDbEntry(itemNameL); + const InSyncFile* dbEntryR = itemNameL == itemNameR ? dbEntryL : getDbEntry(itemNameR); + + if (dbEntryL && dbEntryR && dbEntryL != dbEntryR) //conflict: which db entry to use? + return file.setSyncDirConflict(txtDbAmbiguous_); + + if (const InSyncFile* dbEntry = dbEntryL ? dbEntryL : dbEntryR; + dbEntry && !stillInSync(*dbEntry, cmpVar_, fileTimeTolerance_, ignoreTimeShiftMinutes_)) //check *before* misleadingly reporting txtNoSideChanged_ + return file.setSyncDirConflict(txtDbNotInSync_); + + //consider renamed/moved files as "updated" with regards to "changes"-based sync settings: https://freefilesync.org/forum/viewtopic.php?t=10594 + const bool renamedOrMoved = cat == FILE_RENAMED || file.getMovePair(); + const CudAction changeL = compareDbEntry(file, dbEntryL, fileTimeTolerance_, ignoreTimeShiftMinutes_, renamedOrMoved); + const CudAction changeR = compareDbEntry(file, dbEntryR, fileTimeTolerance_, ignoreTimeShiftMinutes_, renamedOrMoved); + + setSyncDirForChange(file, changeL, changeR); + } + + void processSymlink(SymlinkPair& symlink, const InSyncFolder* dbFolder) const + { + const CompareSymlinkResult cat = symlink.getLinkCategory(); + if (cat == SYMLINK_EQUAL) + return; + else if (cat == SYMLINK_CONFLICT) //take over category conflict: allow *manual* resolution only! + return symlink.setSyncDirConflict(symlink.getCategoryCustomDescription()); + + //try to find corresponding database entry + auto getDbEntry = [dbFolder](const ZstringNorm& linkName) -> const InSyncSymlink* + { + if (dbFolder) + if (auto it = dbFolder->symlinks.find(linkName); + it != dbFolder->symlinks.end()) + return &it->second; + return nullptr; + }; + const ZstringNorm itemNameL = symlink.getItemName(); + const ZstringNorm itemNameR = symlink.getItemName(); + + const InSyncSymlink* dbEntryL = getDbEntry(itemNameL); + const InSyncSymlink* dbEntryR = itemNameL == itemNameR ? dbEntryL : getDbEntry(itemNameR); + + if (dbEntryL && dbEntryR && dbEntryL != dbEntryR) //conflict: which db entry to use? + return symlink.setSyncDirConflict(txtDbAmbiguous_); + + if (const InSyncSymlink* dbEntry = dbEntryL ? dbEntryL : dbEntryR; + dbEntry && !stillInSync(*dbEntry, cmpVar_, fileTimeTolerance_, ignoreTimeShiftMinutes_)) + return symlink.setSyncDirConflict(txtDbNotInSync_); + + const bool renamedOrMoved = cat == SYMLINK_RENAMED; + const CudAction changeL = compareDbEntry(symlink, dbEntryL, fileTimeTolerance_, ignoreTimeShiftMinutes_, renamedOrMoved); + const CudAction changeR = compareDbEntry(symlink, dbEntryR, fileTimeTolerance_, ignoreTimeShiftMinutes_, renamedOrMoved); + + setSyncDirForChange(symlink, changeL, changeR); + } + + void processDir(FolderPair& folder, const InSyncFolder* dbFolder) const + { + const CompareDirResult cat = folder.getDirCategory(); + + //########### schedule abandoned temporary recycle bin directory for deletion ########## + if (cat == DIR_LEFT_ONLY && endsWith(folder.getItemName(), AFS::TEMP_FILE_ENDING)) + return setSyncDirectionRec(SyncDirection::left, folder); // + else if (cat == DIR_RIGHT_ONLY && endsWith(folder.getItemName(), AFS::TEMP_FILE_ENDING)) + return setSyncDirectionRec(SyncDirection::right, folder); //don't recurse below! + //####################################################################################### + + //try to find corresponding database entry + auto getDbEntry = [dbFolder](const ZstringNorm& folderName) -> const InSyncFolder* + { + if (dbFolder) + if (auto it = dbFolder->folders.find(folderName); + it != dbFolder->folders.end()) + return &it->second; + return nullptr; + }; + + const ZstringNorm itemNameL = folder.getItemName(); + const ZstringNorm itemNameR = folder.getItemName(); + + const InSyncFolder* dbEntryL = getDbEntry(itemNameL); + const InSyncFolder* dbEntryR = itemNameL == itemNameR ? dbEntryL : getDbEntry(itemNameR); + + if (dbEntryL && dbEntryR && dbEntryL != dbEntryR) //conflict: which db entry to use? + { + auto onFsItem = [&](FileSystemObject& fsObj) + { + if (fsObj.getCategory() != FILE_EQUAL) + fsObj.setSyncDirConflict(txtDbAmbiguous_); + }; + return visitFSObjectRecursively(static_cast(folder), onFsItem, onFsItem, onFsItem); + } + const InSyncFolder* dbEntry = dbEntryL ? dbEntryL : dbEntryR; //exactly one side nullptr? => change in upper/lower case! + + if (cat == DIR_EQUAL) + ; + else if (cat == DIR_CONFLICT) //take over category conflict: allow *manual* resolution only! + folder.setSyncDirConflict(folder.getCategoryCustomDescription()); + else + { + if (dbEntry && !stillInSync(*dbEntry)) + folder.setSyncDirConflict(txtDbNotInSync_); + else + { + const bool renamedOrMoved = cat == DIR_RENAMED; + const CudAction changeL = compareDbEntry(folder, dbEntryL, renamedOrMoved); + const CudAction changeR = compareDbEntry(folder, dbEntryR, renamedOrMoved); + + setSyncDirForChange(folder, changeL, changeR); + } + } + + recurse(folder, dbEntry); + } + + template + SyncDirection getSyncDirForChange(CudAction change) const + { + const auto& changedirs = selectParam(dirs_.left, dirs_.right); + switch (change) + { + case CudAction::noChange: return SyncDirection::none; + case CudAction::create: return changedirs.create; + case CudAction::update: return changedirs.update; + case CudAction::delete_: return changedirs.delete_; + } + throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); + } + + void setSyncDirForChange(FileSystemObject& fsObj, CudAction changeL, CudAction changeR) const + { + const SyncDirection dirL = getSyncDirForChange(changeL); + const SyncDirection dirR = getSyncDirForChange(changeR); + if (changeL != CudAction::noChange) + { + if (changeR != CudAction::noChange) //both sides changed + { + if (dirL == dirR) //but luckily agree on direction + fsObj.setSyncDir(dirL); + else + fsObj.setSyncDirConflict(txtBothSidesChanged_); + } + else //change on left + fsObj.setSyncDir(dirL); + } + else + { + if (changeR != CudAction::noChange) //change on right + fsObj.setSyncDir(dirR); + else //no change on either side + fsObj.setSyncDirConflict(txtNoSideChanged_); //obscure, but possible if user widens "fileTimeTolerance" + } + } + + //need ref-counted strings! see FileSystemObject::syncDirectionConflict_ + const Zstringc txtBothSidesChanged_ = utfTo(_("Both sides have changed since last synchronization.")); + const Zstringc txtNoSideChanged_ = utfTo(_("Cannot determine sync-direction:") + L'\n' + TAB_SPACE + _("No change since last synchronization.")); + const Zstringc txtDbNotInSync_ = utfTo(_("Cannot determine sync-direction:") + L'\n' + TAB_SPACE + _("The database entry is not in sync, considering current settings.")); + const Zstringc txtDbAmbiguous_ = utfTo(_("Cannot determine sync-direction:") + L'\n' + TAB_SPACE + _("The database entry is ambiguous.")); + + const DirectionByChange dirs_; + const CompareVariant cmpVar_; + const unsigned int fileTimeTolerance_; + const std::vector ignoreTimeShiftMinutes_; +}; +} + + +std::vector> fff::extractDirectionCfg(FolderComparison& folderCmp, const MainConfiguration& mainCfg) +{ + if (folderCmp.empty()) + return {}; + + //merge first and additional pairs + std::vector allPairs; + allPairs.push_back(mainCfg.firstPair); + allPairs.insert(allPairs.end(), + mainCfg.additionalPairs.begin(), //add additional pairs + mainCfg.additionalPairs.end()); + + if (folderCmp.size() != allPairs.size()) + throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); + + std::vector> output; + + for (auto it = folderCmp.begin(); it != folderCmp.end(); ++it) + { + BaseFolderPair& baseFolder = it->ref(); + const LocalPairConfig& lpc = allPairs[it - folderCmp.begin()]; + + output.emplace_back(&baseFolder, lpc.localSyncCfg ? lpc.localSyncCfg->directionCfg : mainCfg.syncCfg.directionCfg); + } + return output; +} + + +void fff::redetermineSyncDirection(const std::vector>& directCfgs, + PhaseCallback& callback /*throw X*/) //throw X +{ + if (directCfgs.empty()) + return; + + std::unordered_set pairsToSkip; + std::unordered_map> lastSyncStates; + + //best effort: always set sync directions (even on DB load error and when user cancels during file loading) + ZEN_ON_SCOPE_EXIT + ( + //*INDENT-OFF* + for (const auto& [baseFolder, dirCfg] : directCfgs) + if (!pairsToSkip.contains(baseFolder)) + { + //if only one folder is selected instead of a pair, sync directions don't make sense: (user already received warning during comparison) + if (AFS::isNullPath(baseFolder->getAbstractPath()) || + AFS::isNullPath(baseFolder->getAbstractPath())) + { + SetSyncDirViaDifferences::execute({.leftOnly = SyncDirection::none, + .rightOnly = SyncDirection::none, + .leftNewer = SyncDirection::none, + .rightNewer = SyncDirection::none}, *baseFolder); + } + else if (const DirectionByDiff* diffDirs = std::get_if(&dirCfg.dirs)) + SetSyncDirViaDifferences::execute(*diffDirs, *baseFolder); + else + { + const DirectionByChange& changeDirs = std::get(dirCfg.dirs); + + auto it = lastSyncStates.find(baseFolder); + if (const InSyncFolder* lastSyncState = it != lastSyncStates.end() ? &it->second.ref() : nullptr) + { + //detect moved files (*before* setting sync directions: might combine moved files into single file pairs, which changes category!) + DetectMovedFiles::execute(*baseFolder, *lastSyncState); + + SetSyncDirViaChanges::execute(*baseFolder, *lastSyncState, changeDirs); + } + else //fallback: + { + std::wstring msg = _("Database file is not available: Setting default directions for synchronization."); + if (directCfgs.size() > 1) + msg += SPACED_DASH + getShortDisplayNameForFolderPair(baseFolder->getAbstractPath(), + baseFolder->getAbstractPath()); + try { callback.logMessage(msg, PhaseCallback::MsgType::warning); /*throw X*/} catch (...) {}; + + SetSyncDirViaDifferences::execute(getDiffDirDefault(changeDirs), *baseFolder); + } + } + } + //*INDENT-ON* + ); + + std::vector baseFoldersForDbLoad; + for (const auto& [baseFolder, dirCfg] : directCfgs) + if (std::get_if(&dirCfg.dirs)) + { + if (allItemsCategoryEqual(*baseFolder)) //nothing to do: don't even try to open DB files + pairsToSkip.insert(baseFolder); + else + baseFoldersForDbLoad.push_back(baseFolder); + } + + //(try to) load sync-database files + lastSyncStates = loadLastSynchronousState(baseFoldersForDbLoad, + callback /*throw X*/); //throw X + + callback.updateStatus(_("Calculating sync directions...")); //throw X + callback.requestUiUpdate(true /*force*/); //throw X +} + +//--------------------------------------------------------------------------------------------------------------- + +void fff::setSyncDirectionRec(SyncDirection newDirection, FileSystemObject& fsObj) +{ + auto onFsItem = [newDirection](FileSystemObject& fsObj2) + { + if (fsObj2.getCategory() != FILE_EQUAL) + fsObj2.setSyncDir(newDirection); + }; + visitFSObjectRecursively(fsObj, onFsItem, onFsItem, onFsItem); +} + +//--------------- functions related to filtering ------------------------------------------------------------------------------------ + +void fff::setActiveStatus(bool newStatus, FolderComparison& folderCmp) +{ + auto onFsItem = [newStatus](FileSystemObject& fsObj) { fsObj.setActive(newStatus); }; + + for (BaseFolderPair& baseFolder : asRange(folderCmp)) + visitFSObjectRecursively(baseFolder, onFsItem, onFsItem, onFsItem); +} + + +void fff::setActiveStatus(bool newStatus, FileSystemObject& fsObj) +{ + auto onFsItem = [newStatus](FileSystemObject& fsObj2) { fsObj2.setActive(newStatus); }; + + visitFSObjectRecursively(fsObj, onFsItem, onFsItem, onFsItem); +} + + +namespace +{ +enum FilterStrategy +{ + STRATEGY_SET, + STRATEGY_AND + //STRATEGY_OR -> usage of inOrExcludeAllRows doesn't allow for strategy "or" +}; + +template struct Eval; + +template <> +struct Eval //process all elements +{ + template + static bool process(const T& obj) { return true; } +}; + +template <> +struct Eval +{ + template + static bool process(const T& obj) { return obj.isActive(); } +}; + + +template +class ApplyPathFilter +{ +public: + static void execute(ContainerObject& conObj, const PathFilter& filter) { ApplyPathFilter(conObj, filter); } + +private: + ApplyPathFilter(ContainerObject& conObj, const PathFilter& filter) : filter_(filter) { recurse(conObj); } + + void recurse(ContainerObject& conObj) const + { + for (FilePair& file : conObj.files()) + processFile(file); + for (SymlinkPair& symlink : conObj.symlinks()) + processLink(symlink); + for (FolderPair& folder : conObj.subfolders()) + processDir(folder); + } + + void processFile(FilePair& file) const + { + if (Eval::process(file)) + file.setActive(file.passFileFilter(filter_)); + } + + void processLink(SymlinkPair& symlink) const + { + if (Eval::process(symlink)) + symlink.setActive(symlink.passFileFilter(filter_)); + } + + void processDir(FolderPair& folder) const + { + bool childItemMightMatch = true; + const bool filterPassed = folder.passDirFilter(filter_, &childItemMightMatch); + + if (Eval::process(folder)) + folder.setActive(filterPassed); + + if (!childItemMightMatch) //use same logic like directory traversing: evaluate filter in subdirs only if objects *could* match + { + //exclude all files dirs in subfolders => incompatible with STRATEGY_OR! + auto onFsItem = [](FileSystemObject& fsObj) { fsObj.setActive(false); }; + visitFSObjectRecursively(static_cast(folder), onFsItem, onFsItem, onFsItem); + return; + } + + recurse(folder); + } + + const PathFilter& filter_; +}; + + +template +class ApplySoftFilter //falsify only! -> can run directly after "hard/base filter" +{ +public: + static void execute(ContainerObject& conObj, const SoftFilter& timeSizeFilter) { ApplySoftFilter(conObj, timeSizeFilter); } + +private: + ApplySoftFilter(ContainerObject& conObj, const SoftFilter& timeSizeFilter) : timeSizeFilter_(timeSizeFilter) { recurse(conObj); } + + void recurse(fff::ContainerObject& conObj) const + { + for (FilePair& file : conObj.files()) + processFile(file); + for (SymlinkPair& symlink : conObj.symlinks()) + processLink(symlink); + for (FolderPair& folder : conObj.subfolders()) + processDir(folder); + } + + void processFile(FilePair& file) const + { + if (Eval::process(file)) + { + if (file.isEmpty()) + file.setActive(matchSize(file) && + matchTime(file)); + else if (file.isEmpty()) + file.setActive(matchSize(file) && + matchTime(file)); + else + /* the only case with partially unclear semantics: + file and time filters may match or not match on each side, leaving a total of 16 combinations for both sides! + + ST S T - ST := match size and time + --------- S := match size only + ST |I|I|I|I| T := match time only + ------------ - := no match + S |I|E|?|E| + ------------ I := include row + T |I|?|E|E| E := exclude row + ------------ ? := unclear + - |I|E|E|E| + ------------ + let's set ? := E */ + file.setActive((matchSize(file) && + matchTime(file)) || + (matchSize(file) && + matchTime(file))); + } + } + + void processLink(SymlinkPair& symlink) const + { + if (Eval::process(symlink)) + { + if (symlink.isEmpty()) + symlink.setActive(matchTime(symlink)); + else if (symlink.isEmpty()) + symlink.setActive(matchTime(symlink)); + else + symlink.setActive(matchTime(symlink) || + matchTime (symlink)); + } + } + + void processDir(FolderPair& folder) const + { + if (Eval::process(folder)) + folder.setActive(timeSizeFilter_.matchFolder()); //if date filter is active we deactivate all folders: effectively gets rid of empty folders! + + recurse(folder); + } + + template + bool matchTime(const T& obj) const + { + return timeSizeFilter_.matchTime(obj.template getLastWriteTime()); + } + + template + bool matchSize(const T& obj) const + { + return timeSizeFilter_.matchSize(obj.template getFileSize()); + } + + const SoftFilter timeSizeFilter_; +}; +} + + +void fff::addHardFiltering(BaseFolderPair& baseFolder, const Zstring& excludeFilter) +{ + ApplyPathFilter::execute(baseFolder, NameFilter(FilterConfig().includeFilter, excludeFilter)); +} + + +void fff::addSoftFiltering(BaseFolderPair& baseFolder, const SoftFilter& timeSizeFilter) +{ + if (!timeSizeFilter.isNull()) //since we use STRATEGY_AND, we may skip a "null" filter + ApplySoftFilter::execute(baseFolder, timeSizeFilter); +} + + +void fff::applyFiltering(FolderComparison& folderCmp, const MainConfiguration& mainCfg) +{ + if (folderCmp.empty()) + return; + else if (folderCmp.size() != mainCfg.additionalPairs.size() + 1) + throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); + + //merge first and additional pairs + std::vector allPairs; + allPairs.push_back(mainCfg.firstPair); + allPairs.insert(allPairs.end(), + mainCfg.additionalPairs.begin(), //add additional pairs + mainCfg.additionalPairs.end()); + + for (auto it = allPairs.begin(); it != allPairs.end(); ++it) + { + BaseFolderPair& baseFolder = folderCmp[it - allPairs.begin()].ref(); + + const NormalizedFilter normFilter = normalizeFilters(mainCfg.globalFilter, it->localFilter); + + //"set" hard filter + ApplyPathFilter::execute(baseFolder, normFilter.nameFilter.ref()); + + //"and" soft filter + addSoftFiltering(baseFolder, normFilter.timeSizeFilter); + } +} + + +namespace +{ +template inline +bool matchesTime(const T& obj, time_t timeFrom, time_t timeTo) +{ + return timeFrom <= obj.template getLastWriteTime() && + /**/ obj.template getLastWriteTime() <= timeTo; +} +} + + +void fff::applyTimeSpanFilter(FolderComparison& folderCmp, time_t timeFrom, time_t timeTo) +{ + for (BaseFolderPair& baseFolder : asRange(folderCmp)) + { + visitFSObjectRecursively(baseFolder, [](FolderPair& folder) { folder.setActive(false); }, + + [timeFrom, timeTo](FilePair& file) + { + if (file.isEmpty()) + file.setActive(matchesTime(file, timeFrom, timeTo)); + else if (file.isEmpty()) + file.setActive(matchesTime(file, timeFrom, timeTo)); + else + file.setActive(matchesTime(file, timeFrom, timeTo) || + matchesTime(file, timeFrom, timeTo)); + }, + + [timeFrom, timeTo](SymlinkPair& symlink) + { + if (symlink.isEmpty()) + symlink.setActive(matchesTime(symlink, timeFrom, timeTo)); + else if (symlink.isEmpty()) + symlink.setActive(matchesTime(symlink, timeFrom, timeTo)); + else + symlink.setActive(matchesTime(symlink, timeFrom, timeTo) || + matchesTime (symlink, timeFrom, timeTo)); + }); + } +} + + +std::optional fff::getPathDependency(const AbstractPath& itemPathL, const AbstractPath& itemPathR) +{ + if (!AFS::isNullPath(itemPathL) && !AFS::isNullPath(itemPathR)) + { + if (itemPathL.afsDevice == itemPathR.afsDevice) + { + const std::vector relPathL = splitCpy(itemPathL.afsPath.value, FILE_NAME_SEPARATOR, SplitOnEmpty::skip); + const std::vector relPathR = splitCpy(itemPathR.afsPath.value, FILE_NAME_SEPARATOR, SplitOnEmpty::skip); + + const bool leftParent = relPathL.size() <= relPathR.size(); + + const auto& relPathP = leftParent ? relPathL : relPathR; + const auto& relPathC = leftParent ? relPathR : relPathL; + + if (std::equal(relPathP.begin(), relPathP.end(), relPathC.begin(), [](const Zstring& lhs, const Zstring& rhs) { return equalNoCase(lhs, rhs); })) + { + Zstring relDirPath; + std::for_each(relPathC.begin() + relPathP.size(), relPathC.end(), [&](const Zstring& itemName) + { + relDirPath = appendPath(relDirPath, itemName); + }); + + return PathDependency{leftParent ? itemPathL : itemPathR, relDirPath}; + } + } + } + return {}; +} + + +std::optional fff::getFolderPathDependency(const AbstractPath& folderPathL, const PathFilter& filterL, + const AbstractPath& folderPathR, const PathFilter& filterR) +{ + if (std::optional pd = getPathDependency(folderPathL, folderPathR)) + { + const PathFilter& filterP = pd->itemPathParent == folderPathL ? filterL : filterR; + //if there's a dependency, check if the sub directory is (fully) excluded via filter + //=> easy to check but still insufficient in general: + // - one folder may have a *.txt include-filter, the other a *.lng include filter => no dependencies, but "childItemMightMatch = true" below! + // - user may have manually excluded the conflicting items or changed the filter settings without running a re-compare + bool childItemMightMatch = true; + if (pd->relPath.empty() || filterP.passDirFilter(pd->relPath, &childItemMightMatch) || childItemMightMatch) + return pd; + } + return {}; +} + +//############################################################################################################ + +namespace +{ +template +void copyToAlternateFolderFrom(const std::vector& rowsToCopy, + const AbstractPath& targetFolderPath, + bool keepRelPaths, + bool overwriteIfExists, + ProcessCallback& callback /*throw X*/) //throw X +{ + auto reportItemInfo = [&](const std::wstring& msgTemplate, const AbstractPath& itemPath) //throw X + { + reportInfo(replaceCpy(msgTemplate, L"%x", fmtPath(AFS::getDisplayPath(itemPath))), callback); //throw X + }; + const std::wstring txtCreatingFile (_("Creating file %x" )); + const std::wstring txtCreatingFolder(_("Creating folder %x" )); + const std::wstring txtCreatingLink (_("Creating symbolic link %x")); + + auto copyItem = [&](const AbstractPath& targetPath, //throw FileError + const std::function& deleteTargetItem)>& copyItemPlain) //throw FileError + { + //start deleting existing target as required by copyFileTransactional(): + //best amortized performance if "already existing" is the most common case + std::exception_ptr deletionError; + auto tryDeleteTargetItem = [&] + { + if (overwriteIfExists) + try { AFS::removeFilePlain(targetPath); /*throw FileError*/ } + catch (FileError&) { deletionError = std::current_exception(); } //probably "not existing" error, defer evaluation + //else: copyFileTransactional() => undefined behavior! (e.g. fail/overwrite/auto-rename) + }; + + try + { + copyItemPlain(tryDeleteTargetItem); //throw FileError + } + catch (FileError&) + { + bool alreadyExisting = false; + try + { + AFS::getItemType(targetPath); //throw FileError + alreadyExisting = true; + } + catch (FileError&) {} //=> not yet existing (=> fine, no path issue) or access error: + //- let's pretend it doesn't happen :> if it does, worst case: the retry fails with (useless) already existing error + //- itemExists()? too expensive, considering that "already existing" is the most common case + + if (alreadyExisting) + { + if (deletionError) + std::rethrow_exception(deletionError); + throw; + } + + //parent folder missing => create + retry + //parent folder existing (maybe externally created shortly after copy attempt) => retry + if (const std::optional& targetParentPath = AFS::getParentPath(targetPath)) + AFS::createFolderIfMissingRecursion(*targetParentPath); //throw FileError + + //retry: + copyItemPlain(nullptr /*deleteTargetItem*/); //throw FileError + } + }; + + for (const FileSystemObject* fsObj : rowsToCopy) + tryReportingError([&] + { + const Zstring& relPath = keepRelPaths ? fsObj->getRelativePath() : fsObj->getItemName(); + const AbstractPath sourcePath = fsObj->getAbstractPath(); + const AbstractPath targetPath = AFS::appendRelPath(targetFolderPath, relPath); + + visitFSObject(*fsObj, [&](const FolderPair& folder) + { + ItemStatReporter statReporter(1, 0, callback); + reportItemInfo(txtCreatingFolder, targetPath); //throw X + + AFS::createFolderIfMissingRecursion(targetPath); //throw FileError + statReporter.reportDelta(1, 0); + //folder might already exist: see creation of intermediate directories below + }, + + [&](const FilePair& file) + { + ItemStatReporter statReporter(1, file.getFileSize(), callback); + reportItemInfo(txtCreatingFile, targetPath); //throw X + + std::wstring statusMsg = replaceCpy(txtCreatingFile, L"%x", fmtPath(AFS::getDisplayPath(targetPath))); + PercentStatReporter percentReporter(statusMsg, file.getFileSize(), statReporter); + + const FileAttributes attr = file.getAttributes(); + const AFS::StreamAttributes sourceAttr{attr.modTime, attr.fileSize, attr.filePrint}; + + copyItem(targetPath, [&](const std::function& deleteTargetItem) //throw FileError + { + //already existing + !overwriteIfExists: undefined behavior! (e.g. fail/overwrite/auto-rename) + const AFS::FileCopyResult result = AFS::copyFileTransactional(sourcePath, sourceAttr, targetPath, //throw FileError, ErrorFileLocked, X + false /*copyFilePermissions*/, true /*transactionalCopy*/, deleteTargetItem, + [&](int64_t bytesDelta) + { + percentReporter.updateDeltaAndStatus(bytesDelta); //throw X + callback.requestUiUpdate(); //throw X => not reliably covered by PercentStatReporter::updateDeltaAndStatus()! e.g. during first few seconds: STATUS_PERCENT_DELAY! + }); + + if (result.errorModTime) //log only; no popup + callback.logMessage(result.errorModTime->toString(), PhaseCallback::MsgType::warning); + }); + statReporter.reportDelta(1, 0); + }, + + [&](const SymlinkPair& symlink) + { + ItemStatReporter statReporter(1, 0, callback); + reportItemInfo(txtCreatingLink, targetPath); //throw X + + copyItem(targetPath, [&](const std::function& deleteTargetItem) //throw FileError + { + deleteTargetItem(); //throw FileError + AFS::copySymlink(sourcePath, targetPath, false /*copyFilePermissions*/); //throw FileError + }); + statReporter.reportDelta(1, 0); + }); + }, callback); //throw X +} +} + + +void fff::copyToAlternateFolder(const std::vector& selectionL, + const std::vector& selectionR, + const Zstring& targetFolderPathPhrase, + bool keepRelPaths, bool overwriteIfExists, + WarningDialogs& warnings, + ProcessCallback& callback /*throw X*/) //throw X +{ + assert(std::all_of(selectionL.begin(), selectionL.end(), [](const FileSystemObject* fsObj) { return !fsObj->isEmpty(); })); + assert(std::all_of(selectionR.begin(), selectionR.end(), [](const FileSystemObject* fsObj) { return !fsObj->isEmpty(); })); + + const int itemTotal = static_cast(selectionL.size() + selectionR.size()); + int64_t bytesTotal = 0; + + for (const FileSystemObject* fsObj : selectionL) + visitFSObject(*fsObj, [](const FolderPair& folder) {}, + [&](const FilePair& file) { bytesTotal += static_cast(file.getFileSize()); }, [](const SymlinkPair& symlink) {}); + + for (const FileSystemObject* fsObj : selectionR) + visitFSObject(*fsObj, [](const FolderPair& folder) {}, + [&](const FilePair& file) { bytesTotal += static_cast(file.getFileSize()); }, [](const SymlinkPair& symlink) {}); + + callback.initNewPhase(itemTotal, bytesTotal, ProcessPhase::none); //throw X + //------------------------------------------------------------------------------ + + const AbstractPath targetFolderPath = createAbstractPath(targetFolderPathPhrase); + + copyToAlternateFolderFrom(selectionL, targetFolderPath, keepRelPaths, overwriteIfExists, callback); + copyToAlternateFolderFrom(selectionR, targetFolderPath, keepRelPaths, overwriteIfExists, callback); +} + +//############################################################################################################ + +namespace +{ +template +void deleteFilesOneSide(const std::vector& rowsToDelete, + bool moveToRecycler, + bool& recyclerMissingReportOnce, + bool& warnRecyclerMissing, //WarningDialogs::warnRecyclerMissing + const std::unordered_map& baseFolderCfgs, + PhaseCallback& callback /*throw X*/) //throw X +{ + const std::wstring txtDelFilePermanent_ = _("Deleting file %x"); + const std::wstring txtDelFileRecycler_ = _("Moving file %x to the recycle bin"); + + const std::wstring txtDelSymlinkPermanent_ = _("Deleting symbolic link %x"); + const std::wstring txtDelSymlinkRecycler_ = _("Moving symbolic link %x to the recycle bin"); + + const std::wstring txtDelFolderPermanent_ = _("Deleting folder %x"); + const std::wstring txtDelFolderRecycler_ = _("Moving folder %x to the recycle bin"); + + auto removeFile = [&](const AbstractPath& filePath, ItemStatReporter& statReporter) + { + if (moveToRecycler) + try + { + reportInfo(replaceCpy(txtDelFileRecycler_, L"%x", fmtPath(AFS::getDisplayPath(filePath))), statReporter); //throw X + AFS::moveToRecycleBinIfExists(filePath); //throw FileError, RecycleBinUnavailable + } + catch (const RecycleBinUnavailable& e) + { + if (!recyclerMissingReportOnce) + { + recyclerMissingReportOnce = true; + callback.reportWarning(e.toString() + L"\n\n" + _("Ignore and delete permanently each time recycle bin is unavailable?"), warnRecyclerMissing); //throw X + } + callback.logMessage(replaceCpy(txtDelFilePermanent_, L"%x", fmtPath(AFS::getDisplayPath(filePath))) + + L" [" + _("Recycle bin unavailable") + L']', PhaseCallback::MsgType::warning); //throw X + AFS::removeFileIfExists(filePath); //throw FileError + } + else + { + reportInfo(replaceCpy(txtDelFilePermanent_, L"%x", fmtPath(AFS::getDisplayPath(filePath))), statReporter); //throw X + AFS::removeFileIfExists(filePath); //throw FileError + } + statReporter.reportDelta(1, 0); + }; + + auto removeSymlink = [&](const AbstractPath& symlinkPath, ItemStatReporter& statReporter) + { + if (moveToRecycler) + try + { + reportInfo(replaceCpy(txtDelSymlinkRecycler_, L"%x", fmtPath(AFS::getDisplayPath(symlinkPath))), statReporter); //throw X + AFS::moveToRecycleBinIfExists(symlinkPath); //throw FileError, RecycleBinUnavailable + } + catch (const RecycleBinUnavailable& e) + { + if (!recyclerMissingReportOnce) + { + recyclerMissingReportOnce = true; + callback.reportWarning(e.toString() + L"\n\n" + _("Ignore and delete permanently each time recycle bin is unavailable?"), warnRecyclerMissing); //throw X + } + callback.logMessage(replaceCpy(txtDelSymlinkPermanent_, L"%x", fmtPath(AFS::getDisplayPath(symlinkPath))) + + L" [" + _("Recycle bin unavailable") + L']', PhaseCallback::MsgType::warning); //throw X + AFS::removeSymlinkIfExists(symlinkPath); //throw FileError + } + else + { + reportInfo(replaceCpy(txtDelSymlinkPermanent_, L"%x", fmtPath(AFS::getDisplayPath(symlinkPath))), statReporter); //throw X + AFS::removeSymlinkIfExists(symlinkPath); //throw FileError + } + statReporter.reportDelta(1, 0); + }; + + auto removeFolder = [&](const AbstractPath& folderPath, ItemStatReporter& statReporter) + { + auto removeFolderPermanently = [&] + { + auto onBeforeDeletion = [&](const std::wstring& msgTemplate, const std::wstring& displayPath) + { + reportInfo(replaceCpy(msgTemplate, L"%x", fmtPath(displayPath)), statReporter); //throw X + statReporter.reportDelta(1, 0); //it would be more correct to report *after* work was done! + }; + + AFS::removeFolderIfExistsRecursion(folderPath, + [&](const std::wstring& displayPath) { onBeforeDeletion(txtDelFilePermanent_, displayPath); }, + [&](const std::wstring& displayPath) { onBeforeDeletion(txtDelSymlinkPermanent_, displayPath); }, + [&](const std::wstring& displayPath) { onBeforeDeletion(txtDelFolderPermanent_, displayPath); }); //throw FileError, X + }; + + if (moveToRecycler) + try + { + reportInfo(replaceCpy(txtDelFolderRecycler_, L"%x", fmtPath(AFS::getDisplayPath(folderPath))), statReporter); //throw X + AFS::moveToRecycleBinIfExists(folderPath); //throw FileError, RecycleBinUnavailable + statReporter.reportDelta(1, 0); + } + catch (const RecycleBinUnavailable& e) + { + if (!recyclerMissingReportOnce) + { + recyclerMissingReportOnce = true; + callback.reportWarning(e.toString() + L"\n\n" + _("Ignore and delete permanently each time recycle bin is unavailable?"), warnRecyclerMissing); //throw X + } + callback.logMessage(replaceCpy(txtDelFolderPermanent_, L"%x", fmtPath(AFS::getDisplayPath(folderPath))) + + L" [" + _("Recycle bin unavailable") + L']', PhaseCallback::MsgType::warning); //throw X + removeFolderPermanently(); //throw FileError, X + } + else + { + reportInfo(replaceCpy(txtDelFolderPermanent_, L"%x", fmtPath(AFS::getDisplayPath(folderPath))), statReporter); //throw X + removeFolderPermanently(); //throw FileError, X + } + }; + + + for (FileSystemObject* fsObj : rowsToDelete) //all pointers are required(!) to be bound + tryReportingError([&] + { + ItemStatReporter statReporter(1, 0, callback); + + if (!fsObj->isEmpty()) //element may be implicitly deleted, e.g. if parent folder was deleted first + { + visitFSObject(*fsObj, [&](FolderPair& folder) + { + if (folder.isFollowedSymlink()) + removeSymlink(folder.getAbstractPath(), statReporter); //throw FileError, X + else + removeFolder(folder.getAbstractPath(), statReporter); //throw FileError, X + + folder.removeItem(); //removes recursively! + }, + + [&](FilePair& file) + { + if (file.isFollowedSymlink()) + removeSymlink(file.getAbstractPath(), statReporter); //throw FileError, X + else + removeFile(file.getAbstractPath(), statReporter); //throw FileError, X + + file.removeItem(); + }, + + [&](SymlinkPair& symlink) + { + removeSymlink(symlink.getAbstractPath(), statReporter); //throw FileError, X + symlink.removeItem(); + }); + //------- no-throw from here on ------- + const CompareFileResult catOld = fsObj->getCategory(); + + //update sync direction: don't call redetermineSyncDirection() because user may have manually changed directions + if (catOld == CompareFileResult::FILE_EQUAL) + { + const SyncDirection newDir = [&] + { + const SyncDirectionConfig& dirCfg = baseFolderCfgs.find(&fsObj->base())->second; //not found? let it crash! + + if (const DirectionByDiff* diffDirs = std::get_if(&dirCfg.dirs)) + return side == SelectSide::left ? diffDirs->rightOnly : diffDirs->leftOnly; + else + { + const DirectionByChange& changeDirs = std::get(dirCfg.dirs); + return side == SelectSide::left ? changeDirs.left.delete_ : changeDirs.right.delete_; + } + }(); + + setSyncDirectionRec(newDir, *fsObj); //set new direction (recursively) + } + //else: keep old syncDir_ + } + }, callback); //throw X +} +} + + +void fff::deleteFiles(const std::vector& selectionL, + const std::vector& selectionR, + const std::vector>& directCfgs, + bool moveToRecycler, + bool& warnRecyclerMissing, + ProcessCallback& callback /*throw X*/) //throw X +{ + assert(std::all_of(selectionL.begin(), selectionL.end(), [](const FileSystemObject* fsObj) { return !fsObj->isEmpty(); })); + assert(std::all_of(selectionR.begin(), selectionR.end(), [](const FileSystemObject* fsObj) { return !fsObj->isEmpty(); })); + + const int itemCount = static_cast(selectionL.size() + selectionR.size()); + callback.initNewPhase(itemCount, 0, ProcessPhase::none); //throw X + //------------------------------------------------------------------------------ + + ZEN_ON_SCOPE_EXIT + ( + //*INDENT-OFF* + for (const auto& [baseFolder, dirCfg] : directCfgs) + baseFolder->removeDoubleEmpty(); + //*INDENT-ON* + ); + + //build up mapping from base directory to corresponding direction config + std::unordered_map baseFolderCfgs; + for (const auto& [baseFolder, dirCfg] : directCfgs) + baseFolderCfgs[baseFolder] = dirCfg; + + bool recyclerMissingReportOnce = false; + deleteFilesOneSide(selectionL, moveToRecycler, recyclerMissingReportOnce, warnRecyclerMissing, baseFolderCfgs, callback); //throw X + deleteFilesOneSide(selectionR, moveToRecycler, recyclerMissingReportOnce, warnRecyclerMissing, baseFolderCfgs, callback); // +} + +//############################################################################################################ + +namespace +{ +template +void renameItemsOneSide(const std::vector& selection, + const std::span newNames, + const std::unordered_map& baseFolderCfgs, + PhaseCallback& callback /*throw X*/) //throw X +{ + assert(selection.size() == newNames.size()); + + const std::wstring txtRenamingFileXtoY_ {_("Renaming file %x to %y")}; + const std::wstring txtRenamingLinkXtoY_ {_("Renaming symbolic link %x to %y")}; + const std::wstring txtRenamingFolderXtoY_{_("Renaming folder %x to %y")}; + + for (size_t i = 0; i < selection.size(); ++i) + tryReportingError([&] + { + FileSystemObject& fsObj = *selection[i]; + const Zstring& newName = newNames[i]; + + assert(!fsObj.isEmpty()); + + auto haveNameClash = [newNameNorm = getUnicodeNormalForm(newName)](const FileSystemObject& fsObj2) + { + return !fsObj2.isEmpty() && getUnicodeNormalForm(fsObj2.getItemName()) == newNameNorm; + }; + + const bool nameAlreadyExisting = [&] + { + for (const FilePair& file : fsObj.parent().files()) + if (haveNameClash(file)) + return true; + + for (const SymlinkPair& symlink : fsObj.parent().symlinks()) + if (haveNameClash(symlink)) + return true; + + for (const FolderPair& folder : fsObj.parent().subfolders()) + if (haveNameClash(folder)) + return true; + return false; + }(); + + //--------------------------------------------------------------- + ItemStatReporter statReporter(1, 0, callback); + + const std::wstring* txtRenamingXtoY_ = nullptr; + visitFSObject(fsObj, + [&](const FolderPair& folder) { txtRenamingXtoY_ = &txtRenamingFolderXtoY_; }, + [&](const FilePair& file) { txtRenamingXtoY_ = &txtRenamingFileXtoY_; }, + [&](const SymlinkPair& symlink) { txtRenamingXtoY_ = &txtRenamingLinkXtoY_; }); + + reportInfo(replaceCpy(replaceCpy(*txtRenamingXtoY_, L"%x", fmtPath(AFS::getDisplayPath(fsObj.getAbstractPath()))), + L"%y", fmtPath(newName)), statReporter); //throw X + + if (haveNameClash(fsObj)) + return assert(false); //theoretically possible, but practically showRenameDialog() won't return until there is an actual name change + + if (nameAlreadyExisting) //avoid inconsistent file model: expecting moveAndRenameItem() to fail (ERROR_ALREADY_EXISTS) is not good enough + return callback.reportFatalError(replaceCpy(replaceCpy(_("Cannot rename %x to %y."), + L"%x", fmtPath(AFS::getDisplayPath(fsObj.getAbstractPath()))), + L"%y", fmtPath(newName)) + L"\n\n" + + replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(newName))); //throw X + + AFS::moveAndRenameItem(fsObj.getAbstractPath(), + AFS::appendRelPath(fsObj.parent().getAbstractPath(), newName)); //throw FileError, (ErrorMoveUnsupported) + //------- no-throw from here on ------- + statReporter.reportDelta(1, 0); + + const CompareFileResult catOld = fsObj.getCategory(); + + fsObj.setItemName(newName); + +#warning("TODO: some users want to manually fix renamed folders/files: combine them here, don't require a re-compare!") + + //update sync direction: don't call redetermineSyncDirection() because user may have manually changed directions + if (catOld == CompareFileResult::FILE_EQUAL) + { + const SyncDirection newDir = [&] + { + const SyncDirectionConfig& dirCfg = baseFolderCfgs.find(&fsObj.base())->second; //not found? let it crash! + + if (const DirectionByDiff* diffDirs = std::get_if(&dirCfg.dirs)) + return side == SelectSide::left ? diffDirs->leftNewer : diffDirs->rightNewer; + else + { + const DirectionByChange& changeDirs = std::get(dirCfg.dirs); + return side == SelectSide::left ? changeDirs.left.update : changeDirs.right.update; + } + }(); + + fsObj.setSyncDir(newDir); //folder? => do not recurse! + } + //else: keep old syncDir_ + else if (fsObj.getCategory() == FILE_EQUAL) //edge-case, but possible + fsObj.setSyncDir(SyncDirection::none); //shouldn't matter, but avoids hitting some asserts + + }, callback); //throw X +} +} + + +void fff::renameItems(const std::vector& selectionL, + const std::span newNamesL, + const std::vector& selectionR, + const std::span newNamesR, + const std::vector>& directCfgs, + ProcessCallback& callback /*throw X*/) //throw X +{ + assert(std::all_of(selectionL.begin(), selectionL.end(), [](const FileSystemObject* fsObj) { return !fsObj->isEmpty(); })); + assert(std::all_of(selectionR.begin(), selectionR.end(), [](const FileSystemObject* fsObj) { return !fsObj->isEmpty(); })); + + const int itemCount = static_cast(selectionL.size() + selectionR.size()); + callback.initNewPhase(itemCount, 0, ProcessPhase::none); //throw X + //------------------------------------------------------------------------------ + + //build up mapping from base directory to corresponding direction config + std::unordered_map baseFolderCfgs; + for (const auto& [baseFolder, dirCfg] : directCfgs) + baseFolderCfgs[baseFolder] = dirCfg; + + renameItemsOneSide(selectionL, newNamesL, baseFolderCfgs, callback); //throw X + renameItemsOneSide(selectionR, newNamesR, baseFolderCfgs, callback); // +} + +//############################################################################################################ + +void fff::deleteListOfFiles(const std::vector& filesToDeletePaths, + std::vector& deletedPaths, + bool moveToRecycler, + bool& warnRecyclerMissing, + ProcessCallback& cb /*throw X*/) //throw X +{ + assert(deletedPaths.empty()); + + cb.initNewPhase(static_cast(filesToDeletePaths.size()), 0 /*bytesTotal*/, ProcessPhase::none); //throw X + + bool recyclerMissingReportOnce = false; + + for (const Zstring& filePath : filesToDeletePaths) + tryReportingError([&] + { + const AbstractPath cfgPath = createItemPathNative(filePath); + ItemStatReporter statReporter(1, 0, cb); + + if (moveToRecycler) + try + { + reportInfo(replaceCpy(_("Moving file %x to the recycle bin"), L"%x", fmtPath(AFS::getDisplayPath(cfgPath))), cb); //throw X + AFS::moveToRecycleBinIfExists(cfgPath); //throw FileError, RecycleBinUnavailable + } + catch (const RecycleBinUnavailable& e) + { + if (!recyclerMissingReportOnce) + { + recyclerMissingReportOnce = true; + cb.reportWarning(e.toString() + L"\n\n" + _("Ignore and delete permanently each time recycle bin is unavailable?"), warnRecyclerMissing); //throw X + } + cb.logMessage(replaceCpy(_("Deleting file %x"), L"%x", fmtPath(AFS::getDisplayPath(cfgPath))) + + L" [" + _("Recycle bin unavailable") + L']', PhaseCallback::MsgType::warning); //throw X + AFS::removeFileIfExists(cfgPath); //throw FileError + } + else + { + reportInfo(replaceCpy(_("Deleting file %x"), L"%x", fmtPath(AFS::getDisplayPath(cfgPath))), cb); //throw X + AFS::removeFileIfExists(cfgPath); //throw FileError + } + + statReporter.reportDelta(1, 0); + deletedPaths.push_back(filePath); + }, cb); //throw X +} + +//############################################################################################################ + +TempFileBuffer::~TempFileBuffer() +{ + if (!tempFolderPath_.empty()) + try + { + removeDirectoryPlainRecursion(tempFolderPath_); //throw FileError + } + catch (const FileError& e) { logExtraError(e.toString()); } +} + + +void TempFileBuffer::createTempFolderPath() //throw FileError +{ + if (tempFolderPath_.empty()) + { + //generate random temp folder path e.g. C:\Users\Zenju\AppData\Local\Temp\FFS-068b2e88 + const uint32_t shortGuid = getCrc32(generateGUID()); //no need for full-blown (pseudo-)random numbers for this one-time invocation + + const Zstring& tempPathTmp = appendPath(getTempFolderPath(), //throw FileError + Zstr("FFS-") + printNumber(Zstr("%08x"), static_cast(shortGuid))); + + createDirectoryIfMissingRecursion(tempPathTmp); //throw FileError + + tempFolderPath_ = tempPathTmp; + } +} + + +Zstring TempFileBuffer::getAndCreateFolderPath() //throw FileError +{ + createTempFolderPath(); //throw FileError + return tempFolderPath_; +} + + +//returns empty if not available (item not existing, error during copy) +Zstring TempFileBuffer::getTempPath(const FileDescriptor& descr) const +{ + auto it = tempFilePaths_.find(descr); + if (it != tempFilePaths_.end()) + return it->second; + return Zstring(); +} + + +void TempFileBuffer::createTempFiles(const std::set& workLoad, ProcessCallback& callback /*throw X*/) //throw X +{ + const int itemTotal = static_cast(workLoad.size()); + int64_t bytesTotal = 0; + + for (const FileDescriptor& descr : workLoad) + bytesTotal += descr.attr.fileSize; + + callback.initNewPhase(itemTotal, bytesTotal, ProcessPhase::none); //throw X + //------------------------------------------------------------------------------ + + const std::wstring errMsg = tryReportingError([&] + { + createTempFolderPath(); //throw FileError + }, callback); //throw X + if (!errMsg.empty()) return; + + for (const FileDescriptor& descr : workLoad) + { + assert(!tempFilePaths_.contains(descr)); //ensure correct stats, NO overwrite-copy => caller-contract! + + MemoryStreamOut cookie; //create hash to distinguish different versions and file locations + writeNumber (cookie, descr.attr.modTime); + writeNumber (cookie, descr.attr.fileSize); + writeNumber (cookie, descr.attr.filePrint); + writeNumber (cookie, descr.attr.isFollowedSymlink); + writeContainer(cookie, AFS::getInitPathPhrase(descr.path)); + + const uint16_t crc16 = getCrc16(cookie.ref()); + const Zstring descrHash = printNumber(Zstr("%04x"), static_cast(crc16)); + + const Zstring fileName = AFS::getItemName(descr.path); + + auto it = findLast(fileName.begin(), fileName.end(), Zstr('.')); //gracefully handle case of missing "." + const Zstring tempFileName = Zstring(fileName.begin(), it) + Zstr('~') + descrHash + Zstring(it, fileName.end()); + + const Zstring tempFilePath = appendPath(tempFolderPath_, tempFileName); + const AFS::StreamAttributes sourceAttr{descr.attr.modTime, descr.attr.fileSize, descr.attr.filePrint}; + + tryReportingError([&] + { + std::wstring statusMsg = replaceCpy(_("Creating file %x"), L"%x", fmtPath(tempFilePath)); + + ItemStatReporter statReporter(1, descr.attr.fileSize, callback); + PercentStatReporter percentReporter(statusMsg, descr.attr.fileSize, statReporter); + + reportInfo(std::move(statusMsg), callback); //throw X + + //already existing: undefined behavior! (e.g. fail/overwrite/auto-rename) + /*const AFS::FileCopyResult result =*/ + AFS::copyFileTransactional(descr.path, sourceAttr, //throw FileError, ErrorFileLocked, X + createItemPathNative(tempFilePath), + false /*copyFilePermissions*/, true /*transactionalCopy*/, nullptr /*onDeleteTargetFile*/, + [&](int64_t bytesDelta) + { + percentReporter.updateDeltaAndStatus(bytesDelta); //throw X + callback.requestUiUpdate(); //throw X => not reliably covered by PercentStatReporter::updateDeltaAndStatus()! e.g. during first few seconds: STATUS_PERCENT_DELAY! + }); + //result.errorModTime? => irrelevant for temp files! + statReporter.reportDelta(1, 0); + + tempFilePaths_[descr] = tempFilePath; + }, callback); //throw X + } +} diff --git a/FreeFileSync/Source/base/algorithm.h b/FreeFileSync/Source/base/algorithm.h new file mode 100644 index 0000000..e091644 --- /dev/null +++ b/FreeFileSync/Source/base/algorithm.h @@ -0,0 +1,111 @@ +// ***************************************************************************** +// * 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 ALGORITHM_H_34218518475321452548 +#define ALGORITHM_H_34218518475321452548 + +#include +#include "structures.h" +#include "file_hierarchy.h" +#include "soft_filter.h" +#include "process_callback.h" + + +namespace fff +{ +void swapGrids(const MainConfiguration& mainCfg, FolderComparison& folderCmp, + PhaseCallback& callback /*throw X*/); //throw X + +std::vector> extractDirectionCfg(FolderComparison& folderCmp, const MainConfiguration& mainCfg); + +void redetermineSyncDirection(const std::vector>& directCfgs, + PhaseCallback& callback /*throw X*/); //throw X + +void setSyncDirectionRec(SyncDirection newDirection, FileSystemObject& fsObj); //set new direction (recursively) + +bool allElementsEqual(const FolderComparison& folderCmp); + +//filtering +void applyFiltering (FolderComparison& folderCmp, const MainConfiguration& mainCfg); //full filter apply +void addHardFiltering(BaseFolderPair& baseFolder, const Zstring& excludeFilter); //exclude additional entries only +void addSoftFiltering(BaseFolderPair& baseFolder, const SoftFilter& timeSizeFilter); //exclude additional entries only + +void applyTimeSpanFilter(FolderComparison& folderCmp, time_t timeFrom, time_t timeTo); //overwrite current active/inactive settings + +void setActiveStatus(bool newStatus, FolderComparison& folderCmp); //activate or deactivate all rows +void setActiveStatus(bool newStatus, FileSystemObject& fsObj); //activate or deactivate row: (not recursively anymore) + +struct PathDependency +{ + AbstractPath itemPathParent; + Zstring relPath; //filled if child path is subfolder of parent path; empty if child path == parent path +}; +std::optional getPathDependency(const AbstractPath& itemPathL, const AbstractPath& itemPathR); +std::optional getFolderPathDependency(const AbstractPath& folderPathL, const PathFilter& filterL, + const AbstractPath& folderPathR, const PathFilter& filterR); + +//manual copy to alternate folder: +void copyToAlternateFolder(const std::vector& selectionL, //all pointers need to be bound and !isEmpty! + const std::vector& selectionR, // + const Zstring& targetFolderPathPhrase, + bool keepRelPaths, bool overwriteIfExists, + WarningDialogs& warnings, + ProcessCallback& callback /*throw X*/); //throw X + +//manual deletion of files on main grid +void deleteFiles(const std::vector& selectionL, //all pointers need to be bound and !isEmpty! + const std::vector& selectionR, //refresh GUI grid after deletion to remove invalid rows + const std::vector>& directCfgs, //attention: rows will be physically deleted! + bool moveToRecycler, + bool& warnRecyclerMissing, + ProcessCallback& callback /*throw X*/); //throw X + +void renameItems(const std::vector& selectionL, //all pointers need to be bound and !isEmpty! + const std::span newNamesL, + const std::vector& selectionR, //refresh GUI grid after deletion to remove invalid rows + const std::span newNamesR, + const std::vector>& directCfgs, + ProcessCallback& callback /*throw X*/); //throw X + +void deleteListOfFiles(const std::vector& filesToDeletePaths, + std::vector& deletedPaths, + bool moveToRecycler, + bool& warnRecyclerMissing, + ProcessCallback& callback /*throw X*/); //throw X + +struct FileDescriptor +{ + AbstractPath path; + FileAttributes attr; + + std::weak_ordering operator<=>(const FileDescriptor&) const = default; +}; + +//get native Win32 paths or create temporary copy for SFTP/MTP, etc. +class TempFileBuffer +{ +public: + TempFileBuffer() {} + ~TempFileBuffer(); + + Zstring getAndCreateFolderPath(); //throw FileError + + Zstring getTempPath(const FileDescriptor& descr) const; //returns empty if not in buffer (item not existing, error during copy) + + //contract: only add files not yet in the buffer! + void createTempFiles(const std::set& workLoad, ProcessCallback& callback /*throw X*/); //throw X + +private: + TempFileBuffer (const TempFileBuffer&) = delete; + TempFileBuffer& operator=(const TempFileBuffer&) = delete; + + void createTempFolderPath(); //throw FileError + + std::map tempFilePaths_; + Zstring tempFolderPath_; +}; +} +#endif //ALGORITHM_H_34218518475321452548 diff --git a/FreeFileSync/Source/base/binary.cpp b/FreeFileSync/Source/base/binary.cpp new file mode 100644 index 0000000..a908bfa --- /dev/null +++ b/FreeFileSync/Source/base/binary.cpp @@ -0,0 +1,79 @@ +// ***************************************************************************** +// * 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 "binary.h" + +using namespace zen; +using namespace fff; +using AFS = AbstractFileSystem; + + +bool fff::filesHaveSameContent(const AbstractPath& filePath1, const AbstractPath& filePath2, const IoCallback& notifyUnbufferedIO /*throw X*/) //throw FileError, X +{ + int64_t totalBytesNotified = 0; + IoCallback /*[!] as expected by InputStream::tryRead()*/ notifyIoDiv = IOCallbackDivider(notifyUnbufferedIO, totalBytesNotified); + + const std::unique_ptr stream1 = AFS::getInputStream(filePath1); //throw FileError + const std::unique_ptr stream2 = AFS::getInputStream(filePath2); // + + const size_t blockSize1 = stream1->getBlockSize(); //throw FileError + const size_t blockSize2 = stream2->getBlockSize(); // + + const size_t bufCapacity = blockSize2 - 1 + blockSize1 + blockSize2; + + const std::unique_ptr buf(new std::byte[bufCapacity]); + + std::byte* const buf1 = buf.get() + blockSize2; //capacity: blockSize2 - 1 + blockSize1 + std::byte* const buf2 = buf.get(); //capacity: blockSize2 + + size_t buf1PosEnd = 0; + for (;;) + { + const size_t bytesRead1 = stream1->tryRead(buf1 + buf1PosEnd, blockSize1, notifyIoDiv); //throw FileError, X; may return short; only 0 means EOF + + if (bytesRead1 == 0) //end of file + { + size_t buf1Pos = 0; + while (buf1Pos < buf1PosEnd) + { + const size_t bytesRead2 = stream2->tryRead(buf2, blockSize2, notifyIoDiv); //throw FileError, X; may return short; only 0 means EOF + + if (bytesRead2 == 0 ||//end of file + bytesRead2 > buf1PosEnd - buf1Pos) + return false; + + if (std::memcmp(buf1 + buf1Pos, buf2, bytesRead2) != 0) + return false; + + buf1Pos += bytesRead2; + } + return stream2->tryRead(buf2, blockSize2, notifyIoDiv) == 0; //throw FileError, X; expect EOF + } + else + { + buf1PosEnd += bytesRead1; + + size_t buf1Pos = 0; + while (buf1PosEnd - buf1Pos >= blockSize2) + { + const size_t bytesRead2 = stream2->tryRead(buf2, blockSize2, notifyIoDiv); //throw FileError, X; may return short; only 0 means EOF + + if (bytesRead2 == 0) //end of file + return false; + + if (std::memcmp(buf1 + buf1Pos, buf2, bytesRead2) != 0) + return false; + + buf1Pos += bytesRead2; + } + if (buf1Pos > 0) + { + buf1PosEnd -= buf1Pos; + std::memmove(buf1, buf1 + buf1Pos, buf1PosEnd); + } + } + } +} diff --git a/FreeFileSync/Source/base/binary.h b/FreeFileSync/Source/base/binary.h new file mode 100644 index 0000000..d635b92 --- /dev/null +++ b/FreeFileSync/Source/base/binary.h @@ -0,0 +1,20 @@ +// ***************************************************************************** +// * 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 BINARY_H_3941281398513241134 +#define BINARY_H_3941281398513241134 + +#include "../afs/abstract.h" + + +namespace fff +{ +bool filesHaveSameContent(const AbstractPath& filePath1, + const AbstractPath& filePath2, + const zen::IoCallback& notifyUnbufferedIO /*throw X*/); //throw FileError, X +} + +#endif //BINARY_H_3941281398513241134 diff --git a/FreeFileSync/Source/base/cmp_filetime.h b/FreeFileSync/Source/base/cmp_filetime.h new file mode 100644 index 0000000..0061306 --- /dev/null +++ b/FreeFileSync/Source/base/cmp_filetime.h @@ -0,0 +1,93 @@ +// ***************************************************************************** +// * 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 CMP_FILETIME_H_032180451675845 +#define CMP_FILETIME_H_032180451675845 + +#include + + +namespace fff +{ +inline +bool sameFileTime(time_t lhs, time_t rhs, /*unsigned*/ int tolerance, const std::vector& ignoreTimeShiftMinutes) +{ + assert(tolerance >= 0); + + if (lhs < rhs) + std::swap(lhs, rhs); + + if (rhs > std::numeric_limits::max() - tolerance) //protect against overflow! + return true; + + if (lhs <= rhs + tolerance) + return true; + + for (const unsigned int minutes : ignoreTimeShiftMinutes) + { + assert(minutes > 0); + const int shiftSec = static_cast(minutes) * 60; + + time_t low = rhs; + time_t high = lhs; + + if (low <= std::numeric_limits::max() - shiftSec) //protect against overflow! + low += shiftSec; + else + high -= shiftSec; + + if (high < low) + std::swap(high, low); + + if (low > std::numeric_limits::max() - tolerance) //protect against overflow! + return true; + + if (high <= low + tolerance) + return true; + } + + return false; +} + +//--------------------------------------------------------------------------------------------------------------- + +enum class TimeResult +{ + equal, + leftNewer, + rightNewer, + leftInvalid, + rightInvalid +}; + + +//number of seconds since Jan 1st 1970 + 1 year (needn't be too precise) +inline const time_t oneYearFromNow = std::time(nullptr) + 365 * 24 * 3600; + + +inline +TimeResult compareFileTime(time_t lhs, time_t rhs, unsigned int tolerance, const std::vector& ignoreTimeShiftMinutes) +{ + assert(oneYearFromNow != 0); + if (sameFileTime(lhs, rhs, tolerance, ignoreTimeShiftMinutes)) //last write time may differ by up to 2 seconds (NTFS vs FAT32) + return TimeResult::equal; + + //check for erroneous dates + if (lhs < 0 || lhs > oneYearFromNow) //earlier than Jan 1st 1970 or more than one year in future + return TimeResult::leftInvalid; + + if (rhs < 0 || rhs > oneYearFromNow) + return TimeResult::rightInvalid; + + //regular time comparison + if (lhs < rhs) + return TimeResult::rightNewer; + else + return TimeResult::leftNewer; +} +} + +#endif //CMP_FILETIME_H_032180451675845 diff --git a/FreeFileSync/Source/base/comparison.cpp b/FreeFileSync/Source/base/comparison.cpp new file mode 100644 index 0000000..31e0d47 --- /dev/null +++ b/FreeFileSync/Source/base/comparison.cpp @@ -0,0 +1,1196 @@ +// ***************************************************************************** +// * 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 "comparison.h" +#include +#include +#include +#include "algorithm.h" +#include "parallel_scan.h" +#include "dir_exist_async.h" +#include "db_file.h" +#include "binary.h" +#include "cmp_filetime.h" +#include "status_handler_impl.h" +#include "../afs/concrete.h" +#include "../afs/native.h" + +using namespace zen; +using namespace fff; + + +std::vector fff::extractCompareCfg(const MainConfiguration& mainCfg) +{ + //merge first and additional pairs + std::vector localCfgs = {mainCfg.firstPair}; + append(localCfgs, mainCfg.additionalPairs); + + std::vector output; + + for (const LocalPairConfig& lpc : localCfgs) + { + const CompConfig cmpCfg = lpc.localCmpCfg ? *lpc.localCmpCfg : mainCfg.cmpCfg; + const SyncConfig syncCfg = lpc.localSyncCfg ? *lpc.localSyncCfg : mainCfg.syncCfg; + NormalizedFilter filter = normalizeFilters(mainCfg.globalFilter, lpc.localFilter); + + //exclude sync.ffs_db and lock files + //=> can't put inside fff::parallelFolderScan() which is also used by versioning + filter.nameFilter = filter.nameFilter.ref().copyFilterAddingExclusion(Zstring(Zstr("*")) + SYNC_DB_FILE_ENDING + Zstr("\n*") + LOCK_FILE_ENDING); + + output.push_back( + { + lpc.folderPathPhraseLeft, lpc.folderPathPhraseRight, + cmpCfg.compareVar, + cmpCfg.handleSymlinks, + cmpCfg.ignoreTimeShiftMinutes, + filter, + syncCfg.directionCfg + }); + } + return output; +} + +//------------------------------------------------------------------------------------------ +namespace +{ +struct ResolvedFolderPair +{ + AbstractPath folderPathLeft; + AbstractPath folderPathRight; +}; + + +struct ResolvedBaseFolders +{ + std::vector resolvedPairs; + FolderStatus baseFolderStatus; +}; + + +ResolvedBaseFolders initializeBaseFolders(const std::vector& fpCfgList, + const AFS::RequestPasswordFun& requestPassword /*throw X*/, + WarningDialogs& warnings, + PhaseCallback& callback /*throw X*/) //throw X +{ + std::vector pathPhrases; + for (const FolderPairCfg& fpCfg : fpCfgList) + { + pathPhrases.push_back(fpCfg.folderPathPhraseLeft_); + pathPhrases.push_back(fpCfg.folderPathPhraseRight_); + } + + ResolvedBaseFolders output; + std::set allFolders; + + tryReportingError([&] + { + //createAbstractPath() -> tryExpandVolumeName() hangs for idle HDD! => run async to make it cancellable + auto protCurrentPhrase = makeSharedRef>(); + + std::future> futFolderPaths = runAsync([pathPhrases, currentPhraseWeak = std::weak_ptr(protCurrentPhrase.ptr())] + { + setCurrentThreadName(Zstr("Normalizing folder paths")); + + std::vector folderPaths; + for (const Zstring& pathPhrase : pathPhrases) + { + if (auto protCurrentPhrase2 = currentPhraseWeak.lock()) //[!] not owned by worker thread! + protCurrentPhrase2->access([&](Zstring& currentPathPhrase) { currentPathPhrase = pathPhrase; }); + else + throw std::runtime_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Caller context gone!"); + + folderPaths.push_back(createAbstractPath(pathPhrase)); + } + return folderPaths; + }); + + while (futFolderPaths.wait_for(UI_UPDATE_INTERVAL / 2) == std::future_status::timeout) + { + const Zstring pathPhrase = protCurrentPhrase.ref().access([](const Zstring& currentPathPhrase) { return currentPathPhrase; }); + callback.updateStatus(_("Normalizing folder paths...") + L' ' + utfTo(pathPhrase)); //throw X + } + + const std::vector& folderPaths = futFolderPaths.get(); //throw (std::runtime_error) + + //support "retry" for environment variable and variable driver letter resolution! + allFolders.clear(); + allFolders.insert(folderPaths.begin(), folderPaths.end()); + + output.resolvedPairs.clear(); + for (size_t i = 0; i < folderPaths.size(); i += 2) + output.resolvedPairs.push_back({folderPaths[i], folderPaths[i + 1]}); + //--------------------------------------------------------------------------- + + output.baseFolderStatus = getFolderStatusParallel(allFolders, + true /*authenticateAccess*/, requestPassword, callback); //throw X + if (!output.baseFolderStatus.failedChecks.empty()) + { + std::wstring msg = _("Cannot find the following folders:") + L'\n'; + + for (const auto& [folderPath, error] : output.baseFolderStatus.failedChecks) + msg += L'\n' + AFS::getDisplayPath(folderPath); + + msg += L"\n___________________________________________"; + for (const auto& [folderPath, error] : output.baseFolderStatus.failedChecks) + msg += L"\n\n" + replaceCpy(error.toString(), L"\n\n", L'\n'); + + throw FileError(msg); + } + }, callback); //throw X + + + if (!output.baseFolderStatus.notExisting.empty()) + { + std::wstring msg = _("The following folders do not yet exist:") + L'\n'; + + for (const AbstractPath& folderPath : output.baseFolderStatus.notExisting) + msg += L'\n' + AFS::getDisplayPath(folderPath); + + msg += L"\n\n"; + msg += _("The folders are created automatically when needed."); + + callback.reportWarning(msg, warnings.warnFolderNotExisting); //throw X + } + + //--------------------------------------------------------------------------- + std::map, std::set> ciPathAliases; + + for (const AbstractPath& folderPath : allFolders) + ciPathAliases[std::pair(folderPath.afsDevice, folderPath.afsPath.value)].insert(folderPath); + + if (std::any_of(ciPathAliases.begin(), ciPathAliases.end(), [](const auto& item) { return item.second/*aliases*/.size() > 1; })) + { + std::wstring msg = _("The following folder paths differ in case. Please use a single form in order to avoid duplicate accesses."); + for (const auto& [key, aliases] : ciPathAliases) + if (aliases.size() > 1) + { + msg += L'\n'; + for (const AbstractPath& aliasPath : aliases) + msg += L'\n' + AFS::getDisplayPath(aliasPath); + } + + callback.reportWarning(msg, warnings.warnFoldersDifferInCase); //throw X + + //what about /folder and /Folder/subfolder? => yes, inconsistent, but doesn't matter for FFS + } + //--------------------------------------------------------------------------- + + return output; +} + +//############################################################################################################################# + +class ComparisonBuffer +{ +public: + ComparisonBuffer(const FolderStatus& folderStatus, + unsigned int fileTimeTolerance, + ProcessCallback& callback) : + fileTimeTolerance_(fileTimeTolerance), + folderStatus_(folderStatus), + cb_(callback) {} + + FolderComparison execute(const std::vector>& workLoad); + +private: + ComparisonBuffer (const ComparisonBuffer&) = delete; + ComparisonBuffer& operator=(const ComparisonBuffer&) = delete; + + //create comparison result table and fill category except for files existing on both sides: undefinedFiles and undefinedSymlinks are appended! + SharedRef compareByTimeSize(const ResolvedFolderPair& fp, const FolderPairCfg& fpConfig) const; + SharedRef compareBySize (const ResolvedFolderPair& fp, const FolderPairCfg& fpConfig) const; + std::vector> compareByContent(const std::vector>& workLoad) const; + + SharedRef performComparison(const ResolvedFolderPair& fp, + const FolderPairCfg& fpCfg, + std::vector& undefinedFiles, + std::vector& undefinedSymlinks) const; + + BaseFolderStatus getBaseFolderStatus(const AbstractPath& folderPath) const + { + if (folderStatus_.existing.contains(folderPath)) + return BaseFolderStatus::existing; + if (folderStatus_.notExisting.contains(folderPath)) + return BaseFolderStatus::notExisting; + if (folderStatus_.failedChecks.contains(folderPath)) + return BaseFolderStatus::failure; + assert(AFS::isNullPath(folderPath)); + return BaseFolderStatus::notExisting; + }; + + const unsigned int fileTimeTolerance_; + const FolderStatus& folderStatus_; + std::map folderBuffer_; //contains entries for *all* scanned folders! + ProcessCallback& cb_; +}; + + +FolderComparison ComparisonBuffer::execute(const std::vector>& workLoad) +{ + std::set foldersToRead; + for (const auto& [folderPair, fpCfg] : workLoad) + if (getBaseFolderStatus(folderPair.folderPathLeft ) != BaseFolderStatus::failure && //no need to list or display one-sided results if + getBaseFolderStatus(folderPair.folderPathRight) != BaseFolderStatus::failure) //*either* folder existence check fails + { + //+ only traverse *existing* folders + if (getBaseFolderStatus(folderPair.folderPathLeft) == BaseFolderStatus::existing) + foldersToRead.emplace(DirectoryKey{folderPair.folderPathLeft, fpCfg.filter.nameFilter, fpCfg.handleSymlinks}); + if (getBaseFolderStatus(folderPair.folderPathRight) == BaseFolderStatus::existing) + foldersToRead.emplace(DirectoryKey{folderPair.folderPathRight, fpCfg.filter.nameFilter, fpCfg.handleSymlinks}); + } + + //------------------------------------------------------------------ + StopWatch scanTime; + int itemsReported = 0; + + auto onStatusUpdate = [&, textScanning = _("Scanning:") + L' '](const std::wstring& statusLine, int itemsTotal) + { + cb_.updateDataProcessed(itemsTotal - itemsReported, 0); //noexcept + itemsReported = itemsTotal; + + cb_.updateStatus(textScanning + statusLine); //throw X + }; + + folderBuffer_ = parallelFolderScan(foldersToRead, + [&](const PhaseCallback::ErrorInfo& errorInfo) { return cb_.reportError(errorInfo); }, //throw X + onStatusUpdate, //throw X + UI_UPDATE_INTERVAL / 2); //every ~25 ms + + //------------------------------------------------------------------ + const int64_t totalTimeSec = std::chrono::duration_cast(scanTime.elapsed()).count(); + + cb_.logMessage(_P("1 item found", "%x items found", itemsReported) + L" | " + + _("Time elapsed:") + L' ' + utfTo(formatTimeSpan(totalTimeSec)), + PhaseCallback::MsgType::info); //throw X + //caveat: the time while waiting on error dialog or while paused is counted, too :/ OTOH other threads continue working => unclear how to count... + //------------------------------------------------------------------ + + //process binary comparison as one junk + std::vector> workLoadByContent; + for (const auto& [folderPair, fpCfg] : workLoad) + if (fpCfg.compareVar == CompareVariant::content) + workLoadByContent.push_back({folderPair, fpCfg}); + + std::vector> outputByContent = compareByContent(workLoadByContent); + auto itOByC = outputByContent.begin(); + + FolderComparison output; + + //write output in expected order + for (const auto& [folderPair, fpCfg] : workLoad) + switch (fpCfg.compareVar) + { + case CompareVariant::timeSize: + output.push_back(compareByTimeSize(folderPair, fpCfg)); + break; + case CompareVariant::size: + output.push_back(compareBySize(folderPair, fpCfg)); + break; + case CompareVariant::content: + assert(itOByC != outputByContent.end()); + if (itOByC != outputByContent.end()) + output.push_back(*itOByC++); + break; + } + return output; +} + + +//--------------------assemble conflict descriptions--------------------------- + +//const wchar_t arrowLeft [] = L"\u2190"; unicode arrows -> too small +//const wchar_t arrowRight[] = L"\u2192"; +const wchar_t arrowLeft [] = L"<-"; +const wchar_t arrowRight[] = L"->"; + +//NOTE: conflict texts are NOT expected to contain additional path info (already implicit through associated item!) +// => only add path info if information is relevant, e.g. conflict is specific to left/right side only + +template inline +Zstringc getConflictInvalidDate(const FileOrLinkPair& file) +{ + return utfTo(replaceCpy(_("File %x has an invalid date."), L"%x", fmtPath(AFS::getDisplayPath(file.template getAbstractPath()))) + L'\n' + + _("Date:") + L' ' + formatUtcToLocalTime(file.template getLastWriteTime())); +} + + +Zstringc getConflictSameDateDiffSize(const FilePair& file) +{ + return utfTo(_("Files have the same date but a different size.") + L'\n' + + _("Date:") + L' ' + formatUtcToLocalTime(file.getLastWriteTime()) + TAB_SPACE + _("Size:") + L' ' + formatNumber(file.getFileSize()) + L' ' + arrowLeft + L'\n' + + _("Date:") + L' ' + formatUtcToLocalTime(file.getLastWriteTime()) + TAB_SPACE + _("Size:") + L' ' + formatNumber(file.getFileSize()) + L' ' + arrowRight); +} + + +Zstringc getConflictSkippedBinaryComparison() +{ + return utfTo(_("Content comparison was skipped for excluded files.")); +} + + +Zstringc getConflictAmbiguousItemName(const Zstring& itemName) +{ + return utfTo(replaceCpy(_("The name %x is used by more than one item in the folder."), L"%x", fmtPath(itemName))); +} + +//----------------------------------------------------------------------------- + +void categorizeSymlinkByTime(SymlinkPair& symlink) +{ + //categorize symlinks that exist on both sides + switch (compareFileTime(symlink.getLastWriteTime(), + symlink.getLastWriteTime(), symlink.base().getFileTimeTolerance(), symlink.base().getIgnoredTimeShift())) + { + case TimeResult::equal: + symlink.setContentCategory(FileContentCategory::equal); + break; + + case TimeResult::leftNewer: + symlink.setContentCategory(FileContentCategory::leftNewer); + break; + + case TimeResult::rightNewer: + symlink.setContentCategory(FileContentCategory::rightNewer); + break; + + case TimeResult::leftInvalid: + symlink.setCategoryInvalidTime(getConflictInvalidDate(symlink)); + break; + + case TimeResult::rightInvalid: + symlink.setCategoryInvalidTime(getConflictInvalidDate(symlink)); + break; + } +} + + +SharedRef ComparisonBuffer::compareByTimeSize(const ResolvedFolderPair& fp, const FolderPairCfg& fpConfig) const +{ + //do basis scan and retrieve files existing on both sides as "compareCandidates" + std::vector uncategorizedFiles; + std::vector uncategorizedLinks; + SharedRef output = performComparison(fp, fpConfig, uncategorizedFiles, uncategorizedLinks); + + //finish symlink categorization + for (SymlinkPair* symlink : uncategorizedLinks) + categorizeSymlinkByTime(*symlink); + + //categorize files that exist on both sides + for (FilePair* file : uncategorizedFiles) + { + switch (compareFileTime(file->getLastWriteTime(), + file->getLastWriteTime(), fileTimeTolerance_, fpConfig.ignoreTimeShiftMinutes)) + { + case TimeResult::equal: + if (file->getFileSize() == file->getFileSize()) + file->setContentCategory(FileContentCategory::equal); + else + file->setCategoryInvalidTime(getConflictSameDateDiffSize(*file)); + break; + + case TimeResult::leftNewer: + file->setContentCategory(FileContentCategory::leftNewer); + break; + + case TimeResult::rightNewer: + file->setContentCategory(FileContentCategory::rightNewer); + break; + + case TimeResult::leftInvalid: + file->setCategoryInvalidTime(getConflictInvalidDate(*file)); + break; + + case TimeResult::rightInvalid: + file->setCategoryInvalidTime(getConflictInvalidDate(*file)); + break; + } + } + return output; +} + + +namespace +{ +void categorizeSymlinkByContent(SymlinkPair& symlink, PhaseCallback& callback) +{ + //categorize symlinks that exist on both sides + callback.updateStatus(replaceCpy(_("Resolving symbolic link %x"), L"%x", fmtPath(AFS::getDisplayPath(symlink.getAbstractPath())))); //throw X + callback.updateStatus(replaceCpy(_("Resolving symbolic link %x"), L"%x", fmtPath(AFS::getDisplayPath(symlink.getAbstractPath())))); //throw X + + bool equalContent = false; + const std::wstring errMsg = tryReportingError([&] + { + equalContent = AFS::equalSymlinkContent(symlink.getAbstractPath(), + symlink.getAbstractPath()); //throw FileError + }, callback); //throw X + + if (!errMsg.empty()) + symlink.setCategoryConflict(utfTo(errMsg)); + else + symlink.setContentCategory(equalContent ? FileContentCategory::equal : FileContentCategory::different); +} +} + + +SharedRef ComparisonBuffer::compareBySize(const ResolvedFolderPair& fp, const FolderPairCfg& fpConfig) const +{ + //do basis scan and retrieve files existing on both sides as "compareCandidates" + std::vector uncategorizedFiles; + std::vector uncategorizedLinks; + SharedRef output = performComparison(fp, fpConfig, uncategorizedFiles, uncategorizedLinks); + + //finish symlink categorization + for (SymlinkPair* symlink : uncategorizedLinks) + categorizeSymlinkByContent(*symlink, cb_); //"compare by size" has the semantics of a quick content-comparison! + //harmonize with algorithm.cpp, stillInSync()! + + //categorize files that exist on both sides + for (FilePair* file : uncategorizedFiles) + { + //Caveat: + //1. FILE_EQUAL may only be set if file names match in case: InSyncFolder's mapping tables use file name as a key! see db_file.cpp + //2. FILE_EQUAL is expected to mean identical file sizes! See InSyncFile + //3. harmonize with "bool stillInSync()" in algorithm.cpp, FilePair::setSyncedTo() in file_hierarchy.h + if (file->getFileSize() == file->getFileSize()) + file->setContentCategory(FileContentCategory::equal); + else + file->setContentCategory(FileContentCategory::different); + } + return output; +} + + +namespace parallel +{ +//-------------------------------------------------------------- +//ATTENTION CALLBACKS: they also run asynchronously *outside* the singleThread lock! +//-------------------------------------------------------------- +inline +bool filesHaveSameContent(const AbstractPath& filePath1, const AbstractPath& filePath2, //throw FileError, X + const IoCallback& notifyUnbufferedIO /*throw X*/, + std::mutex& singleThread) +{ return parallelScope([=] { return filesHaveSameContent(filePath1, filePath2, notifyUnbufferedIO); /*throw FileError, X*/ }, singleThread); } +} + + +namespace +{ +void categorizeFileByContent(FilePair& file, const std::wstring& txtComparingContentOfFiles, AsyncCallback& acb, std::mutex& singleThread) //throw ThreadStopRequest +{ + bool haveSameContent = false; + const std::wstring errMsg = tryReportingError([&] + { + std::wstring statusMsg = replaceCpy(txtComparingContentOfFiles, L"%x", fmtPath(file.getRelativePath())); + //is it possible that right side has a different relPath? maybe, but who cares, it's a short-lived status message + + ItemStatReporter statReporter(1, file.getFileSize(), acb); + PercentStatReporter percentReporter(statusMsg, file.getFileSize(), statReporter); + + acb.updateStatus(std::move(statusMsg)); //throw ThreadStopRequest + + //callbacks run *outside* singleThread lock! => fine + auto notifyUnbufferedIO = [&percentReporter](int64_t bytesDelta) + { + percentReporter.updateDeltaAndStatus(bytesDelta); //throw ThreadStopRequest + interruptionPoint(); //throw ThreadStopRequest => not reliably covered by PercentStatReporter::updateDeltaAndStatus()! + }; + + haveSameContent = parallel::filesHaveSameContent(file.getAbstractPath(), + file.getAbstractPath(), notifyUnbufferedIO, singleThread); //throw FileError, ThreadStopRequest + statReporter.reportDelta(1, 0); + }, acb); //throw ThreadStopRequest + + if (!errMsg.empty()) + file.setCategoryConflict(utfTo(errMsg)); + else + file.setContentCategory(haveSameContent ? FileContentCategory::equal : FileContentCategory::different); +} +} + + +std::vector> ComparisonBuffer::compareByContent(const std::vector>& workLoad) const +{ + struct ParallelOps + { + size_t current = 0; + }; + std::map parallelOpsStatus; + + struct BinaryWorkload + { + ParallelOps& parallelOpsL; // + ParallelOps& parallelOpsR; //consider aliasing! + RingBuffer filesToCompareBytewise; + }; + std::vector fpWorkload; + + auto addToBinaryWorkload = [&](const AbstractPath& basePathL, const AbstractPath& basePathR, RingBuffer&& filesToCompareBytewise) + { + ParallelOps& posL = parallelOpsStatus[basePathL.afsDevice]; + ParallelOps& posR = parallelOpsStatus[basePathR.afsDevice]; + fpWorkload.push_back({posL, posR, std::move(filesToCompareBytewise)}); + }; + + std::vector> output; + + const Zstringc txtConflictSkippedBinaryComparison = getConflictSkippedBinaryComparison(); //avoid premature pess.: save memory via ref-counted string + + for (const auto& [folderPair, fpCfg] : workLoad) + { + std::vector undefinedFiles; + std::vector uncategorizedLinks; + //run basis scan and retrieve candidates for binary comparison (files existing on both sides) + output.push_back(performComparison(folderPair, fpCfg, undefinedFiles, uncategorizedLinks)); + + RingBuffer filesToCompareBytewise; + //content comparison of file content happens AFTER finding corresponding files and AFTER filtering + //in order to separate into two processes (scanning and comparing) + for (FilePair* file : undefinedFiles) + //pre-check: files have different content if they have a different file size (must not be FILE_EQUAL: see InSyncFile) + if (file->getFileSize() != file->getFileSize()) + file->setContentCategory(FileContentCategory::different); + else + { + //perf: skip binary comparison for excluded rows (e.g. via time span and size filter)! + //both soft and hard filter were already applied in ComparisonBuffer::performComparison()! + assert(file->getContentCategory() == FileContentCategory::unknown); //=default + if (!file->isActive()) + file->setCategoryConflict(txtConflictSkippedBinaryComparison); + else + filesToCompareBytewise.push_back(file); + } + if (!filesToCompareBytewise.empty()) + addToBinaryWorkload(output.back().ref().getAbstractPath(), + output.back().ref().getAbstractPath(), std::move(filesToCompareBytewise)); + + //finish symlink categorization + for (SymlinkPair* symlink : uncategorizedLinks) + categorizeSymlinkByContent(*symlink, cb_); + } + + //finish categorization: compare files (that have same size) bytewise... + if (!fpWorkload.empty()) //run ProcessPhase::binaryCompare only when needed + { + int itemsTotal = 0; + uint64_t bytesTotal = 0; + for (const BinaryWorkload& bwl : fpWorkload) + { + itemsTotal += static_cast(bwl.filesToCompareBytewise.size()); + + for (const FilePair* file : bwl.filesToCompareBytewise) + bytesTotal += file->getFileSize(); //left and right file sizes are equal + } + + cb_.initNewPhase(itemsTotal, bytesTotal, ProcessPhase::binaryCompare); //throw X + StopWatch compareTime; + + std::mutex singleThread; //only a single worker thread may run at a time, except for parallel file I/O + + AsyncCallback acb; // + std::function scheduleMoreTasks; //manage life time: enclose ThreadGroup! + + ThreadGroup> tg(std::numeric_limits::max(), Zstr("Binary Comparison")); + + scheduleMoreTasks = [&, txtComparingContentOfFiles = _("Comparing content of files %x")] + { + bool wereDone = true; + + for (size_t j = 0; j < fpWorkload.size(); ++j) + { + BinaryWorkload& bwl = fpWorkload[j]; + ParallelOps& posL = bwl.parallelOpsL; + ParallelOps& posR = bwl.parallelOpsR; + const size_t newTaskCount = std::min({1 - posL.current, 1 - posR.current, bwl.filesToCompareBytewise.size()}); + if (&posL != &posR) + posL.current += newTaskCount; // + posR.current += newTaskCount; //consider aliasing! + + for (size_t i = 0; i < newTaskCount; ++i) + { + tg.run([&, statusPrio = j, &file = *bwl.filesToCompareBytewise.front()] + { + acb.notifyTaskBegin(statusPrio); //prioritize status messages according to natural order of folder pairs + ZEN_ON_SCOPE_EXIT(acb.notifyTaskEnd()); + + std::lock_guard dummy(singleThread); //protect ALL variable accesses unless explicitly not needed ("parallel" scope)! + //--------------------------------------------------------------------------------------------------- + ZEN_ON_SCOPE_SUCCESS(if (&posL != &posR) --posL.current; + /**/ --posR.current; + scheduleMoreTasks()); + + categorizeFileByContent(file, txtComparingContentOfFiles, acb, singleThread); //throw ThreadStopRequest + }); + + bwl.filesToCompareBytewise.pop_front(); + } + if (posL.current != 0 || posR.current != 0 || !bwl.filesToCompareBytewise.empty()) + wereDone = false; + } + if (wereDone) + acb.notifyAllDone(); + }; + + { + std::lock_guard dummy(singleThread); //[!] potential race with worker threads! + scheduleMoreTasks(); //set initial load + } + + const auto [itemsProcessed, bytesProcessed] = acb.waitUntilDone(UI_UPDATE_INTERVAL / 2 /*every ~25 ms*/, cb_); //throw X + + //--------------------------------------------------------------- + const int64_t totalTimeSec = std::chrono::duration_cast(compareTime.elapsed()).count(); + + cb_.logMessage(_("File contents compared:") + L' ' + formatNumber(itemsProcessed) + L" (" + formatFilesizeShort(bytesProcessed) + L") | " + + _("Time elapsed:") + L' ' + utfTo(formatTimeSpan(totalTimeSec)), + PhaseCallback::MsgType::info); //throw X + } + + return output; +} + +//----------------------------------------------------------------------------------------------- + +class MergeSides +{ +public: + static void execute(const FolderContainer& lhs, const FolderContainer& rhs, + const std::unordered_map& errorsByRelPathL, + const std::unordered_map& errorsByRelPathR, + ContainerObject& output, + std::vector& undefinedFilesOut, + std::vector& undefinedSymlinksOut) + { + MergeSides inst(errorsByRelPathL, errorsByRelPathR, undefinedFilesOut, undefinedSymlinksOut); + + const Zstringc* errorMsg = nullptr; + if (auto it = inst.errorsByRelPathL_.find(Zstring()); //empty path if read-error for whole base directory + it != inst.errorsByRelPathL_.end()) + errorMsg = &it->second; + else if (auto it2 = inst.errorsByRelPathR_.find(Zstring()); + it2 != inst.errorsByRelPathR_.end()) + errorMsg = &it2->second; + + inst.mergeFolders(lhs, rhs, errorMsg, output); + } + +private: + MergeSides(const std::unordered_map& errorsByRelPathL, + const std::unordered_map& errorsByRelPathR, + std::vector& undefinedFilesOut, + std::vector& undefinedSymlinksOut) : + errorsByRelPathL_(errorsByRelPathL), + errorsByRelPathR_(errorsByRelPathR), + undefinedFiles_(undefinedFilesOut), + undefinedSymlinks_(undefinedSymlinksOut) {} + + void mergeFolders(const FolderContainer& lhs, const FolderContainer& rhs, const Zstringc* errorMsg, ContainerObject& output); + + template + void fillOneSide(const FolderContainer& folderCont, const Zstringc* errorMsg, ContainerObject& output); + + template + const Zstringc* checkFailedRead(FileSystemObject& fsObj, const Zstringc* errorMsg); + + const Zstringc* checkFailedRead(FileSystemObject& fsObj, const Zstringc* errorMsg); + + const std::unordered_map& errorsByRelPathL_; //base-relative paths or empty if read-error for whole base directory + const std::unordered_map& errorsByRelPathR_; // + std::vector& undefinedFiles_; + std::vector& undefinedSymlinks_; +}; + + +template inline +const Zstringc* MergeSides::checkFailedRead(FileSystemObject& fsObj, const Zstringc* errorMsg) +{ + if (!errorMsg) + { + const std::unordered_map& errorsByRelPath = selectParam(errorsByRelPathL_, errorsByRelPathR_); + + if (!errorsByRelPath.empty()) //only pay for relPath construction when needed + if (const auto it = errorsByRelPath.find(fsObj.getRelativePath()); + it != errorsByRelPath.end()) + errorMsg = &it->second; + } + + if (errorMsg) //make sure all items are disabled => avoid user panicking: https://freefilesync.org/forum/viewtopic.php?t=7582 + { + fsObj.setActive(false); + fsObj.setCategoryConflict(*errorMsg); //peak memory: Zstringc is ref-counted, unlike std::string! + static_assert(std::is_same_v); + } + return errorMsg; +} + + +const Zstringc* MergeSides::checkFailedRead(FileSystemObject& fsObj, const Zstringc* errorMsg) +{ + if (const Zstringc* errorMsgNew = checkFailedRead(fsObj, errorMsg)) + return errorMsgNew; + + return checkFailedRead(fsObj, errorMsg); +} + + +template +void forEachSorted(const MapType& fileMap, Function fun) +{ + using FileRef = const typename MapType::value_type*; + + std::vector fileList; + fileList.reserve(fileMap.size()); + + for (const auto& item : fileMap) + fileList.push_back(&item); + + //sort for natural default sequence on UI file grid: + std::sort(fileList.begin(), fileList.end(), [](const FileRef& lhs, const FileRef& rhs) { return compareNoCase(lhs->first /*item name*/, rhs->first) < 0; }); + + for (const auto& item : fileList) + fun(item->first, item->second); +} + + +template +void MergeSides::fillOneSide(const FolderContainer& folderCont, const Zstringc* errorMsg, ContainerObject& output) +{ + forEachSorted(folderCont.files, [&](const Zstring& fileName, const FileAttributes& attrib) + { + FilePair& newItem = output.addFile(fileName, attrib); + checkFailedRead(newItem, errorMsg); + }); + + forEachSorted(folderCont.symlinks, [&](const Zstring& linkName, const LinkAttributes& attrib) + { + SymlinkPair& newItem = output.addSymlink(linkName, attrib); + checkFailedRead(newItem, errorMsg); + }); + + forEachSorted(folderCont.folders, [&](const Zstring& folderName, const std::pair& attrib) + { + FolderPair& newFolder = output.addFolder(folderName, attrib.first); + const Zstringc* errorMsgNew = checkFailedRead(newFolder, errorMsg); + fillOneSide(attrib.second, errorMsgNew, newFolder); //recurse + }); +} + + +template inline +void matchFolders(const MapType& mapLeft, const MapType& mapRight, ProcessLeftOnly lo, ProcessRightOnly ro, ProcessBoth bo) +{ + struct FileRef + { + Zstring canonicalName; //perf: buffer instead of compareNoCase()/equalNoCase()? => makes no (significant) difference! + const typename MapType::value_type* ref; + SelectSide side; + }; + std::vector fileList; + fileList.reserve(mapLeft.size() + mapRight.size()); //perf: ~5% shorter runtime + + auto getCanonicalName = [](const Zstring& name) { return trimCpy(getUpperCase(name)); }; + + for (const auto& item : mapLeft ) fileList.push_back({getCanonicalName(item.first), &item, SelectSide::left}); + for (const auto& item : mapRight) fileList.push_back({getCanonicalName(item.first), &item, SelectSide::right}); + + //primary sort: ignore upper/lower case, leading/trailing space, Unicode normal form + //bonus: natural default sequence on UI file grid + std::sort(fileList.begin(), fileList.end(), [](const FileRef& lhs, const FileRef& rhs) { return lhs.canonicalName < rhs.canonicalName; }); + + using ItType = typename std::vector::iterator; + auto tryMatchRange = [&](ItType it, ItType itLast) //auto parameters? compiler error on VS 17.2... + { + const size_t equalCountL = std::count_if(it, itLast, [](const FileRef& fr) { return fr.side == SelectSide::left; }); + const size_t equalCountR = itLast - it - equalCountL; + + if (equalCountL == 1 && equalCountR == 1) //we have a match + { + if (it->side == SelectSide::left) + bo(*it[0].ref, *it[1].ref); + else + bo(*it[1].ref, *it[0].ref); + } + else if (equalCountL == 1 && equalCountR == 0) + lo(*it->ref, nullptr); + else if (equalCountL == 0 && equalCountR == 1) + ro(*it->ref, nullptr); + else //ambiguous (yes, even if one side only, e.g. different Unicode normalization forms) + return false; + return true; + }; + + for (auto it = fileList.begin(); it != fileList.end();) + { + //find equal range: ignore upper/lower case, leading/trailing space, Unicode normal form + auto itEndEq = std::find_if(it + 1, fileList.end(), [&](const FileRef& fr) { return fr.canonicalName != it->canonicalName; }); + if (!tryMatchRange(it, itEndEq)) + { + //secondary sort: respect case, ignore Unicode normal forms + std::sort(it, itEndEq, [](const FileRef& lhs, const FileRef& rhs) { return getUnicodeNormalForm(lhs.ref->first) < getUnicodeNormalForm(rhs.ref->first); }); + + for (auto itCase = it; itCase != itEndEq;) + { + //find equal range: respect case, ignore Unicode normal forms + auto itEndCase = std::find_if(itCase + 1, itEndEq, [&](const FileRef& fr) { return getUnicodeNormalForm(fr.ref->first) != getUnicodeNormalForm(itCase->ref->first); }); + if (!tryMatchRange(itCase, itEndCase)) + { + const Zstringc& conflictMsg = getConflictAmbiguousItemName(itCase->ref->first); + std::for_each(itCase, itEndCase, [&](const FileRef& fr) + { + if (fr.side == SelectSide::left) + lo(*fr.ref, &conflictMsg); + else + ro(*fr.ref, &conflictMsg); + }); + } + itCase = itEndCase; + } + } + it = itEndEq; + } +} + + +void MergeSides::mergeFolders(const FolderContainer& lhs, const FolderContainer& rhs, const Zstringc* errorMsg, ContainerObject& output) +{ + using FileData = FolderContainer::FileList::value_type; + + matchFolders(lhs.files, rhs.files, [&](const FileData& fileLeft, const Zstringc* conflictMsg) + { + FilePair& newItem = output.addFile(fileLeft.first, fileLeft.second); + checkFailedRead(newItem, conflictMsg ? conflictMsg : errorMsg); + }, + [&](const FileData& fileRight, const Zstringc* conflictMsg) + { + FilePair& newItem = output.addFile(fileRight.first, fileRight.second); + checkFailedRead(newItem, conflictMsg ? conflictMsg : errorMsg); + }, + [&](const FileData& fileLeft, const FileData& fileRight) + { + FilePair& newItem = output.addFile(fileLeft.first, + fileLeft.second, + fileRight.first, + fileRight.second); + if (!checkFailedRead(newItem, errorMsg)) + undefinedFiles_.push_back(&newItem); + static_assert(std::is_same_v>); + //ContainerObject::addFile() must NOT invalidate references used in "undefinedFiles"! + }); + + //----------------------------------------------------------------------------------------------- + using SymlinkData = FolderContainer::SymlinkList::value_type; + + matchFolders(lhs.symlinks, rhs.symlinks, [&](const SymlinkData& symlinkLeft, const Zstringc* conflictMsg) + { + SymlinkPair& newItem = output.addSymlink(symlinkLeft.first, symlinkLeft.second); + checkFailedRead(newItem, conflictMsg ? conflictMsg : errorMsg); + }, + [&](const SymlinkData& symlinkRight, const Zstringc* conflictMsg) + { + SymlinkPair& newItem = output.addSymlink(symlinkRight.first, symlinkRight.second); + checkFailedRead(newItem, conflictMsg ? conflictMsg : errorMsg); + }, + [&](const SymlinkData& symlinkLeft, const SymlinkData& symlinkRight) //both sides + { + SymlinkPair& newItem = output.addSymlink(symlinkLeft.first, + symlinkLeft.second, + symlinkRight.first, + symlinkRight.second); + if (!checkFailedRead(newItem, errorMsg)) + undefinedSymlinks_.push_back(&newItem); + }); + + //----------------------------------------------------------------------------------------------- + using FolderData = FolderContainer::FolderList::value_type; + + matchFolders(lhs.folders, rhs.folders, [&](const FolderData& dirLeft, const Zstringc* conflictMsg) + { + FolderPair& newFolder = output.addFolder(dirLeft.first, dirLeft.second.first); + const Zstringc* errorMsgNew = checkFailedRead(newFolder, conflictMsg ? conflictMsg : errorMsg); + this->fillOneSide(dirLeft.second.second, errorMsgNew, newFolder); //recurse + }, + [&](const FolderData& dirRight, const Zstringc* conflictMsg) + { + FolderPair& newFolder = output.addFolder(dirRight.first, dirRight.second.first); + const Zstringc* errorMsgNew = checkFailedRead(newFolder, conflictMsg ? conflictMsg : errorMsg); + this->fillOneSide(dirRight.second.second, errorMsgNew, newFolder); //recurse + }, + [&](const FolderData& dirLeft, const FolderData& dirRight) + { + FolderPair& newFolder = output.addFolder(dirLeft.first, dirLeft.second.first, dirRight.first, dirRight.second.first); + const Zstringc* errorMsgNew = checkFailedRead(newFolder, errorMsg); + mergeFolders(dirLeft.second.second, dirRight.second.second, errorMsgNew, newFolder); //recurse + }); +} + +//----------------------------------------------------------------------------------------------- + +//uncheck excluded directories (see parallelFolderScan()) + remove superfluous excluded subdirectories +void stripExcludedDirectories(ContainerObject& conObj, const PathFilter& filter) +{ + for (FolderPair& folder : conObj.subfolders()) + stripExcludedDirectories(folder, filter); + + /* remove superfluous directories: + this does not invalidate "std::vector& undefinedFiles", since we delete folders only + and there is no side-effect for memory positions of FilePair and SymlinkPair thanks to SharedRef! */ + static_assert(std::is_same_v>); + + conObj.foldersRemoveIf([&](FolderPair& folder) + { + const bool included = folder.passDirFilter(filter, nullptr /*childItemMightMatch*/); //child items were already excluded during scanning + + if (!included) //falsify only! (e.g. might already be inactive due to read error!) + folder.setActive(false); + + return !included && //don't check active status, but eval filter directly! + folder.subfolders().empty() && + folder.symlinks ().empty() && + folder.files ().empty(); + }); +} + + +//create comparison result table and fill category except for files existing on both sides: undefinedFiles and undefinedSymlinks are appended! +SharedRef ComparisonBuffer::performComparison(const ResolvedFolderPair& fp, + const FolderPairCfg& fpCfg, + std::vector& undefinedFiles, + std::vector& undefinedSymlinks) const +{ + cb_.updateStatus(_("Generating file list...")); //throw X + cb_.requestUiUpdate(true /*force*/); //throw X + + const BaseFolderStatus folderStatusL = getBaseFolderStatus(fp.folderPathLeft); + const BaseFolderStatus folderStatusR = getBaseFolderStatus(fp.folderPathRight); + + + std::unordered_map failedReadsL; //base-relative paths or empty if read-error for whole base directory + std::unordered_map failedReadsR; // + const FolderContainer* folderContL = nullptr; + const FolderContainer* folderContR = nullptr; + + + const FolderContainer empty; + if (folderStatusL == BaseFolderStatus::failure || + folderStatusR == BaseFolderStatus::failure) + { + auto it = folderStatus_.failedChecks.find(fp.folderPathLeft); + if (it == folderStatus_.failedChecks.end()) + it = folderStatus_.failedChecks.find(fp.folderPathRight); + + failedReadsL[Zstring() /*empty string for root*/] = failedReadsR[Zstring()] = utfTo(it->second.toString()); + + folderContL = ∅ //no need to list or display one-sided results if + folderContR = ∅ //*any* folder existence check failed (even if other side exists in folderBuffer_!) + } + else + { + auto evalBuffer = [&](const AbstractPath& folderPath, const FolderContainer*& folderCont, std::unordered_map& failedReads) + { + auto it = folderBuffer_.find({folderPath, fpCfg.filter.nameFilter, fpCfg.handleSymlinks}); + if (it != folderBuffer_.end()) + { + const DirectoryValue& dirVal = it->second; + + //mix failedFolderReads with failedItemReads: + //associate folder traversing errors with folder (instead of child items only) to show on GUI! See "MergeSides" + //=> minor pessimization for "excludeFilterFailedRead" which needlessly excludes parent folders, too + failedReads = dirVal.failedFolderReads; //failedReads.insert(dirVal.failedFolderReads.begin(), dirVal.failedFolderReads.end()); + failedReads.insert(dirVal.failedItemReads.begin(), dirVal.failedItemReads.end()); + + assert(getBaseFolderStatus(folderPath) == BaseFolderStatus::existing); + folderCont = &dirVal.folderCont; + } + else + { + assert(getBaseFolderStatus(folderPath) == BaseFolderStatus::notExisting); //including AFS::isNullPath() + folderCont = ∅ + } + }; + evalBuffer(fp.folderPathLeft, folderContL, failedReadsL); + evalBuffer(fp.folderPathRight, folderContR, failedReadsR); + } + + + Zstring excludeFilterFailedRead; + if (failedReadsL.contains(Zstring()) || + failedReadsR.contains(Zstring())) //empty path if read-error for whole base directory + excludeFilterFailedRead += Zstr("*\n"); + else + { + for (const auto& [relPath, errorMsg] : failedReadsL) + excludeFilterFailedRead += relPath + Zstr('\n'); //exclude item AND (potential) child items! + + for (const auto& [relPath, errorMsg] : failedReadsR) + excludeFilterFailedRead += relPath + Zstr('\n'); + } + + //somewhat obscure, but it's possible on Linux file systems to have a backslash as part of a file name + //=> avoid misinterpretation when parsing the filter phrase in PathFilter (see path_filter.cpp::parseFilterPhrase()) + if constexpr (FILE_NAME_SEPARATOR != Zstr('/' )) replace(excludeFilterFailedRead, Zstr('/'), Zstr('?')); + if constexpr (FILE_NAME_SEPARATOR != Zstr('\\')) replace(excludeFilterFailedRead, Zstr('\\'), Zstr('?')); + + + SharedRef output = makeSharedRef(fp.folderPathLeft, + folderStatusL, //check folder existence only once! + fp.folderPathRight, + folderStatusR, // + fpCfg.filter.nameFilter.ref().copyFilterAddingExclusion(excludeFilterFailedRead), + fpCfg.compareVar, + fileTimeTolerance_, + fpCfg.ignoreTimeShiftMinutes); + //PERF_START; + MergeSides::execute(*folderContL, *folderContR, failedReadsL, failedReadsR, + output.ref(), undefinedFiles, undefinedSymlinks); + //PERF_STOP; + + //##################### in/exclude rows according to filtering ##################### + //NOTE: we need to finish de-activating rows BEFORE binary comparison is run so that it can skip them! + + //attention: some excluded directories are still in the comparison result! (see include filter handling!) + if (!fpCfg.filter.nameFilter.ref().isNull()) + stripExcludedDirectories(output.ref(), fpCfg.filter.nameFilter.ref()); //mark excluded directories (see parallelFolderScan()) + remove superfluous excluded subdirectories + + //apply soft filtering (hard filter already applied during traversal!) + addSoftFiltering(output.ref(), fpCfg.filter.timeSizeFilter); + + //################################################################################## + return output; +} +} + + +FolderComparison fff::compare(WarningDialogs& warnings, + unsigned int fileTimeTolerance, + const AFS::RequestPasswordFun& requestPassword /*throw X*/, + bool runWithBackgroundPriority, + bool createDirLocks, + std::unique_ptr& dirLocks, + const std::vector& fpCfgList, + ProcessCallback& callback /*throw X*/) //throw X +{ + //indicator at the very beginning of the log to make sense of "total time" + //init process: keep at beginning so that all GUI elements are initialized properly + callback.initNewPhase(-1, -1, ProcessPhase::scan); //throw X; it's unknown how many files will be scanned => -1 objects + //callback.logInfo(Comparison started")); -> still useful? + + //------------------------------------------------------------------------------- + + //prevent operating system going into sleep state + std::optional noStandby; + try + { + noStandby.emplace(runWithBackgroundPriority ? ProcessPriority::background : ProcessPriority::normal); //throw FileError + } + catch (const FileError& e) //failure is not critical => log only + { + callback.logMessage(e.toString(), PhaseCallback::MsgType::warning); //throw X + } + + const ResolvedBaseFolders& resInfo = initializeBaseFolders(fpCfgList, + requestPassword, warnings, callback); //throw X + //directory existence only checked *once* to avoid race conditions! + if (resInfo.resolvedPairs.size() != fpCfgList.size()) + throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); + + std::vector> workLoad; + for (size_t i = 0; i < fpCfgList.size(); ++i) + workLoad.emplace_back(resInfo.resolvedPairs[i], fpCfgList[i]); + + //-----------execute basic checks all at once before starting comparison---------- + + //check for incomplete input + { + bool haveFullPair = false; + std::wstring partialPairList; + + for (const ResolvedFolderPair& fp : resInfo.resolvedPairs) + if (AFS::isNullPath(fp.folderPathLeft) != AFS::isNullPath(fp.folderPathRight)) + { + partialPairList += L"\n" + + (AFS::isNullPath(fp.folderPathLeft ) ? L"<" + _("empty") + L">" : AFS::getDisplayPath(fp.folderPathLeft)) + L" | " + + (AFS::isNullPath(fp.folderPathRight) ? L"<" + _("empty") + L">" : AFS::getDisplayPath(fp.folderPathRight)); + } + else if (!AFS::isNullPath(fp.folderPathLeft)) + haveFullPair = true; + + //error if: all empty or exist both full and partial pairs -> support single-folder comparison scenario + if (!partialPairList.empty() == haveFullPair) + callback.reportFatalError(trimCpy(_("A folder input field is empty.") + L" \n\n" + + _("Please select both left and right folders for synchronization.") + L"\n" + partialPairList)); //throw X + else if (!partialPairList.empty()) //partial pairs only => maybe deliberate, maybe accidental, so give some hint + callback.logMessage(_("A folder input field is empty.") + L"\n" + partialPairList, PhaseCallback::MsgType::warning); //throw X + } + + //Check whether one side is a sub directory of the other side (folder-pair-wise!) + //The similar check (warnDependentBaseFolders) if one directory is read/written by multiple pairs not before beginning of synchronization + { + std::wstring msg; + bool shouldExclude = false; + + for (const auto& [folderPair, fpCfg] : workLoad) + if (std::optional pd = getFolderPathDependency(folderPair.folderPathLeft, fpCfg.filter.nameFilter.ref(), + folderPair.folderPathRight, fpCfg.filter.nameFilter.ref())) + { + msg += L"\n\n" + + AFS::getDisplayPath(folderPair.folderPathLeft) + L" <-> " + L'\n' + + AFS::getDisplayPath(folderPair.folderPathRight); + if (!pd->relPath.empty()) + { + shouldExclude = true; + msg += std::wstring() + L'\n' + L"⇒ " + + _("Exclude:") + L' ' + utfTo(FILE_NAME_SEPARATOR + pd->relPath + FILE_NAME_SEPARATOR); + } + } + + if (!msg.empty()) + callback.reportWarning(_("One folder of the folder pair is a subfolder of the other.") + + (shouldExclude ? L'\n' + _("The folder should be excluded via filter.") : L"") + + msg, warnings.warnDependentFolderPair); //throw X + } + //-------------------end of basic checks------------------------------------------ + + //lock (existing) directories before comparison + if (createDirLocks) + { + std::set folderPathsToLock; + for (const AbstractPath& folderPath : resInfo.baseFolderStatus.existing) + if (const Zstring& nativePath = getNativeItemPath(folderPath); //restrict directory locking to native paths until further + !nativePath.empty()) + folderPathsToLock.insert(nativePath); + + dirLocks = std::make_unique(folderPathsToLock, warnings.warnDirectoryLockFailed, callback); + } + + try + { + FolderComparison output; + //reduce peak memory by restricting lifetime of ComparisonBuffer to have ended when loading potentially huge InSyncFolder instance in redetermineSyncDirection() + { + //------------------- fill directory buffer: traverse/read folders -------------------------- + ComparisonBuffer cmpBuf(resInfo.baseFolderStatus, + fileTimeTolerance, callback); + //PERF_START; + output = cmpBuf.execute(workLoad); + //PERF_STOP; + } + assert(output.size() == fpCfgList.size()); + + //--------- set initial sync-direction -------------------------------------------------- + std::vector> directCfgs; + for (auto it = output.begin(); it != output.end(); ++it) + directCfgs.emplace_back(&it->ref(), fpCfgList[it - output.begin()].directionCfg); + + redetermineSyncDirection(directCfgs, + callback); //throw X + + return output; + } + catch (const std::bad_alloc& e) + { + callback.reportFatalError(_("Out of memory.") + L' ' + utfTo(e.what())); + return {}; + } +} diff --git a/FreeFileSync/Source/base/comparison.h b/FreeFileSync/Source/base/comparison.h new file mode 100644 index 0000000..903a827 --- /dev/null +++ b/FreeFileSync/Source/base/comparison.h @@ -0,0 +1,60 @@ +// ***************************************************************************** +// * 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 COMPARISON_H_8032178534545426 +#define COMPARISON_H_8032178534545426 + +#include "file_hierarchy.h" +#include "process_callback.h" +#include "norm_filter.h" +#include "lock_holder.h" + + +namespace fff +{ +struct FolderPairCfg +{ + FolderPairCfg(const Zstring& folderPathPhraseLeft, + const Zstring& folderPathPhraseRight, + CompareVariant cmpVar, + SymLinkHandling handleSymlinksIn, + const std::vector& ignoreTimeShiftMinutesIn, + const NormalizedFilter& filterIn, + const SyncDirectionConfig& directCfg) : + folderPathPhraseLeft_ (folderPathPhraseLeft), + folderPathPhraseRight_(folderPathPhraseRight), + compareVar(cmpVar), + handleSymlinks(handleSymlinksIn), + ignoreTimeShiftMinutes(ignoreTimeShiftMinutesIn), + filter(filterIn), + directionCfg(directCfg) {} + + Zstring folderPathPhraseLeft_; //unresolved directory names as entered by user! + Zstring folderPathPhraseRight_; // + + CompareVariant compareVar; + SymLinkHandling handleSymlinks; + std::vector ignoreTimeShiftMinutes; + + NormalizedFilter filter; + + SyncDirectionConfig directionCfg; +}; + +std::vector extractCompareCfg(const MainConfiguration& mainCfg); //fill FolderPairCfg and resolve folder pairs + +//FFS core routine: output.size() == fpCfgList.size() or 0 on fatal error +FolderComparison compare(WarningDialogs& warnings, + unsigned int fileTimeTolerance, + const AFS::RequestPasswordFun& requestPassword /*throw X*/, + bool runWithBackgroundPriority, + bool createDirLocks, + std::unique_ptr& dirLocks, //out + const std::vector& fpCfgList, + ProcessCallback& callback /*throw X*/); //throw X +} + +#endif //COMPARISON_H_8032178534545426 diff --git a/FreeFileSync/Source/base/db_file.cpp b/FreeFileSync/Source/base/db_file.cpp new file mode 100644 index 0000000..34061e3 --- /dev/null +++ b/FreeFileSync/Source/base/db_file.cpp @@ -0,0 +1,1047 @@ +// ***************************************************************************** +// * 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 "db_file.h" +#include //std::endian +#include +#include +#include +#include "../afs/native.h" +#include "status_handler_impl.h" + +using namespace zen; +using namespace fff; + + +namespace +{ +//------------------------------------------------------------------------------------------------------------------------------- +const char DB_FILE_DESCR[] = "FreeFileSync"; +const int DB_FILE_VERSION = 11; //2020-02-07 +const int DB_STREAM_VERSION = 5; //2023-07-29 +//------------------------------------------------------------------------------------------------------------------------------- + +struct SessionData +{ + bool isLeadStream = false; + std::string rawStream; + + bool operator==(const SessionData&) const = default; +}; + + +using UniqueId = std::string; +using DbStreams = std::unordered_map; //list of streams by session GUID + +/*------------------------------------------------------------------------------ + | ensure 32/64 bit portability: use fixed size data types only e.g. uint32_t | + ------------------------------------------------------------------------------*/ + +template inline +AbstractPath getDatabaseFilePath(const BaseFolderPair& baseFolder) +{ + static_assert(std::endian::native == std::endian::little); + /* Windows, Linux, macOS considerations for uniform database format: + - different file IDs: no, but the volume IDs are different! + - problem with case sensitivity: no + - are UTC file times identical: yes (at least with 1 sec precision) + - endianess: FFS currently not running on any big-endian platform + - precomposed/decomposed UTF: differences already ignored + - 32 vs 64-bit: already handled + + => give DB files different names: */ + const Zstring dbName = Zstr(".sync"); //files beginning with dots are usually hidden + return AFS::appendRelPath(baseFolder.getAbstractPath(), dbName + SYNC_DB_FILE_ENDING); +} + +//####################################################################################################################################### + +void saveStreams(const DbStreams& streamList, const AbstractPath& dbPath, const IoCallback& notifyUnbufferedIO /*throw X*/) //throw FileError, X +{ + MemoryStreamOut memStreamOut; + + //write FreeFileSync file identifier + writeArray(memStreamOut, DB_FILE_DESCR, sizeof(DB_FILE_DESCR)); + + //save file format version + writeNumber(memStreamOut, DB_FILE_VERSION); + + //write stream list + writeNumber(memStreamOut, static_cast(streamList.size())); + + for (const auto& [sessionID, sessionData] : streamList) + { + writeContainer(memStreamOut, sessionID); + + writeNumber(memStreamOut, sessionData.isLeadStream); + writeContainer (memStreamOut, sessionData.rawStream); + } + + writeNumber(memStreamOut, getCrc32(memStreamOut.ref())); + //------------------------------------------------------------------------------------------------------------------------ + + //already existing: undefined behavior! (e.g. fail/overwrite/auto-rename) + const std::unique_ptr byteStreamOut = AFS::getOutputStream(dbPath, + memStreamOut.ref().size(), + std::nullopt /*modTime*/); //throw FileError + + unbufferedSave(memStreamOut.ref(), [&](const void* buffer, size_t bytesToWrite) + { + return byteStreamOut->tryWrite(buffer, bytesToWrite, notifyUnbufferedIO); //throw FileError, X + }, + byteStreamOut->getBlockSize()); //throw FileError, X + + byteStreamOut->finalize(notifyUnbufferedIO); //throw FileError, X + +} + + +DEFINE_NEW_FILE_ERROR(FileErrorDatabaseNotExisting) +DEFINE_NEW_FILE_ERROR(FileErrorDatabaseCorrupted) + +DbStreams loadStreams(const AbstractPath& dbPath, const IoCallback& notifyUnbufferedIO /*throw X*/) //throw FileError, FileErrorDatabaseNotExisting, FileErrorDatabaseCorrupted, X +{ + std::string byteStream; + try + { + const std::unique_ptr fileIn = AFS::getInputStream(dbPath); //throw FileError, ErrorFileLocked + + byteStream = unbufferedLoad([&](void* buffer, size_t bytesToRead) + { + return fileIn->tryRead(buffer, bytesToRead, notifyUnbufferedIO); //throw FileError, ErrorFileLocked, X; may return short, only 0 means EOF! + }, + fileIn->getBlockSize()); //throw FileError, X + } + catch (const FileError& e) + { + bool dbNotYetExisting = false; + try { dbNotYetExisting = !AFS::itemExists(dbPath); /*throw FileError*/ } + //abstract context => unclear which exception is more relevant/useless: + catch (const FileError& e2) { throw FileError(replaceCpy(e.toString(), L"\n\n", L'\n'), replaceCpy(e2.toString(), L"\n\n", L'\n')); } + //caveat: merging FileError might create redundant error message: https://freefilesync.org/forum/viewtopic.php?t=9377 + + if (dbNotYetExisting) //throw FileError + throw FileErrorDatabaseNotExisting(replaceCpy(_("Database file %x does not yet exist."), L"%x", fmtPath(AFS::getDisplayPath(dbPath)))); + else + throw; + } + //------------------------------------------------------------------------------------------------------------------------ + try + { + MemoryStreamIn memStreamIn(byteStream); + + char formatDescr[sizeof(DB_FILE_DESCR)] = {}; + readArray(memStreamIn, formatDescr, sizeof(formatDescr)); //throw SysErrorUnexpectedEos + + if (!std::equal(DB_FILE_DESCR, DB_FILE_DESCR + sizeof(DB_FILE_DESCR), formatDescr)) + throw SysError(_("File content is corrupted.") + L" (invalid header)"); + + const int version = readNumber(memStreamIn); //throw SysErrorUnexpectedEos + 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 if (version == DB_FILE_VERSION) //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 crcStreamOut; + writeNumber(crcStreamOut, getCrc32(byteStream.begin(), byteStream.end() - sizeof(uint32_t))); + + if (!endsWith(byteStream, crcStreamOut.ref())) + throw SysError(_("File content is corrupted.") + L" (invalid checksum)"); + } + else + throw SysError(_("Unsupported data format.") + L' ' + replaceCpy(_("Version: %x"), L"%x", numberTo(version))); + + DbStreams output; + + //read stream list + size_t streamCount = readNumber(memStreamIn); //throw SysErrorUnexpectedEos + while (streamCount-- != 0) + { + std::string sessionID = readContainer(memStreamIn); //throw SysErrorUnexpectedEos + + SessionData sessionData = {}; + + if (version == 9) //TODO: remove migration code at some time! v9 used until 2017-02-01 + { + sessionData.rawStream = readContainer(memStreamIn); //throw SysErrorUnexpectedEos + + MemoryStreamIn streamIn(sessionData.rawStream); + const int streamVersion = readNumber(streamIn); //throw SysErrorUnexpectedEos + if (streamVersion != 2) //don't throw here due to old stream formats + continue; + sessionData.isLeadStream = readNumber(streamIn) != 0; //throw SysErrorUnexpectedEos + } + else + { + sessionData.isLeadStream = readNumber (memStreamIn) != 0; //throw SysErrorUnexpectedEos + sessionData.rawStream = readContainer(memStreamIn); // + } + + output[sessionID] = std::move(sessionData); + } + return output; + } + catch (const SysError& e) + { + throw FileErrorDatabaseCorrupted(replaceCpy(_("Cannot read database file %x."), L"%x", fmtPath(AFS::getDisplayPath(dbPath))), e.toString()); + } +} + +//####################################################################################################################################### + +class StreamGenerator +{ +public: + static void execute(const InSyncFolder& dbFolder, //throw FileError + const std::wstring& displayFilePathL, //used for diagnostics only + const std::wstring& displayFilePathR, + std::string& streamL, + std::string& streamR) + { + MemoryStreamOut outL; + MemoryStreamOut outR; + //save format version + writeNumber(outL, DB_STREAM_VERSION); + writeNumber(outR, DB_STREAM_VERSION); + + auto compStream = [&](const std::string& stream) //throw FileError + { + try + { + /* Zlib: optimal level - test case 1 million files + level|size [MB]|time [ms] + 0 49.54 272 (uncompressed) + 1 14.53 1013 + 2 14.13 1106 + 3 13.76 1288 - best compromise between speed and compression + 4 13.20 1526 + 5 12.73 1916 + 6 12.58 2765 + 7 12.54 3633 + 8 12.51 9032 + 9 12.50 19698 (maximal compression) */ + return compress(stream, 3 /*level*/); //throw SysError + } + catch (const SysError& e) + { + throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(displayFilePathL + L"/" + displayFilePathR)), e.toString()); + } + }; + + StreamGenerator generator; + //PERF_START + generator.recurse(dbFolder); + //PERF_STOP + + const std::string bufText = compStream(generator.streamOutText_ .ref()); + const std::string bufSmallNum = compStream(generator.streamOutSmallNum_.ref()); + const std::string bufBigNum = compStream(generator.streamOutBigNum_ .ref()); + + MemoryStreamOut streamOut; + writeContainer(streamOut, bufText); + writeContainer(streamOut, bufSmallNum); + writeContainer(streamOut, bufBigNum); + + const std::string& buf = streamOut.ref(); + + //distribute "outputBoth" over left and right streams: + const size_t size1stPart = buf.size() / 2; + const size_t size2ndPart = buf.size() - size1stPart; + + writeNumber(outL, size1stPart); + writeNumber(outR, size2ndPart); + + if (size1stPart > 0) writeArray(outL, buf.c_str(), size1stPart); + if (size2ndPart > 0) writeArray(outR, buf.c_str() + size1stPart, size2ndPart); + + streamL = std::move(outL.ref()); + streamR = std::move(outR.ref()); + } + +private: + void recurse(const InSyncFolder& container) + { + writeNumber(streamOutSmallNum_, static_cast(container.files.size())); + for (const auto& [itemName, inSyncData] : container.files) + { + writeItemName(itemName.normStr); + writeNumber(streamOutSmallNum_, static_cast(inSyncData.cmpVar)); + writeNumber(streamOutSmallNum_, inSyncData.fileSize); + + writeFileDescr(inSyncData.left); + writeFileDescr(inSyncData.right); + } + + writeNumber(streamOutSmallNum_, static_cast(container.symlinks.size())); + for (const auto& [itemName, inSyncData] : container.symlinks) + { + writeItemName(itemName.normStr); + writeNumber(streamOutSmallNum_, static_cast(inSyncData.cmpVar)); + + writeNumber(streamOutBigNum_, inSyncData.left .modTime); + writeNumber(streamOutBigNum_, inSyncData.right.modTime); + } + + writeNumber(streamOutSmallNum_, static_cast(container.folders.size())); + for (const auto& [itemName, inSyncData] : container.folders) + { + writeItemName(itemName.normStr); + + recurse(inSyncData); + } + } + + void writeItemName(const Zstring& str) { writeContainer(streamOutText_, utfTo(str)); } + + void writeFileDescr(const InSyncDescrFile& descr) + { + writeNumber(streamOutBigNum_, descr.modTime); + writeNumber(streamOutBigNum_, descr.filePrint); + static_assert(sizeof(descr.modTime) <= sizeof(int64_t)); //ensure cross-platform compatibility! + } + + /* maximize zlib compression by grouping similar data (=> 20% size reduction!) + -> further ~5% reduction possible by having one container per data type + + other ideas: - avoid left/right side interleaving in writeFileDescr() => pessimization! + - convert CompareVariant/InSyncStatus to "enum : unsigned char" => only 0,4% size reduction! + - split up writeItemName() to use streamOutSmallNum_ + streamOutText_ => pessimization! + - use null-termination in writeItemName() => 5% size reduction (embedded zeros impossible?) + - use empty item name as sentinel => only 0,17% size reduction! + - save fileSize using instreamOutBigNum_ => pessimization! */ + MemoryStreamOut streamOutText_; // + MemoryStreamOut streamOutSmallNum_; //data with bias to lead side (= always left in this context) + MemoryStreamOut streamOutBigNum_; // +}; + + +class StreamParser +{ +public: + static SharedRef execute(bool leadStreamLeft, //throw FileError + const std::string& streamL, + const std::string& streamR, + const std::wstring& displayFilePathL, //for diagnostics only + const std::wstring& displayFilePathR) + { + try + { + MemoryStreamIn streamInL(streamL); + MemoryStreamIn streamInR(streamR); + + const int streamVersion = readNumber(streamInL); //throw SysErrorUnexpectedEos + const int streamVersionR = readNumber(streamInR); // + + if (streamVersion != streamVersionR) + throw SysError(_("File content is corrupted.") + L" (different stream formats)"); + + //TODO: remove migration code at some time! 2017-02-01 + if (streamVersion == 2) + { + const bool has1stPartL = readNumber(streamInL) != 0; //throw SysErrorUnexpectedEos + const bool has1stPartR = readNumber(streamInR) != 0; // + + if (has1stPartL == has1stPartR) + throw SysError(_("File content is corrupted.") + L" (second stream part missing)"); + if (has1stPartL != leadStreamLeft) + throw SysError(_("File content is corrupted.") + L" (has1stPartL != leadStreamLeft)"); + + MemoryStreamIn& in1stPart = leadStreamLeft ? streamInL : streamInR; + MemoryStreamIn& in2ndPart = leadStreamLeft ? streamInR : streamInL; + + const size_t size1stPart = static_cast(readNumber(in1stPart)); + const size_t size2ndPart = static_cast(readNumber(in2ndPart)); + + std::string tmpB(size1stPart + size2ndPart, '\0'); //throw std::bad_alloc + readArray(in1stPart, tmpB.data(), size1stPart); //stream always non-empty + readArray(in2ndPart, tmpB.data() + size1stPart, size2ndPart); //throw SysErrorUnexpectedEos + + const std::string tmpL = readContainer(streamInL); + const std::string tmpR = readContainer(streamInR); + + auto output = makeSharedRef(); + StreamParserV2 parser(decompress(tmpL), // + decompress(tmpR), //throw SysError + decompress(tmpB)); // + parser.recurse(output.ref()); //throw SysError + return output; + } + else if (streamVersion == 3 || //TODO: remove migration code at some time! 2021-02-14 + streamVersion == 4 || //TODO: remove migration code at some time! 2023-07-29 + streamVersion == DB_STREAM_VERSION) + { + MemoryStreamIn& streamInPart1 = leadStreamLeft ? streamInL : streamInR; + MemoryStreamIn& streamInPart2 = leadStreamLeft ? streamInR : streamInL; + + const size_t sizePart1 = static_cast(readNumber(streamInPart1)); + const size_t sizePart2 = static_cast(readNumber(streamInPart2)); + + std::string buf(sizePart1 + sizePart2, '\0'); + if (sizePart1 > 0) readArray(streamInPart1, buf.data(), sizePart1); //throw SysErrorUnexpectedEos + if (sizePart2 > 0) readArray(streamInPart2, buf.data() + sizePart1, sizePart2); // + + MemoryStreamIn streamIn(buf); + const std::string bufText = readContainer(streamIn); // + const std::string bufSmallNum = readContainer(streamIn); //throw SysErrorUnexpectedEos + const std::string bufBigNum = readContainer(streamIn); // + + auto output = makeSharedRef(); + StreamParser parser(streamVersion, + decompress(bufText), // + decompress(bufSmallNum), //throw SysError + decompress(bufBigNum)); // + if (leadStreamLeft) + parser.recurse(output.ref()); //throw SysError + else + parser.recurse(output.ref()); //throw SysError + return output; + } + else + throw SysError(_("Unsupported data format.") + L' ' + replaceCpy(_("Version: %x"), L"%x", numberTo(streamVersion))); + } + catch (const SysError& e) + { + throw FileError(replaceCpy(_("Cannot read database file %x."), L"%x", fmtPath(displayFilePathL) + L", " + fmtPath(displayFilePathR)), e.toString()); + } + } + +private: + StreamParser(int streamVersion, + std::string&& bufText, + std::string&& bufSmallNumbers, + std::string&& bufBigNumbers) : + streamVersion_(streamVersion), + bufText_ (std::move(bufText)), + bufSmallNumbers_(std::move(bufSmallNumbers)), + bufBigNumbers_ (std::move(bufBigNumbers)) {} + + template + void recurse(InSyncFolder& container) //throw SysError + { + size_t fileCount = readNumber(streamInSmallNum_); //throw SysErrorUnexpectedEos + while (fileCount-- != 0) + { + const Zstring itemName = readItemName(); // + const auto cmpVar = static_cast(readNumber(streamInSmallNum_)); // + const uint64_t fileSize = readNumber(streamInSmallNum_); // + + const InSyncDescrFile descrL = readFileDescr(); //throw SysErrorUnexpectedEos + const InSyncDescrFile descrT = readFileDescr(); // + + container.addFile(itemName, + selectParam(descrL, descrT), + selectParam(descrT, descrL), cmpVar, fileSize); + } + + size_t linkCount = readNumber(streamInSmallNum_); + while (linkCount-- != 0) + { + const Zstring itemName = readItemName(); // + const auto cmpVar = static_cast(readNumber(streamInSmallNum_)); // + + const InSyncDescrLink descrL{static_cast(readNumber(streamInBigNum_))}; //throw SysErrorUnexpectedEos + const InSyncDescrLink descrT{static_cast(readNumber(streamInBigNum_))}; // + + container.addSymlink(itemName, + selectParam(descrL, descrT), + selectParam(descrT, descrL), cmpVar); + } + + size_t dirCount = readNumber(streamInSmallNum_); // + while (dirCount-- != 0) + { + const Zstring itemName = readItemName(); // + + if (streamVersion_ <= 4) //TODO: remove migration code at some time! 2023-07-29 + /*const auto status = static_cast(*/ readNumber(streamInSmallNum_); + + InSyncFolder& dbFolder = container.addFolder(itemName); + recurse(dbFolder); + } + } + + Zstring readItemName() { return utfTo(readContainer(streamInText_)); } //throw SysErrorUnexpectedEos + + InSyncDescrFile readFileDescr() //throw SysErrorUnexpectedEos + { + const auto modTime = static_cast(readNumber(streamInBigNum_)); //throw SysErrorUnexpectedEos + + AFS::FingerPrint filePrint = 0; + if (streamVersion_ == 3) //TODO: remove migration code at some time! 2021-02-14 + { + const auto& devFileId = readContainer(streamInBigNum_); //throw SysErrorUnexpectedEos + ino_t fileIndex = 0; + if (devFileId.size() == sizeof(dev_t) + sizeof(fileIndex)) + { + std::memcpy(&fileIndex, &devFileId[devFileId.size() - sizeof(fileIndex)], sizeof(fileIndex)); + filePrint = fileIndex; + } + else assert(devFileId.empty()); + } + else + filePrint = readNumber(streamInBigNum_); //throw SysErrorUnexpectedEos + + return {modTime, filePrint}; + } + + //TODO: remove migration code at some time! 2017-02-01 + class StreamParserV2 + { + public: + StreamParserV2(std::string&& bufferL, + std::string&& bufferR, + std::string&& bufferB) : + bufL_(std::move(bufferL)), + bufR_(std::move(bufferR)), + bufB_(std::move(bufferB)) {} + + void recurse(InSyncFolder& container) //throw SysError + { + size_t fileCount = readNumber(inputBoth_); + while (fileCount-- != 0) + { + const Zstring itemName = utfTo(readContainer(inputBoth_)); + const auto cmpVar = static_cast(readNumber(inputBoth_)); + const uint64_t fileSize = readNumber(inputBoth_); + const auto modTimeL = static_cast(readNumber(inputLeft_)); + /*const auto fileIdL =*/ readContainer(inputLeft_); + const auto modTimeR = static_cast(readNumber(inputRight_)); + /*const auto fileIdR =*/ readContainer(inputRight_); + container.addFile(itemName, InSyncDescrFile{modTimeL, AFS::FingerPrint()}, InSyncDescrFile{modTimeR, AFS::FingerPrint()}, cmpVar, fileSize); + } + + size_t linkCount = readNumber(inputBoth_); + while (linkCount-- != 0) + { + const Zstring itemName = utfTo(readContainer(inputBoth_)); + const auto cmpVar = static_cast(readNumber(inputBoth_)); + const auto modTimeL = static_cast(readNumber(inputLeft_)); + const auto modTimeR = static_cast(readNumber(inputRight_)); + container.addSymlink(itemName, InSyncDescrLink{modTimeL}, InSyncDescrLink{modTimeR}, cmpVar); + } + + size_t dirCount = readNumber(inputBoth_); + while (dirCount-- != 0) + { + const Zstring itemName = utfTo(readContainer(inputBoth_)); + /*const auto status = static_cast(*/ readNumber(inputBoth_); + + InSyncFolder& dbFolder = container.addFolder(itemName); + recurse(dbFolder); + } + } + + private: + const std::string bufL_; + const std::string bufR_; + const std::string bufB_; + MemoryStreamIn inputLeft_ {bufL_}; //data related to one side only + MemoryStreamIn inputRight_{bufR_}; // + MemoryStreamIn inputBoth_ {bufB_}; //data concerning both sides + }; + + const int streamVersion_; + const std::string bufText_; + const std::string bufSmallNumbers_; + const std::string bufBigNumbers_ ; + MemoryStreamIn streamInText_ {bufText_}; // + MemoryStreamIn streamInSmallNum_{bufSmallNumbers_}; //data with bias to lead side + MemoryStreamIn streamInBigNum_ {bufBigNumbers_}; // +}; + +//####################################################################################################################################### + +class LastSynchronousStateUpdater +{ + /* 1. filter by file name does *not* create a new hierarchy, but merely gives a different *view* on the existing file hierarchy + => only update database entries matching this view! + 2. Symlink handling *does* create a new (asymmetric) hierarchy during comparison + => update all database entries! */ +public: + static void execute(const BaseFolderPair& baseFolder, InSyncFolder& dbFolder) + { + LastSynchronousStateUpdater updater(baseFolder.getCompVariant(), baseFolder.getFilter()); + updater.recurse(baseFolder, Zstring(), dbFolder); + } + +private: + LastSynchronousStateUpdater(CompareVariant activeCmpVar, const PathFilter& filter) : + filter_(filter), + activeCmpVar_(activeCmpVar) {} + + void recurse(const ContainerObject& conObj, const Zstring& relPath, InSyncFolder& dbFolder) + { + processFiles (conObj, relPath, dbFolder.files); + processLinks (conObj, relPath, dbFolder.symlinks); + processFolders(conObj, relPath, dbFolder.folders); + } + + void processFiles(const ContainerObject& conObj, const Zstring& parentRelPath, InSyncFolder::FileList& dbFiles) + { + std::unordered_set toPreserve; + + for (const FilePair& file : conObj.files()) + if (!file.isPairEmpty()) + { + if (file.getCategory() == FILE_EQUAL) //data in sync: write current state + { + //Caveat: If FILE_EQUAL, we *implicitly* assume equal left and right file names matching case: InSyncFolder's mapping tables use file name as a key! + //This makes us silently dependent from code in algorithm.h!!! + assert(file.hasEquivalentItemNames()); + const Zstring& fileName = file.getItemName(); + assert(file.getFileSize() == file.getFileSize()); + + //create or update new "in-sync" state + dbFiles.insert_or_assign(fileName, InSyncFile + { + .left = InSyncDescrFile{file.getLastWriteTime(), file.getFilePrint()}, + .right = InSyncDescrFile{file.getLastWriteTime(), file.getFilePrint()}, + .cmpVar = activeCmpVar_, + .fileSize = file.getFileSize(), + }); + toPreserve.insert(fileName); + } + else //not in sync: preserve last synchronous state + { + toPreserve.insert(file.getItemName()); //left/right may differ in case! + toPreserve.insert(file.getItemName()); // + } + } + + //delete removed items (= "in-sync") from database + std::erase_if(dbFiles, [&](const InSyncFolder::FileList::value_type& v) + { + if (toPreserve.contains(v.first)) + return false; + //all items not existing in "currentFiles" have either been deleted meanwhile or been excluded via filter: + const Zstring& itemRelPath = appendPath(parentRelPath, v.first.normStr); + return filter_.passFileFilter(itemRelPath); + //note: items subject to traveral errors are also excluded by this file filter here! see comparison.cpp, modified file filter for read errors + }); + } + + void processLinks(const ContainerObject& conObj, const Zstring& parentRelPath, InSyncFolder::SymlinkList& dbSymlinks) + { + std::unordered_set toPreserve; + + for (const SymlinkPair& symlink : conObj.symlinks()) + if (!symlink.isPairEmpty()) + { + if (symlink.getLinkCategory() == SYMLINK_EQUAL) //data in sync: write current state + { + assert(symlink.hasEquivalentItemNames()); + const Zstring& linkName = symlink.getItemName(); + + //create or update new "in-sync" state + dbSymlinks.insert_or_assign(linkName, InSyncSymlink + { + .left = InSyncDescrLink{symlink.getLastWriteTime()}, + .right = InSyncDescrLink{symlink.getLastWriteTime()}, + .cmpVar = activeCmpVar_, + }); + toPreserve.insert(linkName); + } + else //not in sync: preserve last synchronous state + { + toPreserve.insert(symlink.getItemName()); //left/right may differ in case! + toPreserve.insert(symlink.getItemName()); // + } + } + + //delete removed items (= "in-sync") from database + std::erase_if(dbSymlinks, [&](const InSyncFolder::SymlinkList::value_type& v) + { + if (toPreserve.contains(v.first)) + return false; + //all items not existing in "currentSymlinks" have either been deleted meanwhile or been excluded via filter: + const Zstring& itemRelPath = appendPath(parentRelPath, v.first.normStr); + return filter_.passFileFilter(itemRelPath); + }); + } + + void processFolders(const ContainerObject& conObj, const Zstring& parentRelPath, InSyncFolder::FolderList& dbFolders) + { + std::unordered_map toPreserve; + + for (const FolderPair& folder : conObj.subfolders()) + if (!folder.isPairEmpty()) + { + if (folder.getDirCategory() == DIR_EQUAL) + { + assert(folder.hasEquivalentItemNames()); + const Zstring& folderName = folder.getItemName(); + + //create directory entry if not existing (but do *not touch* existing child elements!!!) + dbFolders.try_emplace(folderName); + + toPreserve.emplace(folderName, &folder); + } + else //not in sync: preserve last synchronous state + { + toPreserve.emplace(folder.getItemName(), &folder); //names differing (in case)? => treat like any other folder rename + toPreserve.emplace(folder.getItemName(), &folder); //=> no *new* database entries even if child items are in sync + //BUT: update existing one: there should be only *one* DB entry after a folder rename (matching either folder name on left or right) + } + } + + //delete removed items (= "in-sync") from database + eraseIf(dbFolders, [&](InSyncFolder::FolderList::value_type& v) + { + const Zstring& itemRelPath = appendPath(parentRelPath, v.first.normStr); + + if (auto it = toPreserve.find(v.first); it != toPreserve.end()) + { + recurse(*(it->second), itemRelPath, v.second); //required even if e.g. DIR_LEFT_ONLY: + //existing child-items may not be in sync, but items deleted on both sides *are* in-sync!!! + return false; + } + + //if folder is not included in "current folders", it is either not existing anymore, in which case it should be deleted from database + //or it was excluded via filter and the database entry should be preserved + bool childItemMightMatch = true; + const bool passFilter = filter_.passDirFilter(itemRelPath, &childItemMightMatch); + if (!passFilter && childItemMightMatch) + dbSetEmptyState(v.second, appendSeparator(itemRelPath)); //child items might match, e.g. *.txt include filter! + return passFilter; + }); + } + + //delete all entries for removed folder (= "in-sync") from database + void dbSetEmptyState(InSyncFolder& dbFolder, const Zstring& parentRelPathPf) + { + std::erase_if(dbFolder.files, [&](const InSyncFolder::FileList ::value_type& v) { return filter_.passFileFilter(parentRelPathPf + v.first.normStr); }); + std::erase_if(dbFolder.symlinks, [&](const InSyncFolder::SymlinkList::value_type& v) { return filter_.passFileFilter(parentRelPathPf + v.first.normStr); }); + + eraseIf(dbFolder.folders, [&](InSyncFolder::FolderList::value_type& v) + { + const Zstring& itemRelPath = parentRelPathPf + v.first.normStr; + + bool childItemMightMatch = true; + const bool passFilter = filter_.passDirFilter(itemRelPath, &childItemMightMatch); + if (!passFilter && childItemMightMatch) + dbSetEmptyState(v.second, appendSeparator(itemRelPath)); + return passFilter; + }); + } + + const PathFilter& filter_; //filter used while scanning directory: generates view on actual files! + const CompareVariant activeCmpVar_; +}; + + +struct StreamStatusNotifier +{ + StreamStatusNotifier(const std::wstring& statusMsg, AsyncCallback& acb /*throw ThreadStopRequest*/) : + msgPrefix_(statusMsg + L' '), acb_(acb) {} + + void operator()(int64_t bytesDelta) //throw ThreadStopRequest + { + bytesTotal_ += bytesDelta; + + const auto now = std::chrono::steady_clock::now(); + if (now >= lastUpdate_ + UI_UPDATE_INTERVAL / 2) //every ~25 ms + { + lastUpdate_ = now; + acb_.updateStatus(msgPrefix_ + formatFilesizeShort(bytesTotal_)); //throw ThreadStopRequest + } + } + +private: + const std::wstring msgPrefix_; + int64_t bytesTotal_ = 0; + AsyncCallback& acb_; + std::chrono::steady_clock::time_point lastUpdate_; +}; + + +std::pair findCommonSession(const DbStreams& streamsLeft, const DbStreams& streamsRight, //throw FileError + const std::wstring& displayFilePathL, //used for diagnostics only + const std::wstring& displayFilePathR) +{ + auto itCommonL = streamsLeft .end(); + auto itCommonR = streamsRight.end(); + + for (auto itL = streamsLeft.begin(); itL != streamsLeft.end(); ++itL) + { + auto itR = streamsRight.find(itL->first); + if (itR != streamsRight.end()) + /* handle case when db file is loaded together with a (former) copy of itself: + - some streams may have been updated in the meantime => must not discard either db file! + - since db file was copied, multiple streams may have matching sessionID + => IGNORE all of them: one of them may be used later against other sync targets! */ + if (itL->second.isLeadStream != itR->second.isLeadStream) + { + if (itCommonL != streamsLeft.end()) //should not be possible! + throw FileError(replaceCpy(_("Cannot read database file %x."), L"%x", fmtPath(displayFilePathL) + L", " + fmtPath(displayFilePathR)), + _("File content is corrupted.") + L" (multiple common sessions found)"); + itCommonL = itL; + itCommonR = itR; + } + } + + return {itCommonL, itCommonR}; +} +} + +//####################################################################################################################################### + +std::unordered_map> fff::loadLastSynchronousState(const std::vector& baseFolders, + PhaseCallback& callback /*throw X*/) //throw X +{ + std::set dbFilePaths; + + for (const BaseFolderPair* baseFolder : baseFolders) + //avoid race condition with directory existence check: reading sync.ffs_db may succeed although first dir check had failed => conflicts! + if (baseFolder->getFolderStatus() == BaseFolderStatus::existing && + baseFolder->getFolderStatus() == BaseFolderStatus::existing) + { + dbFilePaths.insert(getDatabaseFilePath(*baseFolder)); + dbFilePaths.insert(getDatabaseFilePath(*baseFolder)); + } + //else: ignore; there's no value in reporting it other than to confuse users + + std::map dbStreamsByPath; + //------------ (try to) load DB files in parallel ------------------------- + { + Protected&> protDbStreamsByPath(dbStreamsByPath); + std::vector> parallelWorkload; + + for (const AbstractPath& dbPath : dbFilePaths) + parallelWorkload.emplace_back(dbPath, [&protDbStreamsByPath](ParallelContext& ctx) //throw ThreadStopRequest + { + tryReportingError([&] //throw ThreadStopRequest + { + StreamStatusNotifier notifyLoad(replaceCpy(_("Loading file %x..."), L"%x", fmtPath(AFS::getDisplayPath(ctx.itemPath))), ctx.acb); + try + { + DbStreams dbStreams = ::loadStreams(ctx.itemPath, notifyLoad); //throw FileError, FileErrorDatabaseNotExisting, FileErrorDatabaseCorrupted, ThreadStopRequest + + protDbStreamsByPath.access([&](auto& dbStreamsByPath2) { dbStreamsByPath2.emplace(ctx.itemPath, std::move(dbStreams)); }); + } + catch (FileErrorDatabaseNotExisting&) {} //redundant info => no reportInfo() + }, ctx.acb); + }); + + massParallelExecute(parallelWorkload, + Zstr("Load sync.ffs_db"), callback /*throw X*/); //throw X + } + //---------------------------------------------------------------- + + std::unordered_map> output; + + for (const BaseFolderPair* baseFolder : baseFolders) + if (baseFolder->getFolderStatus() == BaseFolderStatus::existing && + baseFolder->getFolderStatus() == BaseFolderStatus::existing) + { + const AbstractPath dbPathL = getDatabaseFilePath(*baseFolder); + const AbstractPath dbPathR = getDatabaseFilePath(*baseFolder); + + auto itL = dbStreamsByPath.find(dbPathL); + auto itR = dbStreamsByPath.find(dbPathR); + + if (itL != dbStreamsByPath.end() && + itR != dbStreamsByPath.end()) + try + { + const DbStreams& streamsL = itL->second; + const DbStreams& streamsR = itR->second; + + //find associated session: there can be at most one session within intersection of left and right IDs + const auto [itStreamL, itStreamR] = findCommonSession(streamsL, streamsR, + AFS::getDisplayPath(dbPathL), + AFS::getDisplayPath(dbPathR)); //throw FileError + if (itStreamL != streamsL.end()) + { + assert(itStreamL->second.isLeadStream != itStreamR->second.isLeadStream); + SharedRef lastSyncState = StreamParser::execute(itStreamL->second.isLeadStream, + itStreamL->second.rawStream, + itStreamR->second.rawStream, + AFS::getDisplayPath(dbPathL), + AFS::getDisplayPath(dbPathR)); //throw FileError + output.emplace(baseFolder, lastSyncState); + } + } + catch (const FileError& e) { callback.reportFatalError(e.toString()); } //throw X + } + + return output; +} + + +void fff::saveLastSynchronousState(const BaseFolderPair& baseFolder, bool transactionalCopy, + PhaseCallback& callback /*throw X*/) //throw X +{ + const AbstractPath dbPathL = getDatabaseFilePath(baseFolder); + const AbstractPath dbPathR = getDatabaseFilePath(baseFolder); + + //------------ (try to) load DB files in parallel ------------------------- + DbStreams streamsL; //list of session ID + DirInfo-stream + DbStreams streamsR; // + { + bool loadSuccessL = false; + bool loadSuccessR = false; + std::vector> parallelWorkload; + + for (const auto& [dbPath, streamsOut, loadSuccess] : + { + std::tuple(dbPathL, &streamsL, &loadSuccessL), + std::tuple(dbPathR, &streamsR, &loadSuccessR) + }) + parallelWorkload.emplace_back(dbPath, [&streamsOut = *streamsOut, &loadSuccess = *loadSuccess](ParallelContext& ctx) //throw ThreadStopRequest + { + const std::wstring errMsg = tryReportingError([&] //throw ThreadStopRequest + { + StreamStatusNotifier notifyLoad(replaceCpy(_("Loading file %x..."), L"%x", fmtPath(AFS::getDisplayPath(ctx.itemPath))), ctx.acb); + + try { streamsOut = ::loadStreams(ctx.itemPath, notifyLoad); } //throw FileError, FileErrorDatabaseNotExisting, FileErrorDatabaseCorrupted, ThreadStopRequest + catch (FileErrorDatabaseNotExisting&) {} + catch (FileErrorDatabaseCorrupted&) {} //=> just overwrite corrupted DB file: error already reported by loadLastSynchronousState() + }, ctx.acb); + + loadSuccess = errMsg.empty(); + }); + + massParallelExecute(parallelWorkload, + Zstr("Load sync.ffs_db"), callback /*throw X*/); //throw X + + if (!loadSuccessL || !loadSuccessR) + return; /* don't continue when one of the two files failed to load (e.g. network drop): + no common session would be found, (although it may exist!) => + a) if file also fails to save: new orphan session in the other file created + b) if file saves successfully: previous stream sessions lost + old session in other file not cleaned up (orphan) */ + } + //---------------------------------------------------------------- + + //load last synchrounous state + auto itStreamOldL = streamsL.cend(); + auto itStreamOldR = streamsR.cend(); + InSyncFolder lastSyncState; + try + { + //find associated session: there can be at most one session within intersection of left and right IDs + std::tie(itStreamOldL, itStreamOldR) = findCommonSession(streamsL, streamsR, + AFS::getDisplayPath(dbPathL), + AFS::getDisplayPath(dbPathR)); //throw FileError + if (itStreamOldL != streamsL.end()) + lastSyncState = std::move(StreamParser::execute(itStreamOldL->second.isLeadStream /*leadStreamLeft*/, + itStreamOldL->second.rawStream, + itStreamOldR->second.rawStream, + AFS::getDisplayPath(dbPathL), + AFS::getDisplayPath(dbPathR)).ref()); //throw FileError + } + catch (const FileError& e) { callback.reportFatalError(e.toString()); } //throw X + //if database files are corrupted: just overwrite! User is already informed about errors right after comparing! + + //update last synchrounous state + LastSynchronousStateUpdater::execute(baseFolder, lastSyncState); + + //serialize again + SessionData sessionDataL = {}; + SessionData sessionDataR = {}; + sessionDataL.isLeadStream = true; + sessionDataR.isLeadStream = false; + + if (const std::wstring errMsg = tryReportingError([&] //throw X +{ + StreamGenerator::execute(lastSyncState, //throw FileError + AFS::getDisplayPath(dbPathL), + AFS::getDisplayPath(dbPathR), + sessionDataL.rawStream, + sessionDataR.rawStream); + }, callback /*throw X*/); !errMsg.empty()) + return; + + //check if there is some work to do at all + if (itStreamOldL != streamsL.end() && itStreamOldL->second == sessionDataL && + itStreamOldR != streamsR.end() && itStreamOldR->second == sessionDataR) + return; //some users monitor the *.ffs_db file with RTS => don't touch the file if it isnt't strictly needed + + //erase old session data + if (itStreamOldL != streamsL.end()) + streamsL.erase(itStreamOldL); + if (itStreamOldR != streamsR.end()) + streamsR.erase(itStreamOldR); + + //create new session data + const std::string sessionID = generateGUID(); + + streamsL[sessionID] = std::move(sessionDataL); + streamsR[sessionID] = std::move(sessionDataR); + + //------------ save DB files in parallel ------------------------- + //1. create *both* ffs_tmp files first (caveat: *not* necessarily in parallel, depending on deviceParallelOps!) + //2. if successful, rename both files (almost) transactionally! + bool saveSuccessL = false; + bool saveSuccessR = false; + std::optional dbPathTmpL; + std::optional dbPathTmpR; + ZEN_ON_SCOPE_EXIT + ( + //*INDENT-OFF* + if (dbPathTmpL) try { AFS::removeFilePlain(*dbPathTmpL); } catch (const FileError& e) { logExtraError(e.toString()); } + if (dbPathTmpR) try { AFS::removeFilePlain(*dbPathTmpR); } catch (const FileError& e) { logExtraError(e.toString()); } + //*INDENT-ON* + ) + + std::vector> parallelWorkloadSave, parallelWorkloadMove; + + for (const auto& [dbPath, streams, saveSuccess, dbPathTmp] : + { + std::tuple(dbPathL, &streamsL, &saveSuccessL, &dbPathTmpL), + std::tuple(dbPathR, &streamsR, &saveSuccessR, &dbPathTmpR) + }) + { + parallelWorkloadSave.emplace_back(dbPath, [&streams = *streams, + &saveSuccess = *saveSuccess, + &dbPathTmp = *dbPathTmp, + transactionalCopy](ParallelContext& ctx) //throw ThreadStopRequest + { + const std::wstring errMsg = tryReportingError([&] //throw ThreadStopRequest + { + StreamStatusNotifier notifySave(replaceCpy(_("Saving file %x..."), L"%x", fmtPath(AFS::getDisplayPath(ctx.itemPath))), ctx.acb); + + if (transactionalCopy && !AFS::hasNativeTransactionalCopy(ctx.itemPath)) //=> write (both?) DB files as a transaction + { + const Zstring shortGuid = printNumber(Zstr("%04x"), static_cast(getCrc16(generateGUID()))); + const AbstractPath tmpPath = AFS::appendRelPath(*AFS::getParentPath(ctx.itemPath), AFS::getItemName(ctx.itemPath) + Zstr('.') + shortGuid + AFS::TEMP_FILE_ENDING); + + saveStreams(streams, tmpPath, notifySave); //throw FileError, ThreadStopRequest + dbPathTmp = tmpPath; //pass file ownership + } + else //some MTP devices don't even allow renaming files: https://freefilesync.org/forum/viewtopic.php?t=6531 + { + AFS::removeFileIfExists(ctx.itemPath); //throw FileError + saveStreams(streams, ctx.itemPath, notifySave); //throw FileError, ThreadStopRequest + } + }, ctx.acb); + + saveSuccess = errMsg.empty(); + }); + //---------------------------------------------------------------------------- + if (transactionalCopy && !AFS::hasNativeTransactionalCopy(dbPath)) + parallelWorkloadMove.emplace_back(dbPath, [&dbPathTmp = *dbPathTmp](ParallelContext& ctx) //throw ThreadStopRequest + { + tryReportingError([&] //throw ThreadStopRequest + { + //rename temp file (almost) transactionally: without write access, file creation would have failed + AFS::removeFileIfExists(ctx.itemPath); //throw FileError + AFS::moveAndRenameItem(*dbPathTmp, ctx.itemPath); //throw FileError, (ErrorMoveUnsupported) + + dbPathTmp = std::nullopt; //basically a "ScopeGuard::dismiss()" + }, ctx.acb); + }); + } + + massParallelExecute(parallelWorkloadSave, + Zstr("Save sync.ffs_db"), callback /*throw X*/); //throw X + //---------------------------------------------------------------- + if (saveSuccessL && saveSuccessR) + massParallelExecute(parallelWorkloadMove, + Zstr("Move sync.ffs_db"), callback /*throw X*/); //throw X +} diff --git a/FreeFileSync/Source/base/db_file.h b/FreeFileSync/Source/base/db_file.h new file mode 100644 index 0000000..04e7b1f --- /dev/null +++ b/FreeFileSync/Source/base/db_file.h @@ -0,0 +1,89 @@ +// ***************************************************************************** +// * 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 DB_FILE_H_834275398588021574 +#define DB_FILE_H_834275398588021574 + +#include +#include +#include "file_hierarchy.h" +#include "process_callback.h" + + +namespace fff +{ +constexpr ZstringView SYNC_DB_FILE_ENDING = Zstr(".ffs_db"); //don't use Zstring as global constant: avoid static initialization order problem in global namespace! + +struct InSyncDescrFile //subset of FileAttributes +{ + time_t modTime = 0; + AFS::FingerPrint filePrint = 0; //optional! +}; + +struct InSyncDescrLink +{ + time_t modTime = 0; +}; + + +//artificial hierarchy of last synchronous state: +struct InSyncFile +{ + InSyncDescrFile left; //support flip()! + InSyncDescrFile right; // + CompareVariant cmpVar = CompareVariant::timeSize; //the one active while finding "file in sync" + uint64_t fileSize = 0; //file size must be identical on both sides! +}; + +struct InSyncSymlink +{ + InSyncDescrLink left; + InSyncDescrLink right; + CompareVariant cmpVar = CompareVariant::timeSize; +}; + +struct InSyncFolder +{ + //------------------------------------------------------------------ + using FolderList = std::unordered_map; // + using FileList = std::unordered_map; // key: file name (ignoring Unicode normal forms) + using SymlinkList = std::unordered_map; // + //------------------------------------------------------------------ + + FolderList folders; + FileList files; + SymlinkList symlinks; //non-followed symlinks + + //convenience + InSyncFolder& addFolder(const Zstring& folderName) + { + const auto [it, inserted] = folders.try_emplace(folderName); + assert(inserted); + return it->second; + } + + void addFile(const Zstring& fileName, const InSyncDescrFile& descrL, const InSyncDescrFile& descrR, CompareVariant cmpVar, uint64_t fileSize) + { + files.emplace(fileName, InSyncFile {descrL, descrR, cmpVar, fileSize}); + assert(inserted); + } + + void addSymlink(const Zstring& linkName, const InSyncDescrLink& descrL, const InSyncDescrLink& descrR, CompareVariant cmpVar) + { + symlinks.emplace(linkName, InSyncSymlink {descrL, descrR, cmpVar}); + assert(inserted); + } +}; + + +std::unordered_map> loadLastSynchronousState(const std::vector& baseFolders, + PhaseCallback& callback /*throw X*/); //throw X + +void saveLastSynchronousState(const BaseFolderPair& baseFolder, bool transactionalCopy, //throw X + PhaseCallback& callback /*throw X*/); +} + +#endif //DB_FILE_H_834275398588021574 diff --git a/FreeFileSync/Source/base/dir_exist_async.h b/FreeFileSync/Source/base/dir_exist_async.h new file mode 100644 index 0000000..640479b --- /dev/null +++ b/FreeFileSync/Source/base/dir_exist_async.h @@ -0,0 +1,159 @@ +// ***************************************************************************** +// * 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 DIR_EXIST_ASYNC_H_0817328167343215806734213 +#define DIR_EXIST_ASYNC_H_0817328167343215806734213 + +#include +#include "process_callback.h" +#include "../afs/abstract.h" + + +namespace fff +{ +namespace +{ +struct FolderStatus +{ + std::set existing; + std::set notExisting; + std::map failedChecks; +}; +//- directory existence checking may hang for non-existent network drives => run asynchronously and update UI! +//- check existence of all directories in parallel! (avoid adding up search times if multiple network drives are not reachable) +//- authenticateAccess() better be integrated into folder existence check: if fails, why bother to go on with the folder!? +//- probably don't need timeout: https://freefilesync.org/forum/viewtopic.php?t=7350#p36817 +// => benefit: waits until user login completed in AFS::authenticateAccess() +FolderStatus getFolderStatusParallel(const std::set& folderPaths, + bool authenticateAccess, const AFS::RequestPasswordFun& requestPassword /*throw X*/, + PhaseCallback& cb /*throw X*/) //throw X +{ + using namespace zen; + + //aggregate folder paths that are on the same root device: see parallel_scan.h + std::map> perDevicePaths; + + for (const AbstractPath& folderPath : folderPaths) + if (!AFS::isNullPath(folderPath)) //skip empty folders + perDevicePaths[folderPath.afsDevice].insert(folderPath); + //---------------------------------------------------------------------- + + std::vector>> futFoldersExist; + + struct AsyncPrompt + { + std::wstring msg; + std::wstring lastErrorMsg; + std::promise promPassword; + }; + auto protPromptsPending = authenticateAccess && requestPassword ? std::make_shared>>() : nullptr; + + //---------------------------------------------------------------------- + std::vector>> deviceThreadGroups; + //---------------------------------------------------------------------- + + for (const auto& [device, deviceFolderPaths] : perDevicePaths) + { + deviceThreadGroups.emplace_back(1, Zstr("DirExist: ") + utfTo(AFS::getDisplayPath(AbstractPath(device, AfsPath())))); + deviceThreadGroups.back().detach(); //don't wait on hanging threads if user cancels + + //1. login to network share, connect with Google Drive, etc. + std::shared_future futAuth; + if (authenticateAccess) + { + AFS::RequestPasswordFun threadRequestPassword; //throw std::future_error + if (requestPassword) + threadRequestPassword = [promptsPendingWeak = std::weak_ptr(protPromptsPending)](const std::wstring& msg, const std::wstring& lastErrorMsg) + { + std::future futPassword; + if (auto protPromptsPending2 = promptsPendingWeak.lock()) //[!] not owned by worker thread! + protPromptsPending2->access([&](RingBuffer& promptsPending) + { + promptsPending.push_back(AsyncPrompt{msg, lastErrorMsg, {}}); + futPassword = promptsPending.back().promPassword.get_future(); + }); + return futPassword.get(); //throw std::future_error -> if std::promise destroyed before password was set + }; + + futAuth = runAsync([device /*clang bug*/= device, threadRequestPassword] + { + setCurrentThreadName(Zstr("Auth: ") + utfTo(AFS::getDisplayPath(AbstractPath(device, AfsPath())))); + AFS::authenticateAccess(device, threadRequestPassword); //throw FileError, std::future_error + }); + } + + for (const AbstractPath& folderPath : deviceFolderPaths) + { + std::packaged_task pt([folderPath, futAuth] + { + if (futAuth.valid()) + futAuth.get(); //throw FileError, std::future_error + + /* 2. check dir existence: + + CAVEAT: the case-sensitive semantics of AFS::itemExists() do not fit here! + BUT: its implementation happens to be okay for our use: + Assume we have a case-insensitive path match: + => AFS::itemExists() first checks AFS::getItemType() + => either succeeds (fine) or fails because of 1. not existing or 2. access error + => if the subsequent case-sensitive folder search also doesn't find the folder: only a problem in case 2 + => FFS tries to create the folder during sync and fails with I. access error (fine) or II. already existing (obscures the previous "access error") */ + return AFS::itemExists(folderPath); //throw FileError; return "false" IFF nothing (of any type) exists + + //check for ItemType::file? too pedantic? + // throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(getDisplayPath(folderPath))), + // replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(getItemName(folderPath)))); + }); + auto futIsExisting = pt.get_future(); + deviceThreadGroups.back().run(std::move(pt)); + + futFoldersExist.emplace_back(folderPath, std::move(futIsExisting)); + } + } + //---------------------------------------------------------------------- + + FolderStatus output; + + for (auto& [folderPath, futFolderExists] : futFoldersExist) + { + cb.updateStatus(replaceCpy(_("Searching for folder %x..."), L"%x", fmtPath(AFS::getDisplayPath(folderPath)))); //throw X + + while (futFolderExists.wait_for(UI_UPDATE_INTERVAL / 2) == std::future_status::timeout) + { + cb.requestUiUpdate(); //throw X + + //marshal password prompt callback from current thread (probably main) to worker threads + //=> polling delay doesn't matter because user interaction is required + if (protPromptsPending) + protPromptsPending->access([&](RingBuffer& promptsPending) + { + //call back while holding Protected<> lock!? device authentication threads blocking doesn't matter because prompts are serialized to GUI anyway + if (!promptsPending.empty()) + { + assert(requestPassword); //... in this context + const Zstring password = requestPassword(promptsPending.front().msg, promptsPending.front().lastErrorMsg); //throw X + promptsPending.front().promPassword.set_value(password); + promptsPending.pop_front(); + } + }); + } + + try + { + //call future::get() only *once*! otherwise: undefined behavior! + if (futFolderExists.get()) //throw FileError, (std::future_error) + output.existing.insert(folderPath); + else + output.notExisting.insert(folderPath); + } + catch (const FileError& e) { output.failedChecks.emplace(folderPath, e); } + } + return output; +} +} +} + +#endif //DIR_EXIST_ASYNC_H_0817328167343215806734213 diff --git a/FreeFileSync/Source/base/dir_lock.cpp b/FreeFileSync/Source/base/dir_lock.cpp new file mode 100644 index 0000000..9162551 --- /dev/null +++ b/FreeFileSync/Source/base/dir_lock.cpp @@ -0,0 +1,560 @@ +// ***************************************************************************** +// * 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 "dir_lock.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + #include //std::cerr + #include //open() + #include //close() + #include //kill() + +using namespace zen; +using namespace fff; + + +namespace +{ +constexpr std::chrono::seconds EMIT_LIFE_SIGN_INTERVAL (5); //show life sign; +constexpr std::chrono::seconds POLL_LIFE_SIGN_INTERVAL (4); //poll for life sign; +constexpr std::chrono::seconds DETECT_ABANDONED_INTERVAL(30); //assume abandoned lock; + +const char LOCK_FILE_DESCR[] = "FreeFileSync"; +const int LOCK_FILE_VERSION = 3; //2020-02-07 +const int ABANDONED_LOCK_LEVEL_MAX = 10; +} + + +Zstring fff::impl::getAbandonedLockFileName(const Zstring& lockFileName) //throw SysError +{ + Zstring fileName = lockFileName; + int level = 0; + + //recursive abandoned locks!? (almost) impossible, except for file system bugs: https://freefilesync.org/forum/viewtopic.php?t=6568 + const Zstring tmp = afterFirst(fileName, Zstr("Delete."), IfNotFoundReturn::none); //e.g. Delete.1.sync.ffs_lock + if (!tmp.empty()) + { + const Zstring levelStr = beforeFirst(tmp, Zstr('.'), IfNotFoundReturn::none); + if (!levelStr.empty() && std::all_of(levelStr.begin(), levelStr.end(), [](Zchar c) { return zen::isDigit(c); })) + { + fileName = afterFirst(tmp, Zstr('.'), IfNotFoundReturn::none); + level = stringTo(levelStr) + 1; + + if (level >= ABANDONED_LOCK_LEVEL_MAX) + throw SysError(L"Endless recursion."); + } + } + + return Zstr("Delete.") + numberTo(level) + Zstr(".") + fileName; //preserve lock file extension! +} + + +namespace +{ +//worker thread +class LifeSigns +{ +public: + LifeSigns(const Zstring& lockFilePath) : lockFilePath_(lockFilePath) + { + } + + void operator()() const //throw ThreadStopRequest + { + const std::optional parentDirPath = getParentFolderPath(lockFilePath_); + setCurrentThreadName(Zstr("DirLock: ") + (parentDirPath ? *parentDirPath : Zstr(""))); + + for (;;) + { + interruptibleSleep(EMIT_LIFE_SIGN_INTERVAL); //throw ThreadStopRequest + emitLifeSign(); //noexcept + } + } + +private: + //try to append one byte... + void emitLifeSign() const //noexcept + { + try + { +#if 1 + const int fdLockFile = ::open(lockFilePath_.c_str(), O_WRONLY | O_APPEND | O_CLOEXEC); + if (fdLockFile == -1) + THROW_LAST_SYS_ERROR("open"); + ZEN_ON_SCOPE_EXIT(::close(fdLockFile)); + +#else //alternative using lseek => no apparent benefit https://freefilesync.org/forum/viewtopic.php?t=7553#p25505 + const int fdLockFile = ::open(lockFilePath_.c_str(), O_WRONLY | O_CLOEXEC); + if (fdLockFile == -1) + THROW_LAST_SYS_ERROR("open"); + ZEN_ON_SCOPE_EXIT(::close(fdLockFile)); + + if (const off_t offset = ::lseek(fdLockFile, 0, SEEK_END); + offset == -1) + THROW_LAST_SYS_ERROR("lseek"); +#endif + const ssize_t bytesWritten = ::write(fdLockFile, " ", 1); //writes *up to* count bytes + if (bytesWritten <= 0) + { + if (bytesWritten == 0) //comment in safe-read.c suggests to treat this as an error due to buggy drivers + errno = ENOSPC; + THROW_LAST_SYS_ERROR("write"); + } + ASSERT_SYSERROR(bytesWritten == 1); //better safe than sorry + } + catch (const SysError& e) + { + logExtraError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(lockFilePath_)) + L"\n\n" + e.toString()); + } + } + + const Zstring lockFilePath_; //thread-local! +}; + + + using ProcessId = pid_t; + using SessionId = pid_t; + +//return ppid on Windows, sid on Linux/Mac, "no value" if process corresponding to "processId" is not existing +std::optional getSessionId(ProcessId processId) //throw FileError +{ + if (::kill(processId, 0) != 0) //sig == 0: no signal sent, just existence check + return {}; + + const pid_t procSid = ::getsid(processId); //NOT to be confused with "login session", e.g. not stable on OS X!!! + if (procSid < 0) //pids are never negative, empiric proof: https://linux.die.net/man/2/wait + THROW_LAST_FILE_ERROR(_("Cannot get process information."), "getsid"); + + return procSid; +} + + +struct LockInformation //throw FileError +{ + std::string lockId; //16 byte GUID - a universal identifier for this lock (no matter what the path is, considering symlinks, distributed network, etc.) + + //identify local computer + std::string computerName; //format: HostName.DomainName + std::string userId; + + //identify running process + SessionId sessionId = 0; //Windows: parent process id; Linux/macOS: session of the process, NOT the user + ProcessId processId = 0; +}; + + +LockInformation getLockInfoFromCurrentProcess() //throw FileError +{ + LockInformation lockInfo = + { + .lockId = generateGUID(), + .userId = utfTo(getLoginUser()), //throw FileError + }; + + const std::string osName = "Linux"; + + //wxGetFullHostName() is a performance killer and can hang for some users, so don't touch! + std::vector buf(10000); + if (::gethostname(buf.data(), buf.size()) != 0) + THROW_LAST_FILE_ERROR(_("Cannot get process information."), "gethostname"); + lockInfo.computerName = osName + ' ' + buf.data() + '.'; + + if (::getdomainname(buf.data(), buf.size()) != 0) + THROW_LAST_FILE_ERROR(_("Cannot get process information."), "getdomainname"); + lockInfo.computerName += buf.data(); //can be "(none)"! + + lockInfo.processId = ::getpid(); //never fails + + std::optional sessionIdTmp = getSessionId(lockInfo.processId); //throw FileError + if (!sessionIdTmp) + throw FileError(_("Cannot get process information."), L"no session id found"); //should not happen? + lockInfo.sessionId = *sessionIdTmp; + + return lockInfo; +} + + +std::string serialize(const LockInformation& lockInfo) +{ + MemoryStreamOut streamOut; + writeArray(streamOut, LOCK_FILE_DESCR, sizeof(LOCK_FILE_DESCR)); + writeNumber(streamOut, LOCK_FILE_VERSION); + + static_assert(sizeof(lockInfo.processId) <= sizeof(uint64_t)); //ensure cross-platform compatibility! + static_assert(sizeof(lockInfo.sessionId) <= sizeof(uint64_t)); // + + writeContainer(streamOut, lockInfo.lockId); + writeContainer(streamOut, lockInfo.computerName); + writeContainer(streamOut, lockInfo.userId); + writeNumber(streamOut, lockInfo.sessionId); + writeNumber(streamOut, lockInfo.processId); + + writeNumber(streamOut, getCrc32(streamOut.ref())); + writeArray(streamOut, "x", 1); //sentinel: mark logical end with a non-space character + return streamOut.ref(); +} + + +LockInformation unserialize(const std::string& byteStream) //throw SysError +{ + MemoryStreamIn streamIn(byteStream); + + char formatDescr[sizeof(LOCK_FILE_DESCR)] = {}; + readArray(streamIn, &formatDescr, sizeof(formatDescr)); //throw SysErrorUnexpectedEos + + if (!std::equal(std::begin(formatDescr), std::end(formatDescr), std::begin(LOCK_FILE_DESCR))) + throw SysError(_("File content is corrupted.") + L" (invalid header)"); + + const int version = readNumber(streamIn); //throw SysErrorUnexpectedEos + if (version != LOCK_FILE_VERSION) + throw SysError(_("Unsupported data format.") + L' ' + replaceCpy(_("Version: %x"), L"%x", numberTo(version))); + + //-------------------------------------------------------------------- + //catch data corruption ASAP + don't rely on std::bad_alloc for consistency checking + const size_t posEnd = byteStream.rfind('x'); //skip blanks (+ unrelated corrupted data e.g. nulls!) + if (posEnd == std::string::npos) + throw SysErrorUnexpectedEos(); + + const std::string_view byteStreamTrm = makeStringView(byteStream.begin(), posEnd); + + MemoryStreamOut crcStreamOut; + writeNumber(crcStreamOut, getCrc32(byteStreamTrm.begin(), byteStreamTrm.end() - sizeof(uint32_t))); + + if (!endsWith(byteStreamTrm, crcStreamOut.ref())) + throw SysError(_("File content is corrupted.") + L" (invalid checksum)"); + //-------------------------------------------------------------------- + + LockInformation lockInfo = {}; + lockInfo.lockId = readContainer(streamIn); // + lockInfo.computerName = readContainer(streamIn); //SysErrorUnexpectedEos + lockInfo.userId = readContainer(streamIn); // + lockInfo.sessionId = static_cast(readNumber(streamIn)); //[!] conversion + lockInfo.processId = static_cast(readNumber(streamIn)); //[!] conversion + return lockInfo; +} + + +LockInformation retrieveLockInfo(const Zstring& lockFilePath) //throw FileError +{ + const std::string byteStream = getFileContent(lockFilePath, nullptr /*notifyUnbufferedIO*/); //throw FileError + try + { + return unserialize(byteStream); //throw SysError + } + catch (const SysError& e) + { + throw FileError(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(lockFilePath)), e.toString()); + } +} + + +inline +std::string retrieveLockId(const Zstring& lockFilePath) //throw FileError +{ + return retrieveLockInfo(lockFilePath).lockId; //throw FileError +} + + +enum class ProcessStatus +{ + notRunning, + running, + itsUs, + noIdea, +}; + +ProcessStatus getProcessStatus(const LockInformation& lockInfo) //throw FileError +{ + const LockInformation localInfo = getLockInfoFromCurrentProcess(); //throw FileError + + if (lockInfo.computerName != localInfo.computerName || + lockInfo.userId != localInfo.userId) //another user may run a session right now! + return ProcessStatus::noIdea; //lock owned by different computer in this network + + if (lockInfo.sessionId == localInfo.sessionId && + lockInfo.processId == localInfo.processId) //obscure, but possible: deletion failed or a lock file is "stolen" and put back while the program is running + return ProcessStatus::itsUs; + + if (std::optional sessionId = getSessionId(lockInfo.processId)) //throw FileError + return *sessionId == lockInfo.sessionId ? ProcessStatus::running : ProcessStatus::notRunning; + return ProcessStatus::notRunning; +} + + +DEFINE_NEW_FILE_ERROR(ErrorFileNotExisting) +uint64_t getLockFileSize(const Zstring& filePath) //throw FileError, ErrorFileNotExisting +{ + struct stat fileInfo = {}; + if (::stat(filePath.c_str(), &fileInfo) == 0) + return fileInfo.st_size; + + if (errno == ENOENT) + throw ErrorFileNotExisting(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(filePath)), formatSystemError("stat", errno)); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(filePath)), "stat"); +} + + +void waitOnDirLock(const Zstring& lockFilePath, const DirLockCallback& notifyStatus /*throw X*/, std::chrono::milliseconds cbInterval) //throw FileError +{ + std::wstring infoMsg = _("Waiting while directory is in use:") + L' ' + fmtPath(lockFilePath); + + if (notifyStatus) notifyStatus(std::wstring(infoMsg)); //throw X + + //convenience optimization only: if we know the owning process crashed, we needn't wait DETECT_ABANDONED_INTERVAL sec + bool lockOwnderDead = false; + std::string originalLockId; //empty if it cannot be retrieved + try + { + const LockInformation& lockInfo = retrieveLockInfo(lockFilePath); //throw FileError + + infoMsg += SPACED_DASH + _("Username:") + L' ' + utfTo(lockInfo.userId); + + originalLockId = lockInfo.lockId; + switch (getProcessStatus(lockInfo)) //throw FileError + { + case ProcessStatus::itsUs: //since we've already passed LockAdmin, the lock file seems abandoned ("stolen"?) although it's from this process + case ProcessStatus::notRunning: + lockOwnderDead = true; + break; + case ProcessStatus::running: + case ProcessStatus::noIdea: + break; + } + } + catch (FileError&) {} //logfile may be only partly written -> this is no error! + //------------------------------------------------------------------------------ + + uint64_t fileSizeOld = 0; + auto lastLifeSign = std::chrono::steady_clock::now(); + + for (;;) + { + uint64_t fileSizeNew = 0; + try + { + fileSizeNew = getLockFileSize(lockFilePath); //throw FileError, ErrorFileNotExisting + } + catch (ErrorFileNotExisting&) { return; } //what we are waiting for... + + const auto lastCheckTime = std::chrono::steady_clock::now(); + + if (fileSizeNew != fileSizeOld) //received life sign from lock + { + fileSizeOld = fileSizeNew; + lastLifeSign = lastCheckTime; + } + + if (lockOwnderDead || //no need to wait any longer... + lastCheckTime >= lastLifeSign + DETECT_ABANDONED_INTERVAL) + { + const Zstring lockFileName = [&] + { + try + { + return fff::impl::getAbandonedLockFileName(getItemName(lockFilePath)); //throw SysError + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot delete file %x."), L"%x", fmtPath(lockFilePath)), e.toString()); } + }(); + + DirLock guardDeletion(*getParentFolderPath(lockFilePath), lockFileName, notifyStatus, cbInterval); //throw FileError + + //now that the lock is in place check existence again: meanwhile another process may have deleted and created a new lock! + std::string currentLockId; + try { currentLockId = retrieveLockId(lockFilePath); /*throw FileError*/ } + catch (FileError&) {} + + if (currentLockId != originalLockId) + return; //another process has placed a new lock, leave scope: the wait for the old lock is technically over... + + try + { + if (getLockFileSize(lockFilePath) != fileSizeOld) //throw FileError, ErrorFileNotExisting + return; //late life sign (or maybe even a different lock if retrieveLockId() failed!) + } + catch (ErrorFileNotExisting&) { return; } //what we are waiting for anyway... + + removeFilePlain(lockFilePath); //throw FileError + return; + } + + //wait some time... + const auto delayUntil = std::chrono::steady_clock::now() + POLL_LIFE_SIGN_INTERVAL; + for (auto now = std::chrono::steady_clock::now(); now < delayUntil; now = std::chrono::steady_clock::now()) + { + if (notifyStatus) + { + //one signal missed: it's likely this is an abandoned lock => show countdown + if (lastCheckTime >= lastLifeSign + EMIT_LIFE_SIGN_INTERVAL + std::chrono::seconds(1)) + { + const int remainingSeconds = std::max(0, static_cast(std::chrono::duration_cast(DETECT_ABANDONED_INTERVAL - (now - lastLifeSign)).count())); + notifyStatus(infoMsg + SPACED_DASH + _("Lock file apparently abandoned...") + L' ' + _P("1 sec", "%x sec", remainingSeconds)); //throw X + } + else + notifyStatus(std::wstring(infoMsg)); //throw X; emit a message in any case (might clear other one) + } + std::this_thread::sleep_for(cbInterval); + } + } +} + + +void releaseLock(const Zstring& lockFilePath) { removeFilePlain(lockFilePath); } //throw FileError + + +bool tryLock(const Zstring& lockFilePath) //throw FileError +{ + //important: we want the lock file to have exactly the permissions specified + //=> yes, disabling umask() is messy (per-process!), but fchmod() may not be supported: https://freefilesync.org/forum/viewtopic.php?t=8096 + const mode_t oldMask = ::umask(0); //always succeeds + ZEN_ON_SCOPE_EXIT(::umask(oldMask)); + + const mode_t lockFileMode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH; //0666 + + //O_EXCL contains a race condition on NFS file systems: https://linux.die.net/man/2/open + const int hFile = ::open(lockFilePath.c_str(), //const char* pathname + O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC, //int flags + lockFileMode); //mode_t mode + if (hFile == -1) + { + if (errno == EEXIST) + return false; + + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(lockFilePath)), "open"); + } + FileOutputPlain fileOut(hFile, lockFilePath); //pass handle ownership + + //write housekeeping info: user, process info, lock GUID + const std::string byteStream = serialize(getLockInfoFromCurrentProcess()); //throw FileError + + unbufferedSave(byteStream, [&](const void* buffer, size_t bytesToWrite) + { + return fileOut.tryWrite(buffer, bytesToWrite); //throw FileError; may return short! CONTRACT: bytesToWrite > 0 + }, + fileOut.getBlockSize()); + + fileOut.close(); //throw FileError + return true; +} +} + + +class DirLock::SharedDirLock +{ +public: + SharedDirLock(const Zstring& lockFilePath, const DirLockCallback& notifyStatus, std::chrono::milliseconds cbInterval) : //throw FileError + lockFilePath_(lockFilePath) + { + if (notifyStatus) notifyStatus(replaceCpy(_("Creating file %x"), L"%x", fmtPath(lockFilePath))); //throw X + + while (!::tryLock(lockFilePath)) //throw FileError + { + ::waitOnDirLock(lockFilePath, notifyStatus, cbInterval); //throw FileError + } + + lifeSignthread_ = InterruptibleThread(LifeSigns(lockFilePath)); + } + + ~SharedDirLock() + { + lifeSignthread_.requestStop(); //thread lifetime is subset of this instances's life + lifeSignthread_.join(); + + try + { + ::releaseLock(lockFilePath_); //throw FileError + } + catch (const FileError& e) { logExtraError(e.toString()); } //inform user about remnant lock files *somehow*! + } + +private: + SharedDirLock (const DirLock&) = delete; + SharedDirLock& operator=(const DirLock&) = delete; + + const Zstring lockFilePath_; + InterruptibleThread lifeSignthread_; +}; + + +class DirLock::LockAdmin //administrate all locks held by this process to avoid deadlock by recursion +{ +public: + static LockAdmin& instance() + { + static LockAdmin inst; + return inst; + } + + //create or retrieve a SharedDirLock + std::shared_ptr retrieve(const Zstring& lockFilePath, const DirLockCallback& notifyStatus, std::chrono::milliseconds cbInterval) //throw FileError + { + assert(runningOnMainThread()); //function is not thread-safe! + + tidyUp(); + + //optimization: check if we already own a lock for this path + if (auto itGuid = guidByPath_.find(lockFilePath); + itGuid != guidByPath_.end()) + if (const std::shared_ptr& activeLock = getActiveLock(itGuid->second)) //returns null-lock if not found + return activeLock; //SharedDirLock is still active -> enlarge circle of shared ownership + + try //check based on lock GUID, deadlock prevention: "lockFilePath" may be an alternative name for a lock already owned by this process + { + const std::string lockId = retrieveLockId(lockFilePath); //throw FileError + if (const std::shared_ptr& activeLock = getActiveLock(lockId)) //returns null-lock if not found + { + guidByPath_[lockFilePath] = lockId; //found an alias for one of our active locks + return activeLock; + } + } + catch (FileError&) {} //catch everything, let SharedDirLock constructor deal with errors, e.g. 0-sized/corrupted lock files + + //lock not owned by us => create a new one + auto newLock = std::make_shared(lockFilePath, notifyStatus, cbInterval); //throw FileError + const std::string& newLockGuid = retrieveLockId(lockFilePath); //throw FileError + + guidByPath_[lockFilePath] = newLockGuid; //update registry + locksByGuid_[newLockGuid] = newLock; // + + return newLock; + } + +private: + LockAdmin() {} + LockAdmin (const LockAdmin&) = delete; + LockAdmin& operator=(const LockAdmin&) = delete; + + using UniqueId = std::string; + + std::shared_ptr getActiveLock(const UniqueId& lockId) //returns null if none found + { + auto it = locksByGuid_.find(lockId); + return it != locksByGuid_.end() ? it->second.lock() : nullptr; //try to get shared_ptr; throw() + } + + void tidyUp() //remove obsolete entries + { + std::erase_if(locksByGuid_, [](const auto& v) { return v.second.expired(); }); + std::erase_if(guidByPath_, [&](const auto& v) { return !locksByGuid_.contains(v.second); }); + } + + std::unordered_map guidByPath_; //lockFilePath |-> GUID; n:1; locks can be referenced by a lockFilePath or alternatively a GUID + std::unordered_map> locksByGuid_; //GUID |-> "shared lock ownership"; 1:1 +}; + + +DirLock::DirLock(const Zstring& folderPath, const Zstring& fileName, const DirLockCallback& notifyStatus, std::chrono::milliseconds cbInterval) //throw FileError +{ + sharedLock_ = LockAdmin::instance().retrieve(appendPath(folderPath, fileName), notifyStatus, cbInterval); //throw FileError +} diff --git a/FreeFileSync/Source/base/dir_lock.h b/FreeFileSync/Source/base/dir_lock.h new file mode 100644 index 0000000..31da3db --- /dev/null +++ b/FreeFileSync/Source/base/dir_lock.h @@ -0,0 +1,59 @@ +// ***************************************************************************** +// * 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 DIR_LOCK_H_81740832174954356 +#define DIR_LOCK_H_81740832174954356 + +#include +#include +#include +#include + + +namespace fff +{ +/* RAII structure to place a directory lock against other FFS processes: + - recursive locking supported, even with alternate lockfile names, e.g. via symlinks, network mounts, case-differences etc. + - ownership shared between all object instances refering to a specific lock location(= GUID) + - can be copied safely and efficiently! (ref-counting) + - detects and resolves abandoned locks (instantly if lock is associated with local pc, else after 30 seconds) + - temporary locks created during abandoned lock resolution keep "lockFilePath"'s extension + - race-free (Windows, almost on Linux(NFS)) + - NOT thread-safe! (1. global LockAdmin 2. locks for directory aliases should be created sequentially to detect duplicate locks!) */ + +//intermediate locks created by DirLock use this extension, too: +constexpr ZstringView LOCK_FILE_ENDING = Zstr(".ffs_lock"); //don't use Zstring as global constant: avoid static initialization order problem in global namespace! + +//while waiting for the lock +using DirLockCallback = std::function; //throw X + +class DirLock +{ +public: + DirLock(const Zstring& folderPath, + const DirLockCallback& notifyStatus, //callback only used during construction + std::chrono::milliseconds cbInterval) : + DirLock(folderPath, Zstring(Zstr("sync")) + LOCK_FILE_ENDING, notifyStatus, cbInterval) {} //throw FileError + + DirLock(const Zstring& folderPath, + const Zstring& fileName, + const DirLockCallback& notifyStatus, + std::chrono::milliseconds cbInterval); //throw FileError + +private: + class LockAdmin; + class SharedDirLock; + std::shared_ptr sharedLock_; +}; + + +namespace impl //declare for unit tests: +{ +Zstring getAbandonedLockFileName(const Zstring& lockFilePath); //throw FileError +} +} + +#endif //DIR_LOCK_H_81740832174954356 diff --git a/FreeFileSync/Source/base/file_hierarchy.cpp b/FreeFileSync/Source/base/file_hierarchy.cpp new file mode 100644 index 0000000..89b9d90 --- /dev/null +++ b/FreeFileSync/Source/base/file_hierarchy.cpp @@ -0,0 +1,575 @@ +// ***************************************************************************** +// * 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 "file_hierarchy.h" +#include +#include +#include + +using namespace zen; +using namespace fff; + + +std::wstring fff::getShortDisplayNameForFolderPair(const AbstractPath& itemPathL, const AbstractPath& itemPathR) +{ + Zstring commonTrail; + AbstractPath tmpPathL = itemPathL; + AbstractPath tmpPathR = itemPathR; + for (;;) + { + const std::optional parentPathL = AFS::getParentPath(tmpPathL); + const std::optional parentPathR = AFS::getParentPath(tmpPathR); + if (!parentPathL || !parentPathR) + break; + + const Zstring itemNameL = AFS::getItemName(tmpPathL); + const Zstring itemNameR = AFS::getItemName(tmpPathR); + if (!equalNoCase(itemNameL, itemNameR)) //let's compare case-insensitively (even on Linux!) + break; + + tmpPathL = *parentPathL; + tmpPathR = *parentPathR; + + commonTrail = appendPath(itemNameL, commonTrail); + } + if (!commonTrail.empty()) + return utfTo(commonTrail); + + auto getLastComponent = [](const AbstractPath& itemPath) + { + if (!AFS::getParentPath(itemPath)) //= device root + return AFS::getDisplayPath(itemPath); + return utfTo(AFS::getItemName(itemPath)); + }; + + if (AFS::isNullPath(itemPathL)) + return getLastComponent(itemPathR); + else if (AFS::isNullPath(itemPathR)) + return getLastComponent(itemPathL); + else + return getLastComponent(itemPathL) + L" | " + + getLastComponent(itemPathR); +} + + +void ContainerObject::removeDoubleEmpty() +{ + eraseIf(files_, [](const auto& fsObj) { return fsObj.ref().isPairEmpty(); }); + eraseIf(symlinks_, [](const auto& fsObj) { return fsObj.ref().isPairEmpty(); }); + eraseIf(subfolders_, [](const auto& fsObj) { return fsObj.ref().isPairEmpty(); }); + + for (FolderPair& folder : subfolders()) + folder.removeDoubleEmpty(); +} + + +namespace +{ +SyncOperation getIsolatedSyncOperation(const FileSystemObject& fsObj, + bool selectedForSync, + SyncDirection syncDir, + bool hasDirectionConflict) +{ + assert(!hasDirectionConflict || syncDir == SyncDirection::none); + + if (fsObj.isEmpty() || fsObj.isEmpty()) + { + if (!selectedForSync) + return SO_DO_NOTHING; + + if (hasDirectionConflict) + return SO_UNRESOLVED_CONFLICT; + + if (fsObj.isEmpty()) + { + if (fsObj.isEmpty()) //both sides empty: should only occur temporarily, if ever + return SO_EQUAL; + else //right-only + switch (syncDir) + { + case SyncDirection::left: return SO_CREATE_LEFT; + case SyncDirection::right: return SO_DELETE_RIGHT; + case SyncDirection::none: return SO_DO_NOTHING; + } + } + else //left-only + switch (syncDir) + { + case SyncDirection::left: return SO_DELETE_LEFT; + case SyncDirection::right: return SO_CREATE_RIGHT; + case SyncDirection::none: return SO_DO_NOTHING; + } + } + //-------------------------------------------------------------- + std::optional result; + + visitFSObject(fsObj, + [&](const FolderPair& folder) //see FolderPair::getCategory() + { + if (folder.hasEquivalentItemNames()) //a.k.a. DIR_EQUAL + { + assert(syncDir == SyncDirection::none); + return result = SO_EQUAL; //no matter if "conflict" (e.g. traversal error) or "not selected" + } + + if (!selectedForSync) + return result = SO_DO_NOTHING; + + if (hasDirectionConflict) + return result = SO_UNRESOLVED_CONFLICT; + + switch (syncDir) + { + case SyncDirection::left: return result = SO_RENAME_LEFT; + case SyncDirection::right: return result = SO_RENAME_RIGHT; + case SyncDirection::none: return result = SO_DO_NOTHING; + } + throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); + }, + //-------------------------------------------------------------- + [&](const FilePair& file) //see FilePair::getCategory() + { + if (file.getContentCategory() == FileContentCategory::equal && file.hasEquivalentItemNames()) //a.k.a. FILE_EQUAL + { + assert(syncDir == SyncDirection::none); + return result = SO_EQUAL; //no matter if "conflict" (e.g. traversal error) or "not selected" + } + + if (!selectedForSync) + return result = SO_DO_NOTHING; + + if (hasDirectionConflict) + return result = SO_UNRESOLVED_CONFLICT; + + switch (file.getContentCategory()) + { + case FileContentCategory::unknown: + case FileContentCategory::leftNewer: + case FileContentCategory::rightNewer: + case FileContentCategory::invalidTime: + case FileContentCategory::different: + case FileContentCategory::conflict: + switch (syncDir) + { + case SyncDirection::left: return result = SO_OVERWRITE_LEFT; + case SyncDirection::right: return result = SO_OVERWRITE_RIGHT; + case SyncDirection::none: return result = SO_DO_NOTHING; + } + break; + + case FileContentCategory::equal: + switch (syncDir) + { + case SyncDirection::left: return result = SO_RENAME_LEFT; + case SyncDirection::right: return result = SO_RENAME_RIGHT; + case SyncDirection::none: return result = SO_DO_NOTHING; + } + break; + } + throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); + }, + //-------------------------------------------------------------- + [&](const SymlinkPair& symlink) //see SymlinkPair::getCategory() + { + if (symlink.getContentCategory() == FileContentCategory::equal && symlink.hasEquivalentItemNames()) //a.k.a. SYMLINK_EQUAL + { + assert(syncDir == SyncDirection::none); + return result = SO_EQUAL; //no matter if "conflict" (e.g. traversal error) or "not selected" + } + + if (!selectedForSync) + return result = SO_DO_NOTHING; + + if (hasDirectionConflict) + return result = SO_UNRESOLVED_CONFLICT; + + switch (symlink.getContentCategory()) + { + case FileContentCategory::unknown: + case FileContentCategory::leftNewer: + case FileContentCategory::rightNewer: + case FileContentCategory::invalidTime: + case FileContentCategory::different: + case FileContentCategory::conflict: + switch (syncDir) + { + case SyncDirection::left: return result = SO_OVERWRITE_LEFT; + case SyncDirection::right: return result = SO_OVERWRITE_RIGHT; + case SyncDirection::none: return result = SO_DO_NOTHING; + } + break; + + case FileContentCategory::equal: + switch (syncDir) + { + case SyncDirection::left: return result = SO_RENAME_LEFT; + case SyncDirection::right: return result = SO_RENAME_RIGHT; + case SyncDirection::none: return result = SO_DO_NOTHING; + } + break; + } + throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); + }); + return *result; +} + + +template inline +bool hasDirectChild(const ContainerObject& conObj, Predicate p) +{ + return std::any_of(conObj.files ().begin(), conObj.files ().end(), p) || + std::any_of(conObj.symlinks ().begin(), conObj.symlinks ().end(), p) || + std::any_of(conObj.subfolders().begin(), conObj.subfolders().end(), p); +} +} + + +SyncOperation FileSystemObject::testSyncOperation(SyncDirection testSyncDir) const //semantics: "what if"! assumes "active, no conflict, no recursion (directory)! +{ + return getIsolatedSyncOperation(*this, true, testSyncDir, false); +} + +//SyncOperation FolderPair::testSyncOperation() const -> no recursion: we do NOT consider child elements when testing! + + +SyncOperation FileSystemObject::getSyncOperation() const +{ + return getIsolatedSyncOperation(*this, selectedForSync_, syncDir_, !syncDirectionConflict_.empty()); + //do *not* make a virtual call to testSyncOperation()! See FilePair::testSyncOperation()! <- better not implement one in terms of the other!!! +} + + +SyncOperation FolderPair::getSyncOperation() const +{ + if (!syncOpBuffered_) //redetermine... + { + //suggested operation *not* considering child elements + syncOpBuffered_ = FileSystemObject::getSyncOperation(); + + //action for child elements may occassionally have to overwrite parent task: + switch (*syncOpBuffered_) + { + case SO_OVERWRITE_LEFT: + case SO_OVERWRITE_RIGHT: + case SO_MOVE_LEFT_FROM: + case SO_MOVE_LEFT_TO: + case SO_MOVE_RIGHT_FROM: + case SO_MOVE_RIGHT_TO: + assert(false); + [[fallthrough]]; + case SO_CREATE_LEFT: + case SO_CREATE_RIGHT: + case SO_RENAME_LEFT: + case SO_RENAME_RIGHT: + case SO_EQUAL: + break; //take over suggestion, no problem for child-elements + case SO_DELETE_LEFT: + case SO_DELETE_RIGHT: + case SO_DO_NOTHING: + case SO_UNRESOLVED_CONFLICT: + if (isEmpty()) + { + //1. if at least one child-element is to be created, make sure parent folder is created also + //note: this automatically fulfills "create parent folders even if excluded" + if (hasDirectChild(*this, [](const FileSystemObject& fsObj) + { + assert(!fsObj.isPairEmpty() || fsObj.getSyncOperation() == SO_DO_NOTHING); + const SyncOperation op = fsObj.getSyncOperation(); + return op == SO_CREATE_LEFT || + op == SO_MOVE_LEFT_TO; + })) + syncOpBuffered_ = SO_CREATE_LEFT; + //2. cancel parent deletion if only a single child is not also scheduled for deletion + else if (*syncOpBuffered_ == SO_DELETE_RIGHT && + hasDirectChild(*this, [](const FileSystemObject& fsObj) + { + if (fsObj.isPairEmpty()) + return false; //fsObj may already be empty because it once contained a "move source" + const SyncOperation op = fsObj.getSyncOperation(); + return op != SO_DELETE_RIGHT && + op != SO_MOVE_RIGHT_FROM; + })) + syncOpBuffered_ = SO_DO_NOTHING; + } + else if (isEmpty()) + { + if (hasDirectChild(*this, [](const FileSystemObject& fsObj) + { + assert(!fsObj.isPairEmpty() || fsObj.getSyncOperation() == SO_DO_NOTHING); + const SyncOperation op = fsObj.getSyncOperation(); + return op == SO_CREATE_RIGHT || + op == SO_MOVE_RIGHT_TO; + })) + syncOpBuffered_ = SO_CREATE_RIGHT; + else if (*syncOpBuffered_ == SO_DELETE_LEFT && + hasDirectChild(*this, [](const FileSystemObject& fsObj) + { + if (fsObj.isPairEmpty()) + return false; + const SyncOperation op = fsObj.getSyncOperation(); + return op != SO_DELETE_LEFT && + op != SO_MOVE_LEFT_FROM; + })) + syncOpBuffered_ = SO_DO_NOTHING; + } + break; + } + } + return *syncOpBuffered_; +} + + +inline //called by private only! +SyncOperation FilePair::applyMoveOptimization(SyncOperation op) const +{ + /* check whether we can optimize "create + delete" via "move": + note: as long as we consider "create + delete" cases only, detection of renamed files, should be fine even for "binary" comparison variant! */ + if (FilePair* refFile = getMovePair()) + { + const SyncOperation opRef = refFile->FileSystemObject::getSyncOperation(); //do *not* make a virtual call! + if (op == SO_CREATE_LEFT && + opRef == SO_DELETE_LEFT) + op = SO_MOVE_LEFT_TO; + else if (op == SO_DELETE_LEFT && + opRef == SO_CREATE_LEFT) + op = SO_MOVE_LEFT_FROM; + else if (op == SO_CREATE_RIGHT && + opRef == SO_DELETE_RIGHT) + op = SO_MOVE_RIGHT_TO; + else if (op == SO_DELETE_RIGHT && + opRef == SO_CREATE_RIGHT) + op = SO_MOVE_RIGHT_FROM; + } + + return op; +} + + +SyncOperation FilePair::testSyncOperation(SyncDirection testSyncDir) const +{ + return applyMoveOptimization(FileSystemObject::testSyncOperation(testSyncDir)); +} + + +SyncOperation FilePair::getSyncOperation() const +{ + return applyMoveOptimization(FileSystemObject::getSyncOperation()); +} + + +std::wstring fff::getCategoryDescription(CompareFileResult cmpRes) +{ + switch (cmpRes) + { + case FILE_EQUAL: + return _("Both sides are equal"); + case FILE_RENAMED: + return _("Items differ in name only"); + case FILE_LEFT_ONLY: + return _("Item exists on left side only"); + case FILE_RIGHT_ONLY: + return _("Item exists on right side only"); + case FILE_LEFT_NEWER: + return _("Left side is newer"); + case FILE_RIGHT_NEWER: + return _("Right side is newer"); + case FILE_DIFFERENT_CONTENT: + return _("Items have different content"); + case FILE_TIME_INVALID: + case FILE_CONFLICT: + return _("Conflict/item cannot be categorized"); + } + assert(false); + return std::wstring(); +} + + +namespace +{ +const wchar_t arrowLeft [] = L"<-"; +const wchar_t arrowRight[] = L"->"; +//const wchar_t arrowRight[] = L"\u2192"; unicode arrows -> too small +} + + +std::wstring fff::getCategoryDescription(const FileSystemObject& fsObj) +{ + const std::wstring footer = [&] + { + if (fsObj.hasEquivalentItemNames()) + return L'\n' + fmtPath(fsObj.getItemName()); + else + return std::wstring(L"\n") + + fmtPath(fsObj.getItemName()) + L' ' + arrowLeft + L'\n' + + fmtPath(fsObj.getItemName()) + L' ' + arrowRight; + }(); + + if (const Zstringc descr = fsObj.getCategoryCustomDescription(); + !descr.empty()) + return utfTo(descr) + footer; + + const CompareFileResult cmpRes = fsObj.getCategory(); + switch (cmpRes) + { + case FILE_EQUAL: + case FILE_RENAMED: + case FILE_LEFT_ONLY: + case FILE_RIGHT_ONLY: + case FILE_DIFFERENT_CONTENT: + return getCategoryDescription(cmpRes) + footer; //use generic description + + case FILE_LEFT_NEWER: + case FILE_RIGHT_NEWER: + { + std::wstring descr = getCategoryDescription(cmpRes); + + visitFSObject(fsObj, [](const FolderPair& folder) {}, + [&](const FilePair& file) + { + descr += std::wstring(L"\n") + + formatUtcToLocalTime(file.getLastWriteTime()) + L' ' + arrowLeft + L'\n' + + formatUtcToLocalTime(file.getLastWriteTime()) + L' ' + arrowRight ; + }, + [&](const SymlinkPair& symlink) + { + descr += std::wstring(L"\n") + + formatUtcToLocalTime(symlink.getLastWriteTime()) + L' ' + arrowLeft + L'\n' + + formatUtcToLocalTime(symlink.getLastWriteTime()) + L' ' + arrowRight ; + }); + return descr + footer; + } + + case FILE_TIME_INVALID: + case FILE_CONFLICT: + assert(false); //should have getCategoryDescription()! + return _("Error") + footer; + } + assert(false); + return std::wstring(); +} + + +std::wstring fff::getSyncOpDescription(SyncOperation op) +{ + switch (op) + { + case SO_CREATE_LEFT: + return _("Copy new item to left"); + case SO_CREATE_RIGHT: + return _("Copy new item to right"); + case SO_DELETE_LEFT: + return _("Delete left item"); + case SO_DELETE_RIGHT: + return _("Delete right item"); + case SO_MOVE_LEFT_FROM: + case SO_MOVE_LEFT_TO: + return _("Move left file"); //move only supported for files + case SO_MOVE_RIGHT_FROM: + case SO_MOVE_RIGHT_TO: + return _("Move right file"); + case SO_OVERWRITE_LEFT: + return _("Update left item"); + case SO_OVERWRITE_RIGHT: + return _("Update right item"); + case SO_DO_NOTHING: + return _("Do nothing"); + case SO_EQUAL: + return _("Both sides are equal"); + case SO_RENAME_LEFT: + return _("Rename left item"); + case SO_RENAME_RIGHT: + return _("Rename right item"); + case SO_UNRESOLVED_CONFLICT: //not used on GUI, but in .csv + return _("Conflict/item cannot be categorized"); + } + assert(false); + return std::wstring(); +} + + +std::wstring fff::getSyncOpDescription(const FileSystemObject& fsObj) +{ + const SyncOperation op = fsObj.getSyncOperation(); + + const std::wstring rightArrowDown = languageLayoutIsRtl() ? + std::wstring() + RTL_MARK + LEFT_ARROW_ANTICLOCK : + std::wstring() + LTR_MARK + RIGHT_ARROW_CURV_DOWN; + //Windows bug: RIGHT_ARROW_CURV_DOWN rendering and extent calculation is buggy (see wx+\tooltip.cpp) => need LTR mark! + + auto generateFooter = [&] + { + if (fsObj.hasEquivalentItemNames()) + return L'\n' + fmtPath(fsObj.getItemName()); + + Zstring itemNameNew = fsObj.getItemName(); + Zstring itemNameOld = fsObj.getItemName(); + + if (const SyncDirection dir = getEffectiveSyncDir(op); + dir != SyncDirection::none) + { + if (dir == SyncDirection::left) + std::swap(itemNameNew, itemNameOld); + + return L'\n' + fmtPath(itemNameOld) + L' ' + rightArrowDown + L'\n' + fmtPath(itemNameNew); + } + else + return L'\n' + + fmtPath(itemNameNew) + L' ' + arrowLeft + L'\n' + + fmtPath(itemNameOld) + L' ' + arrowRight; + }; + + switch (op) + { + case SO_CREATE_LEFT: + case SO_CREATE_RIGHT: + case SO_DELETE_LEFT: + case SO_DELETE_RIGHT: + case SO_OVERWRITE_LEFT: + case SO_OVERWRITE_RIGHT: + case SO_DO_NOTHING: + case SO_EQUAL: + case SO_RENAME_LEFT: + case SO_RENAME_RIGHT: + return getSyncOpDescription(op) + generateFooter(); + + case SO_MOVE_LEFT_FROM: + case SO_MOVE_LEFT_TO: + case SO_MOVE_RIGHT_FROM: + case SO_MOVE_RIGHT_TO: + if (auto fileFrom = dynamic_cast(&fsObj)) + if (const FilePair* fileTo = fileFrom->getMovePair()) + { + const bool onLeft = op == SO_MOVE_LEFT_FROM || op == SO_MOVE_LEFT_TO; + const bool isMoveSource = op == SO_MOVE_LEFT_FROM || op == SO_MOVE_RIGHT_FROM; + + if (!isMoveSource) + std::swap(fileFrom, fileTo); + + auto getRelPath = [&](const FileSystemObject& fso) { return onLeft ? fso.getRelativePath() : fso.getRelativePath(); }; + + const Zstring relPathFrom = getRelPath(*fileFrom); + const Zstring relPathTo = getRelPath(*fileTo); + + //attention: ::SetWindowText() doesn't handle tab characters correctly in combination with certain file names, so don't use + return getSyncOpDescription(op) + L'\n' + + (beforeLast(relPathFrom, FILE_NAME_SEPARATOR, IfNotFoundReturn::none) == + beforeLast(relPathTo, FILE_NAME_SEPARATOR, IfNotFoundReturn::none) ? + //detected pure "rename" + fmtPath(getItemName(relPathFrom)) + L' ' + rightArrowDown + L'\n' + //show file name only + fmtPath(getItemName(relPathTo)) : + //"move" or "move + rename" + fmtPath(relPathFrom) + L' ' + rightArrowDown + L'\n' + + fmtPath(relPathTo)); + } + break; + + case SO_UNRESOLVED_CONFLICT: + return fsObj.getSyncOpConflict() + generateFooter(); + } + + assert(false); + return std::wstring(); +} diff --git a/FreeFileSync/Source/base/file_hierarchy.h b/FreeFileSync/Source/base/file_hierarchy.h new file mode 100644 index 0000000..1089520 --- /dev/null +++ b/FreeFileSync/Source/base/file_hierarchy.h @@ -0,0 +1,1493 @@ +// ***************************************************************************** +// * 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 FILE_HIERARCHY_H_257235289645296 +#define FILE_HIERARCHY_H_257235289645296 + +#include +#include +#include "structures.h" +#include "path_filter.h" +#include "../afs/abstract.h" + + +namespace fff +{ +struct FileAttributes +{ + time_t modTime = 0; //number of seconds since Jan. 1st 1970 GMT + uint64_t fileSize = 0; + AFS::FingerPrint filePrint = 0; //optional + bool isFollowedSymlink = false; + + static_assert(std::is_signed_v, "we need time_t to be signed"); + + std::strong_ordering operator<=>(const FileAttributes&) const = default; +}; + + +struct LinkAttributes +{ + time_t modTime = 0; //number of seconds since Jan. 1st 1970 GMT +}; + + +struct FolderAttributes +{ + bool isFollowedSymlink = false; +}; + + +struct FolderContainer +{ + //------------------------------------------------------------------ + //key: raw file name, without any (Unicode) normalization, preserving original upper-/lower-case + //"Changing data [...] to NFC would cause interoperability problems. Always leave data as it is." + using FolderList = std::unordered_map>; + using FileList = std::unordered_map; + using SymlinkList = std::unordered_map; + //------------------------------------------------------------------ + + FolderContainer() = default; + FolderContainer (const FolderContainer&) = delete; //catch accidental (and unnecessary) copying + FolderContainer& operator=(const FolderContainer&) = delete; // + + FileList files; + SymlinkList symlinks; //non-followed symlinks + FolderList folders; + + void addFile(const Zstring& itemName, const FileAttributes& attr) + { + files.insert_or_assign(itemName, attr); //update entry if already existing (e.g. during folder traverser "retry") + } + + void addSymlink(const Zstring& itemName, const LinkAttributes& attr) + { + symlinks.insert_or_assign(itemName, attr); + } + + FolderContainer& addFolder(const Zstring& itemName, const FolderAttributes& attr) + { + auto& p = folders[itemName]; //value default-construction is okay here + p.first = attr; + return p.second; + } +}; + +//------------------------------------------------------------------ + +enum class SelectSide +{ + left, + right +}; + + +template +constexpr SelectSide getOtherSide = side == SelectSide::left ? SelectSide::right : SelectSide::left; + + +template inline +T& selectParam(T& left, T& right) +{ + if constexpr (side == SelectSide::left) + return left; + else + return right; +} + + +enum class FileContentCategory : unsigned char +{ + unknown, + equal, + leftNewer, + rightNewer, + invalidTime, + different, + conflict, +}; + + +inline +SyncDirection getEffectiveSyncDir(SyncOperation syncOp) +{ + switch (syncOp) + { + case SO_CREATE_LEFT: + case SO_DELETE_LEFT: + case SO_OVERWRITE_LEFT: + case SO_RENAME_LEFT: + case SO_MOVE_LEFT_FROM: + case SO_MOVE_LEFT_TO: + return SyncDirection::left; + + case SO_CREATE_RIGHT: + case SO_DELETE_RIGHT: + case SO_OVERWRITE_RIGHT: + case SO_RENAME_RIGHT: + case SO_MOVE_RIGHT_FROM: + case SO_MOVE_RIGHT_TO: + return SyncDirection::right; + + case SO_DO_NOTHING: + case SO_EQUAL: + case SO_UNRESOLVED_CONFLICT: + break; //nothing to do + } + return SyncDirection::none; +} + + +std::wstring getShortDisplayNameForFolderPair(const AbstractPath& itemPathL, const AbstractPath& itemPathR); + + +class FileSystemObject; +class SymlinkPair; +class FilePair; +class FolderPair; +class BaseFolderPair; + +/*------------------------------------------------------------------ + inheritance diagram: + +std::enable_shared_from_this PathInformation + /|\ /|\ + |____________ _________|_________ + | | | + FileSystemObject ContainerObject + /|\ /|\ + ___________|___________ ______|______ + | | | | | + SymlinkPair FilePair FolderPair BaseFolderPair + +------------------------------------------------------------------*/ + +struct PathInformation //diamond-shaped inheritance! +{ + virtual ~PathInformation() {} + + template AbstractPath getAbstractPath() const; + template Zstring getRelativePath() const; //get path relative to base sync dir (without leading/trailing FILE_NAME_SEPARATOR) + +private: + virtual AbstractPath getAbstractPathL() const = 0; //implemented by FileSystemObject + BaseFolderPair + virtual AbstractPath getAbstractPathR() const = 0; // + + virtual Zstring getRelativePathL() const = 0; //implemented by SymlinkPair/FilePair + ContainerObject + virtual Zstring getRelativePathR() const = 0; // +}; + +template <> inline AbstractPath PathInformation::getAbstractPath() const { return getAbstractPathL(); } +template <> inline AbstractPath PathInformation::getAbstractPath() const { return getAbstractPathR(); } + +template <> inline Zstring PathInformation::getRelativePath() const { return getRelativePathL(); } +template <> inline Zstring PathInformation::getRelativePath() const { return getRelativePathR(); } + +//------------------------------------------------------------------ + +class ContainerObject : public virtual PathInformation +{ + friend class FileSystemObject; //access to updateRelPathsRecursion() + +public: + using FileList = std::vector>; //MergeSides::execute() requires a structure that doesn't invalidate pointers after push_back() + using SymlinkList = std::vector>; // + using FolderList = std::vector>; + + FolderPair& addFolder(const Zstring& itemNameL, const FolderAttributes& attribL, + const Zstring& itemNameR, const FolderAttributes& attribR); //exists on both sides + + template + FolderPair& addFolder(const Zstring& itemName, const FolderAttributes& attr); //exists on one side only + + FilePair& addFile(const Zstring& itemNameL, const FileAttributes& attribL, + const Zstring& itemNameR, const FileAttributes& attribR); //exists on both sides + + template + FilePair& addFile(const Zstring& itemName, const FileAttributes& attr); //exists on one side only + + SymlinkPair& addSymlink(const Zstring& itemNameL, const LinkAttributes& attribL, + const Zstring& itemNameR, const LinkAttributes& attribR); //exists on both sides + + template + SymlinkPair& addSymlink(const Zstring& itemName, const LinkAttributes& attr); //exists on one side only + + zen::Range> files() const { return {files_.begin(), files_.end()}; } + zen::Range> files() { return {files_.begin(), files_.end()}; } + + zen::Range> symlinks() const { return {symlinks_.begin(), symlinks_.end()}; } + zen::Range> symlinks() { return {symlinks_.begin(), symlinks_.end()}; } + + zen::Range> subfolders() const { return {subfolders_.begin(), subfolders_.end()}; } + zen::Range> subfolders() { return {subfolders_.begin(), subfolders_.end()}; } + + void clearFiles () { files_ .clear(); } + void clearSymlinks () { symlinks_ .clear(); } + void clearSubfolders() { subfolders_.clear(); } + + template + void foldersRemoveIf(Function fun) { zen::eraseIf(subfolders_, [fun](auto& fsObj) { return fun(fsObj.ref()); }); } + + const BaseFolderPair& getBase() const { return base_; } + /**/ BaseFolderPair& getBase() { return base_; } + + void removeDoubleEmpty(); //remove all invalid entries (where both sides are empty) recursively + + virtual void flip(); + +protected: + explicit ContainerObject(BaseFolderPair& baseFolder) : //used during BaseFolderPair constructor + base_(baseFolder) //take reference only: baseFolder *not yet* fully constructed at this point! + { assert(relPathL_.c_str() == relPathR_.c_str()); } //expected by the following contructor! + + explicit ContainerObject(const FileSystemObject& fsAlias); //used during FolderPair constructor + + virtual ~ContainerObject() //don't need polymorphic deletion, but we have a vtable anyway + { assert(relPathL_.c_str() == relPathR_.c_str() || relPathL_ != relPathR_); } + + template + void updateRelPathsRecursion(const FileSystemObject& fsAlias); + +private: + ContainerObject (const ContainerObject&) = delete; //this class is referenced by its child elements => make it non-copyable/movable! + ContainerObject& operator=(const ContainerObject&) = delete; + + Zstring getRelativePathL() const override { return relPathL_; } + Zstring getRelativePathR() const override { return relPathR_; } + + FileList files_; + SymlinkList symlinks_; + FolderList subfolders_; + + Zstring relPathL_; //path relative to base sync dir (without leading/trailing FILE_NAME_SEPARATOR) + Zstring relPathR_; //class invariant: shared Zstring iff equal! + + BaseFolderPair& base_; +}; + +//------------------------------------------------------------------ + +enum class BaseFolderStatus +{ + existing, + notExisting, + failure, +}; + +class BaseFolderPair : public ContainerObject +{ +public: + BaseFolderPair(const AbstractPath& folderPathLeft, + BaseFolderStatus folderStatusLeft, + const AbstractPath& folderPathRight, + BaseFolderStatus folderStatusRight, + const FilterRef& filter, + CompareVariant cmpVar, + unsigned int fileTimeTolerance, + const std::vector& ignoreTimeShiftMinutes) : + ContainerObject(*this), //trust that ContainerObject knows that *this is not yet fully constructed! + filter_(filter), cmpVar_(cmpVar), fileTimeTolerance_(fileTimeTolerance), ignoreTimeShiftMinutes_(ignoreTimeShiftMinutes), + folderStatusLeft_ (folderStatusLeft), + folderStatusRight_(folderStatusRight), + folderPathLeft_(folderPathLeft), + folderPathRight_(folderPathRight) {} + + template BaseFolderStatus getFolderStatus() const; //base folder status at the time of comparison! + template void setFolderStatus(BaseFolderStatus value); //update after creating the directory in FFS + + //get settings which were used while creating BaseFolderPair: + const PathFilter& getFilter() const { return filter_.ref(); } + CompareVariant getCompVariant() const { return cmpVar_; } + unsigned int getFileTimeTolerance() const { return fileTimeTolerance_; } + const std::vector& getIgnoredTimeShift() const { return ignoreTimeShiftMinutes_; } + + void flip() override; + +private: + AbstractPath getAbstractPathL() const override { return folderPathLeft_; } + AbstractPath getAbstractPathR() const override { return folderPathRight_; } + + const FilterRef filter_; //filter used while scanning directory: represents sub-view of actual files! + const CompareVariant cmpVar_; + const unsigned int fileTimeTolerance_; + const std::vector ignoreTimeShiftMinutes_; + + BaseFolderStatus folderStatusLeft_; + BaseFolderStatus folderStatusRight_; + + AbstractPath folderPathLeft_; + AbstractPath folderPathRight_; +}; + + +using FolderComparison = std::vector>; //make sure pointers to sub-elements remain valid +//don't change this back to std::vector inconsiderately: comparison uses push_back to add entries which may result in a full copy! + +zen::Range> inline asRange( FolderComparison& vect) { return {vect.begin(), vect.end()}; } +zen::Range> inline asRange(const FolderComparison& vect) { return {vect.begin(), vect.end()}; } + +//------------------------------------------------------------------ +struct FSObjectVisitor +{ + virtual ~FSObjectVisitor() {} + virtual void visit(const FilePair& file ) = 0; + virtual void visit(const SymlinkPair& symlink) = 0; + virtual void visit(const FolderPair& folder ) = 0; +}; + +//------------------------------------------------------------------ + +class FileSystemObject : public std::enable_shared_from_this, public virtual PathInformation +{ +public: + virtual void accept(FSObjectVisitor& visitor) const = 0; + + bool isPairEmpty() const; //true, if both sides are empty + template bool isEmpty() const; + + //path getters always return valid values, even if isEmpty()! + template Zstring getItemName() const; //case sensitive! + + bool hasEquivalentItemNames() const; //*quick* check if left/right names are equivalent when ignoring Unicode normalization forms + + //for use during compare() only: + virtual void setCategoryConflict(const Zstringc& description) = 0; + + //comparison result + virtual CompareFileResult getCategory() const = 0; + virtual Zstringc getCategoryCustomDescription() const = 0; //optional + + //sync settings + void setSyncDir(SyncDirection newDir); + void setSyncDirConflict(const Zstringc& description); //set syncDir = SyncDirection::none + fill conflict description + + bool isActive() const { return selectedForSync_; } + void setActive(bool active); + + //sync operation + virtual SyncOperation testSyncOperation(SyncDirection testSyncDir) const; //"what if" semantics! assumes "active, no conflict, no recursion (directory)! + virtual SyncOperation getSyncOperation() const; + std::wstring getSyncOpConflict() const; //return conflict when determining sync direction or (still unresolved) conflict during categorization + + const ContainerObject& parent() const { return parent_; } + /**/ ContainerObject& parent() { return parent_; } + const BaseFolderPair& base() const { return parent_.getBase(); } + /**/ BaseFolderPair& base() { return parent_.getBase(); } + + bool passFileFilter(const PathFilter& filter) const; //optimized for perf! + + virtual void flip(); + + template + void setItemName(const Zstring& itemName); + +protected: + FileSystemObject(const Zstring& itemNameL, + const Zstring& itemNameR, + ContainerObject& parentObj) : + itemNameL_(itemNameL), + itemNameR_(itemNameL == itemNameR ? itemNameL : itemNameR), //perf: no measurable speed drawback; -3% peak memory => further needed by ContainerObject construction! + parent_(parentObj) + { + assert(itemNameL_.c_str() == itemNameR_.c_str() || itemNameL_ != itemNameR_); //also checks ref-counted string precondition + FileSystemObject::notifySyncCfgChanged(); //non-virtual call! (=> anyway in a constructor!) + } + + virtual ~FileSystemObject() //don't need polymorphic deletion, but we have a vtable anyway + { assert(itemNameL_.c_str() == itemNameR_.c_str() || itemNameL_ != itemNameR_); } + + virtual void notifySyncCfgChanged() + { + if (auto fsParent = dynamic_cast(&parent_)) + fsParent->notifySyncCfgChanged(); //propagate! + } + + template void removeFsObject(); + +private: + FileSystemObject (const FileSystemObject&) = delete; + FileSystemObject& operator=(const FileSystemObject&) = delete; + + AbstractPath getAbstractPathL() const override { return AFS::appendRelPath(base().getAbstractPath(), getRelativePath()); } + AbstractPath getAbstractPathR() const override { return AFS::appendRelPath(base().getAbstractPath(), getRelativePath()); } + + template + void propagateChangedItemName(); //required after any itemName changes + + bool selectedForSync_ = true; + + SyncDirection syncDir_ = SyncDirection::none; + Zstringc syncDirectionConflict_; //non-empty if we have a conflict setting sync-direction + //conserve memory (avoid std::string SSO overhead + allow ref-counting!) + + Zstring itemNameL_; //use as indicator: empty means "not existing on this side" + Zstring itemNameR_; //class invariant: same Zstring.c_str() pointer iff equal! + + ContainerObject& parent_; +}; + +//------------------------------------------------------------------ + + +class FolderPair : public FileSystemObject, public ContainerObject +{ +public: + void accept(FSObjectVisitor& visitor) const override; + + CompareFileResult getCategory() const override; + CompareDirResult getDirCategory() const { return static_cast(getCategory()); } + + FolderPair(const Zstring& itemNameL, const FolderAttributes& attrL, //use empty itemName if "not existing" + const Zstring& itemNameR, const FolderAttributes& attrR, + ContainerObject& parentObj) : + FileSystemObject(itemNameL, itemNameR, parentObj), + ContainerObject(static_cast(*this)), //FileSystemObject fully constructed at this point! + attrL_(attrL), + attrR_(attrR) {} + + template bool isFollowedSymlink() const; + + SyncOperation getSyncOperation() const override; + + template + void setSyncedTo(bool isSymlinkTrg, bool isSymlinkSrc); //call after successful sync + + bool passDirFilter(const PathFilter& filter, bool* childItemMightMatch) const; //optimized for perf! + + void flip() override; + + void setCategoryConflict(const Zstringc& description) override; + Zstringc getCategoryCustomDescription() const override; //optional + + template void removeItem(); + +private: + void notifySyncCfgChanged() override { syncOpBuffered_ = {}; FileSystemObject::notifySyncCfgChanged(); } + + mutable std::optional syncOpBuffered_; //determining sync-op for directory may be expensive as it depends on child-objects => buffer + + FolderAttributes attrL_; + FolderAttributes attrR_; + + Zstringc categoryConflict_; //conserve memory (avoid std::string SSO overhead + allow ref-counting! +}; + + +//------------------------------------------------------------------ + +class FilePair : public FileSystemObject +{ +public: + void accept(FSObjectVisitor& visitor) const override; + + FilePair(const Zstring& itemNameL, //use empty string if "not existing" + const FileAttributes& attrL, + const Zstring& itemNameR, // + const FileAttributes& attrR, + ContainerObject& parentObj) : + FileSystemObject(itemNameL, itemNameR, parentObj), + attrL_(attrL), + attrR_(attrR) {} + + CompareFileResult getCategory() const override; + + template time_t getLastWriteTime() const; + template uint64_t getFileSize() const; + template bool isFollowedSymlink() const; + template FileAttributes getAttributes() const; + template AFS::FingerPrint getFilePrint() const; + template void clearFilePrint(); + + + void setMovePair(FilePair* ref); //reference to corresponding moved/renamed file + FilePair* getMovePair() const; //may be nullptr + + SyncOperation testSyncOperation(SyncDirection testSyncDir) const override; //semantics: "what if"! assumes "active, no conflict, no recursion (directory)! + SyncOperation getSyncOperation() const override; + + template + void setSyncedTo(uint64_t fileSize, + time_t lastWriteTimeTrg, + time_t lastWriteTimeSrc, + AFS::FingerPrint filePrintTrg, + AFS::FingerPrint filePrintSrc, + bool isSymlinkTrg, + bool isSymlinkSrc); //call after successful sync + + void flip() override; + + void setCategoryConflict(const Zstringc& description) override; + void setCategoryInvalidTime(const Zstringc& description); + Zstringc getCategoryCustomDescription() const override; //optional + + void setContentCategory(FileContentCategory category); + FileContentCategory getContentCategory() const; + + template void removeItem(); + +private: + Zstring getRelativePathL() const override { return appendPath(parent().getRelativePath(), getItemName()); } + Zstring getRelativePathR() const override { return appendPath(parent().getRelativePath(), getItemName()); } + + SyncOperation applyMoveOptimization(SyncOperation op) const; + + FileAttributes attrL_; + FileAttributes attrR_; + + std::weak_ptr moveFileRef_; //optional, filled by DetectMovedFiles::findAndSetMovePair() + + FileContentCategory contentCategory_ = FileContentCategory::unknown; + Zstringc categoryDescr_; //optional: custom category description (e.g. FileContentCategory::conflict or invalidTime) +}; + +//------------------------------------------------------------------ + +class SymlinkPair : public FileSystemObject //models an unresolved symbolic link: followed-links should go in FilePair/FolderPair +{ +public: + void accept(FSObjectVisitor& visitor) const override; + + SymlinkPair(const Zstring& itemNameL, //use empty string if "not existing" + const LinkAttributes& attrL, + const Zstring& itemNameR, //use empty string if "not existing" + const LinkAttributes& attrR, + ContainerObject& parentObj) : + FileSystemObject(itemNameL, itemNameR, parentObj), + attrL_(attrL), + attrR_(attrR) {} + + CompareFileResult getCategory() const override; + CompareSymlinkResult getLinkCategory() const { return static_cast(getCategory()); } + + template time_t getLastWriteTime() const; //write time of the link, NOT target! + + template + void setSyncedTo(time_t lastWriteTimeTrg, + time_t lastWriteTimeSrc); //call after successful sync + + void flip() override; + + void setCategoryConflict(const Zstringc& description) override; + void setCategoryInvalidTime(const Zstringc& description); + Zstringc getCategoryCustomDescription() const override; //optional + + void setContentCategory(FileContentCategory category); + FileContentCategory getContentCategory() const; + + template void removeItem(); + +private: + Zstring getRelativePathL() const override { return appendPath(parent().getRelativePath(), getItemName()); } + Zstring getRelativePathR() const override { return appendPath(parent().getRelativePath(), getItemName()); } + + LinkAttributes attrL_; + LinkAttributes attrR_; + + FileContentCategory contentCategory_ = FileContentCategory::unknown; + Zstringc categoryDescr_; //optional: custom category description (e.g. FileContentCategory::conflict or invalidTime) +}; + +//------------------------------------------------------------------ + +//generic descriptions (usecase CSV legend, sync config) +std::wstring getCategoryDescription(CompareFileResult cmpRes); +std::wstring getSyncOpDescription(SyncOperation op); + +//item-specific descriptions +std::wstring getCategoryDescription(const FileSystemObject& fsObj); +std::wstring getSyncOpDescription(const FileSystemObject& fsObj); + +//------------------------------------------------------------------ + +namespace impl +{ +template +struct FSObjectLambdaVisitor : public FSObjectVisitor +{ + FSObjectLambdaVisitor(Function1 onFolder, + Function2 onFile, + Function3 onSymlink) : //unifying assignment + onFolder_(std::move(onFolder)), onFile_(std::move(onFile)), onSymlink_(std::move(onSymlink)) {} +private: + void visit(const FolderPair& folder ) override { onFolder_ (folder); } + void visit(const FilePair& file ) override { onFile_ (file); } + void visit(const SymlinkPair& symlink) override { onSymlink_(symlink); } + + const Function1 onFolder_; + const Function2 onFile_; + const Function3 onSymlink_; +}; +} + +template inline +void visitFSObject(const FileSystemObject& fsObj, Function1 onFolder, Function2 onFile, Function3 onSymlink) +{ + impl::FSObjectLambdaVisitor visitor(onFolder, onFile, onSymlink); + fsObj.accept(visitor); +} + + +template inline +void visitFSObject(FileSystemObject& fsObj, Function1 onFolder, Function2 onFile, Function3 onSymlink) +{ + visitFSObject(static_cast(fsObj), + [onFolder ](const FolderPair& folder) { onFolder (const_cast(folder)); }, // + [onFile ](const FilePair& file) { onFile (const_cast(file )); }, //physical object is not const anyway + [onSymlink](const SymlinkPair& symlink) { onSymlink(const_cast(symlink)); }); // +} + +//------------------------------------------------------------------ + +namespace impl +{ +template +class RecursiveObjectVisitor +{ +public: + RecursiveObjectVisitor(Function1 onFolder, + Function2 onFile, + Function3 onSymlink) : //unifying assignment + onFolder_ (std::move(onFolder)), + onFile_ (std::move(onFile)), + onSymlink_(std::move(onSymlink)) {} + + void execute(ContainerObject& conObj) + { + for (FilePair& file : conObj.files()) + onFile_(file); + for (SymlinkPair& symlink : conObj.symlinks()) + onSymlink_(symlink); + for (FolderPair& subFolder : conObj.subfolders()) + { + onFolder_(subFolder); + execute(subFolder); + } + } + +private: + RecursiveObjectVisitor (const RecursiveObjectVisitor&) = delete; + RecursiveObjectVisitor& operator=(const RecursiveObjectVisitor&) = delete; + + const Function1 onFolder_; + const Function2 onFile_; + const Function3 onSymlink_; +}; +} + +template inline +void visitFSObjectRecursively(ContainerObject& conObj, //consider contained items only + Function1 onFolder, + Function2 onFile, + Function3 onSymlink) +{ + impl::RecursiveObjectVisitor(onFolder, onFile, onSymlink).execute(conObj); +} + +template inline +void visitFSObjectRecursively(FileSystemObject& fsObj, //consider item and contained items (if folder) + Function1 onFolder, + Function2 onFile, + Function3 onSymlink) +{ + visitFSObject(fsObj, [onFolder, onFile, onSymlink](FolderPair& folder) + { + onFolder(folder); + impl::RecursiveObjectVisitor(onFolder, onFile, onSymlink).execute(const_cast(folder)); + }, onFile, onSymlink); +} + + + + + + + + + + + + + + + + + + +//--------------------- implementation ------------------------------------------ + +//inline virtual... admittedly its use may be limited +inline void FolderPair ::accept(FSObjectVisitor& visitor) const { visitor.visit(*this); } +inline void FilePair ::accept(FSObjectVisitor& visitor) const { visitor.visit(*this); } +inline void SymlinkPair::accept(FSObjectVisitor& visitor) const { visitor.visit(*this); } + + +inline +void FileSystemObject::setSyncDir(SyncDirection newDir) +{ + syncDir_ = newDir; + syncDirectionConflict_.clear(); + + notifySyncCfgChanged(); +} + + +inline +void FileSystemObject::setSyncDirConflict(const Zstringc& description) +{ + assert(!description.empty()); + syncDir_ = SyncDirection::none; + syncDirectionConflict_ = description; + + notifySyncCfgChanged(); +} + + +inline +std::wstring FileSystemObject::getSyncOpConflict() const +{ + assert(getSyncOperation() == SO_UNRESOLVED_CONFLICT); + return zen::utfTo(syncDirectionConflict_); +} + + +inline +void FileSystemObject::setActive(bool active) +{ + selectedForSync_ = active; + notifySyncCfgChanged(); +} + + +template inline +bool FileSystemObject::isEmpty() const +{ + return selectParam(itemNameL_, itemNameR_).empty(); +} + + +inline +bool FileSystemObject::isPairEmpty() const +{ + return isEmpty() && isEmpty(); +} + + +template inline +Zstring FileSystemObject::getItemName() const +{ + //assert(!itemNameL_.empty() || !itemNameR_.empty()); //-> file pair might be temporarily empty (until removed after sync) + + const Zstring& itemName = selectParam(itemNameL_, itemNameR_); //empty if not existing + if (!itemName.empty()) //avoid ternary-WTF! (implicit copy-constructor call!!!!!!) + return itemName; + return selectParam>(itemNameL_, itemNameR_); +} + + +inline +bool FileSystemObject::hasEquivalentItemNames() const +{ + if (itemNameL_.c_str() == itemNameR_.c_str() || //most likely case + itemNameL_.empty() || itemNameR_.empty()) // + return true; + + assert(itemNameL_ != itemNameR_); //class invariant + return getUnicodeNormalForm(itemNameL_) == getUnicodeNormalForm(itemNameR_); +} + + +template inline +void FileSystemObject::removeFsObject() +{ + if (isEmpty>()) + { + selectParam(itemNameL_, itemNameR_) = selectParam>(itemNameL_, itemNameR_); //ensure (c_str) class invariant! + setSyncDir(SyncDirection::none); //calls notifySyncCfgChanged() + } + else + { + selectParam(itemNameL_, itemNameR_).clear(); + //keep current syncDir_ + notifySyncCfgChanged(); //needed!? + } + + propagateChangedItemName(); +} + + +template inline +void FilePair::removeItem() +{ + if (isEmpty>()) + setMovePair(nullptr); //cut ties between "move" pairs + + selectParam(attrL_, attrR_) = FileAttributes(); + contentCategory_ = FileContentCategory::unknown; + removeFsObject(); +} + + +template inline +void SymlinkPair::removeItem() +{ + selectParam(attrL_, attrR_) = LinkAttributes(); + contentCategory_ = FileContentCategory::unknown; + removeFsObject(); +} + + +template inline +void FolderPair::removeItem() +{ + for (FilePair& file : files()) + file.removeItem(); + for (SymlinkPair& symlink : symlinks()) + symlink.removeItem(); + for (FolderPair& folder : subfolders()) + folder.removeItem(); + + selectParam(attrL_, attrR_) = FolderAttributes(); + removeFsObject(); +} + + +template inline +void FileSystemObject::setItemName(const Zstring& itemName) +{ + assert(!itemName.empty()); + assert(!isPairEmpty()); + + selectParam(itemNameL_, itemNameR_) = itemName; + + if (itemNameL_.c_str() != itemNameR_.c_str() && + itemNameL_ == itemNameR_) + itemNameL_ = itemNameR_; //preserve class invariant + assert(itemNameL_.c_str() == itemNameR_.c_str() || itemNameL_ != itemNameR_); + + propagateChangedItemName(); +} + + +template inline +void FileSystemObject::propagateChangedItemName() +{ + if (itemNameL_.empty() && itemNameR_.empty()) return; //both sides might just have been deleted by removeItem<> + + if (auto conObj = dynamic_cast(this)) + { + const Zstring& itemNameOld = zen::getItemName(conObj->getRelativePath()); + if (itemNameOld != getItemName()) //perf: premature optimization? + conObj->updateRelPathsRecursion(*this); + } +} + + +template inline +void ContainerObject::updateRelPathsRecursion(const FileSystemObject& fsAlias) +{ + //perf: only call if actual item name changed: + assert(selectParam(relPathL_, relPathR_) != appendPath(fsAlias.parent().getRelativePath(), fsAlias.getItemName())); + + constexpr SelectSide otherSide = getOtherSide; + + if (fsAlias.isEmpty()) //=> 1. other side's relPath also needs updating! 2. both sides have same name + selectParam(relPathL_, relPathR_) = appendPath(selectParam(fsAlias.parent().relPathL_, + fsAlias.parent().relPathR_), fsAlias.getItemName()); + else //assume relPath on other side is up to date! + assert(selectParam(relPathL_, relPathR_) == appendPath(fsAlias.parent().getRelativePath(), fsAlias.getItemName())); + + if (fsAlias.parent().relPathL_.c_str() == // + fsAlias.parent().relPathR_.c_str() && //see ContainerObject constructor and setItemName() + fsAlias.getItemName().c_str() == // + fsAlias.getItemName().c_str()) // + selectParam(relPathL_, relPathR_) = selectParam(relPathL_, relPathR_); + else + selectParam(relPathL_, relPathR_) = appendPath(selectParam(fsAlias.parent().relPathL_, + fsAlias.parent().relPathR_), fsAlias.getItemName()); + assert(relPathL_.c_str() == relPathR_.c_str() || relPathL_ != relPathR_); + + for (FolderPair& folder : subfolders()) + folder.updateRelPathsRecursion(folder); +} + + +inline +ContainerObject::ContainerObject(const FileSystemObject& fsAlias) : + relPathL_(appendPath(fsAlias.parent().relPathL_, fsAlias.getItemName())), + relPathR_(fsAlias.parent().relPathL_.c_str() == // + fsAlias.parent().relPathR_.c_str() && //take advantage of FileSystemObject's Zstring reuse: + fsAlias.getItemName().c_str() == //=> perf: 12% faster merge phase; -4% peak memory + fsAlias.getItemName().c_str() ? // + relPathL_ : //ternary-WTF! (implicit copy-constructor call!!) => no big deal for a Zstring + appendPath(fsAlias.parent().relPathR_, fsAlias.getItemName())), + base_(fsAlias.parent().base_) +{ + assert(relPathL_.c_str() == relPathR_.c_str() || relPathL_ != relPathR_); +} + + +inline +FolderPair& ContainerObject::addFolder(const Zstring& itemNameL, const FolderAttributes& attribL, + const Zstring& itemNameR, const FolderAttributes& attribR) +{ + subfolders_.push_back(makeSharedRef(itemNameL, attribL, itemNameR, attribR, *this)); + return subfolders_.back().ref(); +} + + +template <> inline +FolderPair& ContainerObject::addFolder(const Zstring& itemName, const FolderAttributes& attr) +{ + return addFolder(itemName, attr, Zstring(), FolderAttributes()); +} + + +template <> inline +FolderPair& ContainerObject::addFolder(const Zstring& itemName, const FolderAttributes& attr) +{ + return addFolder(Zstring(), FolderAttributes(), itemName, attr); +} + + +inline +FilePair& ContainerObject::addFile(const Zstring& itemNameL, const FileAttributes& attribL, + const Zstring& itemNameR, const FileAttributes& attribR) +{ + files_.push_back(makeSharedRef(itemNameL, attribL, itemNameR, attribR, *this)); + return files_.back().ref(); +} + + +template <> inline +FilePair& ContainerObject::addFile(const Zstring& itemName, const FileAttributes& attr) +{ + return addFile(itemName, attr, Zstring(), FileAttributes()); +} + + +template <> inline +FilePair& ContainerObject::addFile(const Zstring& itemName, const FileAttributes& attr) +{ + return addFile(Zstring(), FileAttributes(), itemName, attr); +} + + +inline +SymlinkPair& ContainerObject::addSymlink(const Zstring& itemNameL, const LinkAttributes& attribL, + const Zstring& itemNameR, const LinkAttributes& attribR) +{ + symlinks_.push_back(makeSharedRef(itemNameL, attribL, itemNameR, attribR, *this)); + return symlinks_.back().ref(); +} + + +template <> inline +SymlinkPair& ContainerObject::addSymlink(const Zstring& itemName, const LinkAttributes& attr) +{ + return addSymlink(itemName, attr, Zstring(), LinkAttributes()); +} + + +template <> inline +SymlinkPair& ContainerObject::addSymlink(const Zstring& itemName, const LinkAttributes& attr) +{ + return addSymlink(Zstring(), LinkAttributes(), itemName, attr); +} + + +inline +void FileSystemObject::flip() +{ + std::swap(itemNameL_, itemNameR_); + notifySyncCfgChanged(); +} + + +inline +void ContainerObject::flip() +{ + for (FilePair& file : files()) + file.flip(); + for (SymlinkPair& symlink : symlinks()) + symlink.flip(); + for (FolderPair& folder : subfolders()) + folder.flip(); + + std::swap(relPathL_, relPathR_); +} + + +inline +void BaseFolderPair::flip() +{ + ContainerObject::flip(); + std::swap(folderStatusLeft_, folderStatusRight_); + std::swap(folderPathLeft_, folderPathRight_); +} + + +inline +void FolderPair::flip() //this overrides both ContainerObject/FileSystemObject::flip! +{ + ContainerObject ::flip(); //call base class versions + FileSystemObject::flip(); // + std::swap(attrL_, attrR_); +} + + +inline +void FilePair::flip() +{ + FileSystemObject::flip(); //call base class version + std::swap(attrL_, attrR_); + + switch (contentCategory_) + { + case FileContentCategory::unknown: + case FileContentCategory::equal: + case FileContentCategory::invalidTime: + case FileContentCategory::different: + case FileContentCategory::conflict: break; + case FileContentCategory::leftNewer: contentCategory_ = FileContentCategory::rightNewer; break; + case FileContentCategory::rightNewer: contentCategory_ = FileContentCategory::leftNewer; break; + } +} + + +inline +void SymlinkPair::flip() +{ + FileSystemObject::flip(); //call base class versions + std::swap(attrL_, attrR_); + + switch (contentCategory_) + { + case FileContentCategory::unknown: + case FileContentCategory::equal: + case FileContentCategory::invalidTime: + case FileContentCategory::different: + case FileContentCategory::conflict: break; + case FileContentCategory::leftNewer: contentCategory_ = FileContentCategory::rightNewer; break; + case FileContentCategory::rightNewer: contentCategory_ = FileContentCategory::leftNewer; break; + } +} + + +template inline +BaseFolderStatus BaseFolderPair::getFolderStatus() const +{ + return selectParam(folderStatusLeft_, folderStatusRight_); +} + + +template inline +void BaseFolderPair::setFolderStatus(BaseFolderStatus value) +{ + selectParam(folderStatusLeft_, folderStatusRight_) = value; +} + + +inline +void FolderPair::setCategoryConflict(const Zstringc& description) +{ + assert(!description.empty()); + categoryConflict_ = description; +} + + +inline +void FilePair::setCategoryConflict(const Zstringc& description) +{ + assert(!description.empty()); + categoryDescr_ = description; + contentCategory_ = FileContentCategory::conflict; +} + + +inline +void SymlinkPair::setCategoryConflict(const Zstringc& description) +{ + assert(!description.empty()); + categoryDescr_ = description; + contentCategory_ = FileContentCategory::conflict; +} + + +inline +void FilePair::setCategoryInvalidTime(const Zstringc& description) +{ + assert(!description.empty()); + categoryDescr_ = description; + contentCategory_ = FileContentCategory::invalidTime; +} + + +inline +void SymlinkPair::setCategoryInvalidTime(const Zstringc& description) +{ + assert(!description.empty()); + categoryDescr_ = description; + contentCategory_ = FileContentCategory::invalidTime; +} + + +inline Zstringc FolderPair ::getCategoryCustomDescription() const { return categoryConflict_; } +inline Zstringc FilePair ::getCategoryCustomDescription() const { return categoryDescr_; } +inline Zstringc SymlinkPair::getCategoryCustomDescription() const { return categoryDescr_; } + + +inline +void FilePair::setContentCategory(FileContentCategory category) +{ + assert(!isEmpty() &&!isEmpty()); + assert(category != FileContentCategory::unknown); + contentCategory_ = category; +} + + +inline +void SymlinkPair::setContentCategory(FileContentCategory category) +{ + assert(!isEmpty() &&!isEmpty()); + assert(category != FileContentCategory::unknown); + contentCategory_ = category; +} + + +inline +FileContentCategory FilePair::getContentCategory() const +{ + assert(!isEmpty() &&!isEmpty()); + return contentCategory_; +} + + +inline +FileContentCategory SymlinkPair::getContentCategory() const +{ + assert(!isEmpty() &&!isEmpty()); + return contentCategory_; +} + + +inline +CompareFileResult FolderPair::getCategory() const +{ + if (!categoryConflict_.empty()) + return FILE_CONFLICT; + + if (isEmpty()) + { + if (isEmpty()) + return FILE_EQUAL; + else + return FILE_RIGHT_ONLY; + } + else + { + if (isEmpty()) + return FILE_LEFT_ONLY; + else + return hasEquivalentItemNames() ? FILE_EQUAL : FILE_RENAMED; + } +} + + +inline +CompareFileResult FilePair::getCategory() const +{ + assert(contentCategory_ == FileContentCategory::conflict || + (isEmpty() || isEmpty()) == (contentCategory_ == FileContentCategory::unknown)); + assert(contentCategory_ != FileContentCategory::conflict || !categoryDescr_.empty()); + + if (contentCategory_ == FileContentCategory::conflict) + { + assert(!categoryDescr_.empty()); + return FILE_CONFLICT; + } + + if (isEmpty()) + { + if (isEmpty()) + return FILE_EQUAL; + else + return FILE_RIGHT_ONLY; + } + else + { + if (isEmpty()) + return FILE_LEFT_ONLY; + else + //Caveat: + //1. FILE_EQUAL may only be set if names match in case: InSyncFolder's mapping tables use file name as a key! see db_file.cpp + //2. harmonize with "bool stillInSync()" in algorithm.cpp, FilePair::setSyncedTo() in file_hierarchy.h + //3. FILE_EQUAL is expected to mean identical file sizes! See InSyncFile + switch (contentCategory_) + { + case FileContentCategory::unknown: + case FileContentCategory::conflict: assert(false); return FILE_CONFLICT; + case FileContentCategory::equal: return hasEquivalentItemNames() ? FILE_EQUAL : FILE_RENAMED; + case FileContentCategory::leftNewer: return FILE_LEFT_NEWER; + case FileContentCategory::rightNewer: return FILE_RIGHT_NEWER; + case FileContentCategory::invalidTime: return FILE_TIME_INVALID; + case FileContentCategory::different: return FILE_DIFFERENT_CONTENT; + } + } + throw std::logic_error(std::string(__FILE__) + '[' + zen::numberTo(__LINE__) + "] Contract violation!"); +} + + +inline +CompareFileResult SymlinkPair::getCategory() const +{ + assert(contentCategory_ == FileContentCategory::conflict || + (isEmpty() || isEmpty()) == (contentCategory_ == FileContentCategory::unknown)); + assert(contentCategory_ != FileContentCategory::conflict || !categoryDescr_.empty()); + + if (contentCategory_ == FileContentCategory::conflict) + { + assert(!categoryDescr_.empty()); + return FILE_CONFLICT; + } + + if (isEmpty()) + { + if (isEmpty()) + return FILE_EQUAL; + else + return FILE_RIGHT_ONLY; + } + else + { + if (isEmpty()) + return FILE_LEFT_ONLY; + else + //Caveat: + //1. SYMLINK_EQUAL may only be set if names match in case: InSyncFolder's mapping tables use link name as a key! see db_file.cpp + //2. harmonize with "bool stillInSync()" in algorithm.cpp, FilePair::setSyncedTo() in file_hierarchy.h + switch (contentCategory_) + { + case FileContentCategory::unknown: + case FileContentCategory::conflict: assert(false); return FILE_CONFLICT; + case FileContentCategory::equal: return hasEquivalentItemNames() ? FILE_EQUAL : FILE_RENAMED; + case FileContentCategory::leftNewer: return FILE_LEFT_NEWER; + case FileContentCategory::rightNewer: return FILE_RIGHT_NEWER; + case FileContentCategory::invalidTime: return FILE_TIME_INVALID; + case FileContentCategory::different: return FILE_DIFFERENT_CONTENT; + } + } + throw std::logic_error(std::string(__FILE__) + '[' + zen::numberTo(__LINE__) + "] Contract violation!"); +} + + +template inline +FileAttributes FilePair::getAttributes() const +{ + assert(!isEmpty()); + return selectParam(attrL_, attrR_); +} + + +template inline +time_t FilePair::getLastWriteTime() const +{ + assert(!isEmpty()); + return selectParam(attrL_, attrR_).modTime; +} + + +template inline +uint64_t FilePair::getFileSize() const +{ + assert(!isEmpty()); + return selectParam(attrL_, attrR_).fileSize; +} + + +template inline +bool FolderPair::isFollowedSymlink() const +{ + assert(!isEmpty()); + return selectParam(attrL_, attrR_).isFollowedSymlink; +} + + +template inline +bool FilePair::isFollowedSymlink() const +{ + assert(!isEmpty()); + return selectParam(attrL_, attrR_).isFollowedSymlink; +} + + +template inline +AFS::FingerPrint FilePair::getFilePrint() const +{ + assert(!isEmpty()); + return selectParam(attrL_, attrR_).filePrint; +} + + +template inline +void FilePair::clearFilePrint() +{ + selectParam(attrL_, attrR_).filePrint = 0; +} + + +inline +void FilePair::setMovePair(FilePair* ref) +{ + FilePair* refOld = getMovePair(); + if (ref != refOld) + { + if (refOld) + refOld->moveFileRef_.reset(); + + if (ref) + { + FilePair* refOld2 = ref->getMovePair(); + assert(!refOld2); //destroying already exising pair!? why? + if (refOld2) + refOld2 ->moveFileRef_.reset(); + + /**/ moveFileRef_ = std::static_pointer_cast(ref->shared_from_this()); + ref->moveFileRef_ = std::static_pointer_cast( shared_from_this()); + } + else + moveFileRef_.reset(); + } + else + assert(!ref); //are we called needlessly!? +} + + +inline +FilePair* FilePair::getMovePair() const +{ + if (moveFileRef_.expired()) //skip std::shared_ptr construction => premature optimization? + return nullptr; + + FilePair* ref = moveFileRef_.lock().get(); + assert(!ref || (isEmpty() != isEmpty())); + assert(!ref || ref->moveFileRef_.lock().get() == this); //both ends should agree + return ref; +} + + +template inline +void FolderPair::setSyncedTo(bool isSymlinkTrg, + bool isSymlinkSrc) +{ + selectParam< sideTrg >(attrL_, attrR_) = {.isFollowedSymlink = isSymlinkTrg}; + selectParam>(attrL_, attrR_) = {.isFollowedSymlink = isSymlinkSrc}; + + setItemName(getItemName>()); + + categoryConflict_.clear(); + setSyncDir(SyncDirection::none); +} + + +template inline +void FilePair::setSyncedTo(uint64_t fileSize, + time_t lastWriteTimeTrg, + time_t lastWriteTimeSrc, + AFS::FingerPrint filePrintTrg, + AFS::FingerPrint filePrintSrc, + bool isSymlinkTrg, + bool isSymlinkSrc) +{ + setMovePair(nullptr); //cut ties between "move" pairs + + selectParam< sideTrg >(attrL_, attrR_) = {lastWriteTimeTrg, fileSize, filePrintTrg, isSymlinkTrg}; + selectParam>(attrL_, attrR_) = {lastWriteTimeSrc, fileSize, filePrintSrc, isSymlinkSrc}; + + setItemName(getItemName>()); + + contentCategory_ = FileContentCategory::equal; + categoryDescr_.clear(); + setSyncDir(SyncDirection::none); +} + + +template inline +void SymlinkPair::setSyncedTo(time_t lastWriteTimeTrg, + time_t lastWriteTimeSrc) +{ + selectParam< sideTrg >(attrL_, attrR_) = {.modTime = lastWriteTimeTrg}; + selectParam>(attrL_, attrR_) = {.modTime = lastWriteTimeSrc}; + + setItemName(getItemName>()); + + contentCategory_ = FileContentCategory::equal; + categoryDescr_.clear(); + setSyncDir(SyncDirection::none); +} + + +inline +bool FolderPair::passDirFilter(const PathFilter& filter, bool* childItemMightMatch) const +{ + const Zstring& relPathL = getRelativePath(); + const Zstring& relPathR = getRelativePath(); + assert(relPathL.c_str() == relPathR.c_str() || relPathL!= relPathR); + + if (filter.passDirFilter(relPathL, childItemMightMatch)) + return relPathL.c_str() == relPathR.c_str() /*perf!*/ || equalNoCase(relPathL, relPathR) || + filter.passDirFilter(relPathR, childItemMightMatch); + else + { + if (childItemMightMatch && *childItemMightMatch && + relPathL.c_str() != relPathR.c_str() /*perf!*/ && !equalNoCase(relPathL, relPathR)) + filter.passDirFilter(relPathR, childItemMightMatch); + return false; + } +} + + +inline +bool FileSystemObject::passFileFilter(const PathFilter& filter) const +{ + assert(!dynamic_cast(this)); + assert(parent().getRelativePath().c_str() == + parent().getRelativePath().c_str() || + parent().getRelativePath()!= + parent().getRelativePath()); + assert(getItemName().c_str() == + getItemName().c_str() || + getItemName() != + getItemName()); + + const Zstring relPathL = getRelativePath(); + + if (!filter.passFileFilter(relPathL)) + return false; + + if (parent().getRelativePath().c_str() == // + parent().getRelativePath().c_str() && //perf! see ContainerObject constructor + getItemName().c_str() == // + getItemName().c_str()) // + return true; + + const Zstring relPathR = getRelativePath(); + + if (equalNoCase(relPathL, relPathR)) + return true; + + return filter.passFileFilter(relPathR); +} + + +template inline +time_t SymlinkPair::getLastWriteTime() const +{ + return selectParam(attrL_, attrR_).modTime; +} +} + +#endif //FILE_HIERARCHY_H_257235289645296 diff --git a/FreeFileSync/Source/base/icon_loader.cpp b/FreeFileSync/Source/base/icon_loader.cpp new file mode 100644 index 0000000..ee94da0 --- /dev/null +++ b/FreeFileSync/Source/base/icon_loader.cpp @@ -0,0 +1,315 @@ +// ***************************************************************************** +// * 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 "icon_loader.h" +#include //includes + + #include + #include + #include + #include + #include + + +using namespace zen; +using namespace fff; + + +namespace +{ +ImageHolder copyToImageHolder(const GdkPixbuf& pixBuf, int maxSize) //throw SysError +{ + //see: https://developer.gnome.org/gdk-pixbuf/stable/gdk-pixbuf-The-GdkPixbuf-Structure.html + if (const GdkColorspace cs = ::gdk_pixbuf_get_colorspace(&pixBuf); + cs != GDK_COLORSPACE_RGB) + throw SysError(formatSystemError("gdk_pixbuf_get_colorspace", L"", L"Unexpected color space: " + numberTo(static_cast(cs)))); + + if (const int bitCount = ::gdk_pixbuf_get_bits_per_sample(&pixBuf); + bitCount != 8) + throw SysError(formatSystemError("gdk_pixbuf_get_bits_per_sample", L"", L"Unexpected bits per sample: " + numberTo(bitCount))); + + const int channels = ::gdk_pixbuf_get_n_channels(&pixBuf); + if (channels != 3 && channels != 4) + throw SysError(formatSystemError("gdk_pixbuf_get_n_channels", L"", L"Unexpected number of channels: " + numberTo(channels))); + + assert(::gdk_pixbuf_get_has_alpha(&pixBuf) == (channels == 4)); + + const unsigned char* srcBytes = ::gdk_pixbuf_read_pixels(&pixBuf); + const int srcWidth = ::gdk_pixbuf_get_width (&pixBuf); + const int srcHeight = ::gdk_pixbuf_get_height(&pixBuf); + const int srcStride = ::gdk_pixbuf_get_rowstride(&pixBuf); + + //don't stretch small images, shrink large ones only! + int targetWidth = srcWidth; + int targetHeight = srcHeight; + + const int maxExtent = std::max(targetWidth, targetHeight); + if (maxSize < maxExtent) + { + targetWidth = numeric::intDivRound(targetWidth * maxSize, maxExtent); + targetHeight = numeric::intDivRound(targetHeight * maxSize, maxExtent); + } + ImageHolder imgOut(targetWidth, targetHeight, true /*withAlpha*/); + unsigned char* rgbOut = imgOut.getRgb(); + unsigned char* alphaOut = imgOut.getAlpha(); + + if (srcWidth != targetWidth || + srcHeight != targetHeight) + { + const auto pixRead = [srcBytes, srcStride, channels](int x, int y) + { + const unsigned char* const ptr = srcBytes + y * srcStride + channels * x; //RGB(A) byte order + + const int a = channels == 4 ? ptr[3] : 255; + + return [a, ptr](int channel) + { + if (channel == 3) + return a; + + return ptr[channel] * a; + //Limitation: alpha should be applied in gamma-decoded linear RGB space: https://ssp.impulsetrain.com/gamma-premult.html + }; + }; + + const auto pixWrite = [rgbOut, alphaOut](const auto& interpolate) mutable + { + const double a = interpolate(3); + if (a <= 0.0) + { + *alphaOut++ = 0; + rgbOut += 3; //don't care about color + } + else + { + *alphaOut++ = xbrz::byteRound(a); + *rgbOut++ = xbrz::byteRound(interpolate(0) / a); //r + *rgbOut++ = xbrz::byteRound(interpolate(1) / a); //g + *rgbOut++ = xbrz::byteRound(interpolate(2) / a); //b + } + }; + xbrz::bilinearScale(pixRead, //PixReader pixRead + srcWidth, //int srcWidth + srcHeight, //int srcHeight + pixWrite, //PixWriter pixWrite + targetWidth, //int trgWidth + targetHeight, //int trgHeight + 0, //int yFirst + targetHeight); //int yLast +#if 0 //alternative: but does it support alpha-channel? + GdkPixbuf* pixBufShrinked = ::gdk_pixbuf_scale_simple(pixBuf, //const GdkPixbuf* src + targetWidth, //int dest_width + targetHeight, //int dest_height + GDK_INTERP_BILINEAR); //GdkInterpType interp_type + if (!pixBufShrinked) + throw SysError(formatSystemError("gdk_pixbuf_scale_simple", L"", L"Not enough memory.")); + ZEN_ON_SCOPE_EXIT(::g_object_unref(pixBufShrinked)); +#endif + } + else //perf: going overboard? + for (int y = 0; y < srcHeight; ++y) + for (int x = 0; x < srcWidth; ++x) + { + const unsigned char* const ptr = srcBytes + y * srcStride + channels * x; //RGB(A) byte order + + *alphaOut++ = channels == 4 ? ptr[3] : 255; + *rgbOut++ = ptr[0]; + *rgbOut++ = ptr[1]; + *rgbOut++ = ptr[2]; + } + + return imgOut; +} + + +ImageHolder imageHolderFromGicon(GIcon& gicon, int maxSize) //throw SysError +{ + assert(runningOnMainThread()); //GTK is NOT thread safe!!! + assert(!G_IS_FILE_ICON(&gicon) && !G_IS_LOADABLE_ICON(&gicon)); //see comment in image_holder.h => icon loading must not block main thread + + GtkIconTheme* const defaultTheme = ::gtk_icon_theme_get_default(); //not owned! + ASSERT_SYSERROR(defaultTheme); //no more error details + + GtkIconInfo* const iconInfo = ::gtk_icon_theme_lookup_by_gicon(defaultTheme, //GtkIconTheme* icon_theme + &gicon, //GIcon* icon + maxSize, //gint size + GTK_ICON_LOOKUP_USE_BUILTIN); //GtkIconLookupFlags flags + if (!iconInfo) + throw SysError(formatSystemError("gtk_icon_theme_lookup_by_gicon", L"", L"Icon not available.")); +#if GTK_MAJOR_VERSION == 2 + ZEN_ON_SCOPE_EXIT(::gtk_icon_info_free(iconInfo)); +#elif GTK_MAJOR_VERSION == 3 + ZEN_ON_SCOPE_EXIT(::g_object_unref(iconInfo)); +#else +#error unknown GTK version! +#endif + GError* error = nullptr; + ZEN_ON_SCOPE_EXIT(if (error) ::g_error_free(error)); + + GdkPixbuf* const pixBuf = ::gtk_icon_info_load_icon(iconInfo, &error); + if (!pixBuf) + throw SysError(formatGlibError("gtk_icon_info_load_icon", error)); + ZEN_ON_SCOPE_EXIT(::g_object_unref(pixBuf)); + + //we may have to shrink (e.g. GTK3, openSUSE): "an icon theme may have icons that differ slightly from their nominal sizes" + return copyToImageHolder(*pixBuf, maxSize); //throw SysError +} +} + + +FileIconHolder fff::getIconByTemplatePath(const Zstring& templatePath, int maxSize) //throw SysError +{ + //uses full file name, e.g. "AUTHORS" has own mime type on Linux: + gchar* const contentType = ::g_content_type_guess(templatePath.c_str(), //const gchar* filename + nullptr, //const guchar* data + 0, //gsize data_size + nullptr); //gboolean* result_uncertain + if (!contentType) + throw SysError(formatSystemError("g_content_type_guess(" + copyStringTo(templatePath) + ')', L"", L"Unknown content type.")); + ZEN_ON_SCOPE_EXIT(::g_free(contentType)); + + GIcon* const fileIcon = ::g_content_type_get_icon(contentType); + if (!fileIcon) + throw SysError(formatSystemError("g_content_type_get_icon(" + std::string(contentType) + ')', L"", L"Icon not available.")); + + return FileIconHolder(fileIcon /*pass ownership*/, maxSize); + +} + + +FileIconHolder fff::genericFileIcon(int maxSize) //throw SysError +{ + //we're called by getDisplayIcon()! -> avoid endless recursion! + GIcon* const fileIcon = ::g_content_type_get_icon("text/plain"); + if (!fileIcon) + throw SysError(formatSystemError("g_content_type_get_icon(text/plain)", L"", L"Icon not available.")); + + return FileIconHolder(fileIcon /*pass ownership*/, maxSize); + +} + + +FileIconHolder fff::genericDirIcon(int maxSize) //throw SysError +{ + GIcon* const dirIcon = ::g_content_type_get_icon("inode/directory"); //should contain fallback to GTK_STOCK_DIRECTORY ("gtk-directory") + if (!dirIcon) + throw SysError(formatSystemError("g_content_type_get_icon(inode/directory)", L"", L"Icon not available.")); + + return FileIconHolder(dirIcon /*pass ownership*/, maxSize); + +} + + +FileIconHolder fff::getTrashIcon(int maxSize) //throw SysError +{ + GIcon* const trashIcon = ::g_themed_icon_new("user-trash-full"); //empty: "user-trash" + if (!trashIcon) + throw SysError(formatSystemError("g_themed_icon_new(user-trash-full)", L"", L"Icon not available.")); + + return FileIconHolder(trashIcon /*pass ownership*/, maxSize); + +} + + +FileIconHolder fff::getFileManagerIcon(int maxSize) //throw SysError +{ + GIcon* const trashIcon = ::g_themed_icon_new("system-file-manager"); + if (!trashIcon) + throw SysError(formatSystemError("g_themed_icon_new(system-file-manager)", L"", L"Icon not available.")); + + return FileIconHolder(trashIcon /*pass ownership*/, maxSize); + +} + + +FileIconHolder fff::getFileIcon(const Zstring& filePath, int maxSize) //throw SysError +{ + GFile* file = ::g_file_new_for_path(filePath.c_str()); //documented to "never fail" + ZEN_ON_SCOPE_EXIT(::g_object_unref(file)); + + GError* error = nullptr; + ZEN_ON_SCOPE_EXIT(if (error) ::g_error_free(error)); + + GFileInfo* const fileInfo = ::g_file_query_info(file, G_FILE_ATTRIBUTE_STANDARD_ICON, G_FILE_QUERY_INFO_NONE, nullptr /*cancellable*/, &error); + if (!fileInfo) + throw SysError(formatGlibError("g_file_query_info", error)); + ZEN_ON_SCOPE_EXIT(::g_object_unref(fileInfo)); + + GIcon* const gicon = ::g_file_info_get_icon(fileInfo); //no ownership transfer! + if (!gicon) + throw SysError(formatSystemError("g_file_info_get_icon", L"", L"Icon not available.")); + + //https://github.com/GNOME/gtk/blob/master/gtk/gtkicontheme.c#L4082 + if (G_IS_FILE_ICON(gicon) || G_IS_LOADABLE_ICON(gicon)) //see comment in image_holder.h + throw SysError(L"Icon loading might block main thread."); + //shouldn't be a problem for native file systems -> G_IS_THEMED_ICON(gicon) + + //the remaining icon types won't block! + assert(GDK_IS_PIXBUF(gicon) || G_IS_THEMED_ICON(gicon) || G_IS_EMBLEMED_ICON(gicon)); + + g_object_ref(gicon); /*macro!*/ //pass ownership + return FileIconHolder(gicon, maxSize); // + +} + + +ImageHolder fff::getThumbnailImage(const Zstring& filePath, int maxSize) //throw SysError +{ + struct stat fileInfo = {}; + if (::stat(filePath.c_str(), &fileInfo) != 0) + THROW_LAST_SYS_ERROR("stat"); + + if (!S_ISREG(fileInfo.st_mode)) //skip blocking file types, e.g. named pipes, see file_io.cpp + throw SysError(_("Unsupported item type.") + L" [" + printNumber(L"0%06o", fileInfo.st_mode & S_IFMT) + L']'); + + GError* error = nullptr; + ZEN_ON_SCOPE_EXIT(if (error) ::g_error_free(error)); + + GdkPixbuf* const pixBuf = ::gdk_pixbuf_new_from_file(filePath.c_str(), &error); + if (!pixBuf) + throw SysError(formatGlibError("gdk_pixbuf_new_from_file", error)); + ZEN_ON_SCOPE_EXIT(::g_object_unref(pixBuf)); + + return copyToImageHolder(*pixBuf, maxSize); //throw SysError + +} + + +wxImage fff::extractWxImage(ImageHolder&& ih) +{ + assert(runningOnMainThread()); + if (!ih.getRgb()) + return wxNullImage; + + wxImage img(ih.getWidth(), ih.getHeight(), ih.releaseRgb(), false /*static_data*/); //pass ownership + if (ih.getAlpha()) + img.SetAlpha(ih.releaseAlpha(), false /*static_data*/); + else + { + assert(false); + img.SetAlpha(); + ::memset(img.GetAlpha(), wxIMAGE_ALPHA_OPAQUE, ih.getWidth() * ih.getHeight()); + } + return img; +} + + +wxImage fff::extractWxImage(zen::FileIconHolder&& fih) +{ + assert(runningOnMainThread()); + + wxImage img; + if (GIcon* gicon = fih.gicon.get()) + try + { + img = extractWxImage(imageHolderFromGicon(*gicon, fih.maxSize)); //throw SysError + } + catch (SysError&) {} //might fail if icon theme is missing a MIME type! + + fih.gicon.reset(); + return img; + +} diff --git a/FreeFileSync/Source/base/icon_loader.h b/FreeFileSync/Source/base/icon_loader.h new file mode 100644 index 0000000..c3dc06f --- /dev/null +++ b/FreeFileSync/Source/base/icon_loader.h @@ -0,0 +1,34 @@ +// ***************************************************************************** +// * 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 ICON_LOADER_H_1348701985713445 +#define ICON_LOADER_H_1348701985713445 + +#include +#include +#include + + +namespace fff +{ +//=> all functions are safe to call from multiple threads! +//COM needs to be initialized before calling any of these functions! CoInitializeEx/CoUninitialize +//=> don't call from WM_PAINT handler! https://docs.microsoft.com/en-us/archive/blogs/yvesdolc/do-you-receive-wm_paint-when-waiting-for-a-com-call-to-return + +zen::FileIconHolder getIconByTemplatePath(const Zstring& templatePath, int maxSize); //throw SysError +zen::FileIconHolder genericFileIcon(int maxSize); //throw SysError +zen::FileIconHolder genericDirIcon (int maxSize); //throw SysError +zen::FileIconHolder getTrashIcon (int maxSize); //throw SysError +zen::FileIconHolder getFileManagerIcon(int maxSize); //throw SysError +zen::FileIconHolder getFileIcon(const Zstring& filePath, int maxSize); //throw SysError +zen::ImageHolder getThumbnailImage(const Zstring& filePath, int maxSize); //throw SysError + +//invalidates image holder! call from GUI thread only! +wxImage extractWxImage(zen::ImageHolder&& ih); +wxImage extractWxImage(zen::FileIconHolder&& fih); //might fail if icon theme is missing a MIME type! +} + +#endif //ICON_LOADER_H_1348701985713445 diff --git a/FreeFileSync/Source/base/lock_holder.h b/FreeFileSync/Source/base/lock_holder.h new file mode 100644 index 0000000..127d337 --- /dev/null +++ b/FreeFileSync/Source/base/lock_holder.h @@ -0,0 +1,54 @@ +#ifndef LOCK_HOLDER_H_489572039485723453425 +#define LOCK_HOLDER_H_489572039485723453425 + +#include "dir_lock.h" +#include "process_callback.h" + + +namespace fff +{ + +//Attention: 1. call after having checked directory existence! +// 2. perf: remove folder aliases (e.g. case differences) *before* calling this function!!! + +//hold locks for a number of directories without blocking during lock creation +class LockHolder +{ +public: + LockHolder(const std::set& folderPaths, bool& warnDirectoryLockFailed, PhaseCallback& pcb /*throw X*/) + { + using namespace zen; + + std::vector> failedLocks; + + for (const Zstring& folderPath : folderPaths) + try + { + //lock file creation is synchronous and may block noticeably for slow devices (USB sticks, mapped cloud storage) + lockHolder_.emplace_back(folderPath, + [&](std::wstring&& msg) { pcb.updateStatus(std::move(msg)); /*throw X*/ }, + UI_UPDATE_INTERVAL / 2); //throw FileError + } + catch (const FileError& e) { failedLocks.emplace_back(folderPath, e); } + + if (!failedLocks.empty()) + { + std::wstring msg = _("Cannot set directory locks for the following folders:"); + + for (const auto& [folderPath, error] : failedLocks) + { + msg += L"\n\n"; + //msg += fmtPath(folderPath) + L'\n' -> seems redundant + msg += replaceCpy(error.toString(), L"\n\n", L'\n'); + } + + pcb.reportWarning(msg, warnDirectoryLockFailed); //throw X + } + } + +private: + std::vector lockHolder_; +}; +} + +#endif //LOCK_HOLDER_H_489572039485723453425 diff --git a/FreeFileSync/Source/base/multi_rename.cpp b/FreeFileSync/Source/base/multi_rename.cpp new file mode 100644 index 0000000..cd1dd86 --- /dev/null +++ b/FreeFileSync/Source/base/multi_rename.cpp @@ -0,0 +1,198 @@ +// ***************************************************************************** +// * 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 "multi_rename.h" +#include + +using namespace zen; +using namespace fff; + + +namespace +{ +std::wstring_view findLongestSubstring(const std::vector& strings) +{ + if (strings.empty()) + return {}; + + const std::wstring_view strMin = *std::min_element(strings.begin(), strings.end(), + /**/[](const std::wstring_view lhs, const std::wstring_view rhs) { return lhs.size() < rhs.size(); }); + + for (size_t sz = strMin.size(); sz > 0; --sz) //iterate over size, descending + for (size_t i = 0; i + sz <= strMin.size(); ++i) + { + const std::wstring_view substr(strMin.data() + i, sz); + //perf: duplicate substrings, especially für size = 1? + + const bool isCommon = [&] + { + for (const std::wstring_view str : strings) + if (str.data() != strMin.data()) //sufficient check: an extension of strMin necessarily contains "substr" anyway + if (!contains(str, substr)) + return false; + return true; + }(); + + if (isCommon) + return substr; //*first* occuring substring of maximum size + } + + return {}; +} + + +struct StringPart +{ + std::vector diff; //may be empty, but only at beginning (or between filename/extension parts) + std::wstring_view common; //may be empty, but only at end (dito) +}; + +std::vector getStringParts(std::vector&& strings) +{ + std::wstring_view substr = findLongestSubstring(strings); + if (!substr.empty()) + { + std::vector head; + std::vector tail; + + for (const std::wstring_view str : strings) + { + head.push_back(beforeFirst(str, substr, IfNotFoundReturn::none)); + tail.push_back(afterFirst (str, substr, IfNotFoundReturn::none)); + } + + std::vector np = getStringParts(std::move(head)); + assert(np.empty() || np.back().common.empty()); //otherwise we could construct an even longer substring! + + if (np.empty()) + np.push_back({{}, substr}); + else + np.back().common = substr; + + const std::vector npTail = getStringParts(std::move(tail)); + assert(npTail.empty() || !npTail.front().diff.empty()); //otherwise we could construct an even longer substring! + + append(np, npTail); + return np; + } + else + { + if (std::all_of(strings.begin(), strings.end(), [](const std::wstring_view str) { return str.empty(); })) + /**/return {}; + + return {{std::move(strings), {}}}; + } +} + + +constexpr wchar_t placeholders[] = //http://xahlee.info/comp/unicode_circled_numbers.html +{ + //L'\u24FF', //⓿ <- rendered bigger than the rest (same for ⓫) on Centos Linux + L'\u2776', //❶ + L'\u2777', //❷ + L'\u2778', //❸ + L'\u2779', //❹ + L'\u277A', //❺ + L'\u277B', //❻ + L'\u277C', //❼ + L'\u277D', //❽ + L'\u277E', //❾ + L'\u277F', //❿ -> last one is special: represents "all the rest" +}; + + +inline +size_t getPlaceholderIndex(wchar_t c) +{ + static_assert(std::size(placeholders) == 10); + if (placeholders[0] <= c && c <= placeholders[9]) + return static_cast(c - placeholders[0]); + + return static_cast(-1); +} +} + + +bool fff::isRenamePlaceholderChar(wchar_t c) { return getPlaceholderIndex(c) < std::size(placeholders); } + + +struct fff::RenameBuf +{ + explicit RenameBuf(const std::vector& s) : strings(s) + { + //file extensions deserve special treatment: https://freefilesync.org/forum/viewtopic.php?t=11943#p46453 + std::vector names; + std::vector extensions; //including "." + for (const std::wstring& fileName : strings) + { + auto it = findLast(fileName.begin(), fileName.end(), L'.'); + names. push_back(makeStringView(fileName.begin(), it)); + extensions.push_back(makeStringView(it, fileName.end())); + } + + parts = getStringParts(std::move(names)); + append(parts, getStringParts(std::move(extensions))); + } + + std::vector strings; + std::vector parts; +}; + + +//e.g. "Season ❶, Episode ❷ - ❸.avi" +std::pair> fff::getPlaceholderPhrase(const std::vector& strings) +{ + auto renameBuf = makeSharedRef(strings); + + std::wstring phrase; + size_t placeIdx = 0; + + for (const StringPart& p : renameBuf.ref().parts) + { + if (!p.diff.empty()) + { + phrase += placeholders[placeIdx++]; + + if (placeIdx >= std::size(placeholders)) + break; //represent "all the rest" with last placeholder + } + phrase += p.common; //TODO: what if common part incidentally contains placeholder character!? + } + return {phrase, renameBuf}; +} + + +const std::vector fff::resolvePlaceholderPhrase(const std::wstring_view phrase, const RenameBuf& buf) +{ + std::vector*> diffByIdx; + + for (const StringPart& p : buf.parts) + if (!p.diff.empty()) + diffByIdx.push_back(&p.diff), assert(p.diff.size() == buf.strings.size()); + + std::vector output; + + for (size_t i = 0; i < buf.strings.size(); ++i) + { + std::wstring resolved; + + for (const wchar_t c : phrase) + if (const size_t placeIdx = getPlaceholderIndex(c); + placeIdx < diffByIdx.size()) + { + if (placeIdx == std::size(placeholders) - 1) //last placeholder represents "all the rest" + resolved.append((*diffByIdx[placeIdx])[i].data(), buf.strings[i].data() + buf.strings[i].size()); + else + resolved += (*diffByIdx[placeIdx])[i]; + } + else + resolved += c; + + output.push_back(std::move(resolved)); + } + + return output; +} diff --git a/FreeFileSync/Source/base/multi_rename.h b/FreeFileSync/Source/base/multi_rename.h new file mode 100644 index 0000000..ce44ddb --- /dev/null +++ b/FreeFileSync/Source/base/multi_rename.h @@ -0,0 +1,23 @@ +// ***************************************************************************** +// * 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 MULTI_RENAME_H_489572039485723453425 +#define MULTI_RENAME_H_489572039485723453425 + +#include +#include + +namespace fff +{ +struct RenameBuf; + +std::pair> getPlaceholderPhrase(const std::vector& strings); +const std::vector resolvePlaceholderPhrase(const std::wstring_view phrase, const RenameBuf& buf); + +bool isRenamePlaceholderChar(wchar_t c); +} + +#endif //MULTI_RENAME_H_489572039485723453425 diff --git a/FreeFileSync/Source/base/norm_filter.h b/FreeFileSync/Source/base/norm_filter.h new file mode 100644 index 0000000..f96a0ae --- /dev/null +++ b/FreeFileSync/Source/base/norm_filter.h @@ -0,0 +1,67 @@ +// ***************************************************************************** +// * 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 NORM_FILTER_H_974896787346251 +#define NORM_FILTER_H_974896787346251 + +#include "path_filter.h" +#include "soft_filter.h" + + +namespace fff +{ +struct NormalizedFilter //grade-a filter: global/local filter settings combined, units resolved, ready for use +{ + NormalizedFilter(const FilterRef& hf, const SoftFilter& sf) : nameFilter(hf), timeSizeFilter(sf) {} + + //"hard" filter: relevant during comparison, physically skips files + FilterRef nameFilter; + //"soft" filter: relevant after comparison; equivalent to user selection + SoftFilter timeSizeFilter; +}; + + +//combine global and local filters via "logical and" +NormalizedFilter normalizeFilters(const FilterConfig& global, const FilterConfig& local); + +inline +bool isNullFilter(const FilterConfig& filterCfg) +{ + return NameFilter::isNull(filterCfg.includeFilter, filterCfg.excludeFilter) && + SoftFilter(filterCfg.timeSpan, filterCfg.unitTimeSpan, + filterCfg.sizeMin, filterCfg.unitSizeMin, + filterCfg.sizeMax, filterCfg.unitSizeMax).isNull(); +} + + + + + + + + + + +// ----------------------- implementation ----------------------- +inline +NormalizedFilter normalizeFilters(const FilterConfig& global, const FilterConfig& local) +{ + SoftFilter globalTimeSize(global.timeSpan, global.unitTimeSpan, + global.sizeMin, global.unitSizeMin, + global.sizeMax, global.unitSizeMax); + + SoftFilter localTimeSize(local.timeSpan, local.unitTimeSpan, + local.sizeMin, local.unitSizeMin, + local.sizeMax, local.unitSizeMax); + + + return NormalizedFilter(constructFilter(global.includeFilter, global.excludeFilter, + local .includeFilter, local .excludeFilter), + combineFilters(globalTimeSize, localTimeSize)); +} +} + +#endif //NORM_FILTER_H_974896787346251 diff --git a/FreeFileSync/Source/base/parallel_scan.cpp b/FreeFileSync/Source/base/parallel_scan.cpp new file mode 100644 index 0000000..fd9dd70 --- /dev/null +++ b/FreeFileSync/Source/base/parallel_scan.cpp @@ -0,0 +1,465 @@ +// ***************************************************************************** +// * 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 "parallel_scan.h" +#include +#include +#include + +using namespace zen; +using namespace fff; + + +namespace +{ +const int FOLDER_TRAVERSAL_LEVEL_MAX = 100; + +/* PERF NOTE + + --------------------------------------------- + |Test case: Reading from two different disks| + --------------------------------------------- + Windows 7: + 1st(unbuffered) |2nd (OS buffered) + ---------------------------------- + 1 Thread: 57s | 8s + 2 Threads: 39s | 7s + + --------------------------------------------------- + |Test case: Reading two directories from same disk| + --------------------------------------------------- + Windows 7: Windows XP: + 1st(unbuffered) |2nd (OS buffered) 1st(unbuffered) |2nd (OS buffered) + ---------------------------------- ---------------------------------- + 1 Thread: 41s | 13s 1 Thread: 45s | 13s + 2 Threads: 42s | 11s 2 Threads: 38s | 8s + + => Traversing does not take any advantage of file locality so that multiple threads operating on the same disk impose no performance overhead! (even faster on XP) */ + +class AsyncCallback +{ +public: + AsyncCallback(size_t threadsToFinish, std::chrono::milliseconds cbInterval) : threadsToFinish_(threadsToFinish), cbInterval_(cbInterval) {} + + //blocking call: context of worker thread + AFS::TraverserCallback::HandleError reportError(const AFS::TraverserCallback::ErrorInfo& errorInfo) //throw ThreadStopRequest + { + assert(!runningOnMainThread()); + std::unique_lock dummy(lockRequest_); + interruptibleWait(conditionReadyForNewRequest_, dummy, [this] { return !errorRequest_ && !errorResponse_; }); //throw ThreadStopRequest + + errorRequest_ = errorInfo; + conditionNewRequest.notify_all(); + + interruptibleWait(conditionHaveResponse_, dummy, [this] { return static_cast(errorResponse_); }); //throw ThreadStopRequest + + AFS::TraverserCallback::HandleError rv = *errorResponse_; + + errorRequest_ = std::nullopt; + errorResponse_ = std::nullopt; + + dummy.unlock(); //optimization for condition_variable::notify_all() + conditionReadyForNewRequest_.notify_all(); //instead of notify_one(); work around bug: https://svn.boost.org/trac/boost/ticket/7796 + + return rv; + } + + //context of main thread + void waitUntilDone(const TravErrorCb& onError, const TravStatusCb& onStatusUpdate) //throw X + { + assert(runningOnMainThread()); + for (;;) + { + const std::chrono::steady_clock::time_point callbackTime = std::chrono::steady_clock::now() + cbInterval_; + + for (std::unique_lock dummy(lockRequest_) ;;) //process all errors without delay + { + const bool rv = conditionNewRequest.wait_until(dummy, callbackTime, [this] { return (errorRequest_ && !errorResponse_) || (threadsToFinish_ == 0); }); + if (!rv) //time-out + condition not met + break; + + if (errorRequest_ && !errorResponse_) + { + assert(threadsToFinish_ != 0); + switch (onError({errorRequest_->msg, errorRequest_->failTime, errorRequest_->retryNumber})) //throw X + { + case PhaseCallback::ignore: + errorResponse_ = AFS::TraverserCallback::HandleError::ignore; + break; + + case PhaseCallback::retry: + errorResponse_ = AFS::TraverserCallback::HandleError::retry; + break; + } + conditionHaveResponse_.notify_all(); //instead of notify_one(); work around bug: https://svn.boost.org/trac/boost/ticket/7796 + } + if (threadsToFinish_ == 0) + { + dummy.unlock(); + onStatusUpdate(getStatusLine(), itemsScanned_); //throw X; one last call for accurate stat-reporting! + return; + } + } + + //call member functions outside of mutex scope: + onStatusUpdate(getStatusLine(), itemsScanned_); //throw X + } + } + + //perf optimization: comparison phase is 7% faster by avoiding needless std::wstring construction for reportCurrentFile() + bool mayReportCurrentFile(int threadIdx, std::chrono::steady_clock::time_point& lastReportTime) const + { + if (threadIdx != notifyingThreadIdx_) //only one thread at a time may report status: the first in sequential order + return false; + + const auto now = std::chrono::steady_clock::now(); + if (now > lastReportTime + cbInterval_) //perform ui updates not more often than necessary + { + lastReportTime = now; //keep "lastReportTime" at worker thread level to avoid locking! + return true; + } + return false; + } + + void reportCurrentFile(const std::wstring& filePath) //context of worker thread + { + assert(!runningOnMainThread()); + std::lock_guard dummy(lockCurrentStatus_); + currentFile_ = filePath; + } + + void incItemsScanned() { ++itemsScanned_; } + //perf: scanning is almost entirely file I/O bound, not CPU bound! => no prob having multiple threads poking at the same variable! + + void notifyTaskBegin(int threadIdx, size_t parallelOps) + { + assert(!zen::runningOnMainThread()); + std::lock_guard dummy(lockCurrentStatus_); + + [[maybe_unused]] const auto [it, inserted] = activeThreadIdxs_.emplace(threadIdx, parallelOps); + assert(inserted); + + notifyingThreadIdx_ = activeThreadIdxs_.begin()->first; + } + + void notifyTaskEnd(int threadIdx) + { + assert(!zen::runningOnMainThread()); + { + std::lock_guard dummy(lockCurrentStatus_); + + [[maybe_unused]] const size_t no = activeThreadIdxs_.erase(threadIdx); + assert(no == 1); + + notifyingThreadIdx_ = activeThreadIdxs_.empty() ? 0 : activeThreadIdxs_.begin()->first; + } + { + std::lock_guard dummy(lockRequest_); + assert(threadsToFinish_ > 0); + if (--threadsToFinish_ == 0) + conditionNewRequest.notify_all(); //perf: should unlock mutex before notify!? (insignificant) + } + } + +private: + std::wstring getStatusLine() //context of main thread, call repreatedly + { + assert(runningOnMainThread()); + + size_t parallelOpsTotal = 0; + std::wstring filePath; + { + std::lock_guard dummy(lockCurrentStatus_); + parallelOpsTotal = activeThreadIdxs_.size(); + filePath = currentFile_; + } + if (parallelOpsTotal >= 2) + return L'[' + _P("1 thread", "%x threads", parallelOpsTotal) + L"] " + filePath; + else + return filePath; + } + + //---- main <-> worker communication channel ---- + std::mutex lockRequest_; + std::condition_variable conditionReadyForNewRequest_; + std::condition_variable conditionNewRequest; + std::condition_variable conditionHaveResponse_; + std::optional errorRequest_; + std::optional errorResponse_; + size_t threadsToFinish_; //can't use activeThreadIdxs_.size() which is locked by different mutex! + //also note: activeThreadIdxs_.size() may be 0 during worker thread construction! + + //---- status updates ---- + std::mutex lockCurrentStatus_; //different lock for status updates so that we're not blocked by other threads reporting errors + std::wstring currentFile_; + std::map activeThreadIdxs_; + + std::atomic notifyingThreadIdx_{0}; //CAVEAT: do NOT use boost::thread::id: https://svn.boost.org/trac/boost/ticket/5754 + const std::chrono::milliseconds cbInterval_; + + //---- status updates II (lock-free) ---- + std::atomic itemsScanned_{0}; //std:atomic is uninitialized by default! +}; + +//------------------------------------------------------------------------------------------------- + +struct TraverserConfig +{ + const AbstractPath baseFolderPath; //thread-safe like an int! :) + const FilterRef filter; + const SymLinkHandling handleSymlinks; + + std::unordered_map& failedDirReads; + std::unordered_map& failedItemReads; + + AsyncCallback& acb; + const int threadIdx; + std::chrono::steady_clock::time_point& lastReportTime; //thread-level +}; + + +class DirCallback : public AFS::TraverserCallback +{ +public: + DirCallback(TraverserConfig& cfg, + Zstring&& parentRelPathPf, //postfixed with FILE_NAME_SEPARATOR (or empty!) + FolderContainer& output, + int level) : + cfg_(cfg), + parentRelPathPf_(std::move(parentRelPathPf)), + output_(output), + level_(level) {} //MUST NOT use cfg_ during construction! see BaseDirCallback() + + virtual void onFile (const AFS::FileInfo& fi) override; // + virtual std::shared_ptr onFolder (const AFS::FolderInfo& fi) override; //throw ThreadStopRequest + virtual HandleLink onSymlink(const AFS::SymlinkInfo& li) override; // + + HandleError reportDirError (const ErrorInfo& errorInfo) override { return reportError(errorInfo, Zstring()); } //throw ThreadStopRequest + HandleError reportItemError(const ErrorInfo& errorInfo, const Zstring& itemName) override { return reportError(errorInfo, itemName); } // + +private: + HandleError reportError(const ErrorInfo& errorInfo, const Zstring& itemName /*optional*/); //throw ThreadStopRequest + + TraverserConfig& cfg_; + const Zstring parentRelPathPf_; + FolderContainer& output_; + const int level_; +}; + + +class BaseDirCallback : public DirCallback +{ +public: + BaseDirCallback(const DirectoryKey& baseFolderKey, DirectoryValue& output, + AsyncCallback& acb, int threadIdx, std::chrono::steady_clock::time_point& lastReportTime) : + DirCallback(travCfg_ /*not yet constructed!!!*/, Zstring(), output.folderCont, 0 /*level*/), + travCfg_ + { + baseFolderKey.folderPath, + baseFolderKey.filter, + baseFolderKey.handleSymlinks, + output.failedFolderReads, + output.failedItemReads, + acb, + threadIdx, + lastReportTime, + } + { + if (acb.mayReportCurrentFile(threadIdx, lastReportTime)) + acb.reportCurrentFile(AFS::getDisplayPath(baseFolderKey.folderPath)); //just in case first directory access is blocking + } + +private: + TraverserConfig travCfg_; +}; + + +void DirCallback::onFile(const AFS::FileInfo& fi) //throw ThreadStopRequest +{ + interruptionPoint(); //throw ThreadStopRequest + + const Zstring& relPath = parentRelPathPf_ + fi.itemName; + + //update status information no matter if item is excluded or not! + if (cfg_.acb.mayReportCurrentFile(cfg_.threadIdx, cfg_.lastReportTime)) + cfg_.acb.reportCurrentFile(AFS::getDisplayPath(AFS::appendRelPath(cfg_.baseFolderPath, relPath))); + + //------------------------------------------------------------------------------------ + //apply filter before processing (use relative name!) + if (!cfg_.filter.ref().passFileFilter(relPath)) + return; + //note: sync.ffs_db database and lock files are excluded via path filter! + + output_.addFile(fi.itemName, + { + .modTime = fi.modTime, + .fileSize = fi.fileSize, + .filePrint = fi.filePrint, + .isFollowedSymlink = fi.isFollowedSymlink, + }); + + cfg_.acb.incItemsScanned(); //add 1 element to the progress indicator +} + + +std::shared_ptr DirCallback::onFolder(const AFS::FolderInfo& fi) //throw ThreadStopRequest +{ + interruptionPoint(); //throw ThreadStopRequest + + Zstring relPath = parentRelPathPf_ + fi.itemName; + + //update status information no matter if item is excluded or not! + if (cfg_.acb.mayReportCurrentFile(cfg_.threadIdx, cfg_.lastReportTime)) + cfg_.acb.reportCurrentFile(AFS::getDisplayPath(AFS::appendRelPath(cfg_.baseFolderPath, relPath))); + + //------------------------------------------------------------------------------------ + //apply filter before processing (use relative name!) + bool childItemMightMatch = true; + const bool passFilter = cfg_.filter.ref().passDirFilter(relPath, &childItemMightMatch); + if (!passFilter && !childItemMightMatch) + return nullptr; //do NOT traverse subdirs + //else: ensure directory filtering is applied later to exclude actually filtered directories!!! + + FolderContainer& subFolder = output_.addFolder(fi.itemName, {.isFollowedSymlink = fi.isFollowedSymlink}); + if (passFilter) + cfg_.acb.incItemsScanned(); //add 1 element to the progress indicator + + //------------------------------------------------------------------------------------ + if (level_ > FOLDER_TRAVERSAL_LEVEL_MAX) //Win32 traverser: stack overflow approximately at level 1000 + //check after FolderContainer::addFolder() + for (size_t retryNumber = 0;; ++retryNumber) + switch (reportItemError({replaceCpy(_("Cannot read directory %x."), L"%x", AFS::getDisplayPath(AFS::appendRelPath(cfg_.baseFolderPath, relPath))) + + L"\n\n" L"Endless recursion.", std::chrono::steady_clock::now(), retryNumber}, fi.itemName)) //throw ThreadStopRequest + { + case AFS::TraverserCallback::HandleError::retry: + break; + case AFS::TraverserCallback::HandleError::ignore: + return nullptr; + } + + return std::make_shared(cfg_, std::move(relPath += FILE_NAME_SEPARATOR), subFolder, level_ + 1); +} + + +DirCallback::HandleLink DirCallback::onSymlink(const AFS::SymlinkInfo& si) //throw ThreadStopRequest +{ + interruptionPoint(); //throw ThreadStopRequest + + const Zstring& relPath = parentRelPathPf_ + si.itemName; + + //update status information no matter if item is excluded or not! + if (cfg_.acb.mayReportCurrentFile(cfg_.threadIdx, cfg_.lastReportTime)) + cfg_.acb.reportCurrentFile(AFS::getDisplayPath(AFS::appendRelPath(cfg_.baseFolderPath, relPath))); + + switch (cfg_.handleSymlinks) + { + case SymLinkHandling::exclude: + return HandleLink::skip; + + case SymLinkHandling::asLink: + if (cfg_.filter.ref().passFileFilter(relPath)) //always use file filter: Link type may not be "stable" on Linux! + { + output_.addSymlink(si.itemName, {.modTime = si.modTime}); + cfg_.acb.incItemsScanned(); //add 1 element to the progress indicator + } + return HandleLink::skip; + + case SymLinkHandling::follow: + //filter symlinks before trying to follow them: handle user-excluded broken symlinks! + //since we don't know yet what type the symlink will resolve to, only do this when both filter variants agree: + if (!cfg_.filter.ref().passFileFilter(relPath)) + { + bool childItemMightMatch = true; + if (!cfg_.filter.ref().passDirFilter(relPath, &childItemMightMatch)) + if (!childItemMightMatch) + return HandleLink::skip; + } + return HandleLink::follow; + } + + assert(false); + return HandleLink::skip; +} + + +DirCallback::HandleError DirCallback::reportError(const ErrorInfo& errorInfo, const Zstring& itemName /*optional*/) //throw ThreadStopRequest +{ + const HandleError handleErr = cfg_.acb.reportError(errorInfo); //throw ThreadStopRequest + switch (handleErr) + { + case HandleError::ignore: + if (itemName.empty()) + cfg_.failedDirReads.emplace(beforeLast(parentRelPathPf_, FILE_NAME_SEPARATOR, IfNotFoundReturn::none), utfTo(errorInfo.msg)); + else + cfg_.failedItemReads.emplace(parentRelPathPf_ + itemName, utfTo(errorInfo.msg)); + break; + + case HandleError::retry: + break; + } + return handleErr; +} +} + + +std::map fff::parallelFolderScan(const std::set& foldersToRead, + const TravErrorCb& onError, const TravStatusCb& onStatusUpdate, + std::chrono::milliseconds cbInterval) +{ + std::map output; + + //aggregate folder paths that are on the same root device: + // => one worker thread *per device*: avoid excessive parallelism + // => parallel folder traversal considers "parallel file operations" as specified by user + // => (S)FTP: avoid hitting connection limits inadvertently + std::map> perDeviceFolders; + + for (const DirectoryKey& key : foldersToRead) + perDeviceFolders[key.folderPath.afsDevice].insert(key); + + //communication channel used by threads + AsyncCallback acb(perDeviceFolders.size() /*threadsToFinish*/, cbInterval); //manage life time: enclose InterruptibleThread's!!! + + std::vector worker; + ZEN_ON_SCOPE_SUCCESS( for (InterruptibleThread& wt : worker) wt.join(); ); //no stop needed in success case => preempt ~InterruptibleThread() + ZEN_ON_SCOPE_FAIL( for (InterruptibleThread& wt : worker) wt.requestStop(); ); //stop *all* at the same time before join! + + //init worker threads + for (const auto& [afsDevice, dirKeys] : perDeviceFolders) + { + const int threadIdx = static_cast(worker.size()); + Zstring threadName = Zstr("Compare[") + numberTo(threadIdx + 1) + Zstr('/') + numberTo(perDeviceFolders.size()) + Zstr("] ") + + utfTo(AFS::getDisplayPath({afsDevice, AfsPath()})); + + const size_t parallelOps = 1; + std::map workload; + + for (const DirectoryKey& key : dirKeys) + workload.emplace(key, &output[key]); //=> DirectoryValue* unshared for lock-free worker-thread access + + worker.emplace_back([afsDevice /*clang bug*/= afsDevice, workload, threadIdx, &acb, parallelOps, threadName = std::move(threadName)]() mutable + { + setCurrentThreadName(threadName); + + acb.notifyTaskBegin(threadIdx, parallelOps); + ZEN_ON_SCOPE_EXIT(acb.notifyTaskEnd(threadIdx)); + + std::chrono::steady_clock::time_point lastReportTime; //keep thread-local! + + AFS::TraverserWorkload travWorkload; + + for (auto& [folderKey, folderVal] : workload) + { + assert(folderKey.folderPath.afsDevice == afsDevice); + travWorkload.emplace_back(folderKey.folderPath.afsPath, std::make_shared(folderKey, *folderVal, acb, threadIdx, lastReportTime)); + } + AFS::traverseFolderRecursive(afsDevice, travWorkload, parallelOps); //throw ThreadStopRequest + }); + } + acb.waitUntilDone(onError, onStatusUpdate); //throw X + + return output; +} diff --git a/FreeFileSync/Source/base/parallel_scan.h b/FreeFileSync/Source/base/parallel_scan.h new file mode 100644 index 0000000..8148998 --- /dev/null +++ b/FreeFileSync/Source/base/parallel_scan.h @@ -0,0 +1,54 @@ +// ***************************************************************************** +// * 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 PARALLEL_SCAN_H_924588904275284572857 +#define PARALLEL_SCAN_H_924588904275284572857 + +#include +#include +#include +#include "path_filter.h" +#include "structures.h" +#include "file_hierarchy.h" +#include "process_callback.h" + + +namespace fff +{ +struct DirectoryKey +{ + AbstractPath folderPath; + FilterRef filter; + SymLinkHandling handleSymlinks = SymLinkHandling::exclude; + + std::weak_ordering operator<=>(const DirectoryKey&) const = default; +}; + + +struct DirectoryValue +{ + FolderContainer folderCont; + + //relative paths (or empty string for root) for directories that could not be read (completely), e.g. access denied, or temporary network drop + std::unordered_map failedFolderReads; + + //relative paths (never empty) for failure to read single file/dir/symlink + std::unordered_map failedItemReads; +}; + + +//Attention: 1. ensure directory filtering is applied later to exclude filtered folders which have been kept as parent folders +// 2. remove folder aliases (e.g. case differences) *before* calling this function!!! + +using TravErrorCb = std::function; +using TravStatusCb = std::function; + +std::map parallelFolderScan(const std::set& foldersToRead, + const TravErrorCb& onError, const TravStatusCb& onStatusUpdate, //NOT optional + std::chrono::milliseconds cbInterval); +} + +#endif //PARALLEL_SCAN_H_924588904275284572857 diff --git a/FreeFileSync/Source/base/path_filter.cpp b/FreeFileSync/Source/base/path_filter.cpp new file mode 100644 index 0000000..16a206e --- /dev/null +++ b/FreeFileSync/Source/base/path_filter.cpp @@ -0,0 +1,318 @@ +// ***************************************************************************** +// * 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 "path_filter.h" +#include +#include + +using namespace zen; +using namespace fff; + + +std::strong_ordering fff::operator<=>(const FilterRef& lhs, const FilterRef& rhs) +{ + //caveat: typeid returns static type for pointers, dynamic type for references!!! + if (const std::strong_ordering cmp = std::type_index(typeid(lhs.ref())) <=> + std::type_index(typeid(rhs.ref())); + cmp != std::strong_ordering::equal) + return cmp; + + return lhs.ref().compareSameType(rhs.ref()); +} + + +void NameFilter::parseFilterPhrase(const Zstring& filterPhrase, FilterSet& filter) +{ + //normalize filter: 1. ignore Unicode normalization form 2. ignore case + Zstring filterPhraseNorm = getUpperCase(filterPhrase); + //3. fix path separator + if constexpr (FILE_NAME_SEPARATOR != Zstr('/' )) replace(filterPhraseNorm, Zstr('/'), FILE_NAME_SEPARATOR); + if constexpr (FILE_NAME_SEPARATOR != Zstr('\\')) replace(filterPhraseNorm, Zstr('\\'), FILE_NAME_SEPARATOR); + + static_assert(FILE_NAME_SEPARATOR == '/'); + const Zstring sepAsterisk = Zstr("/*"); + const Zstring asteriskSep = Zstr("*/"); + + auto processTail = [&](const ZstringView phrase) + { + if (endsWith(phrase, Zstr(':'))) //file-only tag + filter.fileMasks.insert({phrase.begin(), phrase.end() - 1}); + else if (endsWith(phrase, FILE_NAME_SEPARATOR) || //folder-only tag + endsWith(phrase, sepAsterisk)) // abc\* + filter.folderMasks.insert(Zstring(beforeLast(phrase, FILE_NAME_SEPARATOR, IfNotFoundReturn::none))); + else + { + filter.fileMasks .insert(Zstring(phrase)); + filter.folderMasks.insert(Zstring(phrase)); + } + }; + + split2(filterPhraseNorm, [](Zchar c) { return c == FILTER_ITEM_SEPARATOR || c == Zstr('\n'); }, //delimiters + [&](ZstringView itemPhrase) + { + itemPhrase = trimCpy(itemPhrase); + if (!itemPhrase.empty()) + { + /* phrase | action + +---------+-------- + | \blah | remove \ + | \*blah | remove \ + | \*\blah | remove \ + | \*\* | remove \ + +---------+-------- + | *blah | + | *\blah | -> add blah + | *\*blah | -> add *blah + +---------+-------- + | blah: | remove : (file only) + | blah\*: | remove : (file only) + +---------+-------- + | blah\ | remove \ (folder only) + | blah*\ | remove \ (folder only) + | blah\*\ | remove \ (folder only) + +---------+-------- + | blah* | + | blah\* | remove \* (folder only) + | blah*\* | remove \* (folder only) + +---------+-------- */ + if (startsWith(itemPhrase, FILE_NAME_SEPARATOR)) // \abc + processTail(afterFirst(itemPhrase, FILE_NAME_SEPARATOR, IfNotFoundReturn::none)); + else + { + processTail(itemPhrase); + if (startsWith(itemPhrase, asteriskSep)) // *\abc + processTail(afterFirst(itemPhrase, asteriskSep, IfNotFoundReturn::none)); + } + } + }); +} + + +void NameFilter::MaskMatcher::insert(const Zstring& mask) +{ + assert(mask == getUpperCase(mask)); + if (mask.empty()) + return; + + if (contains(mask, Zstr('?')) || + contains(mask, Zstr('*'))) + realMasks_.insert(mask); + else + { + relPaths_ .insert(mask); + relPathsCmp_.insert(mask); //little memory wasted thanks to COW string! + } +} + + +namespace +{ +//"true" if path or any parent path matches the mask +bool matchesMask(const Zchar* path, const Zchar* const pathEnd, const Zchar* mask /*0-terminated*/) +{ + for (;; ++mask, ++path) + { + Zchar m = *mask; + switch (m) + { + case 0: + return path == pathEnd || *path == FILE_NAME_SEPARATOR; //"full" or parent path match + + case Zstr('?'): //should not match FILE_NAME_SEPARATOR + if (path == pathEnd || *path == FILE_NAME_SEPARATOR) + return false; + break; + + case Zstr('*'): + do //advance mask to next non-* char + { + m = *++mask; + } + while (m == Zstr('*')); + + if (m == 0) //mask ends with '*': + return true; + + ++mask; + if (m == Zstr('?')) //*? pattern + { + while (path != pathEnd) + if (*path++ != FILE_NAME_SEPARATOR) + if (matchesMask(path, pathEnd, mask)) + return true; + } + else //*[letter or /] pattern + while (path != pathEnd) + if (*path++ == m) + if (matchesMask(path, pathEnd, mask)) + return true; + return false; + + default: + if (path == pathEnd || *path != m) + return false; + } + } +} + + +//"true" if path matches (only!) the beginning of mask +template bool matchesMaskBegin(const ZstringView relPath, const Zstring& mask); + +template <> inline +bool matchesMaskBegin(const ZstringView relPath, const Zstring& mask) +{ + auto itP = relPath.begin(); + for (auto itM = mask.begin(); itM != mask.end(); ++itM, ++itP) + { + const Zchar m = *itM; + switch (m) + { + case Zstr('?'): + if (itP == relPath.end() || *itP == FILE_NAME_SEPARATOR) + return false; + break; + + case Zstr('*'): + return true; + + default: + if (itP == relPath.end()) + return m == FILE_NAME_SEPARATOR && mask.end() - itM > 1; //require strict sub match + + if (*itP != m) + return false; + } + } + return false; //not a strict sub match +} + +template <> inline //perf: going overboard? remaining fruits are hanging higher and higher... +bool matchesMaskBegin(const ZstringView relPath, const Zstring& mask) +{ + return mask.size() > relPath.size() + 1 && //room for FILE_NAME_SEPARATOR *and* at least one more char + mask[relPath.size()] == FILE_NAME_SEPARATOR && + startsWith(mask, relPath); +} +} + + +bool NameFilter::MaskMatcher::matches(const ZstringView relPath) const +{ + assert(!relPath.empty()); + + if (std::any_of(realMasks_.begin(), realMasks_.end(), [&](const Zstring& mask) { return matchesMask(relPath.data(), relPath.data() + relPath.size(), mask.c_str()); })) + /**/return true; + + //perf: for relPaths_ we can go from linear to *constant* time!!! => annihilates https://freefilesync.org/forum/viewtopic.php?t=7768#p26519 + + ZstringView parentPath = relPath; + for (;;) //check all parent paths! + { + if (relPaths_.contains(parentPath)) //heterogenous lookup! + return true; + + parentPath = beforeLast(parentPath, FILE_NAME_SEPARATOR, IfNotFoundReturn::none); + if (parentPath.empty()) + return false; + } +} + + +bool NameFilter::MaskMatcher::matchesBegin(const ZstringView relPath) const +{ + return std::any_of(realMasks_.begin(), realMasks_.end(), [&](const Zstring& mask) { return matchesMaskBegin(relPath, mask); }) || + /**/ std::any_of(relPaths_ .begin(), relPaths_ .end(), [&](const Zstring& mask) { return matchesMaskBegin(relPath, mask); }); +} + +//################################################################################################# + +NameFilter::NameFilter(const Zstring& includePhrase, const Zstring& excludePhrase) +{ + parseFilterPhrase(includePhrase, includeFilter); + parseFilterPhrase(excludePhrase, excludeFilter); +} + + +void NameFilter::addExclusion(const Zstring& excludePhrase) +{ + parseFilterPhrase(excludePhrase, excludeFilter); +} + + +bool NameFilter::passFileFilter(const Zstring& relFilePath) const +{ + assert(!startsWith(relFilePath, FILE_NAME_SEPARATOR)); + + //normalize input: 1. ignore Unicode normalization form 2. ignore case + const Zstring& pathFmt = getUpperCase(relFilePath); + + const ZstringView parentPath = beforeLast(pathFmt, FILE_NAME_SEPARATOR, IfNotFoundReturn::none); + + if (excludeFilter.fileMasks.matches(pathFmt) || //either match on file or any parent folder + (!parentPath.empty() && excludeFilter.folderMasks.matches(parentPath))) //match on any parent folder only + return false; + + return includeFilter.fileMasks.matches(pathFmt) || + (!parentPath.empty() && includeFilter.folderMasks.matches(parentPath)); +} + + +bool NameFilter::passDirFilter(const Zstring& relDirPath, bool* childItemMightMatch) const +{ + assert(!startsWith(relDirPath, FILE_NAME_SEPARATOR)); + assert(!childItemMightMatch || *childItemMightMatch); //check correct usage + + //normalize input: 1. ignore Unicode normalization form 2. ignore case + const Zstring& pathFmt = getUpperCase(relDirPath); + + if (excludeFilter.folderMasks.matches(pathFmt)) + { + if (childItemMightMatch) + *childItemMightMatch = false; //perf: no need to traverse deeper; subfolders/subfiles would be excluded by filter anyway! + + /* Attention: If *childItemMightMatch == false, then any direct filter evaluation for a child item must also return "false"! + + This is not a problem for folder traversal which stops at the first *childItemMightMatch == false anyway, but other code continues recursing further, + e.g. the database update code in db_file.cpp recurses unconditionally without *childItemMightMatch check! */ + return false; + } + + if (includeFilter.folderMasks.matches(pathFmt)) + return true; + + if (childItemMightMatch) + *childItemMightMatch = includeFilter.fileMasks .matchesBegin(pathFmt) || //might match a file or folder in subdirectory + includeFilter.folderMasks.matchesBegin(pathFmt); // + return false; +} + + +bool NameFilter::isNull(const Zstring& includePhrase, const Zstring& excludePhrase) +{ + return trimCpy(includePhrase) == Zstr("*") && //harmonize with ui/folder_pair.cpp tooltip + trimCpy(excludePhrase).empty(); + //return NameFilter(includePhrase, excludePhrase).isNull(); -> very expensive for huge lists +} + + +bool NameFilter::isNull() const +{ + return compareSameType(NameFilter(Zstr("*"), Zstr(""))) == std::strong_ordering::equal; + //avoid static non-POD null-NameFilter instance +} + + +std::strong_ordering NameFilter::compareSameType(const PathFilter& other) const +{ + assert(typeid(*this) == typeid(other)); //always given in this context! + + const NameFilter& lhs = *this; + const NameFilter& rhs = static_cast(other); + + return std::tie(lhs.includeFilter, lhs.excludeFilter) <=> + std::tie(rhs.includeFilter, rhs.excludeFilter); +} diff --git a/FreeFileSync/Source/base/path_filter.h b/FreeFileSync/Source/base/path_filter.h new file mode 100644 index 0000000..0ec02e7 --- /dev/null +++ b/FreeFileSync/Source/base/path_filter.h @@ -0,0 +1,258 @@ +// ***************************************************************************** +// * 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 HARD_FILTER_H_825780275842758345 +#define HARD_FILTER_H_825780275842758345 + +#include +#include + + +namespace fff +{ +/* Semantics of PathFilter: + 1. using it creates a NEW folder hierarchy! -> must be considered by variant! + 2. it applies equally to both sides => it always matches either both sides or none! => can be used while traversing a single folder! + + PathFilter (interface) + /|\ + ____________|_____________ + | | | + NullFilter NameFilter CombinedFilter */ + +class PathFilter; +using FilterRef = zen::SharedRef; + +std::strong_ordering operator<=>(const FilterRef& lhs, const FilterRef& rhs); //fix GCC warning: "... has not been declared within ?fff + +const Zchar FILTER_ITEM_SEPARATOR = Zstr('|'); + +class PathFilter +{ +public: + virtual ~PathFilter() {} + + virtual bool passFileFilter(const Zstring& relFilePath) const = 0; + virtual bool passDirFilter (const Zstring& relDirPath, bool* childItemMightMatch) const = 0; + //childItemMightMatch: file/dir in subdirectories could(!) match + //note: this hint is only set if passDirFilter returns false! + + virtual bool isNull() const = 0; //filter is equivalent to NullFilter + + virtual FilterRef copyFilterAddingExclusion(const Zstring& excludePhrase) const = 0; + +private: + friend std::strong_ordering operator<=>(const FilterRef& lhs, const FilterRef& rhs); + + virtual std::strong_ordering compareSameType(const PathFilter& other) const = 0; //assumes typeid(*this) == typeid(other)! +}; + + +//small helper method: merge two hard filters (thereby remove Null-filters) +FilterRef combineFilters(const FilterRef& first, const FilterRef& second); + + +class NullFilter : public PathFilter //no filtering at all +{ +public: + bool passFileFilter(const Zstring& relFilePath) const override { return true; } + bool passDirFilter(const Zstring& relDirPath, bool* childItemMightMatch) const override; + bool isNull() const override { return true; } + FilterRef copyFilterAddingExclusion(const Zstring& excludePhrase) const override; + +private: + std::strong_ordering compareSameType(const PathFilter& other) const override { assert(typeid(*this) == typeid(other)); return std::strong_ordering::equal; } +}; + + +class NameFilter : public PathFilter //filter by base-relative file path +{ +public: + NameFilter(const Zstring& includePhrase, const Zstring& excludePhrase); + + void addExclusion(const Zstring& excludePhrase); + + bool passFileFilter(const Zstring& relFilePath) const override; + bool passDirFilter(const Zstring& relDirPath, bool* childItemMightMatch) const override; + + bool isNull() const override; + static bool isNull(const Zstring& includePhrase, const Zstring& excludePhrase); //*fast* check without expensive NameFilter construction! + FilterRef copyFilterAddingExclusion(const Zstring& excludePhrase) const override; + +private: + friend class CombinedFilter; + std::strong_ordering compareSameType(const PathFilter& other) const override; + + class MaskMatcher + { + public: + void insert(const Zstring& mask); //expected: upper-case + Unicode-normalized! + bool matches(const ZstringView relPath) const; + bool matchesBegin(const ZstringView relPath) const; + + inline friend std::strong_ordering operator<=>(const MaskMatcher& lhs, const MaskMatcher& rhs) + { + return std::tie(lhs.realMasks_, lhs.relPathsCmp_) <=> + std::tie(rhs.realMasks_, rhs.relPathsCmp_); + } + //can't "= default" because std::unordered_set doesn't support <=>! + //CAVEAT: when operator<=> is not "default" we also don't get operator== for free! declare manually: + bool operator==(const MaskMatcher&) const; + //why declare, but not define? if undeclared, "std::tie <=> std::tie" incorrectly deduces std::weak_ordering + //=> bug? no, looks like "C++ standard nonsense": https://cplusplus.github.io/LWG/issue3431 + //std::three_way_comparable requires __WeaklyEqualityComparableWith!! this is stupid on first sight. And on second. And on third. + + private: + std::set realMasks_; //always containing ? or * (use std::set<> to scrap duplicates!) + std::unordered_set relPaths_; //never containing ? or * + std::set relPathsCmp_; //req. for operator<=> only :( + }; + + struct FilterSet + { + MaskMatcher fileMasks; + MaskMatcher folderMasks; + + std::strong_ordering operator<=>(const FilterSet&) const = default; + }; + + static void parseFilterPhrase(const Zstring& filterPhrase, FilterSet& filter); + + FilterSet includeFilter; + FilterSet excludeFilter; +}; + + +class CombinedFilter : public PathFilter //combine two filters to match if and only if both match +{ +public: + CombinedFilter(const NameFilter& first, const NameFilter& second) : first_(first), second_(second) { assert(!first.isNull() && !second.isNull()); } //if either is null, then wy use CombinedFilter? + + bool passFileFilter(const Zstring& relFilePath) const override; + bool passDirFilter(const Zstring& relDirPath, bool* childItemMightMatch) const override; + bool isNull() const override; + FilterRef copyFilterAddingExclusion(const Zstring& excludePhrase) const override; + +private: + std::strong_ordering compareSameType(const PathFilter& other) const override; + + const NameFilter first_; + const NameFilter second_; +}; + + + + + + +//--------------- inline implementation --------------------------------------- +inline +bool NullFilter::passDirFilter(const Zstring& relDirPath, bool* childItemMightMatch) const +{ + assert(!childItemMightMatch || *childItemMightMatch); //check correct usage + return true; +} + + +inline +FilterRef NullFilter::copyFilterAddingExclusion(const Zstring& excludePhrase) const +{ + auto filter = zen::makeSharedRef(Zstr("*"), excludePhrase); + if (filter.ref().isNull()) + return zen::makeSharedRef(); + return filter; +} + + +inline +FilterRef NameFilter::copyFilterAddingExclusion(const Zstring& excludePhrase) const +{ + auto tmp = zen::makeSharedRef(*this); + tmp.ref().addExclusion(excludePhrase); + return tmp; +} + + +inline +bool CombinedFilter::passFileFilter(const Zstring& relFilePath) const +{ + return first_ .passFileFilter(relFilePath) && //short-circuit behavior + second_.passFileFilter(relFilePath); +} + + +inline +bool CombinedFilter::passDirFilter(const Zstring& relDirPath, bool* childItemMightMatch) const +{ + if (first_.passDirFilter(relDirPath, childItemMightMatch)) + return second_.passDirFilter(relDirPath, childItemMightMatch); + else + { + if (childItemMightMatch && *childItemMightMatch) + second_.passDirFilter(relDirPath, childItemMightMatch); + return false; + } +} + + +inline +bool CombinedFilter::isNull() const +{ + return first_.isNull() && second_.isNull(); +} + + +inline +FilterRef CombinedFilter::copyFilterAddingExclusion(const Zstring& excludePhrase) const +{ + NameFilter tmp(first_); + tmp.addExclusion(excludePhrase); + + return zen::makeSharedRef(tmp, second_); +} + + +inline +std::strong_ordering CombinedFilter::compareSameType(const PathFilter& other) const +{ + assert(typeid(*this) == typeid(other)); //always given in this context! + + const CombinedFilter& lhs = *this; + const CombinedFilter& rhs = static_cast(other); + + if (const std::strong_ordering cmp = lhs.first_.compareSameType(rhs.first_); + cmp != std::strong_ordering::equal) + return cmp; + + return lhs.second_.compareSameType(rhs.second_); +} + + +inline +FilterRef constructFilter(const Zstring& includePhrase, + const Zstring& excludePhrase, + const Zstring& includePhrase2, + const Zstring& excludePhrase2) +{ + if (NameFilter::isNull(includePhrase, Zstring())) + { + auto filterTmp = zen::makeSharedRef(includePhrase2, excludePhrase + Zstr('\n') + excludePhrase2); + if (filterTmp.ref().isNull()) + return zen::makeSharedRef(); + + return filterTmp; + } + else + { + if (NameFilter::isNull(includePhrase2, Zstring())) + return zen::makeSharedRef(includePhrase, excludePhrase + Zstr('\n') + excludePhrase2); + else + return zen::makeSharedRef(NameFilter(includePhrase, excludePhrase + Zstr('\n') + excludePhrase2), NameFilter(includePhrase2, Zstring())); + } +} +} + +#endif //HARD_FILTER_H_825780275842758345 diff --git a/FreeFileSync/Source/base/process_callback.h b/FreeFileSync/Source/base/process_callback.h new file mode 100644 index 0000000..8803c04 --- /dev/null +++ b/FreeFileSync/Source/base/process_callback.h @@ -0,0 +1,91 @@ +// ***************************************************************************** +// * 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 PROCESS_CALLBACK_H_48257827842345454545 +#define PROCESS_CALLBACK_H_48257827842345454545 + +#include +#include +#include + + +namespace fff +{ +struct PhaseCallback +{ + virtual ~PhaseCallback() {} + + //note: this one must NOT throw in order to properly allow undoing setting of statistics! + //it is in general paired with a call to requestUiUpdate() to compensate! + virtual void updateDataProcessed(int itemsDelta, int64_t bytesDelta) = 0; //noexcept! + virtual void updateDataTotal (int itemsDelta, int64_t bytesDelta) = 0; // + /* the estimated and actual total workload may change *during* sync: + 1. file cannot be moved -> fallback to copy + delete + 2. file copy, actual size changed after comparison + 3. file contains significant ADS data, is sparse or compressed + 4. file/directory already deleted externally: nothing to do, 0 logical operations and data + 5. auto-resolution for failed create operations due to missing source + 6. directory deletion: may contain more items than scanned by FFS (excluded by filter) or less (contains followed symlinks) + 7. delete directory to recycler: no matter how many child-elements exist, this is only 1 item to process! + 8. user-defined deletion directory on different volume: full file copy required (instead of move) + 9. Binary file comparison: short-circuit behavior after first difference is found + 10. Error during file copy, retry: bytes were copied => increases total workload! */ + + //opportunity to abort must be implemented in a frequently-executed method like requestUiUpdate() + virtual void requestUiUpdate(bool force = false) = 0; //throw X + + //UI info only, should *not* be logged: called periodically after data was processed: expected(!) to request GUI update + virtual void updateStatus(std::wstring&& msg) = 0; //throw X + + enum class MsgType + { + info, + warning, + error, + }; + //log only; must *not* call updateStatus()! + virtual void logMessage(const std::wstring& msg, MsgType type) = 0; //throw X + + virtual void reportWarning(const std::wstring& msg, bool& warningActive) = 0; //throw X + + struct ErrorInfo + { + std::wstring msg; + std::chrono::steady_clock::time_point failTime; + size_t retryNumber = 0; + }; + enum Response + { + ignore, + retry + }; + virtual Response reportError(const ErrorInfo& errorInfo) = 0; //throw X; recoverable error + + virtual void reportFatalError(const std::wstring& msg) = 0; //throw X; non-recoverable error +}; + +//perform ui updates not more often than necessary: +constexpr std::chrono::milliseconds UI_UPDATE_INTERVAL(50); //20 FPS +//- Win 7 copy progress bar uses 100 ms +//- Windows 10: not seeing CPU impact in Process Explorer when going as low as 2ms => too good to be true? + +enum class ProcessPhase +{ + none, //initial status + scan, + binaryCompare, + sync +}; + +//interface for comparison and synchronization process status updates (used by GUI and Batch mode) +struct ProcessCallback : public PhaseCallback +{ + //informs about the estimated amount of data that will be processed in the next synchronization phase + virtual void initNewPhase(int itemsTotal, int64_t bytesTotal, ProcessPhase phaseId) = 0; //throw X +}; +} + +#endif //PROCESS_CALLBACK_H_48257827842345454545 diff --git a/FreeFileSync/Source/base/soft_filter.h b/FreeFileSync/Source/base/soft_filter.h new file mode 100644 index 0000000..170e355 --- /dev/null +++ b/FreeFileSync/Source/base/soft_filter.h @@ -0,0 +1,111 @@ +// ***************************************************************************** +// * 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 SOFT_FILTER_H_810457108534657 +#define SOFT_FILTER_H_810457108534657 + +#include +#include "structures.h" + + +namespace fff +{ +/* +Semantics of SoftFilter: +1. It potentially may match only one side => it MUST NOT be applied while traversing a single folder to avoid mismatches +2. => it is applied after traversing and just marks rows, (NO deletions after comparison are allowed) +3. => equivalent to a user temporarily (de-)selecting rows => not relevant for -mode! +*/ + +class SoftFilter +{ +public: + SoftFilter(size_t timeSpan, UnitTime unitTimeSpan, + uint64_t sizeMin, UnitSize unitSizeMin, + uint64_t sizeMax, UnitSize unitSizeMax); + + bool matchTime(time_t writeTime) const { return timeFrom_ <= writeTime; } + bool matchSize(uint64_t fileSize) const { return sizeMin_ <= fileSize && fileSize <= sizeMax_; } + bool matchFolder() const { return matchesFolder_; } + bool isNull() const; //filter is equivalent to NullFilter, but may be technically slower + + //small helper method: merge two soft filters + friend SoftFilter combineFilters(const SoftFilter& first, const SoftFilter& second); + +private: + SoftFilter(time_t timeFrom, + uint64_t sizeMin, + uint64_t sizeMax, + bool matchesFolder); + + time_t timeFrom_ = 0; //unit: UTC, seconds + uint64_t sizeMin_ = 0; //unit: bytes + uint64_t sizeMax_ = 0; //unit: bytes + const bool matchesFolder_; +}; + + + + + + + + + + + + + + +// ----------------------- implementation ----------------------- +inline +SoftFilter::SoftFilter(size_t timeSpan, UnitTime unitTimeSpan, + uint64_t sizeMin, UnitSize unitSizeMin, + uint64_t sizeMax, UnitSize unitSizeMax) : + matchesFolder_(unitTimeSpan == UnitTime::none && + unitSizeMin == UnitSize::none && + unitSizeMax == UnitSize::none) //exclude folders if size or date filter is active: avoids creating empty folders if not needed! +{ + resolveUnits(timeSpan, unitTimeSpan, + sizeMin, unitSizeMin, + sizeMax, unitSizeMax, + timeFrom_, + sizeMin_, + sizeMax_); +} + +inline +SoftFilter::SoftFilter(time_t timeFrom, + uint64_t sizeMin, + uint64_t sizeMax, + bool matchesFolder) : + timeFrom_(timeFrom), + sizeMin_ (sizeMin), + sizeMax_ (sizeMax), + matchesFolder_(matchesFolder) {} + + +inline +SoftFilter combineFilters(const SoftFilter& lhs, const SoftFilter& rhs) +{ + return SoftFilter(std::max(lhs.timeFrom_, rhs.timeFrom_), + std::max(lhs.sizeMin_, rhs.sizeMin_), + std::min(lhs.sizeMax_, rhs.sizeMax_), + lhs.matchesFolder_ && rhs.matchesFolder_); +} + + +inline +bool SoftFilter::isNull() const //filter is equivalent to NullFilter, but may be technically slower +{ + return timeFrom_ == std::numeric_limits::min() && + sizeMin_ == 0U && + sizeMax_ == std::numeric_limits::max() && + matchesFolder_; +} +} + +#endif //SOFT_FILTER_H_810457108534657 diff --git a/FreeFileSync/Source/base/speed_test.cpp b/FreeFileSync/Source/base/speed_test.cpp new file mode 100644 index 0000000..3480208 --- /dev/null +++ b/FreeFileSync/Source/base/speed_test.cpp @@ -0,0 +1,226 @@ +// ***************************************************************************** +// * 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 "speed_test.h" +#include +#include +#include + +using namespace zen; +using namespace fff; + + +void SpeedTest::addSample(std::chrono::nanoseconds timeElapsed, int itemsCurrent, int64_t bytesCurrent) +{ + //time expected to be monotonously ascending + assert(samples_.empty() || samples_.back().timeElapsed <= timeElapsed); + + samples_.push_back(Sample{timeElapsed, itemsCurrent, bytesCurrent}); + + //remove old records outside of "window" + std::optional lastPop; + while (!samples_.empty() && samples_.front().timeElapsed <= timeElapsed - windowSize_) + { + lastPop = samples_.front(); + samples_.pop_front(); + } + if (lastPop) //keep one point before new start to handle gaps + samples_.push_front(*lastPop); +} + + +std::optional SpeedTest::getRemainingSec(int /*itemsRemaining*/, int64_t bytesRemaining) const +{ + if (!samples_.empty()) + { + const double timeDelta = std::chrono::duration(samples_.back().timeElapsed - samples_.front().timeElapsed).count(); + const int64_t bytesDelta = samples_.back().bytes - samples_.front().bytes; + + //"items" counts logical operations *NOT* disk accesses, so we better play safe and use "bytes" only! + + if (bytesDelta != 0) //sign(dataRemaining) != sign(bytesDelta) usually an error, so show it! + return bytesRemaining * timeDelta / bytesDelta; + } + return std::nullopt; +} + + +std::optional SpeedTest::getBytesPerSec() const +{ + if (!samples_.empty()) + { + const double timeDelta = std::chrono::duration(samples_.back().timeElapsed - samples_.front().timeElapsed).count(); + const int64_t bytesDelta = samples_.back().bytes - samples_.front().bytes; + + if (!numeric::isNull(timeDelta)) + return bytesDelta / timeDelta; + } + return std::nullopt; +} + + +std::optional SpeedTest::getItemsPerSec() const +{ + if (!samples_.empty()) + { + const double timeDelta = std::chrono::duration(samples_.back().timeElapsed - samples_.front().timeElapsed).count(); + const int itemsDelta = samples_.back().items - samples_.front().items; + + if (!numeric::isNull(timeDelta)) + return itemsDelta / timeDelta; + } + return std::nullopt; +} + + +std::wstring SpeedTest::getBytesPerSecFmt() const +{ + if (const std::optional bps = getBytesPerSec()) + return replaceCpy(_("%x/sec"), L"%x", formatFilesizeShort(std::llround(*bps))); + return {}; +} + + +std::wstring SpeedTest::getItemsPerSecFmt() const +{ + if (const std::optional ips = getItemsPerSec()) + return replaceCpy(_("%x/sec"), L"%x", replaceCpy(_("%x items"), L"%x", formatTwoDigitPrecision(*ips))); + return {}; +} + + +/* +class for calculation of remaining time: +---------------------------------------- +"filesize |-> time" is an affine linear function f(x) = z_1 + z_2 x + +For given n measurements, sizes x_0, ..., x_n and times f_0, ..., f_n, the function f (as a polynom of degree 1) can be lineary approximated by + +z_1 = (r - s * q / p) / ((n + 1) - s * s / p) +z_2 = (q - s * z_1) / p = (r - (n + 1) z_1) / s + +with +p := x_0^2 + ... + x_n^2 +q := f_0 x_0 + ... + f_n x_n +r := f_0 + ... + f_n +s := x_0 + ... + x_n + +=> the time to process N files with amount of data D is: N * z_1 + D * z_2 + +Problem: +-------- +Times f_0, ..., f_n can be very small so that precision of the PC clock is poor. +=> Times have to be accumulated to enhance precision: +Copying of m files with sizes x_i and times f_i (i = 1, ..., m) takes sum_i f(x_i) := m * z_1 + z_2 * sum x_i = sum f_i +With X defined as the accumulated sizes and F the accumulated times this gives: (in theory...) +m * z_1 + z_2 * X = F <=> +z_1 + z_2 * X / m = F / m + +=> we obtain a new (artificial) measurement with size X / m and time F / m to be used in the linear approximation above + + +Statistics::Statistics(int totalObjectCount, double totalDataAmount, unsigned recordCount) : + itemsTotal(totalObjectCount), + bytesTotal(totalDataAmount), + recordsMax(recordCount), + objectsLast(0), + dataLast(0), + timeLast(wxGetLocalTimeMillis()), + z1_current(0), + z2_current(0), + dummyRecordPresent(false) {} + + +wxString Statistics::getRemainingTime(int objectsCurrent, double dataCurrent) +{ + //add new measurement point + const int m = objectsCurrent - objectsLast; + if (m != 0) + { + objectsLast = objectsCurrent; + + const double X = dataCurrent - dataLast; + dataLast = dataCurrent; + + const int64_t timeCurrent = wxGetLocalTimeMillis(); + const double F = (timeCurrent - timeLast).ToDouble(); + timeLast = timeCurrent; + + record newEntry; + newEntry.x_i = X / m; + newEntry.f_i = F / m; + + //remove dummy record + if (dummyRecordPresent) + { + measurements.pop_back(); + dummyRecordPresent = false; + } + + //insert new record + measurements.push_back(newEntry); + if (measurements.size() > recordsMax) + measurements.pop_front(); + } + else //dataCurrent increased without processing new objects: + { //modify last measurement until m != 0 + const double X = dataCurrent - dataLast; //do not set dataLast, timeLast variables here, but write dummy record instead + if (!isNull(X)) + { + const int64_t timeCurrent = wxGetLocalTimeMillis(); + const double F = (timeCurrent - timeLast).ToDouble(); + + record modifyEntry; + modifyEntry.x_i = X; + modifyEntry.f_i = F; + + //insert dummy record + if (!dummyRecordPresent) + { + measurements.push_back(modifyEntry); + if (measurements.size() > recordsMax) + measurements.pop_front(); + dummyRecordPresent = true; + } + else //modify dummy record + measurements.back() = modifyEntry; + } + } + + //calculate remaining time based on stored measurement points + double p = 0; + double q = 0; + double r = 0; + double s = 0; + for (const record& rec : measurements) + { + const double x_i = rec.x_i; + const double f_i = rec.f_i; + p += x_i * x_i; + q += f_i * x_i; + r += f_i; + s += x_i; + } + + if (!isNull(p)) + { + const double n = measurements.size(); + const double tmp = (n - s * s / p); + + if (!isNull(tmp) && !isNull(s)) + { + const double z1 = (r - s * q / p) / tmp; + const double z2 = (r - n * z1) / s; //not (n + 1) here, since n already is the number of measurements + + //refresh current values for z1, z2 + z1_current = z1; + z2_current = z2; + } + } + + return formatRemainingTime((itemsTotal - objectsCurrent) * z1_current + (bytesTotal - dataCurrent) * z2_current); +} +*/ diff --git a/FreeFileSync/Source/base/speed_test.h b/FreeFileSync/Source/base/speed_test.h new file mode 100644 index 0000000..dad2f12 --- /dev/null +++ b/FreeFileSync/Source/base/speed_test.h @@ -0,0 +1,47 @@ +// ***************************************************************************** +// * 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 PERF_CHECK_H_87804217589312454 +#define PERF_CHECK_H_87804217589312454 + +#include +#include +#include +#include + + +namespace fff +{ +class SpeedTest +{ +public: + explicit SpeedTest(std::chrono::milliseconds windowSize) : windowSize_(windowSize) {} + + void addSample(std::chrono::nanoseconds timeElapsed, int itemsCurrent, int64_t bytesCurrent); + + std::optional getRemainingSec(int itemsRemaining, int64_t bytesRemaining) const; + std::optional getBytesPerSec() const; + std::optional getItemsPerSec() const; + + std::wstring getBytesPerSecFmt() const; //empty if not (yet) available + std::wstring getItemsPerSecFmt() const; // + + void clear() { samples_.clear(); } + +private: + struct Sample + { + std::chrono::nanoseconds timeElapsed{}; //std::chrono::duration is uninitialized by default! WTF + int items = 0; + int64_t bytes = 0; + }; + + const std::chrono::milliseconds windowSize_; + zen::RingBuffer samples_; +}; +} + +#endif //PERF_CHECK_H_87804217589312454 diff --git a/FreeFileSync/Source/base/status_handler_impl.h b/FreeFileSync/Source/base/status_handler_impl.h new file mode 100644 index 0000000..7f67042 --- /dev/null +++ b/FreeFileSync/Source/base/status_handler_impl.h @@ -0,0 +1,559 @@ +// ***************************************************************************** +// * 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 STATUS_HANDLER_IMPL_H_07682758976 +#define STATUS_HANDLER_IMPL_H_07682758976 + +#include +#include +#include +#include "process_callback.h" +#include "speed_test.h" + + +namespace fff +{ +class AsyncCallback +{ +public: + AsyncCallback() {} + + //non-blocking: context of worker thread (and main thread, see reportStats()) + void updateDataProcessed(int itemsDelta, int64_t bytesDelta) //noexcept! + { + itemsDeltaProcessed_ += itemsDelta; + bytesDeltaProcessed_ += bytesDelta; + } + void updateDataTotal(int itemsDelta, int64_t bytesDelta) //noexcept! + { + itemsDeltaTotal_ += itemsDelta; + bytesDeltaTotal_ += bytesDelta; + } + + //context of worker thread + void updateStatus(std::wstring&& msg) //throw ThreadStopRequest + { + assert(!zen::runningOnMainThread()); + { + std::lock_guard dummy(lockCurrentStatus_); + if (ThreadStatus* ts = getThreadStatus()) //call while holding "lockCurrentStatus_" lock!! + ts->statusMsg = std::move(msg); + else assert(false); + } + zen::interruptionPoint(); //throw ThreadStopRequest + } + + //blocking call: context of worker thread + //=> indirect support for "pause": logInfo() is called under singleThread lock, + // so all other worker threads will wait when coming out of parallel I/O (trying to lock singleThread) + void logMessage(const std::wstring& msg, PhaseCallback::MsgType type) //throw ThreadStopRequest + { + assert(!zen::runningOnMainThread()); + { + std::unique_lock dummy(lockRequest_); + zen::interruptibleWait(conditionReadyForNewRequest_, dummy, [this] { return !logMsgRequest_; }); //throw ThreadStopRequest + + logMsgRequest_ = LogMsgRequest{msg, type}; + } + conditionNewRequest.notify_all(); + } + + //blocking call: context of worker thread + PhaseCallback::Response reportError(const PhaseCallback::ErrorInfo& errorInfo) //throw ThreadStopRequest + { + assert(!zen::runningOnMainThread()); + std::unique_lock dummy(lockRequest_); + zen::interruptibleWait(conditionReadyForNewRequest_, dummy, [this] { return !errorRequest_ && !errorResponse_; }); //throw ThreadStopRequest + + errorRequest_ = errorInfo; + conditionNewRequest.notify_all(); + + zen::interruptibleWait(conditionHaveResponse_, dummy, [this] { return static_cast(errorResponse_); }); //throw ThreadStopRequest + + PhaseCallback::Response rv = *errorResponse_; + + errorRequest_ = std::nullopt; + errorResponse_ = std::nullopt; + + dummy.unlock(); //optimization for condition_variable::notify_all() + conditionReadyForNewRequest_.notify_all(); //=> spurious wake-up for AsyncCallback::logInfo() + return rv; + } + + //blocking call: context of worker thread + void reportWarning(const std::wstring& msg, bool& warningActive) //throw ThreadStopRequest + { + assert(!zen::runningOnMainThread()); + { + std::unique_lock dummy(lockRequest_); + zen::interruptibleWait(conditionReadyForNewRequest_, dummy, [this] { return !warningRequest_ && !warningResponse_; }); //throw ThreadStopRequest + + warningRequest_ = WarningRequest{msg, warningActive}; + conditionNewRequest.notify_all(); + + zen::interruptibleWait(conditionHaveResponse_, dummy, [this] { return static_cast(warningResponse_); }); //throw ThreadStopRequest + + warningActive = warningResponse_->warningActive; + + warningRequest_ = std::nullopt; + warningResponse_ = std::nullopt; + } + conditionReadyForNewRequest_.notify_all(); //=> spurious wake-up for AsyncCallback::logInfo() + } + + //context of main thread + std::pair waitUntilDone(std::chrono::milliseconds cbInterval, PhaseCallback& cb) //throw X + { + assert(zen::runningOnMainThread()); + for (;;) + { + const std::chrono::steady_clock::time_point callbackTime = std::chrono::steady_clock::now() + cbInterval; + + for (std::unique_lock dummy(lockRequest_);;) //process all errors without delay + { + const bool rv = conditionNewRequest.wait_until(dummy, callbackTime, [this] + { + return logMsgRequest_ || (errorRequest_ && !errorResponse_) || (warningRequest_ && !warningResponse_) || finishNowRequest_; + }); + if (!rv) //time-out + condition not met + break; + + if (logMsgRequest_) + { + cb.logMessage(logMsgRequest_->msg, logMsgRequest_->type); //throw X + logMsgRequest_ = {}; + conditionReadyForNewRequest_.notify_all(); //=> spurious wake-up for AsyncCallback::reportError() + } + if (errorRequest_ && !errorResponse_) + { + assert(!finishNowRequest_); + errorResponse_ = cb.reportError(*errorRequest_); //throw X + conditionHaveResponse_.notify_all(); //instead of notify_one(); work around bug: https://svn.boost.org/trac/boost/ticket/7796 + } + if (warningRequest_ && !warningResponse_) + { + assert(!finishNowRequest_); + bool warningActive = warningRequest_->warningActive; + cb.reportWarning(warningRequest_->msg, warningActive); //throw X + warningResponse_ = WarningResponse{warningActive}; + conditionHaveResponse_.notify_all(); + } + if (finishNowRequest_) + { + dummy.unlock(); //call member functions outside of mutex scope: + reportStats(cb); //one last call for accurate stat-reporting! + return std::make_pair(itemsProcessed_, bytesProcessed_); + } + } + + //call back outside of mutex scope: + cb.updateStatus(getStatusMsg()); //throw X + reportStats(cb); + } + } + + void notifyTaskBegin(size_t prio) //noexcept + { + assert(!zen::runningOnMainThread()); + const std::thread::id threadId = std::this_thread::get_id(); + std::lock_guard dummy(lockCurrentStatus_); + assert(!getThreadStatus()); + + if (statusByPriority_.size() < prio + 1) + statusByPriority_.resize(prio + 1); + + statusByPriority_[prio].push_back({threadId, std::wstring()}); + } + + void notifyTaskEnd() //noexcept + { + assert(!zen::runningOnMainThread()); + const std::thread::id threadId = std::this_thread::get_id(); + std::lock_guard dummy(lockCurrentStatus_); + + for (std::vector& sbp : statusByPriority_) + for (ThreadStatus& ts : sbp) + if (ts.threadId == threadId) + { + std::swap(ts, sbp.back()); + sbp.pop_back(); + return; + } + assert(false); + } + + void notifyAllDone() //noexcept + { + { + std::lock_guard dummy(lockRequest_); + assert(!finishNowRequest_); + finishNowRequest_ = true; + } + conditionNewRequest.notify_all(); + } + +private: + AsyncCallback (const AsyncCallback&) = delete; + AsyncCallback& operator=(const AsyncCallback&) = delete; + + struct ThreadStatus + { + std::thread::id threadId; + std::wstring statusMsg; + }; + + ThreadStatus* getThreadStatus() //call while holding "lockCurrentStatus_" lock!! + { + assert(!zen::runningOnMainThread()); + const std::thread::id threadId = std::this_thread::get_id(); + + for (std::vector& sbp : statusByPriority_) + for (ThreadStatus& ts : sbp) //thread count is (hopefully) small enough so that linear search won't hurt perf + if (ts.threadId == threadId) + return &ts; + return nullptr; + } + + //context of main thread + void reportStats(PhaseCallback& cb) + { + assert(zen::runningOnMainThread()); + const int itemsDeltaProcessed = itemsDeltaProcessed_; //get value snapshot from atomics + const int64_t bytesDeltaProcessed = bytesDeltaProcessed_; // + if (itemsDeltaProcessed != 0 || bytesDeltaProcessed != 0) + { + updateDataProcessed (-itemsDeltaProcessed, -bytesDeltaProcessed); //careful with these atomics: don't just set to 0 + cb.updateDataProcessed( itemsDeltaProcessed, bytesDeltaProcessed); //noexcept! + + itemsProcessed_ += itemsDeltaProcessed; + bytesProcessed_ += bytesDeltaProcessed; + } + + const int itemsDeltaTotal = itemsDeltaTotal_; + const int64_t bytesDeltaTotal = bytesDeltaTotal_; + if (itemsDeltaTotal != 0 || bytesDeltaTotal != 0) + { + updateDataTotal (-itemsDeltaTotal, -bytesDeltaTotal); + cb.updateDataTotal( itemsDeltaTotal, bytesDeltaTotal); //noexcept! + } + } + + //context of main thread, call repreatedly + std::wstring getStatusMsg() + { + assert(zen::runningOnMainThread()); + + size_t parallelOpsTotal = 0; + std::wstring statusMsg; + { + std::lock_guard dummy(lockCurrentStatus_); + + for (const auto& sbp : statusByPriority_) + parallelOpsTotal += sbp.size(); + + statusMsg = [&] + { + for (const std::vector& sbp : statusByPriority_) + for (const ThreadStatus& ts : sbp) + if (!ts.statusMsg.empty()) + return ts.statusMsg; + return std::wstring(); + }(); + } + if (parallelOpsTotal >= 2) + return L'[' + _P("1 thread", "%x threads", parallelOpsTotal) + L"] " + statusMsg; + else + return statusMsg; + } + + struct LogMsgRequest + { + std::wstring msg; + PhaseCallback::MsgType type = PhaseCallback::MsgType::error; + }; + struct WarningRequest + { + std::wstring msg; + bool warningActive = false; + }; + struct WarningResponse { bool warningActive = false; }; + + //---- main <-> worker communication channel ---- + std::mutex lockRequest_; + std::condition_variable conditionReadyForNewRequest_; + std::condition_variable conditionNewRequest; + std::condition_variable conditionHaveResponse_; + std::optional logMsgRequest_; + std::optional errorRequest_; + std::optional errorResponse_; + std::optional warningRequest_; + std::optional warningResponse_; + bool finishNowRequest_ = false; + + //---- status updates ---- + std::mutex lockCurrentStatus_; //different lock for status updates so that we're not blocked by other threads reporting errors + std::vector> statusByPriority_; + //give status messages priority according to their folder pair (e.g. first folder pair has prio 0) => visualize (somewhat) natural processing order + + //---- status updates II (lock-free) ---- + std::atomic itemsDeltaProcessed_{0}; // + std::atomic bytesDeltaProcessed_{0}; //std:atomic is uninitialized by default! + std::atomic itemsDeltaTotal_ {0}; // + std::atomic bytesDeltaTotal_ {0}; // + + //---- aggregated numbers; accessed by main thread only ---- + int itemsProcessed_ = 0; + int64_t bytesProcessed_ = 0; +}; + + +//manage statistics reporting for a single item of work +template +class ItemStatReporter +{ +public: + ItemStatReporter(int itemsExpected, int64_t bytesExpected, Callback& cb) : + itemsExpected_(itemsExpected), + bytesExpected_(bytesExpected), + cb_(cb) {} + + ~ItemStatReporter() + { + const bool scopeFail = std::uncaught_exceptions() > exeptionCount_; + if (scopeFail) + cb_.updateDataTotal(itemsReported_, bytesReported_); //=> unexpected increase of total workload + else + //update statistics to consider the real amount of data, e.g. CopyFileEx: more than the "file size" for ADS streams, + //less for sparse and compressed files, or file changed in the meantime! + cb_.updateDataTotal(itemsReported_ - itemsExpected_, bytesReported_ - bytesExpected_); //noexcept! + } + + void updateStatus(std::wstring&& msg) { cb_.updateStatus(std::move(msg)); } //throw X + + void logMessage(const std::wstring& msg, PhaseCallback::MsgType type) { cb_.logMessage(msg, type); } //throw X + + void reportWarning(const std::wstring& msg, bool& warningActive) { cb_.reportWarning(msg, warningActive); }//throw X + + void reportDelta(int itemsDelta, int64_t bytesDelta) //noexcept! + { + cb_.updateDataProcessed(itemsDelta, bytesDelta); //noexcept! + itemsReported_ += itemsDelta; + bytesReported_ += bytesDelta; + + //special rule: avoid temporary statistics mess up, even though they are corrected anyway below: + if (itemsReported_ > itemsExpected_) + { + cb_.updateDataTotal(itemsReported_ - itemsExpected_, 0); //noexcept! + itemsReported_ = itemsExpected_; + } + if (bytesReported_ > bytesExpected_) + { + cb_.updateDataTotal(0, bytesReported_ - bytesExpected_); //=> everything above "bytesExpected" adds to both "processed" and "total" data + bytesReported_ = bytesExpected_; + } + } + +private: + int itemsReported_ = 0; + int64_t bytesReported_ = 0; + const int itemsExpected_; + const int64_t bytesExpected_; + Callback& cb_; + const int exeptionCount_ = std::uncaught_exceptions(); +}; + +using AsyncItemStatReporter = ItemStatReporter; + +//===================================================================================================================== + +constexpr std::chrono::seconds STATUS_PERCENT_DELAY(2); +constexpr std::chrono::seconds STATUS_PERCENT_MIN_DURATION(3); +const int STATUS_PERCENT_MIN_CHANGES_PER_SEC = 2; +constexpr std::chrono::seconds STATUS_PERCENT_SPEED_WINDOW(10); + +template +struct PercentStatReporter +{ + PercentStatReporter(const std::wstring& statusMsg, int64_t bytesExpected, ItemStatReporter& statReporter) : + msgPrefix_(statusMsg + L"... "), + bytesExpected_(bytesExpected), + statReporter_(statReporter) {} + //[!] no "updateStatus() /*throw X*/" in constructor! let caller decide + + void updateDeltaAndStatus(int64_t bytesDelta) //throw X + { + statReporter_.reportDelta(0 /*itemsDelta*/, bytesDelta); + bytesCopied_ += bytesDelta; + + const auto now = std::chrono::steady_clock::now(); + if (now >= lastUpdate_ + UI_UPDATE_INTERVAL / 2) //every ~25 ms + { + lastUpdate_ = now; + + if (!showPercent_ && bytesCopied_ > 0) + { + if (startTime_ == std::chrono::steady_clock::time_point()) + { + startTime_ = now; //get higher-quality perf stats when starting timing here rather than constructor!? + speedTest_.addSample(std::chrono::seconds(0), 0 /*itemsCurrent*/, bytesCopied_); + } + else if (const std::chrono::nanoseconds elapsed = now - startTime_; + elapsed >= STATUS_PERCENT_DELAY) + { + speedTest_.addSample(elapsed, 0 /*itemsCurrent*/, bytesCopied_); + + if (const std::optional remSecs = speedTest_.getRemainingSec(0 /*itemsRemaining*/, bytesExpected_ - bytesCopied_)) + if (*remSecs > std::chrono::duration(STATUS_PERCENT_MIN_DURATION).count()) + { + showPercent_ = true; + speedTest_.clear(); //discard (probably messy) numbers + } + } + } + if (showPercent_) + { + speedTest_.addSample(now - startTime_, 0 /*itemsCurrent*/, bytesCopied_); + const std::optional bps = speedTest_.getBytesPerSec(); + + statReporter_.updateStatus(msgPrefix_ + formatPercent(std::min(static_cast(bytesCopied_) / bytesExpected_, 1.0), //> 100% possible! see process_callback.h notes + bps ? *bps : 0, bytesExpected_)); //throw X + } + } + } + +private: + static std::wstring formatPercent(double fraction, double bytesPerSec, int64_t bytesTotal) + { + const double totalSecs = numeric::isNull(bytesPerSec) ? 0 : bytesTotal / bytesPerSec; + const double expectedSteps = totalSecs * STATUS_PERCENT_MIN_CHANGES_PER_SEC; + + const int decPlaces = [&] //TODO? protect against format flickering!? + { + if (expectedSteps <= 100) return 0; + if (expectedSteps <= 1000) return 1; + if (expectedSteps <= 10000) return 2; + if (expectedSteps <= 100000) return 3; + //return static_cast(std::ceil(std::log10(expectedSteps))) - 2; -> overkill! + /**/ return 4; + }(); + return zen::formatProgressPercent(fraction, decPlaces); + } + + bool showPercent_ = false; + const std::wstring msgPrefix_; + const int64_t bytesExpected_; + int64_t bytesCopied_ = 0; + std::chrono::steady_clock::time_point startTime_; + std::chrono::steady_clock::time_point lastUpdate_; + SpeedTest speedTest_{STATUS_PERCENT_SPEED_WINDOW}; + ItemStatReporter& statReporter_; +}; + +//===================================================================================================================== + +template inline +void reportInfo(std::wstring&& msg, Callback& cb /*throw X*/) //throw X +{ + cb.logMessage(msg, PhaseCallback::MsgType::info); //throw X + cb.updateStatus(std::move(msg)); // +} + + +template inline //return ignored error message if available +std::wstring tryReportingError(Function cmd /*throw FileError*/, Callback& cb /*throw X*/) //throw X +{ + for (size_t retryNumber = 0;; ++retryNumber) + try + { + cmd(); //throw FileError + return std::wstring(); + } + catch (const zen::FileError& e) + { + assert(!e.toString().empty()); + switch (cb.reportError({e.toString(), std::chrono::steady_clock::now(), retryNumber})) //throw X + { + case PhaseCallback::ignore: + return e.toString(); + case PhaseCallback::retry: + break; //continue with loop + } + } +} + +//===================================================================================================================== +struct ParallelContext +{ + const AbstractPath& itemPath; + AsyncCallback& acb; +}; +using ParallelWorkItem = std::function /*throw ThreadStopRequest*/; + + +namespace +{ +void massParallelExecute(const std::vector>& workload, + const Zstring& threadGroupName, + PhaseCallback& callback /*throw X*/) //throw X +{ + using namespace zen; + + std::map*>> perDeviceWorkload; + for (const auto& item : workload) + perDeviceWorkload[item.first.afsDevice].push_back(&item); + + if (perDeviceWorkload.empty()) + return; //[!] otherwise AsyncCallback::notifyAllDone() is never called! + + AsyncCallback acb; //manage life time: enclose ThreadGroup's!!! + std::atomic activeDeviceCount(perDeviceWorkload.size()); // + + //--------------------------------------------------------------------------------------------------------- + std::vector>> deviceThreadGroups; //worker threads live here... + //--------------------------------------------------------------------------------------------------------- + + for (const auto& [afsDevice, wl] : perDeviceWorkload) + { + const size_t statusPrio = deviceThreadGroups.size(); + + const Zstring& deviceGroupName = threadGroupName + Zstr(' ') + utfTo(AFS::getDisplayPath(AbstractPath(afsDevice, AfsPath()))); + deviceThreadGroups.emplace_back(1, deviceGroupName); + auto& threadGroup = deviceThreadGroups.back(); + + for (const std::pair* item : wl) + threadGroup.run([&acb, statusPrio, &itemPath = item->first, &task = item->second] + { + acb.notifyTaskBegin(statusPrio); + ZEN_ON_SCOPE_EXIT(acb.notifyTaskEnd()); + + ParallelContext pctx{itemPath, acb}; + task(pctx); //throw ThreadStopRequest + }); + + threadGroup.notifyWhenDone([&acb, &activeDeviceCount] /*noexcept! runs on worker thread!*/ + { + if (--activeDeviceCount == 0) + acb.notifyAllDone(); //noexcept + }); + } + + acb.waitUntilDone(UI_UPDATE_INTERVAL / 2 /*every ~25 ms*/, callback); //throw X +} +} + +//===================================================================================================================== + +template inline +auto parallelScope(Function&& fun, std::mutex& singleThread) //throw X +{ + singleThread.unlock(); + ZEN_ON_SCOPE_EXIT(singleThread.lock()); + + return fun(); //throw X +} +} + +#endif //STATUS_HANDLER_IMPL_H_07682758976 diff --git a/FreeFileSync/Source/base/structures.cpp b/FreeFileSync/Source/base/structures.cpp new file mode 100644 index 0000000..19554b0 --- /dev/null +++ b/FreeFileSync/Source/base/structures.cpp @@ -0,0 +1,380 @@ +// ***************************************************************************** +// * 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 "structures.h" +#include +#include +#include "../afs/concrete.h" + +using namespace zen; +using namespace fff; + + +std::wstring fff::getVariantName(std::optional var) +{ + if (!var) + return _("Multiple..."); + + switch (*var) + { + case CompareVariant::timeSize: return _("File time and size"); + case CompareVariant::content: return _("File content"); + case CompareVariant::size: return _("File size"); + } + assert(false); + return _("Error"); +} + + +std::wstring fff::getVariantName(std::optional var) +{ + if (!var) + return _("Multiple..."); + + switch (*var) + { + case SyncVariant::twoWay: return _("Two way"); + case SyncVariant::mirror: return _("Mirror"); + case SyncVariant::update: return _("Update"); + case SyncVariant::custom: return _("Custom"); + } + assert(false); + return _("Error"); +} + + +//use in sync log files where users expect ANSI: https://freefilesync.org/forum/viewtopic.php?t=4647 +std::wstring fff::getVariantNameWithSymbol(SyncVariant var) +{ + switch (var) + { + case SyncVariant::twoWay: return _("Two way") + L" <->"; + case SyncVariant::mirror: return _("Mirror") + L" ->"; + case SyncVariant::update: return _("Update") + L" >"; + case SyncVariant::custom: return _("Custom") + L" <>"; + } + assert(false); + return _("Error"); +} + + +DirectionByDiff fff::getDiffDirDefault(const DirectionByChange& changeDirs) +{ + return + { + .leftOnly = changeDirs.left.create, + .rightOnly = changeDirs.right.create, + .leftNewer = changeDirs.left.update, + .rightNewer = changeDirs.right.update, + }; +} + + +DirectionByChange fff::getChangesDirDefault(const DirectionByDiff& diffDirs) +{ + return + { + .left + { + .create = diffDirs.leftOnly, + .update = diffDirs.leftNewer, + .delete_ = diffDirs.rightOnly, + }, + .right + { + .create = diffDirs.rightOnly, + .update = diffDirs.rightNewer, + .delete_ = diffDirs.leftOnly, + } + }; +} + + +namespace +{ +DirectionByChange getTwoWayDirSet() +{ + return + { + .left + { + .create = SyncDirection::right, + .update = SyncDirection::right, + .delete_ = SyncDirection::right, + }, + .right + { + .create = SyncDirection::left, + .update = SyncDirection::left, + .delete_ = SyncDirection::left, + } + }; +} + + +DirectionByDiff getMirrorDirSet() +{ + return + { + .leftOnly = SyncDirection::right, + .rightOnly = SyncDirection::right, + .leftNewer = SyncDirection::right, + .rightNewer = SyncDirection::right, + }; +} + + +DirectionByChange getUpdateDirSet() +{ + return + { + .left + { + .create = SyncDirection::right, + .update = SyncDirection::right, + .delete_ = SyncDirection::none, + }, + .right + { + .create = SyncDirection::none, + .update = SyncDirection::none, + .delete_ = SyncDirection::none, + } + }; +} +} + + +SyncVariant fff::getSyncVariant(const SyncDirectionConfig& cfg) +{ + if (const DirectionByDiff* diffDirs = std::get_if(&cfg.dirs)) + { + if (*diffDirs == getMirrorDirSet()) + return SyncVariant::mirror; + if (*diffDirs == getDiffDirDefault(getUpdateDirSet())) //poor man's "update", still deserves name on GUI + return SyncVariant::update; + } + else + { + const DirectionByChange& changeDirs = std::get(cfg.dirs); + if (changeDirs == getTwoWayDirSet()) + return SyncVariant::twoWay; + if (changeDirs == getChangesDirDefault(getMirrorDirSet())) //equivalent: "mirror" defined in terms of "changes" + return SyncVariant::mirror; + if (changeDirs == getUpdateDirSet()) + return SyncVariant::update; + } + return SyncVariant::custom; +} + + +SyncDirectionConfig fff::getDefaultSyncCfg(SyncVariant syncVar) +{ + switch (syncVar) + { + case SyncVariant::twoWay: return { .dirs = getTwoWayDirSet() }; + case SyncVariant::mirror: return { .dirs = getMirrorDirSet() }; + case SyncVariant::update: return { .dirs = getUpdateDirSet() }; + case SyncVariant::custom: return { .dirs = getDiffDirDefault(getTwoWayDirSet()) }; + } + throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); +} + + +size_t fff::getDeviceParallelOps(const std::map& deviceParallelOps, const AfsDevice& afsDevice) +{ + auto it = deviceParallelOps.find(afsDevice); + return std::max(it != deviceParallelOps.end() ? it->second : 1, 1); +} + + +void fff::setDeviceParallelOps(std::map& deviceParallelOps, const AfsDevice& afsDevice, size_t parallelOps) +{ + assert(parallelOps > 0); + if (!AFS::isNullDevice(afsDevice)) + { + if (parallelOps > 1) + deviceParallelOps[afsDevice] = parallelOps; + else + deviceParallelOps.erase(afsDevice); + } +} + + +size_t fff::getDeviceParallelOps(const std::map& deviceParallelOps, const Zstring& folderPathPhrase) +{ + return getDeviceParallelOps(deviceParallelOps, createAbstractPath(folderPathPhrase).afsDevice); +} + + +void fff::setDeviceParallelOps(std::map& deviceParallelOps, const Zstring& folderPathPhrase, size_t parallelOps) +{ + setDeviceParallelOps(deviceParallelOps, createAbstractPath(folderPathPhrase).afsDevice, parallelOps); +} + + +std::wstring fff::getSymbol(CompareFileResult cmpRes) +{ + switch (cmpRes) + { + case FILE_EQUAL: return L"'="; //added quotation mark to avoid error in Excel cell when exporting to *.cvs + case FILE_RENAMED: return L"renamed"; + case FILE_LEFT_ONLY: return L"only <-"; + case FILE_RIGHT_ONLY: return L"only ->"; + case FILE_LEFT_NEWER: return L"newer <-"; + case FILE_RIGHT_NEWER: return L"newer ->"; + case FILE_DIFFERENT_CONTENT: return L"!="; + case FILE_TIME_INVALID: + case FILE_CONFLICT: return L"conflict"; + } + assert(false); + return std::wstring(); +} + + +std::wstring fff::getSymbol(SyncOperation op) +{ + switch (op) + { + case SO_CREATE_LEFT: return L"create <-"; + case SO_CREATE_RIGHT: return L"create ->"; + case SO_DELETE_LEFT: return L"delete <-"; + case SO_DELETE_RIGHT: return L"delete ->"; + case SO_MOVE_LEFT_FROM: return L"move from <-"; + case SO_MOVE_LEFT_TO: return L"move to <-"; + case SO_MOVE_RIGHT_FROM: return L"move from ->"; + case SO_MOVE_RIGHT_TO: return L"move to ->"; + case SO_OVERWRITE_LEFT: return L"update <-"; + case SO_OVERWRITE_RIGHT: return L"update ->"; + case SO_RENAME_LEFT: return L"rename <-"; + case SO_RENAME_RIGHT: return L"rename ->"; + case SO_DO_NOTHING: return L" -"; + case SO_EQUAL: return L"'="; //added quotation mark to avoid error in Excel cell when exporting to *.cvs + case SO_UNRESOLVED_CONFLICT: return L"conflict"; //portable Unicode symbol: ⚡ + }; + assert(false); + return std::wstring(); +} + + +namespace +{ +time_t resolve(size_t value, UnitTime unit, time_t defaultVal) +{ + TimeComp tcLocal = getLocalTime(); //returns TimeComp() on error + if (tcLocal != TimeComp()) + switch (unit) + { + case UnitTime::none: + return defaultVal; + + case UnitTime::today: + case UnitTime::lastDays: + tcLocal.second = 0; //0-61 + tcLocal.minute = 0; //0-59 + tcLocal.hour = 0; //0-23 + break; + + case UnitTime::thisMonth: + tcLocal.second = 0; //0-61 + tcLocal.minute = 0; //0-59 + tcLocal.hour = 0; //0-23 + tcLocal.day = 1; //1-31 + break; + + case UnitTime::thisYear: + tcLocal.second = 0; //0-61 + tcLocal.minute = 0; //0-59 + tcLocal.hour = 0; //0-23 + tcLocal.day = 1; //1-31 + tcLocal.month = 1; //1-12 + break; + } + if (const auto [localTime, timeValid] = localToTimeT(tcLocal);//convert local time back to UTC + timeValid) + { + if (unit == UnitTime::lastDays) + return localTime - value * 24 * 3600; + + return localTime; + } + + assert(false); + return defaultVal; +} + + +uint64_t resolve(uint64_t value, UnitSize unit, uint64_t defaultVal) +{ + constexpr uint64_t maxVal = std::numeric_limits::max(); + + switch (unit) + { + case UnitSize::none: + return defaultVal; + case UnitSize::byte: + return value; + case UnitSize::kb: + return value > maxVal / bytesPerKilo ? maxVal : //prevent overflow!!! + value * bytesPerKilo; + case UnitSize::mb: + return value > maxVal / (bytesPerKilo * bytesPerKilo) ? maxVal : //prevent overflow!!! + value * bytesPerKilo * bytesPerKilo; + } + assert(false); + return defaultVal; +} +} + +void fff::resolveUnits(size_t timeSpan, UnitTime unitTimeSpan, + uint64_t sizeMin, UnitSize unitSizeMin, + uint64_t sizeMax, UnitSize unitSizeMax, + time_t& timeFrom, //unit: UTC time, seconds + uint64_t& sizeMinBy, //unit: bytes + uint64_t& sizeMaxBy) //unit: bytes +{ + timeFrom = resolve(timeSpan, unitTimeSpan, std::numeric_limits::min()); + sizeMinBy = resolve(sizeMin, unitSizeMin, 0U); + sizeMaxBy = resolve(sizeMax, unitSizeMax, std::numeric_limits::max()); +} + + +std::optional fff::getCommonCompVariant(const MainConfiguration& mainCfg) +{ + const CompareVariant firstVar = mainCfg.firstPair.localCmpCfg ? + mainCfg.firstPair.localCmpCfg->compareVar : + mainCfg.cmpCfg.compareVar; //fallback to main sync cfg + + //test if there's a deviating variant within the additional folder pairs + for (const LocalPairConfig& lpc : mainCfg.additionalPairs) + { + const CompareVariant localVariant = lpc.localCmpCfg ? + lpc.localCmpCfg->compareVar : + mainCfg.cmpCfg.compareVar; //fallback to main sync cfg + if (localVariant != firstVar) + return std::nullopt; + } + return firstVar; //seems to be all in sync... +} + + +std::optional fff::getCommonSyncVariant(const MainConfiguration& mainCfg) +{ + const SyncVariant firstVar = getSyncVariant(mainCfg.firstPair.localSyncCfg ? + mainCfg.firstPair.localSyncCfg->directionCfg : + mainCfg.syncCfg.directionCfg); //fallback to main sync cfg + + //test if there's a deviating variant within the additional folder pairs + for (const LocalPairConfig& lpc : mainCfg.additionalPairs) + { + const SyncVariant localVariant = getSyncVariant(lpc.localSyncCfg ? + lpc.localSyncCfg->directionCfg: + mainCfg.syncCfg.directionCfg); + if (localVariant != firstVar) + return std::nullopt; + } + return firstVar; //seems to be all in sync... +} diff --git a/FreeFileSync/Source/base/structures.h b/FreeFileSync/Source/base/structures.h new file mode 100644 index 0000000..d49a4b8 --- /dev/null +++ b/FreeFileSync/Source/base/structures.h @@ -0,0 +1,393 @@ +// ***************************************************************************** +// * 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 STRUCTURES_H_8210478915019450901745 +#define STRUCTURES_H_8210478915019450901745 + +#include +#include +#include +#include +#include "../afs/abstract.h" + + +namespace fff +{ +using AFS = AbstractFileSystem; + +enum class CompareVariant +{ + timeSize, + content, + size +}; + + +enum class SymLinkHandling +{ + exclude, + asLink, + follow +}; + + +enum class SyncDirection : unsigned char //save space for use in FileSystemObject! +{ + none, + left, + right +}; + + +enum CompareFileResult +{ + FILE_EQUAL, + FILE_RENAMED, //both sides equal, except for different file name + FILE_LEFT_ONLY, + FILE_RIGHT_ONLY, + FILE_LEFT_NEWER, // + FILE_RIGHT_NEWER, //CompareVariant::timeSize only! + FILE_TIME_INVALID, // -> sync dirction can be determined (if leftNewer/rightNewer agree), unlike with FILE_CONFLICT + FILE_DIFFERENT_CONTENT, //CompareVariant::content, CompareVariant::size only! + FILE_CONFLICT +}; +//attention make sure these /|\ \|/ three enums match!!! +enum CompareDirResult +{ + DIR_EQUAL = FILE_EQUAL, + DIR_RENAMED = FILE_RENAMED, + DIR_LEFT_ONLY = FILE_LEFT_ONLY, + DIR_RIGHT_ONLY = FILE_RIGHT_ONLY, + DIR_CONFLICT = FILE_CONFLICT +}; + +enum CompareSymlinkResult +{ + SYMLINK_EQUAL = FILE_EQUAL, + SYMLINK_RENAMED = FILE_RENAMED, + SYMLINK_LEFT_ONLY = FILE_LEFT_ONLY, + SYMLINK_RIGHT_ONLY = FILE_RIGHT_ONLY, + SYMLINK_LEFT_NEWER = FILE_LEFT_NEWER, + SYMLINK_RIGHT_NEWER = FILE_RIGHT_NEWER, + SYMLINK_TIME_INVALID = FILE_TIME_INVALID, + SYMLINK_DIFFERENT_CONTENT = FILE_DIFFERENT_CONTENT, + SYMLINK_CONFLICT = FILE_CONFLICT +}; + + +std::wstring getSymbol(CompareFileResult cmpRes); + + +enum SyncOperation +{ + SO_CREATE_LEFT, + SO_CREATE_RIGHT, + SO_DELETE_LEFT, + SO_DELETE_RIGHT, + + SO_OVERWRITE_LEFT, + SO_OVERWRITE_RIGHT, + + SO_MOVE_LEFT_FROM, //SO_DELETE_LEFT - optimization! + SO_MOVE_LEFT_TO, //SO_CREATE_LEFT + + SO_MOVE_RIGHT_FROM, //SO_DELETE_RIGHT - optimization! + SO_MOVE_RIGHT_TO, //SO_CREATE_RIGHT + + SO_RENAME_LEFT, //items are otherwise equal + SO_RENAME_RIGHT, // + + SO_DO_NOTHING, //nothing will be synced: both sides differ + SO_EQUAL, //nothing will be synced: both sides are equal + SO_UNRESOLVED_CONFLICT +}; + +std::wstring getSymbol(SyncOperation op); //method used for exporting .csv file only! + + +enum class CudAction +{ + noChange, + create, + update, + delete_, //"delete" is a reserved keyword :( +}; + +struct DirectionByDiff +{ + SyncDirection leftOnly = SyncDirection::none; + SyncDirection rightOnly = SyncDirection::none; + SyncDirection leftNewer = SyncDirection::none; + SyncDirection rightNewer = SyncDirection::none; + + bool operator==(const DirectionByDiff&) const = default; +}; + + +struct DirectionByChange //=> requires sync.ffs_db +{ + struct Changes + { + SyncDirection create = SyncDirection::none; + SyncDirection update = SyncDirection::none; + SyncDirection delete_ = SyncDirection::none; //"delete" is a reserved keyword :( + + bool operator==(const Changes&) const = default; + } left, right; + + bool operator==(const DirectionByChange&) const = default; +}; + + +struct SyncDirectionConfig +{ + std::variant dirs; + + bool operator==(const SyncDirectionConfig&) const = default; +}; + + +inline +bool effectivelyEqual(const SyncDirectionConfig& lhs, const SyncDirectionConfig& rhs) { return lhs == rhs; } //no change in behavior + + +enum class SyncVariant +{ + twoWay, + mirror, + update, + custom, +}; +SyncVariant getSyncVariant(const SyncDirectionConfig& cfg); + +SyncDirectionConfig getDefaultSyncCfg(SyncVariant syncVar); + +DirectionByDiff getDiffDirDefault(const DirectionByChange& changeDirs); //= when sync.ffs_db not yet available +DirectionByChange getChangesDirDefault(const DirectionByDiff& diffDirs); + +std::wstring getVariantName(std::optional var); +std::wstring getVariantName(std::optional var); + +std::wstring getVariantNameWithSymbol(SyncVariant var); + + +struct CompConfig +{ + CompareVariant compareVar = CompareVariant::timeSize; + SymLinkHandling handleSymlinks = SymLinkHandling::exclude; + std::vector ignoreTimeShiftMinutes; //treat modification times with these offsets as equal + + bool operator==(const CompConfig&) const = default; +}; + +inline +bool effectivelyEqual(const CompConfig& lhs, const CompConfig& rhs) { return lhs == rhs; } //no change in behavior + + +enum class DeletionVariant +{ + permanent, + recycler, + versioning +}; + +enum class VersioningStyle +{ + replace, + timestampFolder, + timestampFile, +}; + +struct SyncConfig +{ + //sync direction settings + SyncDirectionConfig directionCfg = getDefaultSyncCfg(SyncVariant::twoWay); + + DeletionVariant deletionVariant = DeletionVariant::recycler; //use Recycle Bin, delete permanently or move to user-defined location + + //versioning options + Zstring versioningFolderPhrase; + VersioningStyle versioningStyle = VersioningStyle::replace; + + //limit number of versions per file: (if versioningStyle != replace) + int versionMaxAgeDays = 0; //<= 0 := no limit + int versionCountMin = 0; //only used if versionMaxAgeDays > 0 => < versionCountMax (if versionCountMax > 0) + int versionCountMax = 0; //<= 0 := no limit +}; + + +inline +bool operator==(const SyncConfig& lhs, const SyncConfig& rhs) +{ + return lhs.directionCfg == rhs.directionCfg && + lhs.deletionVariant == rhs.deletionVariant && //!= DeletionVariant::versioning => still consider versioningFolderPhrase: e.g. user temporarily + lhs.versioningFolderPhrase == rhs.versioningFolderPhrase && //switched to "permanent" deletion and accidentally saved cfg => versioning folder can be restored + lhs.versioningStyle == rhs.versioningStyle && + (lhs.versioningStyle == VersioningStyle::replace || + ( + lhs.versionMaxAgeDays == rhs.versionMaxAgeDays && + (lhs.versionMaxAgeDays <= 0 || + lhs.versionCountMin == rhs.versionCountMin) && + lhs.versionCountMax == rhs.versionCountMax + )); + //adapt effectivelyEqual() on changes, too! +} + + +inline +bool effectivelyEqual(const SyncConfig& lhs, const SyncConfig& rhs) +{ + return effectivelyEqual(lhs.directionCfg, rhs.directionCfg) && + lhs.deletionVariant == rhs.deletionVariant && + (lhs.deletionVariant != DeletionVariant::versioning || //only evaluate versioning folder if required! + ( + lhs.versioningFolderPhrase == rhs.versioningFolderPhrase && + lhs.versioningStyle == rhs.versioningStyle && + (lhs.versioningStyle == VersioningStyle::replace || + ( + lhs.versionMaxAgeDays == rhs.versionMaxAgeDays && + (lhs.versionMaxAgeDays <= 0 || + lhs.versionCountMin == rhs.versionCountMin) && + lhs.versionCountMax == rhs.versionCountMax + )) + )); +} + + +enum class UnitSize +{ + none, + byte, + kb, + mb +}; + +enum class UnitTime +{ + none, + today, + thisMonth, + thisYear, + lastDays +}; + +struct FilterConfig +{ + /* Semantics of PathFilter: + 1. using it creates a NEW folder hierarchy! -> must be considered by variant! (fortunately it turns out, doing nothing already has perfect semantics :) + 2. it applies equally to both sides => it always matches either both sides or none! => can be used while traversing a single folder! */ + Zstring includeFilter = Zstr("*"); + Zstring excludeFilter; + + /* Semantics of SoftFilter: + 1. It potentially may match only one side => it MUST NOT be applied while traversing a single folder to avoid mismatches + 2. => it is applied after traversing and just marks rows, (NO deletions after comparison are allowed) + 3. => equivalent to a user temporarily (de-)selecting rows -> not relevant for variant! ;) */ + unsigned int timeSpan = 0; + UnitTime unitTimeSpan = UnitTime::none; + + uint64_t sizeMin = 0; + UnitSize unitSizeMin = UnitSize::none; + + uint64_t sizeMax = 0; + UnitSize unitSizeMax = UnitSize::none; + + bool operator==(const FilterConfig&) const = default; +}; + + +void resolveUnits(size_t timeSpan, UnitTime unitTimeSpan, + uint64_t sizeMin, UnitSize unitSizeMin, + uint64_t sizeMax, UnitSize unitSizeMax, + time_t& timeFrom, //unit: UTC time, seconds + uint64_t& sizeMinBy, //unit: bytes + uint64_t& sizeMaxBy); //unit: bytes + + +struct LocalPairConfig //enhanced folder pairs with (optional) alternate configuration +{ + Zstring folderPathPhraseLeft; //unresolved directory names as entered by user! + Zstring folderPathPhraseRight; // + + std::optional localCmpCfg; + std::optional localSyncCfg; + FilterConfig localFilter; + + bool operator==(const LocalPairConfig& rhs) const = default; +}; + + +enum class ResultsNotification +{ + always, + errorWarning, + errorOnly, +}; + + +enum class PostSyncCondition +{ + completion, + errors, + success +}; + + +struct MainConfiguration +{ + CompConfig cmpCfg; //global compare settings: may be overwritten by folder pair settings + SyncConfig syncCfg; //global synchronisation settings: may be overwritten by folder pair settings + FilterConfig globalFilter; //global filter settings: combined with folder pair settings + + LocalPairConfig firstPair; //there needs to be at least one pair! + std::vector additionalPairs; + + std::map deviceParallelOps; //should only include devices with >= 2 parallel ops + + bool ignoreErrors = false; //true: errors will still be logged + size_t autoRetryCount = 0; + std::chrono::seconds autoRetryDelay{5}; + + 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) + + std::string emailNotifyAddress; //optional + ResultsNotification emailNotifyCondition = ResultsNotification::always; + + bool operator==(const MainConfiguration&) const = default; +}; + + +size_t getDeviceParallelOps(const std::map& deviceParallelOps, const AfsDevice& afsDevice); +void setDeviceParallelOps( std::map& deviceParallelOps, const AfsDevice& afsDevice, size_t parallelOps); +size_t getDeviceParallelOps(const std::map& deviceParallelOps, const Zstring& folderPathPhrase); +void setDeviceParallelOps( std::map& deviceParallelOps, const Zstring& folderPathPhrase, size_t parallelOps); + + +std::optional getCommonCompVariant(const MainConfiguration& mainCfg); +std::optional getCommonSyncVariant(const MainConfiguration& mainCfg); + + +struct WarningDialogs +{ + bool warnFolderNotExisting = true; + bool warnFoldersDifferInCase = true; + bool warnDependentFolderPair = true; + bool warnDependentBaseFolders = true; + bool warnSignificantDifference = true; + bool warnNotEnoughDiskSpace = true; + bool warnUnresolvedConflicts = true; + bool warnRecyclerMissing = true; + bool warnDirectoryLockFailed = true; + bool warnVersioningFolderPartOfSync = true; + + bool operator==(const WarningDialogs&) const = default; +}; +} + +#endif //STRUCTURES_H_8210478915019450901745 diff --git a/FreeFileSync/Source/base/synchronization.cpp b/FreeFileSync/Source/base/synchronization.cpp new file mode 100644 index 0000000..751fb23 --- /dev/null +++ b/FreeFileSync/Source/base/synchronization.cpp @@ -0,0 +1,2994 @@ +// ***************************************************************************** +// * 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 "synchronization.h" +#include +#include +#include "algorithm.h" +#include "db_file.h" +#include "status_handler_impl.h" +#include "versioning.h" +#include "binary.h" +#include "../afs/concrete.h" +#include "../afs/native.h" + + #include //fsync + #include //open + +using namespace zen; +using namespace fff; + + +namespace +{ +const size_t CONFLICTS_PREVIEW_MAX = 25; //=> consider memory consumption, log file size, email size! + + +} + + +SyncStatistics::SyncStatistics(const FolderComparison& folderCmp) +{ + for (const BaseFolderPair& baseFolder : asRange(folderCmp)) + recurse(baseFolder); +} + + +SyncStatistics::SyncStatistics(const ContainerObject& conObj) +{ + recurse(conObj); +} + + +SyncStatistics::SyncStatistics(const FilePair& file) +{ + processFile(file); + ++rowsTotal_; +} + + +inline +void SyncStatistics::recurse(const ContainerObject& conObj) +{ + for (const FilePair& file : conObj.files()) + processFile(file); + for (const SymlinkPair& symlink : conObj.symlinks()) + processLink(symlink); + for (const FolderPair& folder : conObj.subfolders()) + processFolder(folder); + + rowsTotal_ += conObj.subfolders().size(); + rowsTotal_ += conObj.files ().size(); + rowsTotal_ += conObj.symlinks ().size(); +} + + +inline +void SyncStatistics::logConflict(const FileSystemObject& fsObj) +{ + if (conflictsPreview_.size() < CONFLICTS_PREVIEW_MAX) + { + const Zstring& relPathL = fsObj.getRelativePath(); + const Zstring& relPathR = fsObj.getRelativePath(); + + conflictsPreview_.push_back((getUnicodeNormalForm(relPathL) == getUnicodeNormalForm(relPathR) ? + utfTo(relPathL) : + utfTo(relPathL + Zstr('\n') + relPathR)) + L": " + fsObj.getSyncOpConflict()); + } +} + + +inline +void SyncStatistics::processFile(const FilePair& file) +{ + switch (file.getSyncOperation()) //evaluate comparison result and sync direction + { + case SO_CREATE_LEFT: + ++createLeft_; + bytesToProcess_ += static_cast(file.getFileSize()); + break; + + case SO_CREATE_RIGHT: + ++createRight_; + bytesToProcess_ += static_cast(file.getFileSize()); + break; + + case SO_DELETE_LEFT: + ++deleteLeft_; + break; + + case SO_DELETE_RIGHT: + ++deleteRight_; + break; + + case SO_MOVE_LEFT_TO: + ++updateLeft_; + break; + + case SO_MOVE_RIGHT_TO: + ++updateRight_; + break; + + case SO_MOVE_LEFT_FROM: //ignore; already counted + case SO_MOVE_RIGHT_FROM: //=> harmonize with FileView::applyActionFilter() + break; + + case SO_OVERWRITE_LEFT: + ++updateLeft_; + bytesToProcess_ += static_cast(file.getFileSize()); + break; + + case SO_OVERWRITE_RIGHT: + ++updateRight_; + bytesToProcess_ += static_cast(file.getFileSize()); + break; + + case SO_UNRESOLVED_CONFLICT: + ++conflictCount_; + logConflict(file); + break; + + case SO_RENAME_LEFT: + ++updateLeft_; + break; + + case SO_RENAME_RIGHT: + ++updateRight_; + break; + + case SO_DO_NOTHING: + case SO_EQUAL: + break; + } +} + + +inline +void SyncStatistics::processLink(const SymlinkPair& symlink) +{ + switch (symlink.getSyncOperation()) //evaluate comparison result and sync direction + { + case SO_CREATE_LEFT: + ++createLeft_; + break; + + case SO_CREATE_RIGHT: + ++createRight_; + break; + + case SO_DELETE_LEFT: + ++deleteLeft_; + break; + + case SO_DELETE_RIGHT: + ++deleteRight_; + break; + + case SO_OVERWRITE_LEFT: + case SO_RENAME_LEFT: + ++updateLeft_; + break; + + case SO_OVERWRITE_RIGHT: + case SO_RENAME_RIGHT: + ++updateRight_; + break; + + case SO_UNRESOLVED_CONFLICT: + ++conflictCount_; + logConflict(symlink); + break; + + case SO_MOVE_LEFT_FROM: + case SO_MOVE_RIGHT_FROM: + case SO_MOVE_LEFT_TO: + case SO_MOVE_RIGHT_TO: + assert(false); + break; + case SO_DO_NOTHING: + case SO_EQUAL: + break; + } +} + + +inline +void SyncStatistics::processFolder(const FolderPair& folder) +{ + switch (folder.getSyncOperation()) //evaluate comparison result and sync direction + { + case SO_CREATE_LEFT: + ++createLeft_; + break; + + case SO_CREATE_RIGHT: + ++createRight_; + break; + + case SO_DELETE_LEFT: //if deletion variant == versioning with user-defined directory existing on other volume, this results in a full copy + delete operation! + ++deleteLeft_; //however we cannot (reliably) anticipate this situation, fortunately statistics can be adapted during sync! + break; + + case SO_DELETE_RIGHT: + ++deleteRight_; + break; + + case SO_UNRESOLVED_CONFLICT: + ++conflictCount_; + logConflict(folder); + break; + + case SO_RENAME_LEFT: + ++updateLeft_; + break; + + case SO_RENAME_RIGHT: + ++updateRight_; + break; + + case SO_OVERWRITE_LEFT: + case SO_OVERWRITE_RIGHT: + case SO_MOVE_LEFT_FROM: + case SO_MOVE_RIGHT_FROM: + case SO_MOVE_LEFT_TO: + case SO_MOVE_RIGHT_TO: + assert(false); + [[fallthrough]]; + case SO_DO_NOTHING: + case SO_EQUAL: + break; + } + + recurse(folder); //since we model logical stats, we recurse, even if deletion variant is "recycler" or "versioning + same volume", which is a single physical operation! +} + + +/* DeletionVariant::permanent: deletion frees space + DeletionVariant::recycler: won't free space until recycler is full, but then frees space + DeletionVariant::versioning: depends on whether versioning folder is on a different volume + -> if deleted item is a followed symlink, no space is freed + -> created/updated/deleted item may be on a different volume than base directory: consider symlinks, junctions! + + => generally assume deletion frees space; may avoid false-positive disk space warnings for recycler and versioning */ +class MinimumDiskSpaceNeeded +{ +public: + static std::pair calculate(const BaseFolderPair& baseFolder) + { + MinimumDiskSpaceNeeded inst; + inst.recurse(baseFolder); + return {inst.spaceNeededLeft_, inst.spaceNeededRight_}; + } + +private: + void recurse(const ContainerObject& conObj) + { + //process files + for (const FilePair& file : conObj.files()) + switch (file.getSyncOperation()) //evaluate comparison result and sync direction + { + case SO_CREATE_LEFT: + spaceNeededLeft_ += static_cast(file.getFileSize()); + break; + + case SO_CREATE_RIGHT: + spaceNeededRight_ += static_cast(file.getFileSize()); + break; + + case SO_DELETE_LEFT: + if (!file.isFollowedSymlink()) + spaceNeededLeft_ -= static_cast(file.getFileSize()); + break; + + case SO_DELETE_RIGHT: + if (!file.isFollowedSymlink()) + spaceNeededRight_ -= static_cast(file.getFileSize()); + break; + + case SO_OVERWRITE_LEFT: + if (!file.isFollowedSymlink()) + spaceNeededLeft_ -= static_cast(file.getFileSize()); + spaceNeededLeft_ += static_cast(file.getFileSize()); + break; + + case SO_OVERWRITE_RIGHT: + if (!file.isFollowedSymlink()) + spaceNeededRight_ -= static_cast(file.getFileSize()); + spaceNeededRight_ += static_cast(file.getFileSize()); + break; + + case SO_DO_NOTHING: + case SO_EQUAL: + case SO_UNRESOLVED_CONFLICT: + case SO_RENAME_LEFT: + case SO_RENAME_RIGHT: + case SO_MOVE_LEFT_FROM: + case SO_MOVE_RIGHT_FROM: + case SO_MOVE_LEFT_TO: + case SO_MOVE_RIGHT_TO: + break; + } + + //symbolic links + //[...] + + //recurse into sub-dirs + for (const FolderPair& folder : conObj.subfolders()) + switch (folder.getSyncOperation()) + { + case SO_DELETE_LEFT: + if (!folder.isFollowedSymlink()) + recurse(folder); //not 100% correct: in fact more that what our model contains may be deleted (consider file filter!) + break; + case SO_DELETE_RIGHT: + if (!folder.isFollowedSymlink()) + recurse(folder); + break; + + case SO_OVERWRITE_LEFT: + case SO_OVERWRITE_RIGHT: + case SO_MOVE_LEFT_FROM: + case SO_MOVE_RIGHT_FROM: + case SO_MOVE_LEFT_TO: + case SO_MOVE_RIGHT_TO: + assert(false); + [[fallthrough]]; + case SO_CREATE_LEFT: + case SO_CREATE_RIGHT: + case SO_RENAME_LEFT: + case SO_RENAME_RIGHT: + case SO_DO_NOTHING: + case SO_EQUAL: + case SO_UNRESOLVED_CONFLICT: + recurse(folder); //not 100% correct: what if left or right folder is symlink!? => file operations may happen on different volume! + break; + } + } + + int64_t spaceNeededLeft_ = 0; + int64_t spaceNeededRight_ = 0; +}; + +//----------------------------------------------------------------------------------------------------------- + +std::vector fff::extractSyncCfg(const MainConfiguration& mainCfg) +{ + //merge first and additional pairs + std::vector localCfgs = {mainCfg.firstPair}; + append(localCfgs, mainCfg.additionalPairs); + + std::vector output; + + for (const LocalPairConfig& lpc : localCfgs) + { + //const CompConfig cmpCfg = lpc.localCmpCfg ? *lpc.localCmpCfg : mainCfg.cmpCfg; + const SyncConfig syncCfg = lpc.localSyncCfg ? *lpc.localSyncCfg : mainCfg.syncCfg; + + output.push_back( + { + getSyncVariant(syncCfg.directionCfg), + !!std::get_if(&syncCfg.directionCfg.dirs), + syncCfg.deletionVariant, + syncCfg.versioningFolderPhrase, + syncCfg.versioningStyle, + syncCfg.versionMaxAgeDays, + syncCfg.versionCountMin, + syncCfg.versionCountMax + }); + } + return output; +} + +//------------------------------------------------------------------------------------------------------------ + +namespace +{ +//test if user accidentally selected the wrong folders to sync +bool significantDifferenceDetected(const SyncStatistics& folderPairStat) +{ + //initial file copying shall not be detected as major difference + if ((folderPairStat.createCount() == 0 || + folderPairStat.createCount() == 0) && + folderPairStat.updateCount () == 0 && + folderPairStat.deleteCount () == 0 && + folderPairStat.conflictCount() == 0) + return false; + + const int nonMatchingRows = folderPairStat.createCount() + + folderPairStat.deleteCount(); + //folderPairStat.updateCount() + -> not relevant when testing for "wrong folder selected" + //folderPairStat.conflictCount(); + + return nonMatchingRows >= 10 && nonMatchingRows > 0.5 * folderPairStat.rowCount(); +} + +//--------------------------------------------------------------------------------------------- + +template +bool plannedWriteAccess(const FileSystemObject& fsObj) +{ + switch (getEffectiveSyncDir(fsObj.getSyncOperation())) + { + case SyncDirection::none: return false; + case SyncDirection::left: return side == SelectSide::left; + case SyncDirection::right: return side == SelectSide::right; + } + throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); +} + + +inline +AbstractPath getAbstractPath(const FileSystemObject& fsObj, SelectSide side) +{ + return side == SelectSide::left ? fsObj.getAbstractPath() : fsObj.getAbstractPath(); +} + + +struct PathRaceItem +{ + const FileSystemObject* fsObj; + SelectSide side; + + std::strong_ordering operator<=>(const PathRaceItem&) const = default; +}; + + +std::weak_ordering comparePathNoCase(const PathRaceItem& lhs, const PathRaceItem& rhs) +{ + const AbstractPath& itemPathL = getAbstractPath(*lhs.fsObj, lhs.side); + const AbstractPath& itemPathR = getAbstractPath(*rhs.fsObj, rhs.side); + + if (const std::weak_ordering cmp = itemPathL.afsDevice <=> itemPathR.afsDevice; + cmp != std::weak_ordering::equivalent) + return cmp; + + return compareNoCase(itemPathL.afsPath.value, //no hashing: want natural sort order! + itemPathR.afsPath.value); +} + + +std::wstring formatRaceItem(const PathRaceItem& item) +{ + const SyncDirection syncDir = getEffectiveSyncDir(item.fsObj->getSyncOperation()); + + const bool writeAcess = (syncDir == SyncDirection::left && item.side == SelectSide::left) || + (syncDir == SyncDirection::right && item.side == SelectSide::right); + + return AFS::getDisplayPath(item.side == SelectSide::left ? + item.fsObj->base().getAbstractPath() : + item.fsObj->base().getAbstractPath()) + + (writeAcess ? L" 💾 " : L" 👓 ") + + utfTo(item.side == SelectSide::left ? + item.fsObj->getRelativePath() : + item.fsObj->getRelativePath()); + //e.g. D:\folder 💾 subfolder\file.txt + // D:\folder\subfolder 👓 file.txt +} + + +struct ChildPathRef +{ + const FileSystemObject* fsObj = nullptr; + uint64_t afsPathHash = 0; //of *case-normalized* AfsPath +}; + + +template +class GetChildItemsHashed +{ +public: + static std::vector execute(const ContainerObject& folder) + { + FNV1aHash pathHash; + for (const Zstring& itemName : splitCpy(folder.getAbstractPath().afsPath.value, FILE_NAME_SEPARATOR, SplitOnEmpty::skip)) + hashAdd(pathHash, itemName); //not really needed ATM, but it's cleaner to hash *full* afsPath + + GetChildItemsHashed inst; + inst.recurse(folder, pathHash.get()); + return std::move(inst.childPathRefs_); + } + +private: + GetChildItemsHashed (const GetChildItemsHashed&) = delete; + GetChildItemsHashed& operator=(const GetChildItemsHashed&) = delete; + + GetChildItemsHashed() {} + + void recurse(const ContainerObject& conObj, uint64_t parentPathHash) + { + for (const FilePair& file : conObj.files()) + childPathRefs_.push_back({&file, getPathHash(file, parentPathHash)}); + //S1 -> T (update) is not a conflict (anymore) if S1, S2 contain different files + //S2 -> T (update) https://freefilesync.org/forum/viewtopic.php?t=9365#p36466 + for (const SymlinkPair& symlink : conObj.symlinks()) + childPathRefs_.push_back({&symlink, getPathHash(symlink, parentPathHash)}); + + for (const FolderPair& subFolder : conObj.subfolders()) + { + const uint64_t folderPathHash = getPathHash(subFolder, parentPathHash); + + childPathRefs_.push_back({&subFolder, folderPathHash}); + + recurse(subFolder, folderPathHash); + } + } + + static void hashAdd(FNV1aHash& hash, const Zstring& itemName) + { + if (isAsciiString(itemName)) //fast path: no need for extra memory allocation! + for (const Zchar c : itemName) + hash.add(asciiToUpper(c)); + else + for (const Zchar c : getUpperCase(itemName)) + hash.add(c); + } + + static uint64_t getPathHash(const FileSystemObject& fsObj, uint64_t parentPathHash) + { + FNV1aHash hash(parentPathHash); + hashAdd(hash, fsObj.getItemName()); + return hash.get(); + } + + std::vector childPathRefs_; +}; + + +template +std::weak_ordering comparePathNoCase(const ChildPathRef& lhs, const ChildPathRef& rhs) +{ + //assert(lhs.fsObj->getAbstractPath().afsDevice == -> too slow, even for debug build + // rhs.fsObj->getAbstractPath().afsDevice); + + if (const std::weak_ordering cmp = lhs.afsPathHash <=> rhs.afsPathHash; + cmp != std::weak_ordering::equivalent) + return cmp; //fast path! + + return compareNoCase(lhs.fsObj->getAbstractPath().afsPath.value, //fsObj may come from *different* BaseFolderPair + rhs.fsObj->getAbstractPath().afsPath.value); // => don't compare getRelativePath()! +} + + +template +void sortAndRemoveDuplicates(std::vector& pathRefs) +{ + std::sort(pathRefs.begin(), pathRefs.end(), [](const ChildPathRef& lhs, const ChildPathRef& rhs) + { + if (const std::weak_ordering cmp = comparePathNoCase(lhs, rhs); + cmp != std::weak_ordering::equivalent) + return cmp < 0; + + return //multiple (case-insensitive) relPaths? => order write-access before read-access, so that std::unique leaves a write if existing! + plannedWriteAccess(*lhs.fsObj) > + plannedWriteAccess(*rhs.fsObj); + }); + + pathRefs.erase(std::unique(pathRefs.begin(), pathRefs.end(), + [](const ChildPathRef& lhs, const ChildPathRef& rhs) { return comparePathNoCase(lhs, rhs) == std::weak_ordering::equivalent; }), + pathRefs.end()); + + //let's not use removeDuplicates(): we rely too much on implementation details! +} + + +//check if some files/folders are included more than once and form a race condition (:= multiple accesses of which at least one is a write) +// - checking filter for subfolder exclusion is not good enough: one folder may have a *.txt include-filter, the other a *.lng include filter => still no dependencies +// - user may have manually excluded the conflicting items or changed the filter settings without running a re-compare +template +void checkPathRaceCondition(const BaseFolderPair& baseFolderP, const BaseFolderPair& baseFolderC, std::vector& pathRaceItems) +{ + const AbstractPath basePathP = baseFolderP.getAbstractPath(); //parent/child notion is tentative at this point + const AbstractPath basePathC = baseFolderC.getAbstractPath(); //=> will be swapped if necessary + + assert(!AFS::isNullPath(basePathP) && !AFS::isNullPath(basePathC)); + if (basePathP.afsDevice == basePathC.afsDevice) + { + if (basePathP.afsPath.value.size() > basePathC.afsPath.value.size()) + return checkPathRaceCondition(baseFolderC, baseFolderP, pathRaceItems); + + const std::vector relPathP = splitCpy(basePathP.afsPath.value, FILE_NAME_SEPARATOR, SplitOnEmpty::skip); + const std::vector relPathC = splitCpy(basePathC.afsPath.value, FILE_NAME_SEPARATOR, SplitOnEmpty::skip); + + if (relPathP.size() <= relPathC.size() && + /**/std::equal(relPathP.begin(), relPathP.end(), relPathC.begin(), [](const Zstring& lhs, const Zstring& rhs) { return equalNoCase(lhs, rhs); })) + { + //=> at this point parent/child folders are confirmed + //now find child folder match inside baseFolderP + //e.g. C:\folder <-> C:\folder\sub => find "sub" inside C:\folder + std::vector childFolderP{&baseFolderP}; + + std::for_each(relPathC.begin() + relPathP.size(), relPathC.end(), [&](const Zstring& itemName) + { + std::vector childFolderP2; + + for (const ContainerObject* childFolder : childFolderP) + for (const FolderPair& folder : childFolder->subfolders()) + if (equalNoCase(folder.getItemName(), itemName)) + childFolderP2.push_back(&folder); + //no "break": yes, weird, but there could be more than one (for case-sensitive file system) + + childFolderP = std::move(childFolderP2); + }); + + std::vector pathRefsP; + for (const ContainerObject* childFolder : childFolderP) + append(pathRefsP, GetChildItemsHashed::execute(*childFolder)); + + std::vector pathRefsC = GetChildItemsHashed::execute(baseFolderC); + + //--------------------------------------------------------------------------------------------------- + //case-sensitive comparison because items were scanned by FFS (=> no messy user input)? + //not good enough! E.g. not-yet-existing files are set to be created with different case! + // + (weird) a file and a folder are set to be created with same name + // => (throw hands in the air) fine, check path only and don't consider case + sortAndRemoveDuplicates(pathRefsP); + sortAndRemoveDuplicates(pathRefsC); + + mergeTraversal(pathRefsP.begin(), pathRefsP.end(), + pathRefsC.begin(), pathRefsC.end(), + [](const ChildPathRef&) {} /*left only*/, + [&](const ChildPathRef& lhs, const ChildPathRef& rhs) + { + if (plannedWriteAccess(*lhs.fsObj) || + plannedWriteAccess(*rhs.fsObj)) + { + pathRaceItems.push_back({lhs.fsObj, sideP}); + pathRaceItems.push_back({rhs.fsObj, sideC}); + } + }, + [](const ChildPathRef&) {} /*right only*/, comparePathNoCase); + } + } +} + +//################################################################################################################# + +//--------------------- data verification ------------------------- +void flushFileBuffers(const Zstring& nativeFilePath) //throw FileError +{ + const int fdFile = ::open(nativeFilePath.c_str(), O_WRONLY | O_APPEND | O_CLOEXEC); + if (fdFile == -1) + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot open file %x."), L"%x", fmtPath(nativeFilePath)), "open"); + ZEN_ON_SCOPE_EXIT(::close(fdFile)); + + if (::fsync(fdFile) != 0) + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(nativeFilePath)), "fsync"); +} + + +void verifyFiles(const AbstractPath& sourcePath, const AbstractPath& targetPath, const IoCallback& notifyUnbufferedIO /*throw X*/) //throw FileError, X +{ + try + { + //do like "copy /v": 1. flush target file buffers, 2. read again as usual (using OS buffers) + // => it seems OS buffers are not invalidated by this: snake oil??? + if (const Zstring& targetPathNative = getNativeItemPath(targetPath); + !targetPathNative.empty()) + flushFileBuffers(targetPathNative); //throw FileError + + if (!filesHaveSameContent(sourcePath, targetPath, notifyUnbufferedIO)) //throw FileError, X + throw FileError(replaceCpy(replaceCpy(_("%x and %y have different content."), + L"%x", L'\n' + fmtPath(AFS::getDisplayPath(sourcePath))), + L"%y", L'\n' + fmtPath(AFS::getDisplayPath(targetPath)))); + } + catch (const FileError& e) //add some context to error message + { + throw FileError(_("Data verification error:"), e.toString()); + } +} + +//################################################################################################################# +//################################################################################################################# + +/* ________________________________________________________________ + | | + | Multithreaded File Copy: Parallel API for expensive file I/O | + |______________________________________________________________| */ + +namespace parallel +{ +inline +AFS::ItemType getItemType(const AbstractPath& itemPath, std::mutex& singleThread) //throw FileError +{ return parallelScope([itemPath] { return AFS::getItemType(itemPath); /*throw FileError*/ }, singleThread); } + +inline +bool itemExists(const AbstractPath& itemPath, std::mutex& singleThread) //throw FileError +{ return parallelScope([itemPath] { return AFS::itemExists(itemPath); /*throw FileError*/ }, singleThread); } + +inline +void removeFileIfExists(const AbstractPath& filePath, std::mutex& singleThread) //throw FileError +{ parallelScope([filePath] { AFS::removeFileIfExists(filePath); /*throw FileError*/ }, singleThread); } + +inline +void removeSymlinkIfExists(const AbstractPath& linkPath, std::mutex& singleThread) //throw FileError +{ parallelScope([linkPath] { AFS::removeSymlinkIfExists(linkPath); /*throw FileError*/ }, singleThread); } + +inline +void moveAndRenameItem(const AbstractPath& pathFrom, const AbstractPath& pathTo, std::mutex& singleThread) //throw FileError, ErrorMoveUnsupported +{ parallelScope([pathFrom, pathTo] { AFS::moveAndRenameItem(pathFrom, pathTo); /*throw FileError, ErrorMoveUnsupported*/ }, singleThread); } + +inline +AbstractPath getSymlinkResolvedPath(const AbstractPath& linkPath, std::mutex& singleThread) //throw FileError +{ return parallelScope([linkPath] { return AFS::getSymlinkResolvedPath(linkPath); /*throw FileError*/ }, singleThread); } + +inline +void copySymlink(const AbstractPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions, std::mutex& singleThread) //throw FileError +{ parallelScope([sourcePath, targetPath, copyFilePermissions] { AFS::copySymlink(sourcePath, targetPath, copyFilePermissions); /*throw FileError*/ }, singleThread); } + +inline +void copyNewFolder(const AbstractPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions, std::mutex& singleThread) //throw FileError +{ parallelScope([sourcePath, targetPath, copyFilePermissions] { return AFS::copyNewFolder(sourcePath, targetPath, copyFilePermissions); /*throw FileError*/ }, singleThread); } + +inline +void removeFilePlain(const AbstractPath& filePath, std::mutex& singleThread) //throw FileError +{ parallelScope([filePath] { AFS::removeFilePlain(filePath); /*throw FileError*/ }, singleThread); } + +//-------------------------------------------------------------- +//ATTENTION CALLBACKS: they also run asynchronously *outside* the singleThread lock! +//-------------------------------------------------------------- +inline +void removeFolderIfExistsRecursion(const AbstractPath& folderPath, //throw FileError + const std::function& onBeforeFileDeletion /*throw X*/, // + const std::function& onBeforeSymlinkDeletion /*throw X*/, //optional + const std::function& onBeforeFolderDeletion /*throw X*/, // + std::mutex& singleThread) +{ + parallelScope([folderPath, onBeforeFileDeletion, onBeforeSymlinkDeletion, onBeforeFolderDeletion] + { AFS::removeFolderIfExistsRecursion(folderPath, onBeforeFileDeletion, onBeforeSymlinkDeletion, onBeforeFolderDeletion); /*throw FileError*/ }, singleThread); +} + + +inline +AFS::FileCopyResult copyFileTransactional(const AbstractPath& sourcePath, const AFS::StreamAttributes& attrSource, //throw FileError, ErrorFileLocked, X + const AbstractPath& targetPath, + bool copyFilePermissions, + bool transactionalCopy, + const std::function& onDeleteTargetFile /*throw X*/, + const IoCallback& notifyUnbufferedIO /*throw X*/, + std::mutex& singleThread) +{ + return parallelScope([=] + { + return AFS::copyFileTransactional(sourcePath, attrSource, targetPath, copyFilePermissions, transactionalCopy, onDeleteTargetFile, notifyUnbufferedIO); //throw FileError, ErrorFileLocked, X + }, singleThread); +} + +inline //RecycleSession::moveToRecycleBin() is internally synchronized! +void moveToRecycleBinIfExists(AFS::RecycleSession& recyclerSession, const AbstractPath& itemPath, const Zstring& logicalRelPath, std::mutex& singleThread) //throw FileError, RecycleBinUnavailable +{ parallelScope([=, &recyclerSession] { return recyclerSession.moveToRecycleBinIfExists(itemPath, logicalRelPath); /*throw FileError, RecycleBinUnavailable*/ }, singleThread); } + +inline //FileVersioner::revisionFile() is internally synchronized! +void revisionFile(FileVersioner& versioner, const FileDescriptor& fileDescr, const Zstring& relativePath, const IoCallback& notifyUnbufferedIO /*throw X*/, std::mutex& singleThread) //throw FileError, X +{ parallelScope([=, &versioner] { versioner.revisionFile(fileDescr, relativePath, notifyUnbufferedIO); /*throw FileError, X*/ }, singleThread); } + +inline //FileVersioner::revisionSymlink() is internally synchronized! +void revisionSymlink(FileVersioner& versioner, const AbstractPath& linkPath, const Zstring& relativePath, std::mutex& singleThread) //throw FileError +{ parallelScope([=, &versioner] { versioner.revisionSymlink(linkPath, relativePath); /*throw FileError*/ }, singleThread); } + +inline //FileVersioner::revisionFolder() is internally synchronized! +void revisionFolder(FileVersioner& versioner, + const AbstractPath& folderPath, const Zstring& relativePath, + const std::function& onBeforeFileMove /*throw X*/, + const std::function& onBeforeFolderMove /*throw X*/, + const IoCallback& notifyUnbufferedIO /*throw X*/, + std::mutex& singleThread) //throw FileError, X +{ parallelScope([=, &versioner] { versioner.revisionFolder(folderPath, relativePath, onBeforeFileMove, onBeforeFolderMove, notifyUnbufferedIO); /*throw FileError, X*/ }, singleThread); } + +inline +void verifyFiles(const AbstractPath& sourcePath, const AbstractPath& targetPath, const IoCallback& notifyUnbufferedIO /*throw X*/, std::mutex& singleThread) //throw FileError, X +{ parallelScope([=] { ::verifyFiles(sourcePath, targetPath, notifyUnbufferedIO); /*throw FileError, X*/ }, singleThread); } + +} + +//################################################################################################################# +//################################################################################################################# + +class DeletionHandler //abstract deletion variants: permanently, recycle bin, user-defined directory +{ +public: + DeletionHandler(const AbstractPath& baseFolderPath, + bool& recyclerMissingReportOnce, + bool& warnRecyclerMissing, + DeletionVariant deletionVariant, + const AbstractPath& versioningFolderPath, + VersioningStyle versioningStyle, + time_t syncStartTime); //nothrow! + + //clean-up temporary directory (recycle bin optimization) + void tryCleanup(PhaseCallback& cb /*throw X*/); //throw X + + void removeFileWithCallback(const FileDescriptor& fileDescr, const Zstring& relPath, bool beforeOverwrite, AsyncItemStatReporter& statReporter, std::mutex& singleThread); //throw FileError, ThreadStopRequest + void removeLinkWithCallback(const AbstractPath& linkPath, const Zstring& relPath, bool beforeOverwrite, AsyncItemStatReporter& statReporter, std::mutex& singleThread); // + void removeDirWithCallback (const AbstractPath& dirPath, const Zstring& relPath, AsyncItemStatReporter& statReporter, std::mutex& singleThread); // + +private: + DeletionHandler (const DeletionHandler&) = delete; + DeletionHandler& operator=(const DeletionHandler&) = delete; + + void moveToRecycleBinIfExists(const AbstractPath& itemPath, const Zstring& relPath, std::mutex& singleThread) //throw FileError, RecycleBinUnavailable + { + assert(deletionVariant_ == DeletionVariant::recycler); + + //might not be needed => create lazily: + if (!recyclerSession_ && !recyclerUnavailableExcept_) + try + { + recyclerSession_ = AFS::createRecyclerSession(baseFolderPath_); //throw FileError, RecycleBinUnavailable + //double-initialization caveat: do NOT run session initialization in parallel! + // => createRecyclerSession must *not* do file I/O! + } + catch (const RecycleBinUnavailable& e) { recyclerUnavailableExcept_ = e; } + + if (recyclerUnavailableExcept_) //add context, or user might think we're removing baseFolderPath_! + throw RecycleBinUnavailable(replaceCpy(_("Unable to move %x to the recycle bin."), L"%x", fmtPath(AFS::getDisplayPath(itemPath))), + replaceCpy(recyclerUnavailableExcept_->toString(), L"\n\n", L'\n')); + /* "Unable to move "Z:\folder\file.txt" to the recycle bin. + + The recycle bin is not available for "Z:\". + + Ignore and delete permanently each time recycle bin is unavailable?" */ + + parallel::moveToRecycleBinIfExists(*recyclerSession_, itemPath, relPath, singleThread); //throw FileError, RecycleBinUnavailable + } + + //might not be needed => create lazily: + FileVersioner& getOrCreateVersioner() //throw FileError + { + assert(deletionVariant_ == DeletionVariant::versioning); + if (!versioner_) + versioner_.emplace(versioningFolderPath_, versioningStyle_, syncStartTime_); //throw FileError + return *versioner_; + } + + bool& recyclerMissingReportOnce_; //shared by threads! access under "singleThread" lock! + bool& warnRecyclerMissing_; //WarningDialogs::warnRecyclerMissing + + const DeletionVariant deletionVariant_; //keep it invariant! e.g. consider getOrCreateVersioner() one-time construction! + + const AbstractPath baseFolderPath_; + + std::unique_ptr recyclerSession_; //it's one of these (or none if not yet initialized) + std::optional recyclerUnavailableExcept_; // + + //used only for DeletionVariant::versioning: + const AbstractPath versioningFolderPath_; + const VersioningStyle versioningStyle_; + const time_t syncStartTime_; + std::optional versioner_; + + //buffer status texts: + const std::wstring txtDelFilePermanent_ = _("Deleting file %x"); + const std::wstring txtDelFileRecycler_ = _("Moving file %x to the recycle bin"); + const std::wstring txtDelFileVersioning_ = replaceCpy(_("Moving file %x to %y"), L"%y", fmtPath(AFS::getDisplayPath(versioningFolderPath_))); + + const std::wstring txtDelSymlinkPermanent_ = _("Deleting symbolic link %x"); + const std::wstring txtDelSymlinkRecycler_ = _("Moving symbolic link %x to the recycle bin"); + const std::wstring txtDelSymlinkVersioning_ = replaceCpy(_("Moving symbolic link %x to %y"), L"%y", fmtPath(AFS::getDisplayPath(versioningFolderPath_))); + + const std::wstring txtDelFolderPermanent_ = _("Deleting folder %x"); + const std::wstring txtDelFolderRecycler_ = _("Moving folder %x to the recycle bin"); + const std::wstring txtDelFolderVersioning_ = replaceCpy(_("Moving folder %x to %y"), L"%y", fmtPath(AFS::getDisplayPath(versioningFolderPath_))); + + const std::wstring txtMovingFileXtoY_ = _("Moving file %x to %y"); + const std::wstring txtMovingFolderXtoY_ = _("Moving folder %x to %y"); +}; + + +DeletionHandler::DeletionHandler(const AbstractPath& baseFolderPath, + bool& recyclerMissingReportOnce, + bool& warnRecyclerMissing, + DeletionVariant deletionVariant, + const AbstractPath& versioningFolderPath, + VersioningStyle versioningStyle, + time_t syncStartTime) : + recyclerMissingReportOnce_(recyclerMissingReportOnce), + warnRecyclerMissing_(warnRecyclerMissing), + deletionVariant_(deletionVariant), + baseFolderPath_(baseFolderPath), + versioningFolderPath_(versioningFolderPath), + versioningStyle_(versioningStyle), + syncStartTime_(syncStartTime) {} + + +void DeletionHandler::tryCleanup(PhaseCallback& cb /*throw X*/) //throw X +{ + assert(runningOnMainThread()); + switch (deletionVariant_) + { + case DeletionVariant::recycler: + if (recyclerSession_) + { + auto notifyDeletionStatus = [&](const std::wstring& displayPath) + { + if (!displayPath.empty()) + cb.updateStatus(replaceCpy(txtDelFileRecycler_, L"%x", fmtPath(displayPath))); //throw X + else + cb.requestUiUpdate(); //throw X + }; + //move content of temporary directory to recycle bin in one go + tryReportingError([&] { recyclerSession_->tryCleanup(notifyDeletionStatus); /*throw FileError*/}, cb); //throw X + } + break; + + case DeletionVariant::permanent: + case DeletionVariant::versioning: + break; + } +} + + +void DeletionHandler::removeFileWithCallback(const FileDescriptor& fileDescr, const Zstring& relPath, bool beforeOverwrite, + AsyncItemStatReporter& statReporter, std::mutex& singleThread) //throw FileError, ThreadStopRequest +{ + if (deletionVariant_ != DeletionVariant::permanent && + endsWith(relPath, AFS::TEMP_FILE_ENDING)) //special rule: always delete .ffs_tmp files permanently! + { + if (!beforeOverwrite) reportInfo(replaceCpy(txtDelFilePermanent_, L"%x", fmtPath(AFS::getDisplayPath(fileDescr.path))), statReporter); //throw ThreadStopRequest + parallel::removeFileIfExists(fileDescr.path, singleThread); //throw FileError + } + else + /* don't use AsyncItemStatReporter if "beforeOverwrite": + - logInfo/updateStatus() is superfluous/confuses user, except: do show progress and allow cancel for versioning! + - no (logical) item count update desired + => BUT: total byte count should still be adjusted if versioning requires a file copy instead of a move! + - if fail-safe file copy is active, then the next operation will be a simple "rename" + => don't risk updateStatus() throwing ThreadStopRequest() leaving the target deleted rather than updated! */ + switch (deletionVariant_) + { + case DeletionVariant::permanent: + if (!beforeOverwrite) reportInfo(replaceCpy(txtDelFilePermanent_, L"%x", fmtPath(AFS::getDisplayPath(fileDescr.path))), statReporter); //throw ThreadStopRequest + parallel::removeFileIfExists(fileDescr.path, singleThread); //throw FileError + break; + + case DeletionVariant::recycler: + if (!beforeOverwrite) reportInfo(replaceCpy(txtDelFileRecycler_, L"%x", fmtPath(AFS::getDisplayPath(fileDescr.path))), statReporter); //throw ThreadStopRequest + try + { + moveToRecycleBinIfExists(fileDescr.path, relPath, singleThread); //throw FileError, RecycleBinUnavailable + } + catch (const RecycleBinUnavailable& e) + { + if (!recyclerMissingReportOnce_) //shared by threads! access under "singleThread" lock! + { + recyclerMissingReportOnce_ = true; + statReporter.reportWarning(e.toString() + L"\n\n" + _("Ignore and delete permanently each time recycle bin is unavailable?"), warnRecyclerMissing_); //throw ThreadStopRequest + } + if (!beforeOverwrite) statReporter.logMessage(replaceCpy(txtDelFilePermanent_, L"%x", fmtPath(AFS::getDisplayPath(fileDescr.path))) + + L" [" + _("Recycle bin unavailable") + L']', PhaseCallback::MsgType::warning); //throw ThreadStopRequest + parallel::removeFileIfExists(fileDescr.path, singleThread); //throw FileError + } + break; + + case DeletionVariant::versioning: + { + std::wstring statusMsg = replaceCpy(txtDelFileVersioning_, L"%x", fmtPath(AFS::getDisplayPath(fileDescr.path))); + PercentStatReporter percentReporter(statusMsg, fileDescr.attr.fileSize, statReporter); + + if (!beforeOverwrite) reportInfo(std::move(statusMsg), statReporter); //throw ThreadStopRequest + //else: 1. versioning is moving only: no (potentially throwing) status updates + // 2. versioning needs to copy: may throw ThreadStopRequest, but *no* status updates, unless copying takes so long that % needs to be displayed + + //callback runs *outside* singleThread_ lock! => fine + IoCallback notifyUnbufferedIO = [&](int64_t bytesDelta) + { + percentReporter.updateDeltaAndStatus(bytesDelta); //throw ThreadStopRequest + interruptionPoint(); //throw ThreadStopRequest => not reliably covered by PercentStatReporter::updateDeltaAndStatus()! + }; + parallel::revisionFile(getOrCreateVersioner(), fileDescr, relPath, notifyUnbufferedIO, singleThread); //throw FileError, ThreadStopRequest + } + break; + } + + //even if the source item did not exist anymore, significant I/O work was done => report unconditionally + if (!beforeOverwrite) statReporter.reportDelta(1, 0); +} + + +void DeletionHandler::removeLinkWithCallback(const AbstractPath& linkPath, const Zstring& relPath, bool beforeOverwrite, + AsyncItemStatReporter& statReporter, std::mutex& singleThread) //throw FileError, throw ThreadStopRequest +{ + /* don't use AsyncItemStatReporter if "beforeOverwrite": + - logInfo() is superfluous/confuses user + - no (logical) item count update desired + - don't risk updateStatus() throwing ThreadStopRequest() leaving the target deleted rather than updated! */ + switch (deletionVariant_) + { + case DeletionVariant::permanent: + if (!beforeOverwrite) reportInfo(replaceCpy(txtDelSymlinkPermanent_, L"%x", fmtPath(AFS::getDisplayPath(linkPath))), statReporter); //throw ThreadStopRequest + parallel::removeSymlinkIfExists(linkPath, singleThread); //throw FileError + break; + + case DeletionVariant::recycler: + if (!beforeOverwrite) reportInfo(replaceCpy(txtDelSymlinkRecycler_, L"%x", fmtPath(AFS::getDisplayPath(linkPath))), statReporter); //throw ThreadStopRequest + try + { + moveToRecycleBinIfExists(linkPath, relPath, singleThread); //throw FileError, RecycleBinUnavailable + } + catch (const RecycleBinUnavailable& e) + { + if (!recyclerMissingReportOnce_) //shared by threads! access under "singleThread" lock! + { + recyclerMissingReportOnce_ = true; + statReporter.reportWarning(e.toString() + L"\n\n" + _("Ignore and delete permanently each time recycle bin is unavailable?"), warnRecyclerMissing_); //throw ThreadStopRequest + } + if (!beforeOverwrite) statReporter.logMessage(replaceCpy(txtDelSymlinkPermanent_, L"%x", fmtPath(AFS::getDisplayPath(linkPath))) + + L" [" + _("Recycle bin unavailable") + L']', PhaseCallback::MsgType::warning); //throw ThreadStopRequest + parallel::removeSymlinkIfExists(linkPath, singleThread); //throw FileError + } + break; + + case DeletionVariant::versioning: + if (!beforeOverwrite) reportInfo(replaceCpy(txtDelSymlinkVersioning_, L"%x", fmtPath(AFS::getDisplayPath(linkPath))), statReporter); //throw ThreadStopRequest + parallel::revisionSymlink(getOrCreateVersioner(), linkPath, relPath, singleThread); //throw FileError + break; + } + //remain transactional as much as possible => no more callbacks that can throw after successful deletion! (next: update file model!) + + //even if the source item did not exist anymore, significant I/O work was done => report unconditionally + if (!beforeOverwrite) statReporter.reportDelta(1, 0); +} + + +void DeletionHandler::removeDirWithCallback(const AbstractPath& folderPath, const Zstring& relPath, + AsyncItemStatReporter& statReporter, std::mutex& singleThread) //throw FileError, ThreadStopRequest +{ + auto removeFolderPermanently = [&] + { + //callbacks run *outside* singleThread_ lock! => fine + auto onBeforeDeletion = [&statReporter](const std::wstring& statusText, const std::wstring& displayPath) + { + statReporter.updateStatus(replaceCpy(statusText, L"%x", fmtPath(displayPath))); //throw ThreadStopRequest + statReporter.reportDelta(1, 0); //it would be more correct to report *after* work was done! + }; + static_assert(std::is_const_v, "callbacks better be thread-safe!"); + + parallel::removeFolderIfExistsRecursion(folderPath, + [&](const std::wstring& displayPath) { onBeforeDeletion(txtDelFilePermanent_, displayPath); }, + [&](const std::wstring& displayPath) { onBeforeDeletion(txtDelSymlinkPermanent_, displayPath); }, + [&](const std::wstring& displayPath) { onBeforeDeletion(txtDelFolderPermanent_, displayPath); }, singleThread); //throw FileError, ThreadStopRequest + }; + + switch (deletionVariant_) + { + case DeletionVariant::permanent: + { + reportInfo(replaceCpy(txtDelFolderPermanent_, L"%x", fmtPath(AFS::getDisplayPath(folderPath))), statReporter); //throw ThreadStopRequest + removeFolderPermanently(); //throw FileError, ThreadStopRequest + } + break; + + case DeletionVariant::recycler: + reportInfo(replaceCpy(txtDelFolderRecycler_, L"%x", fmtPath(AFS::getDisplayPath(folderPath))), statReporter); //throw ThreadStopRequest + try + { + moveToRecycleBinIfExists(folderPath, relPath, singleThread); //throw FileError, RecycleBinUnavailable + statReporter.reportDelta(1, 0); //moving to recycler is ONE logical operation, irrespective of the number of child elements! + } + catch (const RecycleBinUnavailable& e) + { + if (!recyclerMissingReportOnce_) //shared by threads! access under "singleThread" lock! + { + recyclerMissingReportOnce_ = true; + statReporter.reportWarning(e.toString() + L"\n\n" + _("Ignore and delete permanently each time recycle bin is unavailable?"), warnRecyclerMissing_); //throw ThreadStopRequest + } + statReporter.logMessage(replaceCpy(txtDelFolderPermanent_, L"%x", fmtPath(AFS::getDisplayPath(folderPath))) + + L" [" + _("Recycle bin unavailable") + L']', PhaseCallback::MsgType::warning); //throw ThreadStopRequest + removeFolderPermanently(); //throw FileError, ThreadStopRequest + } + break; + + case DeletionVariant::versioning: + { + reportInfo(replaceCpy(txtDelFolderVersioning_, L"%x", fmtPath(AFS::getDisplayPath(folderPath))), statReporter); //throw ThreadStopRequest + + //callbacks run *outside* singleThread_ lock! => fine + auto notifyMove = [&statReporter](const std::wstring& statusText, const std::wstring& displayPathFrom, const std::wstring& displayPathTo) + { + statReporter.updateStatus(replaceCpy(replaceCpy(statusText, L"%x", L'\n' + fmtPath(displayPathFrom)), L"%y", L'\n' + fmtPath(displayPathTo))); //throw ThreadStopRequest + statReporter.reportDelta(1, 0); //it would be more correct to report *after* work was done! + }; + static_assert(std::is_const_v, "callbacks better be thread-safe!"); + auto onBeforeFileMove = [&](const std::wstring& displayPathFrom, const std::wstring& displayPathTo) { notifyMove(txtMovingFileXtoY_, displayPathFrom, displayPathTo); }; + auto onBeforeFolderMove = [&](const std::wstring& displayPathFrom, const std::wstring& displayPathTo) { notifyMove(txtMovingFolderXtoY_, displayPathFrom, displayPathTo); }; + auto notifyUnbufferedIO = [&](int64_t bytesDelta) { statReporter.reportDelta(0, bytesDelta); interruptionPoint(); }; //throw ThreadStopRequest + + parallel::revisionFolder(getOrCreateVersioner(), folderPath, relPath, onBeforeFileMove, onBeforeFolderMove, notifyUnbufferedIO, singleThread); //throw FileError, ThreadStopRequest + } + break; + } +} + +//=================================================================================================== +//=================================================================================================== + +class Workload +{ +public: + Workload(size_t threadCount, AsyncCallback& acb) : acb_(acb), workload_(threadCount) { assert(threadCount > 0); } + + using WorkItem = std::function; + using WorkItems = RingBuffer; //FIFO! + + //blocking call: context of worker thread + WorkItem getNext(size_t threadIdx) //throw ThreadStopRequest + { + interruptionPoint(); //throw ThreadStopRequest + + std::unique_lock dummy(lockWork_); + for (;;) + { + if (!workload_[threadIdx].empty()) + { + auto wi = std::move(workload_[threadIdx]. front()); + /**/ workload_[threadIdx].pop_front(); + return wi; + } + if (!pendingWorkload_.empty()) + { + workload_[threadIdx] = std::move(pendingWorkload_. front()); + /**/ pendingWorkload_.pop_front(); + assert(!workload_[threadIdx].empty()); + } + else + { + WorkItems& items = *std::max_element(workload_.begin(), workload_.end(), [](const WorkItems& lhs, const WorkItems& rhs) { return lhs.size() < rhs.size(); }); + if (!items.empty()) //=> != workload_[threadIdx] + { + //steal half of largest workload from other thread + const size_t sz = items.size(); //[!] changes during loop! + for (size_t i = 0; i < sz; ++i) + { + auto wi = std::move(items. front()); + /**/ items.pop_front(); + if (i % 2 == 0) + workload_[threadIdx].push_back(std::move(wi)); + else + items.push_back(std::move(wi)); + } + } + else //wait... + { + if (++idleThreads_ == workload_.size()) + acb_.notifyAllDone(); //noexcept + ZEN_ON_SCOPE_EXIT(--idleThreads_); + + auto haveNewWork = [&] { return !pendingWorkload_.empty() || std::any_of(workload_.begin(), workload_.end(), [](const WorkItems& wi) { return !wi.empty(); }); }; + + interruptibleWait(conditionNewWork_, dummy, [&] { return haveNewWork(); }); //throw ThreadStopRequest + //it's sufficient to notify condition in addWorkItems() only (as long as we use std::condition_variable::notify_all()) + } + } + } + } + + void addWorkItems(RingBuffer&& buckets) + { + { + std::lock_guard dummy(lockWork_); + while (!buckets.empty()) + { + pendingWorkload_.push_back(std::move(buckets. front())); + /**/ buckets.pop_front(); + } + } + conditionNewWork_.notify_all(); + } + +private: + Workload (const Workload&) = delete; + Workload& operator=(const Workload&) = delete; + + AsyncCallback& acb_; + + std::mutex lockWork_; + std::condition_variable conditionNewWork_; + + size_t idleThreads_ = 0; + + std::vector workload_; //thread-specific buckets + RingBuffer pendingWorkload_; //FIFO: buckets of work items for use by any thread +}; + + +template inline +bool haveNameClash(const FileSystemObject& fsObj, const List& m) +{ + return std::any_of(m.begin(), m.end(), [itemName = fsObj.getItemName()](const FileSystemObject& sibling) + { return equalNoCase(sibling.getItemName(), itemName); }); //equalNoCase: when in doubt => assume name clash! +} + + +class FolderPairSyncer +{ +public: + struct SyncCtx + { + bool verifyCopiedFiles; + bool copyFilePermissions; + bool failSafeFileCopy; + DeletionHandler& delHandlerLeft; + DeletionHandler& delHandlerRight; + }; + + static void runSync(SyncCtx& syncCtx, BaseFolderPair& baseFolder, PhaseCallback& cb) + { + runPass(PassNo::zero, syncCtx, baseFolder, cb); //prepare file moves + runPass(PassNo::one, syncCtx, baseFolder, cb); //delete files (or overwrite big ones with smaller ones) + runPass(PassNo::two, syncCtx, baseFolder, cb); //copy rest + } + +private: + friend class Workload; + + enum class PassNo + { + zero, //prepare file moves + one, //delete files + two, //create, modify + never //skip item + }; + + FolderPairSyncer(SyncCtx& syncCtx, std::mutex& singleThread, AsyncCallback& acb) : + delHandlerLeft_ (syncCtx.delHandlerLeft), + delHandlerRight_ (syncCtx.delHandlerRight), + verifyCopiedFiles_ (syncCtx.verifyCopiedFiles), + copyFilePermissions_(syncCtx.copyFilePermissions), + failSafeFileCopy_ (syncCtx.failSafeFileCopy), + singleThread_(singleThread), + acb_(acb) {} + + static PassNo getPass(const FilePair& file); + static PassNo getPass(const SymlinkPair& symlink); + static PassNo getPass(const FolderPair& folder); + static bool needZeroPass(const FilePair& file); + static bool needZeroPass(const FolderPair& folder); + + static void runPass(PassNo pass, SyncCtx& syncCtx, BaseFolderPair& baseFolder, PhaseCallback& cb); //throw X + + RingBuffer getFolderLevelWorkItems(PassNo pass, ContainerObject& parentFolder, Workload& workload); + + static bool containsMoveTarget(const FolderPair& parent); + void executeFileMove(FilePair& file); //throw ThreadStopRequest + template void executeFileMoveImpl(FilePair& fileFrom, FilePair& fileTo); //throw ThreadStopRequest + + void synchronizeFile(FilePair& file); // + template void synchronizeFileInt(FilePair& file, SyncOperation syncOp); //throw FileError, ErrorMoveUnsupported, ThreadStopRequest + + void synchronizeLink(SymlinkPair& symlink); // + template void synchronizeLinkInt(SymlinkPair& symlink, SyncOperation syncOp); //throw FileError, ThreadStopRequest + + void synchronizeFolder(FolderPair& folder); // + template void synchronizeFolderInt(FolderPair& folder, SyncOperation syncOp); //throw FileError, ThreadStopRequest + + void reportItemInfo(const std::wstring& msgTemplate, const AbstractPath& itemPath) { reportInfo(replaceCpy(msgTemplate, L"%x", fmtPath(AFS::getDisplayPath(itemPath))), acb_); } + + void reportItemInfo(const std::wstring& msgTemplate, const AbstractPath& itemPath1, const AbstractPath& itemPath2) //throw ThreadStopRequest + { + reportInfo(replaceCpy(replaceCpy(msgTemplate, L"%x", L'\n' + fmtPath(AFS::getDisplayPath(itemPath1))), + L"%y", L'\n' + fmtPath(AFS::getDisplayPath(itemPath2))), acb_); //throw ThreadStopRequest + } + + //already existing after onDeleteTargetFile(): undefined behavior! (e.g. fail/overwrite/auto-rename) + AFS::FileCopyResult copyFileWithCallback(const FileDescriptor& sourceDescr, + const AbstractPath& targetPath, + const std::function& onDeleteTargetFile /*throw X*/, //optional! + AsyncItemStatReporter& statReporter, //ThreadStopRequest + const std::wstring& statusMsg); //throw FileError, ThreadStopRequest, X + + DeletionHandler& delHandlerLeft_; + DeletionHandler& delHandlerRight_; + + const bool verifyCopiedFiles_; + const bool copyFilePermissions_; + const bool failSafeFileCopy_; + + std::mutex& singleThread_; + AsyncCallback& acb_; + + //preload status texts (premature?) + const std::wstring txtCreatingFile_ {_("Creating file %x" )}; + const std::wstring txtCreatingLink_ {_("Creating symbolic link %x")}; + const std::wstring txtCreatingFolder_ {_("Creating folder %x" )}; + const std::wstring txtUpdatingFile_ {_("Updating file %x" )}; + const std::wstring txtUpdatingLink_ {_("Updating symbolic link %x")}; + const std::wstring txtVerifyingFile_ {_("Verifying file %x" )}; + const std::wstring txtRenamingFileXtoY_ {_("Renaming file %x to %y" )}; + const std::wstring txtRenamingLinkXtoY_ {_("Renaming symbolic link %x to %y")}; + const std::wstring txtRenamingFolderXtoY_{_("Renaming folder %x to %y" )}; + const std::wstring txtMovingFileXtoY_ {_("Moving file %x to %y" )}; + const std::wstring txtSourceItemNotExist_{_("Source item %x is not existing")}; +}; + +//=================================================================================================== +//=================================================================================================== +/* ___________________________ + | | + | Multithreaded File Copy | + |_________________________| + + ---------------- ================= + |Async Callback| <-- |Worker Thread 1| + ---------------- ==================== + /|\ |Worker Thread 2| + | ================= + ============= | ... | + GUI <-- |Main Thread| \|/ \|/ +Callback ============= -------------------- + | Workload | + -------------------- + +Notes: - All threads share a single mutex, unlocked only during file I/O => do NOT require file_hierarchy.cpp classes to be thread-safe (i.e. internally synchronized)! + - Workload holds (folder-level-) items in buckets associated with each worker thread (FTP scenario: avoid CWDs) + - If a worker is idle, its Workload bucket is empty and no more pending buckets available: steal from other threads (=> take half of largest bucket) + - Maximize opportunity for parallelization ASAP: Workload buckets serve folder-items *before* files/symlinks => reduce risk of work-stealing + - Memory consumption: work items may grow indefinitely; however: test case "C:\" ~80MB per 1 million work items +*/ + +void FolderPairSyncer::runPass(PassNo pass, SyncCtx& syncCtx, BaseFolderPair& baseFolder, PhaseCallback& cb) //throw X +{ + std::mutex singleThread; //only a single worker thread may run at a time, except for parallel file I/O + + AsyncCallback acb; // + FolderPairSyncer fps(syncCtx, singleThread, acb); //manage life time: enclose InterruptibleThread's!!! + Workload workload(1, acb); + workload.addWorkItems(fps.getFolderLevelWorkItems(pass, baseFolder, workload)); //initial workload: set *before* threads get access! + + std::vector worker; + ZEN_ON_SCOPE_EXIT( for (InterruptibleThread& wt : worker) wt.requestStop(); ); //stop *all* at the same time before join! + + size_t threadIdx = 0; + Zstring threadName = Zstr("Sync"); + worker.emplace_back([threadIdx, &singleThread, &acb, &workload, threadName = std::move(threadName)] + { + setCurrentThreadName(threadName); + + while (/*blocking call:*/ std::function workItem = workload.getNext(threadIdx)) //throw ThreadStopRequest + { + acb.notifyTaskBegin(0 /*prio*/); //same prio, while processing only one folder pair at a time + ZEN_ON_SCOPE_EXIT(acb.notifyTaskEnd()); + + std::lock_guard dummy(singleThread); //protect ALL accesses to "fps" and workItem execution! + workItem(); //throw ThreadStopRequest + } + }); + acb.waitUntilDone(UI_UPDATE_INTERVAL / 2 /*every ~25 ms*/, cb); //throw X +} + + +//thread-safe thanks to std::mutex singleThread +RingBuffer FolderPairSyncer::getFolderLevelWorkItems(PassNo pass, ContainerObject& parentFolder, Workload& workload) +{ + RingBuffer buckets; + + RingBuffer foldersToInspect; + foldersToInspect.push_back(&parentFolder); + + while (!foldersToInspect.empty()) + { + ContainerObject& conObj = *foldersToInspect. front(); + /**/ foldersToInspect.pop_front(); + + RingBuffer> workItems; + + if (pass == PassNo::zero) + { + //create folders as required by file move targets: + for (FolderPair& folder : conObj.subfolders()) + if (needZeroPass(folder)) + workItems.push_back([this, &folder, &workload, pass] + { + tryReportingError([&] { synchronizeFolder(folder); }, acb_); //throw ThreadStopRequest + //error? => still process move targets (for delete + copy fall back!) + workload.addWorkItems(getFolderLevelWorkItems(pass, folder, workload)); + }); + else + foldersToInspect.push_back(&folder); + + for (FilePair& file : conObj.files()) + if (needZeroPass(file)) + workItems.push_back([this, &file] { executeFileMove(file); /*throw ThreadStopRequest*/ }); + } + else + { + //synchronize folders *first* (see comment above "Multithreaded File Copy") + for (FolderPair& folder : conObj.subfolders()) + if (pass == getPass(folder)) + workItems.push_back([this, &folder, &workload, pass] + { + tryReportingError([&]{ synchronizeFolder(folder); }, acb_); //throw ThreadStopRequest + + workload.addWorkItems(getFolderLevelWorkItems(pass, folder, workload)); + }); + else + foldersToInspect.push_back(&folder); + + //synchronize files: + for (FilePair& file : conObj.files()) + if (pass == getPass(file)) + workItems.push_back([this, &file] + { + tryReportingError([&]{ synchronizeFile(file); }, acb_); //throw ThreadStopRequest + }); + + //synchronize symbolic links: + for (SymlinkPair& symlink : conObj.symlinks()) + if (pass == getPass(symlink)) + workItems.push_back([this, &symlink] + { + tryReportingError([&] { synchronizeLink(symlink); }, acb_); //throw ThreadStopRequest + }); + } + + if (!workItems.empty()) + buckets.push_back(std::move(workItems)); + } + + return buckets; +} + + +/* __________________________ + |Move algorithm, 0th pass| + -------------------------- + 1. loop over hierarchy and find "move targets" => remember required parent folders + + 2. create required folders hierarchically: + - name-clash with other file/symlink (=> obscure!): fall back to delete and copy + - source folder missing: child items already deleted by synchronizeFolder() + - ignored error: fall back to delete and copy (in phases 1 and 2) + + 3. start file move (via targets) + - name-clash with other folder/symlink (=> obscure!): fall back to delete and copy + - ErrorMoveUnsupported: fall back to delete and copy + - ignored error: fall back to delete and copy + + __________________ + |killer-scenarios| + ------------------ + propagate the following move sequences: + I) a -> a/a caveat syncing parent directory first leads to circular dependency! + + II) a/a -> a caveat: fixing name clash will remove source! + + III) c -> d caveat: move-sequence needs to be processed in correct order! + b -> c/b + a -> b/a */ + +template +void FolderPairSyncer::executeFileMoveImpl(FilePair& fileFrom, FilePair& fileTo) //throw ThreadStopRequest +{ + assert(fileFrom.getMovePair() == &fileTo); + + const bool fallBackCopyDelete = [&] + { + //creation of parent folder has failed earlier? => fall back to delete + copy + const FolderPair* parentMissing = nullptr; //let's be more specific: go up in hierarchy until first missing parent folder + for (const FolderPair* f = dynamic_cast(&fileTo.parent()); f && f->isEmpty(); f = dynamic_cast(&f->parent())) + parentMissing = f; + + if (parentMissing) + { + reportInfo(AFS::generateMoveErrorMsg(fileFrom.getAbstractPath(), fileTo.getAbstractPath()) + L"\n\n" + + replaceCpy(_("Parent folder %x is not existing."), L"%x", fmtPath(AFS::getDisplayPath(parentMissing->getAbstractPath()))), acb_); //throw ThreadStopRequest + return true; + } + + //name clash with folders/symlinks? obscure => fall back to delete + copy + if (haveNameClash(fileTo, fileTo.parent().subfolders()) || + haveNameClash(fileTo, fileTo.parent().symlinks ())) + { + reportInfo(AFS::generateMoveErrorMsg(fileFrom.getAbstractPath(), fileTo.getAbstractPath()) + L"\n\n" + + replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(fileTo.getItemName())), acb_); //throw ThreadStopRequest + return true; + } + + bool moveSupported = true; + const std::wstring errMsg = tryReportingError([&] //throw ThreadStopRequest + { + try + { + synchronizeFile(fileTo); //throw FileError, ErrorMoveUnsupported, ThreadStopRequest + } + catch (const ErrorMoveUnsupported& e) + { + acb_.logMessage(e.toString(), PhaseCallback::MsgType::info); //let user know that move operation is not supported, then fall back: + moveSupported = false; + } + }, acb_); + + return !errMsg.empty() || !moveSupported; //move failed? We cannot allow to continue and have move source's parent directory deleted, messing up statistics! + }(); + + if (fallBackCopyDelete) + { + auto getStats = [&]() -> std::pair + { + SyncStatistics statSrc(fileFrom); + SyncStatistics statTrg(fileTo); + return {getCUD(statSrc) + getCUD(statTrg), statSrc.getBytesToProcess() + statTrg.getBytesToProcess()}; + }; + const auto [itemsBefore, bytesBefore] = getStats(); + fileFrom.setMovePair(nullptr); + const auto [itemsAfter, bytesAfter] = getStats(); + + //fix statistics total to match "copy + delete" + acb_.updateDataTotal(itemsAfter - itemsBefore, bytesAfter - bytesBefore); //noexcept + } +} + + +void FolderPairSyncer::executeFileMove(FilePair& file) //throw ThreadStopRequest +{ + const SyncOperation syncOp = file.getSyncOperation(); + switch (syncOp) + { + case SO_MOVE_LEFT_TO: + case SO_MOVE_RIGHT_TO: + if (FilePair* fileFrom = file.getMovePair()) + { + if (syncOp == SO_MOVE_LEFT_TO) + executeFileMoveImpl(*fileFrom, file); //throw ThreadStopRequest + else + executeFileMoveImpl(*fileFrom, file); //throw ThreadStopRequest + } + else assert(false); + break; + + case SO_CREATE_LEFT: + case SO_CREATE_RIGHT: + case SO_DELETE_LEFT: + case SO_DELETE_RIGHT: + case SO_MOVE_LEFT_FROM: //don't try to move more than *once* per pair + case SO_MOVE_RIGHT_FROM: // + case SO_OVERWRITE_LEFT: + case SO_OVERWRITE_RIGHT: + case SO_RENAME_LEFT: + case SO_RENAME_RIGHT: + case SO_DO_NOTHING: + case SO_EQUAL: + case SO_UNRESOLVED_CONFLICT: + assert(false); //should have been filtered out by FolderPairSyncer::needZeroPass() + break; + } +} + +//--------------------------------------------------------------------------------------------------------------- + +bool FolderPairSyncer::containsMoveTarget(const FolderPair& parent) +{ + for (const FilePair& file : parent.files()) + if (needZeroPass(file)) + return true; + + for (const FolderPair& subFolder : parent.subfolders()) + if (containsMoveTarget(subFolder)) + return true; + return false; +} + + +//0th pass: execute file moves (+ optional fallback to delete/copy in passes 1 and 2) +bool FolderPairSyncer::needZeroPass(const FolderPair& folder) +{ + switch (folder.getSyncOperation()) + { + case SO_CREATE_LEFT: + return containsMoveTarget(folder) && //recursive! watch perf! + !haveNameClash(folder, folder.parent().files ()) && //name clash with files/symlinks? obscure => skip folder creation + !haveNameClash(folder, folder.parent().symlinks()); // => move: fall back to delete + copy + + case SO_CREATE_RIGHT: + return containsMoveTarget(folder) && //recursive! watch perf! + !haveNameClash(folder, folder.parent().files ()) && //name clash with files/symlinks? obscure => skip folder creation + !haveNameClash(folder, folder.parent().symlinks()); // => move: fall back to delete + copy + + case SO_DO_NOTHING: //implies !isEmpty(); see FolderPair::getSyncOperation() + case SO_UNRESOLVED_CONFLICT: // + case SO_EQUAL: + case SO_RENAME_LEFT: + case SO_RENAME_RIGHT: + assert(!containsMoveTarget(folder) || (!folder.isEmpty() && !folder.isEmpty())); + //we're good to move contained items + break; + case SO_DELETE_LEFT: //not possible in the context of planning to move a child item, see FolderPair::getSyncOperation() + case SO_DELETE_RIGHT: // + assert(!containsMoveTarget(folder)); + break; + case SO_OVERWRITE_LEFT: // + case SO_OVERWRITE_RIGHT: // + case SO_MOVE_LEFT_FROM: // + case SO_MOVE_RIGHT_FROM: //status not possible for folder + case SO_MOVE_LEFT_TO: // + case SO_MOVE_RIGHT_TO: // + assert(false); + break; + } + return false; +} + + +inline +bool FolderPairSyncer::needZeroPass(const FilePair& file) +{ + switch (file.getSyncOperation()) + { + case SO_MOVE_LEFT_TO: + case SO_MOVE_RIGHT_TO: + return true; + + case SO_CREATE_LEFT: + case SO_CREATE_RIGHT: + case SO_DELETE_LEFT: + case SO_DELETE_RIGHT: + case SO_MOVE_LEFT_FROM: //don't try to move more than *once* per pair + case SO_MOVE_RIGHT_FROM: // + case SO_OVERWRITE_LEFT: + case SO_OVERWRITE_RIGHT: + case SO_RENAME_LEFT: + case SO_RENAME_RIGHT: + case SO_DO_NOTHING: + case SO_EQUAL: + case SO_UNRESOLVED_CONFLICT: + break; + } + return false; +} + + +//1st, 2nd pass benefits: +// - avoid disk space shortage: 1. delete files, 2. overwrite big with small files first +// - support change in type: overwrite file by directory, symlink by file, etc. + +inline +FolderPairSyncer::PassNo FolderPairSyncer::getPass(const FilePair& file) +{ + switch (file.getSyncOperation()) //evaluate comparison result and sync direction + { + case SO_DELETE_LEFT: + case SO_DELETE_RIGHT: + return PassNo::one; + + case SO_OVERWRITE_LEFT: + return file.getFileSize() > file.getFileSize() ? PassNo::one : PassNo::two; + + case SO_OVERWRITE_RIGHT: + return file.getFileSize() < file.getFileSize() ? PassNo::one : PassNo::two; + + case SO_MOVE_LEFT_FROM: // + case SO_MOVE_RIGHT_FROM: // [!] + return PassNo::never; + case SO_MOVE_LEFT_TO: // + case SO_MOVE_RIGHT_TO: //make sure 2-step move is processed in second pass, after move *target* parent directory was created! + return PassNo::two; + + case SO_CREATE_LEFT: + case SO_CREATE_RIGHT: + case SO_RENAME_LEFT: + case SO_RENAME_RIGHT: + return PassNo::two; + + case SO_DO_NOTHING: + case SO_EQUAL: + case SO_UNRESOLVED_CONFLICT: + return PassNo::never; + } + assert(false); + return PassNo::never; +} + + +inline +FolderPairSyncer::PassNo FolderPairSyncer::getPass(const SymlinkPair& symlink) +{ + switch (symlink.getSyncOperation()) //evaluate comparison result and sync direction + { + case SO_DELETE_LEFT: + case SO_DELETE_RIGHT: + return PassNo::one; //make sure to delete symlinks in first pass, and equally named file or dir in second pass: usecase "overwrite symlink with regular file"! + + case SO_OVERWRITE_LEFT: + case SO_OVERWRITE_RIGHT: + case SO_CREATE_LEFT: + case SO_CREATE_RIGHT: + case SO_RENAME_LEFT: + case SO_RENAME_RIGHT: + return PassNo::two; + + case SO_MOVE_LEFT_FROM: + case SO_MOVE_RIGHT_FROM: + case SO_MOVE_LEFT_TO: + case SO_MOVE_RIGHT_TO: + assert(false); + [[fallthrough]]; + case SO_DO_NOTHING: + case SO_EQUAL: + case SO_UNRESOLVED_CONFLICT: + return PassNo::never; + } + assert(false); + return PassNo::never; +} + + +inline +FolderPairSyncer::PassNo FolderPairSyncer::getPass(const FolderPair& folder) +{ + switch (folder.getSyncOperation()) //evaluate comparison result and sync direction + { + case SO_DELETE_LEFT: + case SO_DELETE_RIGHT: + return PassNo::one; + + case SO_CREATE_LEFT: + case SO_CREATE_RIGHT: + case SO_RENAME_LEFT: + case SO_RENAME_RIGHT: + return PassNo::two; + + case SO_OVERWRITE_LEFT: + case SO_OVERWRITE_RIGHT: + case SO_MOVE_LEFT_FROM: + case SO_MOVE_RIGHT_FROM: + case SO_MOVE_LEFT_TO: + case SO_MOVE_RIGHT_TO: + assert(false); + [[fallthrough]]; + case SO_DO_NOTHING: + case SO_EQUAL: + case SO_UNRESOLVED_CONFLICT: + return PassNo::never; + } + assert(false); + return PassNo::never; +} + +//--------------------------------------------------------------------------------------------------------------- + +inline +void FolderPairSyncer::synchronizeFile(FilePair& file) //throw FileError, ErrorMoveUnsupported, ThreadStopRequest +{ + assert(isLocked(singleThread_)); + const SyncOperation syncOp = file.getSyncOperation(); + + if (const SyncDirection syncDir = getEffectiveSyncDir(syncOp); + syncDir != SyncDirection::none) + { + if (syncDir == SyncDirection::left) + synchronizeFileInt(file, syncOp); + else + synchronizeFileInt(file, syncOp); + } +} + + +template +void FolderPairSyncer::synchronizeFileInt(FilePair& file, SyncOperation syncOp) //throw FileError, ErrorMoveUnsupported, ThreadStopRequest +{ + constexpr SelectSide sideSrc = getOtherSide; + DeletionHandler& delHandlerTrg = selectParam(delHandlerLeft_, delHandlerRight_); + + switch (syncOp) + { + case SO_CREATE_LEFT: + case SO_CREATE_RIGHT: + { + if (auto parentFolder = dynamic_cast(&file.parent())) + if (parentFolder->isEmpty()) //BaseFolderPair OTOH is always non-empty and existing in this context => else: fatal error in zen::synchronize() + return; //if parent directory creation failed, there's no reason to show more errors! + + const AbstractPath targetPath = file.getAbstractPath(); + + const std::wstring& statusMsg = replaceCpy(txtCreatingFile_, L"%x", fmtPath(AFS::getDisplayPath(targetPath))); + reportInfo(std::wstring(statusMsg), acb_); //throw ThreadStopRequest + + AsyncItemStatReporter statReporter(1, file.getFileSize(), acb_); + try + { + const AFS::FileCopyResult result = copyFileWithCallback({file.getAbstractPath(), file.getAttributes()}, + targetPath, + nullptr, //onDeleteTargetFile: nothing to delete + //if existing: undefined behavior! (e.g. fail/overwrite/auto-rename) + statReporter, + statusMsg); //throw FileError, ThreadStopRequest + statReporter.reportDelta(1, 0); + + //update FilePair + file.setSyncedTo(result.fileSize, + result.modTime, //target time set from source + result.modTime, + result.targetFilePrint, + result.sourceFilePrint, + false, file.isFollowedSymlink()); + + if (result.errorModTime) //log only; no popup + acb_.logMessage(result.errorModTime->toString(), PhaseCallback::MsgType::warning); //throw ThreadStopRequest + } + catch (const FileError& e) + { + bool sourceExists = true; + try { sourceExists = parallel::itemExists(file.getAbstractPath(), singleThread_); /*throw FileError*/ } + //abstract context => unclear which exception is more relevant/useless: + //e could be "item not found": doh; e2 devoid of any details after SFTP error: https://freefilesync.org/forum/viewtopic.php?t=7138#p24064 + catch (const FileError& e2) { throw FileError(replaceCpy(e.toString(), L"\n\n", L'\n'), replaceCpy(e2.toString(), L"\n\n", L'\n')); } + + //do not check on type (symlink, file, folder) -> if there is a type change, FFS should not be quiet about it! + if (!sourceExists) + { + reportItemInfo(txtSourceItemNotExist_, file.getAbstractPath()); //throw ThreadStopRequest + + statReporter.reportDelta(1, 0); + //even if the source item does not exist anymore, significant I/O work was done => report + file.removeItem(); //source deleted meanwhile...nothing was done (logical point of view!) + //remove only *after* evaluating "file, sideSrc"! + } + else + throw; + } + } + break; + + case SO_DELETE_LEFT: + case SO_DELETE_RIGHT: + { + AsyncItemStatReporter statReporter(1, 0, acb_); + + if (file.isFollowedSymlink()) + delHandlerTrg.removeLinkWithCallback(file.getAbstractPath(), file.getRelativePath(), + false /*beforeOverwrite*/, statReporter, singleThread_); //throw FileError, ThreadStopRequest + else + delHandlerTrg.removeFileWithCallback({file.getAbstractPath(), file.getAttributes()}, file.getRelativePath(), + false /*beforeOverwrite*/, statReporter, singleThread_); //throw FileError, ThreadStopRequest + + file.removeItem(); //update FilePair + } + break; + + case SO_MOVE_LEFT_TO: + case SO_MOVE_RIGHT_TO: + if (FilePair* fileFrom = file.getMovePair()) + { + FilePair* fileTo = &file; + + assert((fileFrom->getSyncOperation() == SO_MOVE_LEFT_FROM && fileTo->getSyncOperation() == SO_MOVE_LEFT_TO && sideTrg == SelectSide::left) || + (fileFrom->getSyncOperation() == SO_MOVE_RIGHT_FROM && fileTo->getSyncOperation() == SO_MOVE_RIGHT_TO && sideTrg == SelectSide::right)); + + const AbstractPath pathFrom = fileFrom->getAbstractPath(); + const AbstractPath pathTo = fileTo ->getAbstractPath(); + + reportItemInfo(txtMovingFileXtoY_, pathFrom, pathTo); //throw ThreadStopRequest + + AsyncItemStatReporter statReporter(1, 0, acb_); + + //already existing: undefined behavior! (e.g. fail/overwrite) + parallel::moveAndRenameItem(pathFrom, pathTo, singleThread_); //throw FileError, ErrorMoveUnsupported + + statReporter.reportDelta(1, 0); + + //update FilePair + assert(fileFrom->getFileSize() == fileTo->getFileSize()); + fileTo->setSyncedTo(fileTo ->getFileSize(), + fileFrom->getLastWriteTime(), + fileTo ->getLastWriteTime(), + fileFrom->getFilePrint(), + fileTo ->getFilePrint(), + fileFrom->isFollowedSymlink(), + fileTo ->isFollowedSymlink()); + fileFrom->removeItem(); //remove only *after* evaluating "fileFrom, sideTrg"! + } + else (assert(false)); + break; + + case SO_OVERWRITE_LEFT: + case SO_OVERWRITE_RIGHT: + { + //respect differences in case of source object: + const AbstractPath targetPathLogical = AFS::appendRelPath(file.parent().getAbstractPath(), file.getItemName()); + + AbstractPath targetPathResolvedOld = file.getAbstractPath(); //support change in case when syncing to case-sensitive SFTP on Windows! + AbstractPath targetPathResolvedNew = targetPathLogical; + if (file.isFollowedSymlink()) //follow link when updating file rather than delete it and replace with regular file!!! + targetPathResolvedOld = targetPathResolvedNew = parallel::getSymlinkResolvedPath(file.getAbstractPath(), singleThread_); //throw FileError + + const std::wstring& statusMsg = replaceCpy(txtUpdatingFile_, L"%x", fmtPath(AFS::getDisplayPath(targetPathResolvedOld))); + reportInfo(std::wstring(statusMsg), acb_); //throw ThreadStopRequest + + AsyncItemStatReporter statReporter(1, file.getFileSize(), acb_); + + if (file.isFollowedSymlink()) //since we follow the link, we need to sync case sensitivity of the link manually! + if (!file.hasEquivalentItemNames()) + //already existing: undefined behavior! (e.g. fail/overwrite) + parallel::moveAndRenameItem(file.getAbstractPath(), targetPathLogical, singleThread_); //throw FileError, (ErrorMoveUnsupported) + + auto onDeleteTargetFile = [&] //delete target at appropriate time + { + assert(isLocked(singleThread_)); + FileAttributes followedTargetAttr = file.getAttributes(); + followedTargetAttr.isFollowedSymlink = false; + + if (file.isFollowedSymlink()) + delHandlerTrg.removeLinkWithCallback(targetPathResolvedOld, file.getRelativePath(), + true /*beforeOverwrite*/, statReporter, singleThread_); //throw FileError, ThreadStopRequest + else + delHandlerTrg.removeFileWithCallback({targetPathResolvedOld, followedTargetAttr}, file.getRelativePath(), + true /*beforeOverwrite*/, statReporter, singleThread_); //throw FileError, ThreadStopRequest + + //file.removeItem(); -> doesn't make sense for isFollowedSymlink(); "file, sideTrg" evaluated below! + }; + + const AFS::FileCopyResult result = copyFileWithCallback({file.getAbstractPath(), file.getAttributes()}, + targetPathResolvedNew, + onDeleteTargetFile, + statReporter, + statusMsg); //throw FileError, ThreadStopRequest + statReporter.reportDelta(1, 0); + //we model "delete + copy" as ONE logical operation + + //update FilePair + file.setSyncedTo(result.fileSize, + result.modTime, //target time set from source + result.modTime, + result.targetFilePrint, + result.sourceFilePrint, + file.isFollowedSymlink(), + file.isFollowedSymlink()); + + if (result.errorModTime) //log only; no popup + acb_.logMessage(result.errorModTime->toString(), PhaseCallback::MsgType::warning); //throw ThreadStopRequest + } + break; + + case SO_RENAME_LEFT: + case SO_RENAME_RIGHT: + //harmonize with file_hierarchy.cpp::getSyncOpDescription!! + reportInfo(replaceCpy(replaceCpy(txtRenamingFileXtoY_, L"%x", fmtPath(AFS::getDisplayPath(file.getAbstractPath()))), + L"%y", fmtPath(file.getItemName())), acb_); //throw ThreadStopRequest + { + AsyncItemStatReporter statReporter(1, 0, acb_); + + if (!file.hasEquivalentItemNames()) + //already existing: undefined behavior! (e.g. fail/overwrite) + parallel::moveAndRenameItem(file.getAbstractPath(), //throw FileError, (ErrorMoveUnsupported) + AFS::appendRelPath(file.parent().getAbstractPath(), file.getItemName()), singleThread_); + else + assert(false); + +#if 0 //changing file time without copying content is not justified after CompareVariant::size finds "equal" files! + //Bonus: some devices don't support setting (precise) file times anyway, e.g. FAT or MTP! + if (file.getLastWriteTime() != file.getLastWriteTime()) + //- no need to call sameFileTime() or respect 2 second FAT/FAT32 precision in this comparison + //- do NOT read *current* source file time, but use buffered value which corresponds to time of comparison! + parallel::setModTime(file.getAbstractPath(), file.getLastWriteTime()); //throw FileError +#endif + statReporter.reportDelta(1, 0); + + //-> both sides *should* be completely equal now... + assert(file.getFileSize() == file.getFileSize()); + file.setSyncedTo(file.getFileSize(), + file.getLastWriteTime (), + file.getLastWriteTime (), + file.getFilePrint (), + file.getFilePrint (), + file.isFollowedSymlink(), + file.isFollowedSymlink()); + } + break; + + case SO_MOVE_LEFT_FROM: //use SO_MOVE_LEFT_TO/SO_MOVE_RIGHT_TO to execute move: + case SO_MOVE_RIGHT_FROM: //=> makes sure parent directory has been created + case SO_DO_NOTHING: + case SO_EQUAL: + case SO_UNRESOLVED_CONFLICT: + assert(false); //should have been filtered out by FolderPairSyncer::getPass() + return; //no update on processed data! + } +} + + +inline +void FolderPairSyncer::synchronizeLink(SymlinkPair& symlink) //throw FileError, ThreadStopRequest +{ + assert(isLocked(singleThread_)); + const SyncOperation syncOp = symlink.getSyncOperation(); + + if (const SyncDirection syncDir = getEffectiveSyncDir(syncOp); + syncDir != SyncDirection::none) + { + if (syncDir == SyncDirection::left) + synchronizeLinkInt(symlink, syncOp); + else + synchronizeLinkInt(symlink, syncOp); + } +} + + +template +void FolderPairSyncer::synchronizeLinkInt(SymlinkPair& symlink, SyncOperation syncOp) //throw FileError, ThreadStopRequest +{ + constexpr SelectSide sideSrc = getOtherSide; + DeletionHandler& delHandlerTrg = selectParam(delHandlerLeft_, delHandlerRight_); + + switch (syncOp) + { + case SO_CREATE_LEFT: + case SO_CREATE_RIGHT: + { + if (auto parentFolder = dynamic_cast(&symlink.parent())) + if (parentFolder->isEmpty()) //BaseFolderPair OTOH is always non-empty and existing in this context => else: fatal error in zen::synchronize() + return; //if parent directory creation failed, there's no reason to show more errors! + + const AbstractPath targetPath = symlink.getAbstractPath(); + reportItemInfo(txtCreatingLink_, targetPath); //throw ThreadStopRequest + + AsyncItemStatReporter statReporter(1, 0, acb_); + try + { + parallel::copySymlink(symlink.getAbstractPath(), targetPath, copyFilePermissions_, singleThread_); //throw FileError + + statReporter.reportDelta(1, 0); + + //update SymlinkPair + symlink.setSyncedTo(symlink.getLastWriteTime(), //target time set from source + symlink.getLastWriteTime()); + + } + catch (const FileError& e) + { + bool sourceExists = true; + try { sourceExists = parallel::itemExists(symlink.getAbstractPath(), singleThread_); /*throw FileError*/ } + //abstract context => unclear which exception is more relevant/useless: + catch (const FileError& e2) { throw FileError(replaceCpy(e.toString(), L"\n\n", L'\n'), replaceCpy(e2.toString(), L"\n\n", L'\n')); } + + //do not check on type (symlink, file, folder) -> if there is a type change, FFS should not be quiet about it! + if (!sourceExists) + { + reportItemInfo(txtSourceItemNotExist_, symlink.getAbstractPath()); //throw ThreadStopRequest + + //even if the source item does not exist anymore, significant I/O work was done => report + statReporter.reportDelta(1, 0); + symlink.removeItem(); //source deleted meanwhile...nothing was done (logical point of view!) + } + else + throw; + } + } + break; + + case SO_DELETE_LEFT: + case SO_DELETE_RIGHT: + { + AsyncItemStatReporter statReporter(1, 0, acb_); + + delHandlerTrg.removeLinkWithCallback(symlink.getAbstractPath(), symlink.getRelativePath(), + false /*beforeOverwrite*/, statReporter, singleThread_); //throw FileError, ThreadStopRequest + + symlink.removeItem(); //update SymlinkPair + } + break; + + case SO_OVERWRITE_LEFT: + case SO_OVERWRITE_RIGHT: + { + reportItemInfo(txtUpdatingLink_, symlink.getAbstractPath()); //throw ThreadStopRequest + + AsyncItemStatReporter statReporter(1, 0, acb_); + + delHandlerTrg.removeLinkWithCallback(symlink.getAbstractPath(), symlink.getRelativePath(), + true /*beforeOverwrite*/, statReporter, singleThread_); //throw FileError, ThreadStopRequest + + //symlink.removeItem(); -> "symlink, sideTrg" evaluated below! + + parallel::copySymlink(symlink.getAbstractPath(), + AFS::appendRelPath(symlink.parent().getAbstractPath(), symlink.getItemName()), //respect differences in case of source object + copyFilePermissions_, singleThread_); //throw FileError + + statReporter.reportDelta(1, 0); //we model "delete + copy" as ONE logical operation + + //update SymlinkPair + symlink.setSyncedTo(symlink.getLastWriteTime(), //target time set from source + symlink.getLastWriteTime()); + } + break; + + case SO_RENAME_LEFT: + case SO_RENAME_RIGHT: + reportInfo(replaceCpy(replaceCpy(txtRenamingLinkXtoY_, L"%x", fmtPath(AFS::getDisplayPath(symlink.getAbstractPath()))), + L"%y", fmtPath(symlink.getItemName())), acb_); //throw ThreadStopRequest + { + AsyncItemStatReporter statReporter(1, 0, acb_); + + if (!symlink.hasEquivalentItemNames()) + //already existing: undefined behavior! (e.g. fail/overwrite) + parallel::moveAndRenameItem(symlink.getAbstractPath(), //throw FileError, (ErrorMoveUnsupported) + AFS::appendRelPath(symlink.parent().getAbstractPath(), symlink.getItemName()), singleThread_); + else + assert(false); + + statReporter.reportDelta(1, 0); + + //-> both sides *should* be completely equal now... + symlink.setSyncedTo(symlink.getLastWriteTime(), //target time set from source + symlink.getLastWriteTime()); + } + break; + + case SO_MOVE_LEFT_FROM: + case SO_MOVE_RIGHT_FROM: + case SO_MOVE_LEFT_TO: + case SO_MOVE_RIGHT_TO: + case SO_DO_NOTHING: + case SO_EQUAL: + case SO_UNRESOLVED_CONFLICT: + assert(false); //should have been filtered out by FolderPairSyncer::getPass() + return; //no update on processed data! + } +} + + +inline +void FolderPairSyncer::synchronizeFolder(FolderPair& folder) //throw FileError, ThreadStopRequest +{ + assert(isLocked(singleThread_)); + const SyncOperation syncOp = folder.getSyncOperation(); + + if (const SyncDirection syncDir = getEffectiveSyncDir(syncOp); + syncDir != SyncDirection::none) + { + if (syncDir == SyncDirection::left) + synchronizeFolderInt(folder, syncOp); + else + synchronizeFolderInt(folder, syncOp); + } +} + + +template +void FolderPairSyncer::synchronizeFolderInt(FolderPair& folder, SyncOperation syncOp) //throw FileError, ThreadStopRequest +{ + constexpr SelectSide sideSrc = getOtherSide; + DeletionHandler& delHandlerTrg = selectParam(delHandlerLeft_, delHandlerRight_); + + switch (syncOp) + { + case SO_CREATE_LEFT: + case SO_CREATE_RIGHT: + { + if (auto parentFolder = dynamic_cast(&folder.parent())) + if (parentFolder->isEmpty()) //BaseFolderPair OTOH is always non-empty and existing in this context => else: fatal error in zen::synchronize() + return; //if parent directory creation failed, there's no reason to show more errors! + + const AbstractPath targetPath = folder.getAbstractPath(); + reportItemInfo(txtCreatingFolder_, targetPath); //throw ThreadStopRequest + + //shallow-"copying" a folder might not fail if source is missing, so we need to check this first: + if (parallel::itemExists(folder.getAbstractPath(), singleThread_)) //throw FileError + { + AsyncItemStatReporter statReporter(1, 0, acb_); + try + { + //already existing: fail + parallel::copyNewFolder(folder.getAbstractPath(), targetPath, copyFilePermissions_, singleThread_); //throw FileError + } + catch (FileError&) + { + bool folderAlreadyExists = false; + try { folderAlreadyExists = parallel::getItemType(targetPath, singleThread_) == AFS::ItemType::folder; } /*throw FileError*/ catch (FileError&) {} + //previous exception is more relevant; good enough? https://freefilesync.org/forum/viewtopic.php?t=5266 + + if (!folderAlreadyExists) + throw; + } + + statReporter.reportDelta(1, 0); + + //update FolderPair + folder.setSyncedTo(false, //isSymlinkTrg + folder.isFollowedSymlink()); + } + else //source deleted meanwhile... + { + reportItemInfo(txtSourceItemNotExist_, folder.getAbstractPath()); //throw ThreadStopRequest + + //attention when fixing statistics due to missing folder: child items may be scheduled for move, so deletion will have move-references flip back to copy + delete! + const SyncStatistics statsBefore(folder.base()); //=> don't bother considering individual move operations, just calculate over the whole tree + folder.clearFiles(); // + folder.clearSymlinks(); //update FolderPair + folder.clearSubfolders(); // + folder.removeItem(); // + const SyncStatistics statsAfter(folder.base()); + + acb_.updateDataProcessed(1, 0); //even if the source item does not exist anymore, significant I/O work was done => report + acb_.updateDataTotal(getCUD(statsAfter) - getCUD(statsBefore) + 1, statsAfter.getBytesToProcess() - statsBefore.getBytesToProcess()); //noexcept + } + } + break; + + case SO_DELETE_LEFT: + case SO_DELETE_RIGHT: + { + const SyncStatistics subStats(folder); //counts sub-objects only! + AsyncItemStatReporter statReporter(1 + getCUD(subStats), subStats.getBytesToProcess(), acb_); + + if (folder.isFollowedSymlink()) + delHandlerTrg.removeLinkWithCallback(folder.getAbstractPath(), folder.getRelativePath(), + false /*beforeOverwrite*/, statReporter, singleThread_); //throw FileError, ThreadStopRequest + else + delHandlerTrg.removeDirWithCallback(folder.getAbstractPath(), folder.getRelativePath(), + statReporter, singleThread_); //throw FileError, ThreadStopRequest + + //TODO: implement parallel folder deletion + + folder.clearFiles(); // + folder.clearSymlinks(); //update FolderPair + folder.clearSubfolders(); // + folder.removeItem(); // + } + break; + + case SO_RENAME_LEFT: + case SO_RENAME_RIGHT: + reportInfo(replaceCpy(replaceCpy(txtRenamingFolderXtoY_, L"%x", fmtPath(AFS::getDisplayPath(folder.getAbstractPath()))), + L"%y", fmtPath(folder.getItemName())), acb_); //throw ThreadStopRequest + { + AsyncItemStatReporter statReporter(1, 0, acb_); + + if (!folder.hasEquivalentItemNames()) + //already existing: undefined behavior! (e.g. fail/overwrite) + parallel::moveAndRenameItem(folder.getAbstractPath(), //throw FileError, (ErrorMoveUnsupported) + AFS::appendRelPath(folder.parent().getAbstractPath(), folder.getItemName()), singleThread_); + else + assert(false); + + statReporter.reportDelta(1, 0); + + //-> both sides *should* be completely equal now... + folder.setSyncedTo(folder.isFollowedSymlink(), + folder.isFollowedSymlink()); + } + break; + + case SO_OVERWRITE_LEFT: + case SO_OVERWRITE_RIGHT: + case SO_MOVE_LEFT_FROM: + case SO_MOVE_RIGHT_FROM: + case SO_MOVE_LEFT_TO: + case SO_MOVE_RIGHT_TO: + case SO_DO_NOTHING: + case SO_EQUAL: + case SO_UNRESOLVED_CONFLICT: + assert(false); //should have been filtered out by FolderPairSyncer::getPass() + return; //no update on processed data! + } +} + +//########################################################################################### + +//returns current attributes of source file +AFS::FileCopyResult FolderPairSyncer::copyFileWithCallback(const FileDescriptor& sourceDescr, + const AbstractPath& targetPath, + const std::function& onDeleteTargetFile /*throw X*/, + AsyncItemStatReporter& statReporter /*throw ThreadStopRequest*/, + const std::wstring& statusMsg) //throw FileError, ThreadStopRequest, X +{ + const AbstractPath& sourcePath = sourceDescr.path; + const AFS::StreamAttributes sourceAttr{sourceDescr.attr.modTime, sourceDescr.attr.fileSize, sourceDescr.attr.filePrint}; + + auto copyOperation = [&](const AbstractPath& sourcePathTmp) + { + PercentStatReporter percentReporter(statusMsg, sourceDescr.attr.fileSize, statReporter); + + //already existing + no onDeleteTargetFile: undefined behavior! (e.g. fail/overwrite/auto-rename) + const AFS::FileCopyResult result = parallel::copyFileTransactional(sourcePathTmp, sourceAttr, //throw FileError, ErrorFileLocked, ThreadStopRequest, X + targetPath, + copyFilePermissions_, + failSafeFileCopy_, [&] + { + if (onDeleteTargetFile) //running *outside* singleThread_ lock! => onDeleteTargetFile-callback expects lock being held: + { + std::lock_guard dummy(singleThread_); + onDeleteTargetFile(); //throw X + } + }, + [&](int64_t bytesDelta) //callback runs *outside* singleThread_ lock! => fine + { + percentReporter.updateDeltaAndStatus(bytesDelta); //throw ThreadStopRequest + interruptionPoint(); //throw ThreadStopRequest => not reliably covered by PercentStatReporter::updateDeltaAndStatus()! + }, + singleThread_); + + //#################### Verification ############################# + if (verifyCopiedFiles_) + { + reportItemInfo(txtVerifyingFile_, targetPath); //throw ThreadStopRequest + + //delete target if verification fails + ZEN_ON_SCOPE_FAIL(try { parallel::removeFilePlain(targetPath, singleThread_); } + catch (const FileError& e) { statReporter.logMessage(e.toString(), PhaseCallback::MsgType::error); /*throw ThreadStopRequest*/ }); + + //callback runs *outside* singleThread_ lock! => fine + auto verifyCallback = [&](int64_t bytesDelta) { interruptionPoint(); }; //throw ThreadStopRequest + + parallel::verifyFiles(sourcePathTmp, targetPath, verifyCallback, singleThread_); //throw FileError, ThreadStopRequest + } + //#################### /Verification ############################# + + return result; + }; + + return copyOperation(sourcePath); //throw FileError, (ErrorFileLocked), ThreadStopRequest +} + +//########################################################################################### + +template +bool checkBaseFolderStatus(BaseFolderPair& baseFolder, PhaseCallback& callback /*throw X*/) +{ + const AbstractPath folderPath = baseFolder.getAbstractPath(); + + switch (baseFolder.getFolderStatus()) + { + case BaseFolderStatus::existing: + { + const std::wstring errMsg = tryReportingError([&] + { + AFS::getItemType(folderPath); //throw FileError + }, callback); //throw X + if (!errMsg.empty()) + return false; + } + break; + + case BaseFolderStatus::notExisting: + { + bool folderExisting = false; + + const std::wstring errMsg = tryReportingError([&] + { + folderExisting = AFS::itemExists(folderPath); //throw FileError + }, callback); //throw X + if (!errMsg.empty()) + return false; + if (folderExisting) //=> somebody else created it: problem? + { + /* Is it possible we're catching a "false positive" here, could FFS have created the directory indirectly after comparison? + 1. deletion handling: recycler -> no, temp directory created only at first deletion + 2. deletion handling: versioning -> " + 3. log file creates containing folder -> no, log only created in batch mode, and only *before* comparison + 4. yes, could be us! e.g. multiple folder pairs to non-yet-existing target folder => too obscure!? */ + callback.reportFatalError(replaceCpy(_("The folder %x is already existing, but was not found earlier during comparison."), + L"%x", fmtPath(AFS::getDisplayPath(folderPath)))); //throw X + return false; + } + } + break; + + case BaseFolderStatus::failure: + //e.g. TEMPORARY network drop! base directory not found during comparison + //=> sync-directions are based on false assumptions! Abort. + callback.reportFatalError(replaceCpy(_("Skipping folder pair because %x could not be accessed during comparison."), + L"%x", fmtPath(AFS::getDisplayPath(folderPath)))); //throw X + return false; + } + return true; +} + + +template //create base directories first (if not yet existing) -> no symlink or attribute copying! +bool createBaseFolder(BaseFolderPair& baseFolder, bool copyFilePermissions, PhaseCallback& callback /*throw X*/) //return false if fatal error occurred +{ + switch (baseFolder.getFolderStatus()) + { + case BaseFolderStatus::existing: + break; + + case BaseFolderStatus::notExisting: + { + //create target directory: user presumably ignored warning "dir not yet existing" in order to have it created automatically + const AbstractPath folderPath = baseFolder.getAbstractPath(); + constexpr SelectSide sideSrc = getOtherSide; + + const std::wstring errMsg = tryReportingError([&] + { + if (baseFolder.getFolderStatus() == BaseFolderStatus::existing) //copy file permissions + { + if (const std::optional parentPath = AFS::getParentPath(folderPath)) + AFS::createFolderIfMissingRecursion(*parentPath); //throw FileError + + AFS::copyNewFolder(baseFolder.getAbstractPath(), folderPath, copyFilePermissions); //throw FileError + } + else + AFS::createFolderIfMissingRecursion(folderPath); //throw FileError + assert(baseFolder.getFolderStatus() != BaseFolderStatus::failure); + + baseFolder.setFolderStatus(BaseFolderStatus::existing); //update our model! + }, callback); //throw X + + return errMsg.empty(); + } + + case BaseFolderStatus::failure: + assert(false); //already skipped after checkBaseFolderStatus() + break; + } + return true; +} +} + + +void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime, + bool verifyCopiedFiles, + bool copyLockedFiles, + bool copyFilePermissions, + bool failSafeFileCopy, + bool runWithBackgroundPriority, + const std::vector& syncConfig, + FolderComparison& folderCmp, + WarningDialogs& warnings, + ProcessCallback& callback /*throw X*/) //throw X +{ + //PERF_START; + + if (syncConfig.size() != folderCmp.size()) + throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); + + //aggregate basic information + std::vector folderPairStats; + { + int itemsTotal = 0; + int64_t bytesTotal = 0; + for (const BaseFolderPair& baseFolder : asRange(folderCmp)) + { + SyncStatistics fpStats(baseFolder); + itemsTotal += getCUD(fpStats); + bytesTotal += fpStats.getBytesToProcess(); + folderPairStats.push_back(fpStats); + } + + //inform about the total amount of data that will be processed from now on + //keep at beginning so that all gui elements are initialized properly + callback.initNewPhase(itemsTotal, //throw X + bytesTotal, + ProcessPhase::sync); + } + + //------------------------------------------------------------------------------- + + //prevent operating system going into sleep state + std::optional noStandby; + try + { + noStandby.emplace(runWithBackgroundPriority ? ProcessPriority::background : ProcessPriority::normal); //throw FileError + } + catch (const FileError& e) //failure is not critical => log only + { + callback.logMessage(e.toString(), PhaseCallback::MsgType::warning); //throw X + } + + //-------------------execute basic checks all at once BEFORE starting sync-------------------------------------- + std::vector skipFolderPair(folderCmp.size(), false); //folder pairs may be skipped after fatal errors were found + + std::vector>> checkUnresolvedConflicts; + + std::vector> checkBaseFolderRaceCondition; + + std::vector> checkSignificantDiffPairs; + + std::vector>> checkDiskSpaceMissing; //base folder / space required / space available + + std::set checkVersioningPaths; + std::vector> checkVersioningBasePaths; //hard filter creates new logical hierarchies for otherwise equal AbstractPath... + + std::set checkVersioningLimitPaths; + + //------------------- start checking folder pairs ------------------- + + //skip incomplete folder pairs with only one folder selected, or all => don't support "deletion via source folder" + { + bool haveFullPair = false; + std::wstring partialPairList; + + for (const BaseFolderPair& baseFolder : asRange(folderCmp)) + { + const AbstractPath& folderPathL = baseFolder.getAbstractPath(); + const AbstractPath& folderPathR = baseFolder.getAbstractPath(); + + if (AFS::isNullPath(folderPathL) != AFS::isNullPath(folderPathR)) + { + partialPairList += L"\n" + + (AFS::isNullPath(folderPathL) ? L"<" + _("empty") + L">" : AFS::getDisplayPath(folderPathL)) + L" | " + + (AFS::isNullPath(folderPathR) ? L"<" + _("empty") + L">" : AFS::getDisplayPath(folderPathR)); + } + else if (!AFS::isNullPath(folderPathL)) + haveFullPair = true; + } + + //error if: partial pairs or all empty -> single-folder comparison scenario doesn't include synchronization + if (!partialPairList.empty() || !haveFullPair) + callback.reportFatalError(trimCpy(_("A folder input field is empty.") + L" \n\n" + + _("Please select both left and right folders for synchronization.") + L"\n" + partialPairList)); //throw X + //"skipFolderPair[folderIndex] = true" will be set below + } + + + for (size_t folderIndex = 0; folderIndex < folderCmp.size(); ++folderIndex) + { + BaseFolderPair& baseFolder = folderCmp[folderIndex].ref(); + const FolderPairSyncCfg& folderPairCfg = syncConfig[folderIndex]; + const SyncStatistics& folderPairStat = folderPairStats[folderIndex]; + + //=============== start with checks that may SKIP folder pairs =============== + //============================================================================ + + //skip incomplete folder pairs (fatal error already reported above) + if (AFS::isNullPath(baseFolder.getAbstractPath()) || + AFS::isNullPath(baseFolder.getAbstractPath())) + { + skipFolderPair[folderIndex] = true; + continue; + } + + //exclude a few pathological cases + if (baseFolder.getAbstractPath() == + baseFolder.getAbstractPath()) + { + skipFolderPair[folderIndex] = true; + continue; + } + + //skip folder pair if there is nothing to do (except when DB files need to be updated for two-way mode and move-detection) + //=> avoid redundant errors in checkBaseFolderStatus() if base folder existence test failed during comparison + if (getCUD(folderPairStat) == 0 && !folderPairCfg.saveSyncDB) + { + skipFolderPair[folderIndex] = true; + continue; + } + + //check for network drops after comparison + // - convenience: exit sync right here instead of showing tons of errors during file copy + // - early failure! there's no point in evaluating subsequent warnings + if (!checkBaseFolderStatus(baseFolder, callback) || + !checkBaseFolderStatus(baseFolder, callback)) + { + skipFolderPair[folderIndex] = true; + continue; + } + + //allow propagation of deletions only from *empty* or *existing* source folder: + auto sourceFolderMissing = [&](const AbstractPath& baseFolderPath, BaseFolderStatus folderStatus) //we need to evaluate existence status from time of comparison! + { + //PERMANENT network drop: avoid data loss when source directory is not found AND user chose to ignore errors (else we wouldn't arrive here) + if (folderPairStat.deleteCount() > 0) //check deletions only... (respect filtered items!) + //folderPairStat.conflictCount() == 0 && -> there COULD be conflicts for variant if directory existence check fails, but loading sync.ffs_db succeeds + //https://sourceforge.net/tracker/?func=detail&atid=1093080&aid=3531351&group_id=234430 -> fixed, but still better not consider conflicts! + if (folderStatus != BaseFolderStatus::existing) //avoid race-condition: we need to evaluate existence status from time of comparison! + { + callback.reportFatalError(replaceCpy(_("Source folder %x not found."), L"%x", fmtPath(AFS::getDisplayPath(baseFolderPath)))); + return true; + } + return false; + }; + if (sourceFolderMissing(baseFolder.getAbstractPath(), baseFolder.getFolderStatus()) || + sourceFolderMissing(baseFolder.getAbstractPath(), baseFolder.getFolderStatus())) + { + skipFolderPair[folderIndex] = true; + continue; + } + + //check if user-defined directory for deletion was specified + const AbstractPath versioningFolderPath = createAbstractPath(folderPairCfg.versioningFolderPhrase); + + if (folderPairCfg.handleDeletion == DeletionVariant::versioning) + if (AFS::isNullPath(versioningFolderPath)) + { + callback.reportFatalError(_("Please enter a target folder.")); //user should never see this: already checked in SyncCfgDialog + skipFolderPair[folderIndex] = true; + continue; + } + + //================= Warnings (*after* folder pair skips) ================= + //======================================================================== + + //prepare conflict preview: + if (folderPairStat.conflictCount() > 0) + checkUnresolvedConflicts.emplace_back(&baseFolder, folderPairStat.conflictCount(), folderPairStat.getConflictsPreview()); + + //prepare: check if some files are used by multiple pairs in read/write access + const bool writeLeft = folderPairStat.createCount() + + folderPairStat.updateCount() + + folderPairStat.deleteCount() > 0; + + const bool writeRight = folderPairStat.createCount() + + folderPairStat.updateCount() + + folderPairStat.deleteCount() > 0; + + checkBaseFolderRaceCondition.emplace_back(&baseFolder, SelectSide::left, writeLeft); + checkBaseFolderRaceCondition.emplace_back(&baseFolder, SelectSide::right, writeRight); + + //prepare: check if versioning path itself will be synchronized (and was not excluded via filter) + if (folderPairCfg.handleDeletion == DeletionVariant::versioning) + checkVersioningPaths.insert(versioningFolderPath); + + checkVersioningBasePaths.emplace_back(baseFolder.getAbstractPath(), &baseFolder.getFilter()); + checkVersioningBasePaths.emplace_back(baseFolder.getAbstractPath(), &baseFolder.getFilter()); + + //prepare: versioning folder paths differing only in case + if (folderPairCfg.handleDeletion == DeletionVariant::versioning && + folderPairCfg.versioningStyle != VersioningStyle::replace) + if (folderPairCfg.versionMaxAgeDays > 0 || folderPairCfg.versionCountMax > 0) //same check as in applyVersioningLimit() + checkVersioningLimitPaths.insert(versioningFolderPath); + + //check if more than 50% of total number of files/dirs will be created/overwritten/deleted + if (significantDifferenceDetected(folderPairStat)) + checkSignificantDiffPairs.emplace_back(baseFolder.getAbstractPath(), + baseFolder.getAbstractPath()); + + //check for sufficient free diskspace (folderPath might not yet exist!) + auto checkSpace = [&](const AbstractPath& baseFolderPath, int64_t minSpaceNeeded) + { + if (minSpaceNeeded > 0) + try + { + const int64_t freeSpace = AFS::getFreeDiskSpace(baseFolderPath); //throw FileError, returns < 0 if not available + + if (0 <= freeSpace && + freeSpace < minSpaceNeeded) + checkDiskSpaceMissing.push_back({baseFolderPath, {minSpaceNeeded, freeSpace}}); + } + catch (const FileError& e) //not critical => log only + { + callback.logMessage(e.toString(), PhaseCallback::MsgType::warning); //throw X + } + }; + const std::pair spaceNeeded = MinimumDiskSpaceNeeded::calculate(baseFolder); + + if (baseFolder.getFolderStatus() != BaseFolderStatus::failure) checkSpace(baseFolder.getAbstractPath(), spaceNeeded.first); + if (baseFolder.getFolderStatus() != BaseFolderStatus::failure) checkSpace(baseFolder.getAbstractPath(), spaceNeeded.second); + } + //-------------------------------------------------------------------------------------- + + //check if unresolved conflicts exist + if (!checkUnresolvedConflicts.empty()) + { + //distribute CONFLICTS_PREVIEW_MAX over all pairs, not *per* pair, or else log size with many folder pairs can blow up! + std::vector> conflictPreviewTrim(checkUnresolvedConflicts.size()); + + size_t previewRemain = CONFLICTS_PREVIEW_MAX; + for (size_t i = 0; ; ++i) + { + const size_t previewRemainOld = previewRemain; + + for (size_t j = 0; j < checkUnresolvedConflicts.size(); ++j) + { + const auto& [baseFolder, conflictCount, conflictPreview] = checkUnresolvedConflicts[j]; + + if (i < conflictPreview.size()) + { + conflictPreviewTrim[j].push_back(conflictPreview[i]); + if (--previewRemain == 0) + goto break2; //sigh + } + } + if (previewRemain == previewRemainOld) + break; + } + break2: + + std::wstring msg = _("The following items have unresolved conflicts and will not be synchronized:"); + + auto itPrevi = conflictPreviewTrim.begin(); + for (const auto& [baseFolder, conflictCount, conflictPreview] : checkUnresolvedConflicts) + { + msg += L"\n\n" + _("Folder pair:") + L' ' + + AFS::getDisplayPath(baseFolder->getAbstractPath()) + L" <-> " + + AFS::getDisplayPath(baseFolder->getAbstractPath()); + + for (const std::wstring& conflictMsg : *itPrevi) + msg += L'\n' + conflictMsg; + + if (makeUnsigned(conflictCount) > itPrevi->size()) + msg += L"\n [...] " + replaceCpy(_P("Showing %y of 1 item", "Showing %y of %x items", conflictCount), //%x used as plural form placeholder! + L"%y", formatNumber(itPrevi->size())); + ++itPrevi; + } + + callback.reportWarning(msg, warnings.warnUnresolvedConflicts); //throw X + } + + //check if user accidentally selected wrong directories for sync + if (!checkSignificantDiffPairs.empty()) + { + std::wstring msg = _("The following folders are significantly different. Please check that the correct folders are selected for synchronization."); + + for (const auto& [folderPathL, folderPathR] : checkSignificantDiffPairs) + msg += L"\n\n" + + AFS::getDisplayPath(folderPathL) + L" <-> " + L'\n' + + AFS::getDisplayPath(folderPathR); + + callback.reportWarning(msg, warnings.warnSignificantDifference); //throw X + } + + //check for sufficient free diskspace + if (!checkDiskSpaceMissing.empty()) + { + std::wstring msg = _("Not enough free disk space available in:"); + + for (const auto& [folderPath, space] : checkDiskSpaceMissing) + msg += L"\n\n" + AFS::getDisplayPath(folderPath) + L'\n' + + TAB_SPACE + _("Required:") + L' ' + formatFilesizeShort(space.first) + L'\n' + + TAB_SPACE + _("Available:") + L' ' + formatFilesizeShort(space.second); + + callback.reportWarning(msg, warnings.warnNotEnoughDiskSpace); //throw X + } + + //check if folders are used by multiple pairs in read/write access + { + std::vector pathRaceItems; + + //race condition := multiple accesses of which at least one is a write + //=> use "writeAccess" to reduce list of - not necessarily conflicting - candidates to check (=> perf!) + for (auto it = checkBaseFolderRaceCondition.begin(); it != checkBaseFolderRaceCondition.end(); ++it) + if (const auto& [baseFolder1, side1, writeAccess1] = *it; + writeAccess1) + for (auto it2 = checkBaseFolderRaceCondition.begin(); it2 != checkBaseFolderRaceCondition.end(); ++it2) + { + const auto& [baseFolder2, side2, writeAccess2] = *it2; + + if (!writeAccess2 || + it < it2) //avoid duplicate comparisons + { + //"The Things We Do for [Perf]" + /**/ if (side1 == SelectSide::left && side2 == SelectSide::left ) checkPathRaceCondition(*baseFolder1, *baseFolder2, pathRaceItems); + else if (side1 == SelectSide::left && side2 == SelectSide::right) checkPathRaceCondition(*baseFolder1, *baseFolder2, pathRaceItems); + else if (side1 == SelectSide::right && side2 == SelectSide::left ) checkPathRaceCondition(*baseFolder1, *baseFolder2, pathRaceItems); + else checkPathRaceCondition(*baseFolder1, *baseFolder2, pathRaceItems); + } + } + + removeDuplicates(pathRaceItems); + + //create mapping table for folder pair positions + std::unordered_map folderPairIdxs; + for (size_t folderIndex = 0; folderIndex < folderCmp.size(); ++folderIndex) + folderPairIdxs[&folderCmp[folderIndex].ref()] = folderIndex; + + std::partial_sort(pathRaceItems.begin(), + pathRaceItems.begin() + std::min(pathRaceItems.size(), CONFLICTS_PREVIEW_MAX), + pathRaceItems.end(), [&](const PathRaceItem& lhs, const PathRaceItem& rhs) + { + if (const std::weak_ordering cmp = comparePathNoCase(lhs, rhs); + cmp != std::weak_ordering::equivalent) + return cmp < 0; //1. order by device, and case-insensitive path + + return folderPairIdxs.find(&lhs.fsObj->base())->second < //2. order by folder pair position + folderPairIdxs.find(&rhs.fsObj->base())->second; + }); + + if (!pathRaceItems.empty()) + { + std::wstring msg = _("Some files will be synchronized as part of multiple folder pairs.") + L'\n' + + _("To avoid conflicts, set up exclude filters so that each updated file is included by only one folder pair.") + L"\n\n"; + + auto prevItem = pathRaceItems[0]; + std::for_each(pathRaceItems.begin(), pathRaceItems.begin() + std::min(pathRaceItems.size(), CONFLICTS_PREVIEW_MAX), [&](const PathRaceItem& item) + { + if (comparePathNoCase(item, prevItem) != std::weak_ordering::equivalent) + msg += L"\n"; //visually separate path groups + + msg += formatRaceItem(item) + L"\n"; + prevItem = item; + }); + + if (pathRaceItems.size() > CONFLICTS_PREVIEW_MAX) + msg += L"\n[...] " + replaceCpy(_P("Showing %y of 1 item", "Showing %y of %x items", pathRaceItems.size()), //%x used as plural form placeholder! + L"%y", formatNumber(CONFLICTS_PREVIEW_MAX)); + + msg += L"\n💾: " + _("Write access") + L" 👓: " + _("Read access"); + + callback.reportWarning(msg, warnings.warnDependentBaseFolders); //throw X + } + } + + //check if versioning folder itself will be synchronized (and was not excluded via filter) + { + std::wstring msg; + bool shouldExclude = false; + + for (const AbstractPath& versioningFolderPath : checkVersioningPaths) + { + std::set foldersWithWarnings; //=> at most one msg per base folder (*and* per versioningFolderPath) + + for (const auto& [folderPath, filter] : checkVersioningBasePaths) //may contain duplicate paths, but with *different* hard filter! + if (std::optional pd = getFolderPathDependency(versioningFolderPath, NullFilter(), folderPath, *filter)) + if (const auto [it, inserted] = foldersWithWarnings.insert(folderPath); + inserted) + { + msg += L"\n\n" + + _("Selected folder:") + L" \t" + AFS::getDisplayPath(folderPath) + L'\n' + + _("Versioning folder:") + L" \t" + AFS::getDisplayPath(versioningFolderPath); + + if (pd->itemPathParent == folderPath) //if versioning folder is a subfolder of a base folder + if (!pd->relPath.empty()) //this can be fixed via an exclude filter + { + assert(pd->itemPathParent == folderPath); //otherwise: what the fuck!? + shouldExclude = true; + msg += std::wstring() + L'\n' + + L"⇒ " + _("Exclude:") + L" \t" + utfTo(FILE_NAME_SEPARATOR + pd->relPath + FILE_NAME_SEPARATOR); + } + } + } + if (!msg.empty()) + callback.reportWarning(_("The versioning folder must not be part of the synchronization.") + + (shouldExclude ? L' ' + _("The folder should be excluded via filter.") : L"") + + msg, warnings.warnVersioningFolderPartOfSync); //throw X + } + + //warn if versioning folder paths differ only in case => possible pessimization for applyVersioningLimit() + { + std::map, std::set> ciPathAliases; + + for (const AbstractPath& folderPath : checkVersioningLimitPaths) + ciPathAliases[std::pair(folderPath.afsDevice, folderPath.afsPath.value)].insert(folderPath); + + if (std::any_of(ciPathAliases.begin(), ciPathAliases.end(), [](const auto& item) { return item.second/*aliases*/.size() > 1; })) + { + std::wstring msg = _("The following folder paths differ in case. Please use a single form in order to avoid duplicate accesses."); + for (const auto& [key, aliases] : ciPathAliases) + if (aliases.size() > 1) + { + msg += L'\n'; + for (const AbstractPath& aliasPath : aliases) + msg += L'\n' + AFS::getDisplayPath(aliasPath); + } + callback.reportWarning(msg, warnings.warnFoldersDifferInCase); //throw X + } + //what about /folder and /Folder/subfolder? => yes, inconsistent, but doesn't matter for FFS + } + //-------------------end of basic checks------------------------------------------ + + std::set versionLimitFolders; + + bool recyclerMissingReportOnce = false; //prompt user only *once* per sync, not per failed item! + + class PcbNoThrow : public PhaseCallback + { + public: + explicit PcbNoThrow(ProcessCallback& cb) : cb_(cb) {} + + void updateDataProcessed(int itemsDelta, int64_t bytesDelta) override {} //sync DB/del-handler: logically not part of sync data, so let's ignore + void updateDataTotal (int itemsDelta, int64_t bytesDelta) override {} // + + void requestUiUpdate(bool force) override { try { cb_.requestUiUpdate(force); /*throw X*/} catch (...) {}; } + + void updateStatus(std::wstring&& msg) override { try { cb_.updateStatus(std::move(msg)); /*throw X*/} catch (...) {}; } + void logMessage(const std::wstring& msg, MsgType type) override { try { cb_.logMessage(msg, type); /*throw X*/} catch (...) {}; } + + void reportWarning(const std::wstring& msg, bool& warningActive) override { logMessage(msg, MsgType::warning); /*ignore*/ } + Response reportError (const ErrorInfo& errorInfo) override { logMessage(errorInfo.msg, MsgType::error); return Response::ignore; } + void reportFatalError(const std::wstring& msg) override { logMessage(msg, MsgType::error); /*ignore*/ } + + private: + ProcessCallback& cb_; + } callbackNoThrow(callback); + + try + { + //loop through all directory pairs + for (size_t folderIndex = 0; folderIndex < folderCmp.size(); ++folderIndex) + { + BaseFolderPair& baseFolder = folderCmp[folderIndex].ref(); + const FolderPairSyncCfg& folderPairCfg = syncConfig[folderIndex]; + const SyncStatistics& folderPairStat = folderPairStats[folderIndex]; + + if (skipFolderPair[folderIndex]) //folder pairs may be skipped after fatal errors were found + continue; + + //------------------------------------------------------------------------------------------ + //checking a second time: 1. a long time may have passed since syncing the previous folder pairs! + // 2. expected to be run directly *before* createBaseFolder()! + if (!checkBaseFolderStatus(baseFolder, callback) || + !checkBaseFolderStatus(baseFolder, callback)) + continue; + + //create base folders if not yet existing + if (folderPairStat.createCount() > 0 || folderPairCfg.saveSyncDB) //else: temporary network drop leading to deletions already caught by "sourceFolderMissing" check! + if (!createBaseFolder(baseFolder, copyFilePermissions, callback) || //+ detect temporary network drop!! + !createBaseFolder(baseFolder, copyFilePermissions, callback)) // + continue; + + //------------------------------------------------------------------------------------------ + //update database even when sync is cancelled (or "nothing to sync"): + auto guardDbSave = makeGuard([&] + { + if (folderPairCfg.saveSyncDB) + saveLastSynchronousState(baseFolder, failSafeFileCopy, + callbackNoThrow); + }); + + //------------------------------------------------------------------------------------------ + //execute synchronization recursively + if (getCUD(folderPairStat) > 0) + { + callback.logMessage(_("Synchronizing folder pair:") + L' ' + getVariantNameWithSymbol(folderPairCfg.syncVar) + L'\n' + //throw X + TAB_SPACE + AFS::getDisplayPath(baseFolder.getAbstractPath()) + L'\n' + + TAB_SPACE + AFS::getDisplayPath(baseFolder.getAbstractPath()), PhaseCallback::MsgType::info); + + //guarantee removal of invalid entries (where element is empty on both sides) + ZEN_ON_SCOPE_EXIT(baseFolder.removeDoubleEmpty()); + + bool copyPermissionsFp = false; + tryReportingError([&] + { + copyPermissionsFp = copyFilePermissions && //copy permissions only if asked for and supported by *both* sides! + AFS::supportPermissionCopy(baseFolder.getAbstractPath(), + baseFolder.getAbstractPath()); //throw FileError + }, callback); //throw X + + const AbstractPath versioningFolderPath = createAbstractPath(folderPairCfg.versioningFolderPhrase); + + DeletionHandler delHandlerL(baseFolder.getAbstractPath(), + recyclerMissingReportOnce, + warnings.warnRecyclerMissing, + folderPairCfg.handleDeletion, + versioningFolderPath, + folderPairCfg.versioningStyle, + std::chrono::system_clock::to_time_t(syncStartTime)); + + DeletionHandler delHandlerR(baseFolder.getAbstractPath(), + recyclerMissingReportOnce, + warnings.warnRecyclerMissing, + folderPairCfg.handleDeletion, + versioningFolderPath, + folderPairCfg.versioningStyle, + std::chrono::system_clock::to_time_t(syncStartTime)); + + //always (try to) clean up, even if synchronization is aborted! + auto guardDelCleanup = makeGuard([&] + { + delHandlerL.tryCleanup(callbackNoThrow); + delHandlerR.tryCleanup(callbackNoThrow); + }); + + + FolderPairSyncer::SyncCtx syncCtx = + { + verifyCopiedFiles, copyPermissionsFp, failSafeFileCopy, + delHandlerL, delHandlerR, + }; + FolderPairSyncer::runSync(syncCtx, baseFolder, callback); + + //(try to gracefully) clean up temporary Recycle Bin folders and versioning + delHandlerL.tryCleanup(callback); //throw X + delHandlerR.tryCleanup(callback); // + guardDelCleanup.dismiss(); + + if (folderPairCfg.handleDeletion == DeletionVariant::versioning && + folderPairCfg.versioningStyle != VersioningStyle::replace) + versionLimitFolders.insert( + { + versioningFolderPath, + folderPairCfg.versionMaxAgeDays, + folderPairCfg.versionCountMin, + folderPairCfg.versionCountMax + }); + } + + //(try to gracefully) write database file + if (folderPairCfg.saveSyncDB) + { + saveLastSynchronousState(baseFolder, failSafeFileCopy, + callback /*throw X*/); //throw X + guardDbSave.dismiss(); //[!] dismiss *after* "graceful" try: user might cancel during DB write: ensure DB is still written + } + } + //----------------------------------------------------------------------------------------------------- + + applyVersioningLimit(versionLimitFolders, + callback /*throw X*/); //throw X + } + catch (const std::exception& e) + { + callback.reportFatalError(utfTo(e.what())); + } +} diff --git a/FreeFileSync/Source/base/synchronization.h b/FreeFileSync/Source/base/synchronization.h new file mode 100644 index 0000000..063563b --- /dev/null +++ b/FreeFileSync/Source/base/synchronization.h @@ -0,0 +1,103 @@ +// ***************************************************************************** +// * 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 SYNCHRONIZATION_H_8913470815943295 +#define SYNCHRONIZATION_H_8913470815943295 + +#include +#include "structures.h" +#include "file_hierarchy.h" +#include "process_callback.h" + + +namespace fff +{ +class SyncStatistics //count *logical* operations, (create, update, delete + bytes), *not* disk accesses! +{ + //-> note the fundamental difference compared to counting disk accesses! +public: + explicit SyncStatistics(const FolderComparison& folderCmp); + explicit SyncStatistics(const ContainerObject& conObj); + explicit SyncStatistics(const FilePair& file); + + template + int createCount() const { return selectParam(createLeft_, createRight_); } + int createCount() const { return createLeft_ + createRight_; } + + template + int updateCount() const { return selectParam(updateLeft_, updateRight_); } + int updateCount() const { return updateLeft_ + updateRight_; } + + template + int deleteCount() const { return selectParam(deleteLeft_, deleteRight_); } + int deleteCount() const { return deleteLeft_ + deleteRight_; } + + int64_t getBytesToProcess() const { return bytesToProcess_; } + size_t rowCount () const { return rowsTotal_; } + + const std::vector& getConflictsPreview() const { return conflictsPreview_; } + int conflictCount() const { return conflictCount_; } + +private: + void recurse(const ContainerObject& conObj); + void logConflict(const FileSystemObject& fsObj); + + void processFile (const FilePair& file); + void processLink (const SymlinkPair& symlink); + void processFolder(const FolderPair& folder); + + int createLeft_ = 0; + int createRight_ = 0; + int updateLeft_ = 0; + int updateRight_ = 0; + int deleteLeft_ = 0; + int deleteRight_ = 0; + + int64_t bytesToProcess_ = 0; + size_t rowsTotal_ = 0; + + int conflictCount_ = 0; + std::vector conflictsPreview_; //conflict texts to display as a warning message + //limit conflict count! e.g. there may be hundred thousands of "same date but a different size" +}; + + +inline +int getCUD(const SyncStatistics& stat) +{ + return stat.createCount() + + stat.updateCount() + + stat.deleteCount(); +} + +struct FolderPairSyncCfg +{ + SyncVariant syncVar; + bool saveSyncDB; //save database if in automatic mode or dection of moved files is active + DeletionVariant handleDeletion; + Zstring versioningFolderPhrase; //unresolved directory names as entered by user! + VersioningStyle versioningStyle; + int versionMaxAgeDays; + int versionCountMin; + int versionCountMax; +}; +std::vector extractSyncCfg(const MainConfiguration& mainCfg); + + +//FFS core routine: +void synchronize(const std::chrono::system_clock::time_point& syncStartTime, + bool verifyCopiedFiles, + bool copyLockedFiles, + bool copyFilePermissions, + bool failSafeFileCopy, + bool runWithBackgroundPriority, + const std::vector& syncConfig, //CONTRACT: syncConfig and folderCmp correspond row-wise! + FolderComparison& folderCmp, // + WarningDialogs& warnings, + ProcessCallback& callback /*throw X*/); //throw X +} + +#endif //SYNCHRONIZATION_H_8913470815943295 diff --git a/FreeFileSync/Source/base/versioning.cpp b/FreeFileSync/Source/base/versioning.cpp new file mode 100644 index 0000000..d26b778 --- /dev/null +++ b/FreeFileSync/Source/base/versioning.cpp @@ -0,0 +1,615 @@ +// ***************************************************************************** +// * This file is part of the FreeFileSync project. It is distributed under * +// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * +// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * +// ***************************************************************************** + +#include "versioning.h" +#include "parallel_scan.h" +#include "status_handler_impl.h" +#include "dir_exist_async.h" + +using namespace zen; +using namespace fff; + + +namespace +{ +inline +Zstring getDotExtension(const Zstring& filePath) //including "." if extension is existing, returns empty string otherwise +{ + //const Zstring& extension = getFileExtension(filePath); + //return extension.empty() ? extension : Zstr('.') + extension; + + auto it = findLast(filePath.begin(), filePath.end(), FILE_NAME_SEPARATOR); + if (it == filePath.end()) + it = filePath.begin(); + else + ++it; + + return Zstring(findLast(it, filePath.end(), Zstr('.')), filePath.end()); +} +} + + +//e.g. "Sample.txt 2012-05-15 131513.txt" +//or "Sample 2012-05-15 131513" +std::pair fff::impl::parseVersionedFileName(const Zstring& fileName) +{ + const auto ext = makeStringView(findLast(fileName.begin(), fileName.end(), Zstr('.')), fileName.end()); + + if (fileName.size() < 2 * ext.length() + 18) + return {}; + + const auto itExt1 = fileName.end() - (2 * ext.length() + 18); + if (!equalString(ext, makeStringView(itExt1, ext.length()))) + return {}; + + const auto itTs = itExt1 + ext.length(); + const TimeComp tc = parseTime(Zstr(" %Y-%m-%d %H%M%S"), makeStringView(itTs, 18)); //returns TimeComp() on error + + const auto [localTime, timeValid] = localToTimeT(tc); + if (!timeValid) + return {}; + + Zstring fileNameOrig(fileName.begin(), itTs); + if (fileNameOrig.empty()) + return {}; + + return {localTime, std::move(fileNameOrig)}; +} + + +//e.g. "2012-05-15 131513" +time_t fff::impl::parseVersionedFolderName(const Zstring& folderName) +{ + const TimeComp tc = parseTime(Zstr("%Y-%m-%d %H%M%S"), folderName); //returns TimeComp() on error + + const auto [localTime, timeValid] = localToTimeT(tc); + if (!timeValid) + return 0; + + return localTime; +} + + +AbstractPath FileVersioner::generateVersionedPath(const Zstring& relativePath) const +{ + assert(isValidRelPath(relativePath)); + assert(!relativePath.empty()); + + Zstring versionedRelPath; + switch (versioningStyle_) + { + case VersioningStyle::replace: + versionedRelPath = relativePath; + break; + case VersioningStyle::timestampFolder: + versionedRelPath = timeStamp_ + FILE_NAME_SEPARATOR + relativePath; + break; + case VersioningStyle::timestampFile: //assemble time-stamped version name + versionedRelPath = relativePath + Zstr(' ') + timeStamp_ + getDotExtension(relativePath); + assert(impl::parseVersionedFileName(getItemName(versionedRelPath)) == + std::pair(syncStartTime_, getItemName(relativePath))); + (void)syncStartTime_; //clang: -Wunused-private-field + break; + } + return AFS::appendRelPath(versioningFolderPath_, versionedRelPath); +} + + +namespace +{ +/* move source to target across volumes: + - source is expected to exist + - if target already exists, it is overwritten, unless it is of a different type, e.g. a directory! + - target parent directories are created if missing */ +template +void moveExistingItemToVersioning(const AbstractPath& sourcePath, const AbstractPath& targetPath, //throw FileError + Function copyNewItemPlain /*throw FileError*/) +{ + //start deleting existing target as required by copyFileTransactional()/moveAndRenameItem(): + //best amortized performance if "already existing" is the most common case + std::exception_ptr deletionError; + try { AFS::removeFilePlain(targetPath); /*throw FileError*/ } + catch (FileError&) { deletionError = std::current_exception(); } //probably "not existing" error, defer evaluation + //overwrite AFS::ItemType::folder with FILE? => highly dubious, do not allow + + auto fixTargetPathIssues = [&](const FileError& prevEx) //throw FileError + { + bool alreadyExisting = false; + try + { + AFS::getItemType(targetPath); //throw FileError + alreadyExisting = true; + } + catch (FileError&) {} //=> not yet existing (=> fine, no path issue) or access error: + //- let's pretend it doesn't happen :> if it does, worst case: the retry fails with (useless) already existing error + //- AFS::itemExists()? too expensive, considering that "already existing" is the most common case + + if (alreadyExisting) + { + if (deletionError) + std::rethrow_exception(deletionError); + throw prevEx; //yes, slicing, but not relevant here + } + + //parent folder missing => create + retry + //parent folder existing => maybe created shortly after move attempt by parallel thread! => retry + if (const std::optional targetParentPath = AFS::getParentPath(targetPath)) + AFS::createFolderIfMissingRecursion(*targetParentPath); //throw FileError + }; + + try //first try to move directly without copying + { + //already existing: undefined behavior! (e.g. fail/overwrite) + AFS::moveAndRenameItem(sourcePath, targetPath); //throw FileError, ErrorMoveUnsupported + //great, we get away cheaply! + } + catch (ErrorMoveUnsupported&) + { + try + { + copyNewItemPlain(); //throw FileError + } + catch (const FileError& e) + { + fixTargetPathIssues(e); //throw FileError + + //retry: + copyNewItemPlain(); //throw FileError + } + //[!] remove source file AFTER handling target path errors! + AFS::removeFilePlain(sourcePath); //throw FileError + } + catch (const FileError& e) + { + fixTargetPathIssues(e); //throw FileError + + try //retry + { + //already existing: undefined behavior! (e.g. fail/overwrite) + AFS::moveAndRenameItem(sourcePath, targetPath); //throw FileError, ErrorMoveUnsupported + } + catch (ErrorMoveUnsupported&) + { + copyNewItemPlain(); //throw FileError + AFS::removeFilePlain(sourcePath); //throw FileError + } + } +} +} + + +void FileVersioner::checkPathConflict(const AbstractPath& itemPath, const Zstring& relativePath) const //throw FileError +{ + if (std::optional pd = getPathDependency(itemPath, versioningFolderPath_)) + { + assert(pd->itemPathParent == versioningFolderPath_); //otherwise: what the fuck!? + //user ignored warning about versioning folder being part of sync => + //prevent files from being moved to versioning recursively: + throw FileError(trimCpy(replaceCpy(replaceCpy(_("Cannot move %x to %y."), + L"%x", L'\n' + fmtPath(AFS::getDisplayPath(itemPath))), + L"%y", L'\n' + fmtPath(AFS::getDisplayPath(generateVersionedPath(relativePath))))), + _("Item already located in the versioning folder.")); + } +} + + +void FileVersioner::revisionFile(const FileDescriptor& fileDescr, const Zstring& relativePath, const IoCallback& notifyUnbufferedIO /*throw X*/) const //throw FileError, X +{ + checkPathConflict(fileDescr.path, relativePath); //throw FileError + + if (const std::optional type = AFS::getItemTypeIfExists(fileDescr.path)) //throw FileError + { + assert(*type != AFS::ItemType::symlink); + + if (*type == AFS::ItemType::symlink) + revisionSymlinkImpl(fileDescr.path, relativePath, nullptr /*onBeforeMove*/); //throw FileError + else + revisionFileImpl(fileDescr, relativePath, nullptr /*onBeforeMove*/, notifyUnbufferedIO); //throw FileError, X + } + //else -> missing source item is not an error => check BEFORE deleting target +} + + +void FileVersioner::revisionFileImpl(const FileDescriptor& fileDescr, const Zstring& relativePath, //throw FileError, X + const std::function& onBeforeMove, + const IoCallback& notifyUnbufferedIO /*throw X*/) const +{ + const AbstractPath& filePath = fileDescr.path; + + const AbstractPath targetPath = generateVersionedPath(relativePath); + const AFS::StreamAttributes fileAttr{fileDescr.attr.modTime, fileDescr.attr.fileSize, fileDescr.attr.filePrint}; + + if (onBeforeMove) + onBeforeMove(AFS::getDisplayPath(filePath), AFS::getDisplayPath(targetPath)); + + moveExistingItemToVersioning(filePath, targetPath, [&] //throw FileError + { + //already existing: undefined behavior! (e.g. fail/overwrite/auto-rename) + //=> not expected, but possible if target deletion failed + //already existing + no onDeleteTargetFile: undefined behavior! (e.g. fail/overwrite/auto-rename) + /*const AFS::FileCopyResult result =*/ AFS::copyFileTransactional(filePath, fileAttr, targetPath, //throw FileError, ErrorFileLocked, X + false, //copyFilePermissions + false, //transactionalCopy: not needed for versioning! partial copy will be overwritten next time + nullptr /*onDeleteTargetFile*/, notifyUnbufferedIO); + //result.errorModTime? => irrelevant for versioning! + }); +} + + +void FileVersioner::revisionSymlink(const AbstractPath& linkPath, const Zstring& relativePath) const //throw FileError +{ + checkPathConflict(linkPath, relativePath); //throw FileError + + if (AFS::itemExists(linkPath)) //throw FileError + revisionSymlinkImpl(linkPath, relativePath, nullptr /*onBeforeMove*/); //throw FileError + //else -> missing source item is not an error => check BEFORE deleting target +} + + +void FileVersioner::revisionSymlinkImpl(const AbstractPath& linkPath, const Zstring& relativePath, //throw FileError + const std::function& onBeforeMove) const +{ + + const AbstractPath targetPath = generateVersionedPath(relativePath); + + if (onBeforeMove) + onBeforeMove(AFS::getDisplayPath(linkPath), AFS::getDisplayPath(targetPath)); + + moveExistingItemToVersioning(linkPath, targetPath, [&] { AFS::copySymlink(linkPath, targetPath, false /*copy filesystem permissions*/); }); //throw FileError +} + + +void FileVersioner::revisionFolder(const AbstractPath& folderPath, const Zstring& relativePath, //throw FileError, X + const std::function& onBeforeFileMove /*throw X*/, + const std::function& onBeforeFolderMove /*throw X*/, + const IoCallback& notifyUnbufferedIO /*throw X*/) const +{ + checkPathConflict(folderPath, relativePath); //throw FileError + + //no error situation if directory is not existing! manual deletion relies on it! + if (const std::optional type = AFS::getItemTypeIfExists(folderPath)) //throw FileError + { + assert(*type != AFS::ItemType::symlink); + + if (*type == AFS::ItemType::symlink) //on Linux there is just one type of symlink, and since we do revision file symlinks, we should revision dir symlinks as well! + revisionSymlinkImpl(folderPath, relativePath, onBeforeFileMove); //throw FileError + else + revisionFolderImpl(folderPath, relativePath, onBeforeFileMove, onBeforeFolderMove, notifyUnbufferedIO); //throw FileError, X + } + else //even if the folder does not exist anymore, significant I/O work was done => report + if (onBeforeFolderMove) onBeforeFolderMove(AFS::getDisplayPath(folderPath), AFS::getDisplayPath(AFS::appendRelPath(versioningFolderPath_, relativePath))); +} + + +void FileVersioner::revisionFolderImpl(const AbstractPath& folderPath, const Zstring& relPath, //throw FileError, X + const std::function& onBeforeFileMove, + const std::function& onBeforeFolderMove, + const IoCallback& notifyUnbufferedIO /*throw X*/) const +{ + + //create target directories only when needed in moveFileToVersioning(): avoid empty directories! + std::vector folders; + { + std::vector files; + std::vector symlinks; + + AFS::traverseFolder(folderPath, //throw FileError + [&](const AFS::FileInfo& fi) { files .push_back(fi); assert(!files.back().isFollowedSymlink); }, + [&](const AFS::FolderInfo& fi) { folders .push_back(fi); }, + [&](const AFS::SymlinkInfo& si) { symlinks.push_back(si); }); + + for (const AFS::FileInfo& fileInfo : files) + { + const FileDescriptor fileDescr + { + .path = AFS::appendRelPath(folderPath, fileInfo.itemName), + .attr = {fileInfo.modTime, fileInfo.fileSize, fileInfo.filePrint, false /*isFollowedSymlink*/}, + }; + + revisionFileImpl(fileDescr, appendPath(relPath, fileInfo.itemName), onBeforeFileMove, notifyUnbufferedIO); //throw FileError, X + } + + for (const AFS::SymlinkInfo& linkInfo : symlinks) + revisionSymlinkImpl(AFS::appendRelPath(folderPath, linkInfo.itemName), + appendPath(relPath, linkInfo.itemName), onBeforeFileMove); //throw FileError + } + + //move folders recursively + for (const AFS::FolderInfo& folderInfo : folders) + revisionFolderImpl(AFS::appendRelPath(folderPath, folderInfo.itemName), //throw FileError, X + appendPath(relPath, folderInfo.itemName), + onBeforeFileMove, onBeforeFolderMove, notifyUnbufferedIO); + //delete source + if (onBeforeFolderMove) + onBeforeFolderMove(AFS::getDisplayPath(folderPath), AFS::getDisplayPath(AFS::appendRelPath(versioningFolderPath_, relPath))); + + AFS::removeFolderPlain(folderPath); //throw FileError +} + +//########################################################################################### + +namespace +{ +struct VersionInfo +{ + time_t versionTime = 0; + AbstractPath filePath; + bool isSymlink = false; +}; +using VersionInfoMap = std::unordered_map>; //relPathOrig => + +//subfolder\Sample.txt 2012-05-15 131513.txt => subfolder\Sample.txt version:2012-05-15 131513 +//2012-05-15 131513\subfolder\Sample.txt => " " + +void findFileVersions(VersionInfoMap& versions, + const FolderContainer& folderCont, + const AbstractPath& parentFolderPath, + const Zstring& relPathOrigParent, + const time_t* versionTimeParent) +{ + auto addVersion = [&](const Zstring& fileName, const Zstring& fileNameOrig, time_t versionTime, bool isSymlink) + { + const Zstring& relPathOrig = appendPath(relPathOrigParent, fileNameOrig); + const AbstractPath& filePath = AFS::appendRelPath(parentFolderPath, fileName); + + versions[relPathOrig].push_back(VersionInfo{versionTime, filePath, isSymlink}); + }; + + auto extractFileVersion = [&](const Zstring& fileName, bool isSymlink) + { + if (versionTimeParent) //VersioningStyle::timestampFolder + addVersion(fileName, fileName, *versionTimeParent, isSymlink); + else + { + const std::pair vfn = fff::impl::parseVersionedFileName(fileName); + if (vfn.first != 0) //VersioningStyle::timestampFile + addVersion(fileName, vfn.second, vfn.first, isSymlink); + } + }; + + for (const auto& [fileName, attr] : folderCont.files) + extractFileVersion(fileName, false /*isSymlink*/); + + for (const auto& [linkName, attr] : folderCont.symlinks) + extractFileVersion(linkName, true /*isSymlink*/); + + for (const auto& [folderName, attrAndSub] : folderCont.folders) + { + if (relPathOrigParent.empty() && !versionTimeParent) //VersioningStyle::timestampFolder? + { + assert(!versionTimeParent); + const time_t versionTime = fff::impl::parseVersionedFolderName(folderName); + if (versionTime != 0) + { + findFileVersions(versions, attrAndSub.second, + AFS::appendRelPath(parentFolderPath, folderName), + Zstring(), //[!] skip time-stamped folder + &versionTime); + continue; + } + } + + findFileVersions(versions, attrAndSub.second, + AFS::appendRelPath(parentFolderPath, folderName), + appendPath(relPathOrigParent, folderName), + versionTimeParent); + } +} + + +void getFolderItemCount(std::map& folderItemCount, const FolderContainer& folderCont, const AbstractPath& parentFolderPath) +{ + size_t& itemCount = folderItemCount[parentFolderPath]; + itemCount = std::max(itemCount, folderCont.files.size() + folderCont.symlinks.size() + folderCont.folders.size()); + //theoretically possible that the same folder is found in one case with items, in another case empty (due to an error) + //e.g. "subfolder" for versioning folders c:\folder and c:\folder\subfolder + + for (const auto& [folderName, attrAndSub] : folderCont.folders) + getFolderItemCount(folderItemCount, attrAndSub.second, AFS::appendRelPath(parentFolderPath, folderName)); +} +} + + +std::weak_ordering fff::operator<=>(const VersioningLimitFolder& lhs, const VersioningLimitFolder& rhs) +{ + if (const std::weak_ordering cmp = std::tie(lhs.versioningFolderPath, lhs.versionMaxAgeDays) <=> + std::tie(rhs.versioningFolderPath, rhs.versionMaxAgeDays); + cmp != std::weak_ordering::equivalent) + return cmp; + + if (lhs.versionMaxAgeDays > 0) + if (lhs.versionCountMin != rhs.versionCountMin) + return lhs.versionCountMin <=> rhs.versionCountMin; + + return lhs.versionCountMax <=> rhs.versionCountMax; +} + + +void fff::applyVersioningLimit(const std::set& folderLimits, + PhaseCallback& callback /*throw X*/) //throw X +{ + //--------- determine existing folder paths for traversal --------- + std::set foldersToRead; + std::set folderLimitsTmp; + { + std::set pathsToCheck; + + for (const VersioningLimitFolder& vlf : folderLimits) + if (vlf.versionMaxAgeDays > 0 || vlf.versionCountMax > 0) //only analyze versioning folders when needed! + { + pathsToCheck.insert(vlf.versioningFolderPath); + folderLimitsTmp.insert(vlf); + } + + //what if versioning folder paths differ only in case? => perf pessimization, but already checked, see fff::synchronize() + + //we don't want to show an error if version path does not yet exist! + tryReportingError([&] + { + const FolderStatus status = getFolderStatusParallel(pathsToCheck, + false /*authenticateAccess*/, nullptr /*requestPassword*/, callback); //throw X + foldersToRead.clear(); + for (const AbstractPath& folderPath : status.existing) + foldersToRead.insert(DirectoryKey({folderPath, makeSharedRef(), SymLinkHandling::asLink})); + + if (!status.failedChecks.empty()) + { + std::wstring msg = _("Cannot find the following folders:") + L'\n'; + + for (const auto& [folderPath, error] : status.failedChecks) + msg += L'\n' + AFS::getDisplayPath(folderPath); + + msg += L"\n___________________________________________"; + for (const auto& [folderPath, error] : status.failedChecks) + msg += L"\n\n" + replaceCpy(error.toString(), L"\n\n", L'\n'); + + throw FileError(msg); + } + }, callback); //throw X + } + + //--------- traverse all versioning folders --------- + const std::wstring textScanning = _("Searching for old file versions:") + L' '; + + auto onStatusUpdate = [&](const std::wstring& statusLine, int itemsTotal) + { + callback.updateStatus(textScanning + statusLine); //throw X + }; + + const std::map folderBuf = parallelFolderScan(foldersToRead, + [&](const PhaseCallback::ErrorInfo& errorInfo) { return callback.reportError(errorInfo); } /*throw X*/, + onStatusUpdate /*throw X*/, UI_UPDATE_INTERVAL / 2); //every ~25 ms + + //--------- group versions per (original) relative path --------- + std::map versionDetails; //versioningFolderPath => + std::map folderItemCount; // => for determination of empty folders + + for (const auto& [folderKey, folderVal] : folderBuf) + { + const AbstractPath versioningFolderPath = folderKey.folderPath; + + assert(!versionDetails.contains(versioningFolderPath)); + + findFileVersions(versionDetails[versioningFolderPath], + folderVal.folderCont, + versioningFolderPath, + Zstring() /*relPathOrigParent*/, + nullptr /*versionTimeParent*/); + + //determine item count per folder for later detection and removal of empty folders: + getFolderItemCount(folderItemCount, folderVal.folderCont, versioningFolderPath); + + //make sure the versioning folder is never found empty and is not deleted: + ++folderItemCount[versioningFolderPath]; + + //similarly, failed folder traversal should not make folders look empty: + for (const auto& [relPath, errorMsg] : folderVal.failedFolderReads) ++folderItemCount[AFS::appendRelPath(versioningFolderPath, relPath)]; + for (const auto& [relPath, errorMsg] : folderVal.failedItemReads ) ++folderItemCount[AFS::appendRelPath(versioningFolderPath, beforeLast(relPath, FILE_NAME_SEPARATOR, IfNotFoundReturn::none))]; + } + + //--------- calculate excess file versions --------- + std::map itemsToDelete; + + const time_t lastMidnightTime = [] + { + TimeComp tc = getLocalTime(); //returns TimeComp() on error + tc.second = 0; + tc.minute = 0; + tc.hour = 0; + return localToTimeT(tc).first; //0 on error => swallow => no versions trimmed by versionMaxAgeDays + }(); + + for (const VersioningLimitFolder& vlf : folderLimitsTmp) + { + auto it = versionDetails.find(vlf.versioningFolderPath); + if (it != versionDetails.end()) + for (auto& [versioningFolderPath, versions] : it->second) + { + size_t versionsToKeep = versions.size(); + if (vlf.versionMaxAgeDays > 0) + { + const time_t cutOffTime = lastMidnightTime - static_cast(vlf.versionMaxAgeDays) * 24 * 3600; + + versionsToKeep = std::count_if(versions.begin(), versions.end(), [cutOffTime](const VersionInfo& vi) { return vi.versionTime >= cutOffTime; }); + + if (vlf.versionCountMin > 0) + versionsToKeep = std::max(versionsToKeep, vlf.versionCountMin); + } + if (vlf.versionCountMax > 0) + versionsToKeep = std::min(versionsToKeep, vlf.versionCountMax); + + if (versions.size() > versionsToKeep) + { + std::nth_element(versions.begin(), versions.end() - versionsToKeep, versions.end(), + [](const VersionInfo& lhs, const VersionInfo& rhs) { return lhs.versionTime < rhs.versionTime; }); + //oldest versions sorted to the front + + std::for_each(versions.begin(), versions.end() - versionsToKeep, [&](const VersionInfo& vi) + { + itemsToDelete.emplace(vi.filePath, vi.isSymlink); + }); + } + } + } + + //--------- remove excess file versions --------- + Protected&> protFolderItemCount(folderItemCount); + const std::wstring txtRemoving = _("Removing old file versions:") + L' '; + const std::wstring txtDeletingFolder = _("Deleting folder %x"); + + std::function deleteEmptyFolderTask; + deleteEmptyFolderTask = [&txtDeletingFolder, &protFolderItemCount, &deleteEmptyFolderTask](const AbstractPath& folderPath, AsyncCallback& acb) //throw ThreadStopRequest + { + const std::wstring errMsg = tryReportingError([&] //throw ThreadStopRequest + { + acb.updateStatus(replaceCpy(txtDeletingFolder, L"%x", fmtPath(AFS::getDisplayPath(folderPath)))); //throw ThreadStopRequest + AFS::removeEmptyFolderIfExists(folderPath); //throw FileError + }, acb); + + if (errMsg.empty()) + if (const std::optional parentPath = AFS::getParentPath(folderPath)) + { + bool deleteParent = false; + protFolderItemCount.access([&](auto& folderItemCount2) { deleteParent = --folderItemCount2[*parentPath] == 0; }); + if (deleteParent) //we're done here anyway => no need to schedule parent deletion in a separate task! + deleteEmptyFolderTask(*parentPath, acb); //throw ThreadStopRequest + } + }; + + std::vector> parallelWorkload; + + for (const auto& [folderPath, itemCount] : folderItemCount) + if (itemCount == 0) + parallelWorkload.emplace_back(folderPath, [&deleteEmptyFolderTask](ParallelContext& ctx) + { + deleteEmptyFolderTask(ctx.itemPath, ctx.acb); //throw ThreadStopRequest + }); + + for (const auto& [itemPath, isSymlink] : itemsToDelete) + parallelWorkload.emplace_back(itemPath, [isSymlink /*clang bug*/= isSymlink, &txtRemoving, &protFolderItemCount, &deleteEmptyFolderTask](ParallelContext& ctx) //throw ThreadStopRequest + { + const std::wstring errMsg = tryReportingError([&] //throw ThreadStopRequest + { + reportInfo(txtRemoving + AFS::getDisplayPath(ctx.itemPath), ctx.acb); //throw ThreadStopRequest + if (isSymlink) + AFS::removeSymlinkIfExists(ctx.itemPath); //throw FileError + else + AFS::removeFileIfExists(ctx.itemPath); //throw FileError + }, ctx.acb); + + if (errMsg.empty()) + if (const std::optional parentPath = AFS::getParentPath(ctx.itemPath)) + { + bool deleteParent = false; + protFolderItemCount.access([&](auto& folderItemCount2) { deleteParent = --folderItemCount2[*parentPath] == 0; }); + if (deleteParent) + deleteEmptyFolderTask(*parentPath, ctx.acb); //throw ThreadStopRequest + } + }); + + massParallelExecute(parallelWorkload, + Zstr("Versioning Limit"), callback /*throw X*/); //throw X +} diff --git a/FreeFileSync/Source/base/versioning.h b/FreeFileSync/Source/base/versioning.h new file mode 100644 index 0000000..2f852cd --- /dev/null +++ b/FreeFileSync/Source/base/versioning.h @@ -0,0 +1,114 @@ +// ***************************************************************************** +// * 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 VERSIONING_H_8760247652438056 +#define VERSIONING_H_8760247652438056 + +#include +#include +#include +#include "structures.h" +#include "algorithm.h" +#include "../afs/abstract.h" + + +namespace fff +{ +/* e.g. move C:\Source\subdir\Sample.txt -> D:\Revisions\subdir\Sample.txt 2012-05-15 131513.txt + scheme: \\. YYYY-MM-DD HHMMSS. + + - ignores missing source files/dirs + - creates missing intermediate directories + - does not create empty directories + - handles symlinks + - multi-threading: internally synchronized + - replaces already existing target files/dirs (supports retry) + => (unlikely) risk of data loss for naming convention "versioning": + race-condition if multiple folder pairs process the same filepath!! */ + +class FileVersioner +{ +public: + FileVersioner(const AbstractPath& versioningFolderPath, //throw FileError + VersioningStyle versioningStyle, + time_t syncStartTime) : + versioningFolderPath_(versioningFolderPath), + versioningStyle_(versioningStyle), + syncStartTime_(syncStartTime) + { + using namespace zen; + + if (AbstractFileSystem::isNullPath(versioningFolderPath_)) + throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); + + if (timeStamp_.size() != 17) //formatTime() returns empty string on error; unexpected length: e.g. problem in year 10,000! + throw FileError(_("Unable to create time stamp for versioning:") + L" \"" + utfTo(timeStamp_) + L'"'); + } + + //multi-threaded access: internally synchronized! + void revisionFile(const FileDescriptor& fileDescr, //throw FileError, X + const Zstring& relativePath, + //called frequently if move has to revert to copy + delete => see zen::copyFile for limitations when throwing exceptions! + const zen::IoCallback& notifyUnbufferedIO /*throw X*/) const; + + void revisionSymlink(const AbstractPath& linkPath, const Zstring& relativePath) const; //throw FileError + + void revisionFolder(const AbstractPath& folderPath, const Zstring& relPath, //throw FileError, X + const std::function& onBeforeFileMove, /*throw X*/ + const std::function& onBeforeFolderMove, /*throw X*/ + //called frequently if move has to revert to copy + delete => see zen::copyFile for limitations when throwing exceptions! + const zen::IoCallback& notifyUnbufferedIO /*throw X*/) const; + +private: + FileVersioner (const FileVersioner&) = delete; + FileVersioner& operator=(const FileVersioner&) = delete; + + void checkPathConflict(const AbstractPath& itemPath, const Zstring& relativePath) const; //throw FileError + + void revisionFileImpl(const FileDescriptor& fileDescr, const Zstring& relativePath, //throw FileError, X + const std::function& onBeforeMove, + const zen::IoCallback& notifyUnbufferedIO) const; + + void revisionSymlinkImpl(const AbstractPath& linkPath, const Zstring& relativePath, //throw FileError + const std::function& onBeforeMove) const; + + void revisionFolderImpl(const AbstractPath& folderPath, const Zstring& relativePath, + const std::function& onBeforeFileMove, + const std::function& onBeforeFolderMove, + const zen::IoCallback& notifyUnbufferedIO) const; //throw FileError, X + + AbstractPath generateVersionedPath(const Zstring& relativePath) const; + + const AbstractPath versioningFolderPath_; + const VersioningStyle versioningStyle_; + const time_t syncStartTime_; + const Zstring timeStamp_{zen::formatTime(Zstr("%Y-%m-%d %H%M%S"), zen::getLocalTime(syncStartTime_))}; //e.g. "2012-05-15 131513" +}; + +//-------------------------------------------------------------------------------- + +struct VersioningLimitFolder +{ + AbstractPath versioningFolderPath; + int versionMaxAgeDays = 0; //<= 0 := no limit + int versionCountMin = 0; //only used if versionMaxAgeDays > 0 => < versionCountMax (if versionCountMax > 0) + int versionCountMax = 0; //<= 0 := no limit +}; +std::weak_ordering operator<=>(const VersioningLimitFolder& lhs, const VersioningLimitFolder& rhs); + + +void applyVersioningLimit(const std::set& folderLimits, + PhaseCallback& callback /*throw X*/); + + +namespace impl //declare for unit tests: +{ +std::pair parseVersionedFileName (const Zstring& fileName); +time_t parseVersionedFolderName(const Zstring& folderName); +} +} + +#endif //VERSIONING_H_8760247652438056 diff --git a/FreeFileSync/Source/base_tools.cpp b/FreeFileSync/Source/base_tools.cpp new file mode 100644 index 0000000..75152d0 --- /dev/null +++ b/FreeFileSync/Source/base_tools.cpp @@ -0,0 +1,300 @@ +// ***************************************************************************** +// * 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 "base_tools.h" +#include "base/path_filter.h" + +using namespace zen; +using namespace fff; + + +std::vector fff::fromTimeShiftPhrase(const std::wstring_view timeShiftPhrase) +{ + std::vector minutes; + + split2(timeShiftPhrase, [](wchar_t c) { return c == L',' || c == L';' || c == L' '; }, //delimiters + [&minutes](const std::wstring_view block) + { + if (!block.empty()) + { + std::wstring part(block); + replace(part, L'-', L""); //there is no negative shift => treat as positive! + + const unsigned int timeShift = stringTo(beforeFirst(part, L':', IfNotFoundReturn::all)) * 60 + + stringTo(afterFirst (part, L':', IfNotFoundReturn::none)); + if (timeShift > 0) + minutes.push_back(timeShift); + } + }); + removeDuplicates(minutes); + return minutes; +} + + +std::wstring fff::toTimeShiftPhrase(const std::vector& ignoreTimeShiftMinutes) +{ + std::wstring phrase; + for (const unsigned int timeShift : ignoreTimeShiftMinutes) + { + if (!phrase.empty()) + phrase += L", "; + + phrase += numberTo(timeShift / 60); + + if (const unsigned int shiftRem = timeShift % 60; + shiftRem != 0) + phrase += L':' + printNumber(L"%02d", static_cast(shiftRem)); + } + return phrase; +} + + +void fff::logNonDefaultSettings(const GlobalConfig& globalCfg, PhaseCallback& callback) +{ + const GlobalConfig defaultSettings; + std::wstring changedSettingsMsg; + + if (globalCfg.failSafeFileCopy != defaultSettings.failSafeFileCopy) + changedSettingsMsg += L"\n" + (TAB_SPACE + _("Fail-safe file copy")) + L": " + (globalCfg.failSafeFileCopy ? _("Enabled") : _("Disabled")); + + if (globalCfg.copyLockedFiles != defaultSettings.copyLockedFiles) + changedSettingsMsg += L"\n" + (TAB_SPACE + _("Copy locked files")) + L": " + (globalCfg.copyLockedFiles ? _("Enabled") : _("Disabled")); + + if (globalCfg.copyFilePermissions != defaultSettings.copyFilePermissions) + changedSettingsMsg += L"\n" + (TAB_SPACE + _("Copy file access permissions")) + L": " + (globalCfg.copyFilePermissions ? _("Enabled") : _("Disabled")); + + if (globalCfg.fileTimeTolerance != defaultSettings.fileTimeTolerance) + changedSettingsMsg += L"\n" + (TAB_SPACE + _("File time tolerance")) + L": " + formatNumber(globalCfg.fileTimeTolerance); + + if (globalCfg.runWithBackgroundPriority != defaultSettings.runWithBackgroundPriority) + changedSettingsMsg += L"\n" + (TAB_SPACE + _("Run with background priority")) + L": " + (globalCfg.runWithBackgroundPriority ? _("Enabled") : _("Disabled")); + + if (globalCfg.createLockFile != defaultSettings.createLockFile) + changedSettingsMsg += L"\n" + (TAB_SPACE + _("Lock directories during sync")) + L": " + (globalCfg.createLockFile ? _("Enabled") : _("Disabled")); + + if (globalCfg.verifyFileCopy != defaultSettings.verifyFileCopy) + changedSettingsMsg += L"\n" + (TAB_SPACE + _("Verify copied files")) + L": " + (globalCfg.verifyFileCopy ? _("Enabled") : _("Disabled")); + + if (!changedSettingsMsg.empty()) + callback.logMessage(_("Using non-default global settings:") + changedSettingsMsg, PhaseCallback::MsgType::info); //throw X +} + + +namespace +{ +FilterConfig mergeFilterConfig(const FilterConfig& global, const FilterConfig& local) +{ + FilterConfig out = local; + + //hard filter + if (NameFilter::isNull(local.includeFilter, Zstring())) //fancy way of checking for "*" include + out.includeFilter = global.includeFilter; + //else : if both global and local include filters are set, only local filter is preserved + + out.excludeFilter = trimCpy(trimCpy(global.excludeFilter) + Zstr("\n\n") + trimCpy(local.excludeFilter)); + + //soft filter + time_t loctimeFrom = 0; + uint64_t locSizeMinBy = 0; + uint64_t locSizeMaxBy = 0; + resolveUnits(out.timeSpan, out.unitTimeSpan, + out.sizeMin, out.unitSizeMin, + out.sizeMax, out.unitSizeMax, + loctimeFrom, //unit: UTC time, seconds + locSizeMinBy, //unit: bytes + locSizeMaxBy); //unit: bytes + + //soft filter + time_t glotimeFrom = 0; + uint64_t gloSizeMinBy = 0; + uint64_t gloSizeMaxBy = 0; + resolveUnits(global.timeSpan, global.unitTimeSpan, + global.sizeMin, global.unitSizeMin, + global.sizeMax, global.unitSizeMax, + glotimeFrom, + gloSizeMinBy, + gloSizeMaxBy); + + if (glotimeFrom > loctimeFrom) + { + out.timeSpan = global.timeSpan; + out.unitTimeSpan = global.unitTimeSpan; + } + if (gloSizeMinBy > locSizeMinBy) + { + out.sizeMin = global.sizeMin; + out.unitSizeMin = global.unitSizeMin; + } + if (gloSizeMaxBy < locSizeMaxBy) + { + out.sizeMax = global.sizeMax; + out.unitSizeMax = global.unitSizeMax; + } + return out; +} + + +inline +bool effectivelyEmpty(const LocalPairConfig& lpc) +{ + return AFS::isNullPath(createAbstractPath(lpc.folderPathPhraseLeft)) && + AFS::isNullPath(createAbstractPath(lpc.folderPathPhraseRight)); +} +} + + +FfsGuiConfig fff::merge(const std::vector& guiCfgs) +{ + assert(!guiCfgs.empty()); + if (guiCfgs.empty()) + return FfsGuiConfig(); + + if (guiCfgs.size() == 1) // + return guiCfgs[0]; //return "as is" + + //merge folder pair config + std::vector mergedCfgs; + + for (const FfsGuiConfig& guiCfg : guiCfgs) + { + std::vector tmpCfgs; + + //skip empty folder pairs + if (!effectivelyEmpty(guiCfg.mainCfg.firstPair)) + tmpCfgs.push_back(guiCfg.mainCfg.firstPair); + + for (const LocalPairConfig& lpc : guiCfg.mainCfg.additionalPairs) + if (!effectivelyEmpty(lpc)) + tmpCfgs.push_back(lpc); + + //move all configuration down to item level + for (LocalPairConfig& lpc : tmpCfgs) + { + if (!lpc.localCmpCfg) + lpc.localCmpCfg = guiCfg.mainCfg.cmpCfg; + + if (!lpc.localSyncCfg) + lpc.localSyncCfg = guiCfg.mainCfg.syncCfg; + + lpc.localFilter = mergeFilterConfig(guiCfg.mainCfg.globalFilter, lpc.localFilter); + } + append(mergedCfgs, tmpCfgs); + } + + if (mergedCfgs.empty()) + mergedCfgs.emplace_back(); + + //optimization: remove redundant configuration + + //######################################################################################################################## + //find out which comparison and synchronization setting are used most often and use them as new "header" + std::vector> cmpCfgStat; + std::vector> syncCfgStat; + for (const LocalPairConfig& lpc : mergedCfgs) + { + //a rather inefficient algorithm, but it does not require a less-than operator: + { + const CompConfig& cmpCfg = *lpc.localCmpCfg; + + auto it = std::find_if(cmpCfgStat.begin(), cmpCfgStat.end(), + [&](const std::pair& entry) { return effectivelyEqual(entry.first, cmpCfg); }); + if (it == cmpCfgStat.end()) + cmpCfgStat.emplace_back(cmpCfg, 1); + else + ++(it->second); + } + { + const SyncConfig& syncCfg = *lpc.localSyncCfg; + + auto it = std::find_if(syncCfgStat.begin(), syncCfgStat.end(), + [&](const std::pair& entry) { return effectivelyEqual(entry.first, syncCfg); }); + if (it == syncCfgStat.end()) + syncCfgStat.emplace_back(syncCfg, 1); + else + ++(it->second); + } + } + + //set most-used comparison and synchronization settings as new header options + const CompConfig cmpCfgHead = cmpCfgStat.empty() ? CompConfig() : + std::max_element(cmpCfgStat.begin(), cmpCfgStat.end(), + [](const std::pair& lhs, const std::pair& rhs) { return lhs.second < rhs.second; })->first; + + const SyncConfig syncCfgHead = syncCfgStat.empty() ? SyncConfig() : + std::max_element(syncCfgStat.begin(), syncCfgStat.end(), + [](const std::pair& lhs, const std::pair& rhs) { return lhs.second < rhs.second; })->first; + //######################################################################################################################## + + FilterConfig globalFilter; + const bool allFiltersEqual = std::all_of(mergedCfgs.begin(), mergedCfgs.end(), [&](const LocalPairConfig& lpc) { return lpc.localFilter == mergedCfgs[0].localFilter; }); + if (allFiltersEqual) + globalFilter = mergedCfgs[0].localFilter; + + //strip redundancy... + for (LocalPairConfig& lpc : mergedCfgs) + { + //if local config matches output global config we don't need local one + if (lpc.localCmpCfg && + effectivelyEqual(*lpc.localCmpCfg, cmpCfgHead)) + lpc.localCmpCfg = {}; + + if (lpc.localSyncCfg && + effectivelyEqual(*lpc.localSyncCfg, syncCfgHead)) + lpc.localSyncCfg = {}; + + if (allFiltersEqual) //use global filter in this case + lpc.localFilter = FilterConfig(); + } + + std::map mergedParallelOps; + for (const FfsGuiConfig& guiCfg : guiCfgs) + for (const auto& [rootPath, parallelOps] : guiCfg.mainCfg.deviceParallelOps) + mergedParallelOps[rootPath] = std::max(mergedParallelOps[rootPath], parallelOps); + + //final assembly + FfsGuiConfig cfgOut + { + .mainCfg = MainConfiguration + { + .cmpCfg = cmpCfgHead, + .syncCfg = syncCfgHead, + .globalFilter = globalFilter, + .firstPair = mergedCfgs[0], + .deviceParallelOps = mergedParallelOps, + + .ignoreErrors = std::all_of(guiCfgs.begin(), guiCfgs.end(), [](const FfsGuiConfig& guiCfg) { return guiCfg.mainCfg.ignoreErrors; }), + + .autoRetryCount = std::max_element(guiCfgs.begin(), guiCfgs.end(), + [](const FfsGuiConfig& lhs, const FfsGuiConfig& rhs) { return lhs.mainCfg.autoRetryCount < rhs.mainCfg.autoRetryCount; })->mainCfg.autoRetryCount, + + .autoRetryDelay = std::max_element(guiCfgs.begin(), guiCfgs.end(), + [](const FfsGuiConfig& lhs, const FfsGuiConfig& rhs) { return lhs.mainCfg.autoRetryDelay < rhs.mainCfg.autoRetryDelay; })->mainCfg.autoRetryDelay, + } + }; + cfgOut.mainCfg.additionalPairs.assign(mergedCfgs.begin() + 1, mergedCfgs.end()); + + + for (const FfsGuiConfig& guiCfg : guiCfgs) + { + if (cfgOut.mainCfg.altLogFolderPathPhrase.empty()) + cfgOut.mainCfg.altLogFolderPathPhrase = guiCfg.mainCfg.altLogFolderPathPhrase; + + if (cfgOut.mainCfg.emailNotifyAddress.empty()) + { + cfgOut.mainCfg.emailNotifyAddress = guiCfg.mainCfg.emailNotifyAddress; + cfgOut.mainCfg.emailNotifyCondition = guiCfg.mainCfg.emailNotifyCondition; + } + + if (!guiCfg.notes.empty()) + cfgOut.notes += guiCfg.notes + L"\n\n"; + } + trim(cfgOut.notes); + + //cfgOut.mainCfg.postSyncCommand = -> better leave at default ... !? + //cfgOut.mainCfg.postSyncCondition = -> + //cfgOut.gridViewType -> + return cfgOut; +} diff --git a/FreeFileSync/Source/base_tools.h b/FreeFileSync/Source/base_tools.h new file mode 100644 index 0000000..c8d47ca --- /dev/null +++ b/FreeFileSync/Source/base_tools.h @@ -0,0 +1,28 @@ +// ***************************************************************************** +// * 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 STRUCTURE_TOOLS_H_7823097420397434 +#define STRUCTURE_TOOLS_H_7823097420397434 + +#include "base/structures.h" +#include "base/process_callback.h" +#include "config.h" + + +namespace fff +{ +//convert "ignoreTimeShiftMinutes" into compact format: +std::vector fromTimeShiftPhrase(const std::wstring_view timeShiftPhrase); +std::wstring toTimeShiftPhrase (const std::vector& ignoreTimeShiftMinutes); + +//inform about (important) non-default global settings related to comparison and synchronization +void logNonDefaultSettings(const GlobalConfig& globalCfg, PhaseCallback& callback); + +//facilitate drag & drop config merge: +FfsGuiConfig merge(const std::vector& guiCfgs); +} + +#endif //STRUCTURE_TOOLS_H_7823097420397434 diff --git a/FreeFileSync/Source/config.cpp b/FreeFileSync/Source/config.cpp new file mode 100644 index 0000000..55828bc --- /dev/null +++ b/FreeFileSync/Source/config.cpp @@ -0,0 +1,2451 @@ +// ***************************************************************************** +// * This file is part of the FreeFileSync project. It is distributed under * +// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * +// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * +// ***************************************************************************** + +#include "config.h" +#include +#include +#include +#include +#include +#include "ffs_paths.h" +#include "base_tools.h" + +using namespace zen; +using namespace fff; //required for correct overload resolution! + + +namespace +{ +//------------------------------------------------------------------------------------------------------------------------------- +const int XML_FORMAT_GLOBAL_CFG = 28; //2025-09-25 +const int XML_FORMAT_SYNC_CFG = 23; //2023-08-24 +//------------------------------------------------------------------------------------------------------------------------------- +} + + +const ExternalApp fff::extCommandFileManager +//"xdg-open %parent_path%" -> not good enough: we need %local_path% for proper MTP/Google Drive handling +{L"Show in file manager", "xdg-open \"$(dirname %local_path%)\""}; +//mark for extraction: _("Show in file manager") Linux doesn't use the term "folder" + + +const ExternalApp fff::extCommandOpenDefault +{L"Open with default application", "xdg-open %local_path%"}; + + + + +GlobalConfig::GlobalConfig() : + soundFileSyncFinished(appendPath(getResourceDirPath(), Zstr("bell.wav"))), + soundFileAlertPending(appendPath(getResourceDirPath(), Zstr("remind.wav"))) +{ +} + +//################################################################################################################ + +Zstring fff::getGlobalConfigDefaultPath() { return appendPath(getConfigDirPath(), Zstr("GlobalSettings.xml")); } +Zstring fff::getLogFolderDefaultPath () { return appendPath(getConfigDirPath(), Zstr("Logs")); } + +namespace +{ +std::vector splitFilterByLines(Zstring filterPhrase) +{ + trim(filterPhrase); + if (filterPhrase.empty()) + return {}; + + return splitCpy(filterPhrase, Zstr('\n'), SplitOnEmpty::allow); +} + +Zstring mergeFilterLines(const std::vector& filterLines) +{ + Zstring out; + for (const Zstring& line : filterLines) + { + out += line; + out += Zstr('\n'); + } + return trimCpy(out); +} +} + +namespace zen +{ +template <> inline +void writeText(const wxLanguage& value, std::string& output) +{ + //use description as unique wxLanguage identifier, see localization.cpp + //=> handle changes to wxLanguage enum between wxWidgets versions + + const wxString& canonicalName = wxUILocale::GetLanguageCanonicalName(value); + assert(!canonicalName.empty()); + if (!canonicalName.empty()) + output = utfTo(canonicalName); + else + output = utfTo(wxUILocale::GetLanguageCanonicalName(wxLANGUAGE_ENGLISH_US)); +} + +template <> inline +bool readText(const std::string& input, wxLanguage& value) +{ + if (const wxLanguageInfo* lngInfo = wxUILocale::FindLanguageInfo(utfTo(input))) + { + value = static_cast(lngInfo->Language); + return true; + } + return false; +} + + +template <> inline +void writeText(const ColorTheme& value, std::string& output) +{ + switch (value) + { + case ColorTheme::System: + output = "Default"; + break; + case ColorTheme::Light: + output = "Light"; + break; + case ColorTheme::Dark: + output = "Dark"; + break; + } +} + +template <> inline +bool readText(const std::string& input, ColorTheme& value) +{ + const std::string tmp = trimCpy(input); + if (tmp == "Default") + value = ColorTheme::System; + else if (tmp == "Light") + value = ColorTheme::Light; + else if (tmp == "Dark") + value = ColorTheme::Dark; + else + return false; + return true; +} + + +template <> inline +void writeText(const CompareVariant& value, std::string& output) +{ + switch (value) + { + case CompareVariant::timeSize: + output = "TimeAndSize"; + break; + case CompareVariant::content: + output = "Content"; + break; + case CompareVariant::size: + output = "Size"; + break; + } +} + +template <> inline +bool readText(const std::string& input, CompareVariant& value) +{ + const std::string tmp = trimCpy(input); + if (tmp == "TimeAndSize") + value = CompareVariant::timeSize; + else if (tmp == "Content") + value = CompareVariant::content; + else if (tmp == "Size") + value = CompareVariant::size; + else + return false; + return true; +} + + +template <> inline +void writeText(const SyncDirection& value, std::string& output) +{ + switch (value) + { + case SyncDirection::left: + output = "left"; + break; + case SyncDirection::right: + output = "right"; + break; + case SyncDirection::none: + output = "none"; + break; + } +} + +template <> inline +bool readText(const std::string& input, SyncDirection& value) +{ + const std::string tmp = trimCpy(input); + if (tmp == "left") + value = SyncDirection::left; + else if (tmp == "right") + value = SyncDirection::right; + else if (tmp == "none") + value = SyncDirection::none; + else + return false; + return true; +} + + +template <> inline +void writeText(const BatchErrorHandling& value, std::string& output) +{ + switch (value) + { + case BatchErrorHandling::showPopup: + output = "Show"; + break; + case BatchErrorHandling::cancel: + output = "Cancel"; + break; + } +} + +template <> inline +bool readText(const std::string& input, ResultsNotification& value) +{ + const std::string tmp = trimCpy(input); + if (tmp == "Always") + value = ResultsNotification::always; + else if (tmp == "ErrorWarning") + value = ResultsNotification::errorWarning; + else if (tmp == "ErrorOnly") + value = ResultsNotification::errorOnly; + else + return false; + return true; +} + + +template <> inline +void writeText(const ResultsNotification& value, std::string& output) +{ + switch (value) + { + case ResultsNotification::always: + output = "Always"; + break; + case ResultsNotification::errorWarning: + output = "ErrorWarning"; + break; + case ResultsNotification::errorOnly: + output = "ErrorOnly"; + break; + } +} + + +template <> inline +bool readText(const std::string& input, BatchErrorHandling& value) +{ + const std::string tmp = trimCpy(input); + if (tmp == "Show") + value = BatchErrorHandling::showPopup; + else if (tmp == "Cancel") + value = BatchErrorHandling::cancel; + else + return false; + return true; +} + + +template <> inline +void writeText(const PostSyncCondition& value, std::string& output) +{ + switch (value) + { + case PostSyncCondition::completion: + output = "Completion"; + break; + case PostSyncCondition::errors: + output = "Errors"; + break; + case PostSyncCondition::success: + output = "Success"; + break; + } +} + +template <> inline +bool readText(const std::string& input, PostSyncCondition& value) +{ + const std::string tmp = trimCpy(input); + if (tmp == "Completion") + value = PostSyncCondition::completion; + else if (tmp == "Errors") + value = PostSyncCondition::errors; + else if (tmp == "Success") + value = PostSyncCondition::success; + else + return false; + return true; +} + + +template <> inline +void writeText(const PostBatchAction& value, std::string& output) +{ + switch (value) + { + case PostBatchAction::none: + output = "None"; + break; + case PostBatchAction::sleep: + output = "Sleep"; + break; + case PostBatchAction::shutdown: + output = "Shutdown"; + break; + } +} + +template <> inline +bool readText(const std::string& input, PostBatchAction& value) +{ + const std::string tmp = trimCpy(input); + if (tmp == "None") + value = PostBatchAction::none; + else if (tmp == "Sleep") + value = PostBatchAction::sleep; + else if (tmp == "Shutdown") + value = PostBatchAction::shutdown; + else + return false; + return true; +} + + +template <> inline +void writeText(const GridIconSize& value, std::string& output) +{ + switch (value) + { + case GridIconSize::small: + output = "Small"; + break; + case GridIconSize::medium: + output = "Medium"; + break; + case GridIconSize::large: + output = "Large"; + break; + } +} + +template <> inline +bool readText(const std::string& input, GridIconSize& value) +{ + const std::string tmp = trimCpy(input); + if (tmp == "Small") + value = GridIconSize::small; + else if (tmp == "Medium") + value = GridIconSize::medium; + else if (tmp == "Large") + value = GridIconSize::large; + else + return false; + return true; +} + + +template <> inline +void writeText(const DeletionVariant& value, std::string& output) +{ + switch (value) + { + case DeletionVariant::permanent: + output = "Permanent"; + break; + case DeletionVariant::recycler: + output = "RecycleBin"; + break; + case DeletionVariant::versioning: + output = "Versioning"; + break; + } +} + +template <> inline +bool readText(const std::string& input, DeletionVariant& value) +{ + const std::string tmp = trimCpy(input); + if (tmp == "Permanent") + value = DeletionVariant::permanent; + else if (tmp == "RecycleBin") + value = DeletionVariant::recycler; + else if (tmp == "Versioning") + value = DeletionVariant::versioning; + else + return false; + return true; +} + + +template <> inline +void writeText(const SymLinkHandling& value, std::string& output) +{ + switch (value) + { + case SymLinkHandling::exclude: + output = "Exclude"; + break; + case SymLinkHandling::asLink: + output = "Direct"; + break; + case SymLinkHandling::follow: + output = "Follow"; + break; + } +} + +template <> inline +bool readText(const std::string& input, SymLinkHandling& value) +{ + const std::string tmp = trimCpy(input); + if (tmp == "Exclude") + value = SymLinkHandling::exclude; + else if (tmp == "Direct") + value = SymLinkHandling::asLink; + else if (tmp == "Follow") + value = SymLinkHandling::follow; + else + return false; + return true; +} + + +template <> inline +void writeText(const GridViewType& value, std::string& output) +{ + switch (value) + { + case GridViewType::difference: + output = "Difference"; + break; + case GridViewType::action: + output = "Action"; + break; + } +} + +template <> inline +bool readText(const std::string& input, GridViewType& value) +{ + const std::string tmp = trimCpy(input); + if (tmp == "Difference") + value = GridViewType::difference; + else if (tmp == "Action") + value = GridViewType::action; + else + return false; + return true; +} + + +template <> inline +void writeText(const ColumnTypeRim& value, std::string& output) +{ + switch (value) + { + case ColumnTypeRim::path: + output = "Path"; + break; + case ColumnTypeRim::size: + output = "Size"; + break; + case ColumnTypeRim::date: + output = "Date"; + break; + case ColumnTypeRim::extension: + output = "Ext"; + break; + } +} + +template <> inline +bool readText(const std::string& input, ColumnTypeRim& value) +{ + const std::string tmp = trimCpy(input); + if (tmp == "Path") + value = ColumnTypeRim::path; + else if (tmp == "Size") + value = ColumnTypeRim::size; + else if (tmp == "Date") + value = ColumnTypeRim::date; + else if (tmp == "Ext") + value = ColumnTypeRim::extension; + else + return false; + return true; +} + + +template <> inline +void writeText(const ItemPathFormat& value, std::string& output) +{ + switch (value) + { + case ItemPathFormat::name: + output = "Item"; + break; + case ItemPathFormat::relative: + output = "Relative"; + break; + case ItemPathFormat::full: + output = "Full"; + break; + } +} + +template <> inline +bool readText(const std::string& input, ItemPathFormat& value) +{ + const std::string tmp = trimCpy(input); + if (tmp == "Item") + value = ItemPathFormat::name; + else if (tmp == "Relative") + value = ItemPathFormat::relative; + else if (tmp == "Full") + value = ItemPathFormat::full; + else + return false; + return true; +} + +template <> inline +void writeText(const ColumnTypeCfg& value, std::string& output) +{ + switch (value) + { + case ColumnTypeCfg::name: + output = "Name"; + break; + case ColumnTypeCfg::lastSync: + output = "Last"; + break; + case ColumnTypeCfg::lastLog: + output = "Log"; + break; + } +} + +template <> inline +bool readText(const std::string& input, ColumnTypeCfg& value) +{ + const std::string tmp = trimCpy(input); + if (tmp == "Name") + value = ColumnTypeCfg::name; + else if (tmp == "Last") + value = ColumnTypeCfg::lastSync; + else if (tmp == "Log") + value = ColumnTypeCfg::lastLog; + else + return false; + return true; +} + + +template <> inline +void writeText(const ColumnTypeOverview& value, std::string& output) +{ + switch (value) + { + case ColumnTypeOverview::folder: + output = "Tree"; + break; + case ColumnTypeOverview::itemCount: + output = "Count"; + break; + case ColumnTypeOverview::bytes: + output = "Bytes"; + break; + } +} + +template <> inline +bool readText(const std::string& input, ColumnTypeOverview& value) +{ + const std::string tmp = trimCpy(input); + if (tmp == "Tree") + value = ColumnTypeOverview::folder; + else if (tmp == "Count") + value = ColumnTypeOverview::itemCount; + else if (tmp == "Bytes") + value = ColumnTypeOverview::bytes; + else + return false; + return true; +} + + +template <> inline +void writeText(const UnitSize& value, std::string& output) +{ + switch (value) + { + case UnitSize::none: + output = "None"; + break; + case UnitSize::byte: + output = "Byte"; + break; + case UnitSize::kb: + output = "KB"; + break; + case UnitSize::mb: + output = "MB"; + break; + } +} + +template <> inline +bool readText(const std::string& input, UnitSize& value) +{ + const std::string tmp = trimCpy(input); + if (tmp == "None") + value = UnitSize::none; + else if (tmp == "Byte") + value = UnitSize::byte; + else if (tmp == "KB") + value = UnitSize::kb; + else if (tmp == "MB") + value = UnitSize::mb; + else + return false; + return true; +} + +template <> inline +void writeText(const UnitTime& value, std::string& output) +{ + switch (value) + { + case UnitTime::none: + output = "None"; + break; + case UnitTime::today: + output = "Today"; + break; + case UnitTime::thisMonth: + output = "Month"; + break; + case UnitTime::thisYear: + output = "Year"; + break; + case UnitTime::lastDays: + output = "x-days"; + break; + } +} + +template <> inline +bool readText(const std::string& input, UnitTime& value) +{ + const std::string tmp = trimCpy(input); + if (tmp == "None") + value = UnitTime::none; + else if (tmp == "Today") + value = UnitTime::today; + else if (tmp == "Month") + value = UnitTime::thisMonth; + else if (tmp == "Year") + value = UnitTime::thisYear; + else if (tmp == "x-days") + value = UnitTime::lastDays; + else + return false; + return true; +} + + +template <> inline +void writeText(const LogFileFormat& value, std::string& output) +{ + switch (value) + { + case LogFileFormat::html: + output = "HTML"; + break; + case LogFileFormat::text: + output = "Text"; + break; + } +} + +template <> inline +bool readText(const std::string& input, LogFileFormat& value) +{ + const std::string tmp = trimCpy(input); + if (tmp == "HTML") + value = LogFileFormat::html; + else if (tmp == "Text") + value = LogFileFormat::text; + else + return false; + return true; +} + + +template <> inline +void writeText(const VersioningStyle& value, std::string& output) +{ + switch (value) + { + case VersioningStyle::replace: + output = "Replace"; + break; + case VersioningStyle::timestampFolder: + output = "TimeStamp-Folder"; + break; + case VersioningStyle::timestampFile: + output = "TimeStamp-File"; + break; + } +} + +template <> inline +bool readText(const std::string& input, VersioningStyle& value) +{ + const std::string tmp = trimCpy(input); + if (tmp == "Replace") + value = VersioningStyle::replace; + else if (tmp == "TimeStamp-Folder") + value = VersioningStyle::timestampFolder; + else if (tmp == "TimeStamp-File") + value = VersioningStyle::timestampFile; + else + return false; + return true; +} + + +template <> inline +void writeStruc(const ColAttributesRim& value, XmlElement& output) +{ + output.setAttribute("Type", value.type); + output.setAttribute("Visible", value.visible); + output.setAttribute("Width", value.offset); + output.setAttribute("Stretch", value.stretch); +} + +template <> inline +bool readStruc(const XmlElement& input, ColAttributesRim& value) +{ + bool success = true; + success = input.getAttribute("Type", value.type) && success; + success = input.getAttribute("Visible", value.visible) && success; + success = input.getAttribute("Width", value.offset) && success; //offset == width if stretch is 0 + success = input.getAttribute("Stretch", value.stretch) && success; + return success; //[!] avoid short-circuit evaluation +} + + +template <> inline +void writeStruc(const ColAttributesCfg& value, XmlElement& output) +{ + output.setAttribute("Type", value.type); + output.setAttribute("Visible", value.visible); + output.setAttribute("Width", value.offset); + output.setAttribute("Stretch", value.stretch); +} + +template <> inline +bool readStruc(const XmlElement& input, ColAttributesCfg& value) +{ + bool success = true; + success = input.getAttribute("Type", value.type) && success; + success = input.getAttribute("Visible", value.visible) && success; + success = input.getAttribute("Width", value.offset) && success; //offset == width if stretch is 0 + success = input.getAttribute("Stretch", value.stretch) && success; + return success; //[!] avoid short-circuit evaluation +} + + +template <> inline +void writeStruc(const ColumnAttribOverview& value, XmlElement& output) +{ + output.setAttribute("Type", value.type); + output.setAttribute("Visible", value.visible); + output.setAttribute("Width", value.offset); + output.setAttribute("Stretch", value.stretch); +} + +template <> inline +bool readStruc(const XmlElement& input, ColumnAttribOverview& value) +{ + bool success = true; + success = input.getAttribute("Type", value.type) && success; + success = input.getAttribute("Visible", value.visible) && success; + success = input.getAttribute("Width", value.offset) && success; //offset == width if stretch is 0 + success = input.getAttribute("Stretch", value.stretch) && success; + return success; //[!] avoid short-circuit evaluation +} + + +template <> inline +void writeStruc(const ExternalApp& value, XmlElement& output) +{ + output.setValue(value.cmdLine); + output.setAttribute("Label", value.description); +} + +template <> inline +bool readStruc(const XmlElement& input, ExternalApp& value) +{ + const bool rv1 = input.getValue(value.cmdLine); + const bool rv2 = input.getAttribute("Label", value.description); + return rv1 && rv2; +} + + +template <> inline +void writeText(const TaskResult& value, std::string& output) +{ + switch (value) + { + case TaskResult::success: + output = "Success"; + break; + case TaskResult::warning: + output = "Warning"; + break; + case TaskResult::error: + output = "Error"; + break; + case TaskResult::cancelled: + output = "Stopped"; + break; + } +} + +template <> inline +bool readText(const std::string& input, TaskResult& value) +{ + const std::string tmp = trimCpy(input); + if (tmp == "Success") + value = TaskResult::success; + else if (tmp == "Warning") + value = TaskResult::warning; + else if (tmp == "Error") + value = TaskResult::error; + else if (tmp == "Stopped") + value = TaskResult::cancelled; + else + return false; + return true; +} +} + + +namespace +{ + + +Zstring makePortablePath(const Zstring& pathPhrase) +{ + const Zstring& pathTrm = trimCpy(pathPhrase); + const Zstring& ffsPath = getInstallDirPath(); + + if (pathTrm == ffsPath) + return Zstr("%ffs_path%"); + + if (startsWith(pathTrm, appendSeparator(ffsPath))) //don't allow *partial* component match! + return Zstring(Zstr("%ffs_path%")) + (pathTrm.c_str() + appendSeparator(ffsPath).size() - 1); + + return pathPhrase; +} + + +Zstring resolvePortablePath(const Zstring& portablePathPhrase) +{ + const Zstring& pathTrm = trimCpy(portablePathPhrase); + + if (startsWith(pathTrm, Zstr("%ffs_path%"))) + return appendPath(getInstallDirPath(), afterFirst(pathTrm, FILE_NAME_SEPARATOR, IfNotFoundReturn::none)); //caveat: appendPath() requires relPath! + + //TODO: remove parameter migration after some time! 2022-06-14 + if (startsWith(pathTrm, Zstr("%ffs_resource%"))) + return appendPath(getResourceDirPath(), afterFirst(pathTrm, FILE_NAME_SEPARATOR, IfNotFoundReturn::none)); + + return portablePathPhrase; +} + + +std::vector makePortablePath(std::vector pathPhrases) +{ + for (Zstring& pathPhrase : pathPhrases) + pathPhrase = makePortablePath(pathPhrase); + return pathPhrases; +} + + +std::vector resolvePortablePath(std::vector pathPhrases) +{ + for (Zstring& pathPhrase : pathPhrases) + pathPhrase = resolvePortablePath(pathPhrase); + return pathPhrases; +} +} + + +namespace zen +{ +template <> inline +bool readStruc(const XmlElement& input, ConfigFileItem& value) +{ + bool success = true; + success = input.getAttribute("LastSync", value.lastRunStats.startTime) && success; + success = input.getAttribute("Result", value.lastRunStats.syncResult) && success; + + if (input.hasAttribute("CfgPath")) //TODO: remove after migration! 2020-02-09 + success = input.getAttribute("CfgPath", value.cfgFilePath) && success; // + else + success = input.getAttribute("Config", value.cfgFilePath) && success; + + //FFS portable: use special syntax for config file paths: e.g. "%ffs_drive%\SyncJob.ffs_gui" + value.cfgFilePath = resolvePortablePath(value.cfgFilePath); + + Zstring logFilePhrase; + if (input.hasAttribute("LogPath")) //TODO: remove after migration! 2020-02-09 + success = input.getAttribute("LogPath", logFilePhrase) && success; // + else + success = input.getAttribute("Log", logFilePhrase) && success; + + value.lastRunStats.logFilePath = createAbstractPath(resolvePortablePath(logFilePhrase)); + + if (!input.hasAttribute("Items")) //TODO: remove after migration! 2023-05-13 + ; + else + success = input.getAttribute("Items", value.lastRunStats.itemsProcessed) && success; + + if (!input.hasAttribute("Bytes")) //TODO: remove after migration! 2023-05-13 + ; + else + success = input.getAttribute("Bytes", value.lastRunStats.bytesProcessed) && success; + + if (!input.hasAttribute("TotalTime")) //TODO: remove after migration! 2023-05-13 + ; + else + success = input.getAttribute("TotalTime", value.lastRunStats.totalTime) && success; + + if (!input.hasAttribute("Errors")) //TODO: remove after migration! 2023-05-13 + ; + else + success = input.getAttribute("Errors", value.lastRunStats.errors) && success; + + if (!input.hasAttribute("Warnings")) //TODO: remove after migration! 2023-05-13 + ; + else + success = input.getAttribute("Warnings", value.lastRunStats.warnings) && success; + + std::string hexColor; //optional XML attribute! + if (input.getAttribute("Color", hexColor) && hexColor.size() == 6) + value.backColor.Set(unhexify(hexColor[0], hexColor[1]), + unhexify(hexColor[2], hexColor[3]), + unhexify(hexColor[4], hexColor[5])); + + return success; //[!] avoid short-circuit evaluation +} + +template <> inline +void writeStruc(const ConfigFileItem& value, XmlElement& output) +{ + output.setAttribute("LastSync", value.lastRunStats.startTime); + output.setAttribute("Result", value.lastRunStats.syncResult); + + output.setAttribute("Config", makePortablePath(value.cfgFilePath)); + output.setAttribute("Log", makePortablePath(AFS::getInitPathPhrase(value.lastRunStats.logFilePath))); + + output.setAttribute("Items", value.lastRunStats.itemsProcessed); + output.setAttribute("Bytes", value.lastRunStats.bytesProcessed); + + output.setAttribute("TotalTime", value.lastRunStats.totalTime); + + output.setAttribute("Errors", value.lastRunStats.errors); + output.setAttribute("Warnings", value.lastRunStats.warnings); + + if (value.backColor.IsOk()) + { + assert(value.backColor.Alpha() == wxALPHA_OPAQUE); + const auto [rh, rl] = hexify(value.backColor.Red ()); + const auto [gh, gl] = hexify(value.backColor.Green()); + const auto [bh, bl] = hexify(value.backColor.Blue ()); + output.setAttribute("Color", std::string({rh, rl, gh, gl, bh, bl})); + } +} +} + + +namespace +{ +void readConfig(const XmlIn& in, CompConfig& cmpCfg) +{ + in["Variant" ](cmpCfg.compareVar); + in["Symlinks"](cmpCfg.handleSymlinks); + + std::wstring timeShiftPhrase; + if (in["IgnoreTimeShift"](timeShiftPhrase)) + cmpCfg.ignoreTimeShiftMinutes = fromTimeShiftPhrase(timeShiftPhrase); +} + + +void readConfig(const XmlIn& in, SyncDirectionConfig& dirCfg, int formatVer) +{ + if (formatVer < 21) //TODO: remove if parameter migration after some time! 2023-08-09 + { + std::string varName; + in["Variant"](varName); + trim(varName); + + if (varName == "TwoWay") + dirCfg = getDefaultSyncCfg(SyncVariant::twoWay); + else if (varName == "Mirror") + { + dirCfg = getDefaultSyncCfg(SyncVariant::mirror); + + bool detectMovedFiles = false; + in["DetectMovedFiles"](detectMovedFiles); + if (detectMovedFiles) + { + if (const DirectionByDiff* diffDirs = std::get_if(&dirCfg.dirs)) + dirCfg.dirs = getChangesDirDefault(*diffDirs); //convert to "changes"-based mirror, so that move detection is enabled + else assert(false); + } + } + else if (varName == "Update") + dirCfg.dirs = DirectionByDiff + { + .leftOnly = SyncDirection::right, + .rightOnly = SyncDirection::none, + .leftNewer = SyncDirection::right, + .rightNewer = SyncDirection::none, //note: will be fixed below for CompareVariant::content/size + }; + else + { + assert(varName == "Custom"); + + dirCfg.dirs = DirectionByDiff(); + + XmlIn inCustDir = in["CustomDirections"]; + inCustDir["LeftOnly" ](std::get(dirCfg.dirs).leftOnly); + inCustDir["RightOnly" ](std::get(dirCfg.dirs).rightOnly); + inCustDir["LeftNewer" ](std::get(dirCfg.dirs).leftNewer); //note: will be fixed below for CompareVariant::content/size + inCustDir["RightNewer"](std::get(dirCfg.dirs).rightNewer); // + } + } + else + { + if (XmlIn inDirs = in["Differences"]) + { + dirCfg.dirs = DirectionByDiff(); + inDirs.attribute("LeftOnly", std::get(dirCfg.dirs).leftOnly); + inDirs.attribute("LeftNewer", std::get(dirCfg.dirs).leftNewer); + inDirs.attribute("RightNewer", std::get(dirCfg.dirs).rightNewer); + inDirs.attribute("RightOnly", std::get(dirCfg.dirs).rightOnly); + } + else + { + assert(in["Changes"]); + dirCfg.dirs = DirectionByChange(); + + XmlIn inDirsL = in["Changes"]["Left"]; + inDirsL.attribute("Create", std::get(dirCfg.dirs).left.create); + inDirsL.attribute("Update", std::get(dirCfg.dirs).left.update); + inDirsL.attribute("Delete", std::get(dirCfg.dirs).left.delete_); + + XmlIn inDirsR = in["Changes"]["Right"]; + inDirsR.attribute("Create", std::get(dirCfg.dirs).right.create); + inDirsR.attribute("Update", std::get(dirCfg.dirs).right.update); + inDirsR.attribute("Delete", std::get(dirCfg.dirs).right.delete_); + } + } +} + + +void readConfig(const XmlIn& in, SyncConfig& syncCfg, std::map& deviceParallelOps, int formatVer) +{ + readConfig(in, syncCfg.directionCfg, formatVer); + + in["DeletionPolicy" ](syncCfg.deletionVariant); + in["VersioningFolder"](syncCfg.versioningFolderPhrase); + + XmlIn verFolder = in["VersioningFolder"]; + + size_t parallelOps = 1; + if (verFolder.hasAttribute("Threads")) //*no error* if not available + verFolder.attribute("Threads", parallelOps); //try to get attribute + + const size_t parallelOpsPrev = getDeviceParallelOps(deviceParallelOps, syncCfg.versioningFolderPhrase); + /**/ setDeviceParallelOps(deviceParallelOps, syncCfg.versioningFolderPhrase, std::max(parallelOps, parallelOpsPrev)); + + in["VersioningFolder"].attribute("Style", syncCfg.versioningStyle); + + if (syncCfg.versioningStyle != VersioningStyle::replace) + { + if (verFolder.hasAttribute("MaxAge")) //try to get attributes if available => *no error* if not available + verFolder.attribute("MaxAge", syncCfg.versionMaxAgeDays); + + if (verFolder.hasAttribute("MinCount")) + verFolder.attribute("MinCount", syncCfg.versionCountMin); // => *no error* if not available + if (verFolder.hasAttribute("MaxCount")) + verFolder.attribute("MaxCount", syncCfg.versionCountMax); // + } +} + + +void readConfig(const XmlIn& in, FilterConfig& filter /*int formatVer? but which one; Filter is used by GlobalConfig and FfsGuiConfig! :( */) +{ + std::vector tmpIn; + if (in["Include"](tmpIn)) //else: keep default value + filter.includeFilter = mergeFilterLines(tmpIn); + + std::vector tmpEx; + if (in["Exclude"](tmpEx)) //else: keep default value + filter.excludeFilter = mergeFilterLines(tmpEx); + + in["SizeMin"](filter.sizeMin); + in["SizeMin"].attribute("Unit", filter.unitSizeMin); + + in["SizeMax"](filter.sizeMax); + in["SizeMax"].attribute("Unit", filter.unitSizeMax); + + in["TimeSpan"](filter.timeSpan); + in["TimeSpan"].attribute("Type", filter.unitTimeSpan); +} + + +void readConfig(const XmlIn& in, LocalPairConfig& lpc, std::map& deviceParallelOps, int formatVer) +{ + //read folder pairs + in["Left" ](lpc.folderPathPhraseLeft); + in["Right"](lpc.folderPathPhraseRight); + + size_t parallelOpsL = 1; + size_t parallelOpsR = 1; + if (in["Left" ].hasAttribute("Threads")) in["Left" ].attribute("Threads", parallelOpsL); //try to get attributes: + if (in["Right"].hasAttribute("Threads")) in["Right"].attribute("Threads", parallelOpsR); // => *no error* if not available + + auto setParallelOps = [&](const Zstring& folderPathPhrase, size_t parallelOps) + { + const size_t parallelOpsPrev = getDeviceParallelOps(deviceParallelOps, folderPathPhrase); + /**/ setDeviceParallelOps(deviceParallelOps, folderPathPhrase, std::max(parallelOps, parallelOpsPrev)); + }; + setParallelOps(lpc.folderPathPhraseLeft, parallelOpsL); + setParallelOps(lpc.folderPathPhraseRight, parallelOpsR); + + //TODO: remove after migration! 2020-04-24 + if (formatVer < 16) + { + replaceAsciiNoCase(lpc.folderPathPhraseLeft, Zstr("%weekday%"), Zstr("%WeekDayName%")); + replaceAsciiNoCase(lpc.folderPathPhraseRight, Zstr("%weekday%"), Zstr("%WeekDayName%")); + } + + //########################################################### + //alternate comp configuration (optional) + if (XmlIn inLocalCmp = in["Compare"]) + { + CompConfig cmpCfg; + readConfig(inLocalCmp, cmpCfg); + + lpc.localCmpCfg = cmpCfg; + } + //########################################################### + //alternate sync configuration (optional) + if (XmlIn inLocalSync = in["Synchronize"]) + { + SyncConfig syncCfg; + readConfig(inLocalSync, syncCfg, deviceParallelOps, formatVer); + + lpc.localSyncCfg = syncCfg; + } + + //########################################################### + //alternate filter configuration + if (XmlIn inLocFilter = in["Filter"]) + readConfig(inLocFilter, lpc.localFilter); +} + + +void readConfig(const XmlIn& in, MainConfiguration& mainCfg, int formatVer) +{ + readConfig(in["Compare"], mainCfg.cmpCfg); + + readConfig(in["Synchronize"], mainCfg.syncCfg, mainCfg.deviceParallelOps, formatVer); + + if (formatVer < 20) //TODO: remove if parameter migration after some time! 2023-08-09 + if (mainCfg.cmpCfg.compareVar == CompareVariant::content || + mainCfg.cmpCfg.compareVar == CompareVariant::size) + if (std::string varName; + in["Synchronize"]["Variant"](varName)) + { + if (varName == "Update") + std::get(mainCfg.syncCfg.directionCfg.dirs).rightNewer = SyncDirection::right; + else if (varName == "Custom") + { + SyncDirection different = SyncDirection::none; + in["Synchronize"]["CustomDirections"]["Different"](different); + + std::get(mainCfg.syncCfg.directionCfg.dirs).leftNewer = + std::get(mainCfg.syncCfg.directionCfg.dirs).rightNewer = different; + } + } + + if (formatVer < 23) //TODO: remove if parameter migration after some time! 2023-08-24 + { + bool detectMovedFiles = false; + in["Synchronize"]["DetectMovedFiles"](detectMovedFiles); + if (detectMovedFiles) + if (getSyncVariant(mainCfg.syncCfg.directionCfg) == SyncVariant::mirror) + { + if (const DirectionByDiff* diffDirs = std::get_if(&mainCfg.syncCfg.directionCfg.dirs)) + mainCfg.syncCfg.directionCfg.dirs = getChangesDirDefault(*diffDirs); //convert to "changes"-based mirror, so that move detection is enabled + else assert(false); + } + } + + readConfig(in["Filter"], mainCfg.globalFilter); + + //########################################################### + //read folder pairs + bool firstItem = true; + in["FolderPairs"].visitChildren([&](const XmlIn& inPair) + { + assert(*inPair.getName() == "Pair"); + + LocalPairConfig lpc; + readConfig(inPair, lpc, mainCfg.deviceParallelOps, formatVer); + + if (formatVer < 20) //TODO: remove if parameter migration after some time! 2023-08-09 + if (lpc.localSyncCfg) + { + const CompConfig& cmpCfg = lpc.localCmpCfg ? *lpc.localCmpCfg : mainCfg.cmpCfg; + if (cmpCfg.compareVar == CompareVariant::content || + cmpCfg.compareVar == CompareVariant::size) + if (std::string varName; + inPair["Synchronize"]["Variant"](varName)) + { + if (varName == "Update") + std::get(lpc.localSyncCfg->directionCfg.dirs).rightNewer = SyncDirection::right; + else if (varName == "Custom") + if (inPair["Synchronize"]["CustomDirections"]["Different"]) + { + SyncDirection different = SyncDirection::none; + inPair["Synchronize"]["CustomDirections"]["Different"](different); + + std::get(lpc.localSyncCfg->directionCfg.dirs).leftNewer = + std::get(lpc.localSyncCfg->directionCfg.dirs).rightNewer = different; + } + } + } + if (formatVer < 23) //TODO: remove if parameter migration after some time! 2023-08-24 + if (lpc.localSyncCfg) + { + bool detectMovedFiles = false; + inPair["Synchronize"]["DetectMovedFiles"](detectMovedFiles); + if (detectMovedFiles) + if (getSyncVariant(lpc.localSyncCfg->directionCfg) == SyncVariant::mirror) + { + if (const DirectionByDiff* diffDirs = std::get_if(&lpc.localSyncCfg->directionCfg.dirs)) + lpc.localSyncCfg->directionCfg.dirs = getChangesDirDefault(*diffDirs); //convert to "changes"-based mirror, so that move detection is enabled + else assert(false); + } + } + + if (firstItem) + { + firstItem = false; + mainCfg.firstPair = lpc; + mainCfg.additionalPairs.clear(); + } + else + mainCfg.additionalPairs.push_back(lpc); + }); + + in["Errors"].attribute("Ignore", mainCfg.ignoreErrors); + in["Errors"].attribute("Retry", mainCfg.autoRetryCount); + in["Errors"].attribute("Delay", mainCfg.autoRetryDelay); + + in["PostSyncCommand"](mainCfg.postSyncCommand); + in["PostSyncCommand"].attribute("Condition", mainCfg.postSyncCondition); + + in["LogFolder"](mainCfg.altLogFolderPathPhrase); + + //TODO: remove after migration! 2020-04-24 + if (formatVer < 16) + replaceAsciiNoCase(mainCfg.altLogFolderPathPhrase, Zstr("%weekday%"), Zstr("%WeekDayName%")); + + //TODO: remove if parameter migration after some time! 2020-01-30 + if (formatVer < 15) + ; + else + { + in["EmailNotification"](mainCfg.emailNotifyAddress); + in["EmailNotification"].attribute("Condition", mainCfg.emailNotifyCondition); + } +} + + +void readConfig(const XmlIn& in, FfsGuiConfig& cfg, int formatVer) +{ + if (formatVer < 18) //TODO: remove if parameter migration after some time! 2023-05-15 + ; + else + in["Notes"](cfg.notes); + + readConfig(in, cfg.mainCfg, formatVer); + + if (formatVer < 19) //TODO: remove after migration! 2023-06-09 + { + XmlIn inGui = in["Gui"]; + //TODO: remove after migration! 2020-10-14 + if (formatVer < 17) + { + if (inGui["MiddleGridView"]) + { + std::string tmp; + inGui["MiddleGridView"](tmp); + + if (tmp == "Category") + cfg.gridViewType = GridViewType::difference; + else if (tmp == "Action") + cfg.gridViewType = GridViewType::action; + } + } + else + inGui["GridViewType"](cfg.gridViewType); + } + else + in["GridViewType"](cfg.gridViewType); +} + + +void readConfig(const XmlIn& in, FfsBatchConfig& cfg, int formatVer) +{ + if (formatVer < 19) //TODO: remove after migration! 2023-06-09 + readConfig(in, cfg.guiCfg.mainCfg, formatVer); + else + readConfig(in, cfg.guiCfg, formatVer); + + XmlIn inBatch = in["Batch"]; + inBatch["ProgressDialog"].attribute("Minimized", cfg.batchExCfg.runMinimized); + inBatch["ProgressDialog"].attribute("AutoClose", cfg.batchExCfg.autoCloseSummary); + inBatch["ErrorDialog"](cfg.batchExCfg.batchErrorHandling); + inBatch["PostSyncAction"](cfg.batchExCfg.postBatchAction); +} + + +void readConfig(const XmlIn& in, GlobalConfig& cfg, int formatVer) +{ + assert(cfg.dpiLayouts.empty()); + + XmlIn in2 = in; + + if (in["General"]) //TODO: remove old parameter after migration! 2020-12-03 + in2 = in["General"]; + + //TODO: remove after migration! 2022-04-18 + if (in2["Language"].hasAttribute("Name")) + { + std::string lngName; + in2["Language"].attribute("Name", lngName); + + if (lngName == "English (US)") + cfg.programLanguage = wxLANGUAGE_ENGLISH_US; + else if (lngName == "Chinese (Simplified)") + cfg.programLanguage = wxLANGUAGE_CHINESE_CHINA; + else if (lngName == "Chinese (Traditional)") + cfg.programLanguage = wxLANGUAGE_CHINESE_TAIWAN; + else if (lngName == "English (U.K.)") + cfg.programLanguage = wxLANGUAGE_ENGLISH_UK; + else if (lngName == "Norwegian (Bokmal)") + cfg.programLanguage = wxLANGUAGE_NORWEGIAN; + else if (lngName == "Portuguese (Brazilian)") + cfg.programLanguage = wxLANGUAGE_PORTUGUESE_BRAZILIAN; + else if (const wxLanguageInfo* lngInfo = wxUILocale::FindLanguageInfo(utfTo(lngName))) + cfg.programLanguage = static_cast(lngInfo->Language); + } + else + in2["Language"].attribute("Code", cfg.programLanguage); + + in2["ColorTheme"].attribute("Appearance", cfg.appColorTheme); + + in2["FailSafeFileCopy" ].attribute("Enabled", cfg.failSafeFileCopy); + in2["CopyLockedFiles" ].attribute("Enabled", cfg.copyLockedFiles); + in2["CopyFilePermissions" ].attribute("Enabled", cfg.copyFilePermissions); + in2["FileTimeTolerance" ].attribute("Seconds", cfg.fileTimeTolerance); + in2["RunWithBackgroundPriority"].attribute("Enabled", cfg.runWithBackgroundPriority); + in2["LockDirectoriesDuringSync"].attribute("Enabled", cfg.createLockFile); + in2["VerifyCopiedFiles" ].attribute("Enabled", cfg.verifyFileCopy); + in2["LogFiles" ].attribute("MaxAge", cfg.logfilesMaxAgeDays); + in2["LogFiles" ].attribute("Format", cfg.logFormat); + + //TODO: remove old parameter after migration! 2021-03-06 + if (formatVer < 21) + { + cfg.dpiLayouts[getDpiScalePercent()].progressDlg.size = wxSize(); + in2["ProgressDialog"].attribute("Width", cfg.dpiLayouts[getDpiScalePercent()].progressDlg.size->x); + in2["ProgressDialog"].attribute("Height", cfg.dpiLayouts[getDpiScalePercent()].progressDlg.size->y); + in2["ProgressDialog"].attribute("Maximized", cfg.dpiLayouts[getDpiScalePercent()].progressDlg.isMaximized); + } + + in2["ProgressDialog"].attribute("AutoClose", cfg.progressDlgAutoClose); + + XmlIn inOpt = in2["OptionalDialogs"]; + inOpt["ConfirmStartSync" ].attribute("Show", cfg.confirmDlgs.confirmSyncStart); + inOpt["ConfirmSaveConfig" ].attribute("Show", cfg.confirmDlgs.confirmSaveConfig); + inOpt["ConfirmSwapSides" ].attribute("Show", cfg.confirmDlgs.confirmSwapSides); + if (formatVer < 12) //TODO: remove old parameter after migration! 2019-02-09 + inOpt["ConfirmExternalCommandMassInvoke"].attribute("Show", cfg.confirmDlgs.confirmCommandMassInvoke); + else + inOpt["ConfirmCommandMassInvoke"].attribute("Show", cfg.confirmDlgs.confirmCommandMassInvoke); + inOpt["WarnFolderNotExisting" ].attribute("Show", cfg.warnDlgs.warnFolderNotExisting); + inOpt["WarnFoldersDifferInCase" ].attribute("Show", cfg.warnDlgs.warnFoldersDifferInCase); + inOpt["WarnUnresolvedConflicts" ].attribute("Show", cfg.warnDlgs.warnUnresolvedConflicts); + inOpt["WarnNotEnoughDiskSpace" ].attribute("Show", cfg.warnDlgs.warnNotEnoughDiskSpace); + inOpt["WarnSignificantDifference" ].attribute("Show", cfg.warnDlgs.warnSignificantDifference); + inOpt["WarnRecycleBinNotAvailable" ].attribute("Show", cfg.warnDlgs.warnRecyclerMissing); + inOpt["WarnDependentFolderPair" ].attribute("Show", cfg.warnDlgs.warnDependentFolderPair); + inOpt["WarnDependentBaseFolders" ].attribute("Show", cfg.warnDlgs.warnDependentBaseFolders); + inOpt["WarnDirectoryLockFailed" ].attribute("Show", cfg.warnDlgs.warnDirectoryLockFailed); + inOpt["WarnVersioningFolderPartOfSync"].attribute("Show", cfg.warnDlgs.warnVersioningFolderPartOfSync); + + //TODO: remove after migration! 2022-08-26 + if (formatVer < 25) + cfg.warnDlgs.warnDependentBaseFolders = true; //new semantics! should not be ignored + + //TODO: remove after migration! 2021-12-02 + if (formatVer < 23) + { + in2["NotificationSound"].attribute("CompareFinished", cfg.soundFileCompareFinished); + in2["NotificationSound"].attribute("SyncFinished", cfg.soundFileSyncFinished); + } + else + { + in2["Sounds"]["CompareFinished"].attribute("Path", cfg.soundFileCompareFinished); + in2["Sounds"]["SyncFinished" ].attribute("Path", cfg.soundFileSyncFinished); + in2["Sounds"]["AlertPending" ].attribute("Path", cfg.soundFileAlertPending); + } + + //TODO: remove if parameter migration after some time! 2019-05-29 + if (formatVer < 13) + { + if (!cfg.soundFileCompareFinished.empty()) cfg.soundFileCompareFinished = appendPath(getResourceDirPath(), cfg.soundFileCompareFinished); + if (!cfg.soundFileSyncFinished .empty()) cfg.soundFileSyncFinished = appendPath(getResourceDirPath(), cfg.soundFileSyncFinished); + } + else + { + cfg.soundFileCompareFinished = resolvePortablePath(cfg.soundFileCompareFinished); + cfg.soundFileSyncFinished = resolvePortablePath(cfg.soundFileSyncFinished); + cfg.soundFileAlertPending = resolvePortablePath(cfg.soundFileAlertPending); + } + + XmlIn inMainWin = in["MainDialog"]; + + //TODO: remove old parameter after migration! 2020-12-03 + if (in["Gui"]) + inMainWin = in["Gui"]["MainDialog"]; + + //TODO: remove old parameter after migration! 2021-03-06 + if (formatVer < 21) + { + cfg.dpiLayouts[getDpiScalePercent()].mainDlg.size = wxSize(); + inMainWin.attribute("Width", cfg.dpiLayouts[getDpiScalePercent()].mainDlg.size->x); + inMainWin.attribute("Height", cfg.dpiLayouts[getDpiScalePercent()].mainDlg.size->y); + cfg.dpiLayouts[getDpiScalePercent()].mainDlg.pos = wxPoint(); + inMainWin.attribute("PosX", cfg.dpiLayouts[getDpiScalePercent()].mainDlg.pos->x); + inMainWin.attribute("PosY", cfg.dpiLayouts[getDpiScalePercent()].mainDlg.pos->y); + inMainWin.attribute("Maximized", cfg.dpiLayouts[getDpiScalePercent()].mainDlg.isMaximized); + } + + //########################################################### + + inMainWin["SearchPanel"].attribute("CaseSensitive", cfg.mainDlg.textSearchRespectCase); + + //########################################################### + + XmlIn inConfig = inMainWin["ConfigPanel"]; + inConfig.attribute("ScrollPos", cfg.mainDlg.config.topRowPos); + inConfig.attribute("SyncOverdue", cfg.mainDlg.config.syncOverdueDays); + inConfig.attribute("SortByColumn", cfg.mainDlg.config.lastSortColumn); + inConfig.attribute("SortAscending", cfg.mainDlg.config.lastSortAscending); + + //TODO: remove old parameter after migration! 2021-03-06 + if (formatVer < 21) + inConfig["Columns"](cfg.dpiLayouts[getDpiScalePercent()].configColumnAttribs); + + inConfig["Configurations"].attribute("MaxSize", cfg.mainDlg.config.histItemsMax); + inConfig["Configurations"].attribute("LastSelected", cfg.mainDlg.config.lastSelectedFile); + cfg.mainDlg.config.lastSelectedFile = resolvePortablePath(cfg.mainDlg.config.lastSelectedFile); + + inConfig["Configurations"](cfg.mainDlg.config.fileHistory); + + //TODO: remove after migration! 2019-11-30 + if (formatVer < 15) + { + const Zstring lastRunConfigPath = appendPath(getConfigDirPath(), Zstr("LastRun.ffs_gui")); + for (ConfigFileItem& item : cfg.mainDlg.config.fileHistory) + if (equalNativePath(item.cfgFilePath, lastRunConfigPath)) + item.backColor = wxColor(0xdd, 0xdd, 0xdd); //light grey from onCfgGridContext() + } + + inConfig["LastUsed"](cfg.mainDlg.config.lastUsedFiles); + cfg.mainDlg.config.lastUsedFiles = resolvePortablePath(cfg.mainDlg.config.lastUsedFiles); + + //########################################################### + + XmlIn inOverview = inMainWin["OverviewPanel"]; + inOverview.attribute("ShowPercentage", cfg.mainDlg.overview.showPercentBar); + inOverview.attribute("SortByColumn", cfg.mainDlg.overview.lastSortColumn); + inOverview.attribute("SortAscending", cfg.mainDlg.overview.lastSortAscending); + + //TODO: remove old parameter after migration! 2021-03-06 + if (formatVer < 21) + inOverview["Columns"](cfg.dpiLayouts[getDpiScalePercent()].overviewColumnAttribs); + + XmlIn inFilePanel = inMainWin["FilePanel"]; + + //TODO: remove after migration! 2020-10-13 + if (formatVer < 19) + ; //new icon layout => let user re-evaluate settings + else + { + inFilePanel.attribute("ShowIcons", cfg.mainDlg.showIcons); + inFilePanel.attribute("IconSize", cfg.mainDlg.iconSize); + } + inFilePanel.attribute("SashOffset", cfg.mainDlg.sashOffset); + + //TODO: remove if parameter migration after some time! 2020-01-30 + if (formatVer < 16) + inFilePanel.attribute("MaxFolderPairsShown", cfg.mainDlg.folderPairsVisibleMax); + else + inFilePanel.attribute("FolderPairsMax", cfg.mainDlg.folderPairsVisibleMax); + + //TODO: remove old parameter after migration! 2021-03-06 + if (formatVer < 21) + { + inFilePanel["ColumnsLeft" ](cfg.dpiLayouts[getDpiScalePercent()].fileColumnAttribsLeft); + inFilePanel["ColumnsRight"](cfg.dpiLayouts[getDpiScalePercent()].fileColumnAttribsRight); + + inFilePanel["ColumnsLeft" ].attribute("PathFormat", cfg.mainDlg.itemPathFormatLeftGrid); + inFilePanel["ColumnsRight"].attribute("PathFormat", cfg.mainDlg.itemPathFormatRightGrid); + } + else + { + inFilePanel.attribute("PathFormatLeft", cfg.mainDlg.itemPathFormatLeftGrid); + inFilePanel.attribute("PathFormatRight", cfg.mainDlg.itemPathFormatRightGrid); + } + + inFilePanel["FolderHistoryLeft" ](cfg.mainDlg.folderHistoryLeft); + inFilePanel["FolderHistoryRight"](cfg.mainDlg.folderHistoryRight); + cfg.mainDlg.folderHistoryLeft = resolvePortablePath(cfg.mainDlg.folderHistoryLeft); + cfg.mainDlg.folderHistoryRight = resolvePortablePath(cfg.mainDlg.folderHistoryRight); + + inFilePanel["FolderHistoryLeft" ].attribute("LastSelected", cfg.mainDlg.folderLastSelectedLeft); + inFilePanel["FolderHistoryRight"].attribute("LastSelected", cfg.mainDlg.folderLastSelectedRight); + cfg.mainDlg.folderLastSelectedLeft = resolvePortablePath(cfg.mainDlg.folderLastSelectedLeft); + cfg.mainDlg.folderLastSelectedRight = resolvePortablePath(cfg.mainDlg.folderLastSelectedRight); + + //########################################################### + XmlIn inCopyTo = inMainWin["ManualCopyTo"]; + inCopyTo.attribute("KeepRelativePaths", cfg.mainDlg.copyToCfg.keepRelPaths); + inCopyTo.attribute("OverwriteIfExists", cfg.mainDlg.copyToCfg.overwriteIfExists); + + XmlIn inCopyToHistory = inCopyTo["FolderHistory"]; + + inCopyToHistory(cfg.mainDlg.copyToCfg.folderHistory); + inCopyToHistory.attribute("TargetFolder", cfg.mainDlg.copyToCfg.targetFolderPath); + inCopyToHistory.attribute("LastSelected", cfg.mainDlg.copyToCfg.targetFolderLastSelected); + cfg.mainDlg.copyToCfg.folderHistory = resolvePortablePath(cfg.mainDlg.copyToCfg.folderHistory); + cfg.mainDlg.copyToCfg.targetFolderPath = resolvePortablePath(cfg.mainDlg.copyToCfg.targetFolderPath); + cfg.mainDlg.copyToCfg.targetFolderLastSelected = resolvePortablePath(cfg.mainDlg.copyToCfg.targetFolderLastSelected); + //########################################################### + + XmlIn inDefFilter = inMainWin["DefaultViewFilter"]; + + inDefFilter.attribute("Equal", cfg.mainDlg.viewFilterDefault.equal); + inDefFilter.attribute("Conflict", cfg.mainDlg.viewFilterDefault.conflict); + inDefFilter.attribute("Excluded", cfg.mainDlg.viewFilterDefault.excluded); + + XmlIn diffView = inDefFilter["Difference"]; + //TODO: remove after migration! 2020-10-13 + if (formatVer < 19) + diffView = inDefFilter["CategoryView"]; + + diffView.attribute("LeftOnly", cfg.mainDlg.viewFilterDefault.leftOnly); + diffView.attribute("RightOnly", cfg.mainDlg.viewFilterDefault.rightOnly); + diffView.attribute("LeftNewer", cfg.mainDlg.viewFilterDefault.leftNewer); + diffView.attribute("RightNewer", cfg.mainDlg.viewFilterDefault.rightNewer); + diffView.attribute("Different", cfg.mainDlg.viewFilterDefault.different); + + XmlIn actView = inDefFilter["Action"]; + //TODO: remove after migration! 2020-10-13 + if (formatVer < 19) + actView = inDefFilter["ActionView"]; + + actView.attribute("CreateLeft", cfg.mainDlg.viewFilterDefault.createLeft); + actView.attribute("CreateRight", cfg.mainDlg.viewFilterDefault.createRight); + actView.attribute("UpdateLeft", cfg.mainDlg.viewFilterDefault.updateLeft); + actView.attribute("UpdateRight", cfg.mainDlg.viewFilterDefault.updateRight); + actView.attribute("DeleteLeft", cfg.mainDlg.viewFilterDefault.deleteLeft); + actView.attribute("DeleteRight", cfg.mainDlg.viewFilterDefault.deleteRight); + actView.attribute("DoNothing", cfg.mainDlg.viewFilterDefault.doNothing); + + + //TODO: remove old parameter after migration! 2021-03-06 + if (formatVer < 21) + inMainWin["Perspective"](cfg.dpiLayouts[getDpiScalePercent()].panelLayout); + + //TODO: remove after migration! 2019-11-30 + auto splitEditMerge = [](wxString& perspective, wchar_t delim, const std::function& editItem) + { + std::vector v = splitCpy(perspective, delim, SplitOnEmpty::allow); + assert(!v.empty()); + perspective.clear(); + + std::for_each(v.begin(), v.end() - 1, [&](wxString& item) + { + editItem(item); + perspective += item; + perspective += delim; + }); + editItem(v.back()); + perspective += v.back(); + }; + + //TODO: remove after migration! 2019-11-30 + if (formatVer < 15) + { + //set minimal TopPanel height => search and set actual height to 0 and let MainDialog's min-size handling kick in: + std::optional tpDir; + std::optional tpLayer; + std::optional tpRow; + splitEditMerge(cfg.dpiLayouts[getDpiScalePercent()].panelLayout, L'|', [&](wxString& paneCfg) + { + if (contains(paneCfg, L"name=TopPanel")) + splitEditMerge(paneCfg, L';', [&](wxString& paneAttr) + { + if (startsWith(paneAttr, L"dir=")) + tpDir = stringTo(afterFirst(paneAttr, L'=', IfNotFoundReturn::none)); + else if (startsWith(paneAttr, L"layer=")) + tpLayer = stringTo(afterFirst(paneAttr, L'=', IfNotFoundReturn::none)); + else if (startsWith(paneAttr, L"row=")) + tpRow = stringTo(afterFirst(paneAttr, L'=', IfNotFoundReturn::none)); + }); + }); + + if (tpDir && tpLayer && tpRow) + { + const wxString tpSize = L"dock_size(" + + numberTo(*tpDir ) + L"," + + numberTo(*tpLayer) + L"," + + numberTo(*tpRow ) + L")="; + + splitEditMerge(cfg.dpiLayouts[getDpiScalePercent()].panelLayout, L'|', [&](wxString& paneCfg) + { + if (startsWith(paneCfg, tpSize)) + paneCfg = tpSize + L"0"; + }); + } + } + + //TODO: remove if parameter migration after some time! 2020-01-30 + if (formatVer < 16) + ; + else if (formatVer < 20) //TODO: remove old parameter after migration! 2020-12-03 + in["Gui"]["FolderHistory" ].attribute("MaxSize", cfg.folderHistoryMax); + else + in["FolderHistory" ].attribute("MaxSize", cfg.folderHistoryMax); + + if (formatVer < 20) //TODO: remove old parameter after migration! 2020-12-03 + { + in["Gui"]["SftpKeyFile"].attribute("LastSelected", cfg.sftpKeyFileLastSelected); + } + else + { + in["SftpKeyFile"].attribute("LastSelected", cfg.sftpKeyFileLastSelected); + cfg.sftpKeyFileLastSelected = resolvePortablePath(cfg.sftpKeyFileLastSelected); + } + + if (formatVer < 22) //TODO: remove old parameter after migration! 2021-07-31 + { + } + else + readConfig(in["DefaultFilter"], cfg.defaultFilter); + + if (formatVer < 20) //TODO: remove old parameter after migration! 2020-12-03 + { + in["Gui"]["VersioningFolderHistory"](cfg.versioningFolderHistory); + in["Gui"]["VersioningFolderHistory"].attribute("LastSelected", cfg.versioningFolderLastSelected); + } + else + { + in["VersioningFolderHistory"](cfg.versioningFolderHistory); + in["VersioningFolderHistory"].attribute("LastSelected", cfg.versioningFolderLastSelected); + cfg.versioningFolderLastSelected = resolvePortablePath(cfg.versioningFolderLastSelected); + } + in["LogFolder"](cfg.logFolderPhrase); + cfg.logFolderPhrase = resolvePortablePath(cfg.logFolderPhrase); + + if (formatVer < 20) //TODO: remove old parameter after migration! 2020-12-03 + { + in["Gui"]["LogFolderHistory"](cfg.logFolderHistory); + in["Gui"]["LogFolderHistory"].attribute("LastSelected", cfg.logFolderLastSelected); + } + else + { + in["LogFolderHistory"](cfg.logFolderHistory); + in["LogFolderHistory"].attribute("LastSelected", cfg.logFolderLastSelected); + cfg.logFolderHistory = resolvePortablePath(cfg.logFolderHistory); + cfg.logFolderLastSelected = resolvePortablePath(cfg.logFolderLastSelected); + } + + if (formatVer < 20) //TODO: remove old parameter after migration! 2020-12-03 + { + in["Gui"]["EmailHistory"](cfg.emailHistory); + in["Gui"]["EmailHistory"].attribute("MaxSize", cfg.emailHistoryMax); + } + else + { + in["EmailHistory"](cfg.emailHistory); + in["EmailHistory"].attribute("MaxSize", cfg.emailHistoryMax); + } + + if (formatVer < 20) //TODO: remove old parameter after migration! 2020-12-03 + { + in["Gui"]["CommandHistory"](cfg.commandHistory); + in["Gui"]["CommandHistory"].attribute("MaxSize", cfg.commandHistoryMax); + } + else + { + in["CommandHistory"](cfg.commandHistory); + in["CommandHistory"].attribute("MaxSize", cfg.commandHistoryMax); + } + + //TODO: remove if parameter migration after some time! 2020-01-30 + if (formatVer < 15) + if (cfg.commandHistoryMax <= 8) + cfg.commandHistoryMax = GlobalConfig().commandHistoryMax; + + + if (formatVer < 20) //TODO: remove old parameter after migration! 2020-12-03 + in["Gui"]["ExternalApps"](cfg.externalApps); + else + in["ExternalApps"](cfg.externalApps); + + //TODO: remove after migration! 2019-11-30 + if (formatVer < 15) + for (ExternalApp& item : cfg.externalApps) + { + replace(item.cmdLine, Zstr("%folder_path%"), Zstr("%parent_path%")); + replace(item.cmdLine, Zstr("%folder_path2%"), Zstr("%parent_path2%")); + } + + //TODO: remove after migration! 2020-06-13 + if (formatVer < 18) + for (ExternalApp& item : cfg.externalApps) + { + trim(item.cmdLine); + if (item.cmdLine == "xdg-open \"%parent_path%\"") + item.cmdLine = "xdg-open \"$(dirname %local_path%)\""; + } + + //TODO: remove after migration! 2022-04-29 + if (formatVer < 24) + for (ExternalApp& item : cfg.externalApps) + if (item.description == L"Browse directory") + item.description = L"Show in file manager"; + + //TODO: remove after migration! 2025-09-25 + if (formatVer < 28) + for (ExternalApp& item : cfg.externalApps) + { + trim(item.cmdLine); + + auto removeQuotes = [&](const ZstringView macroName) { replace(item.cmdLine, Zstring() + Zstr('"') + macroName + Zstr('"'), macroName); }; + removeQuotes(Zstr("%item_path%")); + removeQuotes(Zstr("%item_path2%")); + removeQuotes(Zstr("%item_paths%")); + removeQuotes(Zstr("%local_path%")); + removeQuotes(Zstr("%local_path2%")); + removeQuotes(Zstr("%local_paths%")); + removeQuotes(Zstr("%item_name%")); + removeQuotes(Zstr("%item_name2%")); + removeQuotes(Zstr("%item_names%")); + removeQuotes(Zstr("%parent_path%")); + removeQuotes(Zstr("%parent_path2%")); + removeQuotes(Zstr("%parent_paths%")); + } + + if (formatVer < 20) //TODO: remove old parameter after migration! 2020-12-03 + { + in["Gui"]["LastOnlineCheck" ](cfg.lastUpdateCheck); + in["Gui"]["LastOnlineVersion"](cfg.lastOnlineVersion); + } + else + { + in["LastOnlineCheck" ](cfg.lastUpdateCheck); + in["LastOnlineVersion"](cfg.lastOnlineVersion); + } + + in["WelcomeDialogVersion"](cfg.welcomeDialogLastVersion); + + //cfg.dpiLayouts.clear(); -> NO: honor migration code above! + + in["DpiLayouts"].visitChildren([&](const XmlIn& inLayout) + { + assert(*inLayout.getName() == "Layout"); + if (std::string scaleTxt; + inLayout.attribute("Scale", scaleTxt)) + { + const int scalePercent = stringTo(beforeLast(scaleTxt, '%', IfNotFoundReturn::none)); + DpiLayout layout; + + //TODO: remove parameter migration after some time! 2023-02-18 + if (formatVer < 26) + { + XmlIn inLayoutMain = inLayout["MainDialog"]; + layout.mainDlg.size = wxSize(); + inLayoutMain.attribute("Width", layout.mainDlg.size->x); + inLayoutMain.attribute("Height", layout.mainDlg.size->y); + + layout.mainDlg.pos = wxPoint(); + inLayoutMain.attribute("PosX", layout.mainDlg.pos->x); + inLayoutMain.attribute("PosY", layout.mainDlg.pos->y); + + inLayoutMain.attribute("Maximized", layout.mainDlg.isMaximized); + + inLayoutMain["PanelLayout" ](layout.panelLayout); + inLayoutMain["ConfigPanel" ](layout.configColumnAttribs); + inLayoutMain["OverviewPanel" ](layout.overviewColumnAttribs); + inLayoutMain["FilePanelLeft" ](layout.fileColumnAttribsLeft); + inLayoutMain["FilePanelRight"](layout.fileColumnAttribsRight); + + XmlIn inLayoutProgress = inLayout["ProgressDialog"]; + layout.progressDlg.size = wxSize(); + inLayoutProgress.attribute("Width", layout.progressDlg.size->x); + inLayoutProgress.attribute("Height", layout.progressDlg.size->y); + + inLayoutProgress.attribute("Maximized", layout.progressDlg.isMaximized); + } + else + { + XmlIn inLayoutMain = inLayout["MainWindow"]; + if (inLayoutMain.hasAttribute("Width") && + inLayoutMain.hasAttribute("Height")) + { + layout.mainDlg.size = wxSize(); + inLayoutMain.attribute("Width", layout.mainDlg.size->x); + inLayoutMain.attribute("Height", layout.mainDlg.size->y); + } + if (inLayoutMain.hasAttribute("PosX") && + inLayoutMain.hasAttribute("PosY")) + { + layout.mainDlg.pos = wxPoint(); + inLayoutMain.attribute("PosX", layout.mainDlg.pos->x); + inLayoutMain.attribute("PosY", layout.mainDlg.pos->y); + } + inLayoutMain.attribute("Maximized", layout.mainDlg.isMaximized); + + XmlIn inLayoutProgress = inLayout["ProgressDialog"]; + if (inLayoutProgress.hasAttribute("Width") && + inLayoutProgress.hasAttribute("Height")) + { + layout.progressDlg.size = wxSize(); + inLayoutProgress.attribute("Width", layout.progressDlg.size->x); + inLayoutProgress.attribute("Height", layout.progressDlg.size->y); + } + inLayoutProgress.attribute("Maximized", layout.progressDlg.isMaximized); + + inLayout["Panels" ](layout.panelLayout); + inLayout["ConfigPanel" ](layout.configColumnAttribs); + inLayout["OverviewPanel" ](layout.overviewColumnAttribs); + inLayout["FilePanelLeft" ](layout.fileColumnAttribsLeft); + inLayout["FilePanelRight"](layout.fileColumnAttribsRight); + } + + cfg.dpiLayouts.emplace(scalePercent, std::move(layout)); + } + }); +} + + +template +std::pair parseConfig(const XmlDoc& doc, const Zstring& filePath, int currentXmlFormatVer) //noexcept +{ + int formatVer = 0; + /*bool success =*/ doc.root().getAttribute("XmlFormat", formatVer); + + XmlIn in(doc); + ConfigType cfg; + readConfig(in, cfg, formatVer); + + std::wstring warningMsg; + + if (const std::wstring& errors = in.getErrors(); + !errors.empty()) + warningMsg = replaceCpy(_("Configuration file %x is incomplete. The missing elements have been set to their default values."), L"%x", fmtPath(filePath)) + L"\n\n" + + _("The following XML elements could not be read:") + L'\n' + errors; + else //(try to) migrate old configuration if needed + if (formatVer < currentXmlFormatVer) + try + { + fff::writeConfig(cfg, filePath); //throw FileError + } + catch (const FileError& e) { warningMsg = e.toString(); } + + return {cfg, warningMsg}; +} + + +template +std::pair readConfig(const Zstring& filePath, const char* expectedCfgType, int currentXmlFormatVer) //throw FileError +{ + XmlDoc doc = loadXml(filePath); //throw FileError + + const std::string cfgType = [&] + { + if (doc.root().getName() == "FreeFileSync") + { + std::string type; + if (doc.root().getAttribute("XmlType", type)) + return type; + } + return std::string(); + }(); + if (cfgType != expectedCfgType) + throw FileError(replaceCpy(_("File %x does not contain a valid configuration."), L"%x", fmtPath(filePath))); + + return parseConfig(doc, filePath, currentXmlFormatVer); +} +} + + +std::pair fff::readGuiConfig(const Zstring& filePath) +{ + return readConfig(filePath, "GUI", XML_FORMAT_SYNC_CFG); //throw FileError +} + + +std::pair fff::readBatchConfig(const Zstring& filePath) +{ + return readConfig(filePath, "BATCH", XML_FORMAT_SYNC_CFG); //throw FileError +} + + +std::pair fff::readGlobalConfig(const Zstring& filePath) +{ + return readConfig(filePath, "GLOBAL", XML_FORMAT_GLOBAL_CFG); //throw FileError +} + + +std::pair fff::readAnyConfig(const std::vector& filePaths) //throw FileError +{ + assert(!filePaths.empty()); + + std::wstring warningMsgAll; + std::vector guiCfgs; + + for (const Zstring& filePath : filePaths) + if (endsWithAsciiNoCase(filePath, Zstr(".ffs_gui"))) + { + const auto& [guiCfg, warningMsg] = readGuiConfig(filePath); //throw FileError + guiCfgs.push_back(guiCfg); + + if (!warningMsg.empty()) + warningMsgAll += warningMsg + L"\n\n"; + } + else if (endsWithAsciiNoCase(filePath, Zstr(".ffs_batch"))) + { + const auto& [batchCfg, warningMsg] = readBatchConfig(filePath); //throw FileError + guiCfgs.push_back(batchCfg.guiCfg); + + if (!warningMsg.empty()) + warningMsgAll += warningMsg + L"\n\n"; + } + else + throw FileError(replaceCpy(_("Cannot open file %x."), L"%x", fmtPath(filePath)), + _("Unexpected file extension:") + L' ' + fmtPath(getFileExtension(filePath)) + L'\n' + + _("Expected:") + L" ffs_gui, ffs_batch"); + + return {merge(guiCfgs), trimCpy(warningMsgAll)}; +} + +//################################################################################################ + +namespace +{ +void writeConfig(const CompConfig& cmpCfg, XmlOut& out) +{ + out["Variant" ](cmpCfg.compareVar); + out["Symlinks"](cmpCfg.handleSymlinks); + out["IgnoreTimeShift"](toTimeShiftPhrase(cmpCfg.ignoreTimeShiftMinutes)); +} + + +void writeConfig(const SyncDirectionConfig& dirCfg, XmlOut& out) +{ + if (const DirectionByDiff* diffDirs = std::get_if(&dirCfg.dirs)) + { + XmlOut outDirs = out["Differences"]; + outDirs.attribute("LeftOnly", diffDirs->leftOnly); + outDirs.attribute("LeftNewer", diffDirs->leftNewer); + outDirs.attribute("RightNewer", diffDirs->rightNewer); + outDirs.attribute("RightOnly", diffDirs->rightOnly); + } + else + { + const DirectionByChange& changeDirs = std::get(dirCfg.dirs); + + XmlOut outDirsL = out["Changes"]["Left"]; + outDirsL.attribute("Create", changeDirs.left.create); + outDirsL.attribute("Update", changeDirs.left.update); + outDirsL.attribute("Delete", changeDirs.left.delete_); + + XmlOut outDirsR = out["Changes"]["Right"]; + outDirsR.attribute("Create", changeDirs.right.create); + outDirsR.attribute("Update", changeDirs.right.update); + outDirsR.attribute("Delete", changeDirs.right.delete_); + } +} + + +void writeConfig(const SyncConfig& syncCfg, const std::map& deviceParallelOps, XmlOut& out) +{ + writeConfig(syncCfg.directionCfg, out); + + out["DeletionPolicy" ](syncCfg.deletionVariant); + out["VersioningFolder"](syncCfg.versioningFolderPhrase); + + const size_t parallelOps = getDeviceParallelOps(deviceParallelOps, syncCfg.versioningFolderPhrase); + if (parallelOps > 1) out["VersioningFolder"].attribute("Threads", parallelOps); + + out["VersioningFolder"].attribute("Style", syncCfg.versioningStyle); + + if (syncCfg.versioningStyle != VersioningStyle::replace) + { + if (syncCfg.versionMaxAgeDays > 0) out["VersioningFolder"].attribute("MaxAge", syncCfg.versionMaxAgeDays); + if (syncCfg.versionCountMin > 0) out["VersioningFolder"].attribute("MinCount", syncCfg.versionCountMin); + if (syncCfg.versionCountMax > 0) out["VersioningFolder"].attribute("MaxCount", syncCfg.versionCountMax); + } +} + + +void writeConfig(const FilterConfig& filter, XmlOut& out) +{ + out["Include"](splitFilterByLines(filter.includeFilter)); + out["Exclude"](splitFilterByLines(filter.excludeFilter)); + + out["SizeMin"](filter.sizeMin); + out["SizeMin"].attribute("Unit", filter.unitSizeMin); + + out["SizeMax"](filter.sizeMax); + out["SizeMax"].attribute("Unit", filter.unitSizeMax); + + out["TimeSpan"](filter.timeSpan); + out["TimeSpan"].attribute("Type", filter.unitTimeSpan); +} + + +void writeConfig(const LocalPairConfig& lpc, const std::map& deviceParallelOps, XmlOut& out) +{ + XmlOut outPair = out.addChild("Pair"); + + //read folder pairs + outPair["Left" ](lpc.folderPathPhraseLeft); + outPair["Right"](lpc.folderPathPhraseRight); + + const size_t parallelOpsL = getDeviceParallelOps(deviceParallelOps, lpc.folderPathPhraseLeft); + const size_t parallelOpsR = getDeviceParallelOps(deviceParallelOps, lpc.folderPathPhraseRight); + + if (parallelOpsL > 1) outPair["Left" ].attribute("Threads", parallelOpsL); + if (parallelOpsR > 1) outPair["Right"].attribute("Threads", parallelOpsR); + + //avoid "fake" changed configs by only storing "real" parallel-enabled devices in deviceParallelOps + assert(std::all_of(deviceParallelOps.begin(), deviceParallelOps.end(), [](const auto& item) { return item.second > 1; })); + + //########################################################### + //alternate comp configuration (optional) + if (lpc.localCmpCfg) + { + XmlOut outLocalCmp = outPair["Compare"]; + writeConfig(*lpc.localCmpCfg, outLocalCmp); + } + //########################################################### + //alternate sync configuration (optional) + if (lpc.localSyncCfg) + { + XmlOut outLocalSync = outPair["Synchronize"]; + writeConfig(*lpc.localSyncCfg, deviceParallelOps, outLocalSync); + } + + //########################################################### + //alternate filter configuration + if (lpc.localFilter != FilterConfig()) //don't spam .ffs_gui file with default filter entries + { + XmlOut outFilter = outPair["Filter"]; + writeConfig(lpc.localFilter, outFilter); + } +} + + +void writeConfig(const MainConfiguration& mainCfg, XmlOut& out) +{ + XmlOut outCmp = out["Compare"]; + writeConfig(mainCfg.cmpCfg, outCmp); + //########################################################### + + XmlOut outSync = out["Synchronize"]; + writeConfig(mainCfg.syncCfg, mainCfg.deviceParallelOps, outSync); + //########################################################### + + XmlOut outFilter = out["Filter"]; + writeConfig(mainCfg.globalFilter, outFilter); + + //########################################################### + XmlOut outFp = out["FolderPairs"]; + //write folder pairs + writeConfig(mainCfg.firstPair, mainCfg.deviceParallelOps, outFp); + + for (const LocalPairConfig& lpc : mainCfg.additionalPairs) + writeConfig(lpc, mainCfg.deviceParallelOps, outFp); + + out["Errors"].attribute("Ignore", mainCfg.ignoreErrors); + out["Errors"].attribute("Retry", mainCfg.autoRetryCount); + out["Errors"].attribute("Delay", mainCfg.autoRetryDelay); + + out["PostSyncCommand"](mainCfg.postSyncCommand); + out["PostSyncCommand"].attribute("Condition", mainCfg.postSyncCondition); + + out["LogFolder"](mainCfg.altLogFolderPathPhrase); + + out["EmailNotification"](mainCfg.emailNotifyAddress); + out["EmailNotification"].attribute("Condition", mainCfg.emailNotifyCondition); +} + + +void writeConfig(const FfsGuiConfig& cfg, XmlOut& out) +{ + out["Notes"](cfg.notes); + + writeConfig(cfg.mainCfg, out); //write main config + + out["GridViewType"](cfg.gridViewType); +} + + +void writeConfig(const FfsBatchConfig& cfg, XmlOut& out) +{ + writeConfig(cfg.guiCfg, out); + + XmlOut outBatch = out["Batch"]; + outBatch["ProgressDialog"].attribute("Minimized", cfg.batchExCfg.runMinimized); + outBatch["ProgressDialog"].attribute("AutoClose", cfg.batchExCfg.autoCloseSummary); + outBatch["ErrorDialog" ](cfg.batchExCfg.batchErrorHandling); + outBatch["PostSyncAction"](cfg.batchExCfg.postBatchAction); +} + + +void writeConfig(const GlobalConfig& cfg, XmlOut& out) +{ + out["Language"].attribute("Code", cfg.programLanguage); + out["ColorTheme"].attribute("Appearance", cfg.appColorTheme); + + out["FailSafeFileCopy" ].attribute("Enabled", cfg.failSafeFileCopy); + out["CopyLockedFiles" ].attribute("Enabled", cfg.copyLockedFiles); + out["CopyFilePermissions" ].attribute("Enabled", cfg.copyFilePermissions); + out["FileTimeTolerance" ].attribute("Seconds", cfg.fileTimeTolerance); + out["RunWithBackgroundPriority"].attribute("Enabled", cfg.runWithBackgroundPriority); + out["LockDirectoriesDuringSync"].attribute("Enabled", cfg.createLockFile); + out["VerifyCopiedFiles" ].attribute("Enabled", cfg.verifyFileCopy); + out["LogFiles" ].attribute("MaxAge", cfg.logfilesMaxAgeDays); + out["LogFiles" ].attribute("Format", cfg.logFormat); + + out["ProgressDialog"].attribute("AutoClose", cfg.progressDlgAutoClose); + + XmlOut outOpt = out["OptionalDialogs"]; + outOpt["ConfirmStartSync" ].attribute("Show", cfg.confirmDlgs.confirmSyncStart); + outOpt["ConfirmSaveConfig" ].attribute("Show", cfg.confirmDlgs.confirmSaveConfig); + outOpt["ConfirmSwapSides" ].attribute("Show", cfg.confirmDlgs.confirmSwapSides); + outOpt["ConfirmCommandMassInvoke" ].attribute("Show", cfg.confirmDlgs.confirmCommandMassInvoke); + outOpt["WarnFolderNotExisting" ].attribute("Show", cfg.warnDlgs.warnFolderNotExisting); + outOpt["WarnFoldersDifferInCase" ].attribute("Show", cfg.warnDlgs.warnFoldersDifferInCase); + outOpt["WarnUnresolvedConflicts" ].attribute("Show", cfg.warnDlgs.warnUnresolvedConflicts); + outOpt["WarnNotEnoughDiskSpace" ].attribute("Show", cfg.warnDlgs.warnNotEnoughDiskSpace); + outOpt["WarnSignificantDifference" ].attribute("Show", cfg.warnDlgs.warnSignificantDifference); + outOpt["WarnRecycleBinNotAvailable" ].attribute("Show", cfg.warnDlgs.warnRecyclerMissing); + outOpt["WarnDependentFolderPair" ].attribute("Show", cfg.warnDlgs.warnDependentFolderPair); + outOpt["WarnDependentBaseFolders" ].attribute("Show", cfg.warnDlgs.warnDependentBaseFolders); + outOpt["WarnDirectoryLockFailed" ].attribute("Show", cfg.warnDlgs.warnDirectoryLockFailed); + outOpt["WarnVersioningFolderPartOfSync"].attribute("Show", cfg.warnDlgs.warnVersioningFolderPartOfSync); + + out["Sounds"]["CompareFinished"].attribute("Path", makePortablePath(cfg.soundFileCompareFinished)); + out["Sounds"]["SyncFinished" ].attribute("Path", makePortablePath(cfg.soundFileSyncFinished)); + out["Sounds"]["AlertPending" ].attribute("Path", makePortablePath(cfg.soundFileAlertPending)); + + //gui specific global settings (optional) + XmlOut outMainWin = out["MainDialog"]; + + //########################################################### + outMainWin["SearchPanel"].attribute("CaseSensitive", cfg.mainDlg.textSearchRespectCase); + //########################################################### + + XmlOut outConfig = outMainWin["ConfigPanel"]; + outConfig.attribute("ScrollPos", cfg.mainDlg.config.topRowPos); + outConfig.attribute("SyncOverdue", cfg.mainDlg.config.syncOverdueDays); + outConfig.attribute("SortByColumn", cfg.mainDlg.config.lastSortColumn); + outConfig.attribute("SortAscending", cfg.mainDlg.config.lastSortAscending); + + outConfig["Configurations"].attribute("MaxSize", cfg.mainDlg.config.histItemsMax); + outConfig["Configurations"].attribute("LastSelected", makePortablePath(cfg.mainDlg.config.lastSelectedFile)); + outConfig["Configurations"](cfg.mainDlg.config.fileHistory); + + outConfig["LastUsed"](makePortablePath(cfg.mainDlg.config.lastUsedFiles)); + + //########################################################### + + XmlOut outOverview = outMainWin["OverviewPanel"]; + outOverview.attribute("ShowPercentage", cfg.mainDlg.overview.showPercentBar); + outOverview.attribute("SortByColumn", cfg.mainDlg.overview.lastSortColumn); + outOverview.attribute("SortAscending", cfg.mainDlg.overview.lastSortAscending); + + XmlOut outFilePanel = outMainWin["FilePanel"]; + outFilePanel.attribute("ShowIcons", cfg.mainDlg.showIcons); + outFilePanel.attribute("IconSize", cfg.mainDlg.iconSize); + outFilePanel.attribute("SashOffset", cfg.mainDlg.sashOffset); + outFilePanel.attribute("FolderPairsMax", cfg.mainDlg.folderPairsVisibleMax); + outFilePanel.attribute("PathFormatLeft", cfg.mainDlg.itemPathFormatLeftGrid); + outFilePanel.attribute("PathFormatRight", cfg.mainDlg.itemPathFormatRightGrid); + + outFilePanel["FolderHistoryLeft" ](makePortablePath(cfg.mainDlg.folderHistoryLeft)); + outFilePanel["FolderHistoryRight"](makePortablePath(cfg.mainDlg.folderHistoryRight)); + + outFilePanel["FolderHistoryLeft" ].attribute("LastSelected", makePortablePath(cfg.mainDlg.folderLastSelectedLeft)); + outFilePanel["FolderHistoryRight"].attribute("LastSelected", makePortablePath(cfg.mainDlg.folderLastSelectedRight)); + + //########################################################### + XmlOut outCopyTo = outMainWin["ManualCopyTo"]; + outCopyTo.attribute("KeepRelativePaths", cfg.mainDlg.copyToCfg.keepRelPaths); + outCopyTo.attribute("OverwriteIfExists", cfg.mainDlg.copyToCfg.overwriteIfExists); + + XmlOut outCopyToHistory = outCopyTo["FolderHistory"]; + + outCopyToHistory(makePortablePath(cfg.mainDlg.copyToCfg.folderHistory)); + outCopyToHistory.attribute("TargetFolder", makePortablePath(cfg.mainDlg.copyToCfg.targetFolderPath)); + outCopyToHistory.attribute("LastSelected", makePortablePath(cfg.mainDlg.copyToCfg.targetFolderLastSelected)); + //########################################################### + + XmlOut outDefFilter = outMainWin["DefaultViewFilter"]; + outDefFilter.attribute("Equal", cfg.mainDlg.viewFilterDefault.equal); + outDefFilter.attribute("Conflict", cfg.mainDlg.viewFilterDefault.conflict); + outDefFilter.attribute("Excluded", cfg.mainDlg.viewFilterDefault.excluded); + + XmlOut catView = outDefFilter["Difference"]; + catView.attribute("LeftOnly", cfg.mainDlg.viewFilterDefault.leftOnly); + catView.attribute("RightOnly", cfg.mainDlg.viewFilterDefault.rightOnly); + catView.attribute("LeftNewer", cfg.mainDlg.viewFilterDefault.leftNewer); + catView.attribute("RightNewer", cfg.mainDlg.viewFilterDefault.rightNewer); + catView.attribute("Different", cfg.mainDlg.viewFilterDefault.different); + + XmlOut actView = outDefFilter["Action"]; + actView.attribute("CreateLeft", cfg.mainDlg.viewFilterDefault.createLeft); + actView.attribute("CreateRight", cfg.mainDlg.viewFilterDefault.createRight); + actView.attribute("UpdateLeft", cfg.mainDlg.viewFilterDefault.updateLeft); + actView.attribute("UpdateRight", cfg.mainDlg.viewFilterDefault.updateRight); + actView.attribute("DeleteLeft", cfg.mainDlg.viewFilterDefault.deleteLeft); + actView.attribute("DeleteRight", cfg.mainDlg.viewFilterDefault.deleteRight); + actView.attribute("DoNothing", cfg.mainDlg.viewFilterDefault.doNothing); + + out["FolderHistory" ].attribute("MaxSize", cfg.folderHistoryMax); + + out["SftpKeyFile"].attribute("LastSelected", makePortablePath(cfg.sftpKeyFileLastSelected)); + + XmlOut outFileFilter = out["DefaultFilter"]; + writeConfig(cfg.defaultFilter, outFileFilter); + + out["VersioningFolderHistory"](cfg.versioningFolderHistory); + out["VersioningFolderHistory"].attribute("LastSelected", makePortablePath(cfg.versioningFolderLastSelected)); + + out["LogFolder"](makePortablePath(cfg.logFolderPhrase)); + out["LogFolderHistory"](makePortablePath(cfg.logFolderHistory)); + out["LogFolderHistory"].attribute("LastSelected", makePortablePath(cfg.logFolderLastSelected)); + + out["EmailHistory"](cfg.emailHistory); + out["EmailHistory"].attribute("MaxSize", cfg.emailHistoryMax); + + out["CommandHistory"](cfg.commandHistory); + out["CommandHistory"].attribute("MaxSize", cfg.commandHistoryMax); + + //external applications + out["ExternalApps"](cfg.externalApps); + + //last update check + out["LastOnlineCheck" ](cfg.lastUpdateCheck); + out["LastOnlineVersion"](cfg.lastOnlineVersion); + + out["WelcomeDialogVersion"](cfg.welcomeDialogLastVersion); + + + for (const auto& [scalePercent, layout] : cfg.dpiLayouts) + { + XmlOut outLayout = out["DpiLayouts"].addChild("Layout"); + outLayout.attribute("Scale", numberTo(scalePercent) + '%'); + + XmlOut outLayoutMain = outLayout["MainWindow"]; + if (layout.mainDlg.size) + { + outLayoutMain.attribute("Width", layout.mainDlg.size->x); + outLayoutMain.attribute("Height", layout.mainDlg.size->y); + } + if (layout.mainDlg.pos) + { + outLayoutMain.attribute("PosX", layout.mainDlg.pos->x); + outLayoutMain.attribute("PosY", layout.mainDlg.pos->y); + } + outLayoutMain.attribute("Maximized", layout.mainDlg.isMaximized); + + XmlOut outLayoutProgress = outLayout["ProgressDialog"]; + if (layout.progressDlg.size) + { + outLayoutProgress.attribute("Width", layout.progressDlg.size->x); + outLayoutProgress.attribute("Height", layout.progressDlg.size->y); + } + outLayoutProgress.attribute("Maximized", layout.progressDlg.isMaximized); + + outLayout["Panels" ](layout.panelLayout); + outLayout["ConfigPanel" ](layout.configColumnAttribs); + outLayout["OverviewPanel" ](layout.overviewColumnAttribs); + outLayout["FilePanelLeft" ](layout.fileColumnAttribsLeft); + outLayout["FilePanelRight"](layout.fileColumnAttribsRight); + } +} + + +template +void writeConfig(const ConfigType& cfg, const char* cfgType, int xmlFormatVer, const Zstring& filePath) +{ + XmlDoc doc("FreeFileSync"); + doc.root().setAttribute("XmlType", cfgType); + doc.root().setAttribute("XmlFormat", xmlFormatVer); + + XmlOut out(doc); + writeConfig(cfg, out); + + saveXml(doc, filePath); //throw FileError +} +} + +void fff::writeConfig(const FfsGuiConfig& cfg, const Zstring& filePath) +{ + ::writeConfig(cfg, "GUI", XML_FORMAT_SYNC_CFG, filePath); //throw FileError +} + + +void fff::writeConfig(const FfsBatchConfig& cfg, const Zstring& filePath) +{ + ::writeConfig(cfg, "BATCH", XML_FORMAT_SYNC_CFG, filePath); //throw FileError +} + + +void fff::writeConfig(const GlobalConfig& cfg, const Zstring& filePath) +{ + ::writeConfig(cfg, "GLOBAL", XML_FORMAT_GLOBAL_CFG, filePath); //throw FileError +} + + +std::wstring fff::extractJobName(const Zstring& cfgFilePath) +{ + const Zstring fileName = getItemName(cfgFilePath); + const Zstring jobName = beforeLast(fileName, Zstr('.'), IfNotFoundReturn::all); + return utfTo(jobName); +} + + +std::string fff::serializeFilter(const FilterConfig& filterCfg) +{ + XmlDoc doc("Filter"); + doc.setEncoding(""); + + XmlOut out(doc); + ::writeConfig(filterCfg, out); + + return serializeXml(doc); //noexcept +} + + +std::optional fff::parseFilterBuf(const std::string& filterBuf) +{ + try + { + XmlDoc doc = parseXml(filterBuf); //throw XmlParsingError + XmlIn in(doc); + + FilterConfig filterCfg; + ::readConfig(in, filterCfg); + if (in.getErrors().empty()) + return filterCfg; + } + catch (XmlParsingError&) {} + + return std::nullopt; +} + + +void fff::saveErrorLog(const ErrorLog& log, const Zstring& filePath) //throw FileError +{ + XmlDoc doc("Log"); + doc.setEncoding(""); + + XmlOut out(doc); + + for (const LogEntry& e : log) + { + XmlOut outMsg = out.addChild(e.type == MessageType::MSG_TYPE_ERROR ? "Error" : (e.type == MessageType::MSG_TYPE_WARNING ? "Warning" : "Info")); + outMsg.attribute("Time", formatTime(formatIsoDateTimeTag, getLocalTime(e.time))); + outMsg(e.message); + } + + saveXml(doc, filePath); //throw FileError +} + + +ErrorLog fff::loadErrorLog(const Zstring& filePath) //throw FileError +{ + XmlDoc doc = loadXml(filePath); //throw FileError + + XmlIn in(doc); + ErrorLog log; + + in.visitChildren([&](const XmlIn& inMsg) + { + Zstring timeStr; + inMsg.attribute("Time", timeStr); + + Zstringc msg; + inMsg(msg); + + log.push_back( + { + .time = localToTimeT(parseTime(formatIsoDateTimeTag, timeStr)).first, + .type = *inMsg.getName() == "Error" ? MessageType::MSG_TYPE_ERROR : (*inMsg.getName() == "Warning" ? MessageType::MSG_TYPE_WARNING : MessageType::MSG_TYPE_INFO), + .message = std::move(msg), + }); + }); + + if (const std::wstring& errors = in.getErrors(); + !errors.empty()) + throw FileError(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(filePath)), + _("The following XML elements could not be read:") + L'\n' + errors); + return log; +} diff --git a/FreeFileSync/Source/config.h b/FreeFileSync/Source/config.h new file mode 100644 index 0000000..99d918a --- /dev/null +++ b/FreeFileSync/Source/config.h @@ -0,0 +1,283 @@ +// ***************************************************************************** +// * 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 PROCESS_XML_H_28345825704254262435 +#define PROCESS_XML_H_28345825704254262435 + +#include +#include +#include +#include "localization.h" +#include "log_file.h" +#include "base/structures.h" +#include "ui/file_grid_attr.h" +#include "ui/tree_grid_attr.h" //RTS: avoid tree grid's "file_hierarchy.h" dependency! +#include "ui/cfg_grid.h" + + +namespace fff +{ +enum class BatchErrorHandling +{ + showPopup, + cancel +}; + + +enum class PostBatchAction +{ + none, + sleep, + shutdown +}; + +struct ExternalApp +{ + std::wstring description; //must be translated *after* loading from config file + Zstring cmdLine; +}; + +extern const ExternalApp extCommandFileManager; +extern const ExternalApp extCommandOpenDefault; + +//--------------------------------------------------------------------- +struct FfsGuiConfig +{ + MainConfiguration mainCfg; + + //"GuiExclusiveConfig": + std::wstring notes; + GridViewType gridViewType = GridViewType::action; //keep "visual" setting out of "MainConfiguration" + + bool operator==(const FfsGuiConfig&) const = default; +}; + + +struct BatchExclusiveConfig +{ + bool runMinimized = false; + bool autoCloseSummary = false; + BatchErrorHandling batchErrorHandling = BatchErrorHandling::showPopup; + PostBatchAction postBatchAction = PostBatchAction::none; +}; + + +struct FfsBatchConfig +{ + FfsGuiConfig guiCfg; //batch config can be used in GUI (but not the other way round) + BatchExclusiveConfig batchExCfg; +}; + + +struct ConfirmationDialogs +{ + bool confirmSaveConfig = true; + bool confirmSyncStart = true; + bool confirmCommandMassInvoke = true; + bool confirmSwapSides = true; + + bool operator==(const ConfirmationDialogs&) const = default; +}; + + +enum class GridIconSize +{ + small, + medium, + large +}; + + +struct ViewFilterDefault +{ + //shared + bool equal = false; + bool conflict = true; + bool excluded = false; + //difference view + bool leftOnly = true; + bool rightOnly = true; + bool leftNewer = true; + bool rightNewer = true; + bool different = true; + //action view + bool createLeft = true; + bool createRight = true; + bool updateLeft = true; + bool updateRight = true; + bool deleteLeft = true; + bool deleteRight = true; + bool doNothing = true; +}; + + +Zstring getGlobalConfigDefaultPath(); +Zstring getLogFolderDefaultPath(); + +struct DpiLayout +{ + struct + { + std::optional size; + std::optional pos; + bool isMaximized = false; + } mainDlg; //WindowLayout::getBeforeClose() + + struct + { + std::optional size; + //std::optional pos; -> most users probably want it centered, but others at a fixed (relative??) location + bool isMaximized = false; + } progressDlg; + + wxString panelLayout; //for wxAuiManager::LoadPerspective + + std::vector configColumnAttribs = getCfgGridDefaultColAttribs(); + std::vector overviewColumnAttribs = getOverviewDefaultColAttribs(); + std::vector fileColumnAttribsLeft = getFileGridDefaultColAttribsLeft(); + std::vector fileColumnAttribsRight = getFileGridDefaultColAttribsRight(); +}; + + +struct GlobalConfig +{ + GlobalConfig(); + + //--------------------------------------------------------------------- + //Shared (GUI/BATCH) settings + wxLanguage programLanguage = getDefaultLanguage(); + zen::ColorTheme appColorTheme = zen::ColorTheme::System; + bool failSafeFileCopy = true; + bool copyLockedFiles = false; //safer default: avoid copies of partially written files + bool copyFilePermissions = false; + + unsigned int fileTimeTolerance = zen::FAT_FILE_TIME_PRECISION_SEC; //default 2s: FAT vs NTFS + bool runWithBackgroundPriority = false; + bool createLockFile = true; + bool verifyFileCopy = false; + int logfilesMaxAgeDays = 30; //<= 0 := no limit; for log files under %AppData%\FreeFileSync\Logs + LogFileFormat logFormat = LogFileFormat::html; + + Zstring soundFileCompareFinished; + Zstring soundFileSyncFinished; + Zstring soundFileAlertPending; + + ConfirmationDialogs confirmDlgs; + WarningDialogs warnDlgs; + + //--------------------------------------------------------------------- + + struct + { + bool textSearchRespectCase = false; //good default for Linux, too! + int folderPairsVisibleMax = 6; + + struct + { + size_t topRowPos = 0; + int syncOverdueDays = 7; + ColumnTypeCfg lastSortColumn = cfgGridLastSortColumnDefault; + bool lastSortAscending = getDefaultSortDirection(cfgGridLastSortColumnDefault); + size_t histItemsMax = 100; //do we need to limit config items at all? + Zstring lastSelectedFile; + std::vector fileHistory; + std::vector lastUsedFiles; + } config; + + struct + { + bool showPercentBar = overviewPanelShowPercentageDefault; + ColumnTypeOverview lastSortColumn = overviewPanelLastSortColumnDefault; //remember sort on overview panel + bool lastSortAscending = getDefaultSortDirection(overviewPanelLastSortColumnDefault); // + } overview; + + struct + { + bool keepRelPaths = false; + bool overwriteIfExists = false; + Zstring targetFolderPath; + Zstring targetFolderLastSelected; + std::vector folderHistory; + } copyToCfg; + + std::vector folderHistoryLeft; + std::vector folderHistoryRight; + Zstring folderLastSelectedLeft; + Zstring folderLastSelectedRight; + + bool showIcons = true; + GridIconSize iconSize = GridIconSize::small; + int sashOffset = 0; + + ItemPathFormat itemPathFormatLeftGrid = defaultItemPathFormatLeftGrid; + ItemPathFormat itemPathFormatRightGrid = defaultItemPathFormatRightGrid; + + ViewFilterDefault viewFilterDefault; + } mainDlg; + + bool progressDlgAutoClose = false; + + FilterConfig defaultFilter = [] + { + FilterConfig def; + assert(def.excludeFilter.empty()); + def.excludeFilter = + "*/.Trash-*/" "\n" + "*/.recycle/"; + return def; + }(); + + size_t folderHistoryMax = 20; + + Zstring sftpKeyFileLastSelected; + + std::vector versioningFolderHistory; + Zstring versioningFolderLastSelected; + + Zstring logFolderPhrase = getLogFolderDefaultPath(); + std::vector logFolderHistory; + Zstring logFolderLastSelected; + + std::vector emailHistory; + size_t emailHistoryMax = 10; + + std::vector commandHistory; + size_t commandHistoryMax = 10; + + std::vector externalApps{extCommandFileManager, extCommandOpenDefault}; + + time_t lastUpdateCheck = 0; //number of seconds since Jan 1, 1970 GMT + std::string lastOnlineVersion; + + std::string welcomeDialogLastVersion; + + std::unordered_map dpiLayouts; +}; + +//read/write specific config types +std::pair readGuiConfig (const Zstring& filePath); // +std::pair readBatchConfig (const Zstring& filePath); //throw FileError +std::pair readGlobalConfig(const Zstring& filePath); // + +void writeConfig(const FfsGuiConfig& cfg, const Zstring& filePath); // +void writeConfig(const FfsBatchConfig& cfg, const Zstring& filePath); //throw FileError +void writeConfig(const GlobalConfig& cfg, const Zstring& filePath); // + +//convert (multiple) *.ffs_gui, *.ffs_batch files or combinations of both into target config structure: +std::pair readAnyConfig(const std::vector& filePaths); //throw FileError + + +std::wstring extractJobName(const Zstring& cfgFilePath); + +//human-readable/editable format suitable for clipboard +std::string serializeFilter(const FilterConfig& filterCfg); +std::optional parseFilterBuf(const std::string& filterBuf); + +void saveErrorLog(const zen::ErrorLog& log, const Zstring& filePath); //throw FileError +zen::ErrorLog loadErrorLog(const Zstring& filePath); //throw FileError +} + +#endif //PROCESS_XML_H_28345825704254262435 diff --git a/FreeFileSync/Source/ffs_paths.cpp b/FreeFileSync/Source/ffs_paths.cpp new file mode 100644 index 0000000..01a9a69 --- /dev/null +++ b/FreeFileSync/Source/ffs_paths.cpp @@ -0,0 +1,97 @@ +// ***************************************************************************** +// * 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 "ffs_paths.h" +#include +#include + + #include //std::cerr + +using namespace zen; + + +namespace +{ +Zstring getProcessParentFolderPath() +{ + //buffer getSymlinkResolvedPath()! + //note: compiler generates magic-statics code => fine, we don't expect accesses during shutdown => don't need FunStatGlobal<> + static const Zstring exeFolderParentPath = [] + { + try + { + const Zstring& processPath = getProcessPath(); //throw FileError + /* no need for getSymlinkResolvedPath(): + => support file systems with buggy GetFinalPathNameByHandle() implementation, e.g. Dokany-based: https://freefilesync.org/forum/viewtopic.php?t=8828 + => we're already supporting calling FFS via symlink for launcher executable, which guarantees: */ + assert(getItemType( processPath) != ItemType::symlink); //throw FileError + assert(getItemType(*getParentFolderPath(processPath)) != ItemType::symlink); //throw FileError + + return *getParentFolderPath(*getParentFolderPath(processPath)); //no parent folder!!? => let it crash! + } + catch (const FileError& e) + { + throw std::runtime_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Failed to get process parent folder. " + utfTo(e.toString())); + } + }(); + return exeFolderParentPath; +} +} + + +Zstring fff::getInstallDirPath() +{ + return getProcessParentFolderPath(); + +} + + + + +Zstring fff::getResourceDirPath() +{ + return appendPath(getProcessParentFolderPath(), Zstr("Resources")); +} + + +Zstring fff::getConfigDirPath() +{ + //note: compiler generates magic-statics code => fine, we don't expect accesses during shutdown + static const Zstring ffsConfigPath = [] + { + /* Windows: %AppData%\FreeFileSync + macOS: ~/Library/Application Support/FreeFileSync + Linux (XDG layout): ~/.config/FreeFileSync */ + const Zstring& configPath = [] + { + try + { + return appendPath(getUserDataPath(), Zstr("FreeFileSync")); //throw FileError + } + catch (const FileError& e) + { + throw std::runtime_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Failed to get config path. " + utfTo(e.toString())); + } + }(); + + try + { + createDirectoryIfMissingRecursion(configPath); //throw FileError + } + catch (const FileError& e) { logExtraError(e.toString()); } + + return configPath; + }(); + return ffsConfigPath; +} + + +//this function is called by RealTimeSync!!! +Zstring fff::getFreeFileSyncLauncherPath() //throw FileError +{ + return appendPath(getInstallDirPath(), Zstr("FreeFileSync")); + +} diff --git a/FreeFileSync/Source/ffs_paths.h b/FreeFileSync/Source/ffs_paths.h new file mode 100644 index 0000000..6cf122f --- /dev/null +++ b/FreeFileSync/Source/ffs_paths.h @@ -0,0 +1,29 @@ +// ***************************************************************************** +// * 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 FFS_PATHS_H_842759083425342534253 +#define FFS_PATHS_H_842759083425342534253 + +#include + + +namespace fff +{ +//------------------------------------------------------------------------------ +//global program directories +//------------------------------------------------------------------------------ +Zstring getResourceDirPath(); +Zstring getConfigDirPath(); +//------------------------------------------------------------------------------ + + +Zstring getInstallDirPath(); + +Zstring getFreeFileSyncLauncherPath(); //throw FileError +//full path to application launcher C:\...\FreeFileSync.exe +} + +#endif //FFS_PATHS_H_842759083425342534253 diff --git a/FreeFileSync/Source/icon_buffer.cpp b/FreeFileSync/Source/icon_buffer.cpp new file mode 100644 index 0000000..9502fcf --- /dev/null +++ b/FreeFileSync/Source/icon_buffer.cpp @@ -0,0 +1,467 @@ +// ***************************************************************************** +// * 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 "icon_buffer.h" +#include +#include +#include //includes +#include +#include +#include +#include "base/icon_loader.h" + + +using namespace zen; +using namespace fff; +using AFS = AbstractFileSystem; + + +namespace +{ +const size_t BUFFER_SIZE_MAX = 1000; //maximum number of icons to hold in buffer: must be big enough to hold visible icons + preload buffer! + + +} + +//################################################################################################################################################ + +std::variant getDisplayIcon(const AbstractPath& itemPath, IconBuffer::IconSize sz) +{ + //1. try to load thumbnails + switch (sz) + { + case IconBuffer::IconSize::small: + break; + case IconBuffer::IconSize::medium: + case IconBuffer::IconSize::large: + try + { + if (ImageHolder ih = AFS::getThumbnailImage(itemPath, IconBuffer::getPixSize(sz))) //throw FileError; optional return value + return ih; + } + catch (FileError&) {} + + //else: fallback to non-thumbnail icon + break; + } + + //2. retrieve file icons + try + { + if (FileIconHolder fih = AFS::getFileIcon(itemPath, IconBuffer::getPixSize(sz))) //throw FileError; optional return value + return fih; + } + catch (FileError&) {} + + //run getIconByTemplatePath()/genericFileIcon() fallbacks on main thread: + //extractWxImage() might fail if icon theme is missing a MIME type! + return ImageHolder(); +} + +//################################################################################################################################################ + +//---------------------- Shared Data ------------------------- +class WorkLoad +{ +public: + //context of main thread + void set(const std::vector& newLoad) + { + assert(runningOnMainThread()); + { + std::lock_guard dummy(lockFiles_); + workLoad_ = newLoad; + } + conditionNewWork_.notify_all(); //instead of notify_one(); work around bug: https://svn.boost.org/trac/boost/ticket/7796 + //condition handling, see: https://www.boost.org/doc/libs/1_43_0/doc/html/thread/synchronization.html#thread.synchronization.condvar_ref + } + + void add(const AbstractPath& filePath) //context of main thread + { + assert(runningOnMainThread()); + { + std::lock_guard dummy(lockFiles_); + workLoad_.emplace_back(filePath); //set as next item to retrieve + } + conditionNewWork_.notify_all(); + } + + //context of worker thread, blocking: + AbstractPath extractNext() //throw ThreadStopRequest + { + assert(!runningOnMainThread()); + std::unique_lock dummy(lockFiles_); + + interruptibleWait(conditionNewWork_, dummy, [this] { return !workLoad_.empty(); }); //throw ThreadStopRequest + + AbstractPath filePath = workLoad_. back(); //yes, no strong exception guarantee (std::bad_alloc) + /**/ workLoad_.pop_back(); // + return filePath; + } + +private: + //AbstractPath is thread-safe like an int! + std::mutex lockFiles_; + std::condition_variable conditionNewWork_; //signal event: data for processing available + std::vector workLoad_; //processes last elements of vector first! +}; + + +class Buffer +{ +public: + //called by main and worker thread: + bool hasIcon(const AbstractPath& filePath) const + { + std::lock_guard dummy(lockIconList_); + return iconList.contains(filePath); + } + + //- must be called by main thread only! => wxImage is NOT thread-safe like an int (non-atomic ref-count!!!) + //- check wxImage::IsOk() + implement fallback if needed + std::optional retrieve(const AbstractPath& filePath) + { + assert(runningOnMainThread()); + std::lock_guard dummy(lockIconList_); + + auto it = iconList.find(filePath); + if (it == iconList.end()) + return {}; + + markAsHot(it); + + IconData& idata = refData(it); + + if (ImageHolder* ih = std::get_if(&idata.iconHolder)) + { + if (*ih) //if not yet converted... + { + idata.iconImg = std::make_unique(extractWxImage(std::move(*ih))); //convert in main thread! + assert(!*ih); + } + } + else + { + if (FileIconHolder& fih = std::get(idata.iconHolder)) //if not yet converted... + { + idata.iconImg = std::make_unique(extractWxImage(std::move(fih))); //convert in main thread! + assert(!fih); + //!idata.iconImg->IsOk(): extractWxImage() might fail if icon theme is missing a MIME type! + } + } + + return idata.iconImg ? *idata.iconImg : wxNullImage; //idata.iconHolder may be inserted as empty from worker thread! + } + + //called by main and worker thread: + void insert(const AbstractPath& filePath, std::variant&& ih) + { + std::lock_guard dummy(lockIconList_); + + //thread safety: moving ImageHolder is free from side effects, but ~wxImage() is NOT! => do NOT delete items from iconList here! + const auto [it, inserted] = iconList.try_emplace(filePath); + assert(inserted); + if (inserted) + { + refData(it).iconHolder = std::move(ih); + priorityListPushBack(it); + } + } + + //must be called by main thread only! => ~wxImage() is NOT thread-safe! + //call at an appropriate time, e.g. after Workload::set() + void limitSize() + { + assert(runningOnMainThread()); + std::lock_guard dummy(lockIconList_); + + while (iconList.size() > BUFFER_SIZE_MAX) + { + auto itDelPos = firstInsertPos_; + priorityListPopFront(); + iconList.erase(itDelPos); //remove oldest element + } + } + +private: + struct IconData; + using FileIconMap = std::map; + IconData& refData(FileIconMap::iterator it) { return it->second; } + + //call while holding lock: + void priorityListPopFront() + { + assert(firstInsertPos_!= iconList.end()); + firstInsertPos_ = refData(firstInsertPos_).next; + + if (firstInsertPos_ != iconList.end()) + refData(firstInsertPos_).prev = iconList.end(); + else //priority list size > BUFFER_SIZE_MAX in this context, but still for completeness: + lastInsertPos_ = iconList.end(); + } + + //call while holding lock: + void priorityListPushBack(FileIconMap::iterator it) + { + if (lastInsertPos_ == iconList.end()) + { + assert(firstInsertPos_ == iconList.end()); + firstInsertPos_ = lastInsertPos_ = it; + refData(it).prev = refData(it).next = iconList.end(); + } + else + { + refData(it).next = iconList.end(); + refData(it).prev = lastInsertPos_; + refData(lastInsertPos_).next = it; + lastInsertPos_ = it; + } + } + + //call while holding lock: + void markAsHot(FileIconMap::iterator it) //mark existing buffer entry as if newly inserted + { + assert(it != iconList.end()); + if (refData(it).next != iconList.end()) + { + if (refData(it).prev != iconList.end()) + { + refData(refData(it).prev).next = refData(it).next; //remove somewhere from the middle + refData(refData(it).next).prev = refData(it).prev; // + } + else + { + assert(it == firstInsertPos_); + priorityListPopFront(); + } + priorityListPushBack(it); + } + else + { + if (refData(it).prev != iconList.end()) + assert(it == lastInsertPos_); //nothing to do + else + assert(iconList.size() == 1 && it == firstInsertPos_ && it == lastInsertPos_); //nothing to do + } + } + + struct IconData + { + IconData() {} + IconData(IconData&& tmp) noexcept : iconHolder(std::move(tmp.iconHolder)), iconImg(std::move(tmp.iconImg)), prev(tmp.prev), next(tmp.next) {} + + std::variant iconHolder; //native icon representation: may be used by any thread + + std::unique_ptr iconImg; //use ONLY from main thread! + //wxImage is NOT thread-safe: non-atomic ref-count just to begin with... + //- prohibit implicit calls to wxImage() + //- prohibit calls to ~wxImage() and transitively ~IconData() + //- prohibit even wxImage() default constructor - better be safe than sorry! + + FileIconMap::iterator prev; //store list sorted by time of insertion into buffer + FileIconMap::iterator next; // + }; + + mutable std::mutex lockIconList_; + FileIconMap iconList; //shared resource; Zstring is thread-safe like an int + FileIconMap::iterator firstInsertPos_ = iconList.end(); + FileIconMap::iterator lastInsertPos_ = iconList.end(); +}; + +//################################################################################################################################################ + + +//######################### redirect to impl ##################################################### + +struct IconBuffer::Impl +{ + //communication channel used by threads: + WorkLoad workload; //manage life time: enclose InterruptibleThread's (until joined)!!! + Buffer buffer; // + + InterruptibleThread worker; + //------------------------- + //------------------------- + std::unordered_map extensionIcons; //no item count limit!? Test case C:\ ~ 3800 unique file extensions +}; + + +IconBuffer::IconBuffer(IconSize sz) : pimpl_(std::make_unique()), iconSizeType_(sz) +{ + pimpl_->worker = InterruptibleThread([&workload = pimpl_->workload, &buffer = pimpl_->buffer, sz] + { + setCurrentThreadName(Zstr("Icon Buffer")); + + for (;;) + { + //start work: blocks until next icon to load is retrieved: + const AbstractPath itemPath = workload.extractNext(); //throw ThreadStopRequest + + if (!buffer.hasIcon(itemPath)) //perf: workload may contain duplicate entries? + buffer.insert(itemPath, getDisplayIcon(itemPath, sz)); + } + }); +} + + +IconBuffer::~IconBuffer() +{ + setWorkload({}); //make sure interruption point is always reached! needed??? + pimpl_->worker.requestStop(); //end thread life time *before* + pimpl_->worker.join(); //IconBuffer::Impl member clean up! +} + + +int IconBuffer::getPixSize(IconSize sz) +{ + //coordinate with getIconByIndexImpl() and linkOverlayIcon()! + switch (sz) + { + case IconSize::small: + return dipToScreen(getMenuIconDipSize()); + case IconSize::medium: + return dipToScreen(48); + case IconSize::large: + return dipToScreen(128); + } + assert(false); + return 0; +} + + +bool IconBuffer::readyForRetrieval(const AbstractPath& filePath) +{ + return pimpl_->buffer.hasIcon(filePath); +} + + +std::optional IconBuffer::retrieveFileIcon(const AbstractPath& filePath) +{ + const Zstring fileName = AFS::getItemName(filePath); + if (std::optional ico = pimpl_->buffer.retrieve(filePath)) + { + if (ico->IsOk()) + return ico; + else //fallback + return this->getIconByExtension(fileName); //buffered! + } + + //since this icon seems important right now, we don't want to wait until next setWorkload() to start retrieving + pimpl_->workload.add(filePath); + pimpl_->buffer.limitSize(); + return {}; +} + + +void IconBuffer::setWorkload(const std::vector& load) +{ + assert(load.size() < BUFFER_SIZE_MAX / 2); + + pimpl_->workload.set(load); //since buffer can only increase due to new workload, + pimpl_->buffer.limitSize(); //this is the place to impose the limit from main thread! +} + + +wxImage IconBuffer::getIconByExtension(const Zstring& filePath) +{ + const Zstring& ext = getFileExtension(filePath); + + assert(runningOnMainThread()); + + auto it = pimpl_->extensionIcons.find(ext); + if (it == pimpl_->extensionIcons.end()) + { + const Zstring& templateName(ext.empty() ? Zstr("file") : Zstr("file.") + ext); + //don't pass actual file name to getIconByTemplatePath(), e.g. "AUTHORS" has own mime type on Linux!!! + //=> buffer by extension to minimize buffer-misses! + + wxImage img; + try + { + img = extractWxImage(getIconByTemplatePath(templateName, getPixSize(iconSizeType_))); //throw SysError + } + catch (SysError&) {} + if (!img.IsOk()) //Linux: not all MIME types have icons! + img = IconBuffer::genericFileIcon(iconSizeType_); + + it = pimpl_->extensionIcons.emplace(ext, img).first; + } + //need buffer size limit??? + return it->second; +} + + +wxImage IconBuffer::genericFileIcon(IconSize sz) +{ + try + { + return extractWxImage(fff::genericFileIcon(IconBuffer::getPixSize(sz))); //throw SysError + } + catch (SysError&) { assert(false); return wxNullImage; } +} + + +wxImage IconBuffer::genericDirIcon(IconSize sz) +{ + try + { + return extractWxImage(fff::genericDirIcon(IconBuffer::getPixSize(sz))); //throw SysError + } + catch (SysError&) { assert(false); return wxNullImage; } +} + + +wxImage IconBuffer::linkOverlayIcon(IconSize sz) +{ + //coordinate with IconBuffer::getPixSize()! + return loadImage([sz] + { + const int iconSize = IconBuffer::getPixSize(sz); + + if (iconSize >= dipToScreen(128)) return "file_link_128"; + if (iconSize >= dipToScreen( 48)) return "file_link_48"; + if (iconSize >= dipToScreen( 20)) return "file_link_20"; + return "file_link_16"; + }()); +} + + +wxImage IconBuffer::plusOverlayIcon(IconSize sz) +{ + //coordinate with IconBuffer::getPixSize()! + return loadImage([sz] + { + const int iconSize = IconBuffer::getPixSize(sz); + + if (iconSize >= dipToScreen(128)) return "file_plus_128"; + if (iconSize >= dipToScreen( 48)) return "file_plus_48"; + if (iconSize >= dipToScreen( 20)) return "file_plus_20"; + return "file_plus_16"; + }()); +} + + +wxImage IconBuffer::minusOverlayIcon(IconSize sz) +{ + //coordinate with IconBuffer::getPixSize()! + return loadImage([sz] + { + const int iconSize = IconBuffer::getPixSize(sz); + + if (iconSize >= dipToScreen(128)) return "file_minus_128"; + if (iconSize >= dipToScreen( 48)) return "file_minus_48"; + if (iconSize >= dipToScreen( 20)) return "file_minus_20"; + return "file_minus_16"; + }()); +} + + +bool fff::hasLinkExtension(const Zstring& filepath) +{ + const Zstring& ext = getFileExtension(filepath); + return ext == "desktop"; + +} diff --git a/FreeFileSync/Source/icon_buffer.h b/FreeFileSync/Source/icon_buffer.h new file mode 100644 index 0000000..cb66508 --- /dev/null +++ b/FreeFileSync/Source/icon_buffer.h @@ -0,0 +1,57 @@ +// ***************************************************************************** +// * 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 ICON_BUFFER_H_8425703245726394256 +#define ICON_BUFFER_H_8425703245726394256 + +#include +#include +#include +#include +#include "afs/abstract.h" + + +namespace fff +{ +class IconBuffer +{ +public: + enum class IconSize + { + small, + medium, + large + }; + + explicit IconBuffer(IconSize sz); + ~IconBuffer(); + + static int getPixSize(IconSize sz); //expected and *maximum* icon size in pixel + int getPixSize() const { return getPixSize(iconSizeType_); } // + + void setWorkload (const std::vector& load); //(re-)set new workload of icons to be retrieved; + bool readyForRetrieval(const AbstractPath& filePath); + std::optional retrieveFileIcon (const AbstractPath& filePath); //... and mark as hot + wxImage getIconByExtension(const Zstring& filePath); //...and add to buffer + //retrieveFileIcon() + getIconByExtension() are safe to call from within WM_PAINT handler! no COM calls (...on calling thread) + + static wxImage genericFileIcon (IconSize sz); + static wxImage genericDirIcon (IconSize sz); + static wxImage linkOverlayIcon (IconSize sz); + static wxImage plusOverlayIcon (IconSize sz); + static wxImage minusOverlayIcon(IconSize sz); + +private: + struct Impl; + const std::unique_ptr pimpl_; + + const IconSize iconSizeType_; +}; + +bool hasLinkExtension(const Zstring& filepath); +} + +#endif //ICON_BUFFER_H_8425703245726394256 diff --git a/FreeFileSync/Source/localization.cpp b/FreeFileSync/Source/localization.cpp new file mode 100644 index 0000000..37ccd83 --- /dev/null +++ b/FreeFileSync/Source/localization.cpp @@ -0,0 +1,451 @@ +// ***************************************************************************** +// * 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 "localization.h" +#include //setlocale +#include +#include +#include +#include +#include +#include "parse_lng.h" + +using namespace zen; +using namespace fff; + + +namespace +{ +class FFSTranslation : public TranslationHandler +{ +public: + FFSTranslation(const std::string& lngStream, bool haveRtlLayout); //throw lng::ParsingError, plural::ParsingError + + std::wstring translate(const std::wstring& text) const override + { + //look for translation in buffer table + auto it = transMapping_.find(text); + if (it != transMapping_.end() && !it->second.empty()) + return it->second; + return text; //fallback + } + + std::wstring translate(const std::wstring& singular, const std::wstring& plural, int64_t n) const override + { + auto it = transMappingPl_.find({singular, plural}); + if (it != transMappingPl_.end()) + { + const size_t formNo = pluralParser_->getForm(n); + assert(formNo < it->second.size()); + if (formNo < it->second.size()) + return replaceCpy(it->second[formNo], L"%x", formatNumber(n)); + } + return replaceCpy(std::abs(n) == 1 ? singular : plural, L"%x", formatNumber(n)); //fallback + } + + bool layoutIsRtl() const override { return haveRtlLayout_; } + +private: + using Translation = std::unordered_map; //hash_map is 15% faster than std::map on GCC + using TranslationPlural = std::map, std::vector>; + + Translation transMapping_; //map original text |-> translation + TranslationPlural transMappingPl_; + std::optional pluralParser_; //bound! + const bool haveRtlLayout_; +}; + + +FFSTranslation::FFSTranslation(const std::string& lngStream, bool haveRtlLayout) ://throw lng::ParsingError, plural::ParsingError + haveRtlLayout_(haveRtlLayout) +{ + lng::TransHeader header; + lng::TranslationMap transUtf; + lng::TranslationPluralMap transPluralUtf; + lng::parseLng(lngStream, header, transUtf, transPluralUtf); //throw ParsingError + + pluralParser_.emplace(header.pluralDefinition); //throw plural::ParsingError + + for (const auto& [original, translation] : transUtf) + transMapping_.emplace(utfTo(original), + utfTo(translation)); + + for (const auto& [singAndPlural, pluralForms] : transPluralUtf) + { + std::vector transPluralForms; + for (const std::string& pf : pluralForms) + transPluralForms.push_back(utfTo(pf)); + + transMappingPl_.insert({{ + utfTo(singAndPlural.first), + utfTo(singAndPlural.second) + }, + std::move(transPluralForms)}); + } +} + + +std::vector loadTranslations(const Zstring& zipPath) //throw FileError +{ + std::vector> streams; + [&] + { + std::string rawStream; + try //to load from ZIP first: + { + rawStream = getFileContent(zipPath, nullptr /*notifyUnbufferedIO*/); //throw FileError + } + catch (FileError&) //fall back to folder: dev build (only!?) + { + const Zstring fallbackFolder = beforeLast(zipPath, Zstr(".zip"), IfNotFoundReturn::none); + if (!itemExists(fallbackFolder)) //throw FileError + throw; + + traverseFolder(fallbackFolder, [&](const FileInfo& fi) + { + if (endsWith(fi.fullPath, Zstr(".lng"))) + { + std::string stream = getFileContent(fi.fullPath, nullptr /*notifyUnbufferedIO*/); //throw FileError + streams.emplace_back(fi.fullPath, std::move(stream)); + } + }, nullptr, nullptr); //throw FileError + return; + } + //------------------------------------------------------------- + + wxMemoryInputStream byteStream(rawStream.c_str(), rawStream.size()); //does not take ownership + wxZipInputStream zipStream(byteStream, wxConvUTF8); + + while (const auto& entry = std::unique_ptr(zipStream.GetNextEntry())) //take ownership! + { + if (entry->IsDir()) //e.g. translators accidentally ZIPing "Languages" directory + throw FileError(replaceCpy(replaceCpy(L"ZIP file %x contains unexpected sub directory %y.", + L"%x", fmtPath(zipPath)), + L"%y", fmtPath(utfTo(entry->GetName())))); + + if (std::string stream(entry->GetSize(), '\0'); + zipStream.ReadAll(stream.data(), stream.size())) + streams.emplace_back(zipPath + Zstr(':') + utfTo(entry->GetName()), std::move(stream)); + else + assert(false); + } + }(); + //-------------------------------------------------------------------- + + std::vector translations + { + //default entry: + { + .languageID = wxLANGUAGE_ENGLISH_US, + .locale = "en_US", + .languageName = L"English", + .translatorName = L"Zenju", + .languageFlag = "flag_usa", + .lngFileName = Zstr(""), + .lngStream = "", + } + }; + + for (/*const*/ auto& [filePath, stream] : streams) + try + { + const lng::TransHeader lngHeader = lng::parseHeader(stream); //throw ParsingError + assert(!lngHeader.languageName .empty()); + assert(!lngHeader.translatorName.empty()); + assert(!lngHeader.locale .empty()); + assert(!lngHeader.flagFile .empty()); + + const wxLanguageInfo* lngInfo = wxUILocale::FindLanguageInfo(utfTo(lngHeader.locale)); + assert(lngInfo && lngInfo->CanonicalName == utfTo(lngHeader.locale)); + if (lngInfo) + translations.push_back( + { + .languageID = static_cast(lngInfo->Language), + .locale = lngHeader.locale, + .languageName = utfTo(lngHeader.languageName), + .translatorName = utfTo(lngHeader.translatorName), + .languageFlag = lngHeader.flagFile, + .lngFileName = filePath, + .lngStream = std::move(stream), + }); + } + catch (const lng::ParsingError& e) + { + throw FileError(replaceCpy(replaceCpy(replaceCpy(_("Error parsing file %x, row %y, column %z."), + L"%x", fmtPath(filePath)), + L"%y", formatNumber(e.row + 1)), + L"%z", formatNumber(e.col + 1)) + + L"\n\n" + e.msg); + } + + std::sort(translations.begin(), translations.end(), [](const TranslationInfo& lhs, const TranslationInfo& rhs) + { + return LessNaturalSort()(utfTo(lhs.languageName), + utfTo(rhs.languageName)); //"natural" sort: ignore case and diacritics + }); + return translations; +} + + +/* Some ISO codes are used by multiple wxLanguage IDs which can lead to incorrect mapping by wxUILocale::FindLanguageInfo()!!! + => Identify by description, e.g. "Chinese (Traditional)". The following IDs are affected: + - zh_TW: wxLANGUAGE_CHINESE_TAIWAN, wxLANGUAGE_CHINESE, wxLANGUAGE_CHINESE_TRADITIONAL_EXPLICIT + - en_GB: wxLANGUAGE_ENGLISH_UK, wxLANGUAGE_ENGLISH + - es_ES: wxLANGUAGE_SPANISH, wxLANGUAGE_SPANISH_SPAIN */ +wxLanguage mapLanguageDialect(wxLanguage lng) +{ + if (const wxString& canonicalName = wxUILocale::GetLanguageCanonicalName(lng); + !canonicalName.empty()) + { + assert(!contains(canonicalName, L'-')); + const std::string locale = beforeFirst(utfTo(canonicalName), '@', IfNotFoundReturn::all); //e.g. "sr_RS@latin"; see wxUILocale::InitLanguagesDB() + const std::string lngCode = beforeFirst(locale, '_', IfNotFoundReturn::all); + + if (lngCode == "zh") + { + if (lng == wxLANGUAGE_CHINESE) //wxWidgets assigns this to "zh_TW" for some reason + return wxLANGUAGE_CHINESE_CHINA; + + for (const char* l : {"zh_HK", "zh_MO", "zh_TW"}) + if (locale == l) + return wxLANGUAGE_CHINESE_TAIWAN; + + return wxLANGUAGE_CHINESE_CHINA; + } + + if (lngCode == "en") + { + if (lng == wxLANGUAGE_ENGLISH || //wxWidgets assigns this to "en_GB" for some reason + lng == wxLANGUAGE_ENGLISH_WORLD) + return wxLANGUAGE_ENGLISH_US; + + for (const char* l : {"en_US", "en_CA", "en_AS", "en_UM", "en_VI"}) + if (locale == l) + return wxLANGUAGE_ENGLISH_US; + + return wxLANGUAGE_ENGLISH_UK; + } + + if (lngCode == "nb" || lngCode == "nn") //wxLANGUAGE_NORWEGIAN_BOKMAL, wxLANGUAGE_NORWEGIAN_NYNORSK + return wxLANGUAGE_NORWEGIAN; + + if (locale == "pt_BR") + return wxLANGUAGE_PORTUGUESE_BRAZILIAN; + + //all other cases: map to primary language code + if (contains(locale, '_')) + if (const wxLanguageInfo* lngInfo2 = wxUILocale::FindLanguageInfo(utfTo(lngCode))) + return static_cast(lngInfo2->Language); + } + return lng; //including wxLANGUAGE_DEFAULT, wxLANGUAGE_UNKNOWN +} + + +//we need to interface with wxWidgets' translation handling for a few translations used in their internal source files +// => since there is no better API: dynamically generate a MO file and feed it to wxTranslation +class MemoryTranslationLoader : public wxTranslationsLoader +{ +public: + MemoryTranslationLoader(wxLanguage langId, std::map&& transMapping) : + canonicalName_(wxUILocale::GetLanguageCanonicalName(langId)) + { + assert(!canonicalName_.empty()); + static_assert(std::is_same_v, std::map>); //translations *must* be sorted in MO file! + + //https://www.gnu.org/software/gettext/manual/html_node/MO-Files.html + transMapping[""] = L"Content-Type: text/plain; charset=UTF-8\n"; + + const int headerSize = 7 * sizeof(uint32_t); + writeNumber(moBuf_, 0x950412de); //magic number + writeNumber(moBuf_, 0); //format version + writeNumber(moBuf_, transMapping.size()); //string count + writeNumber(moBuf_, headerSize); //string references offset: original + writeNumber(moBuf_, headerSize + (2 * sizeof(uint32_t)) * transMapping.size()); //string references offset: translation + writeNumber(moBuf_, 0); //size of hashing table + writeNumber(moBuf_, 0); //offset of hashing table + + const int stringsOffset = headerSize + 2 * (2 * sizeof(uint32_t)) * transMapping.size(); + std::string stringsList; + + for (const auto& [original, translation] : transMapping) + { + writeNumber(moBuf_, original.size()); //string length + writeNumber(moBuf_, stringsOffset + stringsList.size()); //string offset + stringsList.append(original.c_str(), original.size() + 1); //include 0-termination + } + + for (const auto& [original, translationW] : transMapping) + { + const auto& translation = utfTo(translationW); + writeNumber(moBuf_, translation.size()); //string length + writeNumber(moBuf_, stringsOffset + stringsList.size()); //string offset + stringsList.append(translation.c_str(), translation.size() + 1); //include 0-termination + } + + writeArray(moBuf_, stringsList.c_str(), stringsList.size()); + } + + wxMsgCatalog* LoadCatalog(const wxString& domain, const wxString& lang) override + { + //"lang" is NOT (exactly) what we return from GetAvailableTranslations(), but has a little "extra" + //e.g.: de_DE.WINDOWS-1252 ar.WINDOWS-1252 zh_TW.MacRoman + auto extractIsoLangCode = [](wxString langCode) { return beforeLast(langCode, L".", IfNotFoundReturn::all); }; + + if (equalAsciiNoCase(extractIsoLangCode(lang), extractIsoLangCode(canonicalName_))) + return wxMsgCatalog::CreateFromData(wxScopedCharBuffer::CreateNonOwned(moBuf_.ref().c_str(), moBuf_.ref().size()), domain); + + assert(false); + return nullptr; + } + + wxArrayString GetAvailableTranslations(const wxString& domain) const override + { + wxArrayString available; + available.push_back(canonicalName_); + return available; + } + +private: + const wxString canonicalName_; + MemoryStreamOut moBuf_; +}; + + +std::vector globalTranslations; +wxLanguage globalLang = wxLANGUAGE_UNKNOWN; +} + + +void fff::localizationInit(const Zstring& zipPath) //throw FileError +{ + /* wxLocale vs wxUILocale (since wxWidgets 3.1.6) + ------------------------------------------|-------------------- + calls setlocale() Windows, Linux, maCOS | Linux only + wxTranslations initialized | not initialized + + caveat: setlocale() calls on macOS lead to bugs: + - breaks wxWidgets file drag and drop! https://freefilesync.org/forum/viewtopic.php?t=8215 + - "under macOS C locale must not be changed, as doing this exposes bugs in the system": https://docs.wxwidgets.org/trunk/classwx_u_i_locale.html + + reproduce: - std::setlocale(LC_ALL, ""); + - double-click the app (*) + - drag and drop folder named "アアアア" + - wxFileDropTarget::OnDropFiles() called with empty file array! + + *) CAVEAT: context matters! this yields a different user-preferred locale than running Contents/MacOS/FreeFileSync_main!!! + e.g. 1. locale after wxLocale creation is "en_US" + 2. call std::setlocale(LC_ALL, ""): + a) app was double-clicked: locale is "C" => drag/drop FAILS! + b) run Contents/MacOS/FreeFileSync_main: locale is "en_US.UTF-8" => drag/drop works! */ + [[maybe_unused]] const bool rv = wxUILocale::UseDefault(); + assert(rv); + + //const char* currentLocale = std::setlocale(LC_ALL, nullptr); + + assert(!wxTranslations::Get()); + wxTranslations::Set(new wxTranslations() /*pass ownership*/); //implicitly done by wxLocale, but *not* wxUILocale + + //throw *after* mandatory initialization: setLanguage() requires wxTranslations::Get()! + + assert(globalTranslations.empty()); + globalTranslations = loadTranslations(zipPath); //throw FileError + + setLanguage(getDefaultLanguage()); //throw FileError +} + + +void fff::localizationCleanup() +{ + assert(!globalTranslations.empty()); +#if 0 //good place for clean up rather than some time during static destruction: is this an actual benefit??? + globalLang = wxLANGUAGE_UNKNOWN; + + setTranslator(nullptr); + + globalTranslations.clear(); +#endif +} + + +void fff::setLanguage(wxLanguage lng) //throw FileError +{ + if (globalLang == lng) + return; //support polling + + //(try to) retrieve language file + std::string lngStream; + Zstring lngFileName; + + for (const TranslationInfo& e : getAvailableTranslations()) + if (e.languageID == lng) + { + lngStream = e.lngStream; + lngFileName = e.lngFileName; + break; + } + + //load language file into buffer + if (lngStream.empty()) //if file stream is empty, texts will be English (US) by default + { + setTranslator(nullptr); + lng = wxLANGUAGE_ENGLISH_US; + } + else + try + { + bool haveRtlLayout = false; + if (const wxLanguageInfo* selLngInfo = wxUILocale::GetLanguageInfo(lng)) + haveRtlLayout = selLngInfo->LayoutDirection == wxLayout_RightToLeft; + + setTranslator(std::make_unique(lngStream, haveRtlLayout)); //throw lng::ParsingError, plural::ParsingError + } + catch (const lng::ParsingError& e) + { + throw FileError(replaceCpy(replaceCpy(replaceCpy(_("Error parsing file %x, row %y, column %z."), + L"%x", fmtPath(lngFileName)), + L"%y", formatNumber(e.row + 1)), + L"%z", formatNumber(e.col + 1)) + + L"\n\n" + e.msg); + } + catch (plural::ParsingError&) + { + throw FileError(L"Invalid plural form definition: " + fmtPath(lngFileName)); //user should never see this! + } + //------------------------------------------------------------ + + globalLang = lng; + + //add translation for wxWidgets-internal strings: + std::map transMapping = + { + {"&OK", _("OK")}, //for wxTextEntryDialog + {"&Cancel", _("Cancel")}, //=> shouldn't use accelerator keys! + }; + wxTranslations& wxtrans = *wxTranslations::Get(); //*assert* creation by localizationInit()! + wxtrans.SetLanguage(lng); //!= wxLocale's language, which could be wxLANGUAGE_DEFAULT + wxtrans.SetLoader(new MemoryTranslationLoader(lng, std::move(transMapping))); + [[maybe_unused]] const bool catalogAdded = wxtrans.AddCatalog(wxString()); + assert(catalogAdded || lng == wxLANGUAGE_ENGLISH_US); +} + + +const std::vector& fff::getAvailableTranslations() +{ + assert(!globalTranslations.empty()); //localizationInit() not called, or failed!? + return globalTranslations; +} + + +wxLanguage fff::getDefaultLanguage() +{ + static const wxLanguage defaultLng = mapLanguageDialect(static_cast(wxUILocale::GetSystemLanguage())); + //uses GetUserPreferredUILanguages() since wxWidgets 1.3.6, not GetUserDefaultUILanguage() anymore: + // https://github.com/wxWidgets/wxWidgets/blob/master/src/common/intl.cpp + return defaultLng; +} + + +wxLanguage fff::getLanguage() { return globalLang; } diff --git a/FreeFileSync/Source/localization.h b/FreeFileSync/Source/localization.h new file mode 100644 index 0000000..a701217 --- /dev/null +++ b/FreeFileSync/Source/localization.h @@ -0,0 +1,39 @@ +// ***************************************************************************** +// * 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 LOCALIZATION_H_8917342083178321534 +#define LOCALIZATION_H_8917342083178321534 + +#include +#include +#include + + +namespace fff +{ +struct TranslationInfo +{ + wxLanguage languageID = wxLANGUAGE_UNKNOWN; + std::string locale; + std::wstring languageName; + std::wstring translatorName; + std::string languageFlag; + Zstring lngFileName; + std::string lngStream; +}; +const std::vector& getAvailableTranslations(); + +wxLanguage getDefaultLanguage(); +wxLanguage getLanguage(); + +void setLanguage(wxLanguage lng); //throw FileError + +void localizationInit(const Zstring& zipPath); //throw FileError +void localizationCleanup(); //wxLocale crashes miserably on wxGTK when destructor runs during global cleanup => call in wxApp::OnExit +//"You should delete all wxWidgets object that you created by the time OnExit() finishes. In particular, do not destroy them from application class' destructor!" +} + +#endif //LOCALIZATION_H_8917342083178321534 diff --git a/FreeFileSync/Source/log_file.cpp b/FreeFileSync/Source/log_file.cpp new file mode 100644 index 0000000..64f6aad --- /dev/null +++ b/FreeFileSync/Source/log_file.cpp @@ -0,0 +1,673 @@ +// ***************************************************************************** +// * 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 "log_file.h" +#include +#include + +using namespace zen; +using namespace fff; +using AFS = AbstractFileSystem; + + +namespace +{ +const int LOG_PREVIEW_MAX = 25; + +const int EMAIL_PREVIEW_MAX = LOG_PREVIEW_MAX; +const int EMAIL_ITEMS_MAX = 250; + +const int EMAIL_SHORT_PREVIEW_MAX = 5; //summary email +const int EMAIL_SHORT_ITEMS_MAX = 0; // + +const int SEPARATION_LINE_LEN = 40; + + +std::string generateLogHeaderTxt(const ProcessSummary& s, const ErrorLog& log, int logPreviewMax) +{ + const auto tabSpace = utfTo(TAB_SPACE); + + std::string headerLine; + for (const std::wstring& jobName : s.jobNames) + headerLine += (headerLine.empty() ? "" : " + ") + utfTo(jobName); + + if (!headerLine.empty()) + headerLine += ' '; + + const TimeComp tc = getLocalTime(std::chrono::system_clock::to_time_t(s.startTime)); //returns TimeComp() on error + headerLine += utfTo(formatTime(formatDateTag, tc) + Zstr(" [") + formatTime(formatTimeTag, tc) + Zstr(']')); + + //assemble summary box + std::vector summary; + summary.emplace_back(); + summary.push_back(tabSpace + utfTo(getSyncResultLabel(s.result))); + summary.emplace_back(); + + const ErrorLogStats logCount = getStats(log); + + if (logCount.errors > 0) summary.push_back(tabSpace + utfTo(_("Errors:") + L' ' + formatNumber(logCount.errors))); + if (logCount.warnings > 0) summary.push_back(tabSpace + utfTo(_("Warnings:") + L' ' + formatNumber(logCount.warnings))); + + summary.push_back(tabSpace + utfTo(_("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. cancel during folder comparison + s.statsProcessed == s.statsTotal) //...if everything was processed successfully + ; + else + summary.push_back(tabSpace + utfTo(_("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(s.totalTime).count(); + summary.push_back(tabSpace + utfTo(_("Total time:")) + ' ' + utfTo(formatTimeSpan(totalTimeSec))); + + size_t sepLineLen = 0; //calculate max width (considering Unicode!) + for (const std::string& str : summary) + sepLineLen = std::max(sepLineLen, unicodeLength(str)); + + std::string output = headerLine + '\n'; + output += std::string(sepLineLen + 1, '_') + '\n'; + + for (const std::string& str : summary) + output += '|' + str + '\n'; + + output += '|' + std::string(sepLineLen, '_') + "\n\n"; + + //------------ warnings/errors preview ---------------- + const int logFailTotal = logCount.warnings + logCount.errors; + if (logFailTotal > 0) + { + output += '\n' + utfTo(_("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)) + { + if (previewCount++ >= logPreviewMax) + break; + output += utfTo(formatMessage(entry)); + } + + if (logFailTotal > previewCount) + output += " [...] " + utfTo(replaceCpy(_P("Showing %y of 1 item", "Showing %y of %x items", logFailTotal), //%x used as plural form placeholder! + L"%y", formatNumber(previewCount))) + '\n'; + output += std::string(SEPARATION_LINE_LEN, '_') + "\n\n\n"; + } + return output; +} + + +std::string generateLogFooterTxt(const std::wstring& logFilePath /*optional*/, int logItemsTotal, int logItemsMax) //throw FileError +{ + const ComputerModel cm = getComputerModel(); //throw FileError + + std::string output; + if (logItemsTotal > logItemsMax) + output += " [...] " + utfTo(replaceCpy(_P("Showing %y of 1 item", "Showing %y of %x items", logItemsTotal), //%x used as plural form placeholder! + L"%y", formatNumber(logItemsMax))) + '\n'; + + output += std::string(SEPARATION_LINE_LEN, '_') + '\n' + + + utfTo(getOsDescription() + /*throw FileError*/ + + L" - " + utfTo(getUserDescription()) /*throw FileError*/ + + (!cm.model .empty() ? L" - " + cm.model : L"") + + (!cm.vendor.empty() ? L" - " + cm.vendor : L"")) + '\n'; + if (!logFilePath.empty()) + output += utfTo(_("Log file:") + L' ' + logFilePath) + '\n'; + + return output; +} + + +std::string htmlTxt(const std::string_view& str) +{ + std::string msg = htmlSpecialChars(str); + trim(msg); + if (!contains(msg, '\n')) + return msg; + + std::string msgFmt; + for (auto it = msg.begin(); it != msg.end(); ) + if (*it == '\n') + { + msgFmt += "
\n"; + ++it; + + //skip duplicate newlines + for (; it != msg.end() && *it == L'\n'; ++it) + ; + + //preserve leading spaces + for (; it != msg.end() && *it == L' '; ++it) + msgFmt += " "; + } + else + msgFmt += *it++; + + return msgFmt; +} + +std::string htmlTxt(const Zstring& str) { return htmlTxt(utfTo(str)); } +std::string htmlTxt(const std::wstring& str) { return htmlTxt(utfTo(str)); } +std::string htmlTxt(const wchar_t* str) { return htmlTxt(utfTo(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: typeImage = "msg-error.png"; break; + } + //*both* width + height are required (or at least Thunderbird image size calculation glitches out) + return R"( + )" + htmlTxt(formatTime(formatTimeTag, getLocalTime(entry.time))) + R"( + ) + )" + htmlTxt(makeStringView(entry.message.begin(), entry.message.end())) + R"( + +)"; +} + + +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] "; + + if (!jobNamesFmt.empty()) + title += jobNamesFmt + L' '; + + switch (s.result) + { + case TaskResult::success: title += utfTo("\xe2\x9c\x94" "\xef\xb8\x8f"); break; //✔️ + case TaskResult::warning: title += utfTo("\xe2\x9a\xa0" "\xef\xb8\x8f"); break; //⚠️ + case TaskResult::error: //efb88f (U+FE0F): variation selector-16 to prefer emoji over text rendering + case TaskResult::cancelled: title += utfTo("\xe2\x9d\x8c" "\xef\xb8\x8f"); break; //❌️ + } + return title; +} + + +std::string generateLogHeaderHtml(const ProcessSummary& s, const ErrorLog& log, int logPreviewMax) +{ + //caveat: non-inline CSS is often ignored by email clients! + std::string output = R"( + + + + + )" + htmlTxt(generateLogTitle(s)) + R"( + + + +)"; + + std::string jobNamesFmt; + for (const std::wstring& jobName : s.jobNames) + jobNamesFmt += (jobNamesFmt.empty() ? "" : " + ") + htmlTxt(jobName); + + const TimeComp tc = getLocalTime(std::chrono::system_clock::to_time_t(s.startTime)); //returns TimeComp() on error + output += R"(
)" + jobNamesFmt + R"(  )" + + htmlTxt(formatTime(formatDateTag, tc)) + "  " + htmlTxt(formatTime(formatTimeTag, tc)) + "
\n"; + + std::string resultsStatusImage; + switch (s.result) + { + case TaskResult::success: resultsStatusImage = "result-succes.png"; break; + case TaskResult::warning: resultsStatusImage = "result-warning.png"; break; + case TaskResult::error: + case TaskResult::cancelled: resultsStatusImage = "result-error.png"; break; + } + output += R"( +
+
+ + )" + htmlTxt(getSyncResultLabel(s.result)) + R"( +
+ )"; + + const ErrorLogStats logCount = getStats(log); + + if (logCount.errors > 0) + output += R"( + + + + + )"; + + if (logCount.warnings > 0) + output += R"( + + + + + )"; + + output += R"( + + + + + )"; + + 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"( + + + + + )"; + + const int64_t totalTimeSec = std::chrono::duration_cast(s.totalTime).count(); + output += R"( + + + + + + +
+)"; + + //------------ warnings/errors preview ---------------- + const int logFailTotal = logCount.warnings + logCount.errors; + if (logFailTotal > 0) + { + output += R"( +
)" + htmlTxt(_("Errors and warnings:")) + R"(
+
+ +)"; + int previewCount = 0; + for (const LogEntry& entry : log) + if (entry.type & (MSG_TYPE_WARNING | MSG_TYPE_ERROR)) + { + if (previewCount++ >= logPreviewMax) + break; + output += formatMessageHtml(entry); + } + + output += R"(
+)"; + if (logFailTotal > previewCount) + output += R"(
[…])" + + htmlTxt(replaceCpy(_P("Showing %y of 1 item", "Showing %y of %x items", logFailTotal), //%x used as plural form placeholder! + L"%y", formatNumber(previewCount))) + "
\n"; + + output += R"(

+)"; + } + + output += R"( + +)"; + return output; +} + + +std::string generateLogFooterHtml(const std::wstring& logFilePath /*optional*/, int logItemsTotal, int logItemsMax) //throw FileError +{ + const std::string osImage = "os-linux.png"; + const ComputerModel cm = getComputerModel(); //throw FileError + + std::string output = R"(
+)"; + + if (logItemsTotal > logItemsMax) + output += R"(
[…])" + + htmlTxt(replaceCpy(_P("Showing %y of 1 item", "Showing %y of %x items", logItemsTotal), //%x used as plural form placeholder! + L"%y", formatNumber(logItemsMax))) + "
\n"; + + output += R"( +
+
+ + )" + htmlTxt(getOsDescription()) + /*throw FileError*/ + + " – " + htmlTxt(getUserDescription()) /*throw FileError*/ + + (!cm.model .empty() ? " – " + htmlTxt(cm.model ) : "") + + (!cm.vendor.empty() ? " – " + htmlTxt(cm.vendor) : "") + R"( +
)"; + + if (!logFilePath.empty()) + output += R"( +
+ ) + )" + htmlTxt(logFilePath) + R"( +
)"; + + output += R"( + + +)"; + return output; +} + +//*INDENT-ON* + + +//write log items in blocks instead of creating one big string: memory allocation might fail; think 1 million entries! +template +void streamToLogFile(const ProcessSummary& summary, const ErrorLog& log, + int logPreviewMax, int logItemsMax, + const std::wstring& logFilePath /*optional*/, + LogFileFormat logFormat, Function stringOut /*(const std::string& s); throw X*/) //throw SysError, X +{ + stringOut(logFormat == LogFileFormat::html ? + generateLogHeaderHtml(summary, log, logPreviewMax) : + generateLogHeaderTxt (summary, log, logPreviewMax)); //throw X + + int itemCount = 0; + for (const LogEntry& entry : log) + { + if (itemCount++ >= logItemsMax) + break; + stringOut(logFormat == LogFileFormat::html ? + formatMessageHtml(entry) : + formatMessage (entry)); //throw X + } + + const std::string footer = [&] + { + try + { + return logFormat == LogFileFormat::html ? + generateLogFooterHtml(logFilePath, static_cast(log.size()), logItemsMax): //throw FileError + generateLogFooterTxt (logFilePath, static_cast(log.size()), logItemsMax); // + } + catch (const FileError& e) { throw SysError(replaceCpy(e.toString(), L"\n\n", L'\n')); } //errors should be further enriched by context info => SysError + }(); //caveat: don't catch exceptions thrown by stringOut()! + + stringOut(footer); //throw X +} + + +void saveNewLogFile(const AbstractPath& logFilePath, //throw FileError, X + LogFileFormat logFormat, + const ProcessSummary& summary, + const ErrorLog& log, + const std::function& notifyStatus /*throw X*/) +{ + //create logfile folder if required + if (const std::optional 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, + bytesWritten_ = int64_t(0), + msg_ = replaceCpy(_("Saving file %x..."), L"%x", fmtPath(AFS::getDisplayPath(logFilePath)))] + (int64_t bytesDelta) mutable + { + if (notifyStatus) + notifyStatus(msg_ + L" (" + formatFilesizeShort(bytesWritten_ += bytesDelta) + L')'); //throw X + }; + + //already existing: undefined behavior! (e.g. fail/overwrite/auto-rename) + std::unique_ptr logFileOut = AFS::getOutputStream(logFilePath, + std::nullopt /*streamSize*/, + std::nullopt /*modTime*/); //throw FileError + + BufferedOutputStream streamOut([&](const void* buffer, size_t bytesToWrite) + { + return logFileOut->tryWrite(buffer, bytesToWrite, notifyUnbufferedIO); //throw FileError, X + }, + logFileOut->getBlockSize()); + + try + { + streamToLogFile(summary, log, LOG_PREVIEW_MAX, std::numeric_limits::max() /*logItemsMax*/, + std::wstring() /*logFilePath -> superfluous*/, logFormat, + [&](const std::string& str) { streamOut.write(str.data(), str.size()); } /*throw FileError, X*/); //throw SysError, FileError, X + } + catch (const SysError& e) + { + throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(AFS::getDisplayPath(logFilePath))), e.toString()); + } + + streamOut.flushBuffer(); //throw FileError, X + + logFileOut->finalize(notifyUnbufferedIO); //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 jobNames; //may be empty +}; +std::vector getLogFiles(const AbstractPath& logFolderPath) //throw FileError +{ + std::vector logfiles; + + AFS::traverseFolder(logFolderPath, [&](const AFS::FileInfo& fi) //throw FileError + { + //"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? + endsWith(fi.itemName, Zstr(".html"))) + { + ZstringView itemPhrase = beforeLast(fi.itemName, Zstr('.'), IfNotFoundReturn::none); + + if (endsWith(itemPhrase, STATUS_END_TOKEN)) + itemPhrase = beforeLast(itemPhrase, STATUS_BEGIN_TOKEN, IfNotFoundReturn::all); + + if (itemPhrase.size() >= TIME_STAMP_LENGTH && + itemPhrase.end()[-4] == Zstr('.') && + isdigit(itemPhrase.end()[-3]) && + isdigit(itemPhrase.end()[-2]) && + isdigit(itemPhrase.end()[-1])) + { + const TimeComp tc = parseTime(Zstr("%Y-%m-%d %H%M%S"), makeStringView(itemPhrase.end() - TIME_STAMP_LENGTH, 17)); //returns TimeComp() on error + if (const auto [localTime, timeValid] = localToTimeT(tc); + timeValid) + { + itemPhrase.remove_suffix(TIME_STAMP_LENGTH); + if (!itemPhrase.empty()) + { + assert(itemPhrase.size() >= 2 && endsWith(itemPhrase, Zstr(' '))); + itemPhrase = trimCpy(itemPhrase); + } + + logfiles.push_back({AFS::appendRelPath(logFolderPath, fi.itemName), localTime, utfTo(itemPhrase)}); + } + } + } + }, + nullptr /*onFolder*/, //traverse only one level deep + nullptr /*onSymlink*/); + + return logfiles; +} + + +void limitLogfileCount(const AbstractPath& logFolderPath, //throw FileError, X + int logfilesMaxAgeDays, //<= 0 := no limit + const std::set& logsToKeepPaths, + const std::function& notifyStatus /*throw X*/) +{ + if (logfilesMaxAgeDays > 0) + { + 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 logFiles = getLogFiles(logFolderPath); //throw FileError + + const time_t lastMidnightTime = [] + { + TimeComp tc = getLocalTime(); //returns TimeComp() on error + tc.second = 0; + tc.minute = 0; + tc.hour = 0; + return localToTimeT(tc).first; //0 on error => swallow => no versions trimmed by versionMaxAgeDays + }(); + const time_t cutOffTime = lastMidnightTime - static_cast(logfilesMaxAgeDays) * 24 * 3600; + + std::exception_ptr firstError; + + for (const LogFileInfo& lfi : logFiles) + if (lfi.timeStamp < cutOffTime && + !logsToKeepPaths.contains(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(statusPrefix + fmtPath(AFS::getDisplayPath(lfi.filePath))); //throw X + try + { + AFS::removeFilePlain(lfi.filePath); //throw FileError + } + catch (const FileError&) { if (!firstError) firstError = std::current_exception(); }; + } + + if (firstError) //late failure! + std::rethrow_exception(firstError); + } +} +} + + +//"Backup FreeFileSync 2013-09-15 015052.123.html" +//"Backup FreeFileSync 2013-09-15 015052.123 [Error].html" +//"Backup FreeFileSync + RealTimeSync 2013-09-15 015052.123 [Error].log" +Zstring fff::generateLogFileName(LogFileFormat logFormat, const ProcessSummary& summary) +{ + //const std::string colon = "\xcb\xb8"; //="modifier letter raised colon" => regular colon is forbidden in file names on Windows and macOS + //=> too many issues, most notably cmd.exe is not Unicode-aware: https://freefilesync.org/forum/viewtopic.php?t=1679 + + Zstring jobNamesFmt; + if (!summary.jobNames.empty()) + { + for (const std::wstring& jobName : summary.jobNames) + if (const Zstring jobNameZ = utfTo(jobName); + jobNamesFmt.size() + jobNameZ.size() > 200) + { + jobNamesFmt += Zstr("[...] + "); //avoid hitting file system name length limitations: "lpMaximumComponentLength is commonly 255 characters" + break; //https://freefilesync.org/forum/viewtopic.php?t=7113 + } + else + jobNamesFmt += jobNameZ + Zstr(" + "); + + jobNamesFmt.resize(jobNamesFmt.size() - 3); + } + + const TimeComp tc = getLocalTime(std::chrono::system_clock::to_time_t(summary.startTime)); + if (tc == TimeComp()) + throw FileError(L"Failed to determine current time: (time_t) " + numberTo(summary.startTime.time_since_epoch().count())); + + const auto timeMs = std::chrono::duration_cast(summary.startTime.time_since_epoch()).count() % 1000; + assert(std::chrono::duration_cast(summary.startTime.time_since_epoch()).count() == std::chrono::system_clock::to_time_t(summary.startTime)); + + const std::wstring failStatus = [&] + { + switch (summary.result) + { + case TaskResult::success: break; + case TaskResult::warning: return _("Warning"); + case TaskResult::error: return _("Error"); + case TaskResult::cancelled: return _("Stopped"); + } + return std::wstring(); + }(); + //------------------------------------------------------------------ + + Zstring logFileName = jobNamesFmt; + if (!logFileName.empty()) + logFileName += Zstr(' '); + + logFileName += formatTime(Zstr("%Y-%m-%d %H%M%S"), tc) + + Zstr('.') + printNumber(Zstr("%03d"), static_cast(timeMs)); //[ms] should yield a fairly unique name + static_assert(TIME_STAMP_LENGTH == 21); + + if (!failStatus.empty()) + logFileName += STATUS_BEGIN_TOKEN + utfTo(failStatus) + STATUS_END_TOKEN; + + logFileName += logFormat == LogFileFormat::html ? Zstr(".html") : Zstr(".log"); + + return logFileName; +} + + +void fff::saveLogFile(const AbstractPath& logFilePath, //throw FileError, X + const ProcessSummary& summary, + const ErrorLog& log, + int logfilesMaxAgeDays, + LogFileFormat logFormat, + const std::set& logsToKeepPaths, + const std::function& notifyStatus /*throw X*/) +{ + std::exception_ptr firstError; + try + { + saveNewLogFile(logFilePath, logFormat, summary, log, notifyStatus); //throw FileError, X + } + catch (const FileError&) { if (!firstError) firstError = std::current_exception(); }; + + try + { + const std::optional logFolderPath = AFS::getParentPath(logFilePath); + assert(logFolderPath); //else: logFilePath == device root; not possible with generateLogFilePath() + limitLogfileCount(*logFolderPath, logfilesMaxAgeDays, logsToKeepPaths, notifyStatus); //throw FileError, X + } + catch (const FileError&) { if (!firstError) firstError = std::current_exception(); }; + + if (firstError) //late failure! + std::rethrow_exception(firstError); +} + + + + +void fff::sendLogAsEmail(const std::string& email, //throw FileError, X + const ProcessSummary& summary, + const ErrorLog& log, + const AbstractPath& logFilePath, + const std::function& 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", L'"' + utfTo(email) + L'"'), e.toString()); } +} diff --git a/FreeFileSync/Source/log_file.h b/FreeFileSync/Source/log_file.h new file mode 100644 index 0000000..1598eac --- /dev/null +++ b/FreeFileSync/Source/log_file.h @@ -0,0 +1,40 @@ +// ***************************************************************************** +// * 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 GENERATE_LOGFILE_H_931726432167489732164 +#define GENERATE_LOGFILE_H_931726432167489732164 + +#include +#include "status_handler.h" +#include "afs/abstract.h" + + +namespace fff +{ +enum class LogFileFormat +{ + html, + text +}; + +Zstring generateLogFileName(LogFileFormat logFormat, const ProcessSummary& summary); + +void saveLogFile(const AbstractPath& logFilePath, //throw FileError, X + const ProcessSummary& summary, + const zen::ErrorLog& log, + int logfilesMaxAgeDays, + LogFileFormat logFormat, + const std::set& logsToKeepPaths, + const std::function& notifyStatus /*throw X*/); + +void sendLogAsEmail(const std::string& email, //throw FileError, X + const ProcessSummary& summary, + const zen::ErrorLog& log, + const AbstractPath& logFilePath, + const std::function& notifyStatus /*throw X*/); +} + +#endif //GENERATE_LOGFILE_H_931726432167489732164 diff --git a/FreeFileSync/Source/parse_lng.h b/FreeFileSync/Source/parse_lng.h new file mode 100644 index 0000000..c008855 --- /dev/null +++ b/FreeFileSync/Source/parse_lng.h @@ -0,0 +1,742 @@ +// ***************************************************************************** +// * 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 PARSE_LNG_H_46794693622675638 +#define PARSE_LNG_H_46794693622675638 + +#include +#include +#include +#include "parse_plural.h" + + +namespace lng +{ +//singular forms +using TranslationMap = std::unordered_map; //orig |-> translation + +//plural forms +using SingularPluralPair = std::pair; //1 house | %x houses +using PluralForms = std::vector; //1 dom | 2 domy | %x domów +using TranslationPluralMap = std::unordered_map; //(sing/plu) |-> pluralforms + +struct TransHeader +{ + std::string languageName; //display name: "English (UK)" + std::string translatorName; //"Zenju" + std::string locale; //ISO 639 language code + (optional) ISO 3166 country code, e.g. "de", "en_GB", or "en_US" + std::string flagFile; //"england.png" + int pluralCount = 0; //2 + std::string pluralDefinition; //"n == 1 ? 0 : 1" +}; + +struct ParsingError +{ + std::wstring msg; + size_t row = 0; //starting with 0 + size_t col = 0; // +}; +TransHeader parseHeader(const std::string& byteStream); //throw ParsingError +void parseLng(const std::string& byteStream, TransHeader& header, TranslationMap& out, TranslationPluralMap& pluralOut); //throw ParsingError + +class TranslationUnorderedList; //unordered list of unique translation items +std::string generateLng(const TranslationUnorderedList& in, const TransHeader& header, bool untranslatedToTop); + + + + + + + + + + + + + + + + + + + +//--------------------------- implementation --------------------------- +} + +template<> struct std::hash +{ + size_t operator()(const lng::SingularPluralPair& str) const + { + zen::FNV1aHash hash2; //shut up "GCC: shadow declaration" + for (const char c : str.first ) hash2.add(c); + for (const char c : str.second) hash2.add(c); + return hash2.get(); + } +}; + +namespace lng +{ +class TranslationUnorderedList //unordered list of unique translation items +{ +public: + TranslationUnorderedList(TranslationMap&& transOld, TranslationPluralMap&& transPluralOld) : + transOld_(std::move(transOld)), transPluralOld_(std::move(transPluralOld)) {} + + void addItem(const std::string& orig) + { + if (!transUnique_.insert(orig).second) return; + auto it = transOld_.find(orig); + if (it != transOld_.end() && !it->second.empty()) //preserve old translation from .lng file if existing + sequence_.push_back(std::make_shared(std::make_pair(orig, it->second))); + else + sequence_.push_back(std::make_shared(std::make_pair(orig, std::string()))); + } + + void addItem(const SingularPluralPair& orig) + { + if (!pluralUnique_.insert(orig).second) return; + auto it = transPluralOld_.find(orig); + if (it != transPluralOld_.end() && !it->second.empty()) //preserve old translation from .lng file if existing + sequence_.push_back(std::make_shared(std::make_pair(orig, it->second))); + else + sequence_.push_back(std::make_shared(std::make_pair(orig, PluralForms()))); + } + + bool untranslatedTextExists() const { return std::any_of(sequence_.begin(), sequence_.end(), [](const std::shared_ptr& item) { return !item->hasTranslation(); }); } + + template + void visitItems(Function onTrans, Function2 onPluralTrans) const //onTrans takes (const TranslationMap::value_type&), onPluralTrans takes (const TranslationPluralMap::value_type&) + { + for (const std::shared_ptr& item : sequence_) + if (auto regular = dynamic_cast(item.get())) + onTrans(regular->value); + else if (auto plural = dynamic_cast(item.get())) + onPluralTrans(plural->value); + else assert(false); + } + +private: + struct Item { virtual ~Item() {} virtual bool hasTranslation() const = 0; }; + + struct SingularItem : public Item + { + explicit SingularItem(const TranslationMap::value_type& val) : value(val) {} + bool hasTranslation() const override { return !value.second.empty(); } + TranslationMap::value_type value; + }; + + struct PluralItem : public Item + { + explicit PluralItem(const TranslationPluralMap::value_type& val) : value(val) {} + bool hasTranslation() const override { return !value.second.empty(); } + TranslationPluralMap::value_type value; + }; + + std::vector> sequence_; //ordered list of translation elements + + std::unordered_set transUnique_; //check uniqueness + std::unordered_set pluralUnique_; // + + const TranslationMap transOld_; //reuse existing translation + const TranslationPluralMap transPluralOld_; // +}; + + +enum class TokenType +{ + header, + source, + target, + empty, + text, + plural, + end, +}; + +struct Token +{ + Token(TokenType t) : type(t) {} + + TokenType type; + std::string text; +}; + + +class KnownTokens +{ +public: + KnownTokens() {} //clang wants it, clang gets it + + using TokenMap = std::unordered_map; + + const TokenMap& getList() const { return tokens_; } + + std::string text(TokenType t) const + { + auto it = tokens_.find(t); + if (it != tokens_.end()) + return it->second; + assert(false); + return std::string(); + } + +private: + const TokenMap tokens_ = + { + {TokenType::header, "
"}, + {TokenType::source, ""}, + {TokenType::target, ""}, + {TokenType::empty, ""}, + {TokenType::plural, ""}, + }; +}; + + +class Scanner +{ +public: + explicit Scanner(const std::string& byteStream) : stream_(byteStream), pos_(stream_.begin()) + { + if (zen::startsWith(stream_, zen::BYTE_ORDER_MARK_UTF8)) + pos_ += zen::strLength(zen::BYTE_ORDER_MARK_UTF8); + } + + Token getNextToken() + { + //skip whitespace + pos_ = std::find_if_not(pos_, stream_.end(), zen::isWhiteSpace); + + if (pos_ == stream_.end()) + return Token(TokenType::end); + + for (const auto& [tokenEnum, tokenString] : tokens_.getList()) + if (startsWith(tokenString)) + { + pos_ += tokenString.size(); + return Token(tokenEnum); + } + + //otherwise assume "text" + auto itBegin = pos_; + while (pos_ != stream_.end() && !startsWithKnownTag()) + pos_ = std::find(pos_ + 1, stream_.end(), '<'); + + std::string text(itBegin, pos_); + + normalize(text); //remove whitespace from end etc. + + if (text.empty() && pos_ == stream_.end()) + return Token(TokenType::end); + + Token out(TokenType::text); + out.text = std::move(text); + return out; + } + + size_t posRow() const //current row beginning with 0 + { + //count line endings + const size_t crSum = std::count(stream_.begin(), pos_, '\r'); //carriage returns + const size_t nlSum = std::count(stream_.begin(), pos_, '\n'); //new lines + assert(crSum == 0 || nlSum == 0 || crSum == nlSum); + return std::max(crSum, nlSum); //be compatible with Linux/Mac/Win + } + + size_t posCol() const //current col beginning with 0 + { + //seek beginning of line + for (auto it = pos_; it != stream_.begin(); ) + { + --it; + if (zen::isLineBreak(*it)) + return pos_ - it - 1; + } + return pos_ - stream_.begin(); + } + +private: + bool startsWithKnownTag() const + { + return std::any_of(tokens_.getList().begin(), tokens_.getList().end(), + [&](const KnownTokens::TokenMap::value_type& p) { return startsWith(p.second); }); + } + + bool startsWith(const std::string& prefix) const + { + return zen::startsWith(zen::makeStringView(pos_, stream_.end()), prefix); + } + + static void normalize(std::string& text) + { + zen::trim(text); //remove whitespace from both ends + + //Delimiter: + //---------- + //Linux: 0xA \n + //Mac: 0xD \r + //Win: 0xD 0xA \r\n <- language files are in Windows format + zen::replace(text, "\r\n", '\n'); // + zen::replace(text, '\r', '\n'); //ensure c-style line breaks + } + + const std::string stream_; + std::string::const_iterator pos_; + const KnownTokens tokens_; //no need for static non-POD! +}; + + +class LngParser +{ +public: + explicit LngParser(const std::string& byteStream) : scn_(byteStream), tk_(scn_.getNextToken()) {} + + void parse(TranslationMap& out, TranslationPluralMap& pluralOut, TransHeader& header) //throw ParsingError + { + parseHeader(header); //throw ParsingError + + try + { + plural::PluralFormInfo pi(header.pluralDefinition, header.pluralCount); //throw InvalidPluralForm + + while (token().type != TokenType::end) + parseRegular(out, pluralOut, pi); //throw ParsingError + } + catch (const plural::InvalidPluralForm&) + { + throw ParsingError({L"Invalid plural form definition", scn_.posRow(), scn_.posCol()}); + } + } + + void parseHeader(TransHeader& header) //throw ParsingError + { + using namespace zen; + + consumeToken(TokenType::header); //throw ParsingError + + const std::string headerRaw = token().text; + consumeToken(TokenType::text); //throw ParsingError + + std::unordered_map items; + + split2(headerRaw, [](const char c) { return isLineBreak(c); }, + [&items](const std::string_view block) + { + const std::string_view name = trimCpy(beforeFirst(block, ':', IfNotFoundReturn::none)); + if (!name.empty()) + items.emplace(name, trimCpy(afterFirst(block, ':', IfNotFoundReturn::none))); + }); + + auto getValue = [&](const std::string_view name) + { + auto it = items.find(name); + if (it == items.end()) + throw ParsingError({replaceCpy(L"Cannot find header item \"%x:\"", L"%x", utfTo(name)), scn_.posRow(), scn_.posCol()}); + return it->second; + }; + + header.languageName = getValue("language"); //throw ParsingError + header.locale = getValue("locale"); //throw ParsingError + header.flagFile = getValue("image"); //throw ParsingError + header.pluralCount = stringTo(getValue("plural_count")); //throw ParsingError + header.pluralDefinition = getValue("plural_definition"); //throw ParsingError + header.translatorName = getValue("translator"); //throw ParsingError + } + +private: + void parseRegular(TranslationMap& out, TranslationPluralMap& pluralOut, const plural::PluralFormInfo& pluralInfo) //throw ParsingError + { + consumeToken(TokenType::source); //throw ParsingError + + if (token().type == TokenType::plural) + return parsePlural(pluralOut, pluralInfo); //throw ParsingError + + std::string original = token().text; + consumeToken(TokenType::text); //throw ParsingError + + consumeToken(TokenType::target); //throw ParsingError + std::string translation; + if (token().type == TokenType::text) + { + translation = token().text; + nextToken(); + } + else + consumeToken(TokenType::empty); //throw ParsingError + + validateTranslation(original, translation); //throw ParsingError + + out.emplace(std::move(original), std::move(translation)); + } + + void parsePlural(TranslationPluralMap& pluralOut, const plural::PluralFormInfo& pluralInfo) //throw ParsingError + { + //TokenType::source already consumed + + consumeToken(TokenType::plural); //throw ParsingError + std::string engSingular = token().text; + consumeToken(TokenType::text); //throw ParsingError + + consumeToken(TokenType::plural); //throw ParsingError + std::string engPlural = token().text; + consumeToken(TokenType::text); //throw ParsingError + + const SingularPluralPair original(engSingular, engPlural); + + consumeToken(TokenType::target); //throw ParsingError + + PluralForms pluralList; + while (token().type == TokenType::plural) + { + nextToken(); + std::string pluralForm = token().text; + consumeToken(TokenType::text); //throw ParsingError + + pluralList.push_back(pluralForm); + } + + if (pluralList.empty()) + consumeToken(TokenType::empty); //throw ParsingError + + validateTranslation(original, pluralList, pluralInfo); + + pluralOut.emplace(original, std::move(pluralList)); + } + + void validateTranslation(const std::string& original, const std::string& translation) //throw ParsingError + { + using namespace zen; + + if (original.empty()) + throw ParsingError({L"Translation source text is empty", scn_.posRow(), scn_.posCol()}); + + if (!isValidUtf(original)) + throw ParsingError({L"Translation source text contains UTF-8 encoding error", scn_.posRow(), scn_.posCol()}); + if (!isValidUtf(translation)) + throw ParsingError({L"Translation text contains UTF-8 encoding error", scn_.posRow(), scn_.posCol()}); + + if (!translation.empty()) + { + //if original contains placeholder, so must translation! + auto checkPlaceholder = [&](const std::string& placeholder) + { + if (contains(original, placeholder) && + !contains(translation, placeholder)) + throw ParsingError({replaceCpy(L"Placeholder %x missing in translation", L"%x", utfTo(placeholder)), scn_.posRow(), scn_.posCol()}); + }; + checkPlaceholder("%x"); + checkPlaceholder("%y"); + checkPlaceholder("%z"); + + //if source is a one-liner, so should be the translation + if (!contains(original, '\n') && contains(translation, '\n')) + throw ParsingError({L"Source text is a one-liner, but translation consists of multiple lines", scn_.posRow(), scn_.posCol()}); + + //if source contains ampersand to mark menu accellerator key, so must translation + const size_t ampCount = ampersandTokenCount(original); + if (ampCount > 1 || ampCount != ampersandTokenCount(translation)) + throw ParsingError({L"Source and translation both need exactly one & character to mark a menu item access key or none at all", scn_.posRow(), scn_.posCol()}); + + //ampersand at the end makes buggy wxWidgets crash miserably + if (endsWithSingleAmp(original) || endsWithSingleAmp(translation)) + throw ParsingError({L"The & character to mark a menu item access key must not occur at the end of a string", scn_.posRow(), scn_.posCol()}); + + //if source ends with colon, so must translation + if (endsWithColon(original) && !endsWithColon(translation)) + throw ParsingError({L"Source text ends with a colon character \":\", but translation does not", scn_.posRow(), scn_.posCol()}); + + //if source ends with period, so must translation + if (endsWithSingleDot(original) && !endsWithSingleDot(translation)) + throw ParsingError({L"Source text ends with a punctuation mark character \".\", but translation does not", scn_.posRow(), scn_.posCol()}); + + //if source ends with ellipsis, so must translation + if (endsWithEllipsis(original) && !endsWithEllipsis(translation)) + throw ParsingError({L"Source text ends with an ellipsis \"...\", but translation does not", scn_.posRow(), scn_.posCol()}); + + //check for not-to-be-translated texts + for (const char* fixedStr : {"FreeFileSync", "RealTimeSync", "ffs_gui", "ffs_batch", "ffs_real", "ffs_tmp", "GlobalSettings.xml"}) + if (contains(original, fixedStr) && !contains(translation, fixedStr)) + throw ParsingError({replaceCpy(L"Misspelled \"%x\" in translation", L"%x", utfTo(fixedStr)), scn_.posRow(), scn_.posCol()}); + + //some languages (French!) put a space before punctuation mark => must be a no-brake space! + for (const char punctChar : std::string_view(".!?:;$#")) + if (contains(original, std::string(" ") + punctChar) || + contains(translation, std::string(" ") + punctChar)) + throw ParsingError({replaceCpy(L"Text contains a space before the \"%x\" character. Are line-breaks really allowed here?" + L" Maybe this should be a \"non-breaking space\" (Windows: Alt 0160 UTF8: 0xC2 0xA0)?", + L"%x", utfTo(punctChar)), scn_.posRow(), scn_.posCol()}); + } + } + + void validateTranslation(const SingularPluralPair& original, const PluralForms& translation, const plural::PluralFormInfo& pluralInfo) //throw ParsingError + { + using namespace zen; + + if (original.first.empty() || original.second.empty()) + throw ParsingError({L"Translation source text is empty", scn_.posRow(), scn_.posCol()}); + + const std::vector allTexts = [&] + { + std::vector at{original.first, original.second}; + at.insert(at.end(), translation.begin(), translation.end()); + return at; + }(); + + for (const std::string& str : allTexts) + if (!isValidUtf(str)) + throw ParsingError({L"Text contains UTF-8 encoding error", scn_.posRow(), scn_.posCol()}); + + //check the primary placeholder is existing at least for the second english text + if (!contains(original.second, "%x")) + throw ParsingError({L"Plural form source text does not contain %x placeholder", scn_.posRow(), scn_.posCol()}); + + if (!translation.empty()) + { + //check for invalid number of plural forms + if (pluralInfo.getCount() != translation.size()) + throw ParsingError({replaceCpy(replaceCpy(L"Invalid number of plural forms; actual: %x, expected: %y", + L"%x", formatNumber(translation.size())), + L"%y", formatNumber(pluralInfo.getCount())), scn_.posRow(), scn_.posCol()}); + + //check for duplicate plural form translations (catch copy & paste errors for single-number form translations) + for (auto it = translation.begin(); it != translation.end(); ++it) + if (!contains(*it, "%x")) + { + auto it2 = std::find(it + 1, translation.end(), *it); + if (it2 != translation.end()) + throw ParsingError({replaceCpy(L"Duplicate plural form translation at index position %x", + L"%x", formatNumber(it2 - translation.begin())), scn_.posRow(), scn_.posCol()}); + } + + for (size_t pos = 0; pos < translation.size(); ++pos) + if (pluralInfo.isSingleNumberForm(pos)) + { + //translation needs to use decimal number if english source does so (e.g. frequently changing text like statistics) + if (contains(original.first, "%x") || + contains(original.first, "1")) + { + const int firstNumber = pluralInfo.getFirstNumber(pos); + if (!contains(translation[pos], "%x") && + !contains(translation[pos], numberTo(firstNumber))) + throw ParsingError({replaceCpy(replaceCpy(L"Plural form translation at index position %y needs to use the decimal number %z or the %x placeholder", + L"%y", formatNumber(pos)), L"%z", formatNumber(firstNumber)), scn_.posRow(), scn_.posCol()}); + } + } + else + { + //ensure the placeholder is used when needed + if (!contains(translation[pos], "%x")) + throw ParsingError({replaceCpy(L"Plural form at index position %y is missing the %x placeholder", L"%y", formatNumber(pos)), scn_.posRow(), scn_.posCol()}); + } + + auto checkSecondaryPlaceholder = [&](const std::string& placeholder) + { + //make sure secondary placeholder is used for both source texts (or none) and all plural forms + if (contains(original.first, placeholder) || + contains(original.second, placeholder)) + for (const std::string& str : allTexts) + if (!contains(str, placeholder)) + throw ParsingError({replaceCpy(L"Placeholder %x missing in text", L"%x", utfTo(placeholder)), scn_.posRow(), scn_.posCol()}); + }; + checkSecondaryPlaceholder("%y"); + checkSecondaryPlaceholder("%z"); + + //if source is a one-liner, so should be the translation + if (!contains(original.first, '\n') && !contains(original.second, '\n') && + /**/std::any_of(translation.begin(), translation.end(), [](const std::string& pform) { return contains(pform, '\n'); })) + /**/throw ParsingError({L"Source text is a one-liner, but at least one plural form translation consists of multiple lines", scn_.posRow(), scn_.posCol()}); + + //if source contains ampersand to mark menu accellerator key, so must translation + const size_t ampCount = ampersandTokenCount(original.first); + for (const std::string& str : allTexts) + if (ampCount > 1 || ampersandTokenCount(str) != ampCount) + throw ParsingError({L"Source and translation both need exactly one & character to mark a menu item access key or none at all", scn_.posRow(), scn_.posCol()}); + + //ampersand at the end makes buggy wxWidgets crash miserably + for (const std::string& str : allTexts) + if (endsWithSingleAmp(str)) + throw ParsingError({L"The & character to mark a menu item access key must not occur at the end of a string", scn_.posRow(), scn_.posCol()}); + + //if source ends with colon, so must translation (note: character seems to be universally used, even for asian and arabic languages) + if (endsWith(original.first, ':') || endsWith(original.second, ':')) + for (const std::string& str : allTexts) + if (!endsWithColon(str)) + throw ParsingError({L"Source text ends with a colon character \":\", but translation does not", scn_.posRow(), scn_.posCol()}); + + //if source ends with a period, so must translation (note: character seems to be universally used, even for asian and arabic languages) + if (endsWithSingleDot(original.first) || endsWithSingleDot(original.second)) + for (const std::string& str : allTexts) + if (!endsWithSingleDot(str)) + throw ParsingError({L"Source text ends with a punctuation mark character \".\", but translation does not", scn_.posRow(), scn_.posCol()}); + + //if source ends with an ellipsis, so must translation (note: character seems to be universally used, even for asian and arabic languages) + if (endsWithEllipsis(original.first) || endsWithEllipsis(original.second)) + for (const std::string& str : allTexts) + if (!endsWithEllipsis(str)) + throw ParsingError({L"Source text ends with an ellipsis \"...\", but translation does not", scn_.posRow(), scn_.posCol()}); + + //check for not-to-be-translated texts + for (const char* fixedStr : {"FreeFileSync", "RealTimeSync", "ffs_gui", "ffs_batch", "ffs_tmp", "GlobalSettings.xml"}) + if (contains(original.first, fixedStr) || contains(original.second, fixedStr)) + for (const std::string& str : allTexts) + if (!contains(str, fixedStr)) + throw ParsingError({replaceCpy(L"Misspelled \"%x\" in translation", L"%x", utfTo(fixedStr)), scn_.posRow(), scn_.posCol()}); + + //some languages (French!) put a space before punctuation mark => must be a no-brake space! + for (const char punctChar : std::string(".!?:;$#")) + for (const std::string& str : allTexts) + if (contains(str, std::string(" ") + punctChar)) + throw ParsingError({replaceCpy(L"Text contains a space before the \"%x\" character. Are line-breaks really allowed here?" + L" Maybe this should be a \"non-breaking space\" (Windows: Alt 0160 UTF8: 0xC2 0xA0)?", + L"%x", utfTo(punctChar)), scn_.posRow(), scn_.posCol()}); + } + } + + static size_t ampersandTokenCount(const std::string& str) + { + using namespace zen; + const std::string tmp = replaceCpy(str, "&&", ""); //make sure to not catch && which windows resolves as just one & for display! + return std::count(tmp.begin(), tmp.end(), '&'); + } + + static bool endsWithSingleAmp(const std::string& s) + { + using namespace zen; + return endsWith(s, "&") && !endsWith(s, "&&"); + } + + static bool endsWithEllipsis(const std::string& s) + { + using namespace zen; + return endsWith(s, "...") || + endsWith(s, "\xe2\x80\xa6"); //narrow ellipsis (spanish?) + } + + static bool endsWithColon(const std::string& s) + { + using namespace zen; + return endsWith(s, ':') || + endsWith(s, "\xef\xbc\x9a"); //chinese colon + } + + static bool endsWithSingleDot(const std::string& s) + { + using namespace zen; + return (endsWith(s, ".") || + endsWith(s, "\xe0\xa5\xa4") || //hindi period + endsWith(s, "\xe3\x80\x82")) //chinese period + && + (!endsWith(s, "..") && + !endsWith(s, "\xe0\xa5\xa4" "\xe0\xa5\xa4") && //hindi period + !endsWith(s, "\xe3\x80\x82" "\xe3\x80\x82")); //chinese period + } + + + const Token& token() const { return tk_; } + + void nextToken() { tk_ = scn_.getNextToken(); } + + void expectToken(TokenType t) //throw ParsingError + { + if (token().type != t) + throw ParsingError({L"Unexpected token", scn_.posRow(), scn_.posCol()}); + } + + void consumeToken(TokenType t) //throw ParsingError + { + expectToken(t); //throw ParsingError + nextToken(); + } + + Scanner scn_; + Token tk_; +}; + + +inline +void parseLng(const std::string& byteStream, TransHeader& header, TranslationMap& out, TranslationPluralMap& pluralOut) //throw ParsingError +{ + out.clear(); + pluralOut.clear(); + + LngParser(byteStream).parse(out, pluralOut, header); //throw ParsingError +} + + +inline +TransHeader parseHeader(const std::string& byteStream) //throw ParsingError +{ + TransHeader header; + LngParser(byteStream).parseHeader(header); //throw ParsingError + return header; +} + + +inline +std::string generateLng(const TranslationUnorderedList& in, const TransHeader& header, bool untranslatedToTop) +{ + using namespace zen; + + const KnownTokens tokens; //no need for static non-POD! + + std::string headerLines; + headerLines += tokens.text(TokenType::header) + '\n'; + + headerLines += "\t" "language: " + header.languageName + '\n'; + headerLines += "\t" "locale: " + header.locale + '\n'; + headerLines += "\t" "image: " + header.flagFile + '\n'; + headerLines += "\t" "plural_count: " + numberTo(header.pluralCount) + '\n'; + headerLines += "\t" "plural_definition: " + header.pluralDefinition + '\n'; + headerLines += "\t" "translator: " + header.translatorName; + + + std::string topLines; //untranslated items first? + std::string mainLines; + + in.visitItems([&](const TranslationMap::value_type& trans) + { + std::string& out = untranslatedToTop && trans.second.empty() ? topLines : mainLines; + + std::string original = trans.first; + std::string translation = trans.second; + + out += + "\n\n" + tokens.text(TokenType::source) + ' ' + original + '\n'; + + if (contains(original, '\n')) //multiple lines + out += + '\n'; + + out += tokens.text(TokenType::target) + ' ' + translation; + + if (translation.empty()) //help translators search for untranslated items + out += tokens.text(TokenType::empty); + }, + [&](const TranslationPluralMap::value_type& transPlural) + { + std::string& out = untranslatedToTop && transPlural.second.empty() ? topLines : mainLines; + + std::string engSingular = transPlural.first.first; + std::string engPlural = transPlural.first.second; + const PluralForms& forms = transPlural.second; + + out += "\n\n" + tokens.text(TokenType::source) + '\n'; + out += '\t' + tokens.text(TokenType::plural) + ' ' + engSingular + '\n'; + out += '\t' + tokens.text(TokenType::plural) + ' ' + engPlural + '\n'; + + out += tokens.text(TokenType::target); + + for (std::string plForm : forms) + out += + "\n\t" + tokens.text(TokenType::plural) + ' ' + plForm; + + if (forms.empty()) //help translators search for untranslated items + out += ' ' + tokens.text(TokenType::empty); + }); + + std::string output = headerLines + topLines + mainLines; + assert(!contains(output, "\r\n") && !contains(output, "\r")); + return replaceCpy(output, '\n', "\r\n"); //back to Windows line endings +} +} + +#endif //PARSE_LNG_H_46794693622675638 diff --git a/FreeFileSync/Source/parse_plural.h b/FreeFileSync/Source/parse_plural.h new file mode 100644 index 0000000..665fc28 --- /dev/null +++ b/FreeFileSync/Source/parse_plural.h @@ -0,0 +1,475 @@ +// ***************************************************************************** +// * 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 PARSE_PLURAL_H_180465845670839576 +#define PARSE_PLURAL_H_180465845670839576 + +#include + + +namespace plural +{ +//expression interface +struct Expression { virtual ~Expression() {} }; + +template +struct Expr : public Expression +{ + virtual T eval() const = 0; +}; + + +class ParsingError {}; + +class PluralForm +{ +public: + explicit PluralForm(const std::string& stream); //throw ParsingError + size_t getForm(int64_t n) const { n_ = std::abs(n) ; return static_cast(expr_->eval()); } + +private: + std::shared_ptr> expr_; + mutable int64_t n_ = 0; +}; + + +//validate plural form +class InvalidPluralForm {}; + +class PluralFormInfo +{ +public: + PluralFormInfo(const std::string& definition, int pluralCount); //throw InvalidPluralForm + + size_t getCount() const { return forms_.size(); } + bool isSingleNumberForm(size_t index) const { return index < forms_.size() ? forms_[index].count == 1 : false; } + int getFirstNumber (size_t index) const { return index < forms_.size() ? forms_[index].firstNumber : -1; } + +private: + struct FormInfo + { + int count = 0; + int firstNumber = 0; //which maps to the plural form index position + }; + std::vector forms_; +}; + + + + + +//--------------------------- implementation --------------------------- +/* https://www.gnu.org/software/hello/manual/gettext/Plural-forms.html + https://translate.sourceforge.net/wiki/l10n/pluralforms + + Grammar for Plural forms parser + ------------------------------- + expression: + conditional-expression + + conditional-expression: + logical-or-expression + logical-or-expression ? expression : expression + + logical-or-expression: + logical-and-expression + logical-or-expression || logical-and-expression + + logical-and-expression: + equality-expression + logical-and-expression && equality-expression + + equality-expression: + relational-expression + relational-expression == relational-expression + relational-expression != relational-expression + + relational-expression: + multiplicative-expression + multiplicative-expression > multiplicative-expression + multiplicative-expression < multiplicative-expression + multiplicative-expression >= multiplicative-expression + multiplicative-expression <= multiplicative-expression + + multiplicative-expression: + pm-expression + multiplicative-expression % pm-expression + + pm-expression: + variable-number-n-expression + constant-number-expression + ( expression ) + + + .po format,e.g.: (n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2) */ + +namespace impl +{ +template +struct BinaryExp : public Expr +{ + using ExpLhs = std::shared_ptr>; + using ExpRhs = std::shared_ptr>; + + BinaryExp(const ExpLhs& lhs, const ExpRhs& rhs) : lhs_(lhs), rhs_(rhs) { assert(lhs && rhs); } + ResultType eval() const override { return BinaryOp()(lhs_->eval(), rhs_->eval()); } +private: + ExpLhs lhs_; + ExpRhs rhs_; +}; + + +template inline +std::shared_ptr makeBiExp(const std::shared_ptr& lhs, const std::shared_ptr& rhs) //throw ParsingError +{ + auto exLeft = std::dynamic_pointer_cast>(lhs); + auto exRight = std::dynamic_pointer_cast>(rhs); + if (!exLeft || !exRight) + throw ParsingError(); + + using ResultType = decltype(BinaryOp()(std::declval(), std::declval())); + return std::make_shared>(exLeft, exRight); +} + + +template +struct ConditionalExp : public Expr +{ + ConditionalExp(const std::shared_ptr>& ifExp, + const std::shared_ptr>& thenExp, + const std::shared_ptr>& elseExp) : ifExp_(ifExp), thenExp_(thenExp), elseExp_(elseExp) { assert(ifExp && thenExp && elseExp); } + + T eval() const override { return ifExp_->eval() ? thenExp_->eval() : elseExp_->eval(); } +private: + std::shared_ptr> ifExp_; + std::shared_ptr> thenExp_; + std::shared_ptr> elseExp_; +}; + + +struct ConstNumberExp : public Expr +{ + explicit ConstNumberExp(int64_t n) : n_(n) {} + int64_t eval() const override { return n_; } +private: + int64_t n_; +}; + + +struct VariableNumberNExp : public Expr +{ + explicit VariableNumberNExp(int64_t& n) : n_(n) {} + int64_t eval() const override { return n_; } +private: + int64_t& n_; +}; + +//------------------------------------------------------------------------------- + +enum class TokenType +{ + ternaryQuest, + ternaryColon, + logicOr, + logicAnd, + equal, + notEqual, + less, + lessEqual, + greater, + greaterEqual, + modulus, + variableN, + constNumber, + bracketLeft, + bracketRight, + end, +}; + +struct Token +{ + Token(TokenType t) : type(t) {} + Token(int64_t num) : number(num) {} + + TokenType type = TokenType::constNumber; + int64_t number = 0; //if type == TokenType::constNumber +}; + +class Scanner +{ +public: + explicit Scanner(const std::string& stream) : stream_(stream), pos_(stream_.begin()) {} + + Token getNextToken() //throw ParsingError + { + //skip whitespace + pos_ = std::find_if_not(pos_, stream_.end(), zen::isWhiteSpace); + + if (pos_ == stream_.end()) + return TokenType::end; + + for (const auto& [tokenString, tokenEnum] : tokens_) + if (startsWith(tokenString)) + { + pos_ += tokenString.size(); + return Token(tokenEnum); + } + + auto digitEnd = std::find_if_not(pos_, stream_.end(), zen::isDigit); + if (pos_ == digitEnd) + throw ParsingError(); //unknown token + + auto number = zen::stringTo(std::string(pos_, digitEnd)); + pos_ = digitEnd; + return number; + } + +private: + bool startsWith(const std::string& prefix) const + { + return zen::startsWith(zen::makeStringView(pos_, stream_.end()), prefix); + } + + using TokenList = std::vector>; + const TokenList tokens_ + { + {"?", TokenType::ternaryQuest}, + {":", TokenType::ternaryColon}, + {"||", TokenType::logicOr }, + {"&&", TokenType::logicAnd }, + {"==", TokenType::equal }, + {"!=", TokenType::notEqual }, + {"<=", TokenType::lessEqual }, + {"<", TokenType::less }, + {">=", TokenType::greaterEqual}, + {">", TokenType::greater }, + {"%", TokenType::modulus }, + {"n", TokenType::variableN }, + {"N", TokenType::variableN }, + {"(", TokenType::bracketLeft }, + {")", TokenType::bracketRight}, + }; + + const std::string stream_; + std::string::const_iterator pos_; +}; + +//------------------------------------------------------------------------------- + +class Parser +{ +public: + Parser(const std::string& stream, int64_t& n) : + scn_(stream), + tk_(scn_.getNextToken()), //throw ParsingError + n_(n) {} + + std::shared_ptr> parse() //throw ParsingError; return value always bound! + { + auto e = std::dynamic_pointer_cast>(parseExpression()); //throw ParsingError + if (!e) + throw ParsingError(); + expectToken(TokenType::end); //throw ParsingError + return e; + } + +private: + std::shared_ptr parseExpression() { return parseConditional(); }//throw ParsingError + + std::shared_ptr parseConditional() //throw ParsingError + { + std::shared_ptr e = parseLogicalOr(); + + if (token().type == TokenType::ternaryQuest) + { + nextToken(); //throw ParsingError + + auto ifExp = std::dynamic_pointer_cast>(e); + auto thenExp = std::dynamic_pointer_cast>(parseExpression()); //associativity: <- + + consumeToken(TokenType::ternaryColon); //throw ParsingError + + auto elseExp = std::dynamic_pointer_cast>(parseExpression()); // + if (!ifExp || !thenExp || !elseExp) + throw ParsingError(); + return std::make_shared>(ifExp, thenExp, elseExp); + } + return e; + } + + std::shared_ptr parseLogicalOr() + { + std::shared_ptr e = parseLogicalAnd(); + while (token().type == TokenType::logicOr) //associativity: -> + { + nextToken(); //throw ParsingError + + std::shared_ptr rhs = parseLogicalAnd(); + e = makeBiExp, bool>(e, rhs); //throw ParsingError + } + return e; + } + + std::shared_ptr parseLogicalAnd() + { + std::shared_ptr e = parseEquality(); + while (token().type == TokenType::logicAnd) //associativity: -> + { + nextToken(); //throw ParsingError + std::shared_ptr rhs = parseEquality(); + + e = makeBiExp, bool>(e, rhs); //throw ParsingError + } + return e; + } + + std::shared_ptr parseEquality() + { + std::shared_ptr e = parseRelational(); + + TokenType t = token().type; + if (t == TokenType::equal || //associativity: n/a + t == TokenType::notEqual) + { + nextToken(); //throw ParsingError + std::shared_ptr rhs = parseRelational(); + + if (t == TokenType::equal) return makeBiExp, int64_t>(e, rhs); //throw ParsingError + if (t == TokenType::notEqual) return makeBiExp, int64_t>(e, rhs); // + } + return e; + } + + std::shared_ptr parseRelational() + { + std::shared_ptr e = parseMultiplicative(); + + TokenType t = token().type; + if (t == TokenType::less || //associativity: n/a + t == TokenType::lessEqual || + t == TokenType::greater || + t == TokenType::greaterEqual) + { + nextToken(); //throw ParsingError + std::shared_ptr rhs = parseMultiplicative(); + + if (t == TokenType::less) return makeBiExp, int64_t>(e, rhs); // + if (t == TokenType::lessEqual) return makeBiExp, int64_t>(e, rhs); //throw ParsingError + if (t == TokenType::greater) return makeBiExp, int64_t>(e, rhs); // + if (t == TokenType::greaterEqual) return makeBiExp, int64_t>(e, rhs); // + } + return e; + } + + std::shared_ptr parseMultiplicative() + { + std::shared_ptr e = parsePrimary(); + + while (token().type == TokenType::modulus) //associativity: -> + { + nextToken(); //throw ParsingError + std::shared_ptr rhs = parsePrimary(); + + //"compile-time" check: n % 0 + if (auto literal = std::dynamic_pointer_cast(rhs)) + if (literal->eval() == 0) + throw ParsingError(); + + e = makeBiExp, int64_t>(e, rhs); //throw ParsingError + } + return e; + } + + std::shared_ptr parsePrimary() + { + if (token().type == TokenType::variableN) + { + nextToken(); //throw ParsingError + return std::make_shared(n_); + } + else if (token().type == TokenType::constNumber) + { + const int64_t number = token().number; + nextToken(); //throw ParsingError + return std::make_shared(number); + } + else if (token().type == TokenType::bracketLeft) + { + nextToken(); //throw ParsingError + std::shared_ptr e = parseExpression(); + + expectToken(TokenType::bracketRight); //throw ParsingError + nextToken(); // + return e; + } + else + throw ParsingError(); + } + + const Token& token() const { return tk_; } + + void nextToken() { tk_ = scn_.getNextToken(); } //throw ParsingError + + void expectToken(TokenType t) //throw ParsingError + { + if (token().type != t) + throw ParsingError(); + } + + void consumeToken(TokenType t) //throw ParsingError + { + expectToken(t); //throw ParsingError + nextToken(); + } + + Scanner scn_; + Token tk_; + int64_t& n_; +}; +} + + +inline +PluralFormInfo::PluralFormInfo(const std::string& definition, int pluralCount) //throw InvalidPluralForm +{ + if (pluralCount < 1) + throw InvalidPluralForm(); + + forms_.resize(pluralCount); + try + { + PluralForm pf(definition); //throw ParsingError + //PERF_START + + //perf: 80ns per iteration max (for Arabic) + //=> 1000 iterations should be fast enough and still detect all "single number forms" + for (int j = 0; j < 1000; ++j) + if (const size_t formNo = pf.getForm(j); + formNo < forms_.size()) + { + if (forms_[formNo].count == 0) + forms_[formNo].firstNumber = j; + ++forms_[formNo].count; + } + else + throw InvalidPluralForm(); + } + catch (const plural::ParsingError&) + { + throw InvalidPluralForm(); + } + + //ensure each form is used at least once: + if (!std::all_of(forms_.begin(), forms_.end(), [](const FormInfo& fi) { return fi.count >= 1; })) + throw InvalidPluralForm(); +} + + +inline +PluralForm::PluralForm(const std::string& stream) : expr_(impl::Parser(stream, n_).parse()) {} //throw ParsingError +} + +#endif //PARSE_PLURAL_H_180465845670839576 diff --git a/FreeFileSync/Source/return_codes.h b/FreeFileSync/Source/return_codes.h new file mode 100644 index 0000000..60b6c2e --- /dev/null +++ b/FreeFileSync/Source/return_codes.h @@ -0,0 +1,57 @@ +// ***************************************************************************** +// * 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 RETURN_CODES_H_81307482137054156 +#define RETURN_CODES_H_81307482137054156 + +#include + + +namespace fff +{ +enum class FfsExitCode //as returned on process exit +{ + success = 0, + warning, + error, + cancelled, + exception, +}; + + +inline +void raiseExitCode(FfsExitCode& rc, FfsExitCode rcProposed) +{ + if (rc < rcProposed) + rc = rcProposed; +} + + +enum class TaskResult +{ + success, + warning, + error, + cancelled, +}; + + +inline +std::wstring getSyncResultLabel(TaskResult syncResult) +{ + switch (syncResult) + { + case TaskResult::success: return _("Completed successfully"); + case TaskResult::warning: return _("Completed with warnings"); + case TaskResult::error: return _("Completed with errors"); + case TaskResult::cancelled: return _("Stopped"); + } + assert(false); + return std::wstring(); +} +} + +#endif //RETURN_CODES_H_81307482137054156 diff --git a/FreeFileSync/Source/status_handler.cpp b/FreeFileSync/Source/status_handler.cpp new file mode 100644 index 0000000..69e6964 --- /dev/null +++ b/FreeFileSync/Source/status_handler.cpp @@ -0,0 +1,47 @@ +// ***************************************************************************** +// * 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 "status_handler.h" +#include +#include + +using namespace zen; + + +namespace +{ +std::chrono::steady_clock::time_point lastExec; +} + + +bool fff::uiUpdateDue(bool force) +{ + const auto now = std::chrono::steady_clock::now(); + + if (force || now >= lastExec + UI_UPDATE_INTERVAL) + { + lastExec = now; + return true; + } + return false; +} + + +void fff::delayAndCountDown(std::chrono::nanoseconds delay, const std::function& notifyStatus) +{ + assert(notifyStatus); + if (notifyStatus) + while (delay > std::chrono::nanoseconds(0)) + { + const auto timeRemMs = std::chrono::duration_cast(delay).count(); + notifyStatus(_P("1 sec", "%x sec", numeric::intDivCeil(timeRemMs, 1000))); + + std::this_thread::sleep_for(UI_UPDATE_INTERVAL / 2); + delay -= UI_UPDATE_INTERVAL / 2; //support "Pause" => don't count time spent in notifyStatus()! + } + else + std::this_thread::sleep_for(delay /*may be negative*/); +} diff --git a/FreeFileSync/Source/status_handler.h b/FreeFileSync/Source/status_handler.h new file mode 100644 index 0000000..d009a76 --- /dev/null +++ b/FreeFileSync/Source/status_handler.h @@ -0,0 +1,176 @@ +// ***************************************************************************** +// * 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 STATUS_HANDLER_H_81704805908341534 +#define STATUS_HANDLER_H_81704805908341534 + +#include +#include "base/process_callback.h" +#include "return_codes.h" + +namespace fff +{ +bool uiUpdateDue(bool force = false); //test if a specific amount of time is over + +/* Updating GUI is fast! time per call to ProcessCallback::forceUiRefresh() + - Comparison 0.025 ms + - Synchronization 0.74 ms (despite complex graph control!) */ + +//Exception class used to abort the "compare" and "sync" process +class CancelProcess {}; + + +enum class CancelReason +{ + user, + firstError, +}; + +//GUI may want to abort process +struct CancelCallback +{ + virtual ~CancelCallback() {} + virtual void userRequestCancel() = 0; +}; + + +struct ProgressStats +{ + int items = 0; + int64_t bytes = 0; + + bool operator==(const ProgressStats&) const = default; +}; + + +//common statistics "everybody" needs +struct Statistics +{ + virtual ~Statistics() {} + + virtual ProcessPhase currentPhase() const = 0; + + virtual ProgressStats getCurrentStats() const = 0; + virtual ProgressStats getTotalStats () const = 0; + + struct ErrorStats + { + int errorCount; + int warningCount; + }; + virtual ErrorStats getErrorStats() const = 0; + + virtual std::optional taskCancelled() const = 0; + virtual const std::wstring& currentStatusText() const = 0; +}; + + +struct ProcessSummary +{ + std::chrono::system_clock::time_point startTime; + TaskResult result = TaskResult::cancelled; + std::vector jobNames; //may be empty + ProgressStats statsProcessed; + ProgressStats statsTotal; + std::chrono::milliseconds totalTime{}; +}; + + +//partial callback implementation with common functionality for "batch", "GUI/Compare" and "GUI/Sync" +class StatusHandler : public ProcessCallback, public CancelCallback, public Statistics +{ +public: + //StatusHandler() {} + + //implement parts of ProcessCallback + void initNewPhase(int itemsTotal, int64_t bytesTotal, ProcessPhase phase) override //(throw X) + { + assert((itemsTotal < 0) == (bytesTotal < 0)); + currentPhase_ = phase; + statsCurrent_ = {}; + statsTotal_ = {itemsTotal, bytesTotal}; + } + + void updateDataProcessed(int itemsDelta, int64_t bytesDelta) override { updateData(statsCurrent_, itemsDelta, bytesDelta); } //note: these methods MUST NOT throw in order + void updateDataTotal (int itemsDelta, int64_t bytesDelta) override { updateData(statsTotal_, itemsDelta, bytesDelta); } //to allow usage within destructors! + + void requestUiUpdate(bool force) final //throw CancelProcess + { + if (uiUpdateDue(force)) + { + const bool abortRequestedBefore = static_cast(cancelRequested_); + + forceUiUpdateNoThrow(); + + //triggered by userRequestCancel() + // => sufficient to evaluate occasionally when uiUpdateDue()! + // => refresh *before* throwing: support requestUiUpdate() during destruction + if (cancelRequested_) + { + if (!abortRequestedBefore) + forceUiUpdateNoThrow(); //immediately show the "Stop requested..." status after user clicked cancel + throw CancelProcess(); + } + } + } + + virtual void forceUiUpdateNoThrow() = 0; //noexcept + + void updateStatus(std::wstring&& msg) final //throw CancelProcess + { + //assert(!msg.empty()); -> possible, e.g. start of parallel scan + statusText_ = std::move(msg); //update *before* running operations that can throw + requestUiUpdate(false /*force*/); //throw CancelProcess + } + + [[noreturn]] void cancelProcessNow(CancelReason reason) + { + if (!cancelRequested_ || reason == CancelReason::user) //CancelReason::user overwrites CancelReason::firstError + cancelRequested_ = reason; + + forceUiUpdateNoThrow(); //flush GUI to show new cancelled state + throw CancelProcess(); + } + + //implement CancelCallback + void userRequestCancel() final + { + cancelRequested_ = CancelReason::user; //may overwrite CancelReason::firstError + } //called from GUI code: this does NOT call cancelProcessNow() immediately, but later when we're out of the C GUI call stack + //=> don't call forceUiUpdateNoThrow() here! + + //implement Statistics + ProcessPhase currentPhase() const final { return currentPhase_; } + + ProgressStats getCurrentStats() const override { return statsCurrent_; } + ProgressStats getTotalStats () const override { return statsTotal_; } + + const std::wstring& currentStatusText() const override { return statusText_; } + + std::optional taskCancelled() const override { return cancelRequested_; } + +private: + void updateData(ProgressStats& stats, int itemsDelta, int64_t bytesDelta) + { + assert(stats.items >= 0); + assert(stats.bytes >= 0); + stats.items += itemsDelta; + stats.bytes += bytesDelta; + } + + ProcessPhase currentPhase_ = ProcessPhase::none; + ProgressStats statsCurrent_; + ProgressStats statsTotal_{-1, -1}; + std::wstring statusText_; + + std::optional cancelRequested_; +}; + + +void delayAndCountDown(std::chrono::nanoseconds delay, const std::function& notifyStatus); +} + +#endif //STATUS_HANDLER_H_81704805908341534 diff --git a/FreeFileSync/Source/ui/abstract_folder_picker.cpp b/FreeFileSync/Source/ui/abstract_folder_picker.cpp new file mode 100644 index 0000000..81b6256 --- /dev/null +++ b/FreeFileSync/Source/ui/abstract_folder_picker.cpp @@ -0,0 +1,392 @@ +// ***************************************************************************** +// * 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 "abstract_folder_picker.h" +#include +#include +#include +#include +#include "gui_generated.h" +#include "../icon_buffer.h" + +using namespace zen; +using namespace fff; +using AFS = AbstractFileSystem; + + +namespace +{ +enum class NodeLoadStatus +{ + notLoaded, + loading, + loaded +}; + +struct AfsTreeItemData : public wxTreeItemData +{ + AfsTreeItemData(const AbstractPath& path) : folderPath(path) {} + + const AbstractPath folderPath; + std::wstring errorMsg; //optional + NodeLoadStatus loadStatus = NodeLoadStatus::notLoaded; + std::vector> onLoadCompleted; //bound! +}; + + +wxString getNodeDisplayName(const AbstractPath& folderPath) +{ + if (!AFS::getParentPath(folderPath)) //server root + return utfTo(FILE_NAME_SEPARATOR); + + return utfTo(AFS::getItemName(folderPath)); +} + + +class AbstractFolderPickerDlg : public AbstractFolderPickerGenerated +{ +public: + AbstractFolderPickerDlg(wxWindow* parent, AbstractPath& folderPath); + +private: + void onOkay (wxCommandEvent& event) override; + void onCancel(wxCommandEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } + void onClose (wxCloseEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } + + void onLocalKeyEvent(wxKeyEvent& event); + void onExpandNode(wxTreeEvent& event) override; + void onItemTooltip(wxTreeEvent& event); + + void populateNodeThen(const wxTreeItemId& itemId, const std::function& evalOnGui /*optional*/, bool popupErrors); + + void findAndNavigateToExistingPath(const AbstractPath& folderPath); + void navigateToExistingPath(const wxTreeItemId& itemId, const std::vector& nodeRelPath, AFS::ItemType leafType); + + enum class TreeNodeImage + { + root = 0, //used as zero-based wxImageList index! + folder, + folderSymlink, + error + }; + + AsyncGuiQueue guiQueue_{25 /*polling [ms]*/}; //schedule and run long-running tasks asynchronously, but process results on GUI queue + + //output-only parameters: + AbstractPath& folderPathOut_; +}; + + +AbstractFolderPickerDlg::AbstractFolderPickerDlg(wxWindow* parent, AbstractPath& folderPath) : + AbstractFolderPickerGenerated(parent), + folderPathOut_(folderPath) +{ + setStandardButtonLayout(*bSizerStdButtons, StdButtons().setAffirmative(m_buttonOK).setCancel(m_buttonCancel)); + + m_staticTextStatus->SetLabel(L""); + m_treeCtrlFileSystem->SetMinSize({dipToWxsize(350), dipToWxsize(400)}); + + const int iconSize = screenToWxsize(IconBuffer::getPixSize(IconBuffer::IconSize::small)); + auto imgList = std::make_unique(iconSize, iconSize); + + //add images in same sequence like TreeNodeImage enum!!! + imgList->Add(toScaledBitmap(loadImage("server", wxsizeToScreen(iconSize)))); + imgList->Add(toScaledBitmap( IconBuffer::genericDirIcon (IconBuffer::IconSize::small))); + imgList->Add(toScaledBitmap(layOver(IconBuffer::genericDirIcon (IconBuffer::IconSize::small), + IconBuffer::linkOverlayIcon(IconBuffer::IconSize::small)))); + imgList->Add(toScaledBitmap(loadImage("msg_error", wxsizeToScreen(iconSize)))); + assert(imgList->GetImageCount() == static_cast(TreeNodeImage::error) + 1); + + m_treeCtrlFileSystem->AssignImageList(imgList.release()); //pass ownership + + const AbstractPath rootPath(folderPath.afsDevice, AfsPath()); + + const wxTreeItemId rootId = m_treeCtrlFileSystem->AddRoot(getNodeDisplayName(rootPath), static_cast(TreeNodeImage::root), -1, + new AfsTreeItemData(rootPath)); + m_treeCtrlFileSystem->SetItemHasChildren(rootId); + + if (!AFS::getParentPath(folderPath)) //server root + populateNodeThen(rootId, [this, rootId] { m_treeCtrlFileSystem->Expand(rootId); }, true /*popupErrors*/); + else + try //folder picker has dual responsibility: + { + //1. test server connection: + const AFS::ItemType type = AFS::getItemType(folderPath); //throw FileError + //2. navigate + select path + navigateToExistingPath(rootId, splitCpy(folderPath.afsPath.value, FILE_NAME_SEPARATOR, SplitOnEmpty::skip), type); + } + catch (const FileError& e) //not existing or access error + { + findAndNavigateToExistingPath(*AFS::getParentPath(folderPath)); //let's run async while the error message is shown :) + + showNotificationDialog(parent /*"this" not yet shown!*/, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); + } + + //---------------------------------------------------------------------- + GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() +#ifdef __WXGTK3__ + Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 + //Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404 +#endif + Center(); //apply *after* dialog size change! + + Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onLocalKeyEvent(event); }); //dialog-specific local key events + Bind(wxEVT_TREE_ITEM_GETTOOLTIP, [this](wxTreeEvent& event) { onItemTooltip (event); }); + + m_treeCtrlFileSystem->SetFocus(); +} + + +void AbstractFolderPickerDlg::onLocalKeyEvent(wxKeyEvent& event) +{ + switch (event.GetKeyCode()) + { + //wxTreeCtrl seems to eat up ENTER without adding any functionality; we can do better: + case WXK_RETURN: + case WXK_NUMPAD_ENTER: + if (event.ControlDown()) //Ctrl+Enter or on macOS: Command+Enter + { + wxCommandEvent dummy(wxEVT_COMMAND_BUTTON_CLICKED); + m_buttonOK->Command(dummy); //simulate click + return; + } + break; + } + event.Skip(); +} + + +struct FlatTraverserCallback : public AFS::TraverserCallback +{ + struct Result + { + std::unordered_map folderNames; + std::wstring errorMsg; + }; + + const Result& getResult() { return result_; } + +private: + void onFile (const AFS::FileInfo& fi) override {} + std::shared_ptr onFolder (const AFS::FolderInfo& fi) override { result_.folderNames.emplace(fi.itemName, fi.isFollowedSymlink); return nullptr; } + HandleLink onSymlink(const AFS::SymlinkInfo& si) override { return HandleLink::follow; } + HandleError reportDirError (const ErrorInfo& errorInfo) override { logError(errorInfo.msg); return HandleError::ignore; } + HandleError reportItemError(const ErrorInfo& errorInfo, const Zstring& itemName) override { logError(errorInfo.msg); return HandleError::ignore; } + + void logError(const std::wstring& msg) + { + if (result_.errorMsg.empty()) + result_.errorMsg = msg; + } + + Result result_; +}; + + +void AbstractFolderPickerDlg::populateNodeThen(const wxTreeItemId& itemId, const std::function& evalOnGui, bool popupErrors) +{ + if (auto itemData = dynamic_cast(m_treeCtrlFileSystem->GetItemData(itemId))) + { + switch (itemData->loadStatus) + { + case NodeLoadStatus::notLoaded: + { + if (evalOnGui) + itemData->onLoadCompleted.push_back(evalOnGui); + + itemData->loadStatus = NodeLoadStatus::loading; + + m_treeCtrlFileSystem->SetItemText(itemId, getNodeDisplayName(itemData->folderPath) + L" (" + _("Loading...") + L')'); + + guiQueue_.processAsync([folderPath = itemData->folderPath] //AbstractPath is thread-safe like an int! + { + auto ft = std::make_shared(); //noexcept, traverse directory one level deep + AFS::traverseFolderRecursive(folderPath.afsDevice, {{folderPath.afsPath, ft}}, 1 /*parallelOps*/); + return ft->getResult(); + }, + + [this, itemId, popupErrors](const FlatTraverserCallback::Result& result) + { + if (auto itemData2 = dynamic_cast(m_treeCtrlFileSystem->GetItemData(itemId))) + { + m_treeCtrlFileSystem->SetItemText(itemId, getNodeDisplayName(itemData2->folderPath)); //remove "loading" phrase + + if (result.folderNames.empty()) + m_treeCtrlFileSystem->SetItemHasChildren(itemId, false); + else + { + //let's not use the wxTreeCtrl::OnCompareItems() abomination to implement sorting: + std::vector> folderNamesSorted(result.folderNames.begin(), result.folderNames.end()); + std::sort(folderNamesSorted.begin(), folderNamesSorted.end(), [](const auto& lhs, const auto& rhs) { return LessNaturalSort()(lhs.first, rhs.first); }); + + for (const auto& [childName, isSymlink] : folderNamesSorted) + { + const AbstractPath childFolderPath = AFS::appendRelPath(itemData2->folderPath, childName); + + wxTreeItemId childId = m_treeCtrlFileSystem->AppendItem(itemId, getNodeDisplayName(childFolderPath), + static_cast(isSymlink ? TreeNodeImage::folderSymlink : TreeNodeImage::folder), -1, + new AfsTreeItemData(childFolderPath)); + m_treeCtrlFileSystem->SetItemHasChildren(childId); + } + } + + if (!result.errorMsg.empty()) + { + m_treeCtrlFileSystem->SetItemImage(itemId, static_cast(TreeNodeImage::error)); + itemData2->errorMsg = result.errorMsg; + + if (popupErrors) + showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(result.errorMsg)); + } + + itemData2->loadStatus = NodeLoadStatus::loaded; //set status *before* running callbacks + for (const auto& evalOnGui2 : itemData2->onLoadCompleted) + evalOnGui2(); + } + }); + } + break; + + case NodeLoadStatus::loading: + if (evalOnGui) itemData->onLoadCompleted.push_back(evalOnGui); + break; + + case NodeLoadStatus::loaded: + if (evalOnGui) evalOnGui(); + break; + } + } +} + + +//1. find longest existing/accessible (parent) path +void AbstractFolderPickerDlg::findAndNavigateToExistingPath(const AbstractPath& folderPath) +{ + if (!AFS::getParentPath(folderPath)) + return m_staticTextStatus->SetLabel(L""); + + m_staticTextStatus->SetLabelText(_("Scanning...") + L' ' + utfTo(FILE_NAME_SEPARATOR + folderPath.afsPath.value)); //keep it short! + + guiQueue_.processAsync([folderPath]() -> std::optional + { + try + { + return AFS::getItemType(folderPath); //throw FileError + } + catch (FileError&) { return std::nullopt; } //not existing or access error + }, + + [this, folderPath](std::optional type) + { + if (type) + { + m_staticTextStatus->SetLabel(L""); + navigateToExistingPath(m_treeCtrlFileSystem->GetRootItem(), splitCpy(folderPath.afsPath.value, FILE_NAME_SEPARATOR, SplitOnEmpty::skip), *type); + } + else //split into multiple small async tasks rather than a single large one! + findAndNavigateToExistingPath(*AFS::getParentPath(folderPath)); + }); +} + + +//2. navgiate while ignoring any intermediate (access) errors or problems with hidden folders +void AbstractFolderPickerDlg::navigateToExistingPath(const wxTreeItemId& itemId, const std::vector& nodeRelPath, AFS::ItemType leafType) +{ + if (nodeRelPath.empty() || + (nodeRelPath.size() == 1 && leafType == AFS::ItemType::file)) //let's be *uber* correct + { + m_treeCtrlFileSystem->SelectItem(itemId); + //m_treeCtrlFileSystem->EnsureVisible(itemId); -> not needed: maybe wxTreeCtrl::Expand() does this? + return; + } + + populateNodeThen(itemId, [this, itemId, nodeRelPath, leafType] + { + const Zstring childFolderName = nodeRelPath.front(); + const std::vector childFolderRelPath{nodeRelPath.begin() + 1, nodeRelPath.end()}; + + wxTreeItemId childIdMatch; + size_t insertPos = 0; //let's not use the wxTreeCtrl::OnCompareItems() abomination to implement sorting + + wxTreeItemIdValue cookie = nullptr; + for (wxTreeItemId childId = m_treeCtrlFileSystem->GetFirstChild(itemId, cookie); + childId.IsOk(); + childId = m_treeCtrlFileSystem->GetNextChild(itemId, cookie)) + if (auto itemData = dynamic_cast(m_treeCtrlFileSystem->GetItemData(childId))) + { + const Zstring& itemName = AFS::getItemName(itemData->folderPath); + + if (LessNaturalSort()(itemName, childFolderName)) + ++insertPos; //assume items are already naturally sorted, see populateNodeThen() + + if (equalNoCase(itemName, childFolderName)) + { + childIdMatch = childId; + if (itemName == childFolderName) + break; //exact match => no need to search further! + } + } + + //we *know* that childFolder exists: Maybe it's just hidden during browsing: https://freefilesync.org/forum/viewtopic.php?t=3809 + if (!childIdMatch.IsOk()) // or access to root folder is denied: https://freefilesync.org/forum/viewtopic.php?t=5999 + if (auto itemData = dynamic_cast(m_treeCtrlFileSystem->GetItemData(itemId))) + { + m_treeCtrlFileSystem->SetItemHasChildren(itemId); + + const AbstractPath childFolderPath = AFS::appendRelPath(itemData->folderPath, childFolderName); + + childIdMatch = m_treeCtrlFileSystem->InsertItem(itemId, insertPos, getNodeDisplayName(childFolderPath), + static_cast(childFolderRelPath.empty() && leafType == AFS::ItemType::symlink ? + TreeNodeImage::folderSymlink : TreeNodeImage::folder), -1, + new AfsTreeItemData(childFolderPath)); + m_treeCtrlFileSystem->SetItemHasChildren(childIdMatch); + } + + m_treeCtrlFileSystem->Expand(itemId); //wxTreeCtr::Expand emits wxTreeEvent!!! + + navigateToExistingPath(childIdMatch, childFolderRelPath, leafType); + }, false /*popupErrors*/); +} + + +void AbstractFolderPickerDlg::onExpandNode(wxTreeEvent& event) +{ + const wxTreeItemId itemId = event.GetItem(); + + if (auto itemData = dynamic_cast(m_treeCtrlFileSystem->GetItemData(itemId))) + if (itemData->loadStatus != NodeLoadStatus::loaded) + populateNodeThen(itemId, [this, itemId]() { m_treeCtrlFileSystem->Expand(itemId); }, true /*popupErrors*/); //wxTreeCtr::Expand emits wxTreeEvent!!! watch out for recursion! +} + + +void AbstractFolderPickerDlg::onItemTooltip(wxTreeEvent& event) +{ + wxString tooltip; + if (auto itemData = dynamic_cast(m_treeCtrlFileSystem->GetItemData(event.GetItem()))) + tooltip = itemData->errorMsg; + event.SetToolTip(tooltip); +} + + +void AbstractFolderPickerDlg::onOkay(wxCommandEvent& event) +{ + const wxTreeItemId itemId = m_treeCtrlFileSystem->GetFocusedItem(); + + auto itemData = dynamic_cast(m_treeCtrlFileSystem->GetItemData(itemId)); + assert(itemData); + if (itemData) + folderPathOut_ = itemData->folderPath; + + EndModal(static_cast(ConfirmationButton::accept)); +} +} + + +ConfirmationButton fff::showAbstractFolderPicker(wxWindow* parent, AbstractPath& folderPath) +{ + AbstractFolderPickerDlg pickerDlg(parent, folderPath); + return static_cast(pickerDlg.ShowModal()); +} diff --git a/FreeFileSync/Source/ui/abstract_folder_picker.h b/FreeFileSync/Source/ui/abstract_folder_picker.h new file mode 100644 index 0000000..aba6201 --- /dev/null +++ b/FreeFileSync/Source/ui/abstract_folder_picker.h @@ -0,0 +1,19 @@ +// ***************************************************************************** +// * 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 ABSTRACT_FOLDER_PICKER_HEADER_324872346895690 +#define ABSTRACT_FOLDER_PICKER_HEADER_324872346895690 + +#include +#include "../afs/abstract.h" + + +namespace fff +{ +zen::ConfirmationButton showAbstractFolderPicker(wxWindow* parent, AbstractPath& folderPath); +} + +#endif //ABSTRACT_FOLDER_PICKER_HEADER_324872346895690 diff --git a/FreeFileSync/Source/ui/app_icon.h b/FreeFileSync/Source/ui/app_icon.h new file mode 100644 index 0000000..4dc9f61 --- /dev/null +++ b/FreeFileSync/Source/ui/app_icon.h @@ -0,0 +1,30 @@ +// ***************************************************************************** +// * 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 APP_ICON_H_6748179634932174683214 +#define APP_ICON_H_6748179634932174683214 + +#include +#include + + +namespace fff +{ +inline +wxIcon getFfsIcon() +{ + using namespace zen; + //wxWidgets' bitmap to icon conversion on macOS can only deal with very specific sizes => check on all platforms! + assert(loadImage("FreeFileSync").GetWidth () == loadImage("FreeFileSync").GetHeight() && + loadImage("FreeFileSync").GetWidth() == dipToScreen(128)); + wxIcon icon; //Ubuntu-Linux does a bad job at down-scaling in Unity dash (blocky icons!) => prepare: + icon.CopyFromBitmap(loadImage("FreeFileSync", dipToScreen(64))); + return icon; + +} +} + +#endif //APP_ICON_H_6748179634932174683214 diff --git a/FreeFileSync/Source/ui/batch_config.cpp b/FreeFileSync/Source/ui/batch_config.cpp new file mode 100644 index 0000000..259107a --- /dev/null +++ b/FreeFileSync/Source/ui/batch_config.cpp @@ -0,0 +1,197 @@ +// ***************************************************************************** +// * 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 "batch_config.h" +#include +#include +#include +#include +#include +#include "gui_generated.h" + + +using namespace zen; +using namespace fff; + + +namespace +{ +struct BatchDialogConfig +{ + BatchExclusiveConfig batchExCfg; + bool ignoreErrors = false; +}; + + +class BatchDialog : public BatchDlgGenerated +{ +public: + BatchDialog(wxWindow* parent, BatchDialogConfig& dlgCfg); + +private: + void onClose (wxCloseEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } + void onCancel (wxCommandEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } + void onSaveBatchJob(wxCommandEvent& event) override; + + void onToggleIgnoreErrors(wxCommandEvent& event) override { updateGui(); } + void onToggleRunMinimized(wxCommandEvent& event) override + { + m_checkBoxAutoClose->SetValue(m_checkBoxRunMinimized->GetValue()); //usually user wants to change both + updateGui(); + } + + void onLocalKeyEvent(wxKeyEvent& event); + + void updateGui(); //re-evaluate gui after config changes + + void setConfig(const BatchDialogConfig& batchCfg); + BatchDialogConfig getConfig() const; + + //output-only parameters + BatchDialogConfig& dlgCfgOut_; + + EnumDescrList enumPostBatchAction_ + { + *m_choicePostSyncAction, + { + {PostBatchAction::none, L"", {}/*tooltip*/}, + {PostBatchAction::sleep, _("System: Sleep"), {}/*tooltip*/}, + {PostBatchAction::shutdown, _("System: Shut down"), {}/*tooltip*/}, + } + }; +}; + +//################################################################################################################################### + +BatchDialog::BatchDialog(wxWindow* parent, BatchDialogConfig& dlgCfg) : + BatchDlgGenerated(parent), + dlgCfgOut_(dlgCfg) +{ + setStandardButtonLayout(*bSizerStdButtons, StdButtons().setAffirmative(m_buttonSaveAs).setCancel(m_buttonCancel)); + + m_staticTextHeader->SetLabelText(replaceCpy(m_staticTextHeader->GetLabelText(), L"%x", L"FreeFileSync.exe <" + _("configuration file") + L">.ffs_batch")); + m_staticTextHeader->Wrap(dipToWxsize(520)); + + setImage(*m_bitmapBatchJob, loadImage("cfg_batch")); + + setConfig(dlgCfg); + + Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onLocalKeyEvent(event); }); //enable dialog-specific key events + + GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() +#ifdef __WXGTK3__ + Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 + //Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404 +#endif + Center(); //apply *after* dialog size change! + + m_buttonSaveAs->SetFocus(); +} + + +void BatchDialog::updateGui() //re-evaluate gui after config changes +{ + const BatchDialogConfig dlgCfg = getConfig(); //resolve parameter ownership: some on GUI controls, others member variables + + setImage(*m_bitmapIgnoreErrors, greyScaleIfDisabled(loadImage("error_ignore_active"), dlgCfg.ignoreErrors)); + + m_radioBtnErrorDialogShow ->Enable(!dlgCfg.ignoreErrors); + m_radioBtnErrorDialogCancel->Enable(!dlgCfg.ignoreErrors); + + setImage(*m_bitmapMinimizeToTray, greyScaleIfDisabled(loadImage("minimize_to_tray"), dlgCfg.batchExCfg.runMinimized)); +} + + +void BatchDialog::setConfig(const BatchDialogConfig& dlgCfg) +{ + m_checkBoxIgnoreErrors->SetValue(dlgCfg.ignoreErrors); + + //transfer parameter ownership to GUI + m_radioBtnErrorDialogShow ->SetValue(false); + m_radioBtnErrorDialogCancel->SetValue(false); + + switch (dlgCfg.batchExCfg.batchErrorHandling) + { + case BatchErrorHandling::showPopup: + m_radioBtnErrorDialogShow->SetValue(true); + break; + case BatchErrorHandling::cancel: + m_radioBtnErrorDialogCancel->SetValue(true); + break; + } + + m_checkBoxRunMinimized->SetValue(dlgCfg.batchExCfg.runMinimized); + m_checkBoxAutoClose ->SetValue(dlgCfg.batchExCfg.autoCloseSummary); + enumPostBatchAction_.set(dlgCfg.batchExCfg.postBatchAction); + + updateGui(); //re-evaluate gui after config changes +} + + +BatchDialogConfig BatchDialog::getConfig() const +{ + return + { + .batchExCfg + { + .runMinimized = m_checkBoxRunMinimized->GetValue(), + .autoCloseSummary = m_checkBoxAutoClose ->GetValue(), + .batchErrorHandling = m_radioBtnErrorDialogCancel->GetValue() ? BatchErrorHandling::cancel : BatchErrorHandling::showPopup, + .postBatchAction = enumPostBatchAction_.get(), + }, + .ignoreErrors = m_checkBoxIgnoreErrors->GetValue(), + }; +} + + +void BatchDialog::onLocalKeyEvent(wxKeyEvent& event) +{ + switch (event.GetKeyCode()) + { + case WXK_RETURN: + case WXK_NUMPAD_ENTER: + if (event.ControlDown()) //Ctrl+Enter or on macOS: Command+Enter + { + wxCommandEvent dummy(wxEVT_COMMAND_BUTTON_CLICKED); + m_buttonSaveAs->Command(dummy); //simulate click + return; + } + break; + } + event.Skip(); +} + + +void BatchDialog::onSaveBatchJob(wxCommandEvent& event) +{ + //BatchDialogConfig dlgCfg = getConfig(); + + //------- parameter validation (BEFORE writing output!) ------- + + //------------------------------------------------------------- + + dlgCfgOut_ = getConfig(); + EndModal(static_cast(ConfirmationButton::accept)); +} +} + + +ConfirmationButton fff::showBatchConfigDialog(wxWindow* parent, + BatchExclusiveConfig& batchExCfg, + bool& ignoreErrors) +{ + BatchDialogConfig dlgCfg = {batchExCfg, ignoreErrors}; + + BatchDialog batchDlg(parent, dlgCfg); + + const auto rv = static_cast(batchDlg.ShowModal()); + if (rv == ConfirmationButton::accept) + { + batchExCfg = dlgCfg.batchExCfg; + ignoreErrors = dlgCfg.ignoreErrors; + } + return rv; +} diff --git a/FreeFileSync/Source/ui/batch_config.h b/FreeFileSync/Source/ui/batch_config.h new file mode 100644 index 0000000..15e15c6 --- /dev/null +++ b/FreeFileSync/Source/ui/batch_config.h @@ -0,0 +1,22 @@ +// ***************************************************************************** +// * 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 BATCH_CONFIG_H_3921674832168945 +#define BATCH_CONFIG_H_3921674832168945 + +#include +#include "../config.h" + + +namespace fff +{ +//show and let user customize batch settings (without saving) +zen::ConfirmationButton showBatchConfigDialog(wxWindow* parent, + BatchExclusiveConfig& batchExCfg, + bool& ignoreErrors); +} + +#endif //BATCH_CONFIG_H_3921674832168945 diff --git a/FreeFileSync/Source/ui/batch_status_handler.cpp b/FreeFileSync/Source/ui/batch_status_handler.cpp new file mode 100644 index 0000000..a6334bf --- /dev/null +++ b/FreeFileSync/Source/ui/batch_status_handler.cpp @@ -0,0 +1,433 @@ +// ***************************************************************************** +// * 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 "batch_status_handler.h" +#include +#include +#include +#include + +using namespace zen; +using namespace fff; + + +BatchStatusHandler::BatchStatusHandler(bool showProgress, + const std::wstring& jobName, + const std::chrono::system_clock::time_point& startTime, + bool ignoreErrors, + size_t autoRetryCount, + std::chrono::seconds autoRetryDelay, + const Zstring& soundFileSyncComplete, + const Zstring& soundFileAlertPending, + const WindowLayout::Dimensions& dims, + bool autoCloseDialog, + PostBatchAction postBatchAction, + BatchErrorHandling batchErrorHandling) : + jobName_(jobName), + startTime_(startTime), + autoRetryCount_(autoRetryCount), + autoRetryDelay_(autoRetryDelay), + soundFileSyncComplete_(soundFileSyncComplete), + soundFileAlertPending_(soundFileAlertPending), + batchErrorHandling_(batchErrorHandling) +{ + //set *after* initializer list => callbacks during construction to getErrorStats()! + progressDlg_ = SyncProgressDialog::create(dims, [this] { userRequestCancel(); }, *this, nullptr /*parentWindow*/, showProgress, autoCloseDialog, + {jobName}, std::chrono::system_clock::to_time_t(startTime), ignoreErrors, autoRetryCount, [&] + { + switch (postBatchAction) + { + case PostBatchAction::none: + return PostSyncAction::none; + case PostBatchAction::sleep: + return PostSyncAction::sleep; + case PostBatchAction::shutdown: + return PostSyncAction::shutdown; + } + assert(false); + return PostSyncAction::none; + }()); + //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!!! +} + + +BatchStatusHandler::~BatchStatusHandler() +{ + if (progressDlg_) //prepareResult() was not called! + std::abort(); +} + + +BatchStatusHandler::Result BatchStatusHandler::prepareResult() +{ + //keep correct summary window stats considering count down timer, system sleep + const std::chrono::milliseconds totalTime = progressDlg_->pauseAndGetTotalTime(); + + //append "extra" log for sync errors that could not otherwise be reported: + if (const ErrorLog extraLog = fetchExtraLog(); + !extraLog.empty()) + { + append(errorLog_.ref(), extraLog); + std::stable_sort(errorLog_.ref().begin(), errorLog_.ref().end(), [](const LogEntry& lhs, const LogEntry& rhs) { return lhs.time < rhs.time; }); + } + + //determine post-sync status irrespective of further errors during tear-down + assert(!syncResult_); + syncResult_ = [&] + { + if (taskCancelled()) + { + logMsg(errorLog_.ref(), _("Stopped"), MSG_TYPE_ERROR); //= user cancel or "stop on first error" + return TaskResult::cancelled; + } + const ErrorLogStats logCount = getStats(errorLog_.ref()); + if (logCount.errors > 0) + return TaskResult::error; + else if (logCount.warnings > 0) + return TaskResult::warning; + + if (getTotalStats() == ProgressStats()) + logMsg(errorLog_.ref(), _("Nothing to synchronize"), MSG_TYPE_INFO); + return TaskResult::success; + }(); + + assert(*syncResult_ == TaskResult::cancelled || currentPhase() == ProcessPhase::sync); + + const ProcessSummary summary + { + startTime_, *syncResult_, {jobName_}, + getCurrentStats(), + getTotalStats (), + totalTime + }; + + return {summary, errorLog_}; +} + + +BatchStatusHandler::DlgOptions BatchStatusHandler::showResult() +{ + bool autoClose = false; + bool suspend = false; + FinalRequest finalRequest = FinalRequest::none; + + if (taskCancelled() && *taskCancelled() == CancelReason::user) + { + /* user cancelled => don't run post sync command + => don't send email notification + => don't play sound notification + => don't run post sync action */ + if (switchToGuiRequested_) //-> avoid recursive yield() calls, thous switch not before ending batch mode + { + autoClose = true; + finalRequest = FinalRequest::switchGui; + } + } + else + { + //--------------------- post sync actions ---------------------- + auto proceedWithShutdown = [&](const std::wstring& operationName) + { + if (progressDlg_->getWindowIfVisible()) + try + { + assert(!endsWith(operationName, L".")); + auto notifyStatusThrowOnCancel = [&](const std::wstring& timeRemMsg) + { + try { updateStatus(operationName + L"... " + timeRemMsg); /*throw CancelProcess*/ } + catch (CancelProcess&) + { + if (taskCancelled() && *taskCancelled() == CancelReason::user) + throw; + } + }; + delayAndCountDown(std::chrono::seconds(10), notifyStatusThrowOnCancel); //throw CancelProcess + } + catch (CancelProcess&) { return false; } + + return true; + }; + + switch (progressDlg_->getAndFreezePostSyncAction()) + { + case PostSyncAction::none: + autoClose = progressDlg_->getOptionAutoCloseDialog(); + break; + case PostSyncAction::exit: + assert(false); + break; + case PostSyncAction::sleep: + if (proceedWithShutdown(_("System: Sleep"))) + { + autoClose = progressDlg_->getOptionAutoCloseDialog(); + suspend = true; + } + break; + case PostSyncAction::shutdown: + if (proceedWithShutdown(_("System: Shut down"))) + { + autoClose = true; + finalRequest = FinalRequest::shutdown; //system shutdown must be handled by calling context! + } + break; + } + } + + if (suspend) //...*before* results dialog is shown + try + { + suspendSystem(); //throw FileError + } + catch (const FileError& e) { logMsg(errorLog_.ref(), e.toString(), MSG_TYPE_ERROR); } + + //--------------------- sound notification ---------------------- + if (taskCancelled() && *taskCancelled() == CancelReason::user) + ; + else if (!suspend && !autoClose && //only play when actually showing results dialog + !soundFileSyncComplete_.empty()) + { + //wxWidgets shows modal error dialog by default => "no, wxWidgets, NO!" + wxLog* oldLogTarget = wxLog::SetActiveTarget(new wxLogStderr); //transfer and receive ownership! + ZEN_ON_SCOPE_EXIT(delete wxLog::SetActiveTarget(oldLogTarget)); + + wxSound::Play(utfTo(soundFileSyncComplete_), wxSOUND_ASYNC); + } + //if (::GetForegroundWindow() != GetHWND()) + // RequestUserAttention(); -> probably too much since task bar is already colorized with Taskbar::Status::error or Status::normal + + const auto [autoCloseDialog, dim] = progressDlg_->destroy(autoClose, + true /*restoreParentFrame: n/a here*/, + *syncResult_, errorLog_); + //caveat: calls back to getErrorStats() => share errorLog_ + progressDlg_ = nullptr; + + return {dim, finalRequest}; +} + + +wxWindow* BatchStatusHandler::getWindowIfVisible() +{ + return progressDlg_ ? progressDlg_->getWindowIfVisible() : nullptr; +} + + +void BatchStatusHandler::initNewPhase(int itemsTotal, int64_t bytesTotal, ProcessPhase phaseID) +{ + StatusHandler::initNewPhase(itemsTotal, bytesTotal, phaseID); + progressDlg_->initNewPhase(); //call after "StatusHandler::initNewPhase" + + //macOS needs a full yield to update GUI and get rid of "dummy" texts + requestUiUpdate(true /*force*/); //throw CancelProcess +} + + +void BatchStatusHandler::updateDataProcessed(int itemsDelta, int64_t bytesDelta) //noexcept! +{ + StatusHandler::updateDataProcessed(itemsDelta, bytesDelta); + + //note: this method should NOT throw in order to properly allow undoing setting of statistics! + progressDlg_->notifyProgressChange(); //noexcept + //for "curveDataBytes_->addRecord()" +} + + +void BatchStatusHandler::logMessage(const std::wstring& msg, MsgType type) +{ + logMsg(errorLog_.ref(), msg, [&] + { + switch (type) + { + case MsgType::info: return MSG_TYPE_INFO; + case MsgType::warning: return MSG_TYPE_WARNING; + case MsgType::error: return MSG_TYPE_ERROR; + } + assert(false); + return MSG_TYPE_ERROR; + }()); + requestUiUpdate(false /*force*/); //throw CancelProcess +} + + +void BatchStatusHandler::reportWarning(const std::wstring& msg, bool& warningActive) +{ + PauseTimers dummy(*progressDlg_); + + logMsg(errorLog_.ref(), msg, MSG_TYPE_WARNING); + + if (!warningActive) + return; + + if (!progressDlg_->getOptionIgnoreErrors()) + switch (batchErrorHandling_) + { + case BatchErrorHandling::showPopup: + { + forceUiUpdateNoThrow(); //noexcept! => don't throw here when error occurs during clean up! + + bool dontWarnAgain = false; + switch (showQuestionDialog(progressDlg_->getWindowIfVisible(), DialogInfoType::warning, + PopupDialogCfg().setDetailInstructions(msg + L"\n\n" + _("You can switch to FreeFileSync's main window to resolve this issue.")). + alertWhenPending(soundFileAlertPending_). + setCheckBox(dontWarnAgain, _("&Don't show this warning again"), static_cast(QuestionButton2::no)), + _("&Ignore"), _("&Switch"))) + { + case QuestionButton2::yes: //ignore + warningActive = !dontWarnAgain; + break; + + case QuestionButton2::no: //switch + logMsg(errorLog_.ref(), _("Switching to FreeFileSync's main window"), MSG_TYPE_INFO); + switchToGuiRequested_ = true; //treat as a special kind of cancel + cancelProcessNow(CancelReason::user); //throw CancelProcess + + case QuestionButton2::cancel: + cancelProcessNow(CancelReason::user); //throw CancelProcess + break; + } + } + break; //keep it! last switch might not find match + + case BatchErrorHandling::cancel: + cancelProcessNow(CancelReason::firstError); //throw CancelProcess + break; + } +} + + +ProcessCallback::Response BatchStatusHandler::reportError(const ErrorInfo& errorInfo) +{ + PauseTimers dummy(*progressDlg_); + + //log actual fail time (not "now"!) + const time_t failTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now() - + std::chrono::duration_cast(std::chrono::steady_clock::now() - errorInfo.failTime)); + //auto-retry + if (errorInfo.retryNumber < autoRetryCount_) + { + logMsg(errorLog_.ref(), errorInfo.msg + L"\n-> " + _("Automatic retry"), MSG_TYPE_INFO, failTime); + delayAndCountDown(errorInfo.failTime + autoRetryDelay_ - std::chrono::steady_clock::now(), + [&, statusPrefix = _("Automatic retry") + + (errorInfo.retryNumber == 0 ? L"" : L' ' + formatNumber(errorInfo.retryNumber + 1)) + SPACED_DASH, + statusPostfix = SPACED_DASH + _("Error") + L": " + replaceCpy(errorInfo.msg, L'\n', L' ')](const std::wstring& timeRemMsg) + { this->updateStatus(statusPrefix + timeRemMsg + statusPostfix); }); //throw CancelProcess + return ProcessCallback::retry; + } + + //always, except for "retry": + auto guardWriteLog = makeGuard([&] { logMsg(errorLog_.ref(), errorInfo.msg, MSG_TYPE_ERROR, failTime); }); + + if (!progressDlg_->getOptionIgnoreErrors()) + { + switch (batchErrorHandling_) + { + case BatchErrorHandling::showPopup: + { + forceUiUpdateNoThrow(); //noexcept! => don't throw here when error occurs during clean up! + + switch (showConfirmationDialog(progressDlg_->getWindowIfVisible(), DialogInfoType::error, + PopupDialogCfg().setDetailInstructions(errorInfo.msg). + alertWhenPending(soundFileAlertPending_), + _("&Ignore"), _("Ignore &all"), _("&Retry"))) + { + case ConfirmationButton3::accept: //ignore + return ProcessCallback::ignore; + + case ConfirmationButton3::accept2: //ignore all + progressDlg_->setOptionIgnoreErrors(true); + return ProcessCallback::ignore; + + case ConfirmationButton3::decline: //retry + guardWriteLog.dismiss(); + logMsg(errorLog_.ref(), errorInfo.msg + L"\n-> " + _("Retrying operation..."), MSG_TYPE_INFO, failTime); + return ProcessCallback::retry; + + case ConfirmationButton3::cancel: + cancelProcessNow(CancelReason::user); //throw CancelProcess + break; + } + } + break; //used if last switch didn't find a match + + case BatchErrorHandling::cancel: + cancelProcessNow(CancelReason::firstError); //throw CancelProcess + break; + } + } + else + return ProcessCallback::ignore; + + assert(false); + return ProcessCallback::ignore; //dummy value +} + + +void BatchStatusHandler::reportFatalError(const std::wstring& msg) +{ + PauseTimers dummy(*progressDlg_); + + logMsg(errorLog_.ref(), msg, MSG_TYPE_ERROR); + + if (!progressDlg_->getOptionIgnoreErrors()) + switch (batchErrorHandling_) + { + case BatchErrorHandling::showPopup: + { + forceUiUpdateNoThrow(); //noexcept! => don't throw here when error occurs during clean up! + + switch (showConfirmationDialog(progressDlg_->getWindowIfVisible(), DialogInfoType::error, + PopupDialogCfg().setDetailInstructions(msg). + alertWhenPending(soundFileAlertPending_), + _("&Ignore"), _("Ignore &all"))) + { + case ConfirmationButton2::accept: //ignore + break; + + case ConfirmationButton2::accept2: //ignore all + progressDlg_->setOptionIgnoreErrors(true); + break; + + case ConfirmationButton2::cancel: + cancelProcessNow(CancelReason::user); //throw CancelProcess + break; + } + } + break; + + case BatchErrorHandling::cancel: + cancelProcessNow(CancelReason::firstError); //throw CancelProcess + break; + } +} + + +Statistics::ErrorStats BatchStatusHandler::getErrorStats() const +{ + //errorLog_ is an "append only" structure, so we can make getErrorStats() complexity "constant time": + std::for_each(errorLog_.ref().begin() + errorStatsRowsChecked_, errorLog_.ref().end(), [&](const LogEntry& entry) + { + switch (entry.type) + { + case MSG_TYPE_INFO: + break; + case MSG_TYPE_WARNING: + ++errorStatsBuf_.warningCount; + break; + case MSG_TYPE_ERROR: + ++errorStatsBuf_.errorCount; + break; + } + }); + errorStatsRowsChecked_ = errorLog_.ref().size(); + + return errorStatsBuf_; +} + + +void BatchStatusHandler::forceUiUpdateNoThrow() +{ + progressDlg_->updateGui(); +} diff --git a/FreeFileSync/Source/ui/batch_status_handler.h b/FreeFileSync/Source/ui/batch_status_handler.h new file mode 100644 index 0000000..27b1809 --- /dev/null +++ b/FreeFileSync/Source/ui/batch_status_handler.h @@ -0,0 +1,86 @@ +// ***************************************************************************** +// * 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 BATCH_STATUS_HANDLER_H_857390451451234566 +#define BATCH_STATUS_HANDLER_H_857390451451234566 + +#include +#include "progress_indicator.h" +#include "../config.h" +#include "../status_handler.h" + + +namespace fff +{ +//BatchStatusHandler(SyncProgressDialog) will internally process Window messages! disable GUI controls to avoid unexpected callbacks! +class BatchStatusHandler : public StatusHandler +{ +public: + BatchStatusHandler(bool showProgress, + const std::wstring& jobName, //should not be empty for a batch job! + const std::chrono::system_clock::time_point& startTime, + bool ignoreErrors, + size_t autoRetryCount, + std::chrono::seconds autoRetryDelay, + const Zstring& soundFileSyncComplete, + const Zstring& soundFileAlertPending, + const zen::WindowLayout::Dimensions& dim, + bool autoCloseDialog, + PostBatchAction postBatchAction, + BatchErrorHandling batchErrorHandling); //noexcept!! + ~BatchStatusHandler(); + + void initNewPhase (int itemsTotal, int64_t bytesTotal, ProcessPhase phaseID) override; // + void logMessage (const std::wstring& msg, MsgType type) override; // + void reportWarning (const std::wstring& msg, bool& warningActive) override; //throw CancelProcess + Response reportError (const ErrorInfo& errorInfo) override; // + void reportFatalError(const std::wstring& msg) override; // + ErrorStats getErrorStats() const override; + + void updateDataProcessed(int itemsDelta, int64_t bytesDelta) override; //noexcept + void forceUiUpdateNoThrow() override; // + + struct Result + { + ProcessSummary summary; + zen::SharedRef errorLog; + }; + Result prepareResult(); + + enum class FinalRequest + { + none, + switchGui, + shutdown + }; + struct DlgOptions + { + zen::WindowLayout::Dimensions dim; + FinalRequest finalRequest; + }; + DlgOptions showResult(); + + wxWindow* getWindowIfVisible(); + +private: + const std::wstring jobName_; + const std::chrono::system_clock::time_point startTime_; + const size_t autoRetryCount_; + const std::chrono::seconds autoRetryDelay_; + const Zstring soundFileSyncComplete_; + const Zstring soundFileAlertPending_; + + SyncProgressDialog* progressDlg_; //managed to have the same lifetime as this handler! + zen::SharedRef errorLog_ = zen::makeSharedRef(); + mutable Statistics::ErrorStats errorStatsBuf_{}; + mutable size_t errorStatsRowsChecked_ = 0; + const BatchErrorHandling batchErrorHandling_; + bool switchToGuiRequested_ = false; + std::optional syncResult_; +}; +} + +#endif //BATCH_STATUS_HANDLER_H_857390451451234566 diff --git a/FreeFileSync/Source/ui/cfg_grid.cpp b/FreeFileSync/Source/ui/cfg_grid.cpp new file mode 100644 index 0000000..0cd3a2f --- /dev/null +++ b/FreeFileSync/Source/ui/cfg_grid.cpp @@ -0,0 +1,782 @@ +// ***************************************************************************** +// * 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 "cfg_grid.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../ffs_paths.h" +#include "../afs/native.h" + +using namespace zen; +using namespace fff; +using AFS = AbstractFileSystem; + + +Zstring fff::getLastRunConfigPath() +{ + return appendPath(getConfigDirPath(), Zstr("LastRun.ffs_gui")); +} + + +std::vector ConfigView::get() const +{ + std::map> itemsSorted; //sort by last use; put most recent items *first* (looks better in XML than reverted) + + for (const auto& [filePath, details] : cfgList_) + itemsSorted.emplace(details.lastUseIndex, details.cfgItem); + + std::vector cfgHistory; + for (const auto& [lastUseIndex, cfgItem] : itemsSorted) + cfgHistory.emplace_back(cfgItem); + + return cfgHistory; +} + + +void ConfigView::set(const std::vector& cfgItems) +{ + std::vector filePaths; + for (const ConfigFileItem& item : cfgItems) + filePaths.push_back(item.cfgFilePath); + + //list is stored with last used files first in XML, however addCfgFilesImpl() expects them last!!! + std::reverse(filePaths.begin(), filePaths.end()); + + cfgList_ .clear(); + cfgListView_.clear(); + addCfgFilesImpl(filePaths); + + for (const ConfigFileItem& item : cfgItems) + cfgList_.find(item.cfgFilePath)->second.cfgItem = item; //cfgFilePath must exist after addCfgFilesImpl()! + + sortListView(); +} + + +void ConfigView::addCfgFiles(const std::vector& filePaths) +{ + addCfgFilesImpl(filePaths); + sortListView(); +} + + +void ConfigView::addCfgFilesImpl(const std::vector& filePaths) +{ + //determine highest "last use" index number of m_listBoxHistory + int lastUseIndexMax = 0; + for (const auto& [filePath, details] : cfgList_) + lastUseIndexMax = std::max(lastUseIndexMax, details.lastUseIndex); + + for (const Zstring& filePath : filePaths) + if (auto it = cfgList_.find(filePath); + it == cfgList_.end()) + { + Details detail{.lastUseIndex = ++lastUseIndexMax}; + detail.cfgItem.cfgFilePath = filePath; + + std::tie(detail.name, detail.cfgType, detail.isLastRunCfg) = [&] + { + if (equalNativePath(filePath, lastRunConfigPath_)) + return std::make_tuple(utfTo(L'[' + _("Last session") + L']'), Details::CFG_TYPE_GUI, true); + + const Zstring fileName = getItemName(filePath); + + if (endsWithAsciiNoCase(fileName, ".ffs_gui")) + return std::make_tuple(beforeLast(fileName, Zstr('.'), IfNotFoundReturn::none), Details::CFG_TYPE_GUI, false); + else if (endsWithAsciiNoCase(fileName, ".ffs_batch")) + return std::make_tuple(beforeLast(fileName, Zstr('.'), IfNotFoundReturn::none), Details::CFG_TYPE_BATCH, false); + else + return std::make_tuple(fileName, Details::CFG_TYPE_NONE, false); + }(); + + auto itNew = cfgList_.emplace_hint(cfgList_.end(), filePath, std::move(detail)); + cfgListView_.push_back(itNew); + } + else + it->second.lastUseIndex = ++lastUseIndexMax; +} + + +void ConfigView::removeItems(const std::vector& filePaths) +{ + for (const Zstring& filePath : filePaths) + if (auto it = cfgList_.find(filePath); + it != cfgList_.end()) + { + std::erase(cfgListView_, it); + cfgList_.erase(it); + } + else assert(false); + + assert(cfgList_.size() == cfgListView_.size()); + + if (sortColumn_ == ColumnTypeCfg::name) + sortListView(); //needed if top element of colored-group is removed +} + + +void ConfigView::renameItem(const Zstring& pathFrom, const Zstring& pathTo) +{ + auto it = cfgList_.find(pathFrom); + assert(it != cfgList_.end()); + if (it != cfgList_.end()) + { + const Details detailsOld = it->second; + + std::erase(cfgListView_, it); + cfgList_.erase(it); + assert(cfgList_.size() == cfgListView_.size()); + + addCfgFilesImpl({pathTo}); + + //let's not lose certain metadata after renaming! + auto it2 = cfgList_.find(pathTo); + assert(it2 != cfgList_.end()); + if (it2 != cfgList_.end()) + { + it2->second.cfgItem.lastRunStats = detailsOld.cfgItem.lastRunStats; + it2->second.cfgItem.backColor = detailsOld.cfgItem.backColor; + it2->second.lastUseIndex = detailsOld.lastUseIndex; + it2->second.notes = detailsOld.notes; + } + sortListView(); + } +} + + +void ConfigView::setNotes(const Zstring& filePath, const std::wstring& notes) +{ + if (auto it = cfgList_.find(filePath); + it != cfgList_.end()) + it->second.notes = notes; + else assert(false); +} + + +void ConfigView::setLastRunStats(const std::vector& filePaths, const LastRunStats& lastRun) +{ + for (const Zstring& filePath : filePaths) + { + auto it = cfgList_.find(filePath); + assert(it != cfgList_.end()); + if (it != cfgList_.end()) + it->second.cfgItem.lastRunStats = lastRun; + } + + if (sortColumn_ != ColumnTypeCfg::name) + sortListView(); //needed if sorted by last sync time, or log +} + + +void ConfigView::setLastInSyncTime(const std::vector& filePaths, time_t lastRunTime) +{ + for (const Zstring& filePath : filePaths) + { + auto it = cfgList_.find(filePath); + assert(it != cfgList_.end()); + if (it != cfgList_.end()) + it->second.cfgItem.lastRunStats.startTime = lastRunTime; + } + + if (sortColumn_ != ColumnTypeCfg::name) + sortListView(); //needed if sorted by last sync time, or log +} + + +void ConfigView::setBackColor(const std::vector& filePaths, const wxColor& col, bool previewOnly) +{ + for (const Zstring& filePath : filePaths) + if (auto it = cfgList_.find(filePath); + it != cfgList_.end()) + { + if (previewOnly) + it->second.cfgItem.backColorPreview = col; + else + { + it->second.cfgItem.backColor = col; + it->second.cfgItem.backColorPreview = wxNullColour; + } + } + else assert(false); + + if (!previewOnly && sortColumn_ == ColumnTypeCfg::name) + sortListView(); //needed if top element of colored-group is removed +} + + +const ConfigView::Details* ConfigView::getItem(size_t row) const +{ + if (row < cfgListView_.size()) + return &cfgListView_[row]->second; + return nullptr; +} + + +std::pair ConfigView::getItem(const Zstring& filePath) const +{ + if (auto it = cfgList_.find(filePath); + it != cfgList_.end()) + return {&it->second, std::find(cfgListView_.begin(), cfgListView_.end(), it) - cfgListView_.begin()}; + return {}; +} + + +void ConfigView::setSortDirection(ColumnTypeCfg colType, bool ascending) +{ + sortColumn_ = colType; + sortAscending_ = ascending; + + sortListView(); +} + + +template +void ConfigView::sortListViewImpl() +{ + const auto lessCfgName = [](CfgFileList::iterator lhs, CfgFileList::iterator rhs) + { + if (lhs->second.isLastRunCfg != rhs->second.isLastRunCfg) + return lhs->second.isLastRunCfg; //"last session" should be at top position! + + return LessNaturalSort()(lhs->second.name, rhs->second.name); + }; + + const auto lessLastSync = [](CfgFileList::iterator lhs, CfgFileList::iterator rhs) + { + if (lhs->second.isLastRunCfg != rhs->second.isLastRunCfg) + return lhs->second.isLastRunCfg < rhs->second.isLastRunCfg; //"last session" label should be (always) last + + return makeSortDirection(std::greater(), std::bool_constant())( + lhs->second.cfgItem.lastRunStats.startTime, + rhs->second.cfgItem.lastRunStats.startTime); + //[!] ascending lastSync shows lowest "days past" first <=> highest lastSyncTime first + }; + + const auto lessSyncResult = [](CfgFileList::iterator lhs, CfgFileList::iterator rhs) + { + if (lhs->second.isLastRunCfg != rhs->second.isLastRunCfg) + return lhs->second.isLastRunCfg < rhs->second.isLastRunCfg; //"last session" label should be (always) last + + const bool haveResultL = !AFS::isNullPath(lhs->second.cfgItem.lastRunStats.logFilePath); + const bool haveResultR = !AFS::isNullPath(rhs->second.cfgItem.lastRunStats.logFilePath); + if (haveResultL != haveResultR) + return haveResultL > haveResultR; //move sync jobs that were never run to the back + + //primary sort order + if (haveResultL && lhs->second.cfgItem.lastRunStats.syncResult != rhs->second.cfgItem.lastRunStats.syncResult) + return makeSortDirection(std::greater(), std::bool_constant())(lhs->second.cfgItem.lastRunStats.syncResult, rhs->second.cfgItem.lastRunStats.syncResult); + + //secondary sort order + return LessNaturalSort()(lhs->second.name, rhs->second.name); + }; + + switch (sortColumn_) + { + case ColumnTypeCfg::name: + //pre-sort by name + std::sort(cfgListView_.begin(), cfgListView_.end(), lessCfgName); + + //aggregate groups by color (*almost* like a std::stable_sort) + for (auto it = cfgListView_.begin(); it != cfgListView_.end(); ) + if ((*it)->second.cfgItem.backColor.IsOk()) + it = std::stable_partition(it + 1, cfgListView_.end(), + [&groupCol = (*it)->second.cfgItem.backColor](CfgFileList::iterator item) { return item->second.cfgItem.backColor == groupCol; }); + else + ++it; + + //simplify aggregation logic by not having to consider "ascending/descending" + if (!ascending) + std::reverse(cfgListView_.begin(), cfgListView_.end()); + break; + + case ColumnTypeCfg::lastSync: + std::sort(cfgListView_.begin(), cfgListView_.end(), lessLastSync); + break; + + case ColumnTypeCfg::lastLog: + std::sort(cfgListView_.begin(), cfgListView_.end(), lessSyncResult); + break; + } +} + + +void ConfigView::sortListView() +{ + if (sortAscending_) + sortListViewImpl(); + else + sortListViewImpl(); +} + +//------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------- + +namespace +{ +class GridDataCfg : private wxEvtHandler, public GridData +{ +public: + GridDataCfg(Grid& grid) : grid_(grid) + { + grid.Bind(EVENT_GRID_MOUSE_LEFT_DOWN, [this](GridClickEvent& event) { onMouseLeft (event); }); + grid.Bind(EVENT_GRID_MOUSE_LEFT_DOUBLE, [this](GridClickEvent& event) { onMouseLeftDouble(event); }); + } + + ConfigView& getDataView() { return cfgView_; } + + static int getRowDefaultHeight(const Grid& grid) + { + return std::max(dipToWxsize(getMenuIconDipSize()), + grid.getMainWin().GetCharHeight()) + dipToWxsize(1) /*extra space*/; + } + + int getSyncOverdueDays() const { return syncOverdueDays_; } + void setSyncOverdueDays(int syncOverdueDays) { syncOverdueDays_ = syncOverdueDays; } + +private: + size_t getRowCount() const override { return cfgView_.getRowCount(); } + + static int getDaysPast(time_t last) + { + time_t now = std::time(nullptr); + + const TimeComp tcNow = getLocalTime(now); + const TimeComp tcLast = getLocalTime(last); + if (tcNow == TimeComp() || tcLast == TimeComp()) + { + assert(false); + return 0; + } + + //truncate down to midnight => incorrect during DST switches, but doesn't matter due to rounding below + now -= tcNow .hour * 3600 + tcNow .minute * 60 + tcNow .second; + last -= tcLast.hour * 3600 + tcLast.minute * 60 + tcLast.second; + + return numeric::intDivRound(now - last, 24 * 3600); + } + + std::wstring getValue(size_t row, ColumnType colType) const override + { + if (const ConfigView::Details* item = cfgView_.getItem(row)) + switch (static_cast(colType)) + { + case ColumnTypeCfg::name: + return utfTo(item->name); + + case ColumnTypeCfg::lastSync: + if (!item->isLastRunCfg && item->cfgItem.lastRunStats.startTime > 0) + { + //if (item->cfgItem.lastRunStats.startTime == 0) + // return std::wstring(1, EN_DASH); + + //return utfTo(formatTime(formatDateTimeTag, getLocalTime(item->cfgItem.lastRunStats.startTime))); + + const int daysPast = getDaysPast(item->cfgItem.lastRunStats.startTime); + return daysPast == 0 ? + utfTo(formatTime(Zstr("%R") /*equivalent to "%H:%M"*/, getLocalTime(item->cfgItem.lastRunStats.startTime))) : + //_("Today") : + _P("1 day", "%x days", daysPast); + } + break; + + case ColumnTypeCfg::lastLog: + if (!item->isLastRunCfg && !AFS::isNullPath(item->cfgItem.lastRunStats.logFilePath)) + return getSyncResultLabel(item->cfgItem.lastRunStats.syncResult); + break; + } + return std::wstring(); + } + + void renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected, HoverArea rowHover) override + { + if (selected) + clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHT)); + //else: wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW) already the default! + } + + enum class HoverAreaConfig + { + name, + link, + }; + + void renderCell(wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover) override + { + wxDCTextColourChanger textColor(dc); //accessibility: always set both foreground AND background colors! + if (selected) + textColor.Set(wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHTTEXT)); + //else: wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT) already the default! + + if (const ConfigView::Details* item = cfgView_.getItem(row)) + switch (static_cast(colType)) + { + case ColumnTypeCfg::name: + { + wxColor backColor = item->cfgItem.backColor; + if (item->cfgItem.backColorPreview.IsOk()) + backColor = item->cfgItem.backColorPreview; + + if (backColor.IsOk()) + { + wxRect rectTmp = rect; + if (!selected || item->cfgItem.backColorPreview.IsOk()) + { + rectTmp.width = rect.width * 2 / 3; + clearArea(dc, rectTmp, backColor); //accessibility: always set both foreground AND background colors! + textColor.Set(relativeContrast(backColor, *wxWHITE) > + relativeContrast(backColor, *wxBLACK) ? *wxWHITE : *wxBLACK); // + + rectTmp.x += rectTmp.width; + rectTmp.width = rect.width - rectTmp.width; + dc.GradientFillLinear(rectTmp, backColor, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW), wxEAST); + } + else //always show a glimpse of the background color + { + rectTmp.width = getColumnGapLeft() + dipToWxsize(getMenuIconDipSize()); + clearArea(dc, rectTmp, backColor); + + rectTmp.x += rectTmp.width; + rectTmp.width = getColumnGapLeft(); + dc.GradientFillLinear(rectTmp, backColor, wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHT), wxEAST); + } + } + if (!selected && static_cast(rowHover) == HoverAreaConfig::name) + drawRectangleBorder(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHT), dipToWxsize(1)); + + //------------------------------------------------------------------------------------- + wxRect rectTmp = rect; + rectTmp.x += getColumnGapLeft(); + rectTmp.width -= getColumnGapLeft(); + + const wxImage cfgIcon = [&] + { + switch (item->cfgType) + { + case ConfigView::Details::CFG_TYPE_NONE: + return wxNullImage; + case ConfigView::Details::CFG_TYPE_GUI: + return loadImage("start_sync", dipToScreen(getMenuIconDipSize())); + case ConfigView::Details::CFG_TYPE_BATCH: + return loadImage("cfg_batch", dipToScreen(getMenuIconDipSize())); + } + assert(false); + return wxNullImage; + }(); + if (cfgIcon.IsOk()) + drawBitmapRtlNoMirror(dc, enabled ? cfgIcon : cfgIcon.ConvertToDisabled(), rectTmp, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); + + rectTmp.x += dipToWxsize(getMenuIconDipSize()) + getColumnGapLeft(); + rectTmp.width -= dipToWxsize(getMenuIconDipSize()) + getColumnGapLeft(); + + if (!item->notes.empty()) + rectTmp.width -= dipToWxsize(getMenuIconDipSize()) + getColumnGapLeft(); + + drawCellText(dc, rectTmp, getValue(row, colType)); + + if (!item->notes.empty()) + { + rectTmp.x += rectTmp.width; + rectTmp.width = dipToWxsize(getMenuIconDipSize()); + + const wxImage notesIcon = loadImage("notes", dipToScreen(getMenuIconDipSize())); + drawBitmapRtlNoMirror(dc, enabled ? notesIcon : notesIcon.ConvertToDisabled(), rectTmp, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); + } + } + break; + + case ColumnTypeCfg::lastSync: + { + wxDCTextColourChanger textColor2(dc); + if (syncOverdueDays_ > 0) + if (getDaysPast(item->cfgItem.lastRunStats.startTime) >= syncOverdueDays_) + textColor2.Set(*wxRED); //text barely readable when selected, for 4.5 contrast would need to be white :( + + drawCellText(dc, rect, getValue(row, colType), wxALIGN_CENTER); + } + break; + + case ColumnTypeCfg::lastLog: + if (!item->isLastRunCfg && !AFS::isNullPath(item->cfgItem.lastRunStats.logFilePath)) + { + const wxImage statusIcon = [&] + { + switch (item->cfgItem.lastRunStats.syncResult) + { + case TaskResult::success: + return loadImage("msg_success", dipToScreen(getMenuIconDipSize())); + case TaskResult::warning: + return loadImage("msg_warning", dipToScreen(getMenuIconDipSize())); + case TaskResult::error: + case TaskResult::cancelled: + return loadImage("msg_error", dipToScreen(getMenuIconDipSize())); + } + assert(false); + return wxNullImage; + }(); + drawBitmapRtlNoMirror(dc, enabled ? statusIcon : statusIcon.ConvertToDisabled(), rect, wxALIGN_CENTER); + } + if (static_cast(rowHover) == HoverAreaConfig::link) + drawBitmapRtlNoMirror(dc, loadImage("file_link_16"), rect, wxALIGN_CENTER); + break; + } + } + + int getBestSize(const wxReadOnlyDC& dc, size_t row, ColumnType colType) override + { + // -> synchronize renderCell() <-> getBestSize() + + switch (static_cast(colType)) + { + case ColumnTypeCfg::name: + return getColumnGapLeft() + dipToWxsize(getMenuIconDipSize()) + getColumnGapLeft() + dc.GetTextExtent(getValue(row, colType)).GetWidth() + getColumnGapLeft(); + + case ColumnTypeCfg::lastSync: + return getColumnGapLeft() + dc.GetTextExtent(getValue(row, colType)).GetWidth() + getColumnGapLeft(); + + case ColumnTypeCfg::lastLog: + return dipToWxsize(getMenuIconDipSize()); + } + assert(false); + return 0; + } + + HoverArea getMouseHover(const wxReadOnlyDC& dc, size_t row, ColumnType colType, int cellRelativePosX, int cellWidth) override + { + if (const ConfigView::Details* item = cfgView_.getItem(row)) + { + switch (static_cast(colType)) + { + case ColumnTypeCfg::name: + case ColumnTypeCfg::lastSync: + //if (!item->notes.empty() && cellRelativePosX >= cellWidth - (getColumnGapLeft() + dipToWxsize(getMenuIconDipSize()) + getColumnGapLeft())) + break; + case ColumnTypeCfg::lastLog: + if (!item->isLastRunCfg && !getNativeItemPath(item->cfgItem.lastRunStats.logFilePath).empty()) + return static_cast(HoverAreaConfig::link); + break; + } + return static_cast(HoverAreaConfig::name); + } + return HoverArea::none; + } + + void renderColumnLabel(wxDC& dc, const wxRect& rect, ColumnType colType, bool enabled, bool highlighted) override + { + const auto colTypeCfg = static_cast(colType); + + const wxRect rectInner = drawColumnLabelBackground(dc, rect, highlighted); + wxRect rectRemain = rectInner; + + wxImage sortMarker; + if (const auto [sortCol, ascending] = cfgView_.getSortDirection(); + colTypeCfg == sortCol) + { + sortMarker = loadImage(ascending ? "sort_ascending" : "sort_descending"); + if (!enabled) + sortMarker = sortMarker.ConvertToDisabled(); + } + + switch (colTypeCfg) + { + case ColumnTypeCfg::name: + case ColumnTypeCfg::lastSync: + rectRemain.x += getColumnGapLeft(); + rectRemain.width -= getColumnGapLeft(); + drawColumnLabelText(dc, rectRemain, getColumnLabel(colType), enabled); + + if (sortMarker.IsOk()) + drawBitmapRtlNoMirror(dc, sortMarker, rectInner, wxALIGN_CENTER_HORIZONTAL); + break; + + case ColumnTypeCfg::lastLog: + { + const wxImage logIcon = loadImage("log_file", dipToScreen(getMenuIconDipSize())); + drawBitmapRtlNoMirror(dc, enabled ? logIcon : logIcon.ConvertToDisabled(), rectInner, wxALIGN_CENTER); + + if (sortMarker.IsOk()) + { + const int gapLeft = (rectInner.width + logIcon.GetWidth()) / 2; + rectRemain.x += gapLeft; + rectRemain.width -= gapLeft; + + drawBitmapRtlNoMirror(dc, sortMarker, rectRemain, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); + } + } + break; + } + } + + std::wstring getColumnLabel(ColumnType colType) const override + { + switch (static_cast(colType)) + { + case ColumnTypeCfg::name: + return _("Name"); + case ColumnTypeCfg::lastSync: + return _("Last sync"); + case ColumnTypeCfg::lastLog: + return _("Log"); + } + return std::wstring(); + } + + std::wstring getToolTip(ColumnType colType) const override + { + switch (static_cast(colType)) + { + case ColumnTypeCfg::name: + case ColumnTypeCfg::lastSync: + break; + case ColumnTypeCfg::lastLog: + return getColumnLabel(colType); + } + return std::wstring(); + } + + std::wstring getToolTip(size_t row, ColumnType colType, HoverArea rowHover) override + { + if (const ConfigView::Details* item = cfgView_.getItem(row)) + { + switch (static_cast(colType)) + { + case ColumnTypeCfg::name: + case ColumnTypeCfg::lastSync: + break; + + case ColumnTypeCfg::lastLog: + if (!item->isLastRunCfg && !AFS::isNullPath(item->cfgItem.lastRunStats.logFilePath)) + { + std::wstring tooltip = getSyncResultLabel(item->cfgItem.lastRunStats.syncResult) + L"\n"; + + if (item->cfgItem.lastRunStats.errors > 0) tooltip += TAB_SPACE + _("Errors:") + L' ' + formatNumber(item->cfgItem.lastRunStats.errors) + L"\n"; + if (item->cfgItem.lastRunStats.warnings > 0) tooltip += TAB_SPACE + _("Warnings:") + L' ' + formatNumber(item->cfgItem.lastRunStats.warnings) + L"\n"; + + tooltip += TAB_SPACE + _("Items processed:") + L' ' + formatNumber(item->cfgItem.lastRunStats.itemsProcessed) + + L" (" + formatFilesizeShort(item->cfgItem.lastRunStats.bytesProcessed) + L")\n"; + + const int64_t totalTimeSec = std::chrono::duration_cast(item->cfgItem.lastRunStats.totalTime).count(); + tooltip += TAB_SPACE + _("Total time:") + L' ' + utfTo(formatTimeSpan(totalTimeSec)); + + //non-native path won't be clickable => at least show in tooltip: + if (getNativeItemPath(item->cfgItem.lastRunStats.logFilePath).empty()) + tooltip += L"\n" + AFS::getDisplayPath(item->cfgItem.lastRunStats.logFilePath); + + return tooltip; + } + break; + } + return item->notes; + } + return std::wstring(); + } + + void onMouseLeft(GridClickEvent& event) + { + if (const ConfigView::Details* item = cfgView_.getItem(event.row_)) + switch (static_cast(event.hoverArea_)) + { + case HoverAreaConfig::name: + break; + case HoverAreaConfig::link: + try + { + if (const Zstring& nativePath = getNativeItemPath(item->cfgItem.lastRunStats.logFilePath); + !nativePath.empty()) + openWithDefaultApp(nativePath); //throw FileError + else + assert(false); //see getMouseHover() + } + catch (const FileError& e) { showNotificationDialog(&grid_, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); } + return; + } + event.Skip(); + } + + void onMouseLeftDouble(GridClickEvent& event) + { + switch (static_cast(event.hoverArea_)) + { + case HoverAreaConfig::name: + break; + case HoverAreaConfig::link: + return; //swallow event here before MainDialog considers it as a request to start comparison + } + event.Skip(); + } + +private: + Grid& grid_; + ConfigView cfgView_; + int syncOverdueDays_ = 0; +}; +} + + +void cfggrid::init(Grid& grid) +{ + const int rowHeight = GridDataCfg::getRowDefaultHeight(grid); + + grid.setDataProvider(std::make_shared(grid)); + grid.showRowLabel(false); + grid.setRowHeight(rowHeight); + grid.setColumnLabelHeight(rowHeight + dipToWxsize(2)); +} + + +ConfigView& cfggrid::getDataView(Grid& grid) +{ + if (auto* prov = dynamic_cast(grid.getDataProvider())) + return prov->getDataView(); + throw std::runtime_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] cfggrid was not initialized."); +} + + +void cfggrid::addAndSelect(Grid& grid, const std::vector& filePaths, bool scrollToSelection) +{ + getDataView(grid).addCfgFiles(filePaths); + grid.Refresh(); //[!] let Grid know about changed row count *before* fiddling with selection!!! + + const std::set pathsSorted(filePaths.begin(), filePaths.end()); + std::vector rowsToSelect; + + for (size_t row = 0; row < grid.getRowCount(); ++row) + if (pathsSorted.contains(getDataView(grid).getItem(row)->cfgItem.cfgFilePath)) + rowsToSelect.push_back(row); + + if (scrollToSelection && !rowsToSelect.empty()) + grid.makeRowVisible(rowsToSelect[0]); //don't also set grid cursor: will confuse keyboard selection using shift and arrow keys + + grid.clearSelection(GridEventPolicy::deny); + + for (size_t row : rowsToSelect) + grid.selectRow(row, GridEventPolicy::deny); +} + + +int cfggrid::getSyncOverdueDays(Grid& grid) +{ + if (auto* prov = dynamic_cast(grid.getDataProvider())) + return prov->getSyncOverdueDays(); + throw std::runtime_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] cfggrid was not initialized."); +} + + +void cfggrid::setSyncOverdueDays(Grid& grid, int syncOverdueDays) +{ + auto* prov = dynamic_cast(grid.getDataProvider()); + if (!prov) + throw std::runtime_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] cfggrid was not initialized."); + + prov->setSyncOverdueDays(syncOverdueDays); + grid.Refresh(); +} diff --git a/FreeFileSync/Source/ui/cfg_grid.h b/FreeFileSync/Source/ui/cfg_grid.h new file mode 100644 index 0000000..4a75760 --- /dev/null +++ b/FreeFileSync/Source/ui/cfg_grid.h @@ -0,0 +1,172 @@ +// ***************************************************************************** +// * 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 CONFIG_HISTORY_3248789479826359832 +#define CONFIG_HISTORY_3248789479826359832 + +#include +#include +#include +#include "../return_codes.h" +#include "../afs/concrete.h" + + +namespace fff +{ +struct LastRunStats +{ + time_t startTime = 0; //may be updated separately from log file, e.g. "nothing to sync" after comparison + //---------------------------------------------- + AbstractPath logFilePath = getNullPath(); //optional: available <=> sync took place + TaskResult syncResult = TaskResult::cancelled; + int itemsProcessed = -1; + int64_t bytesProcessed = -1; + std::chrono::milliseconds totalTime{}; + int errors = -1; + int warnings = -1; +}; + +struct ConfigFileItem +{ + ConfigFileItem() {} + ConfigFileItem(const Zstring& filePath, + const LastRunStats& runStats, + wxColor bcol) : + cfgFilePath(filePath), + lastRunStats(runStats), + backColor(bcol) {} + + Zstring cfgFilePath; + LastRunStats lastRunStats; + wxColor backColor; + wxColor backColorPreview; //while the folder picker is shown +}; + + +enum class ColumnTypeCfg +{ + name, + lastSync, + lastLog, +}; + +struct ColAttributesCfg +{ + ColumnTypeCfg type = ColumnTypeCfg::name; + int offset = 0; + int stretch = 0; + bool visible = false; +}; + +inline +std::vector getCfgGridDefaultColAttribs() +{ + using namespace zen; + return + { + {ColumnTypeCfg::name, -dipToWxsize(75) - dipToWxsize(42), 1, true}, + {ColumnTypeCfg::lastSync, dipToWxsize(75), 0, true}, + {ColumnTypeCfg::lastLog, dipToWxsize(42), 0, true}, //leave some room for the sort direction indicator + }; +} + +const ColumnTypeCfg cfgGridLastSortColumnDefault = ColumnTypeCfg::name; + +inline +bool getDefaultSortDirection(ColumnTypeCfg colType) +{ + switch (colType) + { + case ColumnTypeCfg::name: + return true; + case ColumnTypeCfg::lastSync: //actual sort order is "time since last sync" + return false; + case ColumnTypeCfg::lastLog: + return true; + } + assert(false); + return true; +} +//--------------------------------------------------------------------------------------------------------------------- +Zstring getLastRunConfigPath(); + + +class ConfigView +{ +public: + ConfigView() {} + + std::vector get() const; + void set(const std::vector& cfgItems); + + void addCfgFiles(const std::vector& filePaths); + void removeItems(const std::vector& filePaths); + void renameItem(const Zstring& pathFrom, const Zstring& pathTo); + + void setNotes(const Zstring& filePath, const std::wstring& notes); + void setLastRunStats(const std::vector& filePaths, const LastRunStats& lastRun); + void setLastInSyncTime(const std::vector& filePaths, time_t lastRunTime); + void setBackColor(const std::vector& filePaths, const wxColor& col, bool previewOnly = false); + + struct Details + { + ConfigFileItem cfgItem; + + Zstring name; + int lastUseIndex = 0; //support truncating the config list size via last usage, the higher the index the more recent the usage + bool isLastRunCfg = false; //LastRun.ffs_gui + std::wstring notes; + + enum ConfigType + { + CFG_TYPE_NONE, + CFG_TYPE_GUI, + CFG_TYPE_BATCH, + } cfgType = CFG_TYPE_NONE; + }; + + const Details* getItem(size_t row) const; + std::pair getItem(const Zstring& filePath) const; + + size_t getRowCount() const { assert(cfgList_.size() == cfgListView_.size()); return cfgListView_.size(); } + + void setSortDirection(ColumnTypeCfg colType, bool ascending); + std::pair getSortDirection() { return {sortColumn_, sortAscending_}; } + +private: + ConfigView (const ConfigView&) = delete; + ConfigView& operator=(const ConfigView&) = delete; + + void addCfgFilesImpl(const std::vector& filePaths); + + void sortListView(); + template void sortListViewImpl(); + + const Zstring lastRunConfigPath_ = getLastRunConfigPath(); //let's not use another static... + + using CfgFileList = std::map; + + CfgFileList cfgList_; + std::vector cfgListView_; //sorted view on cfgList_ + + ColumnTypeCfg sortColumn_ = cfgGridLastSortColumnDefault; + bool sortAscending_ = getDefaultSortDirection(cfgGridLastSortColumnDefault); +}; + + +namespace cfggrid +{ +void init(zen::Grid& grid); +ConfigView& getDataView(zen::Grid& grid); //grid.Refresh() after making changes! + +void addAndSelect(zen::Grid& grid, const std::vector& filePaths, bool scrollToSelection); + +int getSyncOverdueDays(zen::Grid& grid); +void setSyncOverdueDays(zen::Grid& grid, int syncOverdueDays); +} +} + +#endif //CONFIG_HISTORY_3248789479826359832 diff --git a/FreeFileSync/Source/ui/command_box.cpp b/FreeFileSync/Source/ui/command_box.cpp new file mode 100644 index 0000000..3768f2a --- /dev/null +++ b/FreeFileSync/Source/ui/command_box.cpp @@ -0,0 +1,202 @@ +// ***************************************************************************** +// * 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 "command_box.h" +#include +#include +#include + +using namespace zen; +using namespace fff; + + +namespace +{ +inline +wxString getSeparationLine() { return std::wstring(50, EM_DASH); } //no space between dashes! +} + + +CommandBox::CommandBox(wxWindow* parent, + wxWindowID id, + const wxString& value, + const wxPoint& pos, + const wxSize& size, + int n, + const wxString choices[], + long style, + const wxValidator& validator, + const wxString& name) : + wxComboBox(parent, id, value, pos, size, n, choices, style, validator, name) +{ + //#################################### + /*#*/ SetMinSize({dipToWxsize(150), -1}); //# workaround yet another wxWidgets bug: default minimum size is much too large for a wxComboBox + //#################################### + + Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onKeyEvent (event); }); + Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent& event) { onUpdateList(event); }); + Bind(wxEVT_COMMAND_COMBOBOX_SELECTED, [this](wxCommandEvent& event) { onSelection (event); }); + Bind(wxEVT_MOUSEWHEEL, [] (wxMouseEvent& event) {}); //swallow! this gives confusing UI feedback anyway +} + + +void CommandBox::addItemHistory() +{ + const Zstring newCommand = trimCpy(getValue()); + + if (newCommand == utfTo(getSeparationLine()) || //do not add sep. line + newCommand.empty()) + return; + + //do not add built-in commands to history + for (const auto& [description, cmd] : defaultCommands_) + if (newCommand == utfTo(description) || + equalNoCase(newCommand, cmd)) + return; + + std::erase_if(history_, [&](const Zstring& item) { return equalNoCase(newCommand, item); }); + + history_.insert(history_.begin(), newCommand); + + if (history_.size() > historyMax_) + history_.resize(historyMax_); +} + + +Zstring CommandBox::getValue() const +{ + return utfTo(trimCpy(GetValue())); +} + + +void CommandBox::setValue(const Zstring& value) +{ + setValueAndUpdateList(trimCpy(utfTo(value))); +} + + +//set value and update list are technically entangled: see potential bug description below +void CommandBox::setValueAndUpdateList(const wxString& value) +{ + //it may be a little lame to update the list on each mouse-button click, but it should be working and we dont't have to manipulate wxComboBox internals + + std::vector items; + + //1. built in commands + for (const auto& [description, cmd] : defaultCommands_) + items.push_back(description); + + //2. history elements + auto histSorted = history_; + std::sort(histSorted.begin(), histSorted.end(), LessNaturalSort() /*even on Linux*/); + + if (!items.empty() && !histSorted.empty()) + items.push_back(getSeparationLine()); + + for (const Zstring& hist : histSorted) + items.push_back(utfTo(hist)); + + //attention: if the target value is not part of the dropdown list, SetValue() will look for a string that *starts with* this value: + //e.g. if the dropdown list contains "222" SetValue("22") will erroneously set and select "222" instead, while "111" would be set correctly! + // -> by design on Windows! + if (std::find(items.begin(), items.end(), value) == items.end()) + { + if (!items.empty() && !value.empty()) + items.insert(items.begin(), {value, getSeparationLine()}); + else + items.insert(items.begin(), {value}); + } + + //this->Clear(); -> NO! emits yet another wxEVT_COMMAND_TEXT_UPDATED!!! + wxItemContainer::Clear(); //suffices to clear the selection items only! + this->Append(items); //expensive as fuck! => only call when absolutely needed! + + //this->SetSelection(wxNOT_FOUND); //don't select anything + ChangeValue(value); //preserve main text! +} + + +void CommandBox::onSelection(wxCommandEvent& event) +{ + //we cannot replace built-in commands at this position in call stack, so defer to a later time! + CallAfter([&] { onValidateSelection(); }); + + event.Skip(); +} + + +void CommandBox::onValidateSelection() +{ + const wxString value = GetValue(); + + if (value == getSeparationLine()) + return setValueAndUpdateList(wxString()); + + for (const auto& [description, cmd] : defaultCommands_) + if (description == value) + return setValueAndUpdateList(utfTo(cmd)); //replace GUI name by actual command string +} + + +void CommandBox::onUpdateList(wxEvent& event) +{ + setValue(getValue()); + event.Skip(); +} + + +void CommandBox::onKeyEvent(wxKeyEvent& event) +{ + const int keyCode = event.GetKeyCode(); + + switch (keyCode) + { + case WXK_DELETE: + case WXK_NUMPAD_DELETE: + { + //try to delete the currently selected config history item + int pos = this->GetCurrentSelection(); + if (0 <= pos && pos < static_cast(this->GetCount()) && + //what a mess...: + (GetValue() != GetString(pos) || //avoid problems when a character shall be deleted instead of list item + GetValue().empty())) //exception: always allow removing empty entry + { + const auto selValue = utfTo(GetString(pos)); + + if (std::find(history_.begin(), history_.end(), selValue) != history_.end()) //only history elements may be deleted + { + //save old (selected) value: deletion seems to have influence on this + const wxString currentVal = this->GetValue(); + //this->SetSelection(wxNOT_FOUND); + + //delete selected row + std::erase(history_, selValue); + + SetString(pos, wxString()); //in contrast to Delete(), this one does not kill the drop-down list and gives a nice visual feedback! + //Delete(pos); + + //(re-)set value + SetValue(currentVal); + } + return; //eat up key event + } + } + break; + + case WXK_UP: + case WXK_NUMPAD_UP: + case WXK_DOWN: + case WXK_NUMPAD_DOWN: + case WXK_PAGEUP: + case WXK_NUMPAD_PAGEUP: + case WXK_PAGEDOWN: + case WXK_NUMPAD_PAGEDOWN: + return; //swallow -> using these keys gives a weird effect due to this weird control + } + + + event.Skip(); +} diff --git a/FreeFileSync/Source/ui/command_box.h b/FreeFileSync/Source/ui/command_box.h new file mode 100644 index 0000000..c3431f8 --- /dev/null +++ b/FreeFileSync/Source/ui/command_box.h @@ -0,0 +1,56 @@ +// ***************************************************************************** +// * 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 COMMAND_BOX_H_18947773210473214 +#define COMMAND_BOX_H_18947773210473214 + +#include +#include +#include + + +//combobox with history function + functionality to delete items (DEL) +namespace fff +{ +class CommandBox : public wxComboBox +{ +public: + CommandBox(wxWindow* parent, + wxWindowID id, + const wxString& value = {}, + const wxPoint& pos = wxDefaultPosition, + const wxSize& size = wxDefaultSize, + int n = 0, + const wxString choices[] = nullptr, + long style = 0, + const wxValidator& validator = wxDefaultValidator, + const wxString& name = wxASCII_STR(wxComboBoxNameStr)); + + void setHistory(const std::vector& history, size_t historyMax) { history_ = history; historyMax_ = historyMax; } + std::vector getHistory() const { return history_; } + void addItemHistory(); //adds current item to history + + // use these two accessors instead of GetValue()/SetValue(): + Zstring getValue() const; + void setValue(const Zstring& value); + //required for setting value correctly + Linux to ensure the dropdown is shown as being populated + +private: + void onKeyEvent(wxKeyEvent& event); + void onSelection(wxCommandEvent& event); + void onValidateSelection(); + void onUpdateList(wxEvent& event); + + void setValueAndUpdateList(const wxString& value); + + std::vector history_; + size_t historyMax_ = 0; + + const std::vector> defaultCommands_; //(description/command) pairs +}; +} + +#endif //COMMAND_BOX_H_18947773210473214 diff --git a/FreeFileSync/Source/ui/file_grid.cpp b/FreeFileSync/Source/ui/file_grid.cpp new file mode 100644 index 0000000..4ac9ded --- /dev/null +++ b/FreeFileSync/Source/ui/file_grid.cpp @@ -0,0 +1,2309 @@ +// ***************************************************************************** +// * 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 "file_grid.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../base/file_hierarchy.h" + +using namespace zen; +using namespace fff; + + +namespace fff +{ +wxDEFINE_EVENT(EVENT_GRID_CHECK_ROWS, CheckRowsEvent); +wxDEFINE_EVENT(EVENT_GRID_SYNC_DIRECTION, SyncDirectionEvent); +} + + +namespace +{ +//let's NOT create wxWidgets objects statically: +wxColor getColorSyncBlue(bool faint) +{ + if (faint) return {0xed, 0xee, 0xff}; //faint blue + + return wxSystemSettings::GetAppearance().IsDark() ? wxColor{0x80, 0x94, 0xfe} /*medium blue*/ : + wxColor{0xb9, 0xbc, 0xff} /*light blue*/; +} + +wxColor getColorSyncGreen(bool faint) +{ + if (faint) + return {0xf1, 0xff, 0xed}; //faint green + + return wxSystemSettings::GetAppearance().IsDark() ? wxColor{0x6c, 0xfb, 0x53} /*medium green*/ : + wxColor{0xc4, 0xff, 0xb9} /*light green*/; +} + +wxColor getColorConflictBackground (bool faint) { if (faint) return {0xfe, 0xfe, 0xda}; return {247, 252, 62}; } //yellow +wxColor getColorDifferentBackground(bool faint) { if (faint) return {0xff, 0xed, 0xee}; return {255, 185, 187}; } //red + +wxColor getColorSymlinkBackground() { return {238, 201, 0}; } //orange + +wxColor getColorInactiveBack() { return wxSystemSettings::GetAppearance().IsDark() ? 0x6c6c6c /*medium grey*/ : 0xe4e4e4 /*light grey*/; } +wxColor getColorInactiveText() { return wxSystemSettings::GetAppearance().IsDark() ? 0xffffff /*white*/ : 0x404040 /*dark grey*/; } + +wxColor getColorGridLine() { return wxSystemSettings::GetColour(wxSYS_COLOUR_BTNSHADOW); } + +const int FILE_GRID_GAP_SIZE_DIP = 2; +const int FILE_GRID_GAP_SIZE_WIDE_DIP = 6; + +/* class hierarchy: GridDataBase + /|\ + ___________|____________ + | | + GridDataRim | + /|\ | + ______|_______ | + | | | + GridDataLeft GridDataRight GridDataCenter */ + + +//accessibility, support high-contrast schemes => work with user-defined background color! +wxColor getGridAlternateBackgroundColor() +{ + const wxColor backCol = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); + /* CAVEAT: macOS uses partially-transparent colors! but probably not for this one: + wxSYS_COLOUR_WINDOW RGBA = #171717FF */ + + const bool isColorLight = relativeLuminance(backCol) > 0.5; + + //darken or brighten: only a faint gradient to avoid visual distraction + auto liftChannel = [diff = isColorLight ? -15 : 15](unsigned char c) { return static_cast(std::clamp(c + diff, 0, 255)); }; + + return wxColor(liftChannel(backCol.Red ()), + liftChannel(backCol.Green()), + liftChannel(backCol.Blue ())); +} + + +std::pair getCudAction(SyncOperation so) +{ + switch (so) + { + case SO_CREATE_LEFT: + case SO_MOVE_LEFT_TO: return {CudAction::create, SelectSide::left}; + + case SO_CREATE_RIGHT: + case SO_MOVE_RIGHT_TO: return {CudAction::create, SelectSide::right}; + + case SO_DELETE_LEFT: + case SO_MOVE_LEFT_FROM: return {CudAction::delete_, SelectSide::left}; + + case SO_DELETE_RIGHT: + case SO_MOVE_RIGHT_FROM: return {CudAction::delete_, SelectSide::right}; + + case SO_OVERWRITE_LEFT: + case SO_RENAME_LEFT: return {CudAction::update, SelectSide::left}; + + case SO_OVERWRITE_RIGHT: + case SO_RENAME_RIGHT: return {CudAction::update, SelectSide::right}; + + case SO_DO_NOTHING: + case SO_EQUAL: + case SO_UNRESOLVED_CONFLICT: return {CudAction::noChange, SelectSide::left}; + } + assert(false); + return {CudAction::noChange, SelectSide::left}; +} + + +wxColor getBackGroundColorSyncAction(SyncOperation so) +{ + switch (so) + { + case SO_CREATE_LEFT: + case SO_OVERWRITE_LEFT: + case SO_DELETE_LEFT: + case SO_MOVE_LEFT_FROM: + case SO_MOVE_LEFT_TO: + case SO_RENAME_LEFT: + return getColorSyncBlue(false /*faint*/); + + case SO_CREATE_RIGHT: + case SO_OVERWRITE_RIGHT: + case SO_DELETE_RIGHT: + case SO_MOVE_RIGHT_FROM: + case SO_MOVE_RIGHT_TO: + case SO_RENAME_RIGHT: + return getColorSyncGreen(false /*faint*/); + + case SO_DO_NOTHING: + return getColorInactiveBack(); + case SO_EQUAL: + break; //usually white + case SO_UNRESOLVED_CONFLICT: + return getColorConflictBackground(false /*faint*/); + } + return wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); +} + + +wxColor getBackGroundColorCmpDifference(CompareFileResult cmpResult) +{ + switch (cmpResult) + { + case FILE_EQUAL: + break; //usually white + case FILE_LEFT_ONLY: return getColorSyncBlue(false /*faint*/); + case FILE_LEFT_NEWER: return getColorSyncBlue(true /*faint*/); + + case FILE_RIGHT_ONLY: return getColorSyncGreen(false /*faint*/); + case FILE_RIGHT_NEWER: return getColorSyncGreen(true /*faint*/); + + case FILE_DIFFERENT_CONTENT: + return getColorDifferentBackground(false /*faint*/); + case FILE_RENAMED: //similar to both "equal" and "conflict": give hint via background color + case FILE_TIME_INVALID: + case FILE_CONFLICT: + return getColorConflictBackground(false /*faint*/); + } + return wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); +} + + +class GridEventManager; +class GridDataLeft; +class GridDataRight; + +class IconUpdater : private wxEvtHandler //update file icons periodically: use SINGLE instance to coordinate left and right grids in parallel +{ +public: + IconUpdater(GridDataLeft& provLeft, GridDataRight& provRight, IconBuffer& iconBuffer) : provLeft_(provLeft), provRight_(provRight), iconBuffer_(iconBuffer) + { + timer_.Bind(wxEVT_TIMER, [this](wxTimerEvent& event) { loadIconsAsynchronously(event); }); + } + + void start() { if (!timer_.IsRunning()) timer_.Start(100); } //timer interval in [ms] + //don't check too often! give worker thread some time to fetch data + +private: + void stop() { if (timer_.IsRunning()) timer_.Stop(); } + + void loadIconsAsynchronously(wxEvent& event); //loads all (not yet) drawn icons + + GridDataLeft& provLeft_; + GridDataRight& provRight_; + IconBuffer& iconBuffer_; + wxTimer timer_; +}; + + +struct IconManager +{ + IconManager() {} + + IconManager(GridDataLeft& provLeft, GridDataRight& provRight, IconBuffer::IconSize sz, bool showFileIcons) : + fileIcon_ (IconBuffer::genericFileIcon (showFileIcons ? sz : IconBuffer::IconSize::small)), + dirIcon_ (IconBuffer::genericDirIcon (showFileIcons ? sz : IconBuffer::IconSize::small)), + linkOverlayIcon_ (IconBuffer::linkOverlayIcon (showFileIcons ? sz : IconBuffer::IconSize::small)), + plusOverlayIcon_ (IconBuffer::plusOverlayIcon (showFileIcons ? sz : IconBuffer::IconSize::small)), + minusOverlayIcon_(IconBuffer::minusOverlayIcon(showFileIcons ? sz : IconBuffer::IconSize::small)) + { + if (showFileIcons) + { + iconBuffer_ .emplace(sz); + iconUpdater_.emplace(provLeft, provRight, *iconBuffer_); + } + } + + int getIconWxsize() const { return screenToWxsize(iconBuffer_ ? iconBuffer_->getPixSize() : IconBuffer::getPixSize(IconBuffer::IconSize::small)); } + + IconBuffer* getIconBuffer() { return get(iconBuffer_); } + void startIconUpdater() { assert(iconUpdater_); if (iconUpdater_) iconUpdater_->start(); } + + const wxImage& getGenericFileIcon () const { return fileIcon_; } + const wxImage& getGenericDirIcon () const { return dirIcon_; } + const wxImage& getLinkOverlayIcon () const { return linkOverlayIcon_; } + const wxImage& getPlusOverlayIcon () const { return plusOverlayIcon_; } + const wxImage& getMinusOverlayIcon() const { return minusOverlayIcon_; } + +private: + const wxImage fileIcon_; + const wxImage dirIcon_; + const wxImage linkOverlayIcon_; + const wxImage plusOverlayIcon_; + const wxImage minusOverlayIcon_; + + std::optional iconBuffer_; + std::optional iconUpdater_; //bind ownership to GridDataRim<>! +}; + + +//mark rows selected on overview panel +class NavigationMarker +{ +public: + NavigationMarker() {} + + void set(std::unordered_set&& markedFilesAndLinks, + std::unordered_set&& markedContainer) + { + markedFilesAndLinks_.swap(markedFilesAndLinks); + markedContainer_ .swap(markedContainer); + } + + bool isMarked(const FileSystemObject& fsObj) const + { + if (markedFilesAndLinks_.contains(&fsObj)) //mark files/links directly + return true; + + if (auto folder = dynamic_cast(&fsObj)) + if (markedContainer_.contains(folder)) //mark folders which *are* the given ContainerObject* + return true; + + //also mark all items with any matching ancestors + for (const FileSystemObject* fsObj2 = &fsObj;;) + { + const ContainerObject& parent = fsObj2->parent(); + if (markedContainer_.contains(&parent)) + return true; + + fsObj2 = dynamic_cast(&parent); + if (!fsObj2) + return false; + } + } + +private: + std::unordered_set markedFilesAndLinks_; //mark files/symlinks directly within a container + std::unordered_set markedContainer_; //mark full container including all child-objects + //DO NOT DEREFERENCE!!!! NOT GUARANTEED TO BE VALID!!! +}; + + +struct SharedComponents //...between left, center, and right grids +{ + SharedRef gridDataView = makeSharedRef(); + SharedRef iconMgr = makeSharedRef(); + NavigationMarker navMarker; + std::unique_ptr evtMgr; + GridViewType gridViewType = GridViewType::action; + std::unordered_map compExtentsBuf_; //buffer expensive wxDC::GetTextExtent() calls! + //StringHash, StringEqual => heterogenous lookup by std::wstring_view +}; + +//######################################################################################################## + +class GridDataBase : public GridData +{ +public: + GridDataBase(Grid& grid, const SharedRef& sharedComp) : + grid_(grid), sharedComp_(sharedComp) {} + + void setData(FolderComparison& folderCmp) + { + sharedComp_.ref().gridDataView = makeSharedRef(); //clear old data view first! avoid memory peaks! + sharedComp_.ref().gridDataView = makeSharedRef(folderCmp); + sharedComp_.ref().compExtentsBuf_.clear(); //doesn't become stale! but still: re-calculate and save some memory... + } + + GridEventManager* getEventManager() { return sharedComp_.ref().evtMgr.get(); } + + /**/ FileView& getDataView() { return sharedComp_.ref().gridDataView.ref(); } + const FileView& getDataView() const { return sharedComp_.ref().gridDataView.ref(); } + + void setIconManager(const SharedRef& iconMgr) { sharedComp_.ref().iconMgr = iconMgr; } + + IconManager& getIconManager() { return sharedComp_.ref().iconMgr.ref(); } + + GridViewType getViewType() const { return sharedComp_.ref().gridViewType; } + void setViewType(GridViewType vt) { sharedComp_.ref().gridViewType = vt; } + + bool isNavMarked(const FileSystemObject& fsObj) const { return sharedComp_.ref().navMarker.isMarked(fsObj); } + + void setNavigationMarker(std::unordered_set&& markedFilesAndLinks, + std::unordered_set&& markedContainer) + { + sharedComp_.ref().navMarker.set(std::move(markedFilesAndLinks), std::move(markedContainer)); + } + + Grid& refGrid() { return grid_; } + const Grid& refGrid() const { return grid_; } + + const FileSystemObject* getFsObject(size_t row) const { return getDataView().getFsObject(row); } + + const wxSize& getTextExtentBuffered(const wxReadOnlyDC& dc, const std::wstring_view& text) + { + auto& compExtentsBuf = sharedComp_.ref().compExtentsBuf_; + //- only used for parent path names and file names on view => should not grow "too big" + //- cleaned up during GridDataBase::setData() + assert(!contains(text, L'\n')); + + auto it = compExtentsBuf.find(text); + if (it == compExtentsBuf.end()) + it = compExtentsBuf.emplace(text, dc.GetTextExtent(copyStringTo(text))).first; + //GetTextExtent() returns (0, 0) for empty string! + return it->second; + } + + //- trim while leaving path components intact + //- *always* returns at least one component, even if > maxWidth + size_t getPathTrimmedSize(const wxReadOnlyDC& dc, const std::wstring_view& itemPath, int maxWidth) + { + if (itemPath.size() <= 1) + return itemPath.size(); + + std::vector subComp; + + //split path by components, but skip slash at beginning or end + for (auto it = itemPath.begin() + 1; it != itemPath.end() - 1; ++it) + if (*it == L'/' || + *it == L'\\') + subComp.push_back(makeStringView(itemPath.begin(), it)); + + subComp.push_back(itemPath); + + if (maxWidth <= 0) + return subComp[0].size(); + + size_t low = 0; + size_t high = subComp.size(); + + for (;;) + { + if (high - low == 1) + return subComp[low].size(); + + const size_t middle = (low + high) / 2; //=> never 0 when "high - low > 1" + + if (getTextExtentBuffered(dc, subComp[middle]).GetWidth() <= maxWidth) + low = middle; + else + high = middle; + } + } + + //improve readability (while lacking cell borders) + const wxColor& getDefaultBackgroundColorAlternating(bool wantStandardColor) + { + return wantStandardColor ? gridBackgroundColor_ : gridBackgroundColorAlt_; + } + +private: + size_t getRowCount() const override { return getDataView().rowsOnView(); } + + const wxColor gridBackgroundColor_ = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); + const wxColor gridBackgroundColorAlt_ = getGridAlternateBackgroundColor(); + + Grid& grid_; + SharedRef sharedComp_; +}; + +//######################################################################################################## + +template +class GridDataRim : public GridDataBase +{ +public: + GridDataRim(Grid& grid, const SharedRef& sharedComp) : GridDataBase(grid, sharedComp) {} + + void setItemPathForm(ItemPathFormat fmt) { itemPathFormat_ = fmt; groupItemNamesWidthBuf_.clear(); } + + void getUnbufferedIconsForPreload(std::vector>& newLoad) //return (priority, filepath) list + { + if (IconBuffer* iconBuf = getIconManager().getIconBuffer()) + { + const auto& [rowFirst, rowLast] = refGrid().getVisibleRows(refGrid().getMainWin().GetClientSize()); + const ptrdiff_t visibleRowCount = rowLast - rowFirst; + + //preload icons not yet on screen: + const int preloadSize = 2 * std::max(20, visibleRowCount); //:= sum of lines above and below of visible range to preload + //=> use full visible height to handle "next page" command and a minimum of 20 for excessive mouse wheel scrolls + + for (ptrdiff_t i = 0; i < preloadSize; ++i) + { + const ptrdiff_t currentRow = rowFirst - (preloadSize + 1) / 2 + getAlternatingPos(i, visibleRowCount + preloadSize); //for odd preloadSize start one row earlier + + if (const FileSystemObject* fsObj = getFsObject(currentRow)) + if (getIconInfo(*fsObj).type == IconType::standard) + if (!iconBuf->readyForRetrieval(fsObj->template getAbstractPath())) + newLoad.emplace_back(i, fsObj->template getAbstractPath()); //insert least-important items on outer rim first + } + } + else assert(false); + } + + void updateNewAndGetUnbufferedIcons(std::vector& newLoad) //loads all not yet drawn icons + { + if (IconBuffer* iconBuf = getIconManager().getIconBuffer()) + { + const auto& [rowFirst, rowLast] = refGrid().getVisibleRows(refGrid().getMainWin().GetClientSize()); + const ptrdiff_t visibleRowCount = rowLast - rowFirst; + + for (ptrdiff_t i = 0; i < visibleRowCount; ++i) + { + //alternate when adding rows: first, last, first + 1, last - 1 ... + const ptrdiff_t currentRow = rowFirst + getAlternatingPos(i, visibleRowCount); + + if (isFailedLoad(currentRow)) //find failed attempts to load icon + if (const FileSystemObject* fsObj = getFsObject(currentRow)) + if (getIconInfo(*fsObj).type == IconType::standard) + { + //test if they are already loaded in buffer: + if (iconBuf->readyForRetrieval(fsObj->template getAbstractPath())) + { + //do a *full* refresh for *every* failed load to update partial DC updates while scrolling + refGrid().refreshCell(currentRow, static_cast(ColumnTypeRim::path)); + setFailedLoad(currentRow, false); + } + else //not yet in buffer: mark for async. loading + newLoad.push_back(fsObj->template getAbstractPath()); + } + } + } + else assert(false); + } + +private: + bool isFailedLoad(size_t row) const { return row < failedLoads_.size() ? failedLoads_[row] != 0 : false; } + + void setFailedLoad(size_t row, bool failed = true) + { + if (failedLoads_.size() != refGrid().getRowCount()) + failedLoads_.resize(refGrid().getRowCount()); + + if (row < failedLoads_.size()) + failedLoads_[row] = failed; + } + + //icon buffer will load reversely, i.e. if we want to go from inside out, we need to start from outside in + static size_t getAlternatingPos(size_t pos, size_t total) + { + assert(pos < total); + return pos % 2 == 0 ? pos / 2 : total - 1 - pos / 2; + } + +private: + enum class DisplayType + { + inactive, + normal, + symlink, + }; + static DisplayType getObjectDisplayType(const FileSystemObject& fsObj) + { + if (!fsObj.isActive()) + return DisplayType::inactive; + + DisplayType output = DisplayType::normal; + + visitFSObject(fsObj, [](const FolderPair& folder) {}, + [](const FilePair& file) {}, + [&](const SymlinkPair& symlink) { output = DisplayType::symlink; }); + + return output; + } + + + std::wstring getValue(size_t row, ColumnType colType) const override + { + if (const FileSystemObject* fsObj = getFsObject(row)) + if (!fsObj->isEmpty()) + { + if (static_cast(colType) == ColumnTypeRim::path) + switch (itemPathFormat_) + { + case ItemPathFormat::name: + return utfTo(fsObj->getItemName()); + case ItemPathFormat::relative: + return utfTo(fsObj->getRelativePath()); + case ItemPathFormat::full: + return AFS::getDisplayPath(fsObj->getAbstractPath()); + } + + std::wstring value; //dynamically allocates 16 byte memory! but why? shouldn't SSO make this superfluous?! or is it only in debug? + switch (static_cast(colType)) + { + case ColumnTypeRim::path: + assert(false); + break; + + case ColumnTypeRim::size: + visitFSObject(*fsObj, [&](const FolderPair& folder) { /*value = L'<' + _("Folder") + L'>'; -> redundant!? */ }, + [&](const FilePair& file) { value = formatNumber(file.getFileSize()); }, + //[&](const FilePair& file) { value = numberTo(file.getFilePrint()); }, // -> test file id + [&](const SymlinkPair& symlink) { value = L'<' + _("Symlink") + L'>'; }); + break; + + case ColumnTypeRim::date: + visitFSObject(*fsObj, [](const FolderPair& folder) {}, + [&](const FilePair& file) { value = formatUtcToLocalTime(file .getLastWriteTime()); }, + [&](const SymlinkPair& symlink) { value = formatUtcToLocalTime(symlink.getLastWriteTime()); }); + break; + + case ColumnTypeRim::extension: + visitFSObject(*fsObj, [](const FolderPair& folder) {}, + [&](const FilePair& file) { value = utfTo(getFileExtension(file .getItemName())); }, + [&](const SymlinkPair& symlink) { value = utfTo(getFileExtension(symlink.getItemName())); }); + break; + } + return value; + } + return {}; + } + + void renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected, HoverArea rowHover) override + { + const FileView::PathDrawInfo pdi = getDataView().getDrawInfo(row); + + if (!enabled || !selected) + { + const wxColor backCol = [&] + { + if (pdi.fsObj && !pdi.fsObj->isEmpty()) //do we need color indication for *inactive* empty rows? probably not... + switch (getObjectDisplayType(*pdi.fsObj)) + { + case DisplayType::normal: break; + case DisplayType::symlink: return getColorSymlinkBackground(); + case DisplayType::inactive: return getColorInactiveBack(); + } + return getDefaultBackgroundColorAlternating(pdi.groupIdx % 2 == 0); + }(); + if (backCol != wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW) /*already the default!*/) + clearArea(dc, rect, backCol); + } + else + GridData::renderRowBackgound(dc, rect, row, true /*enabled*/, true /*selected*/, rowHover); + + //---------------------------------------------------------------------------------- + const wxRect rectLine(rect.x, rect.y + rect.height - dipToWxsize(1), rect.width, dipToWxsize(1)); + clearArea(dc, rectLine, row == pdi.groupLastRow - 1 || //last group item + (pdi.fsObj == pdi.folderGroupObj && //folder item => distinctive separation color against subsequent file items + itemPathFormat_ != ItemPathFormat::name) ? + getColorGridLine() : getDefaultBackgroundColorAlternating(pdi.groupIdx % 2 != 0)); + } + + + int getGroupItemNamesWidth(const wxReadOnlyDC& dc, const FileView::PathDrawInfo& pdi) + { + //FileView::updateView() called? => invalidates group item render buffer + if (pdi.viewUpdateId != viewUpdateIdLast_) + { + viewUpdateIdLast_ = pdi.viewUpdateId; + groupItemNamesWidthBuf_.clear(); + } + + auto& widthBuf = groupItemNamesWidthBuf_; + if (pdi.groupIdx >= widthBuf.size()) + widthBuf.resize(pdi.groupIdx + 1, -1 /*sentinel value*/); + + int& itemNamesWidth = widthBuf[pdi.groupIdx]; + if (itemNamesWidth < 0) + { + itemNamesWidth = 0; + //const int ellipsisWidth = getTextExtentBuffered(dc, ELLIPSIS).x; + + std::vector itemWidths; + for (size_t row2 = pdi.groupFirstRow; row2 < pdi.groupLastRow; ++row2) + if (const FileSystemObject* fsObj = getDataView().getFsObject(row2)) + if (itemPathFormat_ == ItemPathFormat::name || fsObj != pdi.folderGroupObj) +#if 0 //render same layout even when items don't exist + if (fsObj->isEmpty()) + itemNamesWidth = ellipsisWidth; + else +#endif + itemWidths.push_back(getTextExtentBuffered(dc, utfTo(fsObj->getItemName())).x); + + if (!itemWidths.empty()) + { + //ignore (small number of) excessive file name widths: + auto itPercentile = itemWidths.begin() + itemWidths.size() * 8 / 10; //80th percentile + std::nth_element(itemWidths.begin(), itPercentile, itemWidths.end()); //complexity: O(n) + itemNamesWidth = std::max(itemNamesWidth, *itPercentile); + } + assert(itemNamesWidth >= 0); + + //Note: A better/faster solution would be to get 80th percentile of all std::wstring::size(), then do a *single* getTextExtentBuffered()! + // However, we need all the getTextExtentBuffered(itemName) later anyway, so above is fine. + } + return itemNamesWidth; + } + + + struct GroupRowLayout + { + std::wstring groupParentPart; //... if distributed over multiple rows, otherwise full group parent folder + std::wstring groupName; //only filled for first row of a group + std::wstring itemName; + int groupParentWidth; + int groupNameWidth; + }; + GroupRowLayout getGroupRowLayout(const wxReadOnlyDC& dc, size_t row, const FileView::PathDrawInfo& pdi, int maxWidth) + { + assert(pdi.fsObj); + + const bool drawFileIcons = getIconManager().getIconBuffer(); + const int iconSize = getIconManager().getIconWxsize(); + + //-------------------------------------------------------------------- + const int ellipsisWidth = getTextExtentBuffered(dc, ELLIPSIS).x; + const int arrowRightDownWidth = getTextExtentBuffered(dc, rightArrowDown_).x; + const int groupItemNamesWidth = getGroupItemNamesWidth(dc, pdi); + //-------------------------------------------------------------------- + + //exception for readability: top row is always group start! + const size_t groupFirstRow = std::max(pdi.groupFirstRow, refGrid().getRowAtWinPos(0)); + + const size_t groupRowCount = pdi.groupLastRow - groupFirstRow; + + std::wstring itemName; + if (itemPathFormat_ == ItemPathFormat::name || //hack: show folder name in item colum since groupName/groupParentFolder are unused! + pdi.fsObj != pdi.folderGroupObj) //=> consider groupItemNamesWidth! + itemName = utfTo(pdi.fsObj->getItemName()); + //=> doesn't matter if isEmpty()! => only indicates if component should be drawn + + std::wstring groupName; + std::wstring groupParentFolder; + switch (itemPathFormat_) + { + case ItemPathFormat::name: + break; + + case ItemPathFormat::relative: + if (pdi.folderGroupObj) + { + groupName = utfTo(pdi.folderGroupObj ->template getItemName ()); + groupParentFolder = utfTo(pdi.folderGroupObj->parent().template getRelativePath()); + } + break; + + case ItemPathFormat::full: + if (pdi.folderGroupObj) + { + groupName = utfTo(pdi.folderGroupObj ->template getItemName ()); + groupParentFolder = AFS::getDisplayPath(pdi.folderGroupObj->parent().template getAbstractPath()); + } + else //=> BaseFolderPair + groupParentFolder = AFS::getDisplayPath(pdi.fsObj->base().getAbstractPath()); + break; + } + + if (!groupParentFolder.empty()) + { + const wchar_t pathSep = [&] + { + for (auto it = groupParentFolder.end(); it != groupParentFolder.begin();) //reverse iteration: 1. check 2. decrement 3. evaluate + { + --it; // + + if (*it == L'/' || + *it == L'\\') + return *it; + } + return static_cast(FILE_NAME_SEPARATOR); + }(); + if (!endsWith(groupParentFolder, pathSep)) //visual hint that this is a parent folder only + groupParentFolder += pathSep; // + } + + /* group details: single row + ________________________ ___________________________________ _____________________________________________________ + | (gap | group parent) | | (gap | icon | gap | group name) | | (2x gap | vline) | (gap | icon) | gap | item name | + ------------------------ ----------------------------------- ----------------------------------------------------- + + group details: stacked + __________________________________ ___________________________________ ___________________________________________________ + | gap | group parent, part 1 | ⤵️ | | (gap | icon | gap | group name) | | | (gap | icon) | gap | item name | + |-------------------------------------------------------------------------------------| | 2x gap | vline |--------------------------------| + | gap | group parent, part n | | | (gap | icon) | gap | item name | + --------------------------------------------------------------------------------------- --------------------------------------------------- + + -> group name on first row + -> parent name distributed over multiple rows, if needed */ + + int groupParentWidth = groupParentFolder.empty() ? 0 : (gapSize_ + getTextExtentBuffered(dc, groupParentFolder).x); + + int groupNameWidth = groupName.empty() ? 0 : (gapSize_ + iconSize + gapSize_ + getTextExtentBuffered(dc, groupName).x); + const int groupNameMinWidth = groupName.empty() ? 0 : (gapSize_ + iconSize + gapSize_ + ellipsisWidth); + + const int groupSepWidth = (groupParentFolder.empty() && groupName.empty()) ? 0 : (2 * gapSize_ + dipToWxsize(1)); + + int groupItemsWidth = groupSepWidth + (drawFileIcons ? gapSize_ + iconSize : 0) + gapSize_ + groupItemNamesWidth; + const int groupItemsMinWidth = groupSepWidth + (drawFileIcons ? gapSize_ + iconSize : 0) + gapSize_ + ellipsisWidth; + + std::wstring groupParentPart; + + //not enough space? => trim or render on multiple rows + if (int excessWidth = groupParentWidth + groupNameWidth + groupItemsWidth - maxWidth; + excessWidth > 0) + { + //1. shrink group parent + if (!groupParentFolder.empty()) + { + const int groupParentMinWidth = !groupName.empty() && groupRowCount > 1 ? //group parent details (possibly) on multiple rows + 0 : gapSize_ + ellipsisWidth; + + groupParentWidth = std::max(groupParentWidth - excessWidth, groupParentMinWidth); + excessWidth = groupParentWidth + groupNameWidth + groupItemsWidth - maxWidth; + } + + if (excessWidth > 0) + { + //2. shrink item rendering + groupItemsWidth = std::max(groupItemsWidth - excessWidth, groupItemsMinWidth); + excessWidth = groupParentWidth + groupNameWidth + groupItemsWidth - maxWidth; + + if (excessWidth > 0) + //3. shrink group name + if (!groupName.empty()) + groupNameWidth = std::max(groupNameWidth - excessWidth, groupNameMinWidth); + } + + //group parent details on multiple lines + if (!groupParentFolder.empty()) + { + //let's not waste empty row space for medium + large icon sizes: print multiple lines per row! + const int linesPerRow = std::max(refGrid().getRowHeight() / charHeight_, 1); + + size_t compPos = 0; + for (size_t i = groupFirstRow; i <= row; ++i) + for (int l = 0; l < linesPerRow; ++l) + { + const size_t compLen = i == pdi.groupLastRow - 1 && l == linesPerRow - 1 ? //not enough rows to show remaining parent folder components? + groupParentFolder.size() - compPos : //=> append the rest: will be truncated with ellipsis + getPathTrimmedSize(dc, makeStringView(groupParentFolder.begin() + compPos, groupParentFolder.end()), + groupParentWidth + (i == groupFirstRow ? 0 : groupNameWidth) - gapSize_ - arrowRightDownWidth); + + if (i == groupFirstRow && !groupName.empty() && groupRowCount > 1 && + getTextExtentBuffered(dc, makeStringView(groupParentFolder.begin() + compPos, compLen)).x > groupParentWidth - gapSize_ - arrowRightDownWidth) + { + if (i == row && l != 0) + groupParentPart.insert(groupParentPart.begin(), linesPerRow - l, L'\n'); //effectively: "align bottom" for first row + break; //exception: never truncate parent component on first row, but continue on second row instead + } + + if (i == row) + groupParentPart += compPos + compLen == groupParentFolder.size() ? + groupParentFolder.substr(compPos) : + groupParentFolder.substr(compPos, compLen) + rightArrowDown_ + L'\n'; + compPos += compLen; + + if (compPos == groupParentFolder.size()) + goto break2; + } + break2: + if (endsWith(groupParentPart, L'\n')) + groupParentPart.pop_back(); + } + } + else + { + if (row == groupFirstRow) + groupParentPart = groupParentFolder; + } + + //path components should follow the app layout direction and are NOT a single piece of text! + //caveat: - add Bidi support only during rendering and not in getValue() or AFS::getDisplayPath(): e.g. support "open file in Explorer" + // - add *after* getPathTrimmedSize(), otherwise LTR-mark can be confused for path component, e.g. "/home" would be two components! + assert(!contains(groupParentPart, slashBidi_) && !contains(groupParentPart, bslashBidi_)); + replace(groupParentPart, L'/', slashBidi_); + replace(groupParentPart, L'\\', bslashBidi_); + + return + { + std::move(groupParentPart), + row == groupFirstRow ? std::move(groupName) : std::wstring{}, + std::move(itemName), + row == groupFirstRow ? groupParentWidth : groupParentWidth + groupNameWidth, + row == groupFirstRow ? groupNameWidth : 0, + }; + } + + + void renderCell(wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover) override + { + //----------------------------------------------- + //don't forget: harmonize with getBestSize()!!! + //----------------------------------------------- + + if (const FileView::PathDrawInfo pdi = getDataView().getDrawInfo(row); + pdi.fsObj) + { + //accessibility: always set both foreground AND background colors! + wxDCTextColourChanger textColor(dc); + if (enabled && selected) //=> coordinate with renderRowBackgound() + textColor.Set(*wxBLACK); + else if (!pdi.fsObj->isEmpty()) + switch (getObjectDisplayType(*pdi.fsObj)) + { + case DisplayType::normal: break; + case DisplayType::symlink: textColor.Set(*wxBLACK); break; + case DisplayType::inactive: textColor.Set(getColorInactiveText()); break; + } + + wxRect rectTmp = rect; + + switch (static_cast(colType)) + { + case ColumnTypeRim::path: + { + auto drawCudHighlight = [&](wxRect rectCud, SyncOperation syncOp) + { + if (getViewType() == GridViewType::action) + if (!enabled || !selected) + if (const auto& [cudAction, cudSide] = getCudAction(syncOp); + cudAction != CudAction::noChange && side == cudSide) + { + rectCud.width = gapSize_ + screenToWxsize(IconBuffer::getPixSize(IconBuffer::IconSize::small)); + //fixed-size looks fine for all icon sizes! use same width even if file icons are disabled! + clearArea(dc, rectCud, getBackGroundColorSyncAction(syncOp)); + + rectCud.x += rectCud.width; + rectCud.width = gapSize_ + dipToWxsize(2); + +#if 0 //wxDC::GetPixel() is broken in GTK3! https://github.com/wxWidgets/wxWidgets/issues/14067 + wxColor backCol = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); + dc.GetPixel(rectCud.GetTopRight(), &backCol); +#else + const wxColor backCol = getDefaultBackgroundColorAlternating(pdi.groupIdx % 2 == 0); +#endif + dc.GradientFillLinear(rectCud, getBackGroundColorSyncAction(syncOp), backCol, wxEAST); + } + }; + + bool navMarkerDrawn = false; + auto tryDrawNavMarker = [&](wxRect rectNav) + { + if (!navMarkerDrawn && + rectNav.x == rect.x && //draw marker *only* if current render group (group parent, group name, item name) is at beginning of a row! + isNavMarked(*pdi.fsObj) && + (!enabled || !selected)) + { + rectNav.width = std::min(rectNav.width, dipToWxsize(10)); + + if (row == pdi.groupLastRow - 1 /*last group item*/) //preserve the group separation line! + rectNav.height -= dipToWxsize(1); + + dc.GradientFillLinear(rectNav, getColorSelectionGradientFrom(), getColorSelectionGradientTo(), wxEAST); + navMarkerDrawn = true; + } + }; + + auto drawIcon = [&](wxImage icon, wxRect rectIcon, bool drawActive) + { + if (!drawActive) + icon = icon.ConvertToGreyscale(1.0 / 3, 1.0 / 3, 1.0 / 3); //treat all channels equally! + + if (!enabled) + icon = icon.ConvertToDisabled(); + + rectIcon.x += gapSize_; + rectIcon.width = getIconManager().getIconWxsize(); //center smaller-than-default icons + drawBitmapRtlNoMirror(dc, icon, rectIcon, wxALIGN_CENTER); + }; + + auto drawFileIcon = [this, &drawIcon](const wxImage& fileIcon, bool drawAsLink, const wxRect& rectIcon, const FileSystemObject& fsObj) + { + if (fileIcon.IsOk()) + drawIcon(fileIcon, rectIcon, fsObj.isActive()); + + if (drawAsLink) + drawIcon(getIconManager().getLinkOverlayIcon(), rectIcon, fsObj.isActive()); + + if (getViewType() == GridViewType::action) + if (const auto& [cudAction, cudSide] = getCudAction(fsObj.getSyncOperation()); + side == cudSide) + switch (cudAction) + { + case CudAction::create: + assert(!fileIcon.IsOk() && !drawAsLink); + if (const bool isFolder = dynamic_cast(&fsObj) != nullptr) + drawIcon(getIconManager().getGenericDirIcon().ConvertToGreyscale(1.0 / 3, 1.0 / 3, 1.0 / 3). //treat all channels equally! + ConvertToDisabled(), rectIcon, true /*drawActive: [!]*/); //visual hint to distinguish file/folder creation + + //too much clutter? => drawIcon(getIconManager().getPlusOverlayIcon(), rectIcon, + // true /*drawActive: [!] e.g. disabled folder, exists left only, where child item is copied*/); + break; + case CudAction::delete_: + drawIcon(getIconManager().getMinusOverlayIcon(), rectIcon, true /*drawActive: [!]*/); + break; + case CudAction::noChange: + case CudAction::update: + break; + }; + }; + //------------------------------------------------------------------------- + + const auto& [groupParentPart, + groupName, + itemName, + groupParentWidth, + groupNameWidth] = getGroupRowLayout(dc, row, pdi, rectTmp.width); + + wxRect rectGroup, rectGroupParent, rectGroupName; + rectGroup = rectGroupParent = rectGroupName = rectTmp; + + rectGroup .width = groupParentWidth + groupNameWidth; + rectGroupParent.width = groupParentWidth; + rectGroupName .width = groupNameWidth; + rectGroupName.x += groupParentWidth; + + rectTmp.x += rectGroup.width; + rectTmp.width -= rectGroup.width; + + wxRect rectGroupItems = rectTmp; + + if (itemName.empty()) //expand group name to include unused item area (e.g. bigger selection border) + { + rectGroupName.width += rectGroupItems.width; + rectGroupItems.width = 0; + } + + //------------------------------------------------------------------------- + { + //clear background below parent path => harmonize with renderRowBackgound() + wxDCTextColourChanger textColorGroup(dc); + if (rectGroup.width > 0 && + (!enabled || !selected)) + { + wxRect rectGroupBack = rectGroup; + rectGroupBack.width += 2 * gapSize_; //include gap before vline + + if (row == pdi.groupLastRow - 1 /*last group item*/) //preserve the group separation line! + rectGroupBack.height -= dipToWxsize(1); + + clearArea(dc, rectGroupBack, getDefaultBackgroundColorAlternating(pdi.groupIdx % 2 == 0)); + //clearArea() is surprisingly expensive => call just once! + textColorGroup.Set(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); + //accessibility: always set *both* foreground AND background colors! + } + + if (!groupParentPart.empty() && + (!pdi.folderGroupObj || !pdi.folderGroupObj->isEmpty())) //don't show for missing folders + { + tryDrawNavMarker(rectGroupParent); + + wxRect rectGroupParentText = rectGroupParent; + rectGroupParentText.x += gapSize_; + rectGroupParentText.width -= gapSize_; + + //let's not waste empty row space for medium + large icon sizes: print multiple lines per row! + split(groupParentPart, L'\n', [&, linesPerRow = std::max(refGrid().getRowHeight() / charHeight_, 1), + lineNo = 0](const std::wstring_view line) mutable + { + drawCellText(dc, { + rectGroupParentText.x, //distribute lines evenly across multiple rows: + rectGroupParentText.y + (rectGroupParentText.height * (1 + lineNo++ * 2) - linesPerRow * charHeight_) / (linesPerRow * 2), + rectGroupParentText.width, charHeight_ + }, line, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, &getTextExtentBuffered(dc, line)); + }); +#if 0 + drawCellText(dc, rectGroupParentText, groupParentPart, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, &getTextExtentBuffered(dc, groupParentPart)); +#endif + } + + if (!groupName.empty()) + { + wxRect rectGroupNameBack = rectGroupName; + + if (!itemName.empty()) + rectGroupNameBack.width += 2 * gapSize_; //include gap left of item vline + rectGroupNameBack.height -= dipToWxsize(1); //harmonize with item separation lines + + wxDCTextColourChanger textColorGroupName(dc); + //folder background: coordinate with renderRowBackgound() + if (!enabled || !selected) + if (!pdi.folderGroupObj->isEmpty() && + !pdi.folderGroupObj->isActive()) + { + clearArea(dc, rectGroupNameBack, getColorInactiveBack()); + textColorGroupName.Set(getColorInactiveText()); + } + drawCudHighlight(rectGroupNameBack, pdi.folderGroupObj->getSyncOperation()); + tryDrawNavMarker(rectGroupName); + + wxImage folderIcon; + bool drawAsLink = false; + if (!pdi.folderGroupObj->isEmpty()) + { + folderIcon = getIconManager().getGenericDirIcon(); + drawAsLink = pdi.folderGroupObj->isFollowedSymlink(); + } + drawFileIcon(folderIcon, drawAsLink, rectGroupName, *pdi.folderGroupObj); + rectGroupName.x += gapSize_ + getIconManager().getIconWxsize() + gapSize_; + rectGroupName.width -= gapSize_ + getIconManager().getIconWxsize() + gapSize_; + + //mouse highlight: group name + if (static_cast(rowHover) == HoverAreaGroup::groupName || + (static_cast(rowHover) == HoverAreaGroup::item && pdi.fsObj == pdi.folderGroupObj /*exception: extend highlight*/)) + drawRectangleBorder(dc, rectGroupNameBack, mouseHighlightColor_, dipToWxsize(1)); + + if (!pdi.folderGroupObj->isEmpty()) + drawCellText(dc, rectGroupName, groupName, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, &getTextExtentBuffered(dc, groupName)); + } + } + + //------------------------------------------------------------------------- + if (!itemName.empty()) + { + //draw group/items separation line + if (rectGroup.width > 0) + { + rectGroupItems.x += 2 * gapSize_; + rectGroupItems.width -= 2 * gapSize_; + + wxRect rectLine = rectGroupItems; + rectLine.width = dipToWxsize(1); + clearArea(dc, rectLine, getColorGridLine()); + + rectGroupItems.x += dipToWxsize(1); + rectGroupItems.width -= dipToWxsize(1); + } + //------------------------------------------------------------------------- + + wxRect rectItemsBack = rectGroupItems; + rectItemsBack.height -= dipToWxsize(1); //preserve item separation lines! + + drawCudHighlight(rectItemsBack, pdi.fsObj->getSyncOperation()); + tryDrawNavMarker(rectGroupItems); + + if (IconBuffer* iconBuf = getIconManager().getIconBuffer()) //=> draw file icons + { + /* whenever there's something new to render on screen, start up watching for failed icon drawing: + => ideally it would suffice to start watching only when scrolling grid or showing new grid content, but this solution is more robust + and the icon updater will stop automatically when finished anyway + Note: it's not sufficient to start up on failed icon loads only, since we support prefetching of not yet visible rows!!! */ + getIconManager().startIconUpdater(); + + wxImage fileIcon; + const IconInfo ii = getIconInfo(*pdi.fsObj); + switch (ii.type) + { + case IconType::folder: + fileIcon = getIconManager().getGenericDirIcon(); + break; + + case IconType::standard: + if (std::optional tmpIco = iconBuf->retrieveFileIcon(pdi.fsObj->template getAbstractPath())) + fileIcon = *tmpIco; + else + { + setFailedLoad(row); //save status of failed icon load -> used for async. icon loading + //falsify only! avoid writing incorrect success status when only partially updating the DC, e.g. during scrolling, + //see repaint behavior of ::ScrollWindow() function! + fileIcon = iconBuf->getIconByExtension(pdi.fsObj->template getItemName()); //better than nothing + } + break; + + case IconType::none: + break; + } + drawFileIcon(fileIcon, ii.drawAsLink, rectGroupItems, *pdi.fsObj); + rectGroupItems.x += gapSize_ + getIconManager().getIconWxsize(); + rectGroupItems.width -= gapSize_ + getIconManager().getIconWxsize(); + } + + rectGroupItems.x += gapSize_; + rectGroupItems.width -= gapSize_; + + //mouse highlight: item name + if (static_cast(rowHover) == HoverAreaGroup::item) + drawRectangleBorder(dc, rectItemsBack, mouseHighlightColor_, dipToWxsize(1)); + + if (!pdi.fsObj->isEmpty()) + drawCellText(dc, rectGroupItems, itemName, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, &getTextExtentBuffered(dc, itemName)); + } + + //if not done yet: + tryDrawNavMarker(rect); + } + break; + + case ColumnTypeRim::size: + case ColumnTypeRim::date: + case ColumnTypeRim::extension: + { + if (refGrid().GetLayoutDirection() == wxLayout_RightToLeft || //remain left-justified for RTL languages + static_cast(colType) == ColumnTypeRim::extension) + { + rectTmp.x += gapSize_; + rectTmp.width -= gapSize_; + drawCellText(dc, rectTmp, getValue(row, colType), wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); + } + else + { + rectTmp.width -= gapSize_; + drawCellText(dc, rectTmp, getValue(row, colType), wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL); + //macOS: wxALIGN_RIGHT also helps mitigate NSDateFormatter not zero-padding dates! + } + } + break; + } + } + } + + + HoverArea getMouseHover(const wxReadOnlyDC& dc, size_t row, ColumnType colType, int cellRelativePosX, int cellWidth) override + { + if (static_cast(colType) == ColumnTypeRim::path) + if (const FileView::PathDrawInfo pdi = getDataView().getDrawInfo(row); + pdi.fsObj) + { + const auto& [groupParentPart, + groupName, + itemName, + groupParentWidth, + groupNameWidth] = getGroupRowLayout(dc, row, pdi, cellWidth); + + if (!groupName.empty() && pdi.fsObj != pdi.folderGroupObj) + { + const int groupNameCellBeginX = groupParentWidth; + + if (groupNameCellBeginX <= cellRelativePosX && cellRelativePosX < groupNameCellBeginX + groupNameWidth + 2 * gapSize_ /*include gap before vline*/) + return static_cast(HoverAreaGroup::groupName); + } + } + return static_cast(HoverAreaGroup::item); + } + + + int getBestSize(const wxReadOnlyDC& dc, size_t row, ColumnType colType) override + { + if (static_cast(colType) == ColumnTypeRim::path) + { + int bestSize = 0; + + if (const FileView::PathDrawInfo pdi = getDataView().getDrawInfo(row); + pdi.fsObj) + { + const int insanelyHugeWidth = 1000'000'000; //(hopefully) still small enough to avoid integer overflows + /* ________________________ ___________________________________ _____________________________________________________ + | (gap | group parent) | | (gap | icon | gap | group name) | | (2x gap | vline) | (gap | icon) | gap | item name | + ------------------------ ----------------------------------- ----------------------------------------------------- */ + const auto& [groupParentPart, + groupName, + itemName, + groupParentWidth, + groupNameWidth] = getGroupRowLayout(dc, row, pdi, insanelyHugeWidth); + + const int groupSepWidth = groupParentWidth + groupNameWidth <= 0 ? 0 : (2 * gapSize_ + dipToWxsize(1)); + const int fileIconWidth = getIconManager().getIconBuffer() ? gapSize_ + getIconManager().getIconWxsize() : 0; + const int ellipsisWidth = getTextExtentBuffered(dc, ELLIPSIS).x; + const int itemWidth = itemName.empty() ? 0 : + (groupSepWidth + fileIconWidth + gapSize_ + + (pdi.fsObj->isEmpty() ? ellipsisWidth : getTextExtentBuffered(dc, itemName).x)); + + bestSize += groupParentWidth + groupNameWidth + itemWidth + gapSize_ /*[!]*/; + } + return bestSize; + } + else + { + const wxReadOnlyDC& infoDc = dc; + const std::wstring cellValue = getValue(row, colType); + return gapSize_ + infoDc.GetTextExtent(cellValue).GetWidth() + gapSize_; + } + } + + + std::wstring getColumnLabel(ColumnType colType) const override + { + switch (static_cast(colType)) + { + case ColumnTypeRim::path: + switch (itemPathFormat_) + { + case ItemPathFormat::name: return _("Item name"); + case ItemPathFormat::relative: return _("Relative path"); + case ItemPathFormat::full: return _("Full path"); + } + assert(false); + break; + case ColumnTypeRim::size: return _("Size"); + case ColumnTypeRim::date: return _("Date"); + case ColumnTypeRim::extension: return _("Extension"); + } + //assert(false); may be ColumnType::none + return std::wstring(); + } + + void renderColumnLabel(wxDC& dc, const wxRect& rect, ColumnType colType, bool enabled, bool highlighted) override + { + const wxRect rectInner = drawColumnLabelBackground(dc, rect, highlighted); + wxRect rectRemain = rectInner; + + rectRemain.x += getColumnGapLeft(); + rectRemain.width -= getColumnGapLeft(); + drawColumnLabelText(dc, rectRemain, getColumnLabel(colType), enabled); + + //draw sort marker + if (auto sortInfo = getDataView().getSortConfig()) + if (const ColumnTypeRim* sortType = std::get_if(&sortInfo->sortCol)) + if (*sortType == static_cast(colType) && sortInfo->onLeft == (side == SelectSide::left)) + { + bool ascending = sortInfo->ascending; //work around MSVC 17.4 compiler bug :( "error C2039: 'sortCol': is not a member of 'fff::FileView::SortInfo'" + + const wxImage sortMarker = loadImage(ascending ? "sort_ascending" : "sort_descending"); + drawBitmapRtlNoMirror(dc, enabled ? sortMarker : sortMarker.ConvertToDisabled(), rectInner, wxALIGN_CENTER_HORIZONTAL); + } + } + + std::wstring getToolTip(size_t row, ColumnType colType, HoverArea rowHover) override + { + const FileView::PathDrawInfo pdi = getDataView().getDrawInfo(row); + + std::wstring toolTip; + + if (const FileSystemObject* tipObj = static_cast(rowHover) == HoverAreaGroup::groupName ? pdi.folderGroupObj : pdi.fsObj) + { + if (getDataView().getEffectiveFolderPairCount() > 1) + toolTip += AFS::getDisplayPath(tipObj->base().getAbstractPath()) + rightArrowDown_ + L"\n\n"; + + toolTip += utfTo(tipObj->getRelativePath()); + + //path components should follow the app layout direction and are NOT a single piece of text! + //caveat: add Bidi support only during rendering and not in getValue() or AFS::getDisplayPath(): e.g. support "open file in Explorer" + assert(!contains(toolTip, slashBidi_) && !contains(toolTip, bslashBidi_)); + replace(toolTip, L'/', slashBidi_); + replace(toolTip, L'\\', bslashBidi_); + + if (tipObj->isEmpty()) + toolTip += std::wstring(L"\n") + TAB_SPACE + L'<' + _("Item not existing") + L'>'; + else + visitFSObject(*tipObj, [&](const FolderPair& folder) + { + //toolTip += std::wstring(L"\n") + TAB_SPACE + '<' + _("Folder") + L'>'; -> redundant!? + }, + [&](const FilePair& file) + { + toolTip += std::wstring(L"\n") + TAB_SPACE + _("Size:") + L' ' + formatFilesizeShort (file.getFileSize ()) + + /**/ L'\n' + TAB_SPACE + _("Date:") + L' ' + formatUtcToLocalTime(file.getLastWriteTime()); + }, + [&](const SymlinkPair& symlink) + { + toolTip += std::wstring(L"\n") + TAB_SPACE + L'<' + _("Symlink") + L'>' + + /**/ L'\n' + TAB_SPACE + _("Date:") + L' ' + formatUtcToLocalTime(symlink.getLastWriteTime()); + }); + } + return toolTip; + } + + + enum class IconType + { + none, + folder, + standard, + }; + struct IconInfo + { + IconType type = IconType::none; + bool drawAsLink = false; + }; + static IconInfo getIconInfo(const FileSystemObject& fsObj) + { + IconInfo out; + + if (!fsObj.isEmpty()) + visitFSObject(fsObj, [&](const FolderPair& folder) + { + out.type = IconType::folder; + out.drawAsLink = folder.isFollowedSymlink(); + }, + + [&](const FilePair& file) + { + out.type = IconType::standard; + out.drawAsLink = file.isFollowedSymlink() || hasLinkExtension(file.getItemName()); + }, + + [&](const SymlinkPair& symlink) + { + out.type = IconType::standard; + out.drawAsLink = true; + }); + return out; + } + + const int gapSize_ = dipToWxsize(FILE_GRID_GAP_SIZE_DIP); + const int gapSizeWide_ = dipToWxsize(FILE_GRID_GAP_SIZE_WIDE_DIP); + + const wxColor mouseHighlightColor_ = enhanceContrast(*wxBLUE, //primarily needed for dark mode! + wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW), 5 /*contrastRatioMin*/); //W3C recommends >= 4.5 + + const int charHeight_ = refGrid().getMainWin().GetCharHeight(); + + + ItemPathFormat itemPathFormat_ = ItemPathFormat::full; + + std::vector failedLoads_; //effectively a vector of size "number of rows" + + const std::wstring slashBidi_ = (wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft ? RTL_MARK : LTR_MARK) + std::wstring(L"/"); + const std::wstring bslashBidi_ = (wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft ? RTL_MARK : LTR_MARK) + std::wstring(L"\\"); + //no need for LTR/RTL marks on both sides: text follows main direction if slash is between two strong characters with different directions + + const std::wstring rightArrowDown_ = wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft ? + std::wstring() + RTL_MARK + LEFT_ARROW_ANTICLOCK : + std::wstring() + LTR_MARK + RIGHT_ARROW_CURV_DOWN; + //Windows bug: RIGHT_ARROW_CURV_DOWN rendering and extent calculation is buggy (see wx+\tooltip.cpp) => need LTR mark! + + std::vector groupItemNamesWidthBuf_; //buffer! groupItemNamesWidths essentially only depends on (groupIdx, side) + uint64_t viewUpdateIdLast_ = 0; // +}; + + +class GridDataLeft : public GridDataRim +{ +public: + GridDataLeft(Grid& grid, const SharedRef& sharedComp) : GridDataRim(grid, sharedComp) {} +}; + +class GridDataRight : public GridDataRim +{ +public: + GridDataRight(Grid& grid, const SharedRef& sharedComp) : GridDataRim(grid, sharedComp) {} +}; + +//######################################################################################################## + +class GridDataCenter : public GridDataBase +{ +public: + GridDataCenter(Grid& grid, const SharedRef& sharedComp) : GridDataBase(grid, sharedComp), + toolTip_(grid) {} //tool tip must not live longer than grid! + + void onSelectBegin() + { + selectionInProgress_ = true; + refGrid().clearSelection(GridEventPolicy::deny); //don't emit event, prevent recursion! + toolTip_.hide(); //handle custom tooltip + } + + void onSelectEnd(size_t rowFirst, size_t rowLast, HoverArea rowHover, ptrdiff_t clickInitRow) + { + refGrid().clearSelection(GridEventPolicy::deny); //don't emit event, prevent recursion! + + //issue custom event + if (selectionInProgress_) //don't process selections initiated by right-click + if (rowFirst < rowLast && rowLast <= refGrid().getRowCount()) //empty? probably not in this context + switch (static_cast(rowHover)) + { + case HoverAreaCenter::checkbox: + if (const FileSystemObject* fsObj = getFsObject(clickInitRow)) + { + const bool setIncluded = !fsObj->isActive(); + CheckRowsEvent evt(rowFirst, rowLast, setIncluded); + refGrid().GetEventHandler()->ProcessEvent(evt); + } + break; + case HoverAreaCenter::dirLeft: + { + SyncDirectionEvent evt(rowFirst, rowLast, SyncDirection::left); + refGrid().GetEventHandler()->ProcessEvent(evt); + } + break; + case HoverAreaCenter::dirNone: + { + SyncDirectionEvent evt(rowFirst, rowLast, SyncDirection::none); + refGrid().GetEventHandler()->ProcessEvent(evt); + } + break; + case HoverAreaCenter::dirRight: + { + SyncDirectionEvent evt(rowFirst, rowLast, SyncDirection::right); + refGrid().GetEventHandler()->ProcessEvent(evt); + } + break; + } + selectionInProgress_ = false; + + //update highlight_ and tooltip: on OS X no mouse movement event is generated after a mouse button click (unlike on Windows) + wxPoint clientPos = refGrid().getMainWin().ScreenToClient(wxGetMousePosition()); + evalMouseMovement(clientPos); + } + + void evalMouseMovement(const wxPoint& clientPos) + { + //manage block highlighting and custom tooltip + if (!selectionInProgress_) + { + const size_t row = refGrid().getRowAtWinPos (clientPos.y); //return -1 for invalid position, rowCount if past the end + const Grid::ColumnPosInfo cpi = refGrid().getColumnAtWinPos(clientPos.x); //returns ColumnType::none if no column at x position! + + if (row < refGrid().getRowCount() && cpi.colType != ColumnType::none && + refGrid().getMainWin().GetClientRect().Contains(clientPos)) //cursor might have moved outside visible client area + showToolTip(row, static_cast(cpi.colType), refGrid().getMainWin().ClientToScreen(clientPos)); + else + toolTip_.hide(); + } + } + + void onMouseLeave() //wxEVT_LEAVE_WINDOW does not respect mouse capture! + { + toolTip_.hide(); //handle custom tooltip + } + +private: + std::wstring getValue(size_t row, ColumnType colType) const override + { + if (const FileSystemObject* fsObj = getFsObject(row)) + switch (static_cast(colType)) + { + case ColumnTypeCenter::checkbox: + break; + case ColumnTypeCenter::difference: + return getSymbol(fsObj->getCategory()); + case ColumnTypeCenter::action: + return getSymbol(fsObj->getSyncOperation()); + } + return std::wstring(); + } + + void renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected, HoverArea rowHover) override + { + const FileView::PathDrawInfo pdi = getDataView().getDrawInfo(row); + + if (!enabled || !selected) + { + const wxColor backCol = [&] + { + if (!pdi.fsObj) + return wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); + + if (!pdi.fsObj->isActive()) + return getColorInactiveBack(); + + return getDefaultBackgroundColorAlternating(pdi.groupIdx % 2 == 0); + }(); + if (backCol != wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW) /*already the default!*/) + clearArea(dc, rect, backCol); + } + else + GridData::renderRowBackgound(dc, rect, row, true /*enabled*/, true /*selected*/, rowHover); + + //---------------------------------------------------------------------------------- + const wxRect rectLine(rect.x, rect.y + rect.height - dipToWxsize(1), rect.width, dipToWxsize(1)); + clearArea(dc, rectLine, row == pdi.groupLastRow - 1 /*last group item*/ ? + getColorGridLine() : getDefaultBackgroundColorAlternating(pdi.groupIdx % 2 != 0)); + } + + enum class HoverAreaCenter //each cell can be divided into four blocks concerning mouse selections + { + checkbox, + dirLeft, + dirNone, + dirRight + }; + + void renderCell(wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover) override + { + if (const FileView::PathDrawInfo pdi = getDataView().getDrawInfo(row); + pdi.fsObj) + { + auto drawHighlightBackground = [&](const wxColor& col) + { + if ((!enabled || !selected) && pdi.fsObj->isActive()) //coordinate with renderRowBackgound()! + { + wxRect rectBack = rect; + if (row == pdi.groupLastRow - 1 /*last group item*/) //preserve the group separation line! + rectBack.height -= dipToWxsize(1); + + clearArea(dc, rectBack, col); + } + }; + + switch (static_cast(colType)) + { + case ColumnTypeCenter::checkbox: + { + const bool drawMouseHover = static_cast(rowHover) == HoverAreaCenter::checkbox; + + wxImage icon = loadImage(pdi.fsObj->isActive() ? + (drawMouseHover ? "checkbox_true_hover" : "checkbox_true") : + (drawMouseHover ? "checkbox_false_hover" : "checkbox_false")); + if (!enabled) + icon = icon.ConvertToDisabled(); + + drawBitmapRtlNoMirror(dc, icon, rect, wxALIGN_CENTER); + } + break; + + case ColumnTypeCenter::difference: + { + if (getViewType() == GridViewType::difference) + drawHighlightBackground(getBackGroundColorCmpDifference(pdi.fsObj->getCategory())); + + wxRect rectTmp = rect; + { + //draw notch on left side + if (notch_.GetHeight() != wxsizeToScreen(rectTmp.height)) + notch_ = notch_.Scale(notch_.GetWidth(), wxsizeToScreen(rectTmp.height)); + + //wxWidgets screws up again and has wxALIGN_RIGHT off by one pixel! -> use wxALIGN_LEFT instead + const wxRect rectNotch(rectTmp.x + rectTmp.width - screenToWxsize(notch_.GetWidth()), rectTmp.y, + screenToWxsize(notch_.GetWidth()), rectTmp.height); + drawBitmapRtlNoMirror(dc, notch_, rectNotch, wxALIGN_LEFT); + rectTmp.width -= screenToWxsize(notch_.GetWidth()); + } + + auto drawIcon = [&](wxImage icon, int alignment) + { + if (!enabled) + icon = icon.ConvertToDisabled(); + + drawBitmapRtlMirror(dc, icon, rectTmp, alignment, renderBufCmp_); + }; + + if (getViewType() == GridViewType::difference) + drawIcon(getCmpResultImage(pdi.fsObj->getCategory()), wxALIGN_CENTER); + else if (pdi.fsObj->getCategory() != FILE_EQUAL) //don't show = in both middle columns + drawIcon(greyScale(getCmpResultImage(pdi.fsObj->getCategory())), wxALIGN_CENTER); + } + break; + + case ColumnTypeCenter::action: + { + if (getViewType() == GridViewType::action) + drawHighlightBackground(getBackGroundColorSyncAction(pdi.fsObj->getSyncOperation())); + + auto drawIcon = [&](wxImage icon, int alignment) + { + if (!enabled) + icon = icon.ConvertToDisabled(); + + drawBitmapRtlMirror(dc, icon, rect, alignment, renderBufSync_); + }; + + //synchronization preview + const auto rowHoverCenter = rowHover == HoverArea::none ? HoverAreaCenter::checkbox : static_cast(rowHover); + switch (rowHoverCenter) + { + case HoverAreaCenter::dirLeft: + drawIcon(getSyncOpImage(pdi.fsObj->testSyncOperation(SyncDirection::left)), wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); + break; + case HoverAreaCenter::dirNone: + drawIcon(getSyncOpImage(pdi.fsObj->testSyncOperation(SyncDirection::none)), wxALIGN_CENTER); + break; + case HoverAreaCenter::dirRight: + drawIcon(getSyncOpImage(pdi.fsObj->testSyncOperation(SyncDirection::right)), wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL); + break; + case HoverAreaCenter::checkbox: + if (getViewType() == GridViewType::action) + drawIcon(getSyncOpImage(pdi.fsObj->getSyncOperation()), wxALIGN_CENTER); + else if (pdi.fsObj->getSyncOperation() != SO_EQUAL) //don't show = in both middle columns + drawIcon(greyScale(getSyncOpImage(pdi.fsObj->getSyncOperation())), wxALIGN_CENTER); + break; + } + } + break; + } + } + } + + HoverArea getMouseHover(const wxReadOnlyDC& dc, size_t row, ColumnType colType, int cellRelativePosX, int cellWidth) override + { + if (const FileSystemObject* const fsObj = getFsObject(row)) + switch (static_cast(colType)) + { + case ColumnTypeCenter::checkbox: + case ColumnTypeCenter::difference: + return static_cast(HoverAreaCenter::checkbox); + + case ColumnTypeCenter::action: + if (fsObj->getSyncOperation() == SO_EQUAL) //in sync-preview equal files shall be treated like a checkbox + return static_cast(HoverAreaCenter::checkbox); + /* cell: ------------------------ + | left | middle | right| + ------------------------ */ + if (0 <= cellRelativePosX) + { + if (cellRelativePosX < cellWidth / 3) + return static_cast(HoverAreaCenter::dirLeft); + else if (cellRelativePosX < 2 * cellWidth / 3) + return static_cast(HoverAreaCenter::dirNone); + else if (cellRelativePosX < cellWidth) + return static_cast(HoverAreaCenter::dirRight); + } + break; + } + return HoverArea::none; + } + + std::wstring getColumnLabel(ColumnType colType) const override + { + switch (static_cast(colType)) + { + case ColumnTypeCenter::checkbox: + break; + case ColumnTypeCenter::difference: + return _("Difference"); + case ColumnTypeCenter::action: + return _("Action"); + } + return std::wstring(); + } + + std::wstring getToolTip(ColumnType colType) const override { return getColumnLabel(colType) + L" (F11)"; } + + void renderColumnLabel(wxDC& dc, const wxRect& rect, ColumnType colType, bool enabled, bool highlighted) override + { + const auto colTypeCenter = static_cast(colType); + + const wxRect rectInner = drawColumnLabelBackground(dc, rect, highlighted && colTypeCenter != ColumnTypeCenter::checkbox); + + wxImage colIcon; + switch (colTypeCenter) + { + case ColumnTypeCenter::checkbox: + break; + + case ColumnTypeCenter::difference: + colIcon = greyScaleIfDisabled(loadImage("compare", dipToScreen(getMenuIconDipSize())), getViewType() == GridViewType::difference); + break; + + case ColumnTypeCenter::action: + colIcon = greyScaleIfDisabled(loadImage("start_sync", dipToScreen(getMenuIconDipSize())), getViewType() == GridViewType::action); + break; + } + + if (colIcon.IsOk()) + drawBitmapRtlNoMirror(dc, enabled ? colIcon : colIcon.ConvertToDisabled(), rectInner, wxALIGN_CENTER); + + //draw sort marker + if (auto sortInfo = getDataView().getSortConfig()) + if (const ColumnTypeCenter* sortType = std::get_if(&sortInfo->sortCol)) + if (*sortType == colTypeCenter) + { + const int gapLeft = (rectInner.width + screenToWxsize(colIcon.GetWidth())) / 2; + wxRect rectRemain = rectInner; + rectRemain.x += gapLeft; + rectRemain.width -= gapLeft; + + const wxImage sortMarker = loadImage(sortInfo->ascending ? "sort_ascending" : "sort_descending"); + drawBitmapRtlNoMirror(dc, enabled ? sortMarker : sortMarker.ConvertToDisabled(), rectRemain, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); + } + } + + void showToolTip(size_t row, ColumnTypeCenter colType, wxPoint posScreen) + { + if (const FileSystemObject* fsObj = getFsObject(row)) + { + switch (colType) + { + case ColumnTypeCenter::checkbox: + case ColumnTypeCenter::difference: + { + const char* imageName = [&] + { + switch (fsObj->getCategory()) + { + case FILE_RENAMED: //similar to both "equal" and "conflict" + case FILE_EQUAL: return "cat_equal"; + case FILE_LEFT_ONLY: return "cat_left_only"; + case FILE_RIGHT_ONLY: return "cat_right_only"; + case FILE_LEFT_NEWER: return "cat_left_newer"; + case FILE_RIGHT_NEWER: return "cat_right_newer"; + case FILE_DIFFERENT_CONTENT: return "cat_different"; + case FILE_TIME_INVALID: + case FILE_CONFLICT: return "cat_conflict"; + } + assert(false); + return ""; + }(); + const auto& img = mirrorIfRtl(loadImage(imageName)); + toolTip_.show(getCategoryDescription(*fsObj), posScreen, &img); + } + break; + + case ColumnTypeCenter::action: + { + const char* imageName = [&] + { + switch (fsObj->getSyncOperation()) + { + case SO_CREATE_LEFT: return "so_create_left"; + case SO_CREATE_RIGHT: return "so_create_right"; + case SO_DELETE_LEFT: return "so_delete_left"; + case SO_DELETE_RIGHT: return "so_delete_right"; + case SO_MOVE_LEFT_FROM: return "so_move_left_source"; + case SO_MOVE_LEFT_TO: return "so_move_left_target"; + case SO_MOVE_RIGHT_FROM: return "so_move_right_source"; + case SO_MOVE_RIGHT_TO: return "so_move_right_target"; + case SO_OVERWRITE_LEFT: return "so_update_left"; + case SO_OVERWRITE_RIGHT: return "so_update_right"; + case SO_RENAME_LEFT: return "so_move_left"; + case SO_RENAME_RIGHT: return "so_move_right"; + case SO_DO_NOTHING: return "so_none"; + case SO_EQUAL: return "cat_equal"; + case SO_UNRESOLVED_CONFLICT: return "cat_conflict"; + }; + assert(false); + return ""; + }(); + const auto& img = mirrorIfRtl(loadImage(imageName)); + toolTip_.show(getSyncOpDescription(*fsObj), posScreen, &img); + } + break; + } + } + else + toolTip_.hide(); //if invalid row... + } + + bool selectionInProgress_ = false; + + std::optional renderBufCmp_; //avoid costs of recreating this temporary variable + std::optional renderBufSync_; + Tooltip toolTip_; + wxImage notch_ = loadImage("notch"); +}; + +//######################################################################################################## + +class GridEventManager : private wxEvtHandler +{ +public: + GridEventManager(Grid& gridL, + Grid& gridC, + Grid& gridR, + GridDataCenter& provCenter) : + gridL_(gridL), gridC_(gridC), gridR_(gridR), + provCenter_(provCenter) + { + gridL_.Bind(EVENT_GRID_COL_RESIZE, [this](GridColumnResizeEvent& event) { onResizeColumn(event, gridL_, gridR_); }); + gridR_.Bind(EVENT_GRID_COL_RESIZE, [this](GridColumnResizeEvent& event) { onResizeColumn(event, gridR_, gridL_); }); + + gridL_.Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onKeyDown(event, gridL_); }); + gridC_.Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onKeyDown(event, gridC_); }); + gridR_.Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onKeyDown(event, gridR_); }); + + gridC_.getMainWin().Bind(wxEVT_MOTION, [this](wxMouseEvent& event) { onCenterMouseMovement(event); }); + gridC_.getMainWin().Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent& event) { onCenterMouseLeave (event); }); + + gridC_.Bind(EVENT_GRID_MOUSE_LEFT_DOWN, [this](GridClickEvent& event) { onCenterSelectBegin(event); }); + gridC_.Bind(EVENT_GRID_SELECT_RANGE, [this](GridSelectEvent& event) { onCenterSelectEnd (event); }); + + gridL_.Bind(EVENT_GRID_MOUSE_LEFT_DOWN, [this](GridClickEvent& event) { onGridClickRim(event, gridL_); }); + gridR_.Bind(EVENT_GRID_MOUSE_LEFT_DOWN, [this](GridClickEvent& event) { onGridClickRim(event, gridR_); }); + + //clear selection of other grid when selecting on + gridL_.Bind(EVENT_GRID_MOUSE_LEFT_DOWN, [this]( GridClickEvent& event) { onGridLeftClick(event, gridR_); }); //clear immediately, + gridL_.Bind(EVENT_GRID_MOUSE_RIGHT_DOWN, [this]( GridClickEvent& event) { onGridRightClick(event, gridR_, gridL_); }); //don't wait for GridSelectEvent + gridL_.Bind(EVENT_GRID_SELECT_RANGE, [this](GridSelectEvent& event) { onGridSelection(event, gridR_); }); + + gridR_.Bind(EVENT_GRID_MOUSE_LEFT_DOWN, [this]( GridClickEvent& event) { onGridLeftClick(event, gridL_); }); + gridR_.Bind(EVENT_GRID_MOUSE_RIGHT_DOWN, [this]( GridClickEvent& event) { onGridRightClick(event, gridL_, gridR_); }); + gridR_.Bind(EVENT_GRID_SELECT_RANGE, [this](GridSelectEvent& event) { onGridSelection(event, gridL_); }); + + //parallel grid scrolling: do NOT use DoPrepareDC() to align grids! GDI resource leak! Use regular paint event instead: + gridL_.getMainWin().Bind(wxEVT_PAINT, [this](wxPaintEvent& event) { onPaintGrid(gridL_); event.Skip(); }); + gridC_.getMainWin().Bind(wxEVT_PAINT, [this](wxPaintEvent& event) { onPaintGrid(gridC_); event.Skip(); }); + gridR_.getMainWin().Bind(wxEVT_PAINT, [this](wxPaintEvent& event) { onPaintGrid(gridR_); event.Skip(); }); + + + //----------------------------------------------------------------------------------------------------- + //scroll master event handling: connect LAST, so that scrollMaster_ is set BEFORE other event handling! + //----------------------------------------------------------------------------------------------------- + auto connectGridAccess = [&](Grid& grid, std::function handler) + { + grid.Bind(wxEVT_SCROLLWIN_TOP, handler); + grid.Bind(wxEVT_SCROLLWIN_BOTTOM, handler); + grid.Bind(wxEVT_SCROLLWIN_LINEUP, handler); + grid.Bind(wxEVT_SCROLLWIN_LINEDOWN, handler); + grid.Bind(wxEVT_SCROLLWIN_PAGEUP, handler); + grid.Bind(wxEVT_SCROLLWIN_PAGEDOWN, handler); + grid.Bind(wxEVT_SCROLLWIN_THUMBTRACK, handler); + //wxEVT_KILL_FOCUS -> there's no need to reset "scrollMaster" + //wxEVT_SET_FOCUS -> not good enough: + //e.g.: left grid has input, right grid is "scrollMaster" due to dragging scroll thumb via mouse. + //=> Next keyboard input on left does *not* emit focus change event, but still "scrollMaster" needs to change + //=> hook keyboard input instead of focus event: + grid.getMainWin().Bind(wxEVT_CHAR, handler); + grid.Bind(wxEVT_KEY_DOWN, handler); + //grid.getMainWin().Bind(wxEVT_KEY_UP, handler); -> superfluous? + + grid.getMainWin().Bind(wxEVT_LEFT_DOWN, handler); + grid.getMainWin().Bind(wxEVT_LEFT_DCLICK, handler); + grid.getMainWin().Bind(wxEVT_RIGHT_DOWN, handler); + grid.getMainWin().Bind(wxEVT_MOUSEWHEEL, handler); + }; + connectGridAccess(gridL_, [this](wxEvent& event) { setScrollMaster(gridL_); event.Skip(); }); // + connectGridAccess(gridC_, [this](wxEvent& event) { setScrollMaster(gridC_); event.Skip(); }); //connect *after* onKeyDown() in order to receive callback *before*!!! + connectGridAccess(gridR_, [this](wxEvent& event) { setScrollMaster(gridR_); event.Skip(); }); // + } + + ~GridEventManager() + { + //assert(!scrollbarAlignPending_); => false-positives: e.g. start ffs, right-click on grid, close dialog by clicking X + } + + void setScrollMaster(const Grid& grid) + { + if (&grid != &gridC_ && + &grid != &gridL_ && + &grid != &gridR_) + { + assert(false); //does this ever happen? + return ; + } +#if 0 + if (const std::string& logtext = "new scroll master: " + printNumber("%llx", reinterpret_cast(&grid)) + "\n"; + scrollMaster_ != &grid) + + std::cerr << logtext; +#endif + scrollMaster_ = &grid; + } + +private: + void onCenterSelectBegin(GridClickEvent& event) + { + provCenter_.onSelectBegin(); + event.Skip(); + } + + void onCenterSelectEnd(GridSelectEvent& event) + { + if (event.positive_) + { + if (event.mouseClick_) + provCenter_.onSelectEnd(event.rowFirst_, event.rowLast_, event.mouseClick_->hoverArea_, event.mouseClick_->row_); + else + provCenter_.onSelectEnd(event.rowFirst_, event.rowLast_, HoverArea::none, -1); + } + event.Skip(); + } + + void onCenterMouseMovement(wxMouseEvent& event) + { + provCenter_.evalMouseMovement(event.GetPosition()); + event.Skip(); + } + + void onCenterMouseLeave(wxMouseEvent& event) + { + provCenter_.onMouseLeave(); + event.Skip(); + } + + void onGridClickRim(GridClickEvent& event, Grid& grid) + { + if (static_cast(event.hoverArea_) == HoverAreaGroup::groupName) + if (const FileView::PathDrawInfo pdi = provCenter_.getDataView().getDrawInfo(event.row_); + pdi.fsObj) + { + const ptrdiff_t topRowOld = grid.getRowAtWinPos(0); + grid.makeRowVisible(pdi.groupFirstRow); + const ptrdiff_t topRowNew = grid.getRowAtWinPos(0); + + if (topRowNew != topRowOld) //=> grid was scrolled: prevent AddPendingEvent() recursion! + { + assert(topRowNew == makeSigned(pdi.groupFirstRow)); + assert(topRowNew == grid.getRowAtWinPos((event.mousePos_ - grid.getMainWin().GetPosition()).y)); + //don't waste a click: simulate start of new selection at Grid::MainWin-relative position (0/0): + grid.getMainWin().GetEventHandler()->AddPendingEvent(wxMouseEvent(wxEVT_LEFT_DOWN)); + return; + } + } + event.Skip(); + } + + void onGridLeftClick(GridClickEvent& event, Grid& gridOther) + { + //see grid.cpp Grid::MainWin::onMouseDown(): + if (!wxGetKeyState(WXK_CONTROL) && !wxGetKeyState(WXK_SHIFT)) //clear other grid unless user is holding CTRL, or SHIFT + gridOther.clearSelection(GridEventPolicy::deny); //don't emit event, prevent recursion! + event.Skip(); + } + + void onGridRightClick(GridClickEvent& event, Grid& gridOther, Grid& gridThis) + { + const std::vector& selectedRows = gridThis.getSelectedRows(); + const bool rowSelected = std::find(selectedRows.begin(), selectedRows.end(), makeUnsigned(event.row_)) != selectedRows.end(); + + //clear other grid unless GridContextMenuEvent is about to happen, or user is holding CTRL, or SHIFT + if (!rowSelected && !wxGetKeyState(WXK_CONTROL) && !wxGetKeyState(WXK_SHIFT)) + gridOther.clearSelection(GridEventPolicy::deny); //don't emit event, prevent recursion! + event.Skip(); + } + + void onGridSelection(GridSelectEvent& event, Grid& gridOther) + { + if (!event.mouseClick_ && !wxGetKeyState(WXK_SHIFT)) //clear other grid during keyboard selection, unless user is holding SHIFT + gridOther.clearSelection(GridEventPolicy::deny); //don't emit event, prevent recursion! + event.Skip(); + } + + void onKeyDown(wxKeyEvent& event, const Grid& grid) + { + int keyCode = event.GetKeyCode(); + if (grid.GetLayoutDirection() == wxLayout_RightToLeft) + { + if (keyCode == WXK_LEFT || keyCode == WXK_NUMPAD_LEFT) + keyCode = WXK_RIGHT; + else if (keyCode == WXK_RIGHT || keyCode == WXK_NUMPAD_RIGHT) + keyCode = WXK_LEFT; + } + + //skip middle component when navigating via keyboard + const size_t row = grid.getGridCursor(); + + if (event.ShiftDown()) + ; + else if (event.ControlDown()) + ; + else + switch (keyCode) + { + case WXK_LEFT: + case WXK_NUMPAD_LEFT: + gridL_.setGridCursor(row, GridEventPolicy::allow); + gridL_.SetFocus(); + //since key event is likely originating from right grid, we need to set scrollMaster manually! + setScrollMaster(gridL_); //onKeyDown is called *after* onGridAccessL()! + return; //swallow event + + case WXK_RIGHT: + case WXK_NUMPAD_RIGHT: + gridR_.setGridCursor(row, GridEventPolicy::allow); + gridR_.SetFocus(); + setScrollMaster(gridR_); + return; //swallow event + } + + event.Skip(); + } + + void onResizeColumn(GridColumnResizeEvent& event, const Grid& grid, Grid& gridOther) + { + //find stretch factor of resized column: type is unique due to makeConsistent()! + std::vector cfgSrc = grid.getColumnConfig(); + auto it = std::find_if(cfgSrc.begin(), cfgSrc.end(), [&](Grid::ColAttributes& ca) { return ca.type == event.colType_; }); + if (it == cfgSrc.end()) + return; + const int stretchSrc = it->stretch; + + //we do not propagate resizings on stretched columns to the other side: awkward user experience + if (stretchSrc > 0) + return; + + //apply resized offset to other side, but only if stretch factors match! + std::vector cfgTrg = gridOther.getColumnConfig(); + for (Grid::ColAttributes& ca : cfgTrg) + if (ca.type == event.colType_ && ca.stretch == stretchSrc) + ca.offset = event.offset_; + gridOther.setColumnConfig(cfgTrg); + } + + void onPaintGrid(const Grid& grid) + { +#if 0 + const std::string& logtext = "wxEVT_PAINT: " + printNumber("%llx", reinterpret_cast(&grid)) + "\n"; + std::cerr << logtext; +#endif + /* keep scroll positions of all three grids in sync + + wxGrid::Scroll() *during* vs *after* paint event: + ------------------------------------------------ + macOS: doesn't matter; 3 paint events per mouse scroll + + Linux: no visible perf issue, but + a) *during* paint event: 6 paint events + b) *after* paint event: 4 paint events + + Windows: a) *during* paint event: + 1. double-buffering(WS_EX_COMPOSITED) => excessive amount of additional paint events and accidental RECURSION!!! + Apparently multiple paint events sent (with clipped DC), then during wxGrid::Scroll() -> wxWindow::Update() -> onPaintGrid() for *SAME* grid! + 2. no double buffering => 4 paint events per mouse scroll + b) *after* paint event: + 1. double-buffering(WS_EX_COMPOSITED) => 6 paint events per mouse scroll + => no visible perf-difference compared to 2. but 60% higher CPU time during excessive scrolling + 2. no double buffering => 4 paint events per mouse scroll */ + if (&grid == scrollMaster_ && !scrollPosAlignPending_) + { + scrollPosAlignPending_ = true; + + CallAfter([this] + { + auto scroll = [this](Grid& target, int y) //support polling + { + if (&target != scrollMaster_) + { + //scroll vertically only - scrolling horizontally becomes annoying if left and right sides have different widths; + //e.g. h-scroll on left would be undone when scrolling vertically on right which doesn't have a h-scrollbar + int yOld = 0; + target.GetViewStart(nullptr, &yOld); + if (yOld != y) + target.Scroll(-1, y); //empirical test Windows/Ubuntu: this call does NOT trigger a wxEVT_SCROLLWIN event, + // which would incorrectly set "scrollMaster" to "&target"! + //CAVEAT: wxScrolledWindow::Scroll() internally calls wxWindow::Update(), leading to immediate WM_PAINT handling in the target grid! + // and this while we're still in our WM_PAINT handler! => no recursion, thanks to scrollMaster_ (hopefully) + } + }; + int y = 0; + scrollMaster_->GetViewStart(nullptr, &y); + scroll(gridC_, y); + scroll(gridL_, y); + scroll(gridR_, y); + + assert(scrollPosAlignPending_); + scrollPosAlignPending_ = false; + }); + } + + //harmonize placement of horizontal scrollbar to avoid grids getting out of sync! + //since this affects the grid that is currently repainted, run asynchronously! + if (!scrollbarAlignPending_) //send one async event at most, else they may accumulate and create perf issues, see grid.cpp + { + scrollbarAlignPending_ = true; + + CallAfter([this] //update *outside* of wxPaint event + { + auto needsHorizontalScrollbars = [](const Grid& target) + { + const wxWindow& mainWin = target.getMainWin(); + return mainWin.GetVirtualSize().GetWidth() > mainWin.GetClientSize().GetWidth(); + //assuming Grid::updateWindowSizes() does its job well, this should suffice! + //CAVEAT: if horizontal and vertical scrollbar are circular dependent from each other + //(h-scrollbar is shown due to v-scrollbar consuming horizontal width, etc...) + //while in fact both are NOT needed, this special case results in a bogus need for scrollbars! + //see https://sourceforge.net/tracker/?func=detail&aid=3514183&group_id=234430&atid=1093083 + // => since we're outside the Grid abstraction, we should not duplicate code to handle this special case as it seems to be insignificant + }; + + Grid::ScrollBarStatus sbStatusX = needsHorizontalScrollbars(gridL_) || + needsHorizontalScrollbars(gridR_) ? + Grid::SB_SHOW_ALWAYS : Grid::SB_SHOW_NEVER; + gridL_.showScrollBars(sbStatusX, Grid::SB_SHOW_NEVER); + gridC_.showScrollBars(sbStatusX, Grid::SB_SHOW_NEVER); + gridR_.showScrollBars(sbStatusX, Grid::SB_SHOW_AUTOMATIC); + + assert(scrollbarAlignPending_); + scrollbarAlignPending_ = false; + }); + } + } + + Grid& gridL_; + Grid& gridC_; + Grid& gridR_; + + const Grid* scrollMaster_ = &gridL_; //for address check only; this needn't be the grid having focus! + //e.g. mouse wheel events should set window under cursor as scrollMaster, but *not* change focus + + GridDataCenter& provCenter_; + bool scrollbarAlignPending_ = false; + bool scrollPosAlignPending_ = false; +}; +} + +//######################################################################################################## + +void filegrid::init(Grid& gridLeft, Grid& gridCenter, Grid& gridRight) +{ + auto sharedComp = makeSharedRef(); + + auto provLeft_ = std::make_shared(gridLeft, sharedComp); + auto provCenter_ = std::make_shared(gridCenter, sharedComp); + auto provRight_ = std::make_shared(gridRight, sharedComp); + + sharedComp.ref().evtMgr = std::make_unique(gridLeft, gridCenter, gridRight, *provCenter_); + + gridLeft .setDataProvider(provLeft_); //data providers reference grid => + gridCenter.setDataProvider(provCenter_); //ownership must belong *exclusively* to grid! + gridRight .setDataProvider(provRight_); + + gridCenter.enableColumnMove (false); + gridCenter.enableColumnResize(false); + + gridCenter.showRowLabel(false); + gridRight .showRowLabel(false); + + //gridLeft .showScrollBars(Grid::SB_SHOW_AUTOMATIC, Grid::SB_SHOW_NEVER); -> redundant: configuration happens in GridEventManager::onPaintGrid() + //gridCenter.showScrollBars(Grid::SB_SHOW_NEVER, Grid::SB_SHOW_NEVER); + + const int widthCheckbox = screenToWxsize( loadImage("checkbox_true").GetWidth() + dipToScreen(3)); + const int widthDifference = screenToWxsize(2 * loadImage("sort_ascending").GetWidth() + loadImage("cat_left_only_sicon").GetWidth() + loadImage("notch").GetWidth()); + const int widthAction = screenToWxsize(3 * loadImage("so_create_left_sicon").GetWidth()); + gridCenter.SetSize(widthDifference + widthCheckbox + widthAction, -1); + + gridCenter.setColumnConfig( + { + {static_cast(ColumnTypeCenter::checkbox), widthCheckbox, 0, true}, + {static_cast(ColumnTypeCenter::difference), widthDifference, 0, true}, + {static_cast(ColumnTypeCenter::action), widthAction, 0, true}, + }); +} + + +void filegrid::setData(Grid& grid, FolderComparison& folderCmp) +{ + if (auto* prov = dynamic_cast(grid.getDataProvider())) + return prov->setData(folderCmp); + + throw std::runtime_error("filegrid was not initialized! " + std::string(__FILE__) + ':' + numberTo(__LINE__)); +} + + +FileView& filegrid::getDataView(Grid& grid) +{ + if (auto* prov = dynamic_cast(grid.getDataProvider())) + return prov->getDataView(); + + throw std::runtime_error("filegrid was not initialized! " + std::string(__FILE__) + ':' + numberTo(__LINE__)); +} + + +namespace +{ +//resolve circular linker dependencies +void IconUpdater::loadIconsAsynchronously(wxEvent& event) //loads all (not yet) drawn icons +{ + std::vector> prefetchLoad; + provLeft_ .getUnbufferedIconsForPreload(prefetchLoad); + provRight_.getUnbufferedIconsForPreload(prefetchLoad); + + //make sure least-important prefetch rows are inserted first into workload (=> processed last) + //priority index nicely considers both grids at the same time! + std::sort(prefetchLoad.begin(), prefetchLoad.end(), [](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; }); + + //last inserted items are processed first in icon buffer: + std::vector newLoad; + for (const auto& [priority, filePath] : prefetchLoad) + newLoad.push_back(filePath); + + provRight_.updateNewAndGetUnbufferedIcons(newLoad); + provLeft_ .updateNewAndGetUnbufferedIcons(newLoad); + + iconBuffer_.setWorkload(newLoad); + + if (newLoad.empty()) //let's only pay for IconUpdater while needed + stop(); +} +} + + +void filegrid::setupIcons(Grid& gridLeft, Grid& gridCenter, Grid& gridRight, bool showFileIcons, IconBuffer::IconSize sz) +{ + auto* provLeft = dynamic_cast(gridLeft .getDataProvider()); + auto* provRight = dynamic_cast(gridRight.getDataProvider()); + + if (provLeft && provRight) + { + auto iconMgr = makeSharedRef(*provLeft, *provRight, sz, showFileIcons); + provLeft ->setIconManager(iconMgr); + + const int newRowHeight = std::max(iconMgr.ref().getIconWxsize(), gridLeft.getMainWin().GetCharHeight()) + dipToWxsize(1); //add some space + + gridLeft .setRowHeight(newRowHeight); + gridCenter.setRowHeight(newRowHeight); + gridRight .setRowHeight(newRowHeight); + } + else + assert(false); +} + + +void filegrid::setItemPathForm(Grid& grid, ItemPathFormat fmt) +{ + if (auto* provLeft = dynamic_cast(grid.getDataProvider())) + provLeft->setItemPathForm(fmt); + else if (auto* provRight = dynamic_cast(grid.getDataProvider())) + provRight->setItemPathForm(fmt); + else + assert(false); + grid.Refresh(); +} + + +void filegrid::refresh(Grid& gridLeft, Grid& gridCenter, Grid& gridRight) +{ + gridLeft .Refresh(); + gridCenter.Refresh(); + gridRight .Refresh(); +} + + +void filegrid::setScrollMaster(Grid& grid) +{ + if (auto prov = dynamic_cast(grid.getDataProvider())) + if (auto evtMgr = prov->getEventManager()) + { + evtMgr->setScrollMaster(grid); + return; + } + assert(false); +} + + +void filegrid::setNavigationMarker(Grid& gridLeft, + zen::Grid& gridRight, + std::unordered_set&& markedFilesAndLinks, + std::unordered_set&& markedContainer) +{ + if (auto grid = dynamic_cast(gridLeft.getDataProvider())) + grid->setNavigationMarker(std::move(markedFilesAndLinks), std::move(markedContainer)); + else + assert(false); + gridLeft .Refresh(); + gridRight.Refresh(); +} + + +void filegrid::setViewType(Grid& gridCenter, GridViewType vt) +{ + if (auto prov = dynamic_cast(gridCenter.getDataProvider())) + prov->setViewType(vt); + else + assert(false); + gridCenter.Refresh(); +} + + +wxImage fff::getSyncOpImage(SyncOperation syncOp) +{ + switch (syncOp) //evaluate comparison result and sync direction + { + case SO_CREATE_LEFT: return loadImage("so_create_left_sicon"); + case SO_CREATE_RIGHT: return loadImage("so_create_right_sicon"); + case SO_DELETE_LEFT: return loadImage("so_delete_left_sicon"); + case SO_DELETE_RIGHT: return loadImage("so_delete_right_sicon"); + case SO_MOVE_LEFT_FROM: return loadImage("so_move_left_source_sicon"); + case SO_MOVE_LEFT_TO: return loadImage("so_move_left_target_sicon"); + case SO_MOVE_RIGHT_FROM: return loadImage("so_move_right_source_sicon"); + case SO_MOVE_RIGHT_TO: return loadImage("so_move_right_target_sicon"); + case SO_OVERWRITE_LEFT: return loadImage("so_update_left_sicon"); + case SO_OVERWRITE_RIGHT: return loadImage("so_update_right_sicon"); + case SO_RENAME_LEFT: return loadImage("so_move_left_sicon"); + case SO_RENAME_RIGHT: return loadImage("so_move_right_sicon"); + case SO_DO_NOTHING: return loadImage("so_none_sicon"); + case SO_EQUAL: return loadImage("cat_equal_sicon"); + case SO_UNRESOLVED_CONFLICT: return loadImage("cat_conflict_small"); + } + assert(false); + return wxNullImage; +} + + +wxImage fff::getCmpResultImage(CompareFileResult cmpResult) +{ + switch (cmpResult) + { + case FILE_RENAMED: //similar to both "equal" and "conflict" + case FILE_EQUAL: return loadImage("cat_equal_sicon"); + case FILE_LEFT_ONLY: return loadImage("cat_left_only_sicon"); + case FILE_RIGHT_ONLY: return loadImage("cat_right_only_sicon"); + case FILE_LEFT_NEWER: return loadImage("cat_left_newer_sicon"); + case FILE_RIGHT_NEWER: return loadImage("cat_right_newer_sicon"); + case FILE_DIFFERENT_CONTENT: return loadImage("cat_different_sicon"); + case FILE_TIME_INVALID: + case FILE_CONFLICT: return loadImage("cat_conflict_small"); + } + assert(false); + return wxNullImage; +} diff --git a/FreeFileSync/Source/ui/file_grid.h b/FreeFileSync/Source/ui/file_grid.h new file mode 100644 index 0000000..355c900 --- /dev/null +++ b/FreeFileSync/Source/ui/file_grid.h @@ -0,0 +1,81 @@ +// ***************************************************************************** +// * 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 CUSTOM_GRID_H_8405817408327894 +#define CUSTOM_GRID_H_8405817408327894 + +#include +#include "file_view.h" +#include "../icon_buffer.h" + + +namespace fff +{ +//setup grid to show grid view within three components: +namespace filegrid +{ +void init(zen::Grid& gridLeft, zen::Grid& gridCenter, zen::Grid& gridRight); +FileView& getDataView(zen::Grid& grid); + +void setData(zen::Grid& grid, FolderComparison& folderCmp); //takes (shared) ownership + +void setViewType(zen::Grid& gridCenter, GridViewType vt); + +void setupIcons(zen::Grid& gridLeft, zen::Grid& gridCenter, zen::Grid& gridRight, bool showFileIcons, IconBuffer::IconSize sz); + +void setItemPathForm(zen::Grid& grid, ItemPathFormat fmt); //only for left/right grid + +void refresh(zen::Grid& gridLeft, zen::Grid& gridCenter, zen::Grid& gridRight); + +void setScrollMaster(zen::Grid& grid); + +//mark rows selected in overview panel and navigate to leading object +void setNavigationMarker(zen::Grid& gridLeft, zen::Grid& gridRight, + std::unordered_set&& markedFilesAndLinks,//mark files/symlinks directly within a container + std::unordered_set&& markedContainer); //mark full container including child-objects +} + +wxImage getSyncOpImage(SyncOperation syncOp); +wxImage getCmpResultImage(CompareFileResult cmpResult); + + +//grid hover area for file group rendering +enum class HoverAreaGroup +{ + groupName, + item +}; + +//---------- custom events for middle grid ---------- +struct CheckRowsEvent; +struct SyncDirectionEvent; +wxDECLARE_EVENT(EVENT_GRID_CHECK_ROWS, CheckRowsEvent); +wxDECLARE_EVENT(EVENT_GRID_SYNC_DIRECTION, SyncDirectionEvent); + + +struct CheckRowsEvent : public wxEvent +{ + CheckRowsEvent(size_t rowFirst, size_t rowLast, bool setIncluded) : wxEvent(0 /*winid*/, EVENT_GRID_CHECK_ROWS), rowFirst_(rowFirst), rowLast_(rowLast), setActive_(setIncluded) { assert(rowFirst <= rowLast); } + CheckRowsEvent* Clone() const override { return new CheckRowsEvent(*this); } + + const size_t rowFirst_; //selected range: [rowFirst_, rowLast_) + const size_t rowLast_; //range is empty when clearing selection + const bool setActive_; +}; + + +struct SyncDirectionEvent : public wxEvent +{ + SyncDirectionEvent(size_t rowFirst, size_t rowLast, SyncDirection direction) : wxEvent(0 /*winid*/, EVENT_GRID_SYNC_DIRECTION), rowFirst_(rowFirst), rowLast_(rowLast), direction_(direction) { assert(rowFirst <= rowLast); } + SyncDirectionEvent* Clone() const override { return new SyncDirectionEvent(*this); } + + const size_t rowFirst_; //see CheckRowsEvent + const size_t rowLast_; // + const SyncDirection direction_; +}; +} + +#endif //CUSTOM_GRID_H_8405817408327894 diff --git a/FreeFileSync/Source/ui/file_grid_attr.h b/FreeFileSync/Source/ui/file_grid_attr.h new file mode 100644 index 0000000..fbf8341 --- /dev/null +++ b/FreeFileSync/Source/ui/file_grid_attr.h @@ -0,0 +1,105 @@ +// ***************************************************************************** +// * 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 COLUMN_ATTR_H_189467891346732143213 +#define COLUMN_ATTR_H_189467891346732143213 + +#include +#include +#include + + +namespace fff +{ +enum class GridViewType +{ + difference, + action, +}; + +enum class ColumnTypeRim +{ + path, + size, + date, + extension, +}; + +struct ColAttributesRim +{ + ColumnTypeRim type = ColumnTypeRim::path; + int offset = 0; + int stretch = 0; + bool visible = false; +}; + +inline +std::vector getFileGridDefaultColAttribsLeft() +{ + using namespace zen; + return //harmonize with main_dlg.cpp::onGridLabelContextRim() => expects stretched path and non-stretched other columns! + { + {ColumnTypeRim::path, -dipToWxsize(100), 1, true }, + {ColumnTypeRim::extension, dipToWxsize( 60), 0, false}, + {ColumnTypeRim::date, dipToWxsize(140), 0, false}, //optimal: Ubuntu: 138, macOS: 121 + {ColumnTypeRim::size, dipToWxsize(100), 0, true }, //optimal: Ubuntu: 96, macOS: 94 for 2GB size + }; +} + +inline +std::vector getFileGridDefaultColAttribsRight() +{ + return getFileGridDefaultColAttribsLeft(); //*currently* same default +} + + +inline +bool getDefaultSortDirection(ColumnTypeRim type) //true: ascending; false: descending +{ + switch (type) + { + case ColumnTypeRim::size: + case ColumnTypeRim::date: + return false; + + case ColumnTypeRim::path: + case ColumnTypeRim::extension: + return true; + } + assert(false); + return true; +} + + +enum class ItemPathFormat +{ + name, + relative, + full, +}; + +const ItemPathFormat defaultItemPathFormatLeftGrid = ItemPathFormat::relative; +const ItemPathFormat defaultItemPathFormatRightGrid = ItemPathFormat::relative; + +//------------------------------------------------------------------ +enum class ColumnTypeCenter +{ + checkbox, + difference, + action, +}; + + +inline +bool getDefaultSortDirection(ColumnTypeCenter type) //true: ascending; false: descending +{ + assert(type != ColumnTypeCenter::checkbox); + return true; +} +//------------------------------------------------------------------ +} + +#endif //COLUMN_ATTR_H_189467891346732143213 diff --git a/FreeFileSync/Source/ui/file_view.cpp b/FreeFileSync/Source/ui/file_view.cpp new file mode 100644 index 0000000..33acb70 --- /dev/null +++ b/FreeFileSync/Source/ui/file_view.cpp @@ -0,0 +1,857 @@ +// ***************************************************************************** +// * 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 "file_view.h" +#include +#include +#include + +using namespace zen; +using namespace fff; + + +namespace +{ +void serializeHierarchy(ContainerObject& conObj, std::vector>& output) +{ + for (FilePair& file : conObj.files()) + output.push_back(file.weak_from_this()); + + for (SymlinkPair& symlink : conObj.symlinks()) + output.push_back(symlink.weak_from_this()); + + for (FolderPair& folder : conObj.subfolders()) + { + output.push_back(folder.weak_from_this()); + serializeHierarchy(folder, output); //add recursion here to list sub-objects directly below parent! + } + +#if 0 + /* Spend additional CPU cycles to sort the standard file list? + + Test case: 690.000 item pairs, Windows 7 x64 (C:\ vs I:\) + ---------------------- + CmpNaturalSort: 850 ms + CmpLocalPath: 233 ms + CmpAsciiNoCase: 189 ms + No sorting: 30 ms */ + + template + static std::vector getItemsSorted(std::list& itemList) + { + std::vector output; + for (ItemPair& item : itemList) + output.push_back(&item); + + std::sort(output.begin(), output.end(), [](const ItemPair* lhs, const ItemPair* rhs) { return LessNaturalSort()(lhs->getItemNameAny(), rhs->getItemNameAny()); }); + return output; + } +#endif +} +} + + +FileView::FileView(FolderComparison& folderCmp) +{ + for (BaseFolderPair& baseObj : asRange(folderCmp)) + //remove truly empty folder pairs as early as this: we want to distinguish single/multiple folder pair cases by looking at "folderPairs_" + if (!AFS::isNullPath(baseObj.getAbstractPath()) || + !AFS::isNullPath(baseObj.getAbstractPath())) + { + serializeHierarchy(baseObj, sortedRef_); + + folderPairs_.emplace_back(&baseObj, + baseObj.getAbstractPath(), + baseObj.getAbstractPath()); + } +} + + +template +void FileView::updateView(Predicate pred) +{ + viewRef_ .clear(); + groupDetails_ .clear(); + rowPositions_ .clear(); + rowPositionsFirstChild_.clear(); + + static uint64_t globalViewUpdateId; + viewUpdateId_ = ++globalViewUpdateId; + assert(runningOnMainThread()); + + std::vector parentsBuf; //from bottom to top of hierarchy + const ContainerObject* groupStartObj = nullptr; + + for (const std::weak_ptr& objRef : sortedRef_) + if (const FileSystemObject* fsObj = objRef.lock().get()) + if (pred(*fsObj)) + { + const size_t row = viewRef_.size(); + + //save row position for direct random access to FilePair or FolderPair + rowPositions_.emplace(fsObj, row); //costs: 0.28 µs per call - MSVC based on std::set + + parentsBuf.clear(); + for (const FileSystemObject* fsObj2 = fsObj;;) + { + const ContainerObject& parent = fsObj2->parent(); + parentsBuf.push_back(&parent); + + fsObj2 = dynamic_cast(&parent); + if (!fsObj2) + break; + } + + //save row position to identify first child *on sorted subview* of FolderPair or BaseFolderPair in case latter are filtered out + for (const ContainerObject* parent : parentsBuf) + if (const auto [it, inserted] = this->rowPositionsFirstChild_.emplace(parent, row); + !inserted) //=> parents further up in hierarchy already inserted! + break; + + //------ save info to aggregate rows by parent folders ------ + if (const auto folder = dynamic_cast(fsObj)) + { + groupStartObj = folder; + groupDetails_.push_back({row}); + } + else if (&fsObj->parent() != groupStartObj) + { + groupStartObj = &fsObj->parent(); + groupDetails_.push_back({row}); + } + assert(!groupDetails_.empty()); + const size_t groupIdx = groupDetails_.size() - 1; + //----------------------------------------------------------- + viewRef_.push_back({objRef, groupIdx}); + } +} + + +ptrdiff_t FileView::findRowDirect(const FileSystemObject* fsObj) const +{ + auto it = rowPositions_.find(fsObj); + return it != rowPositions_.end() ? it->second : -1; +} + + +ptrdiff_t FileView::findRowFirstChild(const ContainerObject* conObj) const +{ + auto it = rowPositionsFirstChild_.find(conObj); + return it != rowPositionsFirstChild_.end() ? it->second : -1; +} + + +namespace +{ +template +void addNumbers(const FileSystemObject& fsObj, ViewStats& stats) +{ + visitFSObject(fsObj, [&](const FolderPair& folder) + { + if (!folder.isEmpty()) + ++stats.fileStatsLeft.folderCount; + + if (!folder.isEmpty()) + ++stats.fileStatsRight.folderCount; + }, + + [&](const FilePair& file) + { + if (!file.isEmpty()) + { + stats.fileStatsLeft.bytes += file.getFileSize(); + ++stats.fileStatsLeft.fileCount; + } + if (!file.isEmpty()) + { + stats.fileStatsRight.bytes += file.getFileSize(); + ++stats.fileStatsRight.fileCount; + } + }, + + [&](const SymlinkPair& symlink) + { + if (!symlink.isEmpty()) + ++stats.fileStatsLeft.fileCount; + + if (!symlink.isEmpty()) + ++stats.fileStatsRight.fileCount; + }); +} +} + + +FileView::DifferenceViewStats FileView::applyDifferenceFilter(bool showExcluded, //maps sortedRef to viewRef + bool showLeftOnly, + bool showRightOnly, + bool showLeftNewer, + bool showRightNewer, + bool showDifferent, + bool showEqual, + bool showConflict) +{ + DifferenceViewStats stats; + + updateView([&](const FileSystemObject& fsObj) + { + auto categorize = [&](bool showCategory, int& categoryCount) + { + if (!fsObj.isActive()) + { + ++stats.excluded; + if (!showExcluded) + return false; + } + ++categoryCount; + if (!showCategory) + return false; + + addNumbers(fsObj, stats); //calculate total number of bytes for each side + return true; + }; + + switch (fsObj.getCategory()) + { + case FILE_LEFT_ONLY: + return categorize(showLeftOnly, stats.leftOnly); + case FILE_RIGHT_ONLY: + return categorize(showRightOnly, stats.rightOnly); + case FILE_LEFT_NEWER: + return categorize(showLeftNewer, stats.leftNewer); + case FILE_RIGHT_NEWER: + return categorize(showRightNewer, stats.rightNewer); + case FILE_DIFFERENT_CONTENT: + return categorize(showDifferent, stats.different); + case FILE_EQUAL: + return categorize(showEqual, stats.equal); + case FILE_RENAMED: + case FILE_CONFLICT: + case FILE_TIME_INVALID: + return categorize(showConflict, stats.conflict); + } + assert(false); + return true; + }); + + return stats; +} + + +FileView::ActionViewStats FileView::applyActionFilter(bool showExcluded, //maps sortedRef to viewRef + bool showCreateLeft, + bool showCreateRight, + bool showDeleteLeft, + bool showDeleteRight, + bool showUpdateLeft, + bool showUpdateRight, + bool showDoNothing, + bool showEqual, + bool showConflict) +{ + ActionViewStats stats; + + int moveLeft = 0; + int moveRight = 0; + + updateView([&](const FileSystemObject& fsObj) + { + auto categorize = [&](bool showCategory, int& categoryCount) + { + if (!fsObj.isActive()) + { + ++stats.excluded; + if (!showExcluded) + return false; + } + ++categoryCount; + if (!showCategory) + return false; + + addNumbers(fsObj, stats); //calculate total number of bytes for each side + return true; + }; + + switch (fsObj.getSyncOperation()) //evaluate comparison result and sync direction + { + case SO_CREATE_LEFT: + return categorize(showCreateLeft, stats.createLeft); + case SO_CREATE_RIGHT: + return categorize(showCreateRight, stats.createRight); + case SO_DELETE_LEFT: + return categorize(showDeleteLeft, stats.deleteLeft); + case SO_DELETE_RIGHT: + return categorize(showDeleteRight, stats.deleteRight); + case SO_OVERWRITE_LEFT: + case SO_RENAME_LEFT: + return categorize(showUpdateLeft, stats.updateLeft); + case SO_MOVE_LEFT_FROM: + case SO_MOVE_LEFT_TO: + return categorize(showUpdateLeft, moveLeft); + case SO_OVERWRITE_RIGHT: + case SO_RENAME_RIGHT: + return categorize(showUpdateRight, stats.updateRight); + case SO_MOVE_RIGHT_FROM: + case SO_MOVE_RIGHT_TO: + return categorize(showUpdateRight, moveRight); + case SO_DO_NOTHING: + return categorize(showDoNothing, stats.updateNone); + case SO_EQUAL: + return categorize(showEqual, stats.equal); + case SO_UNRESOLVED_CONFLICT: + return categorize(showConflict, stats.conflict); + } + assert(false); + return true; + }); + + assert(moveLeft % 2 == 0 && moveRight % 2 == 0); + stats.updateLeft += moveLeft / 2; //count move operations as single update + stats.updateRight += moveRight / 2; //=> harmonize with SyncStatistics::processFile() + + return stats; +} + + +std::vector FileView::getAllFileRef(const std::vector& rows) +{ + const size_t viewSize = viewRef_.size(); + + std::vector output; + + for (size_t pos : rows) + if (pos < viewSize) + if (const std::shared_ptr fsObj = viewRef_[pos].objRef.lock()) + output.push_back(fsObj.get()); + + return output; +} + + +FileView::PathDrawInfo FileView::getDrawInfo(size_t row) +{ + if (row < viewRef_.size()) + { + const size_t groupIdx = viewRef_[row].groupIdx; + assert(groupIdx < groupDetails_.size()); + + const size_t groupFirstRow = groupDetails_[groupIdx].groupFirstRow; + + const size_t groupLastRow = groupIdx + 1 < groupDetails_.size() ? + groupDetails_[groupIdx + 1].groupFirstRow : + viewRef_.size(); + FileSystemObject* fsObj = viewRef_[row].objRef.lock().get(); + + FolderPair* folderGroupObj = dynamic_cast(fsObj); + if (fsObj && !folderGroupObj) + folderGroupObj = dynamic_cast(&fsObj->parent()); + + return {groupFirstRow, groupLastRow, groupIdx, viewUpdateId_, folderGroupObj, fsObj}; + } + assert(false); //unexpected: check rowsOnView()! + return {}; +} + + +void FileView::removeInvalidRows() +{ + //remove rows that have been deleted meanwhile + std::erase_if(sortedRef_, [&](const std::weak_ptr& objRef) { return objRef.expired(); }); + + viewRef_ .clear(); + groupDetails_ .clear(); + rowPositions_ .clear(); + rowPositionsFirstChild_.clear(); +} + + +//------------------------------------ SORTING ----------------------------------------- +namespace +{ +struct CompileTimeReminder : public FSObjectVisitor +{ + void visit(const FilePair& file ) override {} + void visit(const SymlinkPair& symlink) override {} + void visit(const FolderPair& folder ) override {} +} checkDymanicCasts; //just a compile-time reminder to manually check dynamic casts in this file if ever needed + + +inline +bool isDirectoryPair(const FileSystemObject& fsObj) +{ + return dynamic_cast(&fsObj) != nullptr; +} + + +template inline +bool lessFileName(const FileSystemObject& lhs, const FileSystemObject& rhs) +{ + //sort order: first files/symlinks, then directories then empty rows + + //empty rows always last + if (lhs.isEmpty()) + return false; + else if (rhs.isEmpty()) + return true; + + //directories after files/symlinks: + if (isDirectoryPair(lhs)) + { + if (!isDirectoryPair(rhs)) + return false; + } + else if (isDirectoryPair(rhs)) + return true; + + return zen::makeSortDirection(LessNaturalSort() /*even on Linux*/, std::bool_constant())(lhs.getItemName(), rhs.getItemName()); +} + + +template inline +bool lessFilePath(const std::weak_ptr& lhs, const std::weak_ptr& rhs, + const std::unordered_map& sortedPos, + std::vector& tempBuf) +{ + const FileSystemObject* fsObjL = lhs.lock().get(); + const FileSystemObject* fsObjR = rhs.lock().get(); + if (!fsObjL) //invalid rows shall appear at the end + return false; + else if (!fsObjR) + return true; + + //------- presort by folder pair ---------- + { + auto itL = sortedPos.find(&fsObjL->base()); + auto itR = sortedPos.find(&fsObjR->base()); + assert(itL != sortedPos.end() && itR != sortedPos.end()); + if (itL == sortedPos.end()) //invalid rows shall appear at the end + return false; + else if (itR == sortedPos.end()) + return true; + + const size_t basePosL = itL->second; + const size_t basePosR = itR->second; + + if (basePosL != basePosR) + return zen::makeSortDirection(std::less(), std::bool_constant())(basePosL, basePosR); + } + + //------- sort component-wise ---------- + const auto folderL = dynamic_cast(fsObjL); + const auto folderR = dynamic_cast(fsObjR); + + std::vector& parentsBuf = tempBuf; //from bottom to top of hierarchy, excluding base + parentsBuf.clear(); + + const auto collectParents = [&](const FileSystemObject* fsObj) + { + for (;;) + if (const auto folder = dynamic_cast(&fsObj->parent())) //perf: most expensive part of this function! + { + parentsBuf.push_back(folder); + fsObj = folder; + } + else + break; + }; + if (folderL) + parentsBuf.push_back(folderL); + collectParents(fsObjL); + const size_t parentsSizeL = parentsBuf.size(); + + if (folderR) + parentsBuf.push_back(folderR); + collectParents(fsObjR); + + const std::span parentsL(parentsBuf.data(), parentsSizeL); //no construction via iterator (yet): https://github.com/cplusplus/draft/pull/3456 + const std::span parentsR(parentsBuf.data() + parentsSizeL, parentsBuf.size() - parentsSizeL); + + const auto& [itL, itR] = std::mismatch(parentsL.rbegin(), parentsL.rend(), + parentsR.rbegin(), parentsR.rend()); + if (itL == parentsL.rend()) + { + if (itR == parentsR.rend()) + { + //make folders always appear before contained files + if (folderR) + return false; + else if (folderL) + return true; + + return zen::makeSortDirection(LessNaturalSort(), std::bool_constant())(fsObjL->getItemName(), fsObjR->getItemName()); + } + else + return true; + } + else if (itR == parentsR.rend()) + return false; + + //different components... + if (const std::weak_ordering cmp = compareNatural((*itL)->getItemName(), (*itR)->getItemName()); + cmp != std::weak_ordering::equivalent) + { + if constexpr (ascending) + return std::is_lt(cmp); + else + return std::is_gt(cmp); + } + //return zen::makeSortDirection(std::less(), std::bool_constant())(rv, 0); + + /*...with equivalent names: + 1. functional correctness => must not compare equal! e.g. a/a/x and a/A/y + 2. ensure stable sort order */ + return *itL < *itR; +} + + +template inline +bool lessFilesize(const FileSystemObject& lhs, const FileSystemObject& rhs) +{ + //empty rows always last + if (lhs.isEmpty()) + return false; + else if (rhs.isEmpty()) + return true; + + //directories second last + if (isDirectoryPair(lhs)) + return false; + else if (isDirectoryPair(rhs)) + return true; + + const FilePair* fileL = dynamic_cast(&lhs); + const FilePair* fileR = dynamic_cast(&rhs); + + //then symlinks + if (!fileL) + return false; + else if (!fileR) + return true; + + //return list beginning with largest files first + return zen::makeSortDirection(std::less(), std::bool_constant())(fileL->getFileSize(), fileR->getFileSize()); +} + + +template inline +bool lessFiletime(const FileSystemObject& lhs, const FileSystemObject& rhs) +{ + if (lhs.isEmpty()) + return false; //empty rows always last + else if (rhs.isEmpty()) + return true; //empty rows always last + + const FilePair* fileL = dynamic_cast(&lhs); + const FilePair* fileR = dynamic_cast(&rhs); + + const SymlinkPair* symlinkL = dynamic_cast(&lhs); + const SymlinkPair* symlinkR = dynamic_cast(&rhs); + + if (!fileL && !symlinkL) + return false; //directories last + else if (!fileR && !symlinkR) + return true; //directories last + + const int64_t dateL = fileL ? fileL->getLastWriteTime() : symlinkL->getLastWriteTime(); + const int64_t dateR = fileR ? fileR->getLastWriteTime() : symlinkR->getLastWriteTime(); + + //return list beginning with newest files first + return zen::makeSortDirection(std::less(), std::bool_constant())(dateL, dateR); +} + + +template inline +bool lessExtension(const FileSystemObject& lhs, const FileSystemObject& rhs) +{ + if (lhs.isEmpty()) + return false; //empty rows always last + else if (rhs.isEmpty()) + return true; //empty rows always last + + if (dynamic_cast(&lhs)) + return false; //directories last + else if (dynamic_cast(&rhs)) + return true; //directories last + + auto getExtension = [](const FileSystemObject& fsObj) + { + return afterLast(fsObj.getItemName(), Zstr('.'), zen::IfNotFoundReturn::none); + }; + + return zen::makeSortDirection(LessNaturalSort() /*even on Linux*/, std::bool_constant())(getExtension(lhs), getExtension(rhs)); +} + + +template inline +bool lessCmpResult(const FileSystemObject& lhs, const FileSystemObject& rhs) +{ + return zen::makeSortDirection([](CompareFileResult lhs2, CompareFileResult rhs2) + { + //presort: equal shall appear at end of list + if (lhs2 == FILE_EQUAL) + return false; + if (rhs2 == FILE_EQUAL) + return true; + return lhs2 < rhs2; + }, + std::bool_constant())(lhs.getCategory(), rhs.getCategory()); +} + + +template inline +bool lessSyncDirection(const FileSystemObject& lhs, const FileSystemObject& rhs) +{ + return zen::makeSortDirection(std::less(), std::bool_constant())(lhs.getSyncOperation(), rhs.getSyncOperation()); +} + + +template +struct LessFullPath +{ + LessFullPath(std::vector> folderPairs) + { + //calculate positions of base folders sorted by name + std::sort(folderPairs.begin(), folderPairs.end(), [](const auto& a, const auto& b) + { + const auto& [baseObjA, basePathLA, basePathRA] = a; + const auto& [baseObjB, basePathLB, basePathRB] = b; + + const AbstractPath& basePathA = selectParam(basePathLA, basePathRA); + const AbstractPath& basePathB = selectParam(basePathLB, basePathRB); + + return LessNaturalSort()/*even on Linux*/(utfTo(AFS::getDisplayPath(basePathA)), + utfTo(AFS::getDisplayPath(basePathB))); + }); + + size_t pos = 0; + for (const auto& [baseObj, basePathL, basePathR] : folderPairs) + shared_.ref().sortedPos.emplace(baseObj, pos++); + } + + bool operator()(const std::weak_ptr& lhs, const std::weak_ptr& rhs) const + { + return lessFilePath(lhs, rhs, shared_.ref().sortedPos, shared_.ref().tempBuf); + } + +private: + struct Shared + { + std::unordered_map sortedPos; + mutable std::vector tempBuf; //avoid repeated memory allocation in lessFilePath() + }; + SharedRef shared_ = makeSharedRef(); //std::sort makes lots of predicate copies during its "divide and conquer" +}; + + +template +struct LessRelativeFolder +{ + LessRelativeFolder(const std::vector>& folderPairs) + { + size_t pos = 0; //take over positions of base folders as set up by user + for (const auto& [baseObj, basePathL, basePathR] : folderPairs) + shared_.ref().sortedPos.emplace(baseObj, pos++); + } + + bool operator()(const std::weak_ptr& lhs, const std::weak_ptr& rhs) const + { + return lessFilePath(lhs, rhs, shared_.ref().sortedPos, shared_.ref().tempBuf); + } + +private: + struct Shared + { + std::unordered_map sortedPos; + mutable std::vector tempBuf; //avoid repeated memory allocation in lessFilePath() + }; + SharedRef shared_ = makeSharedRef(); //std::sort makes lots of predicate copies during its "divide and conquer" +}; + + +template +struct LessFileName +{ + bool operator()(const std::weak_ptr& lhs, const std::weak_ptr& rhs) const + { + const std::shared_ptr fsObjL = lhs.lock(); + const std::shared_ptr fsObjR = rhs.lock(); + if (!fsObjL) //invalid rows shall appear at the end + return false; + else if (!fsObjR) + return true; + + return lessFileName(*fsObjL, *fsObjR); + } +}; + + +template +struct LessFilesize +{ + bool operator()(const std::weak_ptr& lhs, const std::weak_ptr& rhs) const + { + const std::shared_ptr fsObjL = lhs.lock(); + const std::shared_ptr fsObjR = rhs.lock(); + if (!fsObjL) //invalid rows shall appear at the end + return false; + else if (!fsObjR) + return true; + + return lessFilesize(*fsObjL, *fsObjR); + } +}; + + +template +struct LessFiletime +{ + bool operator()(const std::weak_ptr& lhs, const std::weak_ptr& rhs) const + { + const std::shared_ptr fsObjL = lhs.lock(); + const std::shared_ptr fsObjR = rhs.lock(); + if (!fsObjL) //invalid rows shall appear at the end + return false; + else if (!fsObjR) + return true; + + return lessFiletime(*fsObjL, *fsObjR); + } +}; + + +template +struct LessExtension +{ + bool operator()(const std::weak_ptr& lhs, const std::weak_ptr& rhs) const + { + const std::shared_ptr fsObjL = lhs.lock(); + const std::shared_ptr fsObjR = rhs.lock(); + if (!fsObjL) //invalid rows shall appear at the end + return false; + else if (!fsObjR) + return true; + + return lessExtension(*fsObjL, *fsObjR); + } +}; + + +template +struct LessCmpResult +{ + bool operator()(const std::weak_ptr& lhs, const std::weak_ptr& rhs) const + { + const std::shared_ptr fsObjL = lhs.lock(); + const std::shared_ptr fsObjR = rhs.lock(); + if (!fsObjL) //invalid rows shall appear at the end + return false; + else if (!fsObjR) + return true; + + return lessCmpResult(*fsObjL, *fsObjR); + } +}; + + +template +struct LessSyncDirection +{ + bool operator()(const std::weak_ptr& lhs, const std::weak_ptr& rhs) const + { + const std::shared_ptr fsObjL = lhs.lock(); + const std::shared_ptr fsObjR = rhs.lock(); + if (!fsObjL) //invalid rows shall appear at the end + return false; + else if (!fsObjR) + return true; + + return lessSyncDirection(*fsObjL, *fsObjR); + } +}; +} + +//------------------------------------------------------------------------------------------------------- + +void FileView::sortView(ColumnTypeRim type, ItemPathFormat pathFmt, bool onLeft, bool ascending) +{ + viewRef_ .clear(); + groupDetails_ .clear(); + rowPositions_ .clear(); + rowPositionsFirstChild_.clear(); + currentSort_ = SortInfo({type, onLeft, ascending}); + + switch (type) + { + case ColumnTypeRim::path: + switch (pathFmt) + { + case ItemPathFormat::name: + if ( ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFileName()); + else if ( ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFileName()); + else if (!ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFileName()); + else if (!ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFileName()); + break; + + case ItemPathFormat::relative: + if ( ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessRelativeFolder(folderPairs_)); + else if ( ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessRelativeFolder(folderPairs_)); + else if (!ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessRelativeFolder(folderPairs_)); + else if (!ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessRelativeFolder(folderPairs_)); + break; + + case ItemPathFormat::full: + if ( ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFullPath(folderPairs_)); + else if ( ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFullPath(folderPairs_)); + else if (!ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFullPath(folderPairs_)); + else if (!ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFullPath(folderPairs_)); + break; + } + break; + + case ColumnTypeRim::size: + if ( ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFilesize()); + else if ( ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFilesize()); + else if (!ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFilesize()); + else if (!ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFilesize()); + break; + case ColumnTypeRim::date: + if ( ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFiletime()); + else if ( ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFiletime()); + else if (!ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFiletime()); + else if (!ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFiletime()); + break; + case ColumnTypeRim::extension: + if ( ascending && onLeft) std::stable_sort(sortedRef_.begin(), sortedRef_.end(), LessExtension()); + else if ( ascending && !onLeft) std::stable_sort(sortedRef_.begin(), sortedRef_.end(), LessExtension()); + else if (!ascending && onLeft) std::stable_sort(sortedRef_.begin(), sortedRef_.end(), LessExtension()); + else if (!ascending && !onLeft) std::stable_sort(sortedRef_.begin(), sortedRef_.end(), LessExtension()); + break; + } +} + + +void FileView::sortView(ColumnTypeCenter type, bool ascending) +{ + viewRef_ .clear(); + groupDetails_ .clear(); + rowPositions_ .clear(); + rowPositionsFirstChild_.clear(); + currentSort_ = SortInfo({type, false, ascending}); + + switch (type) + { + case ColumnTypeCenter::checkbox: + assert(false); + break; + case ColumnTypeCenter::difference: + if ( ascending) std::stable_sort(sortedRef_.begin(), sortedRef_.end(), LessCmpResult()); + else if (!ascending) std::stable_sort(sortedRef_.begin(), sortedRef_.end(), LessCmpResult()); + break; + case ColumnTypeCenter::action: + if ( ascending) std::stable_sort(sortedRef_.begin(), sortedRef_.end(), LessSyncDirection()); + else if (!ascending) std::stable_sort(sortedRef_.begin(), sortedRef_.end(), LessSyncDirection()); + break; + } +} diff --git a/FreeFileSync/Source/ui/file_view.h b/FreeFileSync/Source/ui/file_view.h new file mode 100644 index 0000000..fd05c33 --- /dev/null +++ b/FreeFileSync/Source/ui/file_view.h @@ -0,0 +1,163 @@ +// ***************************************************************************** +// * 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 GRID_VIEW_H_9285028345703475842569 +#define GRID_VIEW_H_9285028345703475842569 + +#include +#include +#include +#include +#include "file_grid_attr.h" +#include "../base/file_hierarchy.h" + + +namespace fff +{ +class FileView //grid view of FolderComparison +{ +public: + FileView() {} + explicit FileView(FolderComparison& folderCmp); //takes weak (non-owning) references + + size_t rowsOnView() const { return viewRef_ .size(); } //only visible elements + size_t rowsTotal () const { return sortedRef_.size(); } //total rows available + + //returns nullptr if object is not found; complexity: constant! + const FileSystemObject* getFsObject(size_t row) const { return row < viewRef_.size() ? viewRef_[row].objRef.lock().get() : nullptr; } + /**/ FileSystemObject* getFsObject(size_t row) { return const_cast(static_cast(*this).getFsObject(row)); } //see Meyers Effective C++ + + //references to FileSystemObject: no nullptr-check needed! everything is bound + std::vector getAllFileRef(const std::vector& rows); + + struct PathDrawInfo + { + size_t groupFirstRow = 0; //half-open range + size_t groupLastRow = 0; // + const size_t groupIdx = 0; + uint64_t viewUpdateId = 0; //help detect invalid buffers after updateView() + FolderPair* folderGroupObj = nullptr; //nullptr if group is BaseFolderPair (or fsObj not found) + FileSystemObject* fsObj = nullptr; //nullptr if object is not found + }; + PathDrawInfo getDrawInfo(size_t row); //complexity: constant! + + struct FileStats + { + int fileCount = 0; + int folderCount = 0; + uint64_t bytes = 0; + }; + + struct DifferenceViewStats + { + int excluded = 0; + int equal = 0; + int conflict = 0; + + int leftOnly = 0; + int rightOnly = 0; + int leftNewer = 0; + int rightNewer = 0; + int different = 0; + + FileStats fileStatsLeft; + FileStats fileStatsRight; + }; + DifferenceViewStats applyDifferenceFilter(bool showExcluded, + bool showLeftOnly, + bool showRightOnly, + bool showLeftNewer, + bool showRightNewer, + bool showDifferent, + bool showEqual, + bool showConflict); + struct ActionViewStats + { + int excluded = 0; + int equal = 0; + int conflict = 0; + + int createLeft = 0; + int createRight = 0; + int deleteLeft = 0; + int deleteRight = 0; + int updateLeft = 0; + int updateRight = 0; + int updateNone = 0; + + FileStats fileStatsLeft; + FileStats fileStatsRight; + }; + ActionViewStats applyActionFilter(bool showExcluded, + bool showCreateLeft, + bool showCreateRight, + bool showDeleteLeft, + bool showDeleteRight, + bool showUpdateLeft, + bool showUpdateRight, + bool showDoNothing, + bool showEqual, + bool showConflict); + + void removeInvalidRows(); //remove references to rows that have been deleted meanwhile: call after manual deletion and synchronization! + + //sorting... + void sortView(ColumnTypeRim type, ItemPathFormat pathFmt, bool onLeft, bool ascending); //always call these; never sort externally! + void sortView(ColumnTypeCenter type, bool ascending); // + + struct SortInfo + { + std::variant sortCol; + bool onLeft = false; //only use if sortCol is ColumnTypeRim + bool ascending = false; + }; + const SortInfo* getSortConfig() const { return zen::get(currentSort_); } //return nullptr if currently not sorted + + ptrdiff_t findRowDirect (const FileSystemObject* fsObj) const; //find an object's row position on view list directly, return < 0 if not found + ptrdiff_t findRowFirstChild(const ContainerObject* conObj) const; //find first child of FolderPair or BaseFolderPair *on sorted sub view* + //"conObj" may be invalid, it is NOT dereferenced, return < 0 if not found + + //count non-empty pairs to distinguish single/multiple folder pair cases + size_t getEffectiveFolderPairCount() const { return folderPairs_.size(); } + +private: + FileView (const FileView&) = delete; + FileView& operator=(const FileView&) = delete; + + template void updateView(Predicate pred); + + + std::unordered_map rowPositions_; //find row positions on viewRef_ directly + std::unordered_map rowPositionsFirstChild_; //find first child on sortedRef of a container object + //void* instead of ContainerObject*: these pointers should *never be dereferenced*! + + struct GroupDetail + { + size_t groupFirstRow = 0; + }; + std::vector groupDetails_; + + uint64_t viewUpdateId_ = 0; //help clients detect invalid buffers after updateView() + + struct ViewRow + { + std::weak_ptr objRef; + size_t groupIdx = 0; //...into groupDetails_ + }; + std::vector viewRef_; //partial view on sortedRef_ + /* /|\ + | (applyFilterBy...) */ + std::vector> sortedRef_; //flat view of weak pointers on folderCmp; may be sorted + /* /|\ + | (constructor) + FolderComparison folderCmp */ + std::vector> folderPairs_; + + std::optional currentSort_; +}; +} + +#endif //GRID_VIEW_H_9285028345703475842569 diff --git a/FreeFileSync/Source/ui/folder_history_box.cpp b/FreeFileSync/Source/ui/folder_history_box.cpp new file mode 100644 index 0000000..2508f45 --- /dev/null +++ b/FreeFileSync/Source/ui/folder_history_box.cpp @@ -0,0 +1,139 @@ +// ***************************************************************************** +// * 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 "folder_history_box.h" +#include + #include +#include "../afs/concrete.h" + +using namespace zen; +using namespace fff; +using AFS = AbstractFileSystem; + + +FolderHistoryBox::FolderHistoryBox(wxWindow* parent, + wxWindowID id, + const wxString& value, + const wxPoint& pos, + const wxSize& size, + int n, + const wxString choices[], + long style, + const wxValidator& validator, + const wxString& name) : + wxComboBox(parent, id, value, pos, size, n, choices, style, validator, name) +{ + //##################################### + /*##*/ SetMinSize({dipToWxsize(150), -1}); //## workaround yet another wxWidgets bug: default minimum size is much too large for a wxComboBox + //##################################### + + Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onKeyEvent(event); }); + + /* + we can't attach to wxEVT_COMMAND_TEXT_UPDATED, since setValueAndUpdateList() will implicitly emit wxEVT_COMMAND_TEXT_UPDATED again when calling Clear()! + => Crash on Suse/X11/wxWidgets 2.9.4 on startup (setting a flag to guard against recursion does not work, still crash) + + On OS X attaching to wxEVT_LEFT_DOWN leads to occasional crashes, especially when double-clicking + */ + + //file drag and drop directly into the text control unhelpfully inserts in format "file://.." + //1. this format's implementation is a mess: http://www.lephpfacile.com/manuel-php-gtk/tutorials.filednd.urilist.php + //2. even if we handle "drag-data-received" for "text/uri-list" this doesn't consider logic in dirname.cpp + //=> disable all drop events on the text control (disables text drop, too, but not a big loss) + //=> all drops are nicely propagated as regular file drop events like they should have been in the first place! + if (GtkWidget* widget = GetConnectWidget()) + ::gtk_drag_dest_unset(widget); +} + + +void FolderHistoryBox::onRequireHistoryUpdate(wxEvent& event) +{ + setValueAndUpdateList(GetValue()); + event.Skip(); +} + + +//set value and update list are technically entangled: see potential bug description below +void FolderHistoryBox::setValueAndUpdateList(const wxString& folderPathPhrase) +{ + //populate selection list.... + std::vector items; + { + auto trimTrailingSep = [](Zstring path) + { + if (endsWith(path, Zstr('/')) || + endsWith(path, Zstr('\\'))) + path.pop_back(); + return path; + }; + + const Zstring& folderPathPhraseTrimmed = trimTrailingSep(trimCpy(utfTo(folderPathPhrase))); + + //path phrase aliases: allow user changing to volume name and back + for (const Zstring& aliasPhrase : AFS::getPathPhraseAliases(createAbstractPath(utfTo(folderPathPhrase)))) //may block when resolving [] + if (!equalNoCase(folderPathPhraseTrimmed, + trimTrailingSep(aliasPhrase))) //don't add redundant aliases + items.push_back(utfTo(aliasPhrase)); + } + + if (sharedHistory_.get()) + { + std::vector tmp = sharedHistory_->getList(); + std::sort(tmp.begin(), tmp.end(), LessNaturalSort() /*even on Linux*/); + + if (!items.empty() && !tmp.empty()) + items.push_back(HistoryList::separationLine()); + + for (const Zstring& str : tmp) + items.push_back(utfTo(str)); + } + + //########################################################################################### + + //attention: if the target value is not part of the dropdown list, SetValue() will look for a string that *starts with* this value: + //e.g. if the dropdown list contains "222" SetValue("22") will erroneously set and select "222" instead, while "111" would be set correctly! + // -> by design on Windows! + if (std::find(items.begin(), items.end(), folderPathPhrase) == items.end()) + items.insert(items.begin(), folderPathPhrase); + + //this->Clear(); -> NO! emits yet another wxEVT_COMMAND_TEXT_UPDATED!!! + wxItemContainer::Clear(); //suffices to clear the selection items only! + this->Append(items); //expensive as fuck! => only call when absolutely needed! + + //this->SetSelection(wxNOT_FOUND); //don't select anything + ChangeValue(folderPathPhrase); //preserve main text! +} + + +void FolderHistoryBox::onKeyEvent(wxKeyEvent& event) +{ + const int keyCode = event.GetKeyCode(); + + if (keyCode == WXK_DELETE || + keyCode == WXK_NUMPAD_DELETE) + //try to delete the currently selected config history item + if (const int pos = this->GetCurrentSelection(); + 0 <= pos && pos < static_cast(this->GetCount()) && + //what a mess...: + (GetValue() != GetString(pos) || //avoid problems when a character shall be deleted instead of list item + GetValue().empty())) //exception: always allow removing empty entry + { + //save old (selected) value: deletion seems to have influence on this + const wxString currentVal = this->GetValue(); + //this->SetSelection(wxNOT_FOUND); + + //delete selected row + if (sharedHistory_.get()) + sharedHistory_->delItem(utfTo(GetString(pos))); + SetString(pos, wxString()); //in contrast to "Delete(pos)", this one does not kill the drop-down list and gives a nice visual feedback! + + this->SetValue(currentVal); + return; //eat up key event + } + + + event.Skip(); +} diff --git a/FreeFileSync/Source/ui/folder_history_box.h b/FreeFileSync/Source/ui/folder_history_box.h new file mode 100644 index 0000000..a47c70c --- /dev/null +++ b/FreeFileSync/Source/ui/folder_history_box.h @@ -0,0 +1,91 @@ +// ***************************************************************************** +// * 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 FOLDER_HISTORY_BOX_H_08170517045945 +#define FOLDER_HISTORY_BOX_H_08170517045945 + +#include +#include +#include +#include + + +namespace fff +{ +class HistoryList +{ +public: + HistoryList(const std::vector& folderPathPhrases, size_t maxSize) : + maxSize_(maxSize), + folderPathPhrases_(folderPathPhrases) { truncate(); } + + const std::vector& getList() const { return folderPathPhrases_; } + + static const wxString separationLine() { return wxString(50, EM_DASH); } + + void addItem(Zstring folderPathPhrase) + { + zen::trim(folderPathPhrase); + + if (folderPathPhrase.empty() || folderPathPhrase == zen::utfTo(separationLine())) + return; + + //insert new folder or put it to the front if already existing + std::erase_if(folderPathPhrases_, [&](const Zstring& item) { return equalNoCase(item, folderPathPhrase); }); + + folderPathPhrases_.insert(folderPathPhrases_.begin(), folderPathPhrase); + truncate(); + } + + void delItem(const Zstring& folderPathPhrase) { std::erase_if(folderPathPhrases_, [&](const Zstring& item) { return equalNoCase(item, folderPathPhrase); }); } + +private: + void truncate() + { + if (folderPathPhrases_.size() > maxSize_) //keep maximal size of history list + folderPathPhrases_.resize(maxSize_); + } + + const size_t maxSize_ = 0; + std::vector folderPathPhrases_; +}; + + +//combobox with history function + functionality to delete items (DEL) +class FolderHistoryBox : public wxComboBox +{ +public: + FolderHistoryBox(wxWindow* parent, + wxWindowID id, + const wxString& value = {}, + const wxPoint& pos = wxDefaultPosition, + const wxSize& size = wxDefaultSize, + int n = 0, + const wxString choices[] = nullptr, + long style = 0, + const wxValidator& validator = wxDefaultValidator, + const wxString& name = wxASCII_STR(wxComboBoxNameStr)); + + void setHistory(std::shared_ptr sharedHistory) { sharedHistory_ = std::move(sharedHistory); } + std::shared_ptr getHistory() { return sharedHistory_; } + + void setValue(const wxString& folderPathPhrase) + { + setValueAndUpdateList(folderPathPhrase); //required for setting value correctly; Linux: ensure the dropdown is shown as being populated + } + + //wxString wxComboBox::GetValue() const; + +private: + void onKeyEvent(wxKeyEvent& event); + void onRequireHistoryUpdate(wxEvent& event); + void setValueAndUpdateList(const wxString& folderPathPhrase); + + std::shared_ptr sharedHistory_; +}; +} + +#endif //FOLDER_HISTORY_BOX_H_08170517045945 diff --git a/FreeFileSync/Source/ui/folder_pair.h b/FreeFileSync/Source/ui/folder_pair.h new file mode 100644 index 0000000..4f2a46a --- /dev/null +++ b/FreeFileSync/Source/ui/folder_pair.h @@ -0,0 +1,240 @@ +// ***************************************************************************** +// * 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 FOLDER_PAIR_H_89341750847252345 +#define FOLDER_PAIR_H_89341750847252345 + +#include +#include +#include +#include +#include "../base/norm_filter.h" + + +namespace fff +{ +//basic functionality for handling alternate folder pair configuration: change sync-cfg/filter cfg, right-click context menu, button icons... +std::wstring getFilterSummaryForTooltip(const FilterConfig& filterCfg); + + +template +class FolderPairPanelBasic : private wxEvtHandler +{ +public: + explicit FolderPairPanelBasic(GuiPanel& basicPanel) : //takes reference on basic panel to be enhanced + basicPanel_(basicPanel) + { + using namespace zen; + + //register events for removal of alternate configuration + basicPanel_.m_bpButtonLocalCompCfg ->Bind(wxEVT_RIGHT_DOWN, [this](wxMouseEvent& event) { onLocalCompCfgContext (event); }); + basicPanel_.m_bpButtonLocalSyncCfg ->Bind(wxEVT_RIGHT_DOWN, [this](wxMouseEvent& event) { onLocalSyncCfgContext (event); }); + basicPanel_.m_bpButtonLocalFilter ->Bind(wxEVT_RIGHT_DOWN, [this](wxMouseEvent& event) { onLocalFilterCfgContext(event); }); + + setImage(*basicPanel_.m_bpButtonRemovePair, loadImage("item_remove")); + } + + + void setConfig(const std::optional& compConfig, const std::optional& syncCfg, const FilterConfig& filter) + { + localCmpCfg_ = compConfig; + localSyncCfg_ = syncCfg; + localFilter_ = filter; + refreshButtons(); + } + + std::optional getCompConfig () const { return localCmpCfg_; } + std::optional getSyncConfig () const { return localSyncCfg_; } + FilterConfig getFilterConfig() const { return localFilter_; } + +private: + void refreshButtons() + { + using namespace zen; + + setImage(*basicPanel_.m_bpButtonLocalCompCfg, greyScaleIfDisabled(imgCmp_, !!localCmpCfg_)); + basicPanel_.m_bpButtonLocalCompCfg->SetToolTip(localCmpCfg_ ? + _("Local comparison settings") + L"\n(" + getVariantName(localCmpCfg_->compareVar) + L')' : + _("Local comparison settings")); + + setImage(*basicPanel_.m_bpButtonLocalSyncCfg, greyScaleIfDisabled(imgSync_, !!localSyncCfg_)); + basicPanel_.m_bpButtonLocalSyncCfg->SetToolTip(localSyncCfg_ ? + _("Local synchronization settings") + L"\n(" + getVariantName(getSyncVariant(localSyncCfg_->directionCfg)) + L')' : + _("Local synchronization settings")); + + setImage(*basicPanel_.m_bpButtonLocalFilter, greyScaleIfDisabled(imgFilter_, !isNullFilter(localFilter_))); + basicPanel_.m_bpButtonLocalFilter->SetToolTip(_("Local filter") + getFilterSummaryForTooltip(localFilter_)); + } + + void onLocalCompCfgContext(wxEvent& event) + { + using namespace zen; + + ContextMenu menu; + + auto setVariant = [&](CompareVariant var) + { + if (!this->localCmpCfg_) + this->localCmpCfg_ = CompConfig(); + this->localCmpCfg_->compareVar = var; + + this->refreshButtons(); + this->onLocalCompCfgChange(); + }; + + auto addVariantItem = [&](CompareVariant cmpVar, const char* iconName) + { + const wxImage imgSel = loadImage(iconName, -1 /*maxWidth*/, dipToScreen(getMenuIconDipSize())); + + menu.addItem(getVariantName(cmpVar), [&setVariant, cmpVar] { setVariant(cmpVar); }, + greyScaleIfDisabled(imgSel, this->localCmpCfg_ && this->localCmpCfg_->compareVar == cmpVar)); + }; + addVariantItem(CompareVariant::timeSize, "cmp_time"); + addVariantItem(CompareVariant::content, "cmp_content"); + addVariantItem(CompareVariant::size, "cmp_size"); + + //---------------------------------------------------------------------------------------- + menu.addSeparator(); + + auto removeLocalCompCfg = [&] + { + this->localCmpCfg_ = {}; //"this->" galore: workaround GCC compiler bugs + this->refreshButtons(); + this->onLocalCompCfgChange(); + }; + menu.addItem(_("Remove local settings"), removeLocalCompCfg, wxNullImage, static_cast(localCmpCfg_)); + menu.popup(*basicPanel_.m_bpButtonLocalCompCfg, {basicPanel_.m_bpButtonLocalCompCfg->GetSize().x, 0}); + } + + void onLocalSyncCfgContext(wxEvent& event) + { + using namespace zen; + + ContextMenu menu; + + auto setVariant = [&](SyncVariant var) + { + if (!this->localSyncCfg_) + this->localSyncCfg_ = SyncConfig(); + this->localSyncCfg_->directionCfg = getDefaultSyncCfg(var); + + this->refreshButtons(); + this->onLocalSyncCfgChange(); + }; + + auto addVariantItem = [&](SyncVariant syncVar, const char* iconName) + { + const wxImage imgSel = mirrorIfRtl(loadImage(iconName, -1 /*maxWidth*/, dipToScreen(getMenuIconDipSize()))); + + menu.addItem(getVariantName(syncVar), [&setVariant, syncVar] { setVariant(syncVar); }, + greyScaleIfDisabled(imgSel, this->localSyncCfg_ && getSyncVariant(this->localSyncCfg_->directionCfg) == syncVar)); + }; + addVariantItem(SyncVariant::twoWay, "sync_twoway"); + addVariantItem(SyncVariant::mirror, "sync_mirror"); + addVariantItem(SyncVariant::update, "sync_update"); + //addVariantItem(SyncVariant::custom, "sync_custom"); -> doesn't make sense, does it? + + //---------------------------------------------------------------------------------------- + menu.addSeparator(); + + auto removeLocalSyncCfg = [&] + { + this->localSyncCfg_ = {}; + this->refreshButtons(); + this->onLocalSyncCfgChange(); + }; + menu.addItem(_("Remove local settings"), removeLocalSyncCfg, wxNullImage, static_cast(localSyncCfg_)); + menu.popup(*basicPanel_.m_bpButtonLocalSyncCfg, {basicPanel_.m_bpButtonLocalSyncCfg->GetSize().x, 0}); + } + + void onLocalFilterCfgContext(wxEvent& event) + { + using namespace zen; + + std::optional filterCfgOnClipboard; + if (std::optional clipTxt = getClipboardText()) + filterCfgOnClipboard = parseFilterBuf(utfTo(*clipTxt)); + + auto cutFilter = [&] + { + setClipboardText(utfTo(serializeFilter(this->localFilter_))); + this->localFilter_ = FilterConfig(); + this->refreshButtons(); + this->onLocalFilterCfgChange(); + }; + + auto copyFilter = [&] { setClipboardText(utfTo(serializeFilter(this->localFilter_))); }; + + auto pasteFilter = [&] + { + this->localFilter_ = *filterCfgOnClipboard; + this->refreshButtons(); + this->onLocalFilterCfgChange(); + }; + + zen::ContextMenu menu; + menu.addItem( _("&Copy"), copyFilter, loadImage("item_copy_sicon"), !isNullFilter(localFilter_)); + menu.addItem( _("&Paste"), pasteFilter, loadImage("item_paste_sicon"), filterCfgOnClipboard.has_value()); + menu.addSeparator(); + menu.addItem( _("Cu&t"), cutFilter, loadImage("item_cut_sicon"), !isNullFilter(localFilter_)); + + menu.popup(*basicPanel_.m_bpButtonLocalFilter, {basicPanel_.m_bpButtonLocalFilter->GetSize().x, 0}); + } + + + virtual MainConfiguration getMainConfig() const = 0; + virtual wxWindow* getParentWindow() = 0; + + virtual void onLocalCompCfgChange () = 0; + virtual void onLocalSyncCfgChange () = 0; + virtual void onLocalFilterCfgChange() = 0; + + GuiPanel& basicPanel_; //panel to be enhanced by this template + + //alternate configuration attached to it + std::optional localCmpCfg_; + std::optional localSyncCfg_; + FilterConfig localFilter_; + + const wxImage imgCmp_ = zen::loadImage("options_compare", zen::dipToScreen(20)); + const wxImage imgSync_ = zen::loadImage("options_sync", zen::dipToScreen(20)); + const wxImage imgFilter_ = zen::loadImage("options_filter", zen::dipToScreen(20)); +}; + + +inline +std::wstring getFilterSummaryForTooltip(const FilterConfig& filterCfg) +{ + using namespace zen; + + auto indentLines = [](Zstring str) + { + std::wstring out; + split(str, Zstr('\n'), [&out](ZstringView block) + { + block = trimCpy(block); + if (!block.empty()) + { + out += L'\n'; + out += TAB_SPACE; + out += utfTo(block); + } + }); + return out; + }; + + std::wstring filterSummary; + if (trimCpy(filterCfg.includeFilter) != Zstr("*")) //harmonize with base/path_filter.cpp NameFilter::isNull + filterSummary += L"\n\n" + _("Include:") + indentLines(filterCfg.includeFilter); + + if (!trimCpy(filterCfg.excludeFilter).empty()) + filterSummary += L"\n\n" + _("Exclude:") + indentLines(filterCfg.excludeFilter); + + return filterSummary; +} +} + +#endif //FOLDER_PAIR_H_89341750847252345 diff --git a/FreeFileSync/Source/ui/folder_selector.cpp b/FreeFileSync/Source/ui/folder_selector.cpp new file mode 100644 index 0000000..69319d1 --- /dev/null +++ b/FreeFileSync/Source/ui/folder_selector.cpp @@ -0,0 +1,304 @@ +// ***************************************************************************** +// * 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 "folder_selector.h" +#include +#include +#include +#include +#include +#include +#include "small_dlgs.h" //includes structures.h, which defines "AFS" +#include "../afs/concrete.h" +#include "../afs/native.h" +#include "../afs/gdrive.h" + + #include + + +using namespace zen; +using namespace fff; + + +namespace +{ +constexpr std::chrono::milliseconds FOLDER_SELECTED_EXISTENCE_CHECK_TIME_MAX(200); + + +void setFolderPathPhrase(const Zstring& folderPathPhrase, FolderHistoryBox* comboBox, wxWindow& tooltipWnd, wxStaticText* staticText) //pointers are optional +{ + if (comboBox) + comboBox->setValue(utfTo(folderPathPhrase)); + + const Zstring folderPathPhraseFmt = AFS::getInitPathPhrase(createAbstractPath(folderPathPhrase)); //noexcept + //may block when resolving [] + + if (folderPathPhraseFmt.empty()) + tooltipWnd.UnsetToolTip(); //wxGTK doesn't allow wxToolTip with empty text! + else + tooltipWnd.SetToolTip(utfTo(folderPathPhraseFmt)); + + auto trimTrailingSep = [](Zstring path) + { + if (endsWith(path, Zstr('/')) || + endsWith(path, Zstr('\\'))) + path.pop_back(); + return path; + }; + + if (staticText) //change static box label only if there is a real difference to what is shown in wxTextCtrl anyway + staticText->SetLabel(equalNoCase(trimTrailingSep(trimCpy(folderPathPhrase)), + trimTrailingSep(folderPathPhraseFmt)) ? + wxString(_("Drag && drop")) : utfTo(folderPathPhraseFmt)); +} + + +} + +//############################################################################################################## + +namespace fff +{ +wxDEFINE_EVENT(EVENT_ON_FOLDER_SELECTED, wxCommandEvent); +} + + +FolderSelector::FolderSelector(wxWindow* parent, + wxWindow& dropWindow, + wxButton& selectFolderButton, + wxButton& selectAltFolderButton, + FolderHistoryBox& folderComboBox, + Zstring& folderLastSelected, Zstring& sftpKeyFileLastSelected, + wxStaticText* staticText, + wxWindow* dropWindow2, + const std::function& shellItemPaths)>& droppedPathsFilter, + const std::function& getDeviceParallelOps, + const std::function& setDeviceParallelOps) : + droppedPathsFilter_ (droppedPathsFilter), + getDeviceParallelOps_(getDeviceParallelOps), + setDeviceParallelOps_(setDeviceParallelOps), + parent_(parent), + dropWindow_(dropWindow), + dropWindow2_(dropWindow2), + selectFolderButton_(selectFolderButton), + selectAltFolderButton_(selectAltFolderButton), + folderComboBox_(folderComboBox), + folderLastSelected_(folderLastSelected), + sftpKeyFileLastSelected_(sftpKeyFileLastSelected), + staticText_(staticText) +{ + assert(getDeviceParallelOps_); + + auto setupDragDrop = [&](wxWindow& dropWin) + { + setupFileDrop(dropWin); + dropWin.Bind(EVENT_DROP_FILE, &FolderSelector::onItemPathDropped, this); + }; + + setupDragDrop(dropWindow_); + if (dropWindow2_) + setupDragDrop(*dropWindow2_); + + setImage(selectAltFolderButton_, loadImage("cloud_small")); + + //keep folderSelector and dirpath synchronous + folderComboBox_ .Bind(wxEVT_MOUSEWHEEL, &FolderSelector::onMouseWheel, this); + folderComboBox_ .Bind(wxEVT_COMMAND_TEXT_UPDATED, &FolderSelector::onEditFolderPath, this); + //folderComboBox_.Bind(wxEVT_COMMAND_COMBOBOX_SELECTED, &FolderSelector::onHistoryPathSelected, this); + // => wxEVT_COMMAND_COMBOBOX_SELECTED implies wxEVT_COMMAND_TEXT_UPDATED + selectFolderButton_ .Bind(wxEVT_COMMAND_BUTTON_CLICKED, &FolderSelector::onSelectFolder, this); + selectAltFolderButton_.Bind(wxEVT_COMMAND_BUTTON_CLICKED, &FolderSelector::onSelectAltFolder, this); +} + + +FolderSelector::~FolderSelector() +{ + [[maybe_unused]] bool ubOk1 = dropWindow_.Unbind(EVENT_DROP_FILE, &FolderSelector::onItemPathDropped, this); + [[maybe_unused]] bool ubOk2 = true; + if (dropWindow2_) + ubOk2 = dropWindow2_->Unbind(EVENT_DROP_FILE, &FolderSelector::onItemPathDropped, this); + + [[maybe_unused]] bool ubOk3 = folderComboBox_ .Unbind(wxEVT_MOUSEWHEEL, &FolderSelector::onMouseWheel, this); + [[maybe_unused]] bool ubOk4 = folderComboBox_ .Unbind(wxEVT_COMMAND_TEXT_UPDATED, &FolderSelector::onEditFolderPath, this); + //[[maybe_unused]] bool ubOk5 = folderComboBox_ .Unbind(wxEVT_COMMAND_COMBOBOX_SELECTED, &FolderSelector::onHistoryPathSelected, this); + // => wxEVT_COMMAND_COMBOBOX_SELECTED implies wxEVT_COMMAND_TEXT_UPDATED + [[maybe_unused]] bool ubOk6 = selectFolderButton_ .Unbind(wxEVT_COMMAND_BUTTON_CLICKED, &FolderSelector::onSelectFolder, this); + [[maybe_unused]] bool ubOk7 = selectAltFolderButton_.Unbind(wxEVT_COMMAND_BUTTON_CLICKED, &FolderSelector::onSelectAltFolder, this); + assert(ubOk1 && ubOk2 && ubOk3 && ubOk4 && /*ubOk5 &&*/ ubOk6 && ubOk7); +} + + +void FolderSelector::onMouseWheel(wxMouseEvent& event) +{ + //for combobox: although switching through available items is wxWidgets default, this is NOT Windows default, e.g. Explorer + //additionally this will delete manual entries, although all the users wanted is scroll the parent window! + + //redirect to parent scrolled window! + for (wxWindow* wnd = folderComboBox_.GetParent(); wnd; wnd = wnd->GetParent()) + if (dynamic_cast(wnd)) + if (wxEvtHandler* evtHandler = wnd->GetEventHandler()) + return evtHandler->AddPendingEvent(event); + assert(false); //get here when attempting to scroll first folder pair (which is not inside a wxScrolledWindow) + //event.Skip(); +} + + +void FolderSelector::onItemPathDropped(FileDropEvent& event) +{ + if (event.itemPaths_.empty()) + return; + + if (!droppedPathsFilter_ || droppedPathsFilter_(event.itemPaths_)) + { + auto fmtShellPath = [](Zstring shellItemPath) + { + if (endsWith(shellItemPath, Zstr(' '))) //prevent createAbstractPath() from trimming legit trailing blank! + shellItemPath += FILE_NAME_SEPARATOR; + + const AbstractPath itemPath = createAbstractPath(shellItemPath); + try + { + if (AFS::getItemType(itemPath) == AFS::ItemType::file) //throw FileError + if (const std::optional 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 + //make sure FFS-specific explicit MTP-syntax is applied! + return AFS::getInitPathPhrase(itemPath); + }; + + setPath(fmtShellPath(event.itemPaths_[0])); + //drop two folder paths at once: + if (siblingSelector_ && event.itemPaths_.size() >= 2) + siblingSelector_->setPath(fmtShellPath(event.itemPaths_[1])); + + //notify action invoked by user + wxCommandEvent dummy(EVENT_ON_FOLDER_SELECTED); + ProcessEvent(dummy); + } + + //event.Skip(); //let other handlers try -> are there any?? +} + + +void FolderSelector::onEditFolderPath(wxCommandEvent& event) +{ + setFolderPathPhrase(utfTo(event.GetString()), nullptr, folderComboBox_, staticText_); + + wxCommandEvent dummy(EVENT_ON_FOLDER_SELECTED); + ProcessEvent(dummy); + event.Skip(); +} + + +void FolderSelector::onSelectFolder(wxCommandEvent& event) +{ + Zstring defaultFolderNative; + { + //make sure default folder exists: don't let folder picker hang on non-existing network share! + auto folderAccessible = [stopTime = std::chrono::steady_clock::now() + FOLDER_SELECTED_EXISTENCE_CHECK_TIME_MAX](const AbstractPath& folderPath) + { + if (AFS::isNullPath(folderPath)) + return false; + + auto ft = runAsync([folderPath] + { + try + { + return AFS::getItemType(folderPath) != AFS::ItemType::file; //throw FileError + } + catch (FileError&) { return false; } + }); + return ft.wait_until(stopTime) == std::future_status::ready && ft.get(); //potentially slow network access: wait 200ms at most + }; + + auto trySetDefaultPath = [&](const Zstring& folderPathPhrase) + { + if (acceptsItemPathPhraseNative(folderPathPhrase)) //noexcept + { + const AbstractPath folderPath = createItemPathNative(folderPathPhrase); + if (folderAccessible(folderPath)) + if (const Zstring& nativePath = getNativeItemPath(folderPath); + !nativePath.empty()) + defaultFolderNative = nativePath; + } + }; + const Zstring& currentFolderPath = getPath(); + trySetDefaultPath(currentFolderPath); + + if (defaultFolderNative.empty() && //=> fallback: use last user-selected path + trimCpy(folderLastSelected_) != trimCpy(currentFolderPath) /*case-sensitive comp for path phrase!*/) + trySetDefaultPath(folderLastSelected_); + } + + Zstring shellItemPath; + //default size? Windows: not implemented, Linux(GTK2): not implemented, macOS: not implemented => wxWidgets, what is this shit!? + wxDirDialog folderSelector(parent_, _("Select a folder"), utfTo(defaultFolderNative), wxDD_DEFAULT_STYLE | wxDD_SHOW_HIDDEN); + //GTK2: "Show hidden" is also available as a context menu option in the folder picker! + //It looks like wxDD_SHOW_HIDDEN only sets the default when opening for the first time!? + if (folderSelector.ShowModal() != wxID_OK) + return; + shellItemPath = utfTo(folderSelector.GetPath()); + if (endsWith(shellItemPath, Zstr(' '))) //prevent createAbstractPath() from trimming legit trailing blank! + shellItemPath += FILE_NAME_SEPARATOR; + + //make sure FFS-specific explicit MTP-syntax is applied! + const Zstring newFolderPathPhrase = AFS::getInitPathPhrase(createAbstractPath(shellItemPath)); //noexcept + + setPath(newFolderPathPhrase); + folderLastSelected_ = newFolderPathPhrase; + + //notify action invoked by user + wxCommandEvent dummy(EVENT_ON_FOLDER_SELECTED); + ProcessEvent(dummy); +} + + +void FolderSelector::onSelectAltFolder(wxCommandEvent& event) +{ + Zstring folderPathPhrase = getPath(); + size_t parallelOps = getDeviceParallelOps_ ? getDeviceParallelOps_(folderPathPhrase) : 1; + + const AbstractPath oldPath = createAbstractPath(folderPathPhrase); + + if (showCloudSetupDialog(parent_, folderPathPhrase, sftpKeyFileLastSelected_, parallelOps, static_cast(setDeviceParallelOps_)) != ConfirmationButton::accept) + return; + + setPath(folderPathPhrase); + + if (setDeviceParallelOps_) + setDeviceParallelOps_(folderPathPhrase, parallelOps); + + //notify action invoked by user + if (createAbstractPath(folderPathPhrase) != oldPath) + { + wxCommandEvent dummy(EVENT_ON_FOLDER_SELECTED); + ProcessEvent(dummy); + } + //else: don't notify if user only changed connection settings, e.g. parallel Ops +} + + +Zstring FolderSelector::getPath() const +{ + return utfTo(folderComboBox_.GetValue()); +} + + +void FolderSelector::setPath(const Zstring& folderPathPhrase) +{ + setFolderPathPhrase(folderPathPhrase, &folderComboBox_, folderComboBox_, staticText_); +} + + +void fff::openFolderInFileBrowser(const AbstractPath& folderPath) //throw FileError +{ + if (const Zstring& gdriveUrl = getGoogleDriveFolderUrl(folderPath); //throw FileError + !gdriveUrl.empty()) + return openWithDefaultApp(gdriveUrl); //throw FileError + else + openWithDefaultApp(utfTo(AFS::getDisplayPath(folderPath))); //throw FileError +} diff --git a/FreeFileSync/Source/ui/folder_selector.h b/FreeFileSync/Source/ui/folder_selector.h new file mode 100644 index 0000000..1700ca6 --- /dev/null +++ b/FreeFileSync/Source/ui/folder_selector.h @@ -0,0 +1,82 @@ +// ***************************************************************************** +// * 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 FOLDER_SELECTOR_H_24857842375234523463425 +#define FOLDER_SELECTOR_H_24857842375234523463425 + +#include +#include +#include +#include +#include "folder_history_box.h" +#include "../afs/abstract.h" + + +namespace fff +{ +/* handle drag and drop, tooltip, label and manual input, coordinating a wxWindow, wxButton, and wxComboBox/wxTextCtrl + + Reasons NOT to use wxDirPickerCtrl, but wxButton instead: + - Crash on GTK 2: https://favapps.wordpress.com/2012/06/11/freefilesync-crash-in-linux-when-syncing-solved/ + - still uses outdated ::SHBrowseForFolder() (even on Windows 7) + - selection dialog remembers size, but NOT position => if user enlarges window, the next time he opens the dialog it may leap out of visible screen + - hard-codes "Browse" button label */ + +wxDECLARE_EVENT(EVENT_ON_FOLDER_SELECTED, wxCommandEvent); //directory is changed by the user, including manual type-in +//example: wnd.Bind(EVENT_ON_FOLDER_SELECTED, [this](wxCommandEvent& event) { onDirSelected(event); }); + +class FolderSelector: public wxEvtHandler +{ +public: + FolderSelector(wxWindow* parent, + wxWindow& dropWindow, + wxButton& selectFolderButton, + wxButton& selectAltFolderButton, + FolderHistoryBox& folderComboBox, + Zstring& folderLastSelected, Zstring& sftpKeyFileLastSelected, + wxStaticText* staticText, //optional + wxWindow* dropWindow2, // + const std::function& shellItemPaths)>& droppedPathsFilter, //optional + const std::function& getDeviceParallelOps, //mandatory + const std::function& setDeviceParallelOps); //optional + + ~FolderSelector(); + + void setSiblingSelector(FolderSelector* selector) { siblingSelector_ = selector; } + + void setPath(const Zstring& folderPathPhrase); + Zstring getPath() const; + +private: + void onMouseWheel (wxMouseEvent& event); + void onItemPathDropped(zen::FileDropEvent& event); + void onEditFolderPath (wxCommandEvent& event); + void onSelectFolder (wxCommandEvent& event); + void onSelectAltFolder(wxCommandEvent& event); + + const std::function& shellItemPaths)> droppedPathsFilter_; + + const std::function getDeviceParallelOps_; + const std::function setDeviceParallelOps_; + + wxWindow* parent_; + wxWindow& dropWindow_; + wxWindow* dropWindow2_ = nullptr; // + wxButton& selectFolderButton_; + wxButton& selectAltFolderButton_; + FolderHistoryBox& folderComboBox_; + Zstring& folderLastSelected_; + Zstring& sftpKeyFileLastSelected_; + wxStaticText* staticText_ = nullptr; //optional + FolderSelector* siblingSelector_ = nullptr; // +}; + + +//abstract version of openWithDefaultApp() +void openFolderInFileBrowser(const AbstractPath& folderPath); //throw FileError +} + +#endif //FOLDER_SELECTOR_H_24857842375234523463425 diff --git a/FreeFileSync/Source/ui/gui_generated.cpp b/FreeFileSync/Source/ui/gui_generated.cpp new file mode 100644 index 0000000..01d35ad --- /dev/null +++ b/FreeFileSync/Source/ui/gui_generated.cpp @@ -0,0 +1,6232 @@ +/////////////////////////////////////////////////////////////////////////// +// C++ code generated with wxFormBuilder (version 3.10.1-0-g8feb16b3) +// http://www.wxformbuilder.org/ +// +// PLEASE DO *NOT* EDIT THIS FILE! +/////////////////////////////////////////////////////////////////////////// + +#include "gui_generated.h" + +/////////////////////////////////////////////////////////////////////////// + +MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style ) : wxFrame( parent, id, title, pos, size, style ) +{ + this->SetSizeHints( wxDefaultSize, wxDefaultSize ); + + m_menubar = new wxMenuBar( 0 ); + m_menuFile = new wxMenu(); + m_menuItemNew = new wxMenuItem( m_menuFile, wxID_NEW, wxString( _("&New") ) + wxT('\t') + wxT("Ctrl+N"), wxEmptyString, wxITEM_NORMAL ); + m_menuFile->Append( m_menuItemNew ); + + m_menuItemLoad = new wxMenuItem( m_menuFile, wxID_OPEN, wxString( _("&Open...") ) + wxT('\t') + wxT("Ctrl+O"), wxEmptyString, wxITEM_NORMAL ); + m_menuFile->Append( m_menuItemLoad ); + + m_menuFile->AppendSeparator(); + + m_menuItemSave = new wxMenuItem( m_menuFile, wxID_SAVE, wxString( _("&Save") ) + wxT('\t') + wxT("Ctrl+S"), wxEmptyString, wxITEM_NORMAL ); + m_menuFile->Append( m_menuItemSave ); + + m_menuItemSaveAs = new wxMenuItem( m_menuFile, wxID_SAVEAS, wxString( _("Save &as...") ), wxEmptyString, wxITEM_NORMAL ); + m_menuFile->Append( m_menuItemSaveAs ); + + m_menuItemSaveAsBatch = new wxMenuItem( m_menuFile, wxID_ANY, wxString( _("Save as &batch job...") ), wxEmptyString, wxITEM_NORMAL ); + m_menuFile->Append( m_menuItemSaveAsBatch ); + + m_menuFile->AppendSeparator(); + + m_menuItemQuit = new wxMenuItem( m_menuFile, wxID_EXIT, wxString( _("E&xit") ), wxEmptyString, wxITEM_NORMAL ); + m_menuFile->Append( m_menuItemQuit ); + + m_menubar->Append( m_menuFile, _("&File") ); + + m_menuActions = new wxMenu(); + m_menuItemShowLog = new wxMenuItem( m_menuActions, wxID_ANY, wxString( _("Show &log") ) + wxT('\t') + wxT("F4"), wxEmptyString, wxITEM_NORMAL ); + m_menuActions->Append( m_menuItemShowLog ); + + m_menuActions->AppendSeparator(); + + m_menuItemCompare = new wxMenuItem( m_menuActions, wxID_ANY, wxString( _("Start &comparison") ) + wxT('\t') + wxT("F5"), wxEmptyString, wxITEM_NORMAL ); + m_menuActions->Append( m_menuItemCompare ); + + m_menuActions->AppendSeparator(); + + m_menuItemCompSettings = new wxMenuItem( m_menuActions, wxID_ANY, wxString( _("C&omparison settings") ) + wxT('\t') + wxT("F6"), wxEmptyString, wxITEM_NORMAL ); + m_menuActions->Append( m_menuItemCompSettings ); + + m_menuItemFilter = new wxMenuItem( m_menuActions, wxID_ANY, wxString( _("&Filter settings") ) + wxT('\t') + wxT("F7"), wxEmptyString, wxITEM_NORMAL ); + m_menuActions->Append( m_menuItemFilter ); + + m_menuItemSyncSettings = new wxMenuItem( m_menuActions, wxID_ANY, wxString( _("S&ynchronization settings") ) + wxT('\t') + wxT("F8"), wxEmptyString, wxITEM_NORMAL ); + m_menuActions->Append( m_menuItemSyncSettings ); + + m_menuActions->AppendSeparator(); + + m_menuItemSynchronize = new wxMenuItem( m_menuActions, wxID_ANY, wxString( _("Start &synchronization") ) + wxT('\t') + wxT("F9"), wxEmptyString, wxITEM_NORMAL ); + m_menuActions->Append( m_menuItemSynchronize ); + + m_menubar->Append( m_menuActions, _("&Actions") ); + + m_menuTools = new wxMenu(); + m_menuItemOptions = new wxMenuItem( m_menuTools, wxID_PREFERENCES, wxString( _("&Preferences") ) + wxT('\t') + wxT("Ctrl+,"), wxEmptyString, wxITEM_NORMAL ); + m_menuTools->Append( m_menuItemOptions ); + + m_menuLanguages = new wxMenu(); + wxMenuItem* m_menuLanguagesItem = new wxMenuItem( m_menuTools, wxID_ANY, _("&Language"), wxEmptyString, wxITEM_NORMAL, m_menuLanguages ); + m_menuTools->Append( m_menuLanguagesItem ); + + m_menuTools->AppendSeparator(); + + m_menuItemFind = new wxMenuItem( m_menuTools, wxID_FIND, wxString( _("&Find...") ) + wxT('\t') + wxT("Ctrl+F"), wxEmptyString, wxITEM_NORMAL ); + m_menuTools->Append( m_menuItemFind ); + + m_menuItemExportList = new wxMenuItem( m_menuTools, wxID_ANY, wxString( _("&Export file list") ), wxEmptyString, wxITEM_NORMAL ); + m_menuTools->Append( m_menuItemExportList ); + + m_menuTools->AppendSeparator(); + + m_menuItemResetLayout = new wxMenuItem( m_menuTools, wxID_ANY, wxString( _("&Reset layout") ), wxEmptyString, wxITEM_NORMAL ); + m_menuTools->Append( m_menuItemResetLayout ); + + m_menuItemShowMain = new wxMenuItem( m_menuTools, wxID_ANY, wxString( _("dummy") ), wxEmptyString, wxITEM_NORMAL ); + m_menuTools->Append( m_menuItemShowMain ); + + m_menuItemShowFolders = new wxMenuItem( m_menuTools, wxID_ANY, wxString( _("dummy") ), wxEmptyString, wxITEM_NORMAL ); + m_menuTools->Append( m_menuItemShowFolders ); + + m_menuItemShowViewFilter = new wxMenuItem( m_menuTools, wxID_ANY, wxString( _("dummy") ), wxEmptyString, wxITEM_NORMAL ); + m_menuTools->Append( m_menuItemShowViewFilter ); + + m_menuItemShowConfig = new wxMenuItem( m_menuTools, wxID_ANY, wxString( _("dummy") ), wxEmptyString, wxITEM_NORMAL ); + m_menuTools->Append( m_menuItemShowConfig ); + + m_menuItemShowOverview = new wxMenuItem( m_menuTools, wxID_ANY, wxString( _("dummy") ), wxEmptyString, wxITEM_NORMAL ); + m_menuTools->Append( m_menuItemShowOverview ); + + m_menubar->Append( m_menuTools, _("&Tools") ); + + m_menuHelp = new wxMenu(); + m_menuItemHelp = new wxMenuItem( m_menuHelp, wxID_HELP, wxString( _("&View help") ) + wxT('\t') + wxT("F1"), wxEmptyString, wxITEM_NORMAL ); + m_menuHelp->Append( m_menuItemHelp ); + + m_menuHelp->AppendSeparator(); + + m_menuItemCheckVersionNow = new wxMenuItem( m_menuHelp, wxID_ANY, wxString( _("&Check for updates now") ), wxEmptyString, wxITEM_NORMAL ); + m_menuHelp->Append( m_menuItemCheckVersionNow ); + + m_menuHelp->AppendSeparator(); + + m_menuItemAbout = new wxMenuItem( m_menuHelp, wxID_ABOUT, wxString( _("&About") ) + wxT('\t') + wxT("Shift+F1"), wxEmptyString, wxITEM_NORMAL ); + m_menuHelp->Append( m_menuItemAbout ); + + m_menubar->Append( m_menuHelp, _("&Help") ); + + this->SetMenuBar( m_menubar ); + + bSizerPanelHolder = new wxBoxSizer( wxVERTICAL ); + + m_panelTopButtons = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panelTopButtons->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer1791; + bSizer1791 = new wxBoxSizer( wxHORIZONTAL ); + + bSizerTopButtons = new wxBoxSizer( wxHORIZONTAL ); + + wxBoxSizer* bSizer261; + bSizer261 = new wxBoxSizer( wxHORIZONTAL ); + + + bSizer261->Add( 0, 0, 1, 0, 5 ); + + m_buttonCancel = new wxButton( m_panelTopButtons, wxID_CANCEL, _("Cancel"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_buttonCancel->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + m_buttonCancel->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNTEXT ) ); + m_buttonCancel->Enable( false ); + m_buttonCancel->Hide(); + + bSizer261->Add( m_buttonCancel, 0, wxEXPAND, 5 ); + + m_buttonCompare = new zen::BitmapTextButton( m_panelTopButtons, wxID_ANY, _("Compare"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_buttonCompare->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + m_buttonCompare->SetToolTip( _("dummy") ); + + bSizer261->Add( m_buttonCompare, 0, wxEXPAND, 5 ); + + + bSizerTopButtons->Add( bSizer261, 1, wxEXPAND, 5 ); + + + bSizerTopButtons->Add( 8, 8, 0, 0, 5 ); + + wxBoxSizer* bSizer2942; + bSizer2942 = new wxBoxSizer( wxHORIZONTAL ); + + m_bpButtonCmpConfig = new wxBitmapButton( m_panelTopButtons, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + m_bpButtonCmpConfig->SetToolTip( _("dummy") ); + + bSizer2942->Add( m_bpButtonCmpConfig, 0, wxEXPAND, 5 ); + + m_bpButtonCmpContext = new wxBitmapButton( m_panelTopButtons, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + bSizer2942->Add( m_bpButtonCmpContext, 0, wxEXPAND, 5 ); + + + bSizer2942->Add( 0, 0, 1, 0, 5 ); + + + bSizer2942->Add( 8, 0, 0, 0, 5 ); + + m_bpButtonFilter = new wxBitmapButton( m_panelTopButtons, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + m_bpButtonFilter->SetToolTip( _("dummy") ); + + bSizer2942->Add( m_bpButtonFilter, 0, wxEXPAND, 5 ); + + m_bpButtonFilterContext = new wxBitmapButton( m_panelTopButtons, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + bSizer2942->Add( m_bpButtonFilterContext, 0, wxEXPAND, 5 ); + + + bSizer2942->Add( 8, 0, 0, 0, 5 ); + + + bSizer2942->Add( 0, 0, 1, 0, 5 ); + + m_bpButtonSyncConfig = new wxBitmapButton( m_panelTopButtons, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + m_bpButtonSyncConfig->SetToolTip( _("dummy") ); + + bSizer2942->Add( m_bpButtonSyncConfig, 0, wxEXPAND, 5 ); + + m_bpButtonSyncContext = new wxBitmapButton( m_panelTopButtons, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + bSizer2942->Add( m_bpButtonSyncContext, 0, wxEXPAND, 5 ); + + + bSizerTopButtons->Add( bSizer2942, 1, wxEXPAND, 5 ); + + + bSizerTopButtons->Add( 8, 8, 0, 0, 5 ); + + wxBoxSizer* bSizer262; + bSizer262 = new wxBoxSizer( wxHORIZONTAL ); + + m_buttonSync = new zen::BitmapTextButton( m_panelTopButtons, wxID_ANY, _("Synchronize"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_buttonSync->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + m_buttonSync->SetToolTip( _("dummy") ); + + bSizer262->Add( m_buttonSync, 0, wxEXPAND, 5 ); + + + bSizerTopButtons->Add( bSizer262, 1, wxEXPAND, 5 ); + + + bSizer1791->Add( bSizerTopButtons, 1, wxALIGN_CENTER_VERTICAL, 5 ); + + + m_panelTopButtons->SetSizer( bSizer1791 ); + m_panelTopButtons->Layout(); + bSizer1791->Fit( m_panelTopButtons ); + bSizerPanelHolder->Add( m_panelTopButtons, 0, wxEXPAND, 5 ); + + m_panelDirectoryPairs = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL|wxBORDER_STATIC ); + wxBoxSizer* bSizer1601; + bSizer1601 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer91; + bSizer91 = new wxBoxSizer( wxHORIZONTAL ); + + m_panelTopLeft = new wxPanel( m_panelDirectoryPairs, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panelTopLeft->SetMinSize( wxSize( 1, -1 ) ); + + wxFlexGridSizer* fgSizer8; + fgSizer8 = new wxFlexGridSizer( 0, 2, 0, 0 ); + fgSizer8->AddGrowableCol( 1 ); + fgSizer8->SetFlexibleDirection( wxBOTH ); + fgSizer8->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_ALL ); + + + fgSizer8->Add( 0, 0, 1, wxEXPAND, 5 ); + + m_staticTextResolvedPathL = new wxStaticText( m_panelTopLeft, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextResolvedPathL->Wrap( -1 ); + fgSizer8->Add( m_staticTextResolvedPathL, 0, wxALIGN_CENTER_VERTICAL|wxALL, 2 ); + + wxBoxSizer* bSizer159; + bSizer159 = new wxBoxSizer( wxHORIZONTAL ); + + m_bpButtonAddPair = new wxBitmapButton( m_panelTopLeft, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + m_bpButtonAddPair->SetToolTip( _("Add folder pair") ); + + bSizer159->Add( m_bpButtonAddPair, 0, wxEXPAND, 5 ); + + m_bpButtonRemovePair = new wxBitmapButton( m_panelTopLeft, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + m_bpButtonRemovePair->SetToolTip( _("Remove folder pair") ); + + bSizer159->Add( m_bpButtonRemovePair, 0, wxEXPAND, 5 ); + + + fgSizer8->Add( bSizer159, 0, wxEXPAND, 5 ); + + wxBoxSizer* bSizer182; + bSizer182 = new wxBoxSizer( wxHORIZONTAL ); + + m_folderPathLeft = new fff::FolderHistoryBox( m_panelTopLeft, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0, NULL, 0 ); + bSizer182->Add( m_folderPathLeft, 1, wxALIGN_CENTER_VERTICAL, 5 ); + + m_buttonSelectFolderLeft = new wxButton( m_panelTopLeft, wxID_ANY, _("Browse"), wxDefaultPosition, wxDefaultSize, 0 ); + m_buttonSelectFolderLeft->SetToolTip( _("Select a folder") ); + + bSizer182->Add( m_buttonSelectFolderLeft, 0, wxEXPAND, 5 ); + + m_bpButtonSelectAltFolderLeft = new wxBitmapButton( m_panelTopLeft, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + m_bpButtonSelectAltFolderLeft->SetToolTip( _("Access online storage") ); + + bSizer182->Add( m_bpButtonSelectAltFolderLeft, 0, wxEXPAND, 5 ); + + + fgSizer8->Add( bSizer182, 0, wxEXPAND, 5 ); + + + m_panelTopLeft->SetSizer( fgSizer8 ); + m_panelTopLeft->Layout(); + fgSizer8->Fit( m_panelTopLeft ); + bSizer91->Add( m_panelTopLeft, 1, wxLEFT|wxALIGN_BOTTOM, 5 ); + + m_panelTopCenter = new wxPanel( m_panelDirectoryPairs, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + wxBoxSizer* bSizer1771; + bSizer1771 = new wxBoxSizer( wxVERTICAL ); + + m_bpButtonSwapSides = new wxBitmapButton( m_panelTopCenter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + m_bpButtonSwapSides->SetToolTip( _("dummy") ); + + bSizer1771->Add( m_bpButtonSwapSides, 0, wxEXPAND, 5 ); + + wxBoxSizer* bSizer160; + bSizer160 = new wxBoxSizer( wxHORIZONTAL ); + + m_bpButtonLocalCompCfg = new wxBitmapButton( m_panelTopCenter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + m_bpButtonLocalCompCfg->SetToolTip( _("dummy") ); + + bSizer160->Add( m_bpButtonLocalCompCfg, 0, wxEXPAND, 5 ); + + m_bpButtonLocalFilter = new wxBitmapButton( m_panelTopCenter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + m_bpButtonLocalFilter->SetToolTip( _("dummy") ); + + bSizer160->Add( m_bpButtonLocalFilter, 0, wxEXPAND, 5 ); + + m_bpButtonLocalSyncCfg = new wxBitmapButton( m_panelTopCenter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + m_bpButtonLocalSyncCfg->SetToolTip( _("dummy") ); + + bSizer160->Add( m_bpButtonLocalSyncCfg, 0, wxEXPAND, 5 ); + + + bSizer1771->Add( bSizer160, 1, wxALIGN_CENTER_HORIZONTAL, 5 ); + + + m_panelTopCenter->SetSizer( bSizer1771 ); + m_panelTopCenter->Layout(); + bSizer1771->Fit( m_panelTopCenter ); + bSizer91->Add( m_panelTopCenter, 0, wxRIGHT|wxLEFT|wxALIGN_CENTER_VERTICAL, 5 ); + + m_panelTopRight = new wxPanel( m_panelDirectoryPairs, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panelTopRight->SetMinSize( wxSize( 1, -1 ) ); + + wxBoxSizer* bSizer183; + bSizer183 = new wxBoxSizer( wxVERTICAL ); + + m_staticTextResolvedPathR = new wxStaticText( m_panelTopRight, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextResolvedPathR->Wrap( -1 ); + bSizer183->Add( m_staticTextResolvedPathR, 0, wxALL, 2 ); + + wxBoxSizer* bSizer179; + bSizer179 = new wxBoxSizer( wxHORIZONTAL ); + + m_folderPathRight = new fff::FolderHistoryBox( m_panelTopRight, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0, NULL, 0 ); + bSizer179->Add( m_folderPathRight, 1, wxALIGN_CENTER_VERTICAL, 5 ); + + m_buttonSelectFolderRight = new wxButton( m_panelTopRight, wxID_ANY, _("Browse"), wxDefaultPosition, wxDefaultSize, 0 ); + m_buttonSelectFolderRight->SetToolTip( _("Select a folder") ); + + bSizer179->Add( m_buttonSelectFolderRight, 0, wxEXPAND, 5 ); + + m_bpButtonSelectAltFolderRight = new wxBitmapButton( m_panelTopRight, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + m_bpButtonSelectAltFolderRight->SetToolTip( _("Access online storage") ); + + bSizer179->Add( m_bpButtonSelectAltFolderRight, 0, wxEXPAND, 5 ); + + + bSizer183->Add( bSizer179, 0, wxEXPAND, 5 ); + + + m_panelTopRight->SetSizer( bSizer183 ); + m_panelTopRight->Layout(); + bSizer183->Fit( m_panelTopRight ); + bSizer91->Add( m_panelTopRight, 1, wxRIGHT|wxALIGN_BOTTOM, 5 ); + + + bSizer1601->Add( bSizer91, 0, wxEXPAND, 5 ); + + m_scrolledWindowFolderPairs = new wxScrolledWindow( m_panelDirectoryPairs, wxID_ANY, wxDefaultPosition, wxSize( -1, -1 ), wxHSCROLL|wxVSCROLL ); + m_scrolledWindowFolderPairs->SetScrollRate( 5, 5 ); + m_scrolledWindowFolderPairs->SetMinSize( wxSize( -1, 0 ) ); + + bSizerAddFolderPairs = new wxBoxSizer( wxVERTICAL ); + + + m_scrolledWindowFolderPairs->SetSizer( bSizerAddFolderPairs ); + m_scrolledWindowFolderPairs->Layout(); + bSizerAddFolderPairs->Fit( m_scrolledWindowFolderPairs ); + bSizer1601->Add( m_scrolledWindowFolderPairs, 1, wxEXPAND, 5 ); + + + m_panelDirectoryPairs->SetSizer( bSizer1601 ); + m_panelDirectoryPairs->Layout(); + bSizer1601->Fit( m_panelDirectoryPairs ); + bSizerPanelHolder->Add( m_panelDirectoryPairs, 0, wxEXPAND, 5 ); + + m_gridOverview = new zen::Grid( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL|wxVSCROLL ); + m_gridOverview->SetScrollRate( 5, 5 ); + bSizerPanelHolder->Add( m_gridOverview, 0, 0, 5 ); + + m_panelCenter = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + wxBoxSizer* bSizer1711; + bSizer1711 = new wxBoxSizer( wxVERTICAL ); + + m_splitterMain = new fff::TripleSplitter( m_panelCenter, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + wxBoxSizer* bSizer1781; + bSizer1781 = new wxBoxSizer( wxHORIZONTAL ); + + m_gridMainL = new zen::Grid( m_splitterMain, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL|wxVSCROLL ); + m_gridMainL->SetScrollRate( 5, 5 ); + bSizer1781->Add( m_gridMainL, 1, wxEXPAND, 5 ); + + m_gridMainC = new zen::Grid( m_splitterMain, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL|wxVSCROLL ); + m_gridMainC->SetScrollRate( 5, 5 ); + bSizer1781->Add( m_gridMainC, 0, wxEXPAND, 5 ); + + m_gridMainR = new zen::Grid( m_splitterMain, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL|wxVSCROLL ); + m_gridMainR->SetScrollRate( 5, 5 ); + bSizer1781->Add( m_gridMainR, 1, wxEXPAND, 5 ); + + + m_splitterMain->SetSizer( bSizer1781 ); + m_splitterMain->Layout(); + bSizer1781->Fit( m_splitterMain ); + bSizer1711->Add( m_splitterMain, 1, wxEXPAND, 5 ); + + m_panelStatusBar = new wxPanel( m_panelCenter, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL|wxBORDER_STATIC ); + wxBoxSizer* bSizer451; + bSizer451 = new wxBoxSizer( wxHORIZONTAL ); + + wxBoxSizer* bSizer314; + bSizer314 = new wxBoxSizer( wxHORIZONTAL ); + + + bSizer314->Add( 0, 0, 1, wxALIGN_CENTER_VERTICAL, 5 ); + + bSizerStatusLeftDirectories = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapSmallDirectoryLeft = new wxStaticBitmap( m_panelStatusBar, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizerStatusLeftDirectories->Add( m_bitmapSmallDirectoryLeft, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizerStatusLeftDirectories->Add( 2, 0, 0, 0, 5 ); + + m_staticTextStatusLeftDirs = new wxStaticText( m_panelStatusBar, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextStatusLeftDirs->Wrap( -1 ); + bSizerStatusLeftDirectories->Add( m_staticTextStatusLeftDirs, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 ); + + + bSizer314->Add( bSizerStatusLeftDirectories, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + bSizerStatusLeftFiles = new wxBoxSizer( wxHORIZONTAL ); + + + bSizerStatusLeftFiles->Add( 10, 0, 0, 0, 5 ); + + m_bitmapSmallFileLeft = new wxStaticBitmap( m_panelStatusBar, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizerStatusLeftFiles->Add( m_bitmapSmallFileLeft, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizerStatusLeftFiles->Add( 2, 0, 0, 0, 5 ); + + m_staticTextStatusLeftFiles = new wxStaticText( m_panelStatusBar, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextStatusLeftFiles->Wrap( -1 ); + bSizerStatusLeftFiles->Add( m_staticTextStatusLeftFiles, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizerStatusLeftFiles->Add( 4, 0, 0, 0, 5 ); + + m_staticTextStatusLeftBytes = new wxStaticText( m_panelStatusBar, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextStatusLeftBytes->Wrap( -1 ); + bSizerStatusLeftFiles->Add( m_staticTextStatusLeftBytes, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer314->Add( bSizerStatusLeftFiles, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer314->Add( 0, 0, 1, wxALIGN_CENTER_VERTICAL, 5 ); + + wxStaticLine* m_staticline9; + m_staticline9 = new wxStaticLine( m_panelStatusBar, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL ); + bSizer314->Add( m_staticline9, 0, wxEXPAND|wxTOP, 2 ); + + + bSizer451->Add( bSizer314, 1, wxEXPAND, 5 ); + + + bSizer451->Add( 26, 0, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + m_staticTextStatusCenter = new wxStaticText( m_panelStatusBar, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextStatusCenter->Wrap( -1 ); + bSizer451->Add( m_staticTextStatusCenter, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer451->Add( 26, 0, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + wxBoxSizer* bSizer315; + bSizer315 = new wxBoxSizer( wxHORIZONTAL ); + + wxStaticLine* m_staticline10; + m_staticline10 = new wxStaticLine( m_panelStatusBar, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL ); + bSizer315->Add( m_staticline10, 0, wxEXPAND|wxTOP, 2 ); + + + bSizer315->Add( 0, 0, 1, wxALIGN_CENTER_VERTICAL, 5 ); + + bSizerStatusRightDirectories = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapSmallDirectoryRight = new wxStaticBitmap( m_panelStatusBar, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizerStatusRightDirectories->Add( m_bitmapSmallDirectoryRight, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizerStatusRightDirectories->Add( 2, 0, 0, 0, 5 ); + + m_staticTextStatusRightDirs = new wxStaticText( m_panelStatusBar, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextStatusRightDirs->Wrap( -1 ); + bSizerStatusRightDirectories->Add( m_staticTextStatusRightDirs, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer315->Add( bSizerStatusRightDirectories, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + bSizerStatusRightFiles = new wxBoxSizer( wxHORIZONTAL ); + + + bSizerStatusRightFiles->Add( 10, 0, 0, 0, 5 ); + + m_bitmapSmallFileRight = new wxStaticBitmap( m_panelStatusBar, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizerStatusRightFiles->Add( m_bitmapSmallFileRight, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizerStatusRightFiles->Add( 2, 0, 0, 0, 5 ); + + m_staticTextStatusRightFiles = new wxStaticText( m_panelStatusBar, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextStatusRightFiles->Wrap( -1 ); + bSizerStatusRightFiles->Add( m_staticTextStatusRightFiles, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizerStatusRightFiles->Add( 4, 0, 0, 0, 5 ); + + m_staticTextStatusRightBytes = new wxStaticText( m_panelStatusBar, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextStatusRightBytes->Wrap( -1 ); + bSizerStatusRightFiles->Add( m_staticTextStatusRightBytes, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer315->Add( bSizerStatusRightFiles, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer315->Add( 0, 0, 1, wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer451->Add( bSizer315, 1, wxEXPAND, 5 ); + + + m_panelStatusBar->SetSizer( bSizer451 ); + m_panelStatusBar->Layout(); + bSizer451->Fit( m_panelStatusBar ); + bSizer1711->Add( m_panelStatusBar, 0, wxEXPAND, 5 ); + + + m_panelCenter->SetSizer( bSizer1711 ); + m_panelCenter->Layout(); + bSizer1711->Fit( m_panelCenter ); + bSizerPanelHolder->Add( m_panelCenter, 1, wxEXPAND, 5 ); + + m_panelSearch = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + wxBoxSizer* bSizer1713; + bSizer1713 = new wxBoxSizer( wxHORIZONTAL ); + + m_bpButtonHideSearch = new wxBitmapButton( m_panelSearch, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + m_bpButtonHideSearch->SetToolTip( _("Close search bar") ); + + bSizer1713->Add( m_bpButtonHideSearch, 0, wxRIGHT|wxALIGN_CENTER_VERTICAL, 5 ); + + wxStaticText* m_staticText101; + m_staticText101 = new wxStaticText( m_panelSearch, wxID_ANY, _("Find:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText101->Wrap( -1 ); + bSizer1713->Add( m_staticText101, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + m_textCtrlSearchTxt = new wxTextCtrl( m_panelSearch, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize( -1, -1 ), wxTE_PROCESS_ENTER|wxWANTS_CHARS ); + bSizer1713->Add( m_textCtrlSearchTxt, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT, 5 ); + + m_checkBoxMatchCase = new wxCheckBox( m_panelSearch, wxID_ANY, _("Match case"), wxDefaultPosition, wxDefaultSize, 0 ); + bSizer1713->Add( m_checkBoxMatchCase, 1, wxALIGN_CENTER_VERTICAL|wxALL, 5 ); + + + m_panelSearch->SetSizer( bSizer1713 ); + m_panelSearch->Layout(); + bSizer1713->Fit( m_panelSearch ); + bSizerPanelHolder->Add( m_panelSearch, 0, 0, 5 ); + + m_panelLog = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panelLog->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + bSizerLog = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer42; + bSizer42 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapSyncResult = new wxStaticBitmap( m_panelLog, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizer42->Add( m_bitmapSyncResult, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 ); + + m_staticTextSyncResult = new wxStaticText( m_panelLog, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextSyncResult->Wrap( -1 ); + m_staticTextSyncResult->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + + bSizer42->Add( m_staticTextSyncResult, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxRIGHT, 10 ); + + + bSizer42->Add( 10, 0, 0, 0, 5 ); + + wxFlexGridSizer* ffgSizer11; + ffgSizer11 = new wxFlexGridSizer( 2, 0, 5, 5 ); + ffgSizer11->SetFlexibleDirection( wxBOTH ); + ffgSizer11->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); + + m_staticTextProcessed = new wxStaticText( m_panelLog, wxID_ANY, _("Processed:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextProcessed->Wrap( -1 ); + ffgSizer11->Add( m_staticTextProcessed, 0, wxALIGN_RIGHT|wxRIGHT|wxALIGN_CENTER_VERTICAL, 5 ); + + m_staticTextRemaining = new wxStaticText( m_panelLog, wxID_ANY, _("Remaining:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextRemaining->Wrap( -1 ); + ffgSizer11->Add( m_staticTextRemaining, 0, wxALIGN_RIGHT|wxRIGHT|wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer42->Add( ffgSizer11, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM, 10 ); + + m_panelItemStats = new wxPanel( m_panelLog, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0 ); + m_panelItemStats->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) ); + + wxBoxSizer* bSizer291; + bSizer291 = new wxBoxSizer( wxVERTICAL ); + + wxFlexGridSizer* ffgSizer111; + ffgSizer111 = new wxFlexGridSizer( 0, 2, 5, 5 ); + ffgSizer111->SetFlexibleDirection( wxBOTH ); + ffgSizer111->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); + + wxBoxSizer* bSizer293; + bSizer293 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapItemStat = new wxStaticBitmap( m_panelItemStats, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer293->Add( m_bitmapItemStat, 0, wxRIGHT|wxALIGN_CENTER_VERTICAL, 5 ); + + m_staticTextItemsProcessed = new wxStaticText( m_panelItemStats, wxID_ANY, _("dummy"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_staticTextItemsProcessed->Wrap( -1 ); + m_staticTextItemsProcessed->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + + bSizer293->Add( m_staticTextItemsProcessed, 0, wxALIGN_BOTTOM, 5 ); + + + ffgSizer111->Add( bSizer293, 0, wxEXPAND|wxALIGN_RIGHT, 5 ); + + m_staticTextBytesProcessed = new wxStaticText( m_panelItemStats, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextBytesProcessed->Wrap( -1 ); + ffgSizer111->Add( m_staticTextBytesProcessed, 0, wxALIGN_RIGHT|wxALIGN_BOTTOM, 5 ); + + m_staticTextItemsRemaining = new wxStaticText( m_panelItemStats, wxID_ANY, _("dummy"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_staticTextItemsRemaining->Wrap( -1 ); + m_staticTextItemsRemaining->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + + ffgSizer111->Add( m_staticTextItemsRemaining, 0, wxALIGN_RIGHT|wxALIGN_BOTTOM, 5 ); + + m_staticTextBytesRemaining = new wxStaticText( m_panelItemStats, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextBytesRemaining->Wrap( -1 ); + ffgSizer111->Add( m_staticTextBytesRemaining, 0, wxALIGN_RIGHT|wxALIGN_BOTTOM, 5 ); + + + bSizer291->Add( ffgSizer111, 0, wxALL, 5 ); + + + m_panelItemStats->SetSizer( bSizer291 ); + m_panelItemStats->Layout(); + bSizer291->Fit( m_panelItemStats ); + bSizer42->Add( m_panelItemStats, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxRIGHT, 10 ); + + m_panelTimeStats = new wxPanel( m_panelLog, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0 ); + m_panelTimeStats->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) ); + + wxBoxSizer* bSizer292; + bSizer292 = new wxBoxSizer( wxVERTICAL ); + + wxFlexGridSizer* ffgSizer112; + ffgSizer112 = new wxFlexGridSizer( 0, 1, 5, 5 ); + ffgSizer112->SetFlexibleDirection( wxBOTH ); + ffgSizer112->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); + + wxBoxSizer* bSizer294; + bSizer294 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapTimeStat = new wxStaticBitmap( m_panelTimeStats, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer294->Add( m_bitmapTimeStat, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 ); + + m_staticTextTimeElapsed = new wxStaticText( m_panelTimeStats, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextTimeElapsed->Wrap( -1 ); + m_staticTextTimeElapsed->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + + bSizer294->Add( m_staticTextTimeElapsed, 0, wxALIGN_BOTTOM, 5 ); + + + ffgSizer112->Add( bSizer294, 0, wxEXPAND|wxALIGN_RIGHT, 5 ); + + + bSizer292->Add( ffgSizer112, 0, wxALL, 5 ); + + + m_panelTimeStats->SetSizer( bSizer292 ); + m_panelTimeStats->Layout(); + bSizer292->Fit( m_panelTimeStats ); + bSizer42->Add( m_panelTimeStats, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxRIGHT, 10 ); + + + bSizerLog->Add( bSizer42, 0, wxLEFT, 5 ); + + wxStaticLine* m_staticline70; + m_staticline70 = new wxStaticLine( m_panelLog, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizerLog->Add( m_staticline70, 0, wxEXPAND, 5 ); + + + m_panelLog->SetSizer( bSizerLog ); + m_panelLog->Layout(); + bSizerLog->Fit( m_panelLog ); + bSizerPanelHolder->Add( m_panelLog, 0, 0, 5 ); + + m_panelConfig = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panelConfig->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + bSizerConfig = new wxBoxSizer( wxVERTICAL ); + + bSizerCfgHistoryButtons = new wxBoxSizer( wxHORIZONTAL ); + + wxBoxSizer* bSizer17611; + bSizer17611 = new wxBoxSizer( wxVERTICAL ); + + m_bpButtonNew = new wxBitmapButton( m_panelConfig, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + m_bpButtonNew->SetToolTip( _("dummy") ); + + bSizer17611->Add( m_bpButtonNew, 0, wxEXPAND, 5 ); + + wxStaticText* m_staticText951; + m_staticText951 = new wxStaticText( m_panelConfig, wxID_ANY, _("New"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText951->Wrap( -1 ); + bSizer17611->Add( m_staticText951, 0, wxALIGN_CENTER_HORIZONTAL|wxRIGHT|wxLEFT, 2 ); + + + bSizerCfgHistoryButtons->Add( bSizer17611, 0, 0, 5 ); + + wxBoxSizer* bSizer1761; + bSizer1761 = new wxBoxSizer( wxVERTICAL ); + + m_bpButtonOpen = new wxBitmapButton( m_panelConfig, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + m_bpButtonOpen->SetToolTip( _("dummy") ); + + bSizer1761->Add( m_bpButtonOpen, 0, wxEXPAND, 5 ); + + wxStaticText* m_staticText95; + m_staticText95 = new wxStaticText( m_panelConfig, wxID_ANY, _("Open..."), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText95->Wrap( -1 ); + bSizer1761->Add( m_staticText95, 0, wxALIGN_CENTER_HORIZONTAL|wxRIGHT|wxLEFT, 2 ); + + + bSizerCfgHistoryButtons->Add( bSizer1761, 0, 0, 5 ); + + wxBoxSizer* bSizer175; + bSizer175 = new wxBoxSizer( wxVERTICAL ); + + m_bpButtonSave = new wxBitmapButton( m_panelConfig, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + m_bpButtonSave->SetToolTip( _("dummy") ); + + bSizer175->Add( m_bpButtonSave, 0, wxEXPAND, 5 ); + + wxStaticText* m_staticText961; + m_staticText961 = new wxStaticText( m_panelConfig, wxID_ANY, _("Save"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText961->Wrap( -1 ); + bSizer175->Add( m_staticText961, 0, wxALIGN_CENTER_HORIZONTAL|wxRIGHT|wxLEFT, 2 ); + + + bSizerCfgHistoryButtons->Add( bSizer175, 0, 0, 5 ); + + wxBoxSizer* bSizer174; + bSizer174 = new wxBoxSizer( wxVERTICAL ); + + bSizerSaveAs = new wxBoxSizer( wxHORIZONTAL ); + + m_bpButtonSaveAs = new wxBitmapButton( m_panelConfig, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + m_bpButtonSaveAs->SetToolTip( _("dummy") ); + + bSizerSaveAs->Add( m_bpButtonSaveAs, 1, 0, 5 ); + + m_bpButtonSaveAsBatch = new wxBitmapButton( m_panelConfig, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + m_bpButtonSaveAsBatch->SetToolTip( _("dummy") ); + + bSizerSaveAs->Add( m_bpButtonSaveAsBatch, 1, 0, 5 ); + + + bSizer174->Add( bSizerSaveAs, 0, wxEXPAND, 5 ); + + wxStaticText* m_staticText97; + m_staticText97 = new wxStaticText( m_panelConfig, wxID_ANY, _("Save as..."), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText97->Wrap( -1 ); + bSizer174->Add( m_staticText97, 0, wxALIGN_CENTER_HORIZONTAL|wxRIGHT|wxLEFT, 2 ); + + + bSizerCfgHistoryButtons->Add( bSizer174, 0, 0, 5 ); + + + bSizerConfig->Add( bSizerCfgHistoryButtons, 0, wxALIGN_CENTER_HORIZONTAL, 5 ); + + wxStaticLine* m_staticline81; + m_staticline81 = new wxStaticLine( m_panelConfig, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizerConfig->Add( m_staticline81, 0, wxEXPAND|wxTOP, 5 ); + + + bSizerConfig->Add( 10, 0, 0, 0, 5 ); + + m_gridCfgHistory = new zen::Grid( m_panelConfig, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL|wxVSCROLL ); + m_gridCfgHistory->SetScrollRate( 5, 5 ); + bSizerConfig->Add( m_gridCfgHistory, 1, wxEXPAND, 5 ); + + + m_panelConfig->SetSizer( bSizerConfig ); + m_panelConfig->Layout(); + bSizerConfig->Fit( m_panelConfig ); + bSizerPanelHolder->Add( m_panelConfig, 0, 0, 5 ); + + m_panelViewFilter = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panelViewFilter->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + bSizerViewFilter = new wxBoxSizer( wxHORIZONTAL ); + + m_bpButtonToggleLog = new zen::ToggleButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, wxBU_AUTODRAW|0 ); + bSizerViewFilter->Add( m_bpButtonToggleLog, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 ); + + + bSizerViewFilter->Add( 0, 0, 1, wxEXPAND, 5 ); + + bSizerViewButtons = new wxBoxSizer( wxHORIZONTAL ); + + m_bpButtonViewType = new zen::ToggleButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + bSizerViewButtons->Add( m_bpButtonViewType, 0, wxEXPAND, 5 ); + + + bSizerViewButtons->Add( 10, 10, 0, 0, 5 ); + + m_bpButtonShowExcluded = new zen::ToggleButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + bSizerViewButtons->Add( m_bpButtonShowExcluded, 0, wxEXPAND, 5 ); + + + bSizerViewButtons->Add( 10, 10, 0, 0, 5 ); + + m_bpButtonShowDeleteLeft = new zen::ToggleButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + bSizerViewButtons->Add( m_bpButtonShowDeleteLeft, 0, wxEXPAND, 5 ); + + m_bpButtonShowUpdateLeft = new zen::ToggleButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + bSizerViewButtons->Add( m_bpButtonShowUpdateLeft, 0, wxEXPAND, 5 ); + + m_bpButtonShowCreateLeft = new zen::ToggleButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + bSizerViewButtons->Add( m_bpButtonShowCreateLeft, 0, wxEXPAND, 5 ); + + m_bpButtonShowLeftOnly = new zen::ToggleButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + bSizerViewButtons->Add( m_bpButtonShowLeftOnly, 0, wxEXPAND, 5 ); + + m_bpButtonShowLeftNewer = new zen::ToggleButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + bSizerViewButtons->Add( m_bpButtonShowLeftNewer, 0, wxEXPAND, 5 ); + + m_bpButtonShowEqual = new zen::ToggleButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + bSizerViewButtons->Add( m_bpButtonShowEqual, 0, wxEXPAND, 5 ); + + m_bpButtonShowDoNothing = new zen::ToggleButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + bSizerViewButtons->Add( m_bpButtonShowDoNothing, 0, wxEXPAND, 5 ); + + m_bpButtonShowDifferent = new zen::ToggleButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + bSizerViewButtons->Add( m_bpButtonShowDifferent, 0, wxEXPAND, 5 ); + + m_bpButtonShowRightNewer = new zen::ToggleButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + bSizerViewButtons->Add( m_bpButtonShowRightNewer, 0, wxEXPAND, 5 ); + + m_bpButtonShowRightOnly = new zen::ToggleButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + bSizerViewButtons->Add( m_bpButtonShowRightOnly, 0, wxEXPAND, 5 ); + + m_bpButtonShowCreateRight = new zen::ToggleButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + bSizerViewButtons->Add( m_bpButtonShowCreateRight, 0, wxEXPAND, 5 ); + + m_bpButtonShowUpdateRight = new zen::ToggleButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + bSizerViewButtons->Add( m_bpButtonShowUpdateRight, 0, wxEXPAND, 5 ); + + m_bpButtonShowDeleteRight = new zen::ToggleButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + bSizerViewButtons->Add( m_bpButtonShowDeleteRight, 0, wxEXPAND, 5 ); + + m_bpButtonShowConflict = new zen::ToggleButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + bSizerViewButtons->Add( m_bpButtonShowConflict, 0, wxEXPAND, 5 ); + + m_bpButtonViewFilterContext = new wxBitmapButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + bSizerViewButtons->Add( m_bpButtonViewFilterContext, 0, wxEXPAND, 5 ); + + + bSizerViewFilter->Add( bSizerViewButtons, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 ); + + + bSizerViewFilter->Add( 0, 0, 1, wxEXPAND, 5 ); + + wxStaticText* m_staticText96; + m_staticText96 = new wxStaticText( m_panelViewFilter, wxID_ANY, _("Statistics:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText96->Wrap( -1 ); + bSizerViewFilter->Add( m_staticText96, 0, wxALL|wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 ); + + m_panelStatistics = new wxPanel( m_panelViewFilter, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL|wxBORDER_SUNKEN ); + m_panelStatistics->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer1801; + bSizer1801 = new wxBoxSizer( wxVERTICAL ); + + bSizerStatistics = new wxBoxSizer( wxHORIZONTAL ); + + wxBoxSizer* bSizer173; + bSizer173 = new wxBoxSizer( wxVERTICAL ); + + m_bitmapDeleteLeft = new wxStaticBitmap( m_panelStatistics, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + m_bitmapDeleteLeft->SetToolTip( _("Number of files and folders that will be deleted") ); + + bSizer173->Add( m_bitmapDeleteLeft, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer173->Add( 5, 2, 0, 0, 5 ); + + + bSizer173->Add( 0, 0, 1, wxEXPAND, 5 ); + + m_staticTextDeleteLeft = new wxStaticText( m_panelStatistics, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextDeleteLeft->Wrap( -1 ); + m_staticTextDeleteLeft->SetToolTip( _("Number of files and folders that will be deleted") ); + + bSizer173->Add( m_staticTextDeleteLeft, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizerStatistics->Add( bSizer173, 0, wxEXPAND, 5 ); + + + bSizerStatistics->Add( 10, 10, 0, 0, 5 ); + + wxBoxSizer* bSizer172; + bSizer172 = new wxBoxSizer( wxVERTICAL ); + + m_bitmapUpdateLeft = new wxStaticBitmap( m_panelStatistics, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + m_bitmapUpdateLeft->SetToolTip( _("Number of files that will be updated") ); + + bSizer172->Add( m_bitmapUpdateLeft, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer172->Add( 5, 2, 0, 0, 5 ); + + + bSizer172->Add( 0, 0, 1, wxEXPAND, 5 ); + + m_staticTextUpdateLeft = new wxStaticText( m_panelStatistics, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextUpdateLeft->Wrap( -1 ); + m_staticTextUpdateLeft->SetToolTip( _("Number of files that will be updated") ); + + bSizer172->Add( m_staticTextUpdateLeft, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 ); + + + bSizerStatistics->Add( bSizer172, 0, wxEXPAND, 5 ); + + + bSizerStatistics->Add( 10, 5, 0, 0, 5 ); + + wxBoxSizer* bSizer1712; + bSizer1712 = new wxBoxSizer( wxVERTICAL ); + + m_bitmapCreateLeft = new wxStaticBitmap( m_panelStatistics, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + m_bitmapCreateLeft->SetToolTip( _("Number of files and folders that will be created") ); + + bSizer1712->Add( m_bitmapCreateLeft, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 ); + + + bSizer1712->Add( 5, 2, 0, 0, 5 ); + + + bSizer1712->Add( 0, 0, 1, wxEXPAND, 5 ); + + m_staticTextCreateLeft = new wxStaticText( m_panelStatistics, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextCreateLeft->Wrap( -1 ); + m_staticTextCreateLeft->SetToolTip( _("Number of files and folders that will be created") ); + + bSizer1712->Add( m_staticTextCreateLeft, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 ); + + + bSizerStatistics->Add( bSizer1712, 0, wxEXPAND, 5 ); + + + bSizerStatistics->Add( 10, 5, 0, 0, 5 ); + + wxBoxSizer* bSizer311; + bSizer311 = new wxBoxSizer( wxVERTICAL ); + + m_bitmapData = new wxStaticBitmap( m_panelStatistics, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + m_bitmapData->SetToolTip( _("Total bytes to copy") ); + + bSizer311->Add( m_bitmapData, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 ); + + + bSizer311->Add( 5, 2, 0, 0, 5 ); + + + bSizer311->Add( 0, 0, 1, wxEXPAND, 5 ); + + m_staticTextData = new wxStaticText( m_panelStatistics, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextData->Wrap( -1 ); + m_staticTextData->SetToolTip( _("Total bytes to copy") ); + + bSizer311->Add( m_staticTextData, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizerStatistics->Add( bSizer311, 0, wxEXPAND, 5 ); + + + bSizerStatistics->Add( 10, 5, 0, 0, 5 ); + + wxBoxSizer* bSizer178; + bSizer178 = new wxBoxSizer( wxVERTICAL ); + + m_bitmapCreateRight = new wxStaticBitmap( m_panelStatistics, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + m_bitmapCreateRight->SetToolTip( _("Number of files and folders that will be created") ); + + bSizer178->Add( m_bitmapCreateRight, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 ); + + + bSizer178->Add( 5, 2, 0, 0, 5 ); + + + bSizer178->Add( 0, 0, 1, wxEXPAND, 5 ); + + m_staticTextCreateRight = new wxStaticText( m_panelStatistics, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextCreateRight->Wrap( -1 ); + m_staticTextCreateRight->SetToolTip( _("Number of files and folders that will be created") ); + + bSizer178->Add( m_staticTextCreateRight, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizerStatistics->Add( bSizer178, 0, wxEXPAND, 5 ); + + + bSizerStatistics->Add( 10, 5, 0, 0, 5 ); + + wxBoxSizer* bSizer177; + bSizer177 = new wxBoxSizer( wxVERTICAL ); + + m_bitmapUpdateRight = new wxStaticBitmap( m_panelStatistics, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + m_bitmapUpdateRight->SetToolTip( _("Number of files that will be updated") ); + + bSizer177->Add( m_bitmapUpdateRight, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 ); + + + bSizer177->Add( 5, 2, 0, 0, 5 ); + + + bSizer177->Add( 0, 0, 1, wxEXPAND, 5 ); + + m_staticTextUpdateRight = new wxStaticText( m_panelStatistics, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextUpdateRight->Wrap( -1 ); + m_staticTextUpdateRight->SetToolTip( _("Number of files that will be updated") ); + + bSizer177->Add( m_staticTextUpdateRight, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizerStatistics->Add( bSizer177, 0, wxEXPAND, 5 ); + + + bSizerStatistics->Add( 10, 10, 0, 0, 5 ); + + wxBoxSizer* bSizer176; + bSizer176 = new wxBoxSizer( wxVERTICAL ); + + m_bitmapDeleteRight = new wxStaticBitmap( m_panelStatistics, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + m_bitmapDeleteRight->SetToolTip( _("Number of files and folders that will be deleted") ); + + bSizer176->Add( m_bitmapDeleteRight, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer176->Add( 5, 2, 0, 0, 5 ); + + + bSizer176->Add( 0, 0, 1, wxEXPAND, 5 ); + + m_staticTextDeleteRight = new wxStaticText( m_panelStatistics, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextDeleteRight->Wrap( -1 ); + m_staticTextDeleteRight->SetToolTip( _("Number of files and folders that will be deleted") ); + + bSizer176->Add( m_staticTextDeleteRight, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizerStatistics->Add( bSizer176, 0, wxEXPAND, 5 ); + + + bSizer1801->Add( bSizerStatistics, 0, wxALL, 4 ); + + + m_panelStatistics->SetSizer( bSizer1801 ); + m_panelStatistics->Layout(); + bSizer1801->Fit( m_panelStatistics ); + bSizerViewFilter->Add( m_panelStatistics, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 ); + + + m_panelViewFilter->SetSizer( bSizerViewFilter ); + m_panelViewFilter->Layout(); + bSizerViewFilter->Fit( m_panelViewFilter ); + bSizerPanelHolder->Add( m_panelViewFilter, 0, 0, 5 ); + + + this->SetSizer( bSizerPanelHolder ); + this->Layout(); + bSizerPanelHolder->Fit( this ); + + // Connect Events + this->Connect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( MainDialogGenerated::onClose ) ); + m_menuFile->Bind(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDialogGenerated::onConfigNew ), this, m_menuItemNew->GetId()); + m_menuFile->Bind(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDialogGenerated::onConfigLoad ), this, m_menuItemLoad->GetId()); + m_menuFile->Bind(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDialogGenerated::onConfigSave ), this, m_menuItemSave->GetId()); + m_menuFile->Bind(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDialogGenerated::onConfigSaveAs ), this, m_menuItemSaveAs->GetId()); + m_menuFile->Bind(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDialogGenerated::onSaveAsBatchJob ), this, m_menuItemSaveAsBatch->GetId()); + m_menuFile->Bind(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDialogGenerated::onMenuQuit ), this, m_menuItemQuit->GetId()); + m_menuActions->Bind(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDialogGenerated::onToggleLog ), this, m_menuItemShowLog->GetId()); + m_menuActions->Bind(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDialogGenerated::onCompare ), this, m_menuItemCompare->GetId()); + m_menuActions->Bind(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDialogGenerated::onCmpSettings ), this, m_menuItemCompSettings->GetId()); + m_menuActions->Bind(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDialogGenerated::onConfigureFilter ), this, m_menuItemFilter->GetId()); + m_menuActions->Bind(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDialogGenerated::onSyncSettings ), this, m_menuItemSyncSettings->GetId()); + m_menuActions->Bind(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDialogGenerated::onStartSync ), this, m_menuItemSynchronize->GetId()); + m_menuTools->Bind(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDialogGenerated::onMenuOptions ), this, m_menuItemOptions->GetId()); + m_menuTools->Bind(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDialogGenerated::onMenuFindItem ), this, m_menuItemFind->GetId()); + m_menuTools->Bind(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDialogGenerated::onMenuExportFileList ), this, m_menuItemExportList->GetId()); + m_menuTools->Bind(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDialogGenerated::onMenuResetLayout ), this, m_menuItemResetLayout->GetId()); + m_menuHelp->Bind(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDialogGenerated::onShowHelp ), this, m_menuItemHelp->GetId()); + m_menuHelp->Bind(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDialogGenerated::onMenuCheckVersion ), this, m_menuItemCheckVersionNow->GetId()); + m_menuHelp->Bind(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDialogGenerated::onMenuAbout ), this, m_menuItemAbout->GetId()); + m_buttonCompare->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onCompare ), NULL, this ); + m_bpButtonCmpConfig->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onCmpSettings ), NULL, this ); + m_bpButtonCmpConfig->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::onCompSettingsContextMouse ), NULL, this ); + m_bpButtonCmpContext->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onCompSettingsContext ), NULL, this ); + m_bpButtonCmpContext->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::onCompSettingsContextMouse ), NULL, this ); + m_bpButtonFilter->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onConfigureFilter ), NULL, this ); + m_bpButtonFilter->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::onGlobalFilterContextMouse ), NULL, this ); + m_bpButtonFilterContext->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onGlobalFilterContext ), NULL, this ); + m_bpButtonFilterContext->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::onGlobalFilterContextMouse ), NULL, this ); + m_bpButtonSyncConfig->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onSyncSettings ), NULL, this ); + m_bpButtonSyncConfig->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::onSyncSettingsContextMouse ), NULL, this ); + m_bpButtonSyncContext->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onSyncSettingsContext ), NULL, this ); + m_bpButtonSyncContext->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::onSyncSettingsContextMouse ), NULL, this ); + m_buttonSync->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onStartSync ), NULL, this ); + m_bpButtonAddPair->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onTopFolderPairAdd ), NULL, this ); + m_bpButtonRemovePair->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onTopFolderPairRemove ), NULL, this ); + m_bpButtonSwapSides->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onSwapSides ), NULL, this ); + m_bpButtonLocalCompCfg->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onTopLocalCompCfg ), NULL, this ); + m_bpButtonLocalFilter->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onTopLocalFilterCfg ), NULL, this ); + m_bpButtonLocalSyncCfg->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onTopLocalSyncCfg ), NULL, this ); + m_bpButtonHideSearch->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onHideSearchPanel ), NULL, this ); + m_textCtrlSearchTxt->Connect( wxEVT_COMMAND_TEXT_ENTER, wxCommandEventHandler( MainDialogGenerated::onSearchGridEnter ), NULL, this ); + m_bpButtonNew->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onConfigNew ), NULL, this ); + m_bpButtonOpen->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onConfigLoad ), NULL, this ); + m_bpButtonSave->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onConfigSave ), NULL, this ); + m_bpButtonSaveAs->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onConfigSaveAs ), NULL, this ); + m_bpButtonSaveAsBatch->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onSaveAsBatchJob ), NULL, this ); + m_bpButtonToggleLog->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onToggleLog ), NULL, this ); + m_bpButtonViewType->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onToggleViewType ), NULL, this ); + m_bpButtonViewType->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::onViewTypeContextMouse ), NULL, this ); + m_bpButtonShowExcluded->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onToggleViewButton ), NULL, this ); + m_bpButtonShowExcluded->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::onViewFilterContextMouse ), NULL, this ); + m_bpButtonShowDeleteLeft->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onToggleViewButton ), NULL, this ); + m_bpButtonShowDeleteLeft->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::onViewFilterContextMouse ), NULL, this ); + m_bpButtonShowUpdateLeft->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onToggleViewButton ), NULL, this ); + m_bpButtonShowUpdateLeft->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::onViewFilterContextMouse ), NULL, this ); + m_bpButtonShowCreateLeft->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onToggleViewButton ), NULL, this ); + m_bpButtonShowCreateLeft->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::onViewFilterContextMouse ), NULL, this ); + m_bpButtonShowLeftOnly->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onToggleViewButton ), NULL, this ); + m_bpButtonShowLeftOnly->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::onViewFilterContextMouse ), NULL, this ); + m_bpButtonShowLeftNewer->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onToggleViewButton ), NULL, this ); + m_bpButtonShowLeftNewer->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::onViewFilterContextMouse ), NULL, this ); + m_bpButtonShowEqual->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onToggleViewButton ), NULL, this ); + m_bpButtonShowEqual->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::onViewFilterContextMouse ), NULL, this ); + m_bpButtonShowDoNothing->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onToggleViewButton ), NULL, this ); + m_bpButtonShowDoNothing->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::onViewFilterContextMouse ), NULL, this ); + m_bpButtonShowDifferent->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onToggleViewButton ), NULL, this ); + m_bpButtonShowDifferent->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::onViewFilterContextMouse ), NULL, this ); + m_bpButtonShowRightNewer->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onToggleViewButton ), NULL, this ); + m_bpButtonShowRightNewer->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::onViewFilterContextMouse ), NULL, this ); + m_bpButtonShowRightOnly->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onToggleViewButton ), NULL, this ); + m_bpButtonShowRightOnly->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::onViewFilterContextMouse ), NULL, this ); + m_bpButtonShowCreateRight->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onToggleViewButton ), NULL, this ); + m_bpButtonShowCreateRight->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::onViewFilterContextMouse ), NULL, this ); + m_bpButtonShowUpdateRight->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onToggleViewButton ), NULL, this ); + m_bpButtonShowUpdateRight->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::onViewFilterContextMouse ), NULL, this ); + m_bpButtonShowDeleteRight->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onToggleViewButton ), NULL, this ); + m_bpButtonShowDeleteRight->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::onViewFilterContextMouse ), NULL, this ); + m_bpButtonShowConflict->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onToggleViewButton ), NULL, this ); + m_bpButtonShowConflict->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::onViewFilterContextMouse ), NULL, this ); + m_bpButtonViewFilterContext->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onViewFilterContext ), NULL, this ); + m_bpButtonViewFilterContext->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::onViewFilterContextMouse ), NULL, this ); +} + +MainDialogGenerated::~MainDialogGenerated() +{ +} + +FolderPairPanelGenerated::FolderPairPanelGenerated( wxWindow* parent, wxWindowID id, const wxPoint& pos, const wxSize& size, long style, const wxString& name ) : wxPanel( parent, id, pos, size, style, name ) +{ + wxBoxSizer* bSizer74; + bSizer74 = new wxBoxSizer( wxHORIZONTAL ); + + m_panelLeft = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panelLeft->SetMinSize( wxSize( 1, -1 ) ); + + wxBoxSizer* bSizer134; + bSizer134 = new wxBoxSizer( wxHORIZONTAL ); + + m_bpButtonFolderPairOptions = new wxBitmapButton( m_panelLeft, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + m_bpButtonFolderPairOptions->SetToolTip( _("Arrange folder pair") ); + + bSizer134->Add( m_bpButtonFolderPairOptions, 0, wxEXPAND, 5 ); + + m_bpButtonRemovePair = new wxBitmapButton( m_panelLeft, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + m_bpButtonRemovePair->SetToolTip( _("Remove folder pair") ); + + bSizer134->Add( m_bpButtonRemovePair, 0, wxEXPAND, 5 ); + + m_folderPathLeft = new fff::FolderHistoryBox( m_panelLeft, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0, NULL, 0 ); + bSizer134->Add( m_folderPathLeft, 1, wxALIGN_CENTER_VERTICAL, 5 ); + + m_buttonSelectFolderLeft = new wxButton( m_panelLeft, wxID_ANY, _("Browse"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_buttonSelectFolderLeft->SetToolTip( _("Select a folder") ); + + bSizer134->Add( m_buttonSelectFolderLeft, 0, wxEXPAND, 5 ); + + m_bpButtonSelectAltFolderLeft = new wxBitmapButton( m_panelLeft, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + m_bpButtonSelectAltFolderLeft->SetToolTip( _("Access online storage") ); + + bSizer134->Add( m_bpButtonSelectAltFolderLeft, 0, wxEXPAND, 5 ); + + + m_panelLeft->SetSizer( bSizer134 ); + m_panelLeft->Layout(); + bSizer134->Fit( m_panelLeft ); + bSizer74->Add( m_panelLeft, 0, wxLEFT|wxEXPAND, 5 ); + + wxPanel* m_panel20; + m_panel20 = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + wxBoxSizer* bSizer95; + bSizer95 = new wxBoxSizer( wxHORIZONTAL ); + + m_bpButtonLocalCompCfg = new wxBitmapButton( m_panel20, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + m_bpButtonLocalCompCfg->SetToolTip( _("dummy") ); + + bSizer95->Add( m_bpButtonLocalCompCfg, 0, wxEXPAND, 5 ); + + m_bpButtonLocalFilter = new wxBitmapButton( m_panel20, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + m_bpButtonLocalFilter->SetToolTip( _("dummy") ); + + bSizer95->Add( m_bpButtonLocalFilter, 0, wxEXPAND, 5 ); + + m_bpButtonLocalSyncCfg = new wxBitmapButton( m_panel20, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + m_bpButtonLocalSyncCfg->SetToolTip( _("dummy") ); + + bSizer95->Add( m_bpButtonLocalSyncCfg, 0, wxEXPAND, 5 ); + + + m_panel20->SetSizer( bSizer95 ); + m_panel20->Layout(); + bSizer95->Fit( m_panel20 ); + bSizer74->Add( m_panel20, 0, wxRIGHT|wxLEFT|wxEXPAND, 5 ); + + m_panelRight = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panelRight->SetMinSize( wxSize( 1, -1 ) ); + + wxBoxSizer* bSizer135; + bSizer135 = new wxBoxSizer( wxHORIZONTAL ); + + m_folderPathRight = new fff::FolderHistoryBox( m_panelRight, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0, NULL, 0 ); + bSizer135->Add( m_folderPathRight, 1, wxALIGN_CENTER_VERTICAL, 5 ); + + m_buttonSelectFolderRight = new wxButton( m_panelRight, wxID_ANY, _("Browse"), wxDefaultPosition, wxDefaultSize, 0 ); + m_buttonSelectFolderRight->SetToolTip( _("Select a folder") ); + + bSizer135->Add( m_buttonSelectFolderRight, 0, wxEXPAND, 5 ); + + m_bpButtonSelectAltFolderRight = new wxBitmapButton( m_panelRight, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + m_bpButtonSelectAltFolderRight->SetToolTip( _("Access online storage") ); + + bSizer135->Add( m_bpButtonSelectAltFolderRight, 0, wxEXPAND, 5 ); + + + m_panelRight->SetSizer( bSizer135 ); + m_panelRight->Layout(); + bSizer135->Fit( m_panelRight ); + bSizer74->Add( m_panelRight, 1, wxRIGHT|wxEXPAND, 5 ); + + + this->SetSizer( bSizer74 ); + this->Layout(); +} + +FolderPairPanelGenerated::~FolderPairPanelGenerated() +{ +} + +ConfigDlgGenerated::ConfigDlgGenerated( wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style ) : wxDialog( parent, id, title, pos, size, style ) +{ + this->SetSizeHints( wxDefaultSize, wxDefaultSize ); + this->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) ); + + wxBoxSizer* bSizer7; + bSizer7 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer190; + bSizer190 = new wxBoxSizer( wxHORIZONTAL ); + + wxBoxSizer* bSizer1911; + bSizer1911 = new wxBoxSizer( wxVERTICAL ); + + m_staticTextFolderPairLabel = new wxStaticText( this, wxID_ANY, _("Folder pair:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextFolderPairLabel->Wrap( -1 ); + bSizer1911->Add( m_staticTextFolderPairLabel, 0, wxALL, 5 ); + + m_listBoxFolderPair = new wxListBox( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0, NULL, wxLB_NEEDED_SB ); + bSizer1911->Add( m_listBoxFolderPair, 1, 0, 5 ); + + + bSizer190->Add( bSizer1911, 0, wxEXPAND, 5 ); + + m_notebook = new wxNotebook( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNB_NOPAGETHEME ); + m_panelCompSettingsTab = new wxPanel( m_notebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panelCompSettingsTab->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer275; + bSizer275 = new wxBoxSizer( wxVERTICAL ); + + bSizerHeaderCompSettings = new wxBoxSizer( wxVERTICAL ); + + m_staticTextMainCompSettings = new wxStaticText( m_panelCompSettingsTab, wxID_ANY, _("Common settings:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextMainCompSettings->Wrap( -1 ); + bSizerHeaderCompSettings->Add( m_staticTextMainCompSettings, 0, wxALL, 10 ); + + m_checkBoxUseLocalCmpOptions = new wxCheckBox( m_panelCompSettingsTab, wxID_ANY, _("Use local settings:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_checkBoxUseLocalCmpOptions->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + bSizerHeaderCompSettings->Add( m_checkBoxUseLocalCmpOptions, 0, wxALL|wxEXPAND, 10 ); + + m_staticlineCompHeader = new wxStaticLine( m_panelCompSettingsTab, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizerHeaderCompSettings->Add( m_staticlineCompHeader, 0, wxEXPAND, 5 ); + + + bSizer275->Add( bSizerHeaderCompSettings, 0, wxEXPAND, 5 ); + + m_panelComparisonSettings = new wxPanel( m_panelCompSettingsTab, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panelComparisonSettings->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer2561; + bSizer2561 = new wxBoxSizer( wxHORIZONTAL ); + + wxBoxSizer* bSizer159; + bSizer159 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer178; + bSizer178 = new wxBoxSizer( wxHORIZONTAL ); + + wxBoxSizer* bSizer182; + bSizer182 = new wxBoxSizer( wxVERTICAL ); + + wxStaticText* m_staticText91; + m_staticText91 = new wxStaticText( m_panelComparisonSettings, wxID_ANY, _("Select a variant:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText91->Wrap( -1 ); + bSizer182->Add( m_staticText91, 0, wxALL, 5 ); + + wxGridSizer* gSizer2; + gSizer2 = new wxGridSizer( 0, 1, 0, 0 ); + + m_buttonByTimeSize = new zen::ToggleButton( m_panelComparisonSettings, wxID_ANY, _("File time and size"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_buttonByTimeSize->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + m_buttonByTimeSize->SetToolTip( _("dummy") ); + + gSizer2->Add( m_buttonByTimeSize, 0, wxEXPAND, 5 ); + + m_buttonByContent = new zen::ToggleButton( m_panelComparisonSettings, wxID_ANY, _("File content"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_buttonByContent->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + m_buttonByContent->SetToolTip( _("dummy") ); + + gSizer2->Add( m_buttonByContent, 0, wxEXPAND, 5 ); + + m_buttonBySize = new zen::ToggleButton( m_panelComparisonSettings, wxID_ANY, _("File size"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_buttonBySize->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + m_buttonBySize->SetToolTip( _("dummy") ); + + gSizer2->Add( m_buttonBySize, 0, wxEXPAND, 5 ); + + + bSizer182->Add( gSizer2, 0, wxBOTTOM|wxRIGHT|wxLEFT, 5 ); + + + bSizer178->Add( bSizer182, 0, wxALL, 5 ); + + wxBoxSizer* bSizer2371; + bSizer2371 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapCompVariant = new wxStaticBitmap( m_panelComparisonSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizer2371->Add( m_bitmapCompVariant, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + + m_staticTextCompVarDescription = new wxStaticText( m_panelComparisonSettings, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextCompVarDescription->Wrap( -1 ); + m_staticTextCompVarDescription->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + bSizer2371->Add( m_staticTextCompVarDescription, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer178->Add( bSizer2371, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 ); + + + bSizer159->Add( bSizer178, 0, wxEXPAND, 5 ); + + wxStaticLine* m_staticline33; + m_staticline33 = new wxStaticLine( m_panelComparisonSettings, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer159->Add( m_staticline33, 0, wxEXPAND, 5 ); + + wxBoxSizer* bSizer1734; + bSizer1734 = new wxBoxSizer( wxHORIZONTAL ); + + + bSizer1734->Add( 0, 0, 1, 0, 5 ); + + wxBoxSizer* bSizer1721; + bSizer1721 = new wxBoxSizer( wxVERTICAL ); + + m_checkBoxSymlinksInclude = new wxCheckBox( m_panelComparisonSettings, wxID_ANY, _("Include &symbolic links:"), wxDefaultPosition, wxDefaultSize, 0 ); + bSizer1721->Add( m_checkBoxSymlinksInclude, 0, wxALL, 5 ); + + wxBoxSizer* bSizer176; + bSizer176 = new wxBoxSizer( wxVERTICAL ); + + 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 ); + + m_radioBtnSymlinksDirect = new wxRadioButton( m_panelComparisonSettings, wxID_ANY, _("As &link"), wxDefaultPosition, wxDefaultSize, 0 ); + bSizer176->Add( m_radioBtnSymlinksDirect, 0, wxEXPAND|wxBOTTOM|wxRIGHT|wxLEFT, 5 ); + + + bSizer1721->Add( bSizer176, 0, wxLEFT|wxEXPAND, 15 ); + + + bSizer1721->Add( 0, 0, 1, wxEXPAND, 5 ); + + wxHyperlinkCtrl* m_hyperlink24; + m_hyperlink24 = new wxHyperlinkCtrl( m_panelComparisonSettings, wxID_ANY, _("More information"), wxT("https://freefilesync.org/manual.php?topic=comparison-settings"), wxDefaultPosition, wxDefaultSize, wxHL_DEFAULT_STYLE ); + m_hyperlink24->SetToolTip( _("https://freefilesync.org/manual.php?topic=comparison-settings") ); + + bSizer1721->Add( m_hyperlink24, 0, wxBOTTOM|wxRIGHT|wxLEFT, 5 ); + + + bSizer1734->Add( bSizer1721, 0, wxALL|wxEXPAND, 5 ); + + + bSizer1734->Add( 0, 0, 1, 0, 5 ); + + wxStaticLine* m_staticline44; + m_staticline44 = new wxStaticLine( m_panelComparisonSettings, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL ); + bSizer1734->Add( m_staticline44, 0, wxEXPAND, 5 ); + + + bSizer1734->Add( 0, 0, 1, 0, 5 ); + + wxBoxSizer* bSizer1733; + bSizer1733 = new wxBoxSizer( wxVERTICAL ); + + wxStaticText* m_staticText112; + m_staticText112 = new wxStaticText( m_panelComparisonSettings, wxID_ANY, _("&Ignore exact time shift [hh:mm]"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText112->Wrap( -1 ); + bSizer1733->Add( m_staticText112, 0, wxALL, 5 ); + + m_textCtrlTimeShift = new wxTextCtrl( m_panelComparisonSettings, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0 ); + m_textCtrlTimeShift->SetToolTip( _("List of file time offsets to ignore") ); + + bSizer1733->Add( m_textCtrlTimeShift, 0, wxBOTTOM|wxRIGHT|wxLEFT|wxEXPAND, 5 ); + + wxBoxSizer* bSizer197; + bSizer197 = new wxBoxSizer( wxHORIZONTAL ); + + wxStaticText* m_staticText1381; + m_staticText1381 = new wxStaticText( m_panelComparisonSettings, wxID_ANY, _("Example:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText1381->Wrap( -1 ); + m_staticText1381->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + bSizer197->Add( m_staticText1381, 0, wxBOTTOM|wxRIGHT|wxLEFT, 5 ); + + wxStaticText* m_staticText13811; + m_staticText13811 = new wxStaticText( m_panelComparisonSettings, wxID_ANY, _("1, 2, 4:30"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText13811->Wrap( -1 ); + m_staticText13811->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + bSizer197->Add( m_staticText13811, 0, wxBOTTOM|wxRIGHT, 5 ); + + + bSizer1733->Add( bSizer197, 0, 0, 5 ); + + + bSizer1733->Add( 0, 0, 1, wxEXPAND, 5 ); + + wxHyperlinkCtrl* m_hyperlink241; + m_hyperlink241 = new wxHyperlinkCtrl( m_panelComparisonSettings, wxID_ANY, _("Handle daylight saving time"), wxT("https://freefilesync.org/manual.php?topic=daylight-saving-time"), wxDefaultPosition, wxDefaultSize, wxHL_DEFAULT_STYLE ); + m_hyperlink241->SetToolTip( _("https://freefilesync.org/manual.php?topic=daylight-saving-time") ); + + bSizer1733->Add( m_hyperlink241, 0, wxBOTTOM|wxRIGHT|wxLEFT, 5 ); + + + bSizer1734->Add( bSizer1733, 0, wxALL|wxEXPAND, 5 ); + + + bSizer1734->Add( 0, 0, 1, 0, 5 ); + + + bSizer159->Add( bSizer1734, 0, wxEXPAND, 5 ); + + wxStaticLine* m_staticline331; + m_staticline331 = new wxStaticLine( m_panelComparisonSettings, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer159->Add( m_staticline331, 0, wxEXPAND, 5 ); + + + bSizer159->Add( 0, 0, 1, 0, 5 ); + + bSizerCompMisc = new wxBoxSizer( wxVERTICAL ); + + wxStaticLine* m_staticline3311; + m_staticline3311 = new wxStaticLine( m_panelComparisonSettings, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizerCompMisc->Add( m_staticline3311, 0, wxEXPAND, 5 ); + + wxBoxSizer* bSizer2781; + bSizer2781 = new wxBoxSizer( wxHORIZONTAL ); + + wxFlexGridSizer* fgSizer61; + fgSizer61 = new wxFlexGridSizer( 0, 2, 5, 5 ); + fgSizer61->SetFlexibleDirection( wxBOTH ); + fgSizer61->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); + + m_bitmapIgnoreErrors = new wxStaticBitmap( m_panelComparisonSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + fgSizer61->Add( m_bitmapIgnoreErrors, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + m_checkBoxIgnoreErrors = new wxCheckBox( m_panelComparisonSettings, wxID_ANY, _("Ignore errors"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + fgSizer61->Add( m_checkBoxIgnoreErrors, 0, wxALIGN_CENTER_VERTICAL|wxEXPAND, 5 ); + + m_bitmapRetryErrors = new wxStaticBitmap( m_panelComparisonSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + fgSizer61->Add( m_bitmapRetryErrors, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 ); + + m_checkBoxAutoRetry = new wxCheckBox( m_panelComparisonSettings, wxID_ANY, _("Automatic retry"), wxDefaultPosition, wxDefaultSize, 0 ); + fgSizer61->Add( m_checkBoxAutoRetry, 0, wxALIGN_CENTER_VERTICAL|wxEXPAND, 5 ); + + + bSizer2781->Add( fgSizer61, 0, wxALIGN_CENTER_VERTICAL|wxALL, 10 ); + + fgSizerAutoRetry = new wxFlexGridSizer( 0, 2, 5, 10 ); + fgSizerAutoRetry->SetFlexibleDirection( wxBOTH ); + fgSizerAutoRetry->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); + + wxStaticText* m_staticText96; + m_staticText96 = new wxStaticText( m_panelComparisonSettings, wxID_ANY, _("Retry count:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText96->Wrap( -1 ); + fgSizerAutoRetry->Add( m_staticText96, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + m_staticTextAutoRetryDelay = new wxStaticText( m_panelComparisonSettings, wxID_ANY, _("Delay (in seconds):"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextAutoRetryDelay->Wrap( -1 ); + fgSizerAutoRetry->Add( m_staticTextAutoRetryDelay, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + m_spinCtrlAutoRetryCount = new wxSpinCtrl( m_panelComparisonSettings, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize( -1, -1 ), wxSP_ARROW_KEYS, 1, 2000000000, 1 ); + fgSizerAutoRetry->Add( m_spinCtrlAutoRetryCount, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + m_spinCtrlAutoRetryDelay = new wxSpinCtrl( m_panelComparisonSettings, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize( -1, -1 ), wxSP_ARROW_KEYS, 0, 2000000000, 0 ); + fgSizerAutoRetry->Add( m_spinCtrlAutoRetryDelay, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer2781->Add( fgSizerAutoRetry, 0, wxTOP|wxBOTTOM|wxRIGHT|wxALIGN_CENTER_VERTICAL, 10 ); + + + bSizerCompMisc->Add( bSizer2781, 0, wxEXPAND, 5 ); + + + bSizer159->Add( bSizerCompMisc, 0, wxEXPAND, 5 ); + + + bSizer2561->Add( bSizer159, 0, wxEXPAND, 5 ); + + wxStaticLine* m_staticline751; + m_staticline751 = new wxStaticLine( m_panelComparisonSettings, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL ); + bSizer2561->Add( m_staticline751, 0, wxEXPAND, 5 ); + + bSizerPerformance = new wxBoxSizer( wxVERTICAL ); + + m_panel57 = new wxPanel( m_panelComparisonSettings, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panel57->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) ); + + wxBoxSizer* bSizer2191; + bSizer2191 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapPerf = new wxStaticBitmap( m_panel57, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer2191->Add( m_bitmapPerf, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 ); + + wxStaticText* m_staticText13611; + m_staticText13611 = new wxStaticText( m_panel57, wxID_ANY, _("Performance improvements:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText13611->Wrap( -1 ); + bSizer2191->Add( m_staticText13611, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxRIGHT, 10 ); + + + m_panel57->SetSizer( bSizer2191 ); + m_panel57->Layout(); + bSizer2191->Fit( m_panel57 ); + bSizerPerformance->Add( m_panel57, 0, wxEXPAND, 5 ); + + wxStaticLine* m_staticline75; + m_staticline75 = new wxStaticLine( m_panelComparisonSettings, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizerPerformance->Add( m_staticline75, 0, wxEXPAND, 5 ); + + m_hyperlinkPerfDeRequired = new wxHyperlinkCtrl( m_panelComparisonSettings, wxID_ANY, _("Requires FreeFileSync Donation Edition"), wxT("https://freefilesync.org/faq.php#donation-edition"), wxDefaultPosition, wxDefaultSize, wxHL_DEFAULT_STYLE ); + m_hyperlinkPerfDeRequired->SetToolTip( _("https://freefilesync.org/faq.php#donation-edition") ); + + bSizerPerformance->Add( m_hyperlinkPerfDeRequired, 0, wxALL, 10 ); + + bSizer260 = new wxBoxSizer( wxVERTICAL ); + + m_staticTextPerfParallelOps = new wxStaticText( m_panelComparisonSettings, wxID_ANY, _("Parallel file operations:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextPerfParallelOps->Wrap( -1 ); + bSizer260->Add( m_staticTextPerfParallelOps, 0, wxTOP|wxRIGHT|wxLEFT, 5 ); + + m_scrolledWindowPerf = new wxScrolledWindow( m_panelComparisonSettings, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL|wxVSCROLL ); + m_scrolledWindowPerf->SetScrollRate( 5, 5 ); + m_scrolledWindowPerf->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + fgSizerPerf = new wxFlexGridSizer( 0, 2, 5, 5 ); + fgSizerPerf->SetFlexibleDirection( wxBOTH ); + fgSizerPerf->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); + + + m_scrolledWindowPerf->SetSizer( fgSizerPerf ); + m_scrolledWindowPerf->Layout(); + fgSizerPerf->Fit( m_scrolledWindowPerf ); + bSizer260->Add( m_scrolledWindowPerf, 1, wxALL|wxEXPAND, 5 ); + + + bSizerPerformance->Add( bSizer260, 1, wxALL|wxEXPAND, 5 ); + + wxHyperlinkCtrl* m_hyperlink1711; + m_hyperlink1711 = new wxHyperlinkCtrl( m_panelComparisonSettings, wxID_ANY, _("How to get the best performance?"), wxT("https://freefilesync.org/manual.php?topic=performance"), wxDefaultPosition, wxDefaultSize, wxHL_DEFAULT_STYLE ); + m_hyperlink1711->SetToolTip( _("https://freefilesync.org/manual.php?topic=performance") ); + + bSizerPerformance->Add( m_hyperlink1711, 0, wxBOTTOM|wxRIGHT|wxLEFT, 10 ); + + + bSizer2561->Add( bSizerPerformance, 1, wxEXPAND, 5 ); + + + m_panelComparisonSettings->SetSizer( bSizer2561 ); + m_panelComparisonSettings->Layout(); + bSizer2561->Fit( m_panelComparisonSettings ); + bSizer275->Add( m_panelComparisonSettings, 1, wxEXPAND, 5 ); + + + m_panelCompSettingsTab->SetSizer( bSizer275 ); + m_panelCompSettingsTab->Layout(); + bSizer275->Fit( m_panelCompSettingsTab ); + m_notebook->AddPage( m_panelCompSettingsTab, _("dummy"), false ); + m_panelFilterSettingsTab = new wxPanel( m_notebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panelFilterSettingsTab->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer278; + bSizer278 = new wxBoxSizer( wxVERTICAL ); + + bSizerHeaderFilterSettings = new wxBoxSizer( wxVERTICAL ); + + m_staticTextMainFilterSettings = new wxStaticText( m_panelFilterSettingsTab, wxID_ANY, _("Common settings:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextMainFilterSettings->Wrap( -1 ); + bSizerHeaderFilterSettings->Add( m_staticTextMainFilterSettings, 0, wxALL, 10 ); + + m_staticTextLocalFilterSettings = new wxStaticText( m_panelFilterSettingsTab, wxID_ANY, _("Local settings:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextLocalFilterSettings->Wrap( -1 ); + bSizerHeaderFilterSettings->Add( m_staticTextLocalFilterSettings, 0, wxALL, 10 ); + + m_staticlineFilterHeader = new wxStaticLine( m_panelFilterSettingsTab, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizerHeaderFilterSettings->Add( m_staticlineFilterHeader, 0, wxEXPAND, 5 ); + + + bSizer278->Add( bSizerHeaderFilterSettings, 0, wxEXPAND, 5 ); + + wxPanel* m_panel571; + m_panel571 = new wxPanel( m_panelFilterSettingsTab, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panel571->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer307; + bSizer307 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer301; + bSizer301 = new wxBoxSizer( wxHORIZONTAL ); + + wxBoxSizer* bSizer166; + bSizer166 = new wxBoxSizer( wxVERTICAL ); + + + bSizer166->Add( 0, 10, 0, 0, 5 ); + + wxBoxSizer* bSizer1661; + bSizer1661 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapInclude = new wxStaticBitmap( m_panel571, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizer1661->Add( m_bitmapInclude, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL|wxALL, 5 ); + + wxBoxSizer* bSizer1731; + bSizer1731 = new wxBoxSizer( wxVERTICAL ); + + wxStaticText* m_staticText78; + m_staticText78 = new wxStaticText( m_panel571, wxID_ANY, _("Include:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText78->Wrap( -1 ); + bSizer1731->Add( m_staticText78, 0, 0, 5 ); + + m_textCtrlInclude = new wxTextCtrl( m_panel571, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize( -1, -1 ), wxTE_MULTILINE ); + bSizer1731->Add( m_textCtrlInclude, 1, wxEXPAND|wxTOP, 5 ); + + + bSizer1661->Add( bSizer1731, 1, wxEXPAND, 5 ); + + + bSizer166->Add( bSizer1661, 3, wxEXPAND|wxLEFT, 5 ); + + + bSizer166->Add( 0, 10, 0, 0, 5 ); + + wxBoxSizer* bSizer1651; + bSizer1651 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapExclude = new wxStaticBitmap( m_panel571, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizer1651->Add( m_bitmapExclude, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL|wxALL, 5 ); + + wxBoxSizer* bSizer1742; + bSizer1742 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer189; + bSizer189 = new wxBoxSizer( wxHORIZONTAL ); + + wxStaticText* m_staticText77; + m_staticText77 = new wxStaticText( m_panel571, wxID_ANY, _("Exclude:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText77->Wrap( -1 ); + bSizer189->Add( m_staticText77, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer189->Add( 0, 0, 1, wxEXPAND, 5 ); + + wxHyperlinkCtrl* m_hyperlink171; + m_hyperlink171 = new wxHyperlinkCtrl( m_panel571, wxID_ANY, _("Show examples"), wxT("https://freefilesync.org/manual.php?topic=exclude-files"), wxDefaultPosition, wxDefaultSize, wxHL_DEFAULT_STYLE ); + m_hyperlink171->SetToolTip( _("https://freefilesync.org/manual.php?topic=exclude-files") ); + + bSizer189->Add( m_hyperlink171, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT, 5 ); + + + bSizer1742->Add( bSizer189, 0, wxEXPAND, 5 ); + + m_textCtrlExclude = new wxTextCtrl( m_panel571, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize( -1, -1 ), wxTE_MULTILINE ); + bSizer1742->Add( m_textCtrlExclude, 1, wxEXPAND|wxTOP, 5 ); + + + bSizer1651->Add( bSizer1742, 1, wxEXPAND, 5 ); + + + bSizer166->Add( bSizer1651, 5, wxEXPAND|wxLEFT, 5 ); + + + bSizer301->Add( bSizer166, 1, wxEXPAND, 5 ); + + wxStaticLine* m_staticline24; + m_staticline24 = new wxStaticLine( m_panel571, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL ); + bSizer301->Add( m_staticline24, 0, wxEXPAND, 5 ); + + wxBoxSizer* bSizer160; + bSizer160 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer168; + bSizer168 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapFilterSize = new wxStaticBitmap( m_panel571, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizer168->Add( m_bitmapFilterSize, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL|wxALL, 5 ); + + wxBoxSizer* bSizer158; + bSizer158 = new wxBoxSizer( wxVERTICAL ); + + wxStaticText* m_staticText80; + m_staticText80 = new wxStaticText( m_panel571, wxID_ANY, _("File size:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText80->Wrap( -1 ); + bSizer158->Add( m_staticText80, 0, wxBOTTOM, 5 ); + + wxBoxSizer* bSizer162; + bSizer162 = new wxBoxSizer( wxVERTICAL ); + + wxStaticText* m_staticText101; + m_staticText101 = new wxStaticText( m_panel571, wxID_ANY, _("Minimum:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText101->Wrap( -1 ); + bSizer162->Add( m_staticText101, 0, wxBOTTOM, 2 ); + + m_spinCtrlMinSize = new wxSpinCtrl( m_panel571, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 0, 2000000000, 0 ); + bSizer162->Add( m_spinCtrlMinSize, 0, wxEXPAND, 5 ); + + wxArrayString m_choiceUnitMinSizeChoices; + m_choiceUnitMinSize = new wxChoice( m_panel571, wxID_ANY, wxDefaultPosition, wxDefaultSize, m_choiceUnitMinSizeChoices, 0 ); + m_choiceUnitMinSize->SetSelection( 0 ); + bSizer162->Add( m_choiceUnitMinSize, 0, wxEXPAND, 5 ); + + + bSizer158->Add( bSizer162, 0, wxEXPAND, 5 ); + + + bSizer158->Add( 0, 10, 0, 0, 5 ); + + wxBoxSizer* bSizer163; + bSizer163 = new wxBoxSizer( wxVERTICAL ); + + wxStaticText* m_staticText102; + m_staticText102 = new wxStaticText( m_panel571, wxID_ANY, _("Maximum:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText102->Wrap( -1 ); + bSizer163->Add( m_staticText102, 0, wxBOTTOM, 2 ); + + m_spinCtrlMaxSize = new wxSpinCtrl( m_panel571, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 0, 2000000000, 0 ); + bSizer163->Add( m_spinCtrlMaxSize, 0, wxEXPAND, 5 ); + + wxArrayString m_choiceUnitMaxSizeChoices; + m_choiceUnitMaxSize = new wxChoice( m_panel571, wxID_ANY, wxDefaultPosition, wxDefaultSize, m_choiceUnitMaxSizeChoices, 0 ); + m_choiceUnitMaxSize->SetSelection( 0 ); + bSizer163->Add( m_choiceUnitMaxSize, 0, wxEXPAND, 5 ); + + + bSizer158->Add( bSizer163, 0, wxEXPAND, 5 ); + + + bSizer168->Add( bSizer158, 1, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxRIGHT, 5 ); + + + bSizer160->Add( bSizer168, 2, wxEXPAND|wxALL, 5 ); + + wxStaticLine* m_staticline23; + m_staticline23 = new wxStaticLine( m_panel571, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer160->Add( m_staticline23, 0, wxEXPAND, 5 ); + + wxBoxSizer* bSizer167; + bSizer167 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapFilterDate = new wxStaticBitmap( m_panel571, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizer167->Add( m_bitmapFilterDate, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL|wxALL, 5 ); + + wxBoxSizer* bSizer165; + bSizer165 = new wxBoxSizer( wxVERTICAL ); + + wxStaticText* m_staticText79; + m_staticText79 = new wxStaticText( m_panel571, wxID_ANY, _("Time span:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText79->Wrap( -1 ); + bSizer165->Add( m_staticText79, 0, wxBOTTOM, 5 ); + + wxArrayString m_choiceUnitTimespanChoices; + m_choiceUnitTimespan = new wxChoice( m_panel571, wxID_ANY, wxDefaultPosition, wxDefaultSize, m_choiceUnitTimespanChoices, 0 ); + m_choiceUnitTimespan->SetSelection( 0 ); + bSizer165->Add( m_choiceUnitTimespan, 0, wxEXPAND, 5 ); + + m_spinCtrlTimespan = new wxSpinCtrl( m_panel571, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 0, 2000000000, 0 ); + bSizer165->Add( m_spinCtrlTimespan, 0, wxEXPAND, 5 ); + + + bSizer167->Add( bSizer165, 1, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxRIGHT, 5 ); + + + bSizer160->Add( bSizer167, 1, wxEXPAND|wxALL, 5 ); + + wxStaticLine* m_staticline231; + m_staticline231 = new wxStaticLine( m_panel571, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer160->Add( m_staticline231, 0, wxEXPAND, 5 ); + + + bSizer301->Add( bSizer160, 0, wxEXPAND, 5 ); + + + bSizer307->Add( bSizer301, 1, wxEXPAND, 5 ); + + wxBoxSizer* bSizer302; + bSizer302 = new wxBoxSizer( wxHORIZONTAL ); + + m_staticTextFilterDescr = new wxStaticText( m_panel571, wxID_ANY, _("Select filter rules to exclude certain files from synchronization.\nEnter file paths relative to their corresponding folder pair."), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_staticTextFilterDescr->Wrap( -1 ); + m_staticTextFilterDescr->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + bSizer302->Add( m_staticTextFilterDescr, 1, wxALL|wxALIGN_CENTER_VERTICAL, 10 ); + + wxBoxSizer* bSizer303; + bSizer303 = new wxBoxSizer( wxHORIZONTAL ); + + m_buttonDefault = new wxButton( m_panel571, wxID_ANY, _("&Default"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizer303->Add( m_buttonDefault, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + m_bpButtonDefaultContext = new wxBitmapButton( m_panel571, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + bSizer303->Add( m_bpButtonDefaultContext, 0, wxEXPAND, 5 ); + + + bSizer302->Add( bSizer303, 0, wxTOP|wxBOTTOM|wxLEFT|wxALIGN_CENTER_VERTICAL, 10 ); + + m_buttonClear = new wxButton( m_panel571, wxID_ANY, _("C&lear"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizer302->Add( m_buttonClear, 0, wxALL|wxALIGN_CENTER_VERTICAL, 10 ); + + + bSizer307->Add( bSizer302, 0, wxEXPAND, 5 ); + + + m_panel571->SetSizer( bSizer307 ); + m_panel571->Layout(); + bSizer307->Fit( m_panel571 ); + bSizer278->Add( m_panel571, 1, wxEXPAND, 5 ); + + + m_panelFilterSettingsTab->SetSizer( bSizer278 ); + m_panelFilterSettingsTab->Layout(); + bSizer278->Fit( m_panelFilterSettingsTab ); + m_notebook->AddPage( m_panelFilterSettingsTab, _("dummy"), false ); + m_panelSyncSettingsTab = new wxPanel( m_notebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panelSyncSettingsTab->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer276; + bSizer276 = new wxBoxSizer( wxVERTICAL ); + + bSizerHeaderSyncSettings = new wxBoxSizer( wxVERTICAL ); + + m_staticTextMainSyncSettings = new wxStaticText( m_panelSyncSettingsTab, wxID_ANY, _("Common settings:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextMainSyncSettings->Wrap( -1 ); + bSizerHeaderSyncSettings->Add( m_staticTextMainSyncSettings, 0, wxALL, 10 ); + + m_checkBoxUseLocalSyncOptions = new wxCheckBox( m_panelSyncSettingsTab, wxID_ANY, _("Use local settings:"), wxDefaultPosition, wxDefaultSize, 0 ); + bSizerHeaderSyncSettings->Add( m_checkBoxUseLocalSyncOptions, 0, wxALL|wxEXPAND, 10 ); + + m_staticlineSyncHeader = new wxStaticLine( m_panelSyncSettingsTab, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizerHeaderSyncSettings->Add( m_staticlineSyncHeader, 0, wxEXPAND, 5 ); + + + bSizer276->Add( bSizerHeaderSyncSettings, 0, wxEXPAND, 5 ); + + m_panelSyncSettings = new wxPanel( m_panelSyncSettingsTab, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panelSyncSettings->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer232; + bSizer232 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer237; + bSizer237 = new wxBoxSizer( wxHORIZONTAL ); + + wxBoxSizer* bSizer235; + bSizer235 = new wxBoxSizer( wxVERTICAL ); + + wxStaticText* m_staticText86; + m_staticText86 = new wxStaticText( m_panelSyncSettings, wxID_ANY, _("Select a variant:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText86->Wrap( -1 ); + bSizer235->Add( m_staticText86, 0, wxALL, 5 ); + + wxGridSizer* gSizer1; + gSizer1 = new wxGridSizer( 0, 1, 0, 0 ); + + m_buttonTwoWay = new zen::ToggleButton( m_panelSyncSettings, wxID_ANY, _("Two way"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_buttonTwoWay->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + m_buttonTwoWay->SetToolTip( _("dummy") ); + + gSizer1->Add( m_buttonTwoWay, 0, wxEXPAND, 5 ); + + m_buttonMirror = new zen::ToggleButton( m_panelSyncSettings, wxID_ANY, _("Mirror"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_buttonMirror->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + m_buttonMirror->SetToolTip( _("dummy") ); + + gSizer1->Add( m_buttonMirror, 0, wxEXPAND, 5 ); + + m_buttonUpdate = new zen::ToggleButton( m_panelSyncSettings, wxID_ANY, _("Update"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_buttonUpdate->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + m_buttonUpdate->SetToolTip( _("dummy") ); + + gSizer1->Add( m_buttonUpdate, 0, wxEXPAND, 5 ); + + m_buttonCustom = new zen::ToggleButton( m_panelSyncSettings, wxID_ANY, _("Custom"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_buttonCustom->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + m_buttonCustom->SetToolTip( _("dummy") ); + + gSizer1->Add( m_buttonCustom, 0, wxEXPAND, 5 ); + + + bSizer235->Add( gSizer1, 0, wxBOTTOM|wxRIGHT|wxLEFT, 5 ); + + + bSizer237->Add( bSizer235, 0, wxALL, 5 ); + + + bSizer237->Add( 10, 0, 0, 0, 5 ); + + wxBoxSizer* bSizer312; + bSizer312 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer311; + bSizer311 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapDatabase = new wxStaticBitmap( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_bitmapDatabase->SetToolTip( _("sync.ffs_db") ); + + bSizer311->Add( m_bitmapDatabase, 0, wxRIGHT|wxALIGN_CENTER_VERTICAL, 5 ); + + m_checkBoxUseDatabase = new wxCheckBox( m_panelSyncSettings, wxID_ANY, _("Use database file to detect changes"), wxDefaultPosition, wxDefaultSize, 0 ); + bSizer311->Add( m_checkBoxUseDatabase, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer312->Add( bSizer311, 0, wxTOP|wxRIGHT|wxLEFT, 10 ); + + + bSizer312->Add( 0, 0, 1, wxEXPAND, 5 ); + + m_staticTextSyncVarDescription = new wxStaticText( m_panelSyncSettings, wxID_ANY, _("dummy"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_staticTextSyncVarDescription->Wrap( -1 ); + m_staticTextSyncVarDescription->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + bSizer312->Add( m_staticTextSyncVarDescription, 0, wxALL, 10 ); + + + bSizer312->Add( 0, 0, 1, wxEXPAND, 5 ); + + wxBoxSizer* bSizer310; + bSizer310 = new wxBoxSizer( wxVERTICAL ); + + wxStaticLine* m_staticline431; + m_staticline431 = new wxStaticLine( m_panelSyncSettings, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer310->Add( m_staticline431, 0, wxEXPAND, 5 ); + + wxBoxSizer* bSizer201; + bSizer201 = new wxBoxSizer( wxHORIZONTAL ); + + wxStaticLine* m_staticline72; + m_staticline72 = new wxStaticLine( m_panelSyncSettings, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL ); + bSizer201->Add( m_staticline72, 0, wxEXPAND, 5 ); + + wxBoxSizer* bSizer3121; + bSizer3121 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapMoveLeft = new wxStaticBitmap( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_bitmapMoveLeft->SetToolTip( _("- Available not before the *second* synchronization\n- Not supported by all file systems") ); + + bSizer3121->Add( m_bitmapMoveLeft, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + m_bitmapMoveRight = new wxStaticBitmap( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_bitmapMoveRight->SetToolTip( _("- Available not before the *second* synchronization\n- Not supported by all file systems") ); + + bSizer3121->Add( m_bitmapMoveRight, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 ); + + m_staticTextDetectMove = new wxStaticText( m_panelSyncSettings, wxID_ANY, _("Detect moved files"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextDetectMove->Wrap( -1 ); + m_staticTextDetectMove->SetToolTip( _("- Available not before the *second* synchronization\n- Not supported by all file systems") ); + + bSizer3121->Add( m_staticTextDetectMove, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer201->Add( bSizer3121, 0, wxALL, 10 ); + + wxHyperlinkCtrl* m_hyperlink242; + m_hyperlink242 = new wxHyperlinkCtrl( m_panelSyncSettings, wxID_ANY, _("More information"), wxT("https://freefilesync.org/manual.php?topic=synchronization-settings"), wxDefaultPosition, wxDefaultSize, wxHL_DEFAULT_STYLE ); + m_hyperlink242->SetToolTip( _("https://freefilesync.org/manual.php?topic=synchronization-settings") ); + + bSizer201->Add( m_hyperlink242, 0, wxTOP|wxBOTTOM|wxRIGHT|wxALIGN_CENTER_VERTICAL, 10 ); + + wxStaticLine* m_staticline721; + m_staticline721 = new wxStaticLine( m_panelSyncSettings, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL ); + bSizer201->Add( m_staticline721, 0, wxEXPAND, 5 ); + + + bSizer310->Add( bSizer201, 0, 0, 5 ); + + + bSizer312->Add( bSizer310, 0, 0, 5 ); + + + bSizer237->Add( bSizer312, 0, wxEXPAND, 5 ); + + bSizerSyncDirHolder = new wxBoxSizer( wxHORIZONTAL ); + + bSizerSyncDirsDiff = new wxBoxSizer( wxVERTICAL ); + + wxStaticText* m_staticText184; + m_staticText184 = new wxStaticText( m_panelSyncSettings, wxID_ANY, _("Difference"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText184->Wrap( -1 ); + bSizerSyncDirsDiff->Add( m_staticText184, 0, wxALIGN_CENTER_HORIZONTAL, 5 ); + + wxFlexGridSizer* ffgSizer11; + ffgSizer11 = new wxFlexGridSizer( 2, 0, 5, 5 ); + ffgSizer11->SetFlexibleDirection( wxBOTH ); + ffgSizer11->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); + + m_bitmapLeftOnly = new wxStaticBitmap( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_bitmapLeftOnly->SetToolTip( _("Item exists on left side only") ); + + ffgSizer11->Add( m_bitmapLeftOnly, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + m_bitmapLeftNewer = new wxStaticBitmap( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_bitmapLeftNewer->SetToolTip( _("Left side is newer") ); + + ffgSizer11->Add( m_bitmapLeftNewer, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + m_bitmapDifferent = new wxStaticBitmap( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_bitmapDifferent->SetToolTip( _("Items have different content") ); + + ffgSizer11->Add( m_bitmapDifferent, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + m_bitmapRightNewer = new wxStaticBitmap( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_bitmapRightNewer->SetToolTip( _("Right side is newer") ); + + ffgSizer11->Add( m_bitmapRightNewer, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + m_bitmapRightOnly = new wxStaticBitmap( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_bitmapRightOnly->SetToolTip( _("Item exists on right side only") ); + + ffgSizer11->Add( m_bitmapRightOnly, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + m_bpButtonLeftOnly = new wxBitmapButton( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + ffgSizer11->Add( m_bpButtonLeftOnly, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + m_bpButtonLeftNewer = new wxBitmapButton( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + ffgSizer11->Add( m_bpButtonLeftNewer, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + m_bpButtonDifferent = new wxBitmapButton( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + ffgSizer11->Add( m_bpButtonDifferent, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + m_bpButtonRightNewer = new wxBitmapButton( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + ffgSizer11->Add( m_bpButtonRightNewer, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + m_bpButtonRightOnly = new wxBitmapButton( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + ffgSizer11->Add( m_bpButtonRightOnly, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizerSyncDirsDiff->Add( ffgSizer11, 0, 0, 5 ); + + wxStaticText* m_staticText120; + m_staticText120 = new wxStaticText( m_panelSyncSettings, wxID_ANY, _("Action"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText120->Wrap( -1 ); + bSizerSyncDirsDiff->Add( m_staticText120, 0, wxALIGN_CENTER_HORIZONTAL|wxTOP, 5 ); + + + bSizerSyncDirHolder->Add( bSizerSyncDirsDiff, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + bSizerSyncDirsChanges = new wxBoxSizer( wxVERTICAL ); + + wxFlexGridSizer* ffgSizer111; + ffgSizer111 = new wxFlexGridSizer( 0, 3, 5, 5 ); + ffgSizer111->SetFlexibleDirection( wxBOTH ); + ffgSizer111->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); + + wxStaticText* m_staticText12011; + m_staticText12011 = new wxStaticText( m_panelSyncSettings, wxID_ANY, _("Create:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText12011->Wrap( -1 ); + ffgSizer111->Add( m_staticText12011, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT, 5 ); + + m_bpButtonLeftCreate = new wxBitmapButton( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + ffgSizer111->Add( m_bpButtonLeftCreate, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + m_bpButtonRightCreate = new wxBitmapButton( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + ffgSizer111->Add( m_bpButtonRightCreate, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + wxStaticText* m_staticText12012; + m_staticText12012 = new wxStaticText( m_panelSyncSettings, wxID_ANY, _("Update:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText12012->Wrap( -1 ); + ffgSizer111->Add( m_staticText12012, 0, wxALIGN_RIGHT|wxALIGN_CENTER_VERTICAL, 5 ); + + m_bpButtonLeftUpdate = new wxBitmapButton( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + ffgSizer111->Add( m_bpButtonLeftUpdate, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + m_bpButtonRightUpdate = new wxBitmapButton( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + ffgSizer111->Add( m_bpButtonRightUpdate, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + wxStaticText* m_staticText12013; + m_staticText12013 = new wxStaticText( m_panelSyncSettings, wxID_ANY, _("Delete:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText12013->Wrap( -1 ); + ffgSizer111->Add( m_staticText12013, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT, 5 ); + + m_bpButtonLeftDelete = new wxBitmapButton( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + ffgSizer111->Add( m_bpButtonLeftDelete, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + m_bpButtonRightDelete = new wxBitmapButton( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + ffgSizer111->Add( m_bpButtonRightDelete, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + + ffgSizer111->Add( 0, 0, 0, 0, 5 ); + + wxStaticText* m_staticText1201; + m_staticText1201 = new wxStaticText( m_panelSyncSettings, wxID_ANY, _("Left"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText1201->Wrap( -1 ); + ffgSizer111->Add( m_staticText1201, 0, wxALIGN_CENTER_HORIZONTAL, 5 ); + + wxStaticText* m_staticText1202; + m_staticText1202 = new wxStaticText( m_panelSyncSettings, wxID_ANY, _("Right"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText1202->Wrap( -1 ); + ffgSizer111->Add( m_staticText1202, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizerSyncDirsChanges->Add( ffgSizer111, 0, 0, 5 ); + + + bSizerSyncDirHolder->Add( bSizerSyncDirsChanges, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer237->Add( bSizerSyncDirHolder, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM, 5 ); + + + bSizer237->Add( 0, 0, 1, 0, 5 ); + + + bSizer232->Add( bSizer237, 0, wxEXPAND, 5 ); + + wxStaticLine* m_staticline54; + m_staticline54 = new wxStaticLine( m_panelSyncSettings, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer232->Add( m_staticline54, 0, wxEXPAND, 5 ); + + wxBoxSizer* bSizer2361; + bSizer2361 = new wxBoxSizer( wxHORIZONTAL ); + + wxBoxSizer* bSizer202; + bSizer202 = new wxBoxSizer( wxVERTICAL ); + + wxStaticText* m_staticText87; + m_staticText87 = new wxStaticText( m_panelSyncSettings, wxID_ANY, _("Delete and overwrite:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText87->Wrap( -1 ); + bSizer202->Add( m_staticText87, 0, wxALL, 5 ); + + wxBoxSizer* bSizer234; + bSizer234 = new wxBoxSizer( wxVERTICAL ); + + m_buttonRecycler = new zen::ToggleButton( m_panelSyncSettings, wxID_ANY, _("&Recycle bin"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_buttonRecycler->SetToolTip( _("dummy") ); + + bSizer234->Add( m_buttonRecycler, 0, wxEXPAND, 5 ); + + m_buttonPermanent = new zen::ToggleButton( m_panelSyncSettings, wxID_ANY, _("&Permanent"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_buttonPermanent->SetToolTip( _("dummy") ); + + bSizer234->Add( m_buttonPermanent, 0, wxEXPAND, 5 ); + + m_buttonVersioning = new zen::ToggleButton( m_panelSyncSettings, wxID_ANY, _("&Versioning"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_buttonVersioning->SetToolTip( _("dummy") ); + + bSizer234->Add( m_buttonVersioning, 0, wxEXPAND, 5 ); + + + bSizer202->Add( bSizer234, 0, wxBOTTOM|wxRIGHT|wxLEFT, 5 ); + + + bSizer2361->Add( bSizer202, 0, wxALL, 5 ); + + bSizerVersioningHolder = new wxBoxSizer( wxVERTICAL ); + + + bSizerVersioningHolder->Add( 0, 0, 1, wxEXPAND, 5 ); + + wxBoxSizer* bSizer2331; + bSizer2331 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapDeletionType = new wxStaticBitmap( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer2331->Add( m_bitmapDeletionType, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 ); + + m_staticTextDeletionTypeDescription = new wxStaticText( m_panelSyncSettings, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextDeletionTypeDescription->Wrap( -1 ); + m_staticTextDeletionTypeDescription->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + bSizer2331->Add( m_staticTextDeletionTypeDescription, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 ); + + + bSizerVersioningHolder->Add( bSizer2331, 0, wxALL|wxEXPAND, 5 ); + + m_panelVersioning = new wxPanel( m_panelSyncSettings, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panelVersioning->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer191; + bSizer191 = new wxBoxSizer( wxVERTICAL ); + + + bSizer191->Add( 0, 5, 0, 0, 5 ); + + wxBoxSizer* bSizer252; + bSizer252 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapVersioning = new wxStaticBitmap( m_panelVersioning, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer252->Add( m_bitmapVersioning, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxRIGHT, 5 ); + + wxBoxSizer* bSizer253; + bSizer253 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer254; + bSizer254 = new wxBoxSizer( wxHORIZONTAL ); + + wxStaticText* m_staticText155; + m_staticText155 = new wxStaticText( m_panelVersioning, wxID_ANY, _("Move files to a user-defined folder"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText155->Wrap( -1 ); + m_staticText155->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + bSizer254->Add( m_staticText155, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 ); + + + bSizer254->Add( 0, 0, 1, wxEXPAND, 5 ); + + wxHyperlinkCtrl* m_hyperlink243; + m_hyperlink243 = new wxHyperlinkCtrl( m_panelVersioning, wxID_ANY, _("Show examples"), wxT("https://freefilesync.org/manual.php?topic=versioning"), wxDefaultPosition, wxDefaultSize, wxHL_DEFAULT_STYLE ); + m_hyperlink243->SetToolTip( _("https://freefilesync.org/manual.php?topic=versioning") ); + + bSizer254->Add( m_hyperlink243, 0, wxLEFT|wxALIGN_BOTTOM, 5 ); + + + bSizer253->Add( bSizer254, 0, wxEXPAND|wxBOTTOM, 5 ); + + wxBoxSizer* bSizer156; + bSizer156 = new wxBoxSizer( wxHORIZONTAL ); + + m_versioningFolderPath = new fff::FolderHistoryBox( m_panelVersioning, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0, NULL, 0 ); + bSizer156->Add( m_versioningFolderPath, 1, wxALIGN_CENTER_VERTICAL, 5 ); + + m_buttonSelectVersioningFolder = new wxButton( m_panelVersioning, wxID_ANY, _("Browse"), wxDefaultPosition, wxDefaultSize, 0 ); + m_buttonSelectVersioningFolder->SetToolTip( _("Select a folder") ); + + bSizer156->Add( m_buttonSelectVersioningFolder, 0, wxEXPAND, 5 ); + + m_bpButtonSelectVersioningAltFolder = new wxBitmapButton( m_panelVersioning, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + m_bpButtonSelectVersioningAltFolder->SetToolTip( _("Access online storage") ); + + bSizer156->Add( m_bpButtonSelectVersioningAltFolder, 0, wxEXPAND, 5 ); + + + bSizer253->Add( bSizer156, 0, wxEXPAND, 5 ); + + + bSizer252->Add( bSizer253, 1, wxRIGHT, 5 ); + + + bSizer191->Add( bSizer252, 0, wxEXPAND|wxTOP|wxRIGHT|wxLEFT, 5 ); + + wxBoxSizer* bSizer198; + bSizer198 = new wxBoxSizer( wxHORIZONTAL ); + + wxBoxSizer* bSizer255; + bSizer255 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer256; + bSizer256 = new wxBoxSizer( wxHORIZONTAL ); + + wxStaticText* m_staticText93; + m_staticText93 = new wxStaticText( m_panelVersioning, wxID_ANY, _("Naming convention:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText93->Wrap( -1 ); + bSizer256->Add( m_staticText93, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 ); + + wxArrayString m_choiceVersioningStyleChoices; + m_choiceVersioningStyle = new wxChoice( m_panelVersioning, wxID_ANY, wxDefaultPosition, wxDefaultSize, m_choiceVersioningStyleChoices, 0 ); + m_choiceVersioningStyle->SetSelection( 0 ); + bSizer256->Add( m_choiceVersioningStyle, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer255->Add( bSizer256, 0, wxALL, 5 ); + + wxBoxSizer* bSizer257; + bSizer257 = new wxBoxSizer( wxHORIZONTAL ); + + m_staticTextNamingCvtPart1 = new wxStaticText( m_panelVersioning, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextNamingCvtPart1->Wrap( -1 ); + m_staticTextNamingCvtPart1->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + bSizer257->Add( m_staticTextNamingCvtPart1, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + m_staticTextNamingCvtPart2Bold = new wxStaticText( m_panelVersioning, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextNamingCvtPart2Bold->Wrap( -1 ); + m_staticTextNamingCvtPart2Bold->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + m_staticTextNamingCvtPart2Bold->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + bSizer257->Add( m_staticTextNamingCvtPart2Bold, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + m_staticTextNamingCvtPart3 = new wxStaticText( m_panelVersioning, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextNamingCvtPart3->Wrap( -1 ); + m_staticTextNamingCvtPart3->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + bSizer257->Add( m_staticTextNamingCvtPart3, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer255->Add( bSizer257, 0, wxBOTTOM|wxRIGHT|wxLEFT, 5 ); + + + bSizer198->Add( bSizer255, 0, wxALL, 5 ); + + wxStaticLine* m_staticline69; + m_staticline69 = new wxStaticLine( m_panelVersioning, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL ); + bSizer198->Add( m_staticline69, 0, wxEXPAND, 5 ); + + wxBoxSizer* bSizer258; + bSizer258 = new wxBoxSizer( wxVERTICAL ); + + m_staticTextLimitVersions = new wxStaticText( m_panelVersioning, wxID_ANY, _("Limit file versions:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextLimitVersions->Wrap( -1 ); + bSizer258->Add( m_staticTextLimitVersions, 0, wxTOP|wxRIGHT|wxLEFT, 5 ); + + wxFlexGridSizer* fgSizer15; + fgSizer15 = new wxFlexGridSizer( 0, 3, 5, 10 ); + fgSizer15->SetFlexibleDirection( wxBOTH ); + fgSizer15->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); + + m_checkBoxVersionMaxDays = new wxCheckBox( m_panelVersioning, wxID_ANY, _("Last x days:"), wxDefaultPosition, wxDefaultSize, 0 ); + fgSizer15->Add( m_checkBoxVersionMaxDays, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + m_checkBoxVersionCountMin = new wxCheckBox( m_panelVersioning, wxID_ANY, _("Minimum:"), wxDefaultPosition, wxDefaultSize, 0 ); + fgSizer15->Add( m_checkBoxVersionCountMin, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + m_checkBoxVersionCountMax = new wxCheckBox( m_panelVersioning, wxID_ANY, _("Maximum:"), wxDefaultPosition, wxDefaultSize, 0 ); + fgSizer15->Add( m_checkBoxVersionCountMax, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + m_spinCtrlVersionMaxDays = new wxSpinCtrl( m_panelVersioning, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize( -1, -1 ), wxSP_ARROW_KEYS, 1, 2000000000, 1 ); + fgSizer15->Add( m_spinCtrlVersionMaxDays, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + m_spinCtrlVersionCountMin = new wxSpinCtrl( m_panelVersioning, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize( -1, -1 ), wxSP_ARROW_KEYS, 1, 2000000000, 1 ); + fgSizer15->Add( m_spinCtrlVersionCountMin, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + m_spinCtrlVersionCountMax = new wxSpinCtrl( m_panelVersioning, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 1, 2000000000, 1 ); + fgSizer15->Add( m_spinCtrlVersionCountMax, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer258->Add( fgSizer15, 0, wxALL, 5 ); + + + bSizer198->Add( bSizer258, 0, wxALL, 5 ); + + + bSizer191->Add( bSizer198, 0, wxEXPAND, 5 ); + + + m_panelVersioning->SetSizer( bSizer191 ); + m_panelVersioning->Layout(); + bSizer191->Fit( m_panelVersioning ); + bSizerVersioningHolder->Add( m_panelVersioning, 0, wxEXPAND, 5 ); + + + bSizerVersioningHolder->Add( 0, 0, 1, wxEXPAND, 5 ); + + + bSizer2361->Add( bSizerVersioningHolder, 1, wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer232->Add( bSizer2361, 0, wxEXPAND, 5 ); + + wxStaticLine* m_staticline582; + m_staticline582 = new wxStaticLine( m_panelSyncSettings, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer232->Add( m_staticline582, 0, wxEXPAND, 5 ); + + 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, 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|wxLEFT, 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, wxRIGHT, 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, 0, 5 ); + + m_bpButtonEmailErrorWarning = new wxBitmapButton( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, wxBU_AUTODRAW|0 ); + bSizer289->Add( m_bpButtonEmailErrorWarning, 0, 0, 5 ); + + m_bpButtonEmailErrorOnly = new wxBitmapButton( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, wxBU_AUTODRAW|0 ); + bSizer289->Add( m_bpButtonEmailErrorOnly, 0, 0, 5 ); + + + bSizer287->Add( bSizer289, 0, wxLEFT, 5 ); + + + bSizer292->Add( bSizer287, 0, wxEXPAND, 5 ); + + m_hyperlinkPerfDeRequired2 = new wxHyperlinkCtrl( m_panelSyncSettings, wxID_ANY, _("Requires FreeFileSync Donation Edition"), wxT("https://freefilesync.org/faq.php#donation-edition"), wxDefaultPosition, wxDefaultSize, wxHL_DEFAULT_STYLE ); + m_hyperlinkPerfDeRequired2->SetToolTip( _("https://freefilesync.org/faq.php#donation-edition") ); + + bSizer292->Add( m_hyperlinkPerfDeRequired2, 0, wxALL, 5 ); + + + bSizerSyncMisc->Add( bSizer292, 0, wxEXPAND|wxALL, 10 ); + + wxStaticLine* m_staticline57; + 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 ); + + m_panelLogfile = new wxPanel( m_panelSyncSettings, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panelLogfile->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer1912; + bSizer1912 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer279; + bSizer279 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapLogFile = new wxStaticBitmap( m_panelLogfile, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer279->Add( m_bitmapLogFile, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + m_checkBoxOverrideLogPath = new wxCheckBox( m_panelLogfile, wxID_ANY, _("&Change log folder:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_checkBoxOverrideLogPath->SetValue(true); + bSizer279->Add( m_checkBoxOverrideLogPath, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT, 5 ); + + + bSizer279->Add( 0, 0, 1, 0, 5 ); + + m_bpButtonShowLogFolder = new wxBitmapButton( m_panelLogfile, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + m_bpButtonShowLogFolder->SetToolTip( _("dummy") ); + + bSizer279->Add( m_bpButtonShowLogFolder, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer1912->Add( bSizer279, 0, wxEXPAND, 5 ); + + wxBoxSizer* bSizer297; + bSizer297 = new wxBoxSizer( wxHORIZONTAL ); + + m_logFolderPath = new fff::FolderHistoryBox( m_panelLogfile, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0, NULL, 0 ); + bSizer297->Add( m_logFolderPath, 1, wxALIGN_CENTER_VERTICAL, 5 ); + + m_buttonSelectLogFolder = new wxButton( m_panelLogfile, wxID_ANY, _("Browse"), wxDefaultPosition, wxDefaultSize, 0 ); + m_buttonSelectLogFolder->SetToolTip( _("Select a folder") ); + + bSizer297->Add( m_buttonSelectLogFolder, 0, wxEXPAND, 5 ); + + m_bpButtonSelectAltLogFolder = new wxBitmapButton( m_panelLogfile, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + m_bpButtonSelectAltLogFolder->SetToolTip( _("Access online storage") ); + + bSizer297->Add( m_bpButtonSelectAltLogFolder, 0, wxEXPAND, 5 ); + + + bSizer1912->Add( bSizer297, 0, wxEXPAND|wxTOP, 5 ); + + + m_panelLogfile->SetSizer( bSizer1912 ); + m_panelLogfile->Layout(); + bSizer1912->Fit( m_panelLogfile ); + bSizer2372->Add( m_panelLogfile, 1, 0, 5 ); + + + bSizer293->Add( bSizer2372, 0, wxALL|wxEXPAND, 10 ); + + wxStaticLine* m_staticline80; + m_staticline80 = new wxStaticLine( m_panelSyncSettings, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer293->Add( m_staticline80, 0, wxEXPAND, 5 ); + + wxBoxSizer* bSizer247; + bSizer247 = new wxBoxSizer( wxHORIZONTAL ); + + m_staticTextPostSync = new wxStaticText( m_panelSyncSettings, wxID_ANY, _("Run a command:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextPostSync->Wrap( -1 ); + 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 ); + 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 ); + + + bSizer293->Add( bSizer247, 0, wxALL|wxEXPAND, 10 ); + + + bSizerSyncMisc->Add( bSizer293, 1, 0, 5 ); + + + bSizer232->Add( bSizerSyncMisc, 1, wxEXPAND, 5 ); + + + m_panelSyncSettings->SetSizer( bSizer232 ); + m_panelSyncSettings->Layout(); + bSizer232->Fit( m_panelSyncSettings ); + bSizer276->Add( m_panelSyncSettings, 1, wxEXPAND, 5 ); + + + m_panelSyncSettingsTab->SetSizer( bSizer276 ); + m_panelSyncSettingsTab->Layout(); + bSizer276->Fit( m_panelSyncSettingsTab ); + m_notebook->AddPage( m_panelSyncSettingsTab, _("dummy"), true ); + + bSizer190->Add( m_notebook, 1, wxEXPAND, 5 ); + + + bSizer7->Add( bSizer190, 1, wxEXPAND, 5 ); + + m_panelNotes = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panelNotes->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer3021; + bSizer3021 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer17311; + bSizer17311 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapNotes = new wxStaticBitmap( m_panelNotes, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizer17311->Add( m_bitmapNotes, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 10 ); + + wxStaticText* m_staticText781; + m_staticText781 = new wxStaticText( m_panelNotes, wxID_ANY, _("Notes:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText781->Wrap( -1 ); + bSizer17311->Add( m_staticText781, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 ); + + m_textCtrNotes = new wxTextCtrl( m_panelNotes, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize( -1, -1 ), wxTE_MULTILINE ); + bSizer17311->Add( m_textCtrNotes, 1, wxEXPAND, 5 ); + + + bSizer3021->Add( bSizer17311, 1, wxEXPAND, 5 ); + + wxStaticLine* m_staticline83; + m_staticline83 = new wxStaticLine( m_panelNotes, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer3021->Add( m_staticline83, 0, wxEXPAND, 5 ); + + + m_panelNotes->SetSizer( bSizer3021 ); + m_panelNotes->Layout(); + bSizer3021->Fit( m_panelNotes ); + bSizer7->Add( m_panelNotes, 0, wxEXPAND, 5 ); + + bSizerStdButtons = new wxBoxSizer( wxHORIZONTAL ); + + m_buttonAddNotes = new zen::BitmapTextButton( this, wxID_ANY, _("Add ¬es"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizerStdButtons->Add( m_buttonAddNotes, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizerStdButtons->Add( 0, 0, 1, wxEXPAND, 5 ); + + m_buttonOK = new wxButton( this, wxID_OK, _("OK"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + + m_buttonOK->SetDefault(); + m_buttonOK->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + + bSizerStdButtons->Add( m_buttonOK, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 ); + + m_buttonCancel = new wxButton( this, wxID_CANCEL, _("Cancel"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizerStdButtons->Add( m_buttonCancel, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxRIGHT, 5 ); + + + bSizer7->Add( bSizerStdButtons, 0, wxEXPAND, 5 ); + + + this->SetSizer( bSizer7 ); + this->Layout(); + bSizer7->Fit( this ); + + // Connect Events + this->Connect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( ConfigDlgGenerated::onClose ) ); + m_listBoxFolderPair->Connect( wxEVT_KEY_DOWN, wxKeyEventHandler( ConfigDlgGenerated::onListBoxKeyEvent ), NULL, this ); + m_listBoxFolderPair->Connect( wxEVT_COMMAND_LISTBOX_SELECTED, wxCommandEventHandler( ConfigDlgGenerated::onSelectFolderPair ), NULL, this ); + m_checkBoxUseLocalCmpOptions->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onToggleLocalCompSettings ), NULL, this ); + m_buttonByTimeSize->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onCompByTimeSize ), NULL, this ); + m_buttonByTimeSize->Connect( wxEVT_LEFT_DCLICK, wxMouseEventHandler( ConfigDlgGenerated::onCompByTimeSizeDouble ), NULL, this ); + m_buttonByContent->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onCompByContent ), NULL, this ); + m_buttonByContent->Connect( wxEVT_LEFT_DCLICK, wxMouseEventHandler( ConfigDlgGenerated::onCompByContentDouble ), NULL, this ); + m_buttonBySize->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onCompBySize ), NULL, this ); + m_buttonBySize->Connect( wxEVT_LEFT_DCLICK, wxMouseEventHandler( ConfigDlgGenerated::onCompBySizeDouble ), NULL, this ); + m_checkBoxSymlinksInclude->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onChangeCompOption ), NULL, this ); + m_checkBoxIgnoreErrors->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onToggleIgnoreErrors ), NULL, this ); + m_checkBoxAutoRetry->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onToggleAutoRetry ), NULL, this ); + m_textCtrlInclude->Connect( wxEVT_COMMAND_TEXT_UPDATED, wxCommandEventHandler( ConfigDlgGenerated::onChangeFilterOption ), NULL, this ); + m_textCtrlExclude->Connect( wxEVT_COMMAND_TEXT_UPDATED, wxCommandEventHandler( ConfigDlgGenerated::onChangeFilterOption ), NULL, this ); + m_choiceUnitMinSize->Connect( wxEVT_COMMAND_CHOICE_SELECTED, wxCommandEventHandler( ConfigDlgGenerated::onChangeFilterOption ), NULL, this ); + m_choiceUnitMaxSize->Connect( wxEVT_COMMAND_CHOICE_SELECTED, wxCommandEventHandler( ConfigDlgGenerated::onChangeFilterOption ), NULL, this ); + m_choiceUnitTimespan->Connect( wxEVT_COMMAND_CHOICE_SELECTED, wxCommandEventHandler( ConfigDlgGenerated::onChangeFilterOption ), NULL, this ); + m_buttonDefault->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onFilterDefault ), NULL, this ); + m_buttonDefault->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( ConfigDlgGenerated::onFilterDefaultContextMouse ), NULL, this ); + m_bpButtonDefaultContext->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onFilterDefaultContext ), NULL, this ); + m_bpButtonDefaultContext->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( ConfigDlgGenerated::onFilterDefaultContextMouse ), NULL, this ); + m_buttonClear->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onFilterClear ), NULL, this ); + m_checkBoxUseLocalSyncOptions->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onToggleLocalSyncSettings ), NULL, this ); + m_buttonTwoWay->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onSyncTwoWay ), NULL, this ); + m_buttonTwoWay->Connect( wxEVT_LEFT_DCLICK, wxMouseEventHandler( ConfigDlgGenerated::onSyncTwoWayDouble ), NULL, this ); + m_buttonMirror->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onSyncMirror ), NULL, this ); + m_buttonMirror->Connect( wxEVT_LEFT_DCLICK, wxMouseEventHandler( ConfigDlgGenerated::onSyncMirrorDouble ), NULL, this ); + m_buttonUpdate->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onSyncUpdate ), NULL, this ); + m_buttonUpdate->Connect( wxEVT_LEFT_DCLICK, wxMouseEventHandler( ConfigDlgGenerated::onSyncUpdateDouble ), NULL, this ); + m_buttonCustom->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onSyncCustom ), NULL, this ); + m_buttonCustom->Connect( wxEVT_LEFT_DCLICK, wxMouseEventHandler( ConfigDlgGenerated::onSyncCustomDouble ), NULL, this ); + m_checkBoxUseDatabase->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onToggleUseDatabase ), NULL, this ); + m_bpButtonLeftOnly->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onLeftOnly ), NULL, this ); + m_bpButtonLeftNewer->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onLeftNewer ), NULL, this ); + m_bpButtonDifferent->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onDifferent ), NULL, this ); + m_bpButtonRightNewer->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onRightNewer ), NULL, this ); + m_bpButtonRightOnly->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onRightOnly ), NULL, this ); + m_bpButtonLeftCreate->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onLeftCreate ), NULL, this ); + m_bpButtonRightCreate->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onRightCreate ), NULL, this ); + m_bpButtonLeftUpdate->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onLeftUpdate ), NULL, this ); + m_bpButtonRightUpdate->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onRightUpdate ), NULL, this ); + m_bpButtonLeftDelete->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onLeftDelete ), NULL, this ); + m_bpButtonRightDelete->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onRightDelete ), NULL, this ); + m_buttonRecycler->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onDeletionRecycler ), NULL, this ); + m_buttonPermanent->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onDeletionPermanent ), NULL, this ); + m_buttonVersioning->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onDeletionVersioning ), NULL, this ); + m_choiceVersioningStyle->Connect( wxEVT_COMMAND_CHOICE_SELECTED, wxCommandEventHandler( ConfigDlgGenerated::onChangeVersioningStyle ), NULL, this ); + 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_checkBoxSendEmail->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onToggleMiscEmail ), 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_bpButtonShowLogFolder->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onShowLogFolder ), NULL, this ); + m_buttonAddNotes->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onAddNotes ), NULL, this ); + m_buttonOK->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onOkay ), NULL, this ); + m_buttonCancel->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onCancel ), NULL, this ); +} + +ConfigDlgGenerated::~ConfigDlgGenerated() +{ +} + +CloudSetupDlgGenerated::CloudSetupDlgGenerated( wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style ) : wxDialog( parent, id, title, pos, size, style ) +{ + this->SetSizeHints( wxDefaultSize, wxDefaultSize ); + this->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) ); + + wxBoxSizer* bSizer134; + bSizer134 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer72; + bSizer72 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapCloud = new wxStaticBitmap( this, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer72->Add( m_bitmapCloud, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 10 ); + + wxBoxSizer* bSizer272; + bSizer272 = new wxBoxSizer( wxVERTICAL ); + + wxStaticText* m_staticText136; + m_staticText136 = new wxStaticText( this, wxID_ANY, _("Connection type:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText136->Wrap( -1 ); + bSizer272->Add( m_staticText136, 0, wxTOP|wxRIGHT|wxLEFT, 5 ); + + wxBoxSizer* bSizer231; + bSizer231 = new wxBoxSizer( wxHORIZONTAL ); + + m_toggleBtnGdrive = new wxToggleButton( this, wxID_ANY, _("Google Drive"), wxDefaultPosition, wxDefaultSize, 0 ); + m_toggleBtnGdrive->SetValue( true ); + m_toggleBtnGdrive->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + + bSizer231->Add( m_toggleBtnGdrive, 0, wxTOP|wxBOTTOM|wxLEFT|wxEXPAND, 5 ); + + m_toggleBtnSftp = new wxToggleButton( this, wxID_ANY, _("SFTP"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_toggleBtnSftp->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + + bSizer231->Add( m_toggleBtnSftp, 0, wxTOP|wxBOTTOM|wxLEFT|wxEXPAND, 5 ); + + m_toggleBtnFtp = new wxToggleButton( this, wxID_ANY, _("FTP"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_toggleBtnFtp->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + + bSizer231->Add( m_toggleBtnFtp, 0, wxALL|wxEXPAND, 5 ); + + + bSizer272->Add( bSizer231, 0, 0, 5 ); + + + bSizer72->Add( bSizer272, 0, wxALL, 5 ); + + + bSizer134->Add( bSizer72, 0, wxEXPAND, 5 ); + + wxStaticLine* m_staticline371; + m_staticline371 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxSize( -1, -1 ), wxLI_HORIZONTAL ); + bSizer134->Add( m_staticline371, 0, wxEXPAND, 5 ); + + wxPanel* m_panel41; + m_panel41 = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panel41->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer185; + bSizer185 = new wxBoxSizer( wxVERTICAL ); + + bSizerGdrive = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer284; + bSizer284 = new wxBoxSizer( wxHORIZONTAL ); + + wxBoxSizer* bSizer307; + bSizer307 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer306; + bSizer306 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapGdriveUser = new wxStaticBitmap( m_panel41, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer306->Add( m_bitmapGdriveUser, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 ); + + wxStaticText* m_staticText166; + m_staticText166 = new wxStaticText( m_panel41, wxID_ANY, _("Connected user accounts:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText166->Wrap( -1 ); + bSizer306->Add( m_staticText166, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer307->Add( bSizer306, 0, wxALL|wxALIGN_CENTER_HORIZONTAL, 5 ); + + m_listBoxGdriveUsers = new wxListBox( m_panel41, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0, NULL, wxLB_NEEDED_SB|wxLB_SINGLE|wxLB_SORT ); + bSizer307->Add( m_listBoxGdriveUsers, 1, wxBOTTOM|wxRIGHT|wxLEFT|wxEXPAND, 5 ); + + wxBoxSizer* bSizer3002; + bSizer3002 = new wxBoxSizer( wxHORIZONTAL ); + + m_buttonGdriveAddUser = new zen::BitmapTextButton( m_panel41, wxID_ANY, _("&Add connection"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizer3002->Add( m_buttonGdriveAddUser, 1, wxALIGN_CENTER_VERTICAL, 5 ); + + m_buttonGdriveRemoveUser = new zen::BitmapTextButton( m_panel41, wxID_ANY, _("&Disconnect"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizer3002->Add( m_buttonGdriveRemoveUser, 1, wxALIGN_CENTER_VERTICAL|wxLEFT, 5 ); + + + bSizer307->Add( bSizer3002, 0, wxBOTTOM|wxRIGHT|wxLEFT|wxALIGN_CENTER_HORIZONTAL, 5 ); + + + bSizer284->Add( bSizer307, 0, wxALL|wxEXPAND, 5 ); + + wxStaticLine* m_staticline841; + m_staticline841 = new wxStaticLine( m_panel41, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL ); + bSizer284->Add( m_staticline841, 0, wxEXPAND, 5 ); + + wxBoxSizer* bSizer3041; + bSizer3041 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer305; + bSizer305 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapGdriveDrive = new wxStaticBitmap( m_panel41, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer305->Add( m_bitmapGdriveDrive, 0, wxRIGHT|wxALIGN_CENTER_VERTICAL, 5 ); + + wxStaticText* m_staticText186; + m_staticText186 = new wxStaticText( m_panel41, wxID_ANY, _("Select drive:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText186->Wrap( -1 ); + bSizer305->Add( m_staticText186, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer3041->Add( bSizer305, 0, wxALL, 5 ); + + m_listBoxGdriveDrives = new wxListBox( m_panel41, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0, NULL, wxLB_NEEDED_SB|wxLB_SINGLE ); + bSizer3041->Add( m_listBoxGdriveDrives, 1, wxEXPAND|wxBOTTOM|wxRIGHT|wxLEFT, 5 ); + + + bSizer284->Add( bSizer3041, 1, wxALL|wxEXPAND, 5 ); + + + bSizerGdrive->Add( bSizer284, 1, wxEXPAND, 5 ); + + wxStaticLine* m_staticline73; + m_staticline73 = new wxStaticLine( m_panel41, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizerGdrive->Add( m_staticline73, 0, wxEXPAND, 5 ); + + + bSizer185->Add( bSizerGdrive, 1, wxEXPAND, 5 ); + + bSizerServer = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer276; + bSizer276 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapServer = new wxStaticBitmap( m_panel41, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer276->Add( m_bitmapServer, 0, wxTOP|wxBOTTOM|wxLEFT|wxALIGN_CENTER_VERTICAL, 5 ); + + wxStaticText* m_staticText12311; + m_staticText12311 = new wxStaticText( m_panel41, wxID_ANY, _("Server name or IP address:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText12311->Wrap( -1 ); + bSizer276->Add( m_staticText12311, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 5 ); + + m_textCtrlServer = new wxTextCtrl( m_panel41, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizer276->Add( m_textCtrlServer, 1, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + + wxStaticText* m_staticText1233; + m_staticText1233 = new wxStaticText( m_panel41, wxID_ANY, _("Port:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText1233->Wrap( -1 ); + bSizer276->Add( m_staticText1233, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 5 ); + + m_textCtrlPort = new wxTextCtrl( m_panel41, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizer276->Add( m_textCtrlPort, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizerServer->Add( bSizer276, 0, wxALL|wxEXPAND, 5 ); + + wxStaticLine* m_staticline58; + m_staticline58 = new wxStaticLine( m_panel41, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizerServer->Add( m_staticline58, 0, wxEXPAND, 5 ); + + + bSizer185->Add( bSizerServer, 0, wxEXPAND, 5 ); + + bSizerAuth = new wxBoxSizer( wxVERTICAL ); + + bSizerAuthInner = new wxBoxSizer( wxHORIZONTAL ); + + bSizerFtpEncrypt = new wxBoxSizer( wxHORIZONTAL ); + + wxBoxSizer* bSizer2181; + bSizer2181 = new wxBoxSizer( wxVERTICAL ); + + wxStaticText* m_staticText1251; + m_staticText1251 = new wxStaticText( m_panel41, wxID_ANY, _("Encryption:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText1251->Wrap( -1 ); + bSizer2181->Add( m_staticText1251, 0, wxTOP|wxRIGHT|wxLEFT, 5 ); + + m_radioBtnEncryptNone = new wxRadioButton( m_panel41, wxID_ANY, _("&Disabled"), wxDefaultPosition, wxDefaultSize, wxRB_GROUP ); + m_radioBtnEncryptNone->SetValue( true ); + bSizer2181->Add( m_radioBtnEncryptNone, 0, wxEXPAND|wxALL, 5 ); + + m_radioBtnEncryptSsl = new wxRadioButton( m_panel41, wxID_ANY, _("&Explicit SSL/TLS"), wxDefaultPosition, wxDefaultSize, 0 ); + bSizer2181->Add( m_radioBtnEncryptSsl, 0, wxBOTTOM|wxRIGHT|wxLEFT|wxEXPAND, 5 ); + + + bSizerFtpEncrypt->Add( bSizer2181, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + + wxStaticLine* m_staticline5721; + m_staticline5721 = new wxStaticLine( m_panel41, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL ); + bSizerFtpEncrypt->Add( m_staticline5721, 0, wxEXPAND, 5 ); + + + bSizerAuthInner->Add( bSizerFtpEncrypt, 0, wxEXPAND, 5 ); + + bSizerSftpAuth = new wxBoxSizer( wxHORIZONTAL ); + + wxBoxSizer* bSizer218; + bSizer218 = new wxBoxSizer( wxVERTICAL ); + + wxStaticText* m_staticText125; + m_staticText125 = new wxStaticText( m_panel41, wxID_ANY, _("Authentication:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText125->Wrap( -1 ); + bSizer218->Add( m_staticText125, 0, wxTOP|wxRIGHT|wxLEFT, 5 ); + + m_radioBtnPassword = new wxRadioButton( m_panel41, wxID_ANY, _("&Password"), wxDefaultPosition, wxDefaultSize, wxRB_GROUP ); + m_radioBtnPassword->SetValue( true ); + bSizer218->Add( m_radioBtnPassword, 0, wxEXPAND|wxALL, 5 ); + + m_radioBtnKeyfile = new wxRadioButton( m_panel41, wxID_ANY, _("&Key file"), wxDefaultPosition, wxDefaultSize, 0 ); + bSizer218->Add( m_radioBtnKeyfile, 0, wxBOTTOM|wxRIGHT|wxLEFT|wxEXPAND, 5 ); + + m_radioBtnAgent = new wxRadioButton( m_panel41, wxID_ANY, _("&SSH agent"), wxDefaultPosition, wxDefaultSize, 0 ); + bSizer218->Add( m_radioBtnAgent, 0, wxEXPAND|wxBOTTOM|wxRIGHT|wxLEFT, 5 ); + + + bSizerSftpAuth->Add( bSizer218, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + + wxStaticLine* m_staticline572; + m_staticline572 = new wxStaticLine( m_panel41, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL ); + bSizerSftpAuth->Add( m_staticline572, 0, wxEXPAND, 5 ); + + + bSizerAuthInner->Add( bSizerSftpAuth, 0, wxEXPAND, 5 ); + + m_panelAuth = new wxPanel( m_panel41, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panelAuth->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer221; + bSizer221 = new wxBoxSizer( wxVERTICAL ); + + wxFlexGridSizer* fgSizer161; + fgSizer161 = new wxFlexGridSizer( 0, 2, 0, 0 ); + fgSizer161->AddGrowableCol( 1 ); + fgSizer161->SetFlexibleDirection( wxBOTH ); + fgSizer161->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); + + wxStaticText* m_staticText123; + m_staticText123 = new wxStaticText( m_panelAuth, wxID_ANY, _("Username:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText123->Wrap( -1 ); + fgSizer161->Add( m_staticText123, 0, wxALIGN_RIGHT|wxTOP|wxBOTTOM|wxLEFT|wxALIGN_CENTER_VERTICAL, 5 ); + + m_textCtrlUserName = new wxTextCtrl( m_panelAuth, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0 ); + fgSizer161->Add( m_textCtrlUserName, 0, wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL, 5 ); + + m_staticTextKeyfile = new wxStaticText( m_panelAuth, wxID_ANY, _("Private key file:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextKeyfile->Wrap( -1 ); + fgSizer161->Add( m_staticTextKeyfile, 0, wxTOP|wxBOTTOM|wxLEFT|wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT, 5 ); + + bSizerKeyFile = new wxBoxSizer( wxHORIZONTAL ); + + m_textCtrlKeyfilePath = new wxTextCtrl( m_panelAuth, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0 ); + bSizerKeyFile->Add( m_textCtrlKeyfilePath, 1, wxALIGN_CENTER_VERTICAL, 5 ); + + m_buttonSelectKeyfile = new wxButton( m_panelAuth, wxID_ANY, _("Browse"), wxDefaultPosition, wxDefaultSize, 0 ); + m_buttonSelectKeyfile->SetToolTip( _("Select a folder") ); + + bSizerKeyFile->Add( m_buttonSelectKeyfile, 0, wxEXPAND, 5 ); + + + fgSizer161->Add( bSizerKeyFile, 0, wxALL|wxEXPAND, 5 ); + + m_staticTextPassword = new wxStaticText( m_panelAuth, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextPassword->Wrap( -1 ); + fgSizer161->Add( m_staticTextPassword, 0, wxALIGN_RIGHT|wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 5 ); + + bSizerPassword = new wxBoxSizer( wxHORIZONTAL ); + + m_textCtrlPasswordVisible = new wxTextCtrl( m_panelAuth, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0 ); + bSizerPassword->Add( m_textCtrlPasswordVisible, 1, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 5 ); + + m_textCtrlPasswordHidden = new wxTextCtrl( m_panelAuth, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_PASSWORD ); + bSizerPassword->Add( m_textCtrlPasswordHidden, 1, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 5 ); + + m_checkBoxShowPassword = new wxCheckBox( m_panelAuth, wxID_ANY, _("&Show password"), wxDefaultPosition, wxDefaultSize, 0 ); + bSizerPassword->Add( m_checkBoxShowPassword, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 ); + + m_checkBoxPasswordPrompt = new wxCheckBox( m_panelAuth, wxID_ANY, _("Prompt during login"), wxDefaultPosition, wxDefaultSize, 0 ); + bSizerPassword->Add( m_checkBoxPasswordPrompt, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + + + fgSizer161->Add( bSizerPassword, 0, wxALIGN_CENTER_VERTICAL|wxEXPAND, 5 ); + + + bSizer221->Add( fgSizer161, 0, wxALL|wxEXPAND, 5 ); + + + m_panelAuth->SetSizer( bSizer221 ); + m_panelAuth->Layout(); + bSizer221->Fit( m_panelAuth ); + bSizerAuthInner->Add( m_panelAuth, 1, wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizerAuth->Add( bSizerAuthInner, 0, wxEXPAND, 5 ); + + wxStaticLine* m_staticline581; + m_staticline581 = new wxStaticLine( m_panel41, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizerAuth->Add( m_staticline581, 0, wxEXPAND, 5 ); + + + bSizer185->Add( bSizerAuth, 0, wxEXPAND, 5 ); + + wxBoxSizer* bSizer269; + bSizer269 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer3051; + bSizer3051 = new wxBoxSizer( wxHORIZONTAL ); + + wxBoxSizer* bSizer270; + bSizer270 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapServerDir = new wxStaticBitmap( m_panel41, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer270->Add( m_bitmapServerDir, 0, wxTOP|wxBOTTOM|wxLEFT|wxALIGN_CENTER_VERTICAL, 5 ); + + wxStaticText* m_staticText1232; + m_staticText1232 = new wxStaticText( m_panel41, wxID_ANY, _("Directory on server:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText1232->Wrap( -1 ); + bSizer270->Add( m_staticText1232, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer3051->Add( bSizer270, 0, wxTOP|wxRIGHT|wxLEFT|wxALIGN_BOTTOM, 5 ); + + + bSizer3051->Add( 0, 0, 1, 0, 5 ); + + wxBoxSizer* bSizer3031; + bSizer3031 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer303; + bSizer303 = new wxBoxSizer( wxHORIZONTAL ); + + wxStaticLine* m_staticline83; + m_staticline83 = new wxStaticLine( m_panel41, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL ); + bSizer303->Add( m_staticline83, 0, wxEXPAND, 5 ); + + wxBoxSizer* bSizer3042; + bSizer3042 = new wxBoxSizer( wxHORIZONTAL ); + + m_staticTextTimeout = new wxStaticText( m_panel41, wxID_ANY, _("Access timeout (in seconds):"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextTimeout->Wrap( -1 ); + bSizer3042->Add( m_staticTextTimeout, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 5 ); + + m_spinCtrlTimeout = new wxSpinCtrl( m_panel41, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize( -1, -1 ), wxSP_ARROW_KEYS, 1, 2000000000, 1 ); + bSizer3042->Add( m_spinCtrlTimeout, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer303->Add( bSizer3042, 0, wxALL, 5 ); + + + bSizer3031->Add( bSizer303, 0, wxALIGN_RIGHT, 5 ); + + wxStaticLine* m_staticline82; + m_staticline82 = new wxStaticLine( m_panel41, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer3031->Add( m_staticline82, 0, wxEXPAND, 5 ); + + + bSizer3051->Add( bSizer3031, 0, wxBOTTOM, 10 ); + + + bSizer269->Add( bSizer3051, 0, wxEXPAND, 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|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 ); + + + bSizer269->Add( bSizer217, 0, wxRIGHT|wxLEFT|wxEXPAND, 5 ); + + + bSizer269->Add( 0, 10, 0, 0, 5 ); + + + bSizer185->Add( bSizer269, 0, wxEXPAND, 5 ); + + + m_panel41->SetSizer( bSizer185 ); + m_panel41->Layout(); + bSizer185->Fit( m_panel41 ); + bSizer134->Add( m_panel41, 1, wxEXPAND, 5 ); + + wxStaticLine* m_staticline571; + m_staticline571 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer134->Add( m_staticline571, 0, wxEXPAND, 5 ); + + wxBoxSizer* bSizer219; + bSizer219 = new wxBoxSizer( wxHORIZONTAL ); + + + bSizer219->Add( 5, 0, 0, 0, 5 ); + + m_bitmapPerf = new wxStaticBitmap( this, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer219->Add( m_bitmapPerf, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 ); + + wxStaticText* m_staticText1361; + m_staticText1361 = new wxStaticText( this, wxID_ANY, _("Performance improvements:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText1361->Wrap( -1 ); + bSizer219->Add( m_staticText1361, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxRIGHT, 10 ); + + + bSizer219->Add( 0, 0, 1, wxEXPAND, 5 ); + + wxHyperlinkCtrl* m_hyperlink171; + m_hyperlink171 = new wxHyperlinkCtrl( this, wxID_ANY, _("How to get the best performance?"), wxT("https://freefilesync.org/manual.php?topic=ftp-setup"), wxDefaultPosition, wxDefaultSize, wxHL_DEFAULT_STYLE ); + m_hyperlink171->SetToolTip( _("https://freefilesync.org/manual.php?topic=ftp-setup") ); + + bSizer219->Add( m_hyperlink171, 0, wxALL|wxALIGN_CENTER_VERTICAL, 10 ); + + + bSizer134->Add( bSizer219, 0, wxEXPAND, 5 ); + + wxStaticLine* m_staticline57; + m_staticline57 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer134->Add( m_staticline57, 0, wxEXPAND, 5 ); + + wxPanel* m_panel411; + m_panel411 = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panel411->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer1851; + bSizer1851 = new wxBoxSizer( wxVERTICAL ); + + wxFlexGridSizer* fgSizer1611; + fgSizer1611 = new wxFlexGridSizer( 0, 2, 0, 0 ); + fgSizer1611->AddGrowableCol( 1 ); + fgSizer1611->SetFlexibleDirection( wxBOTH ); + fgSizer1611->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); + + bSizerConnectionsLabel = new wxBoxSizer( wxVERTICAL ); + + m_staticTextConnectionsLabel = new wxStaticText( m_panel411, wxID_ANY, _("Parallel file operations:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextConnectionsLabel->Wrap( -1 ); + bSizerConnectionsLabel->Add( m_staticTextConnectionsLabel, 0, 0, 5 ); + + m_staticTextConnectionsLabelSub = new wxStaticText( m_panel411, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextConnectionsLabelSub->Wrap( -1 ); + bSizerConnectionsLabel->Add( m_staticTextConnectionsLabelSub, 0, wxALIGN_RIGHT, 5 ); + + + fgSizer1611->Add( bSizerConnectionsLabel, 0, wxTOP|wxBOTTOM|wxLEFT|wxALIGN_RIGHT|wxALIGN_CENTER_VERTICAL, 5 ); + + wxBoxSizer* bSizer300; + bSizer300 = new wxBoxSizer( wxHORIZONTAL ); + + m_spinCtrlConnectionCount = new wxSpinCtrl( m_panel411, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize( -1, -1 ), wxSP_ARROW_KEYS, 1, 2000000000, 1 ); + bSizer300->Add( m_spinCtrlConnectionCount, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + + m_staticTextConnectionCountDescr = new wxStaticText( m_panel411, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextConnectionCountDescr->Wrap( -1 ); + m_staticTextConnectionCountDescr->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + bSizer300->Add( m_staticTextConnectionCountDescr, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 ); + + m_hyperlinkDeRequired = new wxHyperlinkCtrl( m_panel411, wxID_ANY, _("Requires FreeFileSync Donation Edition"), wxT("https://freefilesync.org/faq.php#donation-edition"), wxDefaultPosition, wxDefaultSize, wxHL_DEFAULT_STYLE ); + m_hyperlinkDeRequired->SetToolTip( _("https://freefilesync.org/faq.php#donation-edition") ); + + bSizer300->Add( m_hyperlinkDeRequired, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + + + fgSizer1611->Add( bSizer300, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + m_staticTextChannelCountSftp = new wxStaticText( m_panel411, wxID_ANY, _("SFTP channels per connection:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextChannelCountSftp->Wrap( -1 ); + fgSizer1611->Add( m_staticTextChannelCountSftp, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT|wxTOP|wxBOTTOM|wxLEFT, 5 ); + + wxBoxSizer* bSizer3001; + bSizer3001 = new wxBoxSizer( wxHORIZONTAL ); + + m_spinCtrlChannelCountSftp = new wxSpinCtrl( m_panel411, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize( -1, -1 ), wxSP_ARROW_KEYS, 1, 2000000000, 1 ); + bSizer3001->Add( m_spinCtrlChannelCountSftp, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + + m_buttonChannelCountSftp = new wxButton( m_panel411, wxID_ANY, _("Detect server limit"), wxDefaultPosition, wxDefaultSize, 0 ); + bSizer3001->Add( m_buttonChannelCountSftp, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 ); + + + fgSizer1611->Add( bSizer3001, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + + fgSizer1611->Add( 0, 0, 0, 0, 5 ); + + wxBoxSizer* bSizer304; + bSizer304 = new wxBoxSizer( wxHORIZONTAL ); + + m_checkBoxAllowZlib = new wxCheckBox( m_panel411, wxID_ANY, _("Enable &compression"), wxDefaultPosition, wxDefaultSize, 0 ); + bSizer304->Add( m_checkBoxAllowZlib, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 ); + + m_staticTextZlibDescr = new wxStaticText( m_panel411, wxID_ANY, _("(zlib)"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextZlibDescr->Wrap( -1 ); + m_staticTextZlibDescr->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + bSizer304->Add( m_staticTextZlibDescr, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxRIGHT, 5 ); + + + fgSizer1611->Add( bSizer304, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer1851->Add( fgSizer1611, 0, wxALL, 5 ); + + + m_panel411->SetSizer( bSizer1851 ); + m_panel411->Layout(); + bSizer1851->Fit( m_panel411 ); + bSizer134->Add( m_panel411, 0, wxEXPAND, 5 ); + + wxStaticLine* m_staticline12; + m_staticline12 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer134->Add( m_staticline12, 0, wxEXPAND, 5 ); + + bSizerStdButtons = new wxBoxSizer( wxHORIZONTAL ); + + m_buttonOK = new wxButton( this, wxID_OK, _("OK"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + + m_buttonOK->SetDefault(); + m_buttonOK->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + + bSizerStdButtons->Add( m_buttonOK, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + + m_buttonCancel = new wxButton( this, wxID_CANCEL, _("Cancel"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizerStdButtons->Add( m_buttonCancel, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxRIGHT, 5 ); + + + bSizer134->Add( bSizerStdButtons, 0, wxALIGN_RIGHT, 5 ); + + + this->SetSizer( bSizer134 ); + this->Layout(); + bSizer134->Fit( this ); + + // Connect Events + this->Connect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( CloudSetupDlgGenerated::onClose ) ); + m_toggleBtnGdrive->Connect( wxEVT_COMMAND_TOGGLEBUTTON_CLICKED, wxCommandEventHandler( CloudSetupDlgGenerated::onConnectionGdrive ), NULL, this ); + m_toggleBtnSftp->Connect( wxEVT_COMMAND_TOGGLEBUTTON_CLICKED, wxCommandEventHandler( CloudSetupDlgGenerated::onConnectionSftp ), NULL, this ); + m_toggleBtnFtp->Connect( wxEVT_COMMAND_TOGGLEBUTTON_CLICKED, wxCommandEventHandler( CloudSetupDlgGenerated::onConnectionFtp ), NULL, this ); + m_listBoxGdriveUsers->Connect( wxEVT_COMMAND_LISTBOX_SELECTED, wxCommandEventHandler( CloudSetupDlgGenerated::onGdriveUserSelect ), NULL, this ); + m_buttonGdriveAddUser->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( CloudSetupDlgGenerated::onGdriveUserAdd ), NULL, this ); + m_buttonGdriveRemoveUser->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( CloudSetupDlgGenerated::onGdriveUserRemove ), NULL, this ); + m_radioBtnPassword->Connect( wxEVT_COMMAND_RADIOBUTTON_SELECTED, wxCommandEventHandler( CloudSetupDlgGenerated::onAuthPassword ), NULL, this ); + m_radioBtnKeyfile->Connect( wxEVT_COMMAND_RADIOBUTTON_SELECTED, wxCommandEventHandler( CloudSetupDlgGenerated::onAuthKeyfile ), NULL, this ); + m_radioBtnAgent->Connect( wxEVT_COMMAND_RADIOBUTTON_SELECTED, wxCommandEventHandler( CloudSetupDlgGenerated::onAuthAgent ), NULL, this ); + m_buttonSelectKeyfile->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( CloudSetupDlgGenerated::onSelectKeyfile ), NULL, this ); + m_textCtrlPasswordVisible->Connect( wxEVT_COMMAND_TEXT_UPDATED, wxCommandEventHandler( CloudSetupDlgGenerated::onTypingPassword ), NULL, this ); + m_textCtrlPasswordHidden->Connect( wxEVT_COMMAND_TEXT_UPDATED, wxCommandEventHandler( CloudSetupDlgGenerated::onTypingPassword ), NULL, this ); + m_checkBoxShowPassword->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( CloudSetupDlgGenerated::onToggleShowPassword ), NULL, this ); + m_checkBoxPasswordPrompt->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( CloudSetupDlgGenerated::onTogglePasswordPrompt ), NULL, this ); + m_buttonSelectFolder->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( CloudSetupDlgGenerated::onBrowseCloudFolder ), NULL, this ); + m_buttonChannelCountSftp->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( CloudSetupDlgGenerated::onDetectServerChannelLimit ), NULL, this ); + m_buttonOK->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( CloudSetupDlgGenerated::onOkay ), NULL, this ); + m_buttonCancel->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( CloudSetupDlgGenerated::onCancel ), NULL, this ); +} + +CloudSetupDlgGenerated::~CloudSetupDlgGenerated() +{ +} + +AbstractFolderPickerGenerated::AbstractFolderPickerGenerated( wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style ) : wxDialog( parent, id, title, pos, size, style ) +{ + this->SetSizeHints( wxDefaultSize, wxDefaultSize ); + this->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) ); + + wxBoxSizer* bSizer134; + bSizer134 = new wxBoxSizer( wxVERTICAL ); + + wxPanel* m_panel41; + m_panel41 = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panel41->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer185; + bSizer185 = new wxBoxSizer( wxVERTICAL ); + + m_staticTextStatus = new wxStaticText( m_panel41, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextStatus->Wrap( -1 ); + bSizer185->Add( m_staticTextStatus, 0, wxALL, 5 ); + + m_treeCtrlFileSystem = new wxTreeCtrl( m_panel41, wxID_ANY, wxDefaultPosition, wxSize( -1, -1 ), wxTR_FULL_ROW_HIGHLIGHT|wxTR_HAS_BUTTONS|wxTR_LINES_AT_ROOT|wxTR_NO_LINES|wxBORDER_NONE ); + bSizer185->Add( m_treeCtrlFileSystem, 1, wxEXPAND, 5 ); + + + m_panel41->SetSizer( bSizer185 ); + m_panel41->Layout(); + bSizer185->Fit( m_panel41 ); + bSizer134->Add( m_panel41, 1, wxEXPAND, 5 ); + + wxStaticLine* m_staticline12; + m_staticline12 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer134->Add( m_staticline12, 0, wxEXPAND, 5 ); + + bSizerStdButtons = new wxBoxSizer( wxHORIZONTAL ); + + m_buttonOK = new wxButton( this, wxID_OK, _("Select Folder"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + + m_buttonOK->SetDefault(); + m_buttonOK->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + + bSizerStdButtons->Add( m_buttonOK, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + + m_buttonCancel = new wxButton( this, wxID_CANCEL, _("Cancel"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizerStdButtons->Add( m_buttonCancel, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxRIGHT, 5 ); + + + bSizer134->Add( bSizerStdButtons, 0, wxALIGN_RIGHT, 5 ); + + + this->SetSizer( bSizer134 ); + this->Layout(); + bSizer134->Fit( this ); + + // Connect Events + this->Connect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( AbstractFolderPickerGenerated::onClose ) ); + m_treeCtrlFileSystem->Connect( wxEVT_COMMAND_TREE_ITEM_EXPANDING, wxTreeEventHandler( AbstractFolderPickerGenerated::onExpandNode ), NULL, this ); + m_buttonOK->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( AbstractFolderPickerGenerated::onOkay ), NULL, this ); + m_buttonCancel->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( AbstractFolderPickerGenerated::onCancel ), NULL, this ); +} + +AbstractFolderPickerGenerated::~AbstractFolderPickerGenerated() +{ +} + +SyncConfirmationDlgGenerated::SyncConfirmationDlgGenerated( wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style ) : wxDialog( parent, id, title, pos, size, style ) +{ + this->SetSizeHints( wxDefaultSize, wxDefaultSize ); + this->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) ); + + wxBoxSizer* bSizer134; + bSizer134 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer72; + bSizer72 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapSync = new wxStaticBitmap( this, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer72->Add( m_bitmapSync, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 10 ); + + m_staticTextCaption = new wxStaticText( this, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextCaption->Wrap( -1 ); + bSizer72->Add( m_staticTextCaption, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL|wxALL, 10 ); + + + bSizer134->Add( bSizer72, 0, 0, 5 ); + + wxStaticLine* m_staticline371; + m_staticline371 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer134->Add( m_staticline371, 0, wxEXPAND, 5 ); + + m_panelStatistics = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0 ); + m_panelStatistics->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer185; + bSizer185 = new wxBoxSizer( wxHORIZONTAL ); + + + bSizer185->Add( 40, 0, 0, 0, 5 ); + + + bSizer185->Add( 0, 0, 1, 0, 5 ); + + wxStaticLine* m_staticline38; + m_staticline38 = new wxStaticLine( m_panelStatistics, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL ); + bSizer185->Add( m_staticline38, 0, wxEXPAND, 5 ); + + wxBoxSizer* bSizer162; + bSizer162 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer182; + bSizer182 = new wxBoxSizer( wxHORIZONTAL ); + + wxStaticText* m_staticText84; + m_staticText84 = new wxStaticText( m_panelStatistics, wxID_ANY, _("Variant:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText84->Wrap( -1 ); + bSizer182->Add( m_staticText84, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 5 ); + + + bSizer182->Add( 0, 0, 1, wxEXPAND, 5 ); + + m_staticTextSyncVar = new wxStaticText( m_panelStatistics, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextSyncVar->Wrap( -1 ); + m_staticTextSyncVar->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + + bSizer182->Add( m_staticTextSyncVar, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 ); + + m_bitmapSyncVar = new wxStaticBitmap( m_panelStatistics, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer182->Add( m_bitmapSyncVar, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxRIGHT, 5 ); + + + bSizer182->Add( 0, 0, 1, wxEXPAND, 5 ); + + + bSizer162->Add( bSizer182, 0, wxALL|wxEXPAND, 5 ); + + wxStaticLine* m_staticline14; + m_staticline14 = new wxStaticLine( m_panelStatistics, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer162->Add( m_staticline14, 0, wxEXPAND, 5 ); + + wxBoxSizer* bSizer181; + bSizer181 = new wxBoxSizer( wxVERTICAL ); + + wxStaticText* m_staticText83; + m_staticText83 = new wxStaticText( m_panelStatistics, wxID_ANY, _("Statistics:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText83->Wrap( -1 ); + bSizer181->Add( m_staticText83, 0, wxALL, 5 ); + + wxFlexGridSizer* fgSizer11; + fgSizer11 = new wxFlexGridSizer( 2, 7, 2, 10 ); + fgSizer11->SetFlexibleDirection( wxBOTH ); + fgSizer11->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); + + m_bitmapDeleteLeft = new wxStaticBitmap( m_panelStatistics, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + m_bitmapDeleteLeft->SetToolTip( _("Number of files and folders that will be deleted") ); + + fgSizer11->Add( m_bitmapDeleteLeft, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + m_bitmapUpdateLeft = new wxStaticBitmap( m_panelStatistics, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + m_bitmapUpdateLeft->SetToolTip( _("Number of files that will be updated") ); + + fgSizer11->Add( m_bitmapUpdateLeft, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + m_bitmapCreateLeft = new wxStaticBitmap( m_panelStatistics, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + m_bitmapCreateLeft->SetToolTip( _("Number of files and folders that will be created") ); + + fgSizer11->Add( m_bitmapCreateLeft, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 ); + + m_bitmapData = new wxStaticBitmap( m_panelStatistics, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + m_bitmapData->SetToolTip( _("Total bytes to copy") ); + + fgSizer11->Add( m_bitmapData, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 ); + + m_bitmapCreateRight = new wxStaticBitmap( m_panelStatistics, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + m_bitmapCreateRight->SetToolTip( _("Number of files and folders that will be created") ); + + fgSizer11->Add( m_bitmapCreateRight, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 ); + + m_bitmapUpdateRight = new wxStaticBitmap( m_panelStatistics, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + m_bitmapUpdateRight->SetToolTip( _("Number of files that will be updated") ); + + fgSizer11->Add( m_bitmapUpdateRight, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 ); + + m_bitmapDeleteRight = new wxStaticBitmap( m_panelStatistics, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + m_bitmapDeleteRight->SetToolTip( _("Number of files and folders that will be deleted") ); + + fgSizer11->Add( m_bitmapDeleteRight, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + m_staticTextDeleteLeft = new wxStaticText( m_panelStatistics, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextDeleteLeft->Wrap( -1 ); + m_staticTextDeleteLeft->SetToolTip( _("Number of files and folders that will be deleted") ); + + fgSizer11->Add( m_staticTextDeleteLeft, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + m_staticTextUpdateLeft = new wxStaticText( m_panelStatistics, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextUpdateLeft->Wrap( -1 ); + m_staticTextUpdateLeft->SetToolTip( _("Number of files that will be updated") ); + + fgSizer11->Add( m_staticTextUpdateLeft, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 ); + + m_staticTextCreateLeft = new wxStaticText( m_panelStatistics, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextCreateLeft->Wrap( -1 ); + m_staticTextCreateLeft->SetToolTip( _("Number of files and folders that will be created") ); + + fgSizer11->Add( m_staticTextCreateLeft, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 ); + + m_staticTextData = new wxStaticText( m_panelStatistics, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextData->Wrap( -1 ); + m_staticTextData->SetToolTip( _("Total bytes to copy") ); + + fgSizer11->Add( m_staticTextData, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + m_staticTextCreateRight = new wxStaticText( m_panelStatistics, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextCreateRight->Wrap( -1 ); + m_staticTextCreateRight->SetToolTip( _("Number of files and folders that will be created") ); + + fgSizer11->Add( m_staticTextCreateRight, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + m_staticTextUpdateRight = new wxStaticText( m_panelStatistics, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextUpdateRight->Wrap( -1 ); + m_staticTextUpdateRight->SetToolTip( _("Number of files that will be updated") ); + + fgSizer11->Add( m_staticTextUpdateRight, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + m_staticTextDeleteRight = new wxStaticText( m_panelStatistics, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextDeleteRight->Wrap( -1 ); + m_staticTextDeleteRight->SetToolTip( _("Number of files and folders that will be deleted") ); + + fgSizer11->Add( m_staticTextDeleteRight, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer181->Add( fgSizer11, 0, wxBOTTOM|wxRIGHT|wxLEFT|wxEXPAND, 5 ); + + + bSizer162->Add( bSizer181, 0, wxEXPAND|wxALL, 5 ); + + + bSizer185->Add( bSizer162, 0, 0, 5 ); + + wxStaticLine* m_staticline381; + m_staticline381 = new wxStaticLine( m_panelStatistics, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL ); + bSizer185->Add( m_staticline381, 0, wxEXPAND, 5 ); + + + bSizer185->Add( 0, 0, 1, 0, 5 ); + + + bSizer185->Add( 40, 0, 0, 0, 5 ); + + + m_panelStatistics->SetSizer( bSizer185 ); + m_panelStatistics->Layout(); + bSizer185->Fit( m_panelStatistics ); + bSizer134->Add( m_panelStatistics, 0, wxEXPAND, 5 ); + + wxStaticLine* m_staticline12; + m_staticline12 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer134->Add( m_staticline12, 0, wxEXPAND, 5 ); + + wxBoxSizer* bSizer164; + bSizer164 = new wxBoxSizer( wxVERTICAL ); + + m_checkBoxDontShowAgain = new wxCheckBox( this, wxID_ANY, _("&Don't show this dialog again"), wxDefaultPosition, wxDefaultSize, 0 ); + bSizer164->Add( m_checkBoxDontShowAgain, 0, wxALIGN_CENTER_HORIZONTAL|wxALL, 5 ); + + bSizerStdButtons = new wxBoxSizer( wxHORIZONTAL ); + + m_buttonOK = new wxButton( this, wxID_OK, _("Start"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + + m_buttonOK->SetDefault(); + m_buttonOK->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + + bSizerStdButtons->Add( m_buttonOK, 0, wxALIGN_CENTER_VERTICAL|wxBOTTOM|wxRIGHT|wxLEFT, 5 ); + + m_buttonCancel = new wxButton( this, wxID_CANCEL, _("Cancel"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizerStdButtons->Add( m_buttonCancel, 0, wxALIGN_CENTER_VERTICAL|wxBOTTOM|wxRIGHT, 5 ); + + + bSizer164->Add( bSizerStdButtons, 0, wxALIGN_RIGHT, 5 ); + + + bSizer134->Add( bSizer164, 1, wxEXPAND, 5 ); + + + this->SetSizer( bSizer134 ); + this->Layout(); + bSizer134->Fit( this ); + + // Connect Events + this->Connect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( SyncConfirmationDlgGenerated::onClose ) ); + m_buttonOK->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( SyncConfirmationDlgGenerated::onStartSync ), NULL, this ); + m_buttonCancel->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( SyncConfirmationDlgGenerated::onCancel ), NULL, this ); +} + +SyncConfirmationDlgGenerated::~SyncConfirmationDlgGenerated() +{ +} + +CompareProgressDlgGenerated::CompareProgressDlgGenerated( wxWindow* parent, wxWindowID id, const wxPoint& pos, const wxSize& size, long style, const wxString& name ) : wxPanel( parent, id, pos, size, style, name ) +{ + this->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer1811; + bSizer1811 = new wxBoxSizer( wxVERTICAL ); + + + bSizer1811->Add( 0, 0, 1, 0, 5 ); + + m_staticTextStatus = new wxStaticText( this, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextStatus->Wrap( -1 ); + bSizer1811->Add( m_staticTextStatus, 0, wxTOP|wxRIGHT|wxLEFT, 10 ); + + wxBoxSizer* bSizer199; + bSizer199 = new wxBoxSizer( wxHORIZONTAL ); + + + bSizer199->Add( 10, 0, 0, 0, 5 ); + + wxFlexGridSizer* ffgSizer11; + ffgSizer11 = new wxFlexGridSizer( 2, 0, 5, 5 ); + ffgSizer11->SetFlexibleDirection( wxBOTH ); + ffgSizer11->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); + + m_staticTextProcessed = new wxStaticText( this, wxID_ANY, _("Processed:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextProcessed->Wrap( -1 ); + ffgSizer11->Add( m_staticTextProcessed, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT|wxRIGHT, 5 ); + + m_staticTextRemaining = new wxStaticText( this, wxID_ANY, _("Remaining:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextRemaining->Wrap( -1 ); + ffgSizer11->Add( m_staticTextRemaining, 0, wxALIGN_RIGHT|wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 ); + + + bSizer199->Add( ffgSizer11, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM, 10 ); + + m_panelItemStats = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0 ); + m_panelItemStats->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) ); + + wxBoxSizer* bSizer291; + bSizer291 = new wxBoxSizer( wxHORIZONTAL ); + + wxFlexGridSizer* ffgSizer111; + ffgSizer111 = new wxFlexGridSizer( 0, 2, 5, 5 ); + ffgSizer111->SetFlexibleDirection( wxBOTH ); + ffgSizer111->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); + + wxBoxSizer* bSizer293; + bSizer293 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapItemStat = new wxStaticBitmap( m_panelItemStats, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer293->Add( m_bitmapItemStat, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 ); + + m_staticTextItemsProcessed = new wxStaticText( m_panelItemStats, wxID_ANY, _("dummy"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_staticTextItemsProcessed->Wrap( -1 ); + m_staticTextItemsProcessed->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + + bSizer293->Add( m_staticTextItemsProcessed, 0, wxALIGN_BOTTOM, 5 ); + + + ffgSizer111->Add( bSizer293, 0, wxEXPAND|wxALIGN_RIGHT, 5 ); + + m_staticTextBytesProcessed = new wxStaticText( m_panelItemStats, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextBytesProcessed->Wrap( -1 ); + ffgSizer111->Add( m_staticTextBytesProcessed, 0, wxALIGN_RIGHT|wxALIGN_BOTTOM, 5 ); + + m_staticTextItemsRemaining = new wxStaticText( m_panelItemStats, wxID_ANY, _("dummy"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_staticTextItemsRemaining->Wrap( -1 ); + m_staticTextItemsRemaining->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + + ffgSizer111->Add( m_staticTextItemsRemaining, 0, wxALIGN_RIGHT|wxALIGN_BOTTOM, 5 ); + + m_staticTextBytesRemaining = new wxStaticText( m_panelItemStats, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextBytesRemaining->Wrap( -1 ); + ffgSizer111->Add( m_staticTextBytesRemaining, 0, wxALIGN_RIGHT|wxALIGN_BOTTOM, 5 ); + + + bSizer291->Add( ffgSizer111, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + + + m_panelItemStats->SetSizer( bSizer291 ); + m_panelItemStats->Layout(); + bSizer291->Fit( m_panelItemStats ); + bSizer199->Add( m_panelItemStats, 0, wxTOP|wxBOTTOM|wxRIGHT|wxEXPAND, 10 ); + + m_panelTimeStats = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0 ); + m_panelTimeStats->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) ); + + wxBoxSizer* bSizer292; + bSizer292 = new wxBoxSizer( wxHORIZONTAL ); + + wxFlexGridSizer* ffgSizer112; + ffgSizer112 = new wxFlexGridSizer( 0, 1, 5, 5 ); + ffgSizer112->SetFlexibleDirection( wxBOTH ); + ffgSizer112->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); + + wxBoxSizer* bSizer294; + bSizer294 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapTimeStat = new wxStaticBitmap( m_panelTimeStats, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer294->Add( m_bitmapTimeStat, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 ); + + m_staticTextTimeElapsed = new wxStaticText( m_panelTimeStats, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextTimeElapsed->Wrap( -1 ); + m_staticTextTimeElapsed->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + + bSizer294->Add( m_staticTextTimeElapsed, 0, wxALIGN_BOTTOM, 5 ); + + + ffgSizer112->Add( bSizer294, 0, wxEXPAND|wxALIGN_RIGHT, 5 ); + + m_staticTextTimeRemaining = new wxStaticText( m_panelTimeStats, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextTimeRemaining->Wrap( -1 ); + m_staticTextTimeRemaining->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + + ffgSizer112->Add( m_staticTextTimeRemaining, 0, wxALIGN_RIGHT|wxALIGN_BOTTOM, 5 ); + + + bSizer292->Add( ffgSizer112, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + + + m_panelTimeStats->SetSizer( bSizer292 ); + m_panelTimeStats->Layout(); + bSizer292->Fit( m_panelTimeStats ); + bSizer199->Add( m_panelTimeStats, 0, wxTOP|wxBOTTOM|wxRIGHT|wxEXPAND, 10 ); + + wxFlexGridSizer* ffgSizer114; + ffgSizer114 = new wxFlexGridSizer( 2, 0, 5, 5 ); + ffgSizer114->SetFlexibleDirection( wxBOTH ); + ffgSizer114->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); + + m_staticTextErrors = new wxStaticText( this, wxID_ANY, _("Errors:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextErrors->Wrap( -1 ); + ffgSizer114->Add( m_staticTextErrors, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT|wxRIGHT, 5 ); + + m_staticTextWarnings = new wxStaticText( this, wxID_ANY, _("Warnings:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextWarnings->Wrap( -1 ); + ffgSizer114->Add( m_staticTextWarnings, 0, wxALIGN_RIGHT|wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 ); + + + bSizer199->Add( ffgSizer114, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM, 10 ); + + m_panelErrorStats = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0 ); + m_panelErrorStats->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) ); + + wxBoxSizer* bSizer2921; + bSizer2921 = new wxBoxSizer( wxHORIZONTAL ); + + wxFlexGridSizer* ffgSizer1121; + ffgSizer1121 = new wxFlexGridSizer( 0, 2, 5, 5 ); + ffgSizer1121->SetFlexibleDirection( wxBOTH ); + ffgSizer1121->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); + + m_bitmapErrors = new wxStaticBitmap( m_panelErrorStats, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + ffgSizer1121->Add( m_bitmapErrors, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + m_staticTextErrorCount = new wxStaticText( m_panelErrorStats, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextErrorCount->Wrap( -1 ); + m_staticTextErrorCount->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + + ffgSizer1121->Add( m_staticTextErrorCount, 0, wxALIGN_RIGHT|wxALIGN_BOTTOM, 5 ); + + m_bitmapWarnings = new wxStaticBitmap( m_panelErrorStats, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + ffgSizer1121->Add( m_bitmapWarnings, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + m_staticTextWarningCount = new wxStaticText( m_panelErrorStats, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextWarningCount->Wrap( -1 ); + m_staticTextWarningCount->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + + ffgSizer1121->Add( m_staticTextWarningCount, 0, wxALIGN_BOTTOM|wxALIGN_RIGHT, 5 ); + + + bSizer2921->Add( ffgSizer1121, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + + + m_panelErrorStats->SetSizer( bSizer2921 ); + m_panelErrorStats->Layout(); + bSizer2921->Fit( m_panelErrorStats ); + bSizer199->Add( m_panelErrorStats, 0, wxEXPAND|wxTOP|wxBOTTOM|wxRIGHT, 10 ); + + wxFlexGridSizer* ffgSizer1141; + ffgSizer1141 = new wxFlexGridSizer( 2, 0, 5, 5 ); + ffgSizer1141->SetFlexibleDirection( wxBOTH ); + ffgSizer1141->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); + + bSizerErrorsRetry = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapRetryErrors = new wxStaticBitmap( this, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizerErrorsRetry->Add( m_bitmapRetryErrors, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + wxStaticText* m_staticText1461; + m_staticText1461 = new wxStaticText( this, wxID_ANY, _("Automatic retry"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText1461->Wrap( -1 ); + bSizerErrorsRetry->Add( m_staticText1461, 0, wxALIGN_CENTER_VERTICAL|wxLEFT, 5 ); + + m_staticTextRetryCount = new wxStaticText( this, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextRetryCount->Wrap( -1 ); + bSizerErrorsRetry->Add( m_staticTextRetryCount, 0, wxALIGN_CENTER_VERTICAL|wxLEFT, 5 ); + + + ffgSizer1141->Add( bSizerErrorsRetry, 0, wxALIGN_CENTER_VERTICAL, 10 ); + + bSizerErrorsIgnore = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapIgnoreErrors = new wxStaticBitmap( this, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizerErrorsIgnore->Add( m_bitmapIgnoreErrors, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + wxStaticText* m_staticText146; + m_staticText146 = new wxStaticText( this, wxID_ANY, _("Ignore errors"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText146->Wrap( -1 ); + bSizerErrorsIgnore->Add( m_staticText146, 0, wxALIGN_CENTER_VERTICAL|wxLEFT, 5 ); + + + ffgSizer1141->Add( bSizerErrorsIgnore, 0, wxALIGN_CENTER_VERTICAL, 10 ); + + + bSizer199->Add( ffgSizer1141, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxRIGHT, 10 ); + + bSizerProgressGraph = new wxBoxSizer( wxHORIZONTAL ); + + wxFlexGridSizer* ffgSizer113; + ffgSizer113 = new wxFlexGridSizer( 2, 0, 5, 5 ); + ffgSizer113->SetFlexibleDirection( wxBOTH ); + ffgSizer113->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); + + wxStaticText* m_staticText99; + m_staticText99 = new wxStaticText( this, wxID_ANY, _("Bytes:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText99->Wrap( -1 ); + ffgSizer113->Add( m_staticText99, 0, wxALIGN_RIGHT|wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 ); + + wxStaticText* m_staticText100; + m_staticText100 = new wxStaticText( this, wxID_ANY, _("Items:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText100->Wrap( -1 ); + ffgSizer113->Add( m_staticText100, 0, wxALIGN_RIGHT|wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 ); + + + bSizerProgressGraph->Add( ffgSizer113, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM, 5 ); + + m_panelProgressGraph = new zen::Graph2D( this, wxID_ANY, wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_panelProgressGraph->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) ); + + bSizerProgressGraph->Add( m_panelProgressGraph, 1, wxEXPAND, 5 ); + + + bSizer199->Add( bSizerProgressGraph, 1, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxRIGHT, 10 ); + + + bSizer1811->Add( bSizer199, 0, wxEXPAND, 5 ); + + + bSizer1811->Add( 0, 0, 1, 0, 5 ); + + + this->SetSizer( bSizer1811 ); + this->Layout(); + bSizer1811->Fit( this ); +} + +CompareProgressDlgGenerated::~CompareProgressDlgGenerated() +{ +} + +SyncProgressPanelGenerated::SyncProgressPanelGenerated( wxWindow* parent, wxWindowID id, const wxPoint& pos, const wxSize& size, long style, const wxString& name ) : wxPanel( parent, id, pos, size, style, name ) +{ + bSizerRoot = new wxBoxSizer( wxVERTICAL ); + + wxPanel* m_panel53; + m_panel53 = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panel53->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer301; + bSizer301 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer42; + bSizer42 = new wxBoxSizer( wxHORIZONTAL ); + + + bSizer42->Add( 0, 0, 1, 0, 5 ); + + m_bitmapStatus = new wxStaticBitmap( m_panel53, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizer42->Add( m_bitmapStatus, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + wxBoxSizer* bSizer305; + bSizer305 = new wxBoxSizer( wxHORIZONTAL ); + + m_staticTextPhase = new wxStaticText( m_panel53, wxID_ANY, _("Synchronizing..."), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextPhase->Wrap( -1 ); + m_staticTextPhase->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + + bSizer305->Add( m_staticTextPhase, 0, wxALIGN_BOTTOM, 5 ); + + m_staticTextPercentTotal = new wxStaticText( m_panel53, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextPercentTotal->Wrap( -1 ); + bSizer305->Add( m_staticTextPercentTotal, 0, wxALIGN_BOTTOM, 5 ); + + + bSizer42->Add( bSizer305, 0, wxALIGN_CENTER_VERTICAL|wxLEFT, 5 ); + + wxBoxSizer* bSizer247; + bSizer247 = new wxBoxSizer( wxHORIZONTAL ); + + + bSizer247->Add( 0, 0, 1, 0, 5 ); + + m_bpButtonMinimizeToTray = new wxBitmapButton( m_panel53, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + m_bpButtonMinimizeToTray->SetToolTip( _("Minimize to notification area") ); + + bSizer247->Add( m_bpButtonMinimizeToTray, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer42->Add( bSizer247, 1, wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer301->Add( bSizer42, 0, wxEXPAND|wxTOP|wxBOTTOM, 5 ); + + bSizerStatusText = new wxBoxSizer( wxVERTICAL ); + + m_staticTextStatus = new wxStaticText( m_panel53, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextStatus->Wrap( -1 ); + bSizerStatusText->Add( m_staticTextStatus, 0, wxEXPAND|wxLEFT, 15 ); + + + bSizerStatusText->Add( 0, 10, 0, 0, 5 ); + + + bSizer301->Add( bSizerStatusText, 0, wxEXPAND, 5 ); + + + m_panel53->SetSizer( bSizer301 ); + m_panel53->Layout(); + bSizer301->Fit( m_panel53 ); + bSizerRoot->Add( m_panel53, 0, wxEXPAND, 5 ); + + m_panelProgress = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panelProgress->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer173; + bSizer173 = new wxBoxSizer( wxHORIZONTAL ); + + wxBoxSizer* bSizer161; + bSizer161 = new wxBoxSizer( wxVERTICAL ); + + m_panelGraphBytes = new zen::Graph2D( m_panelProgress, wxID_ANY, wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_panelGraphBytes->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + bSizer161->Add( m_panelGraphBytes, 1, wxEXPAND|wxLEFT, 10 ); + + wxBoxSizer* bSizer232; + bSizer232 = new wxBoxSizer( wxHORIZONTAL ); + + wxBoxSizer* bSizer233; + bSizer233 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer175; + bSizer175 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapGraphKeyBytes = new wxStaticBitmap( m_panelProgress, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer175->Add( m_bitmapGraphKeyBytes, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 ); + + wxStaticText* m_staticText99; + m_staticText99 = new wxStaticText( m_panelProgress, wxID_ANY, _("Bytes"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText99->Wrap( -1 ); + bSizer175->Add( m_staticText99, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer233->Add( bSizer175, 0, wxALL, 5 ); + + + bSizer233->Add( 0, 0, 1, 0, 5 ); + + wxBoxSizer* bSizer174; + bSizer174 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapGraphKeyItems = new wxStaticBitmap( m_panelProgress, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer174->Add( m_bitmapGraphKeyItems, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 ); + + wxStaticText* m_staticText100; + m_staticText100 = new wxStaticText( m_panelProgress, wxID_ANY, _("Items"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText100->Wrap( -1 ); + bSizer174->Add( m_staticText100, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer233->Add( bSizer174, 0, wxALL, 5 ); + + + bSizer232->Add( bSizer233, 1, wxEXPAND|wxRIGHT|wxLEFT, 5 ); + + wxBoxSizer* bSizer304; + bSizer304 = new wxBoxSizer( wxHORIZONTAL ); + + wxFlexGridSizer* ffgSizer11; + ffgSizer11 = new wxFlexGridSizer( 2, 0, 5, 5 ); + ffgSizer11->SetFlexibleDirection( wxBOTH ); + ffgSizer11->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); + + m_staticTextProcessed = new wxStaticText( m_panelProgress, wxID_ANY, _("Processed:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextProcessed->Wrap( -1 ); + ffgSizer11->Add( m_staticTextProcessed, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT|wxRIGHT, 5 ); + + m_staticTextRemaining = new wxStaticText( m_panelProgress, wxID_ANY, _("Remaining:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextRemaining->Wrap( -1 ); + ffgSizer11->Add( m_staticTextRemaining, 0, wxALIGN_RIGHT|wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 ); + + + bSizer304->Add( ffgSizer11, 0, wxTOP|wxBOTTOM|wxALIGN_CENTER_VERTICAL, 10 ); + + m_panelItemStats = new wxPanel( m_panelProgress, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0 ); + m_panelItemStats->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) ); + + wxBoxSizer* bSizer291; + bSizer291 = new wxBoxSizer( wxHORIZONTAL ); + + wxFlexGridSizer* ffgSizer111; + ffgSizer111 = new wxFlexGridSizer( 2, 0, 5, 5 ); + ffgSizer111->SetFlexibleDirection( wxBOTH ); + ffgSizer111->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); + + wxBoxSizer* bSizer293; + bSizer293 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapItemStat = new wxStaticBitmap( m_panelItemStats, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer293->Add( m_bitmapItemStat, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 ); + + m_staticTextItemsProcessed = new wxStaticText( m_panelItemStats, wxID_ANY, _("dummy"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_staticTextItemsProcessed->Wrap( -1 ); + m_staticTextItemsProcessed->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + + bSizer293->Add( m_staticTextItemsProcessed, 0, wxALIGN_BOTTOM, 5 ); + + + ffgSizer111->Add( bSizer293, 0, wxEXPAND|wxALIGN_RIGHT, 5 ); + + m_staticTextBytesProcessed = new wxStaticText( m_panelItemStats, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextBytesProcessed->Wrap( -1 ); + ffgSizer111->Add( m_staticTextBytesProcessed, 0, wxALIGN_RIGHT|wxALIGN_BOTTOM, 5 ); + + m_staticTextItemsRemaining = new wxStaticText( m_panelItemStats, wxID_ANY, _("dummy"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_staticTextItemsRemaining->Wrap( -1 ); + m_staticTextItemsRemaining->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + + ffgSizer111->Add( m_staticTextItemsRemaining, 0, wxALIGN_RIGHT|wxALIGN_BOTTOM, 5 ); + + m_staticTextBytesRemaining = new wxStaticText( m_panelItemStats, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextBytesRemaining->Wrap( -1 ); + ffgSizer111->Add( m_staticTextBytesRemaining, 0, wxALIGN_RIGHT|wxALIGN_BOTTOM, 5 ); + + + bSizer291->Add( ffgSizer111, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + + + m_panelItemStats->SetSizer( bSizer291 ); + m_panelItemStats->Layout(); + bSizer291->Fit( m_panelItemStats ); + bSizer304->Add( m_panelItemStats, 0, wxTOP|wxBOTTOM|wxRIGHT|wxEXPAND, 10 ); + + m_panelTimeStats = new wxPanel( m_panelProgress, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0 ); + m_panelTimeStats->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) ); + + wxBoxSizer* bSizer292; + bSizer292 = new wxBoxSizer( wxHORIZONTAL ); + + wxFlexGridSizer* ffgSizer112; + ffgSizer112 = new wxFlexGridSizer( 2, 0, 5, 5 ); + ffgSizer112->SetFlexibleDirection( wxBOTH ); + ffgSizer112->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); + + wxBoxSizer* bSizer294; + bSizer294 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapTimeStat = new wxStaticBitmap( m_panelTimeStats, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer294->Add( m_bitmapTimeStat, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 ); + + m_staticTextTimeElapsed = new wxStaticText( m_panelTimeStats, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextTimeElapsed->Wrap( -1 ); + m_staticTextTimeElapsed->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + + bSizer294->Add( m_staticTextTimeElapsed, 0, wxALIGN_BOTTOM, 5 ); + + + ffgSizer112->Add( bSizer294, 0, wxEXPAND|wxALIGN_RIGHT, 5 ); + + m_staticTextTimeRemaining = new wxStaticText( m_panelTimeStats, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextTimeRemaining->Wrap( -1 ); + m_staticTextTimeRemaining->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + + ffgSizer112->Add( m_staticTextTimeRemaining, 0, wxALIGN_RIGHT|wxALIGN_BOTTOM, 5 ); + + + bSizer292->Add( ffgSizer112, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + + + m_panelTimeStats->SetSizer( bSizer292 ); + m_panelTimeStats->Layout(); + bSizer292->Fit( m_panelTimeStats ); + bSizer304->Add( m_panelTimeStats, 0, wxTOP|wxBOTTOM|wxRIGHT|wxEXPAND, 10 ); + + wxFlexGridSizer* ffgSizer114; + ffgSizer114 = new wxFlexGridSizer( 2, 0, 5, 5 ); + ffgSizer114->SetFlexibleDirection( wxBOTH ); + ffgSizer114->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); + + m_staticTextErrors = new wxStaticText( m_panelProgress, wxID_ANY, _("Errors:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextErrors->Wrap( -1 ); + ffgSizer114->Add( m_staticTextErrors, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT|wxRIGHT, 5 ); + + m_staticTextWarnings = new wxStaticText( m_panelProgress, wxID_ANY, _("Warnings:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextWarnings->Wrap( -1 ); + ffgSizer114->Add( m_staticTextWarnings, 0, wxALIGN_RIGHT|wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 ); + + + bSizer304->Add( ffgSizer114, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM, 10 ); + + m_panelErrorStats = new wxPanel( m_panelProgress, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0 ); + m_panelErrorStats->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) ); + + wxBoxSizer* bSizer2921; + bSizer2921 = new wxBoxSizer( wxHORIZONTAL ); + + wxFlexGridSizer* ffgSizer1121; + ffgSizer1121 = new wxFlexGridSizer( 0, 2, 5, 5 ); + ffgSizer1121->SetFlexibleDirection( wxBOTH ); + ffgSizer1121->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); + + m_bitmapErrors = new wxStaticBitmap( m_panelErrorStats, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + ffgSizer1121->Add( m_bitmapErrors, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + m_staticTextErrorCount = new wxStaticText( m_panelErrorStats, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextErrorCount->Wrap( -1 ); + m_staticTextErrorCount->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + + ffgSizer1121->Add( m_staticTextErrorCount, 0, wxALIGN_BOTTOM|wxALIGN_RIGHT, 5 ); + + m_bitmapWarnings = new wxStaticBitmap( m_panelErrorStats, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + ffgSizer1121->Add( m_bitmapWarnings, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + m_staticTextWarningCount = new wxStaticText( m_panelErrorStats, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextWarningCount->Wrap( -1 ); + m_staticTextWarningCount->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + + ffgSizer1121->Add( m_staticTextWarningCount, 0, wxALIGN_RIGHT|wxALIGN_BOTTOM, 5 ); + + + bSizer2921->Add( ffgSizer1121, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + + + m_panelErrorStats->SetSizer( bSizer2921 ); + m_panelErrorStats->Layout(); + bSizer2921->Fit( m_panelErrorStats ); + bSizer304->Add( m_panelErrorStats, 0, wxTOP|wxBOTTOM|wxRIGHT|wxEXPAND, 10 ); + + + bSizer232->Add( bSizer304, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer232->Add( 0, 0, 1, 0, 5 ); + + bSizerDynSpace = new wxBoxSizer( wxVERTICAL ); + + + bSizerDynSpace->Add( 0, 0, 0, 0, 5 ); + + + bSizer232->Add( bSizerDynSpace, 0, 0, 5 ); + + + bSizer161->Add( bSizer232, 0, wxEXPAND, 5 ); + + m_panelGraphItems = new zen::Graph2D( m_panelProgress, wxID_ANY, wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_panelGraphItems->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + bSizer161->Add( m_panelGraphItems, 1, wxEXPAND|wxLEFT, 10 ); + + bSizerProgressFooter = new wxBoxSizer( wxHORIZONTAL ); + + bSizerErrorsRetry = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapRetryErrors = new wxStaticBitmap( m_panelProgress, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizerErrorsRetry->Add( m_bitmapRetryErrors, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + wxStaticText* m_staticText1461; + m_staticText1461 = new wxStaticText( m_panelProgress, wxID_ANY, _("Automatic retry"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText1461->Wrap( -1 ); + bSizerErrorsRetry->Add( m_staticText1461, 0, wxALIGN_CENTER_VERTICAL|wxLEFT, 5 ); + + m_staticTextRetryCount = new wxStaticText( m_panelProgress, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextRetryCount->Wrap( -1 ); + bSizerErrorsRetry->Add( m_staticTextRetryCount, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT, 5 ); + + + bSizerProgressFooter->Add( bSizerErrorsRetry, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 ); + + bSizerErrorsIgnore = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapIgnoreErrors = new wxStaticBitmap( m_panelProgress, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizerErrorsIgnore->Add( m_bitmapIgnoreErrors, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + wxStaticText* m_staticText146; + m_staticText146 = new wxStaticText( m_panelProgress, wxID_ANY, _("Ignore errors"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText146->Wrap( -1 ); + bSizerErrorsIgnore->Add( m_staticText146, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT, 5 ); + + + bSizerProgressFooter->Add( bSizerErrorsIgnore, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 ); + + + bSizerProgressFooter->Add( 0, 0, 1, wxEXPAND, 5 ); + + wxStaticText* m_staticText137; + m_staticText137 = new wxStaticText( m_panelProgress, wxID_ANY, _("When finished:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText137->Wrap( -1 ); + bSizerProgressFooter->Add( m_staticText137, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 ); + + wxArrayString m_choicePostSyncActionChoices; + m_choicePostSyncAction = new wxChoice( m_panelProgress, wxID_ANY, wxDefaultPosition, wxDefaultSize, m_choicePostSyncActionChoices, 0 ); + m_choicePostSyncAction->SetSelection( 0 ); + bSizerProgressFooter->Add( m_choicePostSyncAction, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer161->Add( bSizerProgressFooter, 0, wxEXPAND|wxBOTTOM|wxRIGHT|wxLEFT, 10 ); + + + bSizer173->Add( bSizer161, 1, wxEXPAND|wxLEFT, 5 ); + + + m_panelProgress->SetSizer( bSizer173 ); + m_panelProgress->Layout(); + bSizer173->Fit( m_panelProgress ); + bSizerRoot->Add( m_panelProgress, 1, wxEXPAND, 5 ); + + m_notebookResult = new wxNotebook( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNB_FIXEDWIDTH ); + m_notebookResult->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + + bSizerRoot->Add( m_notebookResult, 1, wxEXPAND, 5 ); + + m_staticlineFooter = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizerRoot->Add( m_staticlineFooter, 0, wxEXPAND, 5 ); + + bSizerStdButtons = new wxBoxSizer( wxHORIZONTAL ); + + + bSizerStdButtons->Add( 0, 0, 1, wxEXPAND, 5 ); + + m_checkBoxAutoClose = new wxCheckBox( this, wxID_ANY, _("Auto-close"), wxDefaultPosition, wxDefaultSize, 0 ); + bSizerStdButtons->Add( m_checkBoxAutoClose, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 5 ); + + m_buttonClose = new wxButton( this, wxID_OK, _("Close"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_buttonClose->Enable( false ); + + bSizerStdButtons->Add( m_buttonClose, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxRIGHT, 5 ); + + m_buttonPause = new wxButton( this, wxID_ANY, _("&Pause"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizerStdButtons->Add( m_buttonPause, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxRIGHT, 5 ); + + m_buttonStop = new wxButton( this, wxID_CANCEL, _("Stop"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizerStdButtons->Add( m_buttonStop, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxRIGHT, 5 ); + + + bSizerRoot->Add( bSizerStdButtons, 0, wxEXPAND, 5 ); + + + this->SetSizer( bSizerRoot ); + this->Layout(); + bSizerRoot->Fit( this ); +} + +SyncProgressPanelGenerated::~SyncProgressPanelGenerated() +{ +} + +LogPanelGenerated::LogPanelGenerated( wxWindow* parent, wxWindowID id, const wxPoint& pos, const wxSize& size, long style, const wxString& name ) : wxPanel( parent, id, pos, size, style, name ) +{ + this->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer153; + bSizer153 = new wxBoxSizer( wxHORIZONTAL ); + + wxBoxSizer* bSizer154; + bSizer154 = new wxBoxSizer( wxVERTICAL ); + + m_bpButtonErrors = new zen::ToggleButton( this, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + bSizer154->Add( m_bpButtonErrors, 0, wxALIGN_CENTER_HORIZONTAL, 5 ); + + m_bpButtonWarnings = new zen::ToggleButton( this, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + bSizer154->Add( m_bpButtonWarnings, 0, wxALIGN_CENTER_HORIZONTAL, 5 ); + + m_bpButtonInfo = new zen::ToggleButton( this, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + bSizer154->Add( m_bpButtonInfo, 0, wxALIGN_CENTER_HORIZONTAL, 5 ); + + + bSizer153->Add( bSizer154, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT, 5 ); + + wxStaticLine* m_staticline13; + m_staticline13 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL ); + bSizer153->Add( m_staticline13, 0, wxEXPAND, 5 ); + + m_gridMessages = new zen::Grid( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL|wxVSCROLL ); + m_gridMessages->SetScrollRate( 5, 5 ); + bSizer153->Add( m_gridMessages, 1, wxEXPAND, 5 ); + + + this->SetSizer( bSizer153 ); + this->Layout(); + bSizer153->Fit( this ); + + // Connect Events + m_bpButtonErrors->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( LogPanelGenerated::onErrors ), NULL, this ); + m_bpButtonWarnings->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( LogPanelGenerated::onWarnings ), NULL, this ); + m_bpButtonInfo->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( LogPanelGenerated::onInfo ), NULL, this ); +} + +LogPanelGenerated::~LogPanelGenerated() +{ +} + +BatchDlgGenerated::BatchDlgGenerated( wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style ) : wxDialog( parent, id, title, pos, size, style ) +{ + this->SetSizeHints( wxSize( -1, -1 ), wxDefaultSize ); + this->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) ); + + wxBoxSizer* bSizer54; + bSizer54 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer72; + bSizer72 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapBatchJob = new wxStaticBitmap( this, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizer72->Add( m_bitmapBatchJob, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 10 ); + + m_staticTextHeader = new wxStaticText( this, wxID_ANY, _("Create a batch file for unattended synchronization. To start, double-click this file or schedule in a task planner: %x"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextHeader->Wrap( -1 ); + bSizer72->Add( m_staticTextHeader, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL|wxALL, 10 ); + + + bSizer54->Add( bSizer72, 0, 0, 5 ); + + wxStaticLine* m_staticline18; + m_staticline18 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer54->Add( m_staticline18, 0, wxEXPAND, 5 ); + + wxPanel* m_panel35; + m_panel35 = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panel35->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer172; + bSizer172 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer180; + bSizer180 = new wxBoxSizer( wxHORIZONTAL ); + + wxBoxSizer* bSizer2361; + bSizer2361 = new wxBoxSizer( wxVERTICAL ); + + wxStaticText* m_staticText146; + m_staticText146 = new wxStaticText( m_panel35, wxID_ANY, _("Progress dialog:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText146->Wrap( -1 ); + bSizer2361->Add( m_staticText146, 0, wxTOP|wxRIGHT|wxLEFT, 5 ); + + wxFlexGridSizer* ffgSizer11; + ffgSizer11 = new wxFlexGridSizer( 0, 2, 5, 5 ); + ffgSizer11->SetFlexibleDirection( wxBOTH ); + ffgSizer11->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); + + m_bitmapMinimizeToTray = new wxStaticBitmap( m_panel35, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + ffgSizer11->Add( m_bitmapMinimizeToTray, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + m_checkBoxRunMinimized = new wxCheckBox( m_panel35, wxID_ANY, _("Run minimized"), wxDefaultPosition, wxDefaultSize, 0 ); + ffgSizer11->Add( m_checkBoxRunMinimized, 0, wxEXPAND|wxALIGN_CENTER_VERTICAL, 5 ); + + + ffgSizer11->Add( 0, 0, 1, wxEXPAND, 5 ); + + m_checkBoxAutoClose = new wxCheckBox( m_panel35, wxID_ANY, _("Auto-close"), wxDefaultPosition, wxDefaultSize, 0 ); + ffgSizer11->Add( m_checkBoxAutoClose, 0, wxALIGN_CENTER_VERTICAL|wxEXPAND, 5 ); + + + bSizer2361->Add( ffgSizer11, 0, wxEXPAND|wxALL, 5 ); + + + bSizer180->Add( bSizer2361, 0, wxALL, 5 ); + + wxStaticLine* m_staticline26; + m_staticline26 = new wxStaticLine( m_panel35, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL ); + bSizer180->Add( m_staticline26, 0, wxEXPAND, 5 ); + + wxBoxSizer* bSizer242; + bSizer242 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer243; + bSizer243 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapIgnoreErrors = new wxStaticBitmap( m_panel35, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer243->Add( m_bitmapIgnoreErrors, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + m_checkBoxIgnoreErrors = new wxCheckBox( m_panel35, wxID_ANY, _("Ignore errors"), wxDefaultPosition, wxDefaultSize, 0 ); + bSizer243->Add( m_checkBoxIgnoreErrors, 1, wxALIGN_CENTER_VERTICAL|wxLEFT, 5 ); + + + bSizer242->Add( bSizer243, 0, wxTOP|wxRIGHT|wxLEFT, 5 ); + + wxBoxSizer* bSizer246; + bSizer246 = new wxBoxSizer( wxVERTICAL ); + + m_radioBtnErrorDialogShow = new wxRadioButton( m_panel35, wxID_ANY, _("&Show error message"), wxDefaultPosition, wxDefaultSize, wxRB_GROUP ); + m_radioBtnErrorDialogShow->SetValue( true ); + m_radioBtnErrorDialogShow->SetToolTip( _("Show pop-up on errors or warnings") ); + + bSizer246->Add( m_radioBtnErrorDialogShow, 0, wxALL|wxEXPAND, 5 ); + + m_radioBtnErrorDialogCancel = new wxRadioButton( m_panel35, wxID_ANY, _("&Cancel"), wxDefaultPosition, wxDefaultSize, 0 ); + m_radioBtnErrorDialogCancel->SetToolTip( _("Stop synchronization at first error") ); + + bSizer246->Add( m_radioBtnErrorDialogCancel, 0, wxBOTTOM|wxRIGHT|wxLEFT|wxEXPAND, 5 ); + + + bSizer242->Add( bSizer246, 0, wxALIGN_CENTER_HORIZONTAL|wxLEFT, 15 ); + + + bSizer180->Add( bSizer242, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + + wxStaticLine* m_staticline261; + m_staticline261 = new wxStaticLine( m_panel35, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL ); + bSizer180->Add( m_staticline261, 0, wxEXPAND, 5 ); + + wxBoxSizer* bSizer247; + bSizer247 = new wxBoxSizer( wxVERTICAL ); + + wxStaticText* m_staticText137; + m_staticText137 = new wxStaticText( m_panel35, wxID_ANY, _("When finished:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText137->Wrap( -1 ); + bSizer247->Add( m_staticText137, 0, wxTOP|wxRIGHT|wxLEFT, 5 ); + + wxArrayString m_choicePostSyncActionChoices; + m_choicePostSyncAction = new wxChoice( m_panel35, wxID_ANY, wxDefaultPosition, wxDefaultSize, m_choicePostSyncActionChoices, 0 ); + m_choicePostSyncAction->SetSelection( 0 ); + bSizer247->Add( m_choicePostSyncAction, 0, wxALL, 5 ); + + + bSizer180->Add( bSizer247, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + + wxStaticLine* m_staticline262; + m_staticline262 = new wxStaticLine( m_panel35, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL ); + bSizer180->Add( m_staticline262, 0, wxEXPAND, 5 ); + + + bSizer172->Add( bSizer180, 0, 0, 5 ); + + wxStaticLine* m_staticline25; + m_staticline25 = new wxStaticLine( m_panel35, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer172->Add( m_staticline25, 0, wxEXPAND, 5 ); + + wxHyperlinkCtrl* m_hyperlink17; + m_hyperlink17 = new wxHyperlinkCtrl( m_panel35, wxID_ANY, _("How can I schedule a batch job?"), wxT("https://freefilesync.org/manual.php?topic=schedule-a-batch-job"), wxDefaultPosition, wxDefaultSize, wxHL_DEFAULT_STYLE ); + m_hyperlink17->SetToolTip( _("https://freefilesync.org/manual.php?topic=schedule-a-batch-job") ); + + bSizer172->Add( m_hyperlink17, 0, wxALL, 10 ); + + + m_panel35->SetSizer( bSizer172 ); + m_panel35->Layout(); + bSizer172->Fit( m_panel35 ); + bSizer54->Add( m_panel35, 1, wxEXPAND, 5 ); + + wxStaticLine* m_staticline13; + m_staticline13 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer54->Add( m_staticline13, 0, wxEXPAND, 5 ); + + bSizerStdButtons = new wxBoxSizer( wxHORIZONTAL ); + + m_buttonSaveAs = new wxButton( this, wxID_SAVE, _("Save &as..."), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + + m_buttonSaveAs->SetDefault(); + m_buttonSaveAs->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + + bSizerStdButtons->Add( m_buttonSaveAs, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL|wxALL, 5 ); + + m_buttonCancel = new wxButton( this, wxID_CANCEL, _("Cancel"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizerStdButtons->Add( m_buttonCancel, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxRIGHT, 5 ); + + + bSizer54->Add( bSizerStdButtons, 0, wxALIGN_RIGHT, 5 ); + + + this->SetSizer( bSizer54 ); + this->Layout(); + bSizer54->Fit( this ); + + // Connect Events + this->Connect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( BatchDlgGenerated::onClose ) ); + m_checkBoxRunMinimized->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( BatchDlgGenerated::onToggleRunMinimized ), NULL, this ); + m_checkBoxIgnoreErrors->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( BatchDlgGenerated::onToggleIgnoreErrors ), NULL, this ); + m_buttonSaveAs->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( BatchDlgGenerated::onSaveBatchJob ), NULL, this ); + m_buttonCancel->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( BatchDlgGenerated::onCancel ), NULL, this ); +} + +BatchDlgGenerated::~BatchDlgGenerated() +{ +} + +DeleteDlgGenerated::DeleteDlgGenerated( wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style ) : wxDialog( parent, id, title, pos, size, style ) +{ + this->SetSizeHints( wxSize( -1, -1 ), wxDefaultSize ); + this->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) ); + + wxBoxSizer* bSizer24; + bSizer24 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer72; + bSizer72 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapDeleteType = new wxStaticBitmap( this, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer72->Add( m_bitmapDeleteType, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 10 ); + + m_staticTextHeader = new wxStaticText( this, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextHeader->Wrap( -1 ); + bSizer72->Add( m_staticTextHeader, 0, wxALIGN_CENTER_VERTICAL|wxALL, 10 ); + + + bSizer24->Add( bSizer72, 0, 0, 5 ); + + wxStaticLine* m_staticline91; + m_staticline91 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer24->Add( m_staticline91, 0, wxEXPAND, 5 ); + + wxPanel* m_panel31; + m_panel31 = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panel31->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer185; + bSizer185 = new wxBoxSizer( wxHORIZONTAL ); + + + bSizer185->Add( 60, 0, 0, 0, 5 ); + + wxStaticLine* m_staticline42; + m_staticline42 = new wxStaticLine( m_panel31, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL ); + bSizer185->Add( m_staticline42, 0, wxEXPAND, 5 ); + + m_textCtrlFileList = new wxTextCtrl( m_panel31, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize( -1, -1 ), wxTE_DONTWRAP|wxTE_MULTILINE|wxTE_READONLY|wxBORDER_NONE ); + bSizer185->Add( m_textCtrlFileList, 1, wxEXPAND, 5 ); + + + m_panel31->SetSizer( bSizer185 ); + m_panel31->Layout(); + bSizer185->Fit( m_panel31 ); + bSizer24->Add( m_panel31, 1, wxEXPAND, 5 ); + + wxStaticLine* m_staticline9; + m_staticline9 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer24->Add( m_staticline9, 0, wxEXPAND, 5 ); + + bSizerStdButtons = new wxBoxSizer( wxHORIZONTAL ); + + m_checkBoxUseRecycler = new wxCheckBox( this, wxID_ANY, _("&Recycle bin"), wxDefaultPosition, wxDefaultSize, 0 ); + bSizerStdButtons->Add( m_checkBoxUseRecycler, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 ); + + + bSizerStdButtons->Add( 0, 0, 1, wxEXPAND, 5 ); + + m_buttonOK = new wxButton( this, wxID_OK, _("dummy"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + + m_buttonOK->SetDefault(); + m_buttonOK->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + + bSizerStdButtons->Add( m_buttonOK, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxRIGHT, 5 ); + + m_buttonCancel = new wxButton( this, wxID_CANCEL, _("Cancel"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizerStdButtons->Add( m_buttonCancel, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxRIGHT, 5 ); + + + bSizer24->Add( bSizerStdButtons, 0, wxEXPAND, 5 ); + + + this->SetSizer( bSizer24 ); + this->Layout(); + bSizer24->Fit( this ); + + // Connect Events + this->Connect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( DeleteDlgGenerated::onClose ) ); + m_checkBoxUseRecycler->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( DeleteDlgGenerated::onUseRecycler ), NULL, this ); + m_buttonOK->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DeleteDlgGenerated::onOkay ), NULL, this ); + m_buttonCancel->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DeleteDlgGenerated::onCancel ), NULL, this ); +} + +DeleteDlgGenerated::~DeleteDlgGenerated() +{ +} + +CopyToDlgGenerated::CopyToDlgGenerated( wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style ) : wxDialog( parent, id, title, pos, size, style ) +{ + this->SetSizeHints( wxSize( -1, -1 ), wxDefaultSize ); + this->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) ); + + wxBoxSizer* bSizer24; + bSizer24 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer72; + bSizer72 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapCopyTo = new wxStaticBitmap( this, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer72->Add( m_bitmapCopyTo, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 10 ); + + m_staticTextHeader = new wxStaticText( this, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextHeader->Wrap( -1 ); + bSizer72->Add( m_staticTextHeader, 0, wxALIGN_CENTER_VERTICAL|wxALL, 10 ); + + + bSizer24->Add( bSizer72, 0, 0, 5 ); + + wxStaticLine* m_staticline91; + m_staticline91 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer24->Add( m_staticline91, 0, wxEXPAND, 5 ); + + wxPanel* m_panel31; + m_panel31 = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panel31->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer242; + bSizer242 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer185; + bSizer185 = new wxBoxSizer( wxHORIZONTAL ); + + + bSizer185->Add( 60, 0, 0, 0, 5 ); + + wxStaticLine* m_staticline42; + m_staticline42 = new wxStaticLine( m_panel31, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL ); + bSizer185->Add( m_staticline42, 0, wxEXPAND, 5 ); + + m_textCtrlFileList = new wxTextCtrl( m_panel31, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize( -1, -1 ), wxTE_DONTWRAP|wxTE_MULTILINE|wxTE_READONLY|wxBORDER_NONE ); + bSizer185->Add( m_textCtrlFileList, 1, wxEXPAND, 5 ); + + + bSizer242->Add( bSizer185, 1, wxEXPAND, 5 ); + + wxBoxSizer* bSizer182; + bSizer182 = new wxBoxSizer( wxHORIZONTAL ); + + m_targetFolderPath = new fff::FolderHistoryBox( m_panel31, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0, NULL, 0 ); + bSizer182->Add( m_targetFolderPath, 1, wxALIGN_CENTER_VERTICAL, 5 ); + + m_buttonSelectTargetFolder = new wxButton( m_panel31, wxID_ANY, _("Browse"), wxDefaultPosition, wxDefaultSize, 0 ); + m_buttonSelectTargetFolder->SetToolTip( _("Select a folder") ); + + bSizer182->Add( m_buttonSelectTargetFolder, 0, wxEXPAND, 5 ); + + m_bpButtonSelectAltTargetFolder = new wxBitmapButton( m_panel31, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + m_bpButtonSelectAltTargetFolder->SetToolTip( _("Access online storage") ); + + bSizer182->Add( m_bpButtonSelectAltTargetFolder, 0, wxEXPAND, 5 ); + + + bSizer242->Add( bSizer182, 0, wxALL|wxEXPAND, 10 ); + + + m_panel31->SetSizer( bSizer242 ); + m_panel31->Layout(); + bSizer242->Fit( m_panel31 ); + bSizer24->Add( m_panel31, 1, wxEXPAND, 5 ); + + wxStaticLine* m_staticline9; + m_staticline9 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer24->Add( m_staticline9, 0, wxEXPAND, 5 ); + + bSizerStdButtons = new wxBoxSizer( wxHORIZONTAL ); + + wxBoxSizer* bSizer189; + bSizer189 = new wxBoxSizer( wxVERTICAL ); + + m_checkBoxKeepRelPath = new wxCheckBox( this, wxID_ANY, _("&Keep relative paths"), wxDefaultPosition, wxDefaultSize, 0 ); + m_checkBoxKeepRelPath->SetValue(true); + bSizer189->Add( m_checkBoxKeepRelPath, 0, wxALL|wxEXPAND, 5 ); + + m_checkBoxOverwriteIfExists = new wxCheckBox( this, wxID_ANY, _("&Overwrite existing files"), wxDefaultPosition, wxDefaultSize, 0 ); + m_checkBoxOverwriteIfExists->SetValue(true); + bSizer189->Add( m_checkBoxOverwriteIfExists, 0, wxBOTTOM|wxRIGHT|wxLEFT|wxEXPAND, 5 ); + + + bSizerStdButtons->Add( bSizer189, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizerStdButtons->Add( 0, 0, 1, wxEXPAND, 5 ); + + m_buttonOK = new wxButton( this, wxID_OK, _("Copy"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + + m_buttonOK->SetDefault(); + m_buttonOK->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + + bSizerStdButtons->Add( m_buttonOK, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxRIGHT, 5 ); + + m_buttonCancel = new wxButton( this, wxID_CANCEL, _("Cancel"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizerStdButtons->Add( m_buttonCancel, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxRIGHT, 5 ); + + + bSizer24->Add( bSizerStdButtons, 0, wxEXPAND, 5 ); + + + this->SetSizer( bSizer24 ); + this->Layout(); + bSizer24->Fit( this ); + + // Connect Events + this->Connect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( CopyToDlgGenerated::onClose ) ); + m_buttonOK->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( CopyToDlgGenerated::onOkay ), NULL, this ); + m_buttonCancel->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( CopyToDlgGenerated::onCancel ), NULL, this ); +} + +CopyToDlgGenerated::~CopyToDlgGenerated() +{ +} + +RenameDlgGenerated::RenameDlgGenerated( wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style ) : wxDialog( parent, id, title, pos, size, style ) +{ + this->SetSizeHints( wxSize( -1, -1 ), wxDefaultSize ); + this->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) ); + + wxBoxSizer* bSizer24; + bSizer24 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer72; + bSizer72 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapRename = new wxStaticBitmap( this, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer72->Add( m_bitmapRename, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 10 ); + + m_staticTextHeader = new wxStaticText( this, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextHeader->Wrap( -1 ); + bSizer72->Add( m_staticTextHeader, 0, wxALIGN_CENTER_VERTICAL|wxALL, 10 ); + + + bSizer24->Add( bSizer72, 0, 0, 5 ); + + wxStaticLine* m_staticline91; + m_staticline91 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer24->Add( m_staticline91, 0, wxEXPAND, 5 ); + + wxPanel* m_panel31; + m_panel31 = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panel31->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer242; + bSizer242 = new wxBoxSizer( wxVERTICAL ); + + m_gridRenamePreview = new zen::Grid( m_panel31, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL|wxVSCROLL ); + m_gridRenamePreview->SetScrollRate( 5, 5 ); + bSizer242->Add( m_gridRenamePreview, 1, wxEXPAND, 5 ); + + m_staticlinePreview = new wxStaticLine( m_panel31, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer242->Add( m_staticlinePreview, 0, wxEXPAND, 5 ); + + m_staticTextPlaceholderDescription = new wxStaticText( m_panel31, wxID_ANY, _("Placeholders represent differences between the names."), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextPlaceholderDescription->Wrap( -1 ); + m_staticTextPlaceholderDescription->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + bSizer242->Add( m_staticTextPlaceholderDescription, 0, wxTOP|wxRIGHT|wxLEFT|wxALIGN_CENTER_HORIZONTAL, 10 ); + + m_textCtrlNewName = new wxTextCtrl( m_panel31, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer242->Add( m_textCtrlNewName, 0, wxEXPAND|wxALL, 10 ); + + + m_panel31->SetSizer( bSizer242 ); + m_panel31->Layout(); + bSizer242->Fit( m_panel31 ); + bSizer24->Add( m_panel31, 1, wxEXPAND, 5 ); + + wxStaticLine* m_staticline9; + m_staticline9 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer24->Add( m_staticline9, 0, wxEXPAND, 5 ); + + bSizerStdButtons = new wxBoxSizer( wxHORIZONTAL ); + + m_buttonOK = new wxButton( this, wxID_OK, _("&Rename"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + + m_buttonOK->SetDefault(); + m_buttonOK->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + + bSizerStdButtons->Add( m_buttonOK, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 ); + + m_buttonCancel = new wxButton( this, wxID_CANCEL, _("Cancel"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizerStdButtons->Add( m_buttonCancel, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxRIGHT, 5 ); + + + bSizer24->Add( bSizerStdButtons, 0, wxALIGN_RIGHT, 5 ); + + + this->SetSizer( bSizer24 ); + this->Layout(); + bSizer24->Fit( this ); + + // Connect Events + this->Connect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( RenameDlgGenerated::onClose ) ); + m_buttonOK->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( RenameDlgGenerated::onOkay ), NULL, this ); + m_buttonCancel->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( RenameDlgGenerated::onCancel ), NULL, this ); +} + +RenameDlgGenerated::~RenameDlgGenerated() +{ +} + +OptionsDlgGenerated::OptionsDlgGenerated( wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style ) : wxDialog( parent, id, title, pos, size, style ) +{ + this->SetSizeHints( wxSize( -1, -1 ), wxDefaultSize ); + this->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) ); + + wxBoxSizer* bSizer95; + bSizer95 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer72; + bSizer72 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapSettings = new wxStaticBitmap( this, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizer72->Add( m_bitmapSettings, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 10 ); + + wxStaticText* m_staticText44; + m_staticText44 = new wxStaticText( this, wxID_ANY, _("The following settings are used for all synchronization jobs."), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_staticText44->Wrap( -1 ); + bSizer72->Add( m_staticText44, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL|wxALL, 10 ); + + + bSizer95->Add( bSizer72, 0, 0, 5 ); + + wxStaticLine* m_staticline20; + m_staticline20 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer95->Add( m_staticline20, 0, wxEXPAND, 5 ); + + wxPanel* m_panel39; + m_panel39 = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panel39->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer166; + bSizer166 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer186; + bSizer186 = new wxBoxSizer( wxHORIZONTAL ); + + wxBoxSizer* bSizer160; + bSizer160 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer176; + bSizer176 = new wxBoxSizer( wxHORIZONTAL ); + + m_checkBoxFailSafe = new wxCheckBox( m_panel39, wxID_ANY, _("Fail-safe file copy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_checkBoxFailSafe->SetValue(true); + m_checkBoxFailSafe->SetToolTip( _("Copy to a temporary file (*.ffs_tmp) before overwriting target.\nThis guarantees a consistent state even in case of a serious error.") ); + + bSizer176->Add( m_checkBoxFailSafe, 1, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + + wxStaticText* m_staticText911; + m_staticText911 = new wxStaticText( m_panel39, wxID_ANY, _("("), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText911->Wrap( -1 ); + m_staticText911->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + bSizer176->Add( m_staticText911, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM, 5 ); + + wxStaticText* m_staticText91; + m_staticText91 = new wxStaticText( m_panel39, wxID_ANY, _("recommended"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText91->Wrap( -1 ); + m_staticText91->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + bSizer176->Add( m_staticText91, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM, 5 ); + + wxStaticText* m_staticText9111; + m_staticText9111 = new wxStaticText( m_panel39, wxID_ANY, _(")"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText9111->Wrap( -1 ); + m_staticText9111->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + bSizer176->Add( m_staticText9111, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxRIGHT, 5 ); + + + bSizer160->Add( bSizer176, 0, wxEXPAND, 5 ); + + bSizerLockedFiles = new wxBoxSizer( wxHORIZONTAL ); + + m_checkBoxCopyLocked = new wxCheckBox( m_panel39, wxID_ANY, _("Copy locked files"), wxDefaultPosition, wxDefaultSize, 0 ); + m_checkBoxCopyLocked->SetValue(true); + m_checkBoxCopyLocked->SetToolTip( _("Copy shared or locked files using the Volume Shadow Copy Service.") ); + + bSizerLockedFiles->Add( m_checkBoxCopyLocked, 1, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + + wxStaticText* m_staticText921; + m_staticText921 = new wxStaticText( m_panel39, wxID_ANY, _("("), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText921->Wrap( -1 ); + m_staticText921->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + bSizerLockedFiles->Add( m_staticText921, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM, 5 ); + + wxStaticText* m_staticText92; + m_staticText92 = new wxStaticText( m_panel39, wxID_ANY, _("requires administrator rights"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText92->Wrap( -1 ); + m_staticText92->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + bSizerLockedFiles->Add( m_staticText92, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM, 5 ); + + wxStaticText* m_staticText922; + m_staticText922 = new wxStaticText( m_panel39, wxID_ANY, _(")"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText922->Wrap( -1 ); + m_staticText922->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + bSizerLockedFiles->Add( m_staticText922, 0, wxTOP|wxBOTTOM|wxRIGHT|wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer160->Add( bSizerLockedFiles, 0, wxEXPAND, 5 ); + + wxBoxSizer* bSizer178; + bSizer178 = new wxBoxSizer( wxHORIZONTAL ); + + m_checkBoxCopyPermissions = new wxCheckBox( m_panel39, wxID_ANY, _("Copy file access permissions"), wxDefaultPosition, wxDefaultSize, 0 ); + m_checkBoxCopyPermissions->SetValue(true); + m_checkBoxCopyPermissions->SetToolTip( _("Transfer file and folder permissions.") ); + + bSizer178->Add( m_checkBoxCopyPermissions, 1, wxALIGN_CENTER_VERTICAL|wxALL, 5 ); + + wxStaticText* m_staticText931; + m_staticText931 = new wxStaticText( m_panel39, wxID_ANY, _("("), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText931->Wrap( -1 ); + m_staticText931->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + bSizer178->Add( m_staticText931, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM, 5 ); + + wxStaticText* m_staticText93; + m_staticText93 = new wxStaticText( m_panel39, wxID_ANY, _("requires administrator rights"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText93->Wrap( -1 ); + m_staticText93->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + bSizer178->Add( m_staticText93, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM, 5 ); + + wxStaticText* m_staticText932; + m_staticText932 = new wxStaticText( m_panel39, wxID_ANY, _(")"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText932->Wrap( -1 ); + m_staticText932->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + bSizer178->Add( m_staticText932, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxRIGHT, 5 ); + + + bSizer160->Add( bSizer178, 0, wxEXPAND, 5 ); + + + bSizer186->Add( bSizer160, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 ); + + wxStaticLine* m_staticline39; + m_staticline39 = new wxStaticLine( m_panel39, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL ); + bSizer186->Add( m_staticline39, 0, wxEXPAND, 5 ); + + bSizerColorTheme = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapColorTheme = new wxStaticBitmap( m_panel39, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizerColorTheme->Add( m_bitmapColorTheme, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 5 ); + + wxBoxSizer* bSizer310; + bSizer310 = new wxBoxSizer( wxVERTICAL ); + + wxStaticText* m_staticText198; + m_staticText198 = new wxStaticText( m_panel39, wxID_ANY, _("Color theme:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText198->Wrap( -1 ); + bSizer310->Add( m_staticText198, 0, wxBOTTOM, 5 ); + + wxArrayString m_choiceColorThemeChoices; + m_choiceColorTheme = new wxChoice( m_panel39, wxID_ANY, wxDefaultPosition, wxDefaultSize, m_choiceColorThemeChoices, 0 ); + m_choiceColorTheme->SetSelection( 0 ); + bSizer310->Add( m_choiceColorTheme, 0, 0, 5 ); + + + bSizerColorTheme->Add( bSizer310, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer186->Add( bSizerColorTheme, 0, wxEXPAND|wxALL, 5 ); + + + bSizer166->Add( bSizer186, 0, wxEXPAND, 5 ); + + wxStaticLine* m_staticline191; + m_staticline191 = new wxStaticLine( m_panel39, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer166->Add( m_staticline191, 0, wxEXPAND, 5 ); + + wxBoxSizer* bSizer292; + bSizer292 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapWarnings = new wxStaticBitmap( m_panel39, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer292->Add( m_bitmapWarnings, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 ); + + wxStaticText* m_staticText182; + m_staticText182 = new wxStaticText( m_panel39, wxID_ANY, _("Show hidden dialogs and warning messages again:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText182->Wrap( -1 ); + bSizer292->Add( m_staticText182, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + m_staticTextHiddenDialogsCount = new wxStaticText( m_panel39, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextHiddenDialogsCount->Wrap( -1 ); + m_staticTextHiddenDialogsCount->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + bSizer292->Add( m_staticTextHiddenDialogsCount, 0, wxLEFT|wxALIGN_CENTER_VERTICAL, 5 ); + + m_buttonShowHiddenDialogs = new wxButton( m_panel39, wxID_ANY, _("&Show details"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizer292->Add( m_buttonShowHiddenDialogs, 0, wxALIGN_CENTER_VERTICAL|wxLEFT, 5 ); + + + bSizer166->Add( bSizer292, 0, wxALL, 10 ); + + wxArrayString m_checkListHiddenDialogsChoices; + m_checkListHiddenDialogs = new wxCheckListBox( m_panel39, wxID_ANY, wxDefaultPosition, wxDefaultSize, m_checkListHiddenDialogsChoices, wxLB_EXTENDED ); + bSizer166->Add( m_checkListHiddenDialogs, 1, wxEXPAND|wxBOTTOM|wxRIGHT|wxLEFT, 10 ); + + wxStaticLine* m_staticline1911; + m_staticline1911 = new wxStaticLine( m_panel39, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer166->Add( m_staticline1911, 0, wxEXPAND, 5 ); + + wxFlexGridSizer* fgSizer25111; + fgSizer25111 = new wxFlexGridSizer( 0, 2, 0, 0 ); + fgSizer25111->AddGrowableCol( 1 ); + fgSizer25111->AddGrowableRow( 0 ); + fgSizer25111->SetFlexibleDirection( wxBOTH ); + fgSizer25111->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); + + m_bitmapLogFile = new wxStaticBitmap( m_panel39, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + fgSizer25111->Add( m_bitmapLogFile, 0, wxRIGHT|wxALIGN_CENTER_VERTICAL, 5 ); + + wxBoxSizer* bSizer296; + bSizer296 = new wxBoxSizer( wxHORIZONTAL ); + + wxStaticText* m_staticText163; + m_staticText163 = new wxStaticText( m_panel39, wxID_ANY, _("Default log folder:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText163->Wrap( -1 ); + bSizer296->Add( m_staticText163, 1, wxALIGN_CENTER_VERTICAL, 5 ); + + m_bpButtonShowLogFolder = new wxBitmapButton( m_panel39, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + m_bpButtonShowLogFolder->SetToolTip( _("dummy") ); + + bSizer296->Add( m_bpButtonShowLogFolder, 0, wxLEFT|wxALIGN_CENTER_VERTICAL, 5 ); + + + fgSizer25111->Add( bSizer296, 0, wxALIGN_CENTER_VERTICAL|wxEXPAND, 5 ); + + + fgSizer25111->Add( 0, 0, 0, 0, 5 ); + + m_panelLogfile = new wxPanel( m_panel39, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panelLogfile->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer279; + bSizer279 = new wxBoxSizer( wxHORIZONTAL ); + + m_logFolderPath = new fff::FolderHistoryBox( m_panelLogfile, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0, NULL, 0 ); + bSizer279->Add( m_logFolderPath, 1, wxALIGN_CENTER_VERTICAL, 5 ); + + m_buttonSelectLogFolder = new wxButton( m_panelLogfile, wxID_ANY, _("Browse"), wxDefaultPosition, wxDefaultSize, 0 ); + m_buttonSelectLogFolder->SetToolTip( _("Select a folder") ); + + bSizer279->Add( m_buttonSelectLogFolder, 0, wxEXPAND, 5 ); + + m_bpButtonSelectAltLogFolder = new wxBitmapButton( m_panelLogfile, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + m_bpButtonSelectAltLogFolder->SetToolTip( _("Access online storage") ); + + bSizer279->Add( m_bpButtonSelectAltLogFolder, 0, wxEXPAND, 5 ); + + + m_panelLogfile->SetSizer( bSizer279 ); + m_panelLogfile->Layout(); + bSizer279->Fit( m_panelLogfile ); + fgSizer25111->Add( m_panelLogfile, 0, wxEXPAND, 5 ); + + + fgSizer25111->Add( 0, 0, 0, 0, 5 ); + + wxBoxSizer* bSizer297; + bSizer297 = new wxBoxSizer( wxHORIZONTAL ); + + m_checkBoxLogFilesMaxAge = new wxCheckBox( m_panel39, wxID_ANY, _("&Delete logs after x days:"), wxDefaultPosition, wxDefaultSize, 0 ); + bSizer297->Add( m_checkBoxLogFilesMaxAge, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + m_spinCtrlLogFilesMaxAge = new wxSpinCtrl( m_panel39, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize( -1, -1 ), wxSP_ARROW_KEYS, 1, 2000000000, 1 ); + bSizer297->Add( m_spinCtrlLogFilesMaxAge, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT, 5 ); + + wxStaticLine* m_staticline81; + m_staticline81 = new wxStaticLine( m_panel39, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL ); + bSizer297->Add( m_staticline81, 0, wxEXPAND|wxRIGHT|wxLEFT, 5 ); + + wxStaticText* m_staticText184; + m_staticText184 = new wxStaticText( m_panel39, wxID_ANY, _("Log file format:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText184->Wrap( -1 ); + bSizer297->Add( m_staticText184, 0, wxALIGN_CENTER_VERTICAL|wxLEFT, 5 ); + + wxFlexGridSizer* fgSizer251; + fgSizer251 = new wxFlexGridSizer( 0, 1, 5, 0 ); + fgSizer251->SetFlexibleDirection( wxBOTH ); + fgSizer251->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); + + m_radioBtnLogHtml = new wxRadioButton( m_panel39, wxID_ANY, _("&HTML"), wxDefaultPosition, wxDefaultSize, wxRB_GROUP ); + m_radioBtnLogHtml->SetValue( true ); + fgSizer251->Add( m_radioBtnLogHtml, 0, wxEXPAND, 5 ); + + m_radioBtnLogText = new wxRadioButton( m_panel39, wxID_ANY, _("&Plain text"), wxDefaultPosition, wxDefaultSize, 0 ); + fgSizer251->Add( m_radioBtnLogText, 0, wxEXPAND, 5 ); + + + bSizer297->Add( fgSizer251, 0, wxLEFT|wxALIGN_CENTER_VERTICAL, 5 ); + + + fgSizer25111->Add( bSizer297, 0, wxTOP, 5 ); + + + bSizer166->Add( fgSizer25111, 0, wxALL|wxEXPAND, 10 ); + + wxStaticLine* m_staticline361; + m_staticline361 = new wxStaticLine( m_panel39, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer166->Add( m_staticline361, 0, wxEXPAND, 5 ); + + wxFlexGridSizer* fgSizer251111; + fgSizer251111 = new wxFlexGridSizer( 0, 2, 0, 0 ); + fgSizer251111->AddGrowableCol( 1 ); + fgSizer251111->AddGrowableRow( 0 ); + fgSizer251111->SetFlexibleDirection( wxBOTH ); + fgSizer251111->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); + + m_bitmapNotificationSounds = new wxStaticBitmap( m_panel39, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + fgSizer251111->Add( m_bitmapNotificationSounds, 0, wxRIGHT|wxALIGN_CENTER_VERTICAL, 5 ); + + wxStaticText* m_staticText851; + m_staticText851 = new wxStaticText( m_panel39, wxID_ANY, _("Notification sounds:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText851->Wrap( -1 ); + fgSizer251111->Add( m_staticText851, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + + fgSizer251111->Add( 0, 0, 0, 0, 5 ); + + wxFlexGridSizer* ffgSizer11; + ffgSizer11 = new wxFlexGridSizer( 0, 3, 0, 10 ); + ffgSizer11->AddGrowableCol( 2 ); + ffgSizer11->SetFlexibleDirection( wxBOTH ); + ffgSizer11->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); + + wxStaticText* m_staticText171; + m_staticText171 = new wxStaticText( m_panel39, wxID_ANY, _("Comparison finished:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText171->Wrap( -1 ); + m_staticText171->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + ffgSizer11->Add( m_staticText171, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + m_bitmapCompareDone = new wxStaticBitmap( m_panel39, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + ffgSizer11->Add( m_bitmapCompareDone, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + wxBoxSizer* bSizer290; + bSizer290 = new wxBoxSizer( wxHORIZONTAL ); + + m_bpButtonPlayCompareDone = new wxBitmapButton( m_panel39, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, wxBU_AUTODRAW|0 ); + bSizer290->Add( m_bpButtonPlayCompareDone, 0, wxEXPAND, 5 ); + + m_textCtrlSoundPathCompareDone = new wxTextCtrl( m_panel39, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer290->Add( m_textCtrlSoundPathCompareDone, 1, wxALIGN_CENTER_VERTICAL, 5 ); + + m_buttonSelectSoundCompareDone = new wxButton( m_panel39, wxID_ANY, _("Browse"), wxDefaultPosition, wxDefaultSize, 0 ); + m_buttonSelectSoundCompareDone->SetToolTip( _("Select a folder") ); + + bSizer290->Add( m_buttonSelectSoundCompareDone, 0, wxEXPAND, 5 ); + + + ffgSizer11->Add( bSizer290, 0, wxALIGN_CENTER_VERTICAL|wxEXPAND, 5 ); + + wxStaticText* m_staticText1711; + m_staticText1711 = new wxStaticText( m_panel39, wxID_ANY, _("Synchronization finished:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText1711->Wrap( -1 ); + m_staticText1711->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + ffgSizer11->Add( m_staticText1711, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + m_bitmapSyncDone = new wxStaticBitmap( m_panel39, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + ffgSizer11->Add( m_bitmapSyncDone, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + wxBoxSizer* bSizer2901; + bSizer2901 = new wxBoxSizer( wxHORIZONTAL ); + + m_bpButtonPlaySyncDone = new wxBitmapButton( m_panel39, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, wxBU_AUTODRAW|0 ); + bSizer2901->Add( m_bpButtonPlaySyncDone, 0, wxEXPAND, 5 ); + + m_textCtrlSoundPathSyncDone = new wxTextCtrl( m_panel39, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer2901->Add( m_textCtrlSoundPathSyncDone, 1, wxALIGN_CENTER_VERTICAL, 5 ); + + m_buttonSelectSoundSyncDone = new wxButton( m_panel39, wxID_ANY, _("Browse"), wxDefaultPosition, wxDefaultSize, 0 ); + m_buttonSelectSoundSyncDone->SetToolTip( _("Select a folder") ); + + bSizer2901->Add( m_buttonSelectSoundSyncDone, 0, wxEXPAND, 5 ); + + + ffgSizer11->Add( bSizer2901, 0, wxALIGN_CENTER_VERTICAL|wxEXPAND, 5 ); + + wxStaticText* m_staticText17111; + m_staticText17111 = new wxStaticText( m_panel39, wxID_ANY, _("Unattended error message:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText17111->Wrap( -1 ); + m_staticText17111->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + ffgSizer11->Add( m_staticText17111, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + m_bitmapAlertPending = new wxStaticBitmap( m_panel39, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + ffgSizer11->Add( m_bitmapAlertPending, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + wxBoxSizer* bSizer29011; + bSizer29011 = new wxBoxSizer( wxHORIZONTAL ); + + m_bpButtonPlayAlertPending = new wxBitmapButton( m_panel39, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, wxBU_AUTODRAW|0 ); + bSizer29011->Add( m_bpButtonPlayAlertPending, 0, wxEXPAND, 5 ); + + m_textCtrlSoundPathAlertPending = new wxTextCtrl( m_panel39, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer29011->Add( m_textCtrlSoundPathAlertPending, 1, wxALIGN_CENTER_VERTICAL, 5 ); + + m_buttonSelectSoundAlertPending = new wxButton( m_panel39, wxID_ANY, _("Browse"), wxDefaultPosition, wxDefaultSize, 0 ); + m_buttonSelectSoundAlertPending->SetToolTip( _("Select a folder") ); + + bSizer29011->Add( m_buttonSelectSoundAlertPending, 0, wxEXPAND, 5 ); + + + ffgSizer11->Add( bSizer29011, 1, wxEXPAND, 5 ); + + + fgSizer251111->Add( ffgSizer11, 0, wxEXPAND|wxTOP, 5 ); + + + bSizer166->Add( fgSizer251111, 0, wxALL|wxEXPAND, 10 ); + + wxStaticLine* m_staticline3611; + m_staticline3611 = new wxStaticLine( m_panel39, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer166->Add( m_staticline3611, 0, wxEXPAND, 5 ); + + wxBoxSizer* bSizer2971; + bSizer2971 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapConsole = new wxStaticBitmap( m_panel39, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer2971->Add( m_bitmapConsole, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + wxStaticText* m_staticText85; + m_staticText85 = new wxStaticText( m_panel39, wxID_ANY, _("Customize context menu:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText85->Wrap( -1 ); + bSizer2971->Add( m_staticText85, 0, wxALIGN_CENTER_VERTICAL|wxLEFT, 5 ); + + m_buttonShowCtxCustomize = new wxButton( m_panel39, wxID_ANY, _("&Show details"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizer2971->Add( m_buttonShowCtxCustomize, 0, wxALIGN_CENTER_VERTICAL|wxLEFT, 5 ); + + + bSizer166->Add( bSizer2971, 0, wxALL, 10 ); + + bSizerContextCustomize = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer181; + bSizer181 = new wxBoxSizer( wxHORIZONTAL ); + + wxBoxSizer* bSizer2991; + bSizer2991 = new wxBoxSizer( wxVERTICAL ); + + + bSizer2991->Add( 0, 0, 1, 0, 5 ); + + wxBoxSizer* bSizer193; + bSizer193 = new wxBoxSizer( wxHORIZONTAL ); + + m_bpButtonAddRow = new wxBitmapButton( m_panel39, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + bSizer193->Add( m_bpButtonAddRow, 0, wxALIGN_BOTTOM, 5 ); + + m_bpButtonRemoveRow = new wxBitmapButton( m_panel39, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); + bSizer193->Add( m_bpButtonRemoveRow, 0, wxALIGN_BOTTOM, 5 ); + + + bSizer2991->Add( bSizer193, 0, 0, 5 ); + + + bSizer181->Add( bSizer2991, 1, wxEXPAND, 5 ); + + wxFlexGridSizer* fgSizer37; + fgSizer37 = new wxFlexGridSizer( 0, 2, 0, 10 ); + fgSizer37->SetFlexibleDirection( wxBOTH ); + fgSizer37->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); + + wxStaticText* m_staticText174; + m_staticText174 = new wxStaticText( m_panel39, wxID_ANY, _("%item_path%"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText174->Wrap( -1 ); + m_staticText174->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_MODERN, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL, false, wxEmptyString ) ); + m_staticText174->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + fgSizer37->Add( m_staticText174, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + wxStaticText* m_staticText175; + m_staticText175 = new wxStaticText( m_panel39, wxID_ANY, _("Full file or folder path"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText175->Wrap( -1 ); + m_staticText175->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + fgSizer37->Add( m_staticText175, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + wxStaticText* m_staticText178; + m_staticText178 = new wxStaticText( m_panel39, wxID_ANY, _("%local_path%"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText178->Wrap( -1 ); + m_staticText178->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_MODERN, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL, false, wxEmptyString ) ); + m_staticText178->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + fgSizer37->Add( m_staticText178, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + wxStaticText* m_staticText179; + m_staticText179 = new wxStaticText( m_panel39, wxID_ANY, _("Temporary local copy for SFTP and MTP storage"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText179->Wrap( -1 ); + m_staticText179->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + fgSizer37->Add( m_staticText179, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + wxStaticText* m_staticText189; + m_staticText189 = new wxStaticText( m_panel39, wxID_ANY, _("%item_name%"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText189->Wrap( -1 ); + m_staticText189->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_MODERN, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL, false, wxEmptyString ) ); + m_staticText189->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + fgSizer37->Add( m_staticText189, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + wxStaticText* m_staticText190; + m_staticText190 = new wxStaticText( m_panel39, wxID_ANY, _("File or folder name"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText190->Wrap( -1 ); + m_staticText190->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + fgSizer37->Add( m_staticText190, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + wxStaticText* m_staticText176; + m_staticText176 = new wxStaticText( m_panel39, wxID_ANY, _("%parent_path%"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText176->Wrap( -1 ); + m_staticText176->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_MODERN, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL, false, wxEmptyString ) ); + m_staticText176->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + fgSizer37->Add( m_staticText176, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + wxBoxSizer* bSizer298; + bSizer298 = new wxBoxSizer( wxHORIZONTAL ); + + wxStaticText* m_staticText177; + m_staticText177 = new wxStaticText( m_panel39, wxID_ANY, _("Parent folder path"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText177->Wrap( -1 ); + m_staticText177->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + bSizer298->Add( m_staticText177, 0, wxRIGHT|wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer298->Add( 0, 0, 1, 0, 5 ); + + wxHyperlinkCtrl* m_hyperlink17; + m_hyperlink17 = new wxHyperlinkCtrl( m_panel39, wxID_ANY, _("Show examples"), wxT("https://freefilesync.org/manual.php?topic=external-applications"), wxDefaultPosition, wxDefaultSize, wxHL_DEFAULT_STYLE ); + m_hyperlink17->SetToolTip( _("https://freefilesync.org/manual.php?topic=external-applications") ); + + bSizer298->Add( m_hyperlink17, 0, wxLEFT|wxALIGN_BOTTOM, 5 ); + + + fgSizer37->Add( bSizer298, 0, wxALIGN_CENTER_VERTICAL|wxEXPAND, 5 ); + + + bSizer181->Add( fgSizer37, 0, wxBOTTOM|wxLEFT, 10 ); + + + bSizerContextCustomize->Add( bSizer181, 0, wxEXPAND, 5 ); + + m_gridCustomCommand = new wxGrid( m_panel39, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0 ); + + // Grid + m_gridCustomCommand->CreateGrid( 3, 2 ); + m_gridCustomCommand->EnableEditing( true ); + m_gridCustomCommand->EnableGridLines( true ); + m_gridCustomCommand->EnableDragGridSize( false ); + m_gridCustomCommand->SetMargins( 0, 0 ); + + // Columns + m_gridCustomCommand->EnableDragColMove( false ); + m_gridCustomCommand->EnableDragColSize( false ); + m_gridCustomCommand->SetColLabelValue( 0, _("Description") ); + m_gridCustomCommand->SetColLabelValue( 1, _("Command line") ); + m_gridCustomCommand->SetColLabelSize( -1 ); + m_gridCustomCommand->SetColLabelAlignment( wxALIGN_CENTER, wxALIGN_CENTER ); + + // Rows + m_gridCustomCommand->EnableDragRowSize( false ); + m_gridCustomCommand->SetRowLabelSize( 1 ); + m_gridCustomCommand->SetRowLabelAlignment( wxALIGN_CENTER, wxALIGN_CENTER ); + + // Label Appearance + + // Cell Defaults + m_gridCustomCommand->SetDefaultCellAlignment( wxALIGN_LEFT, wxALIGN_TOP ); + bSizerContextCustomize->Add( m_gridCustomCommand, 1, wxEXPAND, 5 ); + + + bSizer166->Add( bSizerContextCustomize, 1, wxEXPAND|wxBOTTOM|wxRIGHT|wxLEFT, 10 ); + + + m_panel39->SetSizer( bSizer166 ); + m_panel39->Layout(); + bSizer166->Fit( m_panel39 ); + bSizer95->Add( m_panel39, 1, wxEXPAND, 5 ); + + wxStaticLine* m_staticline36; + m_staticline36 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer95->Add( m_staticline36, 0, wxEXPAND, 5 ); + + bSizerStdButtons = new wxBoxSizer( wxHORIZONTAL ); + + m_buttonDefault = new wxButton( this, wxID_DEFAULT, _("&Default"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizerStdButtons->Add( m_buttonDefault, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizerStdButtons->Add( 0, 0, 1, 0, 5 ); + + m_buttonOK = new wxButton( this, wxID_OK, _("OK"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + + m_buttonOK->SetDefault(); + m_buttonOK->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + + bSizerStdButtons->Add( m_buttonOK, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 ); + + m_buttonCancel = new wxButton( this, wxID_CANCEL, _("Cancel"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizerStdButtons->Add( m_buttonCancel, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxRIGHT, 5 ); + + + bSizer95->Add( bSizerStdButtons, 0, wxEXPAND, 5 ); + + + this->SetSizer( bSizer95 ); + this->Layout(); + bSizer95->Fit( this ); + + // Connect Events + this->Connect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( OptionsDlgGenerated::onClose ) ); + m_choiceColorTheme->Connect( wxEVT_COMMAND_CHOICE_SELECTED, wxCommandEventHandler( OptionsDlgGenerated::onChangeColorTheme ), NULL, this ); + m_buttonShowHiddenDialogs->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( OptionsDlgGenerated::onShowHiddenDialogs ), NULL, this ); + m_checkListHiddenDialogs->Connect( wxEVT_COMMAND_CHECKLISTBOX_TOGGLED, wxCommandEventHandler( OptionsDlgGenerated::onToggleHiddenDialog ), NULL, this ); + m_bpButtonShowLogFolder->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( OptionsDlgGenerated::onShowLogFolder ), NULL, this ); + m_checkBoxLogFilesMaxAge->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( OptionsDlgGenerated::onToggleLogfilesLimit ), NULL, this ); + m_bpButtonPlayCompareDone->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( OptionsDlgGenerated::onPlayCompareDone ), NULL, this ); + m_textCtrlSoundPathCompareDone->Connect( wxEVT_COMMAND_TEXT_UPDATED, wxCommandEventHandler( OptionsDlgGenerated::onChangeSoundFilePath ), NULL, this ); + m_buttonSelectSoundCompareDone->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( OptionsDlgGenerated::onSelectSoundCompareDone ), NULL, this ); + m_bpButtonPlaySyncDone->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( OptionsDlgGenerated::onPlaySyncDone ), NULL, this ); + m_textCtrlSoundPathSyncDone->Connect( wxEVT_COMMAND_TEXT_UPDATED, wxCommandEventHandler( OptionsDlgGenerated::onChangeSoundFilePath ), NULL, this ); + m_buttonSelectSoundSyncDone->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( OptionsDlgGenerated::onSelectSoundSyncDone ), NULL, this ); + m_bpButtonPlayAlertPending->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( OptionsDlgGenerated::onPlayAlertPending ), NULL, this ); + m_textCtrlSoundPathAlertPending->Connect( wxEVT_COMMAND_TEXT_UPDATED, wxCommandEventHandler( OptionsDlgGenerated::onChangeSoundFilePath ), NULL, this ); + m_buttonSelectSoundAlertPending->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( OptionsDlgGenerated::onSelectSoundAlertPending ), NULL, this ); + m_buttonShowCtxCustomize->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( OptionsDlgGenerated::onShowContextCustomize ), NULL, this ); + m_bpButtonAddRow->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( OptionsDlgGenerated::onAddRow ), NULL, this ); + m_bpButtonRemoveRow->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( OptionsDlgGenerated::onRemoveRow ), NULL, this ); + m_buttonDefault->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( OptionsDlgGenerated::onDefault ), NULL, this ); + m_buttonOK->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( OptionsDlgGenerated::onOkay ), NULL, this ); + m_buttonCancel->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( OptionsDlgGenerated::onCancel ), NULL, this ); +} + +OptionsDlgGenerated::~OptionsDlgGenerated() +{ +} + +SelectTimespanDlgGenerated::SelectTimespanDlgGenerated( wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style ) : wxDialog( parent, id, title, pos, size, style ) +{ + this->SetSizeHints( wxDefaultSize, wxDefaultSize ); + this->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) ); + + wxBoxSizer* bSizer96; + bSizer96 = new wxBoxSizer( wxVERTICAL ); + + wxPanel* m_panel35; + m_panel35 = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panel35->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer98; + bSizer98 = new wxBoxSizer( wxHORIZONTAL ); + + m_calendarFrom = new wxCalendarCtrl( m_panel35, wxID_ANY, wxDefaultDateTime, wxDefaultPosition, wxDefaultSize, wxCAL_SHOW_HOLIDAYS|wxCAL_SHOW_SURROUNDING_WEEKS|wxBORDER_NONE ); + bSizer98->Add( m_calendarFrom, 0, wxTOP|wxBOTTOM|wxLEFT, 10 ); + + m_calendarTo = new wxCalendarCtrl( m_panel35, wxID_ANY, wxDefaultDateTime, wxDefaultPosition, wxDefaultSize, wxCAL_SHOW_HOLIDAYS|wxCAL_SHOW_SURROUNDING_WEEKS|wxBORDER_NONE ); + bSizer98->Add( m_calendarTo, 0, wxALL, 10 ); + + + m_panel35->SetSizer( bSizer98 ); + m_panel35->Layout(); + bSizer98->Fit( m_panel35 ); + bSizer96->Add( m_panel35, 0, wxEXPAND, 5 ); + + wxStaticLine* m_staticline21; + m_staticline21 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer96->Add( m_staticline21, 0, wxEXPAND, 5 ); + + bSizerStdButtons = new wxBoxSizer( wxHORIZONTAL ); + + m_buttonOK = new wxButton( this, wxID_OK, _("OK"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + + m_buttonOK->SetDefault(); + m_buttonOK->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + + bSizerStdButtons->Add( m_buttonOK, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 ); + + m_buttonCancel = new wxButton( this, wxID_CANCEL, _("Cancel"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizerStdButtons->Add( m_buttonCancel, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxRIGHT, 5 ); + + + bSizer96->Add( bSizerStdButtons, 0, wxALIGN_RIGHT, 5 ); + + + this->SetSizer( bSizer96 ); + this->Layout(); + bSizer96->Fit( this ); + + // Connect Events + this->Connect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( SelectTimespanDlgGenerated::onClose ) ); + m_calendarFrom->Connect( wxEVT_CALENDAR_SEL_CHANGED, wxCalendarEventHandler( SelectTimespanDlgGenerated::onChangeSelectionFrom ), NULL, this ); + m_calendarTo->Connect( wxEVT_CALENDAR_SEL_CHANGED, wxCalendarEventHandler( SelectTimespanDlgGenerated::onChangeSelectionTo ), NULL, this ); + m_buttonOK->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( SelectTimespanDlgGenerated::onOkay ), NULL, this ); + m_buttonCancel->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( SelectTimespanDlgGenerated::onCancel ), NULL, this ); +} + +SelectTimespanDlgGenerated::~SelectTimespanDlgGenerated() +{ +} + +AboutDlgGenerated::AboutDlgGenerated( wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style ) : wxDialog( parent, id, title, pos, size, style ) +{ + this->SetSizeHints( wxDefaultSize, wxDefaultSize ); + this->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) ); + + wxBoxSizer* bSizer31; + bSizer31 = new wxBoxSizer( wxVERTICAL ); + + wxPanel* m_panel41; + m_panel41 = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panel41->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer174; + bSizer174 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapLogoLeft = new wxStaticBitmap( m_panel41, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer174->Add( m_bitmapLogoLeft, 0, wxBOTTOM, 5 ); + + wxStaticLine* m_staticline81; + m_staticline81 = new wxStaticLine( m_panel41, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL ); + bSizer174->Add( m_staticline81, 0, wxEXPAND, 5 ); + + bSizerMainSection = new wxBoxSizer( wxVERTICAL ); + + wxStaticLine* m_staticline82; + m_staticline82 = new wxStaticLine( m_panel41, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizerMainSection->Add( m_staticline82, 0, wxEXPAND, 5 ); + + m_bitmapLogo = new wxStaticBitmap( m_panel41, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizerMainSection->Add( m_bitmapLogo, 0, 0, 5 ); + + wxStaticLine* m_staticline341; + m_staticline341 = new wxStaticLine( m_panel41, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizerMainSection->Add( m_staticline341, 0, wxEXPAND, 5 ); + + wxBoxSizer* bSizer298; + bSizer298 = new wxBoxSizer( wxHORIZONTAL ); + + m_staticFfsTextVersion = new wxStaticText( m_panel41, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticFfsTextVersion->Wrap( -1 ); + bSizer298->Add( m_staticFfsTextVersion, 0, wxALIGN_BOTTOM, 5 ); + + m_staticTextFfsVariant = new wxStaticText( m_panel41, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextFfsVariant->Wrap( -1 ); + m_staticTextFfsVariant->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + + bSizer298->Add( m_staticTextFfsVariant, 0, wxLEFT|wxALIGN_BOTTOM, 10 ); + + + bSizerMainSection->Add( bSizer298, 0, wxALIGN_CENTER_HORIZONTAL|wxALL, 5 ); + + wxStaticLine* m_staticline3411; + m_staticline3411 = new wxStaticLine( m_panel41, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizerMainSection->Add( m_staticline3411, 0, wxEXPAND, 5 ); + + bSizerDonate = new wxBoxSizer( wxVERTICAL ); + + + bSizerDonate->Add( 0, 0, 1, 0, 5 ); + + m_panelDonate = new wxPanel( m_panel41, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panelDonate->SetBackgroundColour( wxColour( 153, 170, 187 ) ); + + wxBoxSizer* bSizer183; + bSizer183 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapAnimalSmall = new wxStaticBitmap( m_panelDonate, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer183->Add( m_bitmapAnimalSmall, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 ); + + wxPanel* m_panel39; + m_panel39 = new wxPanel( m_panelDonate, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panel39->SetBackgroundColour( wxColour( 248, 248, 248 ) ); + + wxBoxSizer* bSizer184; + bSizer184 = new wxBoxSizer( wxHORIZONTAL ); + + m_staticTextDonate = new wxStaticText( m_panel39, wxID_ANY, _("Get the Donation Edition with bonus features and help keep FreeFileSync ad-free."), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextDonate->Wrap( -1 ); + m_staticTextDonate->SetForegroundColour( wxColour( 0, 0, 0 ) ); + + bSizer184->Add( m_staticTextDonate, 0, wxALIGN_CENTER_HORIZONTAL|wxLEFT|wxALIGN_CENTER_VERTICAL, 10 ); + + + m_panel39->SetSizer( bSizer184 ); + m_panel39->Layout(); + bSizer184->Fit( m_panel39 ); + bSizer183->Add( m_panel39, 1, wxTOP|wxBOTTOM|wxRIGHT|wxEXPAND, 5 ); + + + m_panelDonate->SetSizer( bSizer183 ); + m_panelDonate->Layout(); + bSizer183->Fit( m_panelDonate ); + bSizerDonate->Add( m_panelDonate, 0, wxEXPAND, 5 ); + + m_buttonDonate1 = new zen::BitmapTextButton( m_panel41, wxID_ANY, _("Support with a donation"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_buttonDonate1->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + m_buttonDonate1->SetToolTip( _("https://freefilesync.org/donate") ); + + bSizerDonate->Add( m_buttonDonate1, 0, wxEXPAND|wxALL, 10 ); + + + bSizerDonate->Add( 0, 0, 1, 0, 5 ); + + + bSizerMainSection->Add( bSizerDonate, 1, wxEXPAND, 5 ); + + m_bitmapAnimalBig = new wxStaticBitmap( m_panel41, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizerMainSection->Add( m_bitmapAnimalBig, 0, wxALIGN_CENTER_HORIZONTAL, 5 ); + + wxStaticLine* m_staticline3412; + m_staticline3412 = new wxStaticLine( m_panel41, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizerMainSection->Add( m_staticline3412, 0, wxEXPAND, 5 ); + + wxBoxSizer* bSizer186; + bSizer186 = new wxBoxSizer( wxVERTICAL ); + + wxStaticText* m_staticText94; + m_staticText94 = new wxStaticText( m_panel41, wxID_ANY, _("Share your feedback and ideas:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText94->Wrap( -1 ); + bSizer186->Add( m_staticText94, 0, wxALIGN_CENTER_HORIZONTAL|wxTOP|wxRIGHT|wxLEFT, 5 ); + + wxBoxSizer* bSizer289; + bSizer289 = new wxBoxSizer( wxHORIZONTAL ); + + m_bpButtonForum = new wxBitmapButton( m_panel41, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, wxBU_AUTODRAW|0 ); + m_bpButtonForum->SetToolTip( _("https://freefilesync.org/forum") ); + + bSizer289->Add( m_bpButtonForum, 0, wxALL|wxEXPAND, 5 ); + + m_bpButtonEmail = new wxBitmapButton( m_panel41, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, wxBU_AUTODRAW|0 ); + bSizer289->Add( m_bpButtonEmail, 0, wxTOP|wxBOTTOM|wxRIGHT|wxEXPAND, 5 ); + + + bSizer186->Add( bSizer289, 0, wxALIGN_CENTER_HORIZONTAL, 5 ); + + + bSizerMainSection->Add( bSizer186, 0, wxALIGN_CENTER_HORIZONTAL|wxALL, 5 ); + + + bSizer174->Add( bSizerMainSection, 0, wxEXPAND, 5 ); + + wxStaticLine* m_staticline37; + m_staticline37 = new wxStaticLine( m_panel41, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL ); + bSizer174->Add( m_staticline37, 0, wxEXPAND, 5 ); + + wxBoxSizer* bSizer177; + bSizer177 = new wxBoxSizer( wxVERTICAL ); + + m_staticTextThanksForLoc = new wxStaticText( m_panel41, wxID_ANY, _("Many thanks for translation:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextThanksForLoc->Wrap( -1 ); + bSizer177->Add( m_staticTextThanksForLoc, 0, wxALL, 5 ); + + m_scrolledWindowTranslators = new wxScrolledWindow( m_panel41, wxID_ANY, wxDefaultPosition, wxSize( -1, -1 ), wxVSCROLL ); + m_scrolledWindowTranslators->SetScrollRate( 10, 10 ); + m_scrolledWindowTranslators->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + fgSizerTranslators = new wxFlexGridSizer( 0, 2, 2, 10 ); + fgSizerTranslators->SetFlexibleDirection( wxBOTH ); + fgSizerTranslators->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); + + + m_scrolledWindowTranslators->SetSizer( fgSizerTranslators ); + m_scrolledWindowTranslators->Layout(); + fgSizerTranslators->Fit( m_scrolledWindowTranslators ); + bSizer177->Add( m_scrolledWindowTranslators, 1, wxEXPAND|wxLEFT, 5 ); + + + bSizer174->Add( bSizer177, 0, wxEXPAND|wxLEFT, 5 ); + + + m_panel41->SetSizer( bSizer174 ); + m_panel41->Layout(); + bSizer174->Fit( m_panel41 ); + bSizer31->Add( m_panel41, 0, wxEXPAND, 5 ); + + wxStaticLine* m_staticline36; + m_staticline36 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer31->Add( m_staticline36, 0, wxEXPAND, 5 ); + + bSizerStdButtons = new wxBoxSizer( wxHORIZONTAL ); + + m_buttonShowSupporterDetails = new wxButton( this, wxID_ANY, _("Thank you, %x, for your support!"), wxDefaultPosition, wxDefaultSize, 0 ); + bSizerStdButtons->Add( m_buttonShowSupporterDetails, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 5 ); + + + bSizerStdButtons->Add( 0, 0, 1, 0, 5 ); + + m_buttonDonate2 = new zen::BitmapTextButton( this, wxID_ANY, _("&Donate"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_buttonDonate2->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + m_buttonDonate2->SetToolTip( _("https://freefilesync.org/donate") ); + + bSizerStdButtons->Add( m_buttonDonate2, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 5 ); + + m_buttonClose = new wxButton( this, wxID_OK, _("Close"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + + m_buttonClose->SetDefault(); + bSizerStdButtons->Add( m_buttonClose, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer31->Add( bSizerStdButtons, 0, wxEXPAND, 5 ); + + + this->SetSizer( bSizer31 ); + this->Layout(); + bSizer31->Fit( this ); + + // Connect Events + this->Connect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( AboutDlgGenerated::onClose ) ); + m_buttonDonate1->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( AboutDlgGenerated::onDonate ), NULL, this ); + m_bpButtonForum->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( AboutDlgGenerated::onOpenForum ), NULL, this ); + m_bpButtonEmail->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( AboutDlgGenerated::onSendEmail ), NULL, this ); + m_buttonShowSupporterDetails->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( AboutDlgGenerated::onShowSupporterDetails ), NULL, this ); + m_buttonDonate2->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( AboutDlgGenerated::onDonate ), NULL, this ); + m_buttonClose->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( AboutDlgGenerated::onOkay ), NULL, this ); +} + +AboutDlgGenerated::~AboutDlgGenerated() +{ +} + +DownloadProgressDlgGenerated::DownloadProgressDlgGenerated( wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style ) : wxDialog( parent, id, title, pos, size, style ) +{ + this->SetSizeHints( wxDefaultSize, wxDefaultSize ); + + wxBoxSizer* bSizer24; + bSizer24 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer72; + bSizer72 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapDownloading = new wxStaticBitmap( this, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer72->Add( m_bitmapDownloading, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 10 ); + + m_staticTextHeader = new wxStaticText( this, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextHeader->Wrap( -1 ); + bSizer72->Add( m_staticTextHeader, 0, wxALIGN_CENTER_VERTICAL|wxALL, 10 ); + + + bSizer72->Add( 20, 0, 0, 0, 5 ); + + + bSizer24->Add( bSizer72, 0, 0, 5 ); + + wxBoxSizer* bSizer212; + bSizer212 = new wxBoxSizer( wxVERTICAL ); + + m_gaugeProgress = new wxGauge( this, wxID_ANY, 100, wxDefaultPosition, wxDefaultSize, wxGA_HORIZONTAL ); + m_gaugeProgress->SetValue( 0 ); + bSizer212->Add( m_gaugeProgress, 0, wxEXPAND|wxRIGHT|wxLEFT, 5 ); + + m_staticTextDetails = new wxStaticText( this, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextDetails->Wrap( -1 ); + bSizer212->Add( m_staticTextDetails, 0, wxALIGN_CENTER_HORIZONTAL|wxALL, 5 ); + + + bSizer24->Add( bSizer212, 0, wxBOTTOM|wxRIGHT|wxLEFT, 5 ); + + wxStaticLine* m_staticline9; + m_staticline9 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer24->Add( m_staticline9, 0, wxEXPAND, 5 ); + + bSizerStdButtons = new wxBoxSizer( wxHORIZONTAL ); + + m_buttonCancel = new wxButton( this, wxID_CANCEL, _("Cancel"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + + m_buttonCancel->SetDefault(); + bSizerStdButtons->Add( m_buttonCancel, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 ); + + + bSizer24->Add( bSizerStdButtons, 0, wxALIGN_RIGHT, 5 ); + + + this->SetSizer( bSizer24 ); + this->Layout(); + bSizer24->Fit( this ); + + // Connect Events + m_buttonCancel->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DownloadProgressDlgGenerated::onCancel ), NULL, this ); +} + +DownloadProgressDlgGenerated::~DownloadProgressDlgGenerated() +{ +} + +CfgHighlightDlgGenerated::CfgHighlightDlgGenerated( wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style ) : wxDialog( parent, id, title, pos, size, style ) +{ + this->SetSizeHints( wxDefaultSize, wxDefaultSize ); + this->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) ); + + wxBoxSizer* bSizer96; + bSizer96 = new wxBoxSizer( wxVERTICAL ); + + wxPanel* m_panel35; + m_panel35 = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panel35->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer98; + bSizer98 = new wxBoxSizer( wxHORIZONTAL ); + + wxBoxSizer* bSizer238; + bSizer238 = new wxBoxSizer( wxVERTICAL ); + + m_staticTextHighlight = new wxStaticText( m_panel35, wxID_ANY, _("Highlight configurations that have not been run for more than the following number of days:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextHighlight->Wrap( -1 ); + bSizer238->Add( m_staticTextHighlight, 0, wxTOP|wxRIGHT|wxLEFT, 5 ); + + m_spinCtrlOverdueDays = new wxSpinCtrl( m_panel35, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize( -1, -1 ), wxSP_ARROW_KEYS, 0, 2000000000, 0 ); + bSizer238->Add( m_spinCtrlOverdueDays, 0, wxALL|wxALIGN_CENTER_HORIZONTAL, 5 ); + + + bSizer98->Add( bSizer238, 1, wxALL|wxEXPAND, 5 ); + + + m_panel35->SetSizer( bSizer98 ); + m_panel35->Layout(); + bSizer98->Fit( m_panel35 ); + bSizer96->Add( m_panel35, 0, 0, 5 ); + + wxStaticLine* m_staticline21; + m_staticline21 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer96->Add( m_staticline21, 0, wxEXPAND, 5 ); + + bSizerStdButtons = new wxBoxSizer( wxHORIZONTAL ); + + m_buttonOK = new wxButton( this, wxID_OK, _("OK"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + + m_buttonOK->SetDefault(); + m_buttonOK->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + + bSizerStdButtons->Add( m_buttonOK, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 ); + + m_buttonCancel = new wxButton( this, wxID_CANCEL, _("Cancel"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizerStdButtons->Add( m_buttonCancel, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxRIGHT, 5 ); + + + bSizer96->Add( bSizerStdButtons, 0, wxALIGN_RIGHT, 5 ); + + + this->SetSizer( bSizer96 ); + this->Layout(); + bSizer96->Fit( this ); + + // Connect Events + this->Connect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( CfgHighlightDlgGenerated::onClose ) ); + m_buttonOK->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( CfgHighlightDlgGenerated::onOkay ), NULL, this ); + m_buttonCancel->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( CfgHighlightDlgGenerated::onCancel ), NULL, this ); +} + +CfgHighlightDlgGenerated::~CfgHighlightDlgGenerated() +{ +} + +PasswordPromptDlgGenerated::PasswordPromptDlgGenerated( wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style ) : wxDialog( parent, id, title, pos, size, style ) +{ + this->SetSizeHints( wxDefaultSize, wxDefaultSize ); + this->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) ); + + wxBoxSizer* bSizer96; + bSizer96 = new wxBoxSizer( wxVERTICAL ); + + wxPanel* m_panel35; + m_panel35 = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panel35->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer98; + bSizer98 = new wxBoxSizer( wxHORIZONTAL ); + + wxBoxSizer* bSizer238; + bSizer238 = new wxBoxSizer( wxVERTICAL ); + + m_staticTextMain = new wxStaticText( m_panel35, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextMain->Wrap( -1 ); + bSizer238->Add( m_staticTextMain, 1, wxALL, 5 ); + + wxBoxSizer* bSizer305; + bSizer305 = new wxBoxSizer( wxHORIZONTAL ); + + m_staticTextPassword = new wxStaticText( m_panel35, wxID_ANY, _("Password:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextPassword->Wrap( -1 ); + bSizer305->Add( m_staticTextPassword, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 5 ); + + m_textCtrlPasswordVisible = new wxTextCtrl( m_panel35, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer305->Add( m_textCtrlPasswordVisible, 1, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 5 ); + + m_textCtrlPasswordHidden = new wxTextCtrl( m_panel35, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_PASSWORD ); + bSizer305->Add( m_textCtrlPasswordHidden, 1, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 5 ); + + m_checkBoxShowPassword = new wxCheckBox( m_panel35, wxID_ANY, _("&Show password"), wxDefaultPosition, wxDefaultSize, 0 ); + bSizer305->Add( m_checkBoxShowPassword, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 ); + + + bSizer238->Add( bSizer305, 0, wxEXPAND|wxTOP|wxBOTTOM, 5 ); + + bSizerError = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapError = new wxStaticBitmap( m_panel35, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizerError->Add( m_bitmapError, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + + m_staticTextError = new wxStaticText( m_panel35, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextError->Wrap( -1 ); + bSizerError->Add( m_staticTextError, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 ); + + + bSizer238->Add( bSizerError, 0, 0, 5 ); + + + bSizer98->Add( bSizer238, 1, wxALL, 5 ); + + + m_panel35->SetSizer( bSizer98 ); + m_panel35->Layout(); + bSizer98->Fit( m_panel35 ); + bSizer96->Add( m_panel35, 1, wxEXPAND, 5 ); + + wxStaticLine* m_staticline21; + m_staticline21 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer96->Add( m_staticline21, 0, wxEXPAND, 5 ); + + bSizerStdButtons = new wxBoxSizer( wxHORIZONTAL ); + + m_buttonOK = new wxButton( this, wxID_OK, _("OK"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + + m_buttonOK->SetDefault(); + m_buttonOK->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + + bSizerStdButtons->Add( m_buttonOK, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 ); + + m_buttonCancel = new wxButton( this, wxID_CANCEL, _("Cancel"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizerStdButtons->Add( m_buttonCancel, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxRIGHT, 5 ); + + + bSizer96->Add( bSizerStdButtons, 0, wxALIGN_RIGHT, 5 ); + + + this->SetSizer( bSizer96 ); + this->Layout(); + bSizer96->Fit( this ); + + // Connect Events + this->Connect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( PasswordPromptDlgGenerated::onClose ) ); + m_textCtrlPasswordVisible->Connect( wxEVT_COMMAND_TEXT_UPDATED, wxCommandEventHandler( PasswordPromptDlgGenerated::onTypingPassword ), NULL, this ); + m_textCtrlPasswordHidden->Connect( wxEVT_COMMAND_TEXT_UPDATED, wxCommandEventHandler( PasswordPromptDlgGenerated::onTypingPassword ), NULL, this ); + m_checkBoxShowPassword->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( PasswordPromptDlgGenerated::onToggleShowPassword ), NULL, this ); + m_buttonOK->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( PasswordPromptDlgGenerated::onOkay ), NULL, this ); + m_buttonCancel->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( PasswordPromptDlgGenerated::onCancel ), NULL, this ); +} + +PasswordPromptDlgGenerated::~PasswordPromptDlgGenerated() +{ +} + +ActivationDlgGenerated::ActivationDlgGenerated( wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style ) : wxDialog( parent, id, title, pos, size, style ) +{ + this->SetSizeHints( wxSize( -1, -1 ), wxDefaultSize ); + this->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) ); + + wxBoxSizer* bSizer54; + bSizer54 = new wxBoxSizer( wxVERTICAL ); + + wxPanel* m_panel35; + m_panel35 = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panel35->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer172; + bSizer172 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer165; + bSizer165 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapActivation = new wxStaticBitmap( m_panel35, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizer165->Add( m_bitmapActivation, 0, wxALL, 10 ); + + wxBoxSizer* bSizer16; + bSizer16 = new wxBoxSizer( wxVERTICAL ); + + + bSizer16->Add( 0, 10, 0, 0, 5 ); + + m_richTextLastError = new wxRichTextCtrl( m_panel35, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_READONLY|wxBORDER_NONE|wxVSCROLL|wxWANTS_CHARS ); + bSizer16->Add( m_richTextLastError, 1, wxEXPAND, 5 ); + + + bSizer165->Add( bSizer16, 1, wxEXPAND, 5 ); + + + bSizer172->Add( bSizer165, 1, wxEXPAND, 5 ); + + wxStaticLine* m_staticline82; + m_staticline82 = new wxStaticLine( m_panel35, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer172->Add( m_staticline82, 0, wxEXPAND, 5 ); + + m_staticTextMain = new wxStaticText( m_panel35, wxID_ANY, _("Activate FreeFileSync by one of the following methods:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextMain->Wrap( -1 ); + bSizer172->Add( m_staticTextMain, 0, wxALL, 10 ); + + + m_panel35->SetSizer( bSizer172 ); + m_panel35->Layout(); + bSizer172->Fit( m_panel35 ); + bSizer54->Add( m_panel35, 1, wxEXPAND, 5 ); + + wxStaticLine* m_staticline181; + m_staticline181 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer54->Add( m_staticline181, 0, wxEXPAND|wxBOTTOM, 5 ); + + wxStaticLine* m_staticline18111; + m_staticline18111 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer54->Add( m_staticline18111, 0, wxEXPAND|wxTOP, 5 ); + + wxPanel* m_panel3511; + m_panel3511 = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panel3511->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer263; + bSizer263 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer234; + bSizer234 = new wxBoxSizer( wxHORIZONTAL ); + + m_staticTextMain1 = new wxStaticText( m_panel3511, wxID_ANY, _("1."), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextMain1->Wrap( -1 ); + bSizer234->Add( m_staticTextMain1, 0, wxRIGHT|wxALIGN_CENTER_VERTICAL, 5 ); + + wxStaticText* m_staticText136; + m_staticText136 = new wxStaticText( m_panel3511, wxID_ANY, _("Activate via internet now:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText136->Wrap( -1 ); + bSizer234->Add( m_staticText136, 1, wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 ); + + m_buttonActivateOnline = new wxButton( m_panel3511, wxID_ANY, _("Activate online"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + + m_buttonActivateOnline->SetDefault(); + m_buttonActivateOnline->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + + bSizer234->Add( m_buttonActivateOnline, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer263->Add( bSizer234, 0, wxEXPAND|wxALL, 10 ); + + + m_panel3511->SetSizer( bSizer263 ); + m_panel3511->Layout(); + bSizer263->Fit( m_panel3511 ); + bSizer54->Add( m_panel3511, 0, wxEXPAND, 5 ); + + wxStaticLine* m_staticline181111; + m_staticline181111 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer54->Add( m_staticline181111, 0, wxEXPAND|wxBOTTOM, 5 ); + + wxStaticLine* m_staticline181112; + m_staticline181112 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer54->Add( m_staticline181112, 0, wxEXPAND|wxTOP, 5 ); + + wxPanel* m_panel351; + m_panel351 = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panel351->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer266; + bSizer266 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer237; + bSizer237 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer236; + bSizer236 = new wxBoxSizer( wxHORIZONTAL ); + + wxStaticText* m_staticText175; + m_staticText175 = new wxStaticText( m_panel351, wxID_ANY, _("2."), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText175->Wrap( -1 ); + bSizer236->Add( m_staticText175, 0, wxRIGHT|wxALIGN_BOTTOM, 5 ); + + wxStaticText* m_staticText1361; + m_staticText1361 = new wxStaticText( m_panel351, wxID_ANY, _("Retrieve an offline activation key from the following URL:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText1361->Wrap( -1 ); + bSizer236->Add( m_staticText1361, 1, wxRIGHT|wxALIGN_BOTTOM, 5 ); + + m_buttonCopyUrl = new wxButton( m_panel351, wxID_ANY, _("&Copy to clipboard"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizer236->Add( m_buttonCopyUrl, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer237->Add( bSizer236, 0, wxEXPAND|wxBOTTOM, 5 ); + + m_richTextManualActivationUrl = new wxRichTextCtrl( m_panel351, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_READONLY|wxBORDER_NONE|wxVSCROLL|wxWANTS_CHARS ); + bSizer237->Add( m_richTextManualActivationUrl, 0, wxEXPAND|wxBOTTOM, 5 ); + + wxBoxSizer* bSizer235; + bSizer235 = new wxBoxSizer( wxHORIZONTAL ); + + wxStaticText* m_staticText13611; + m_staticText13611 = new wxStaticText( m_panel351, wxID_ANY, _("Enter activation key:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText13611->Wrap( -1 ); + bSizer235->Add( m_staticText13611, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 ); + + m_textCtrlOfflineActivationKey = new wxTextCtrl( m_panel351, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize( -1, -1 ), wxTE_PROCESS_ENTER ); + bSizer235->Add( m_textCtrlOfflineActivationKey, 1, wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 ); + + m_buttonActivateOffline = new wxButton( m_panel351, wxID_ANY, _("Activate offline"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_buttonActivateOffline->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + + bSizer235->Add( m_buttonActivateOffline, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer237->Add( bSizer235, 0, wxEXPAND|wxTOP, 5 ); + + + bSizer266->Add( bSizer237, 0, wxALL|wxEXPAND, 10 ); + + + m_panel351->SetSizer( bSizer266 ); + m_panel351->Layout(); + bSizer266->Fit( m_panel351 ); + bSizer54->Add( m_panel351, 0, wxEXPAND, 5 ); + + wxStaticLine* m_staticline13; + m_staticline13 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer54->Add( m_staticline13, 0, wxEXPAND, 5 ); + + bSizerStdButtons = new wxBoxSizer( wxHORIZONTAL ); + + m_buttonCancel = new wxButton( this, wxID_CANCEL, _("Cancel"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizerStdButtons->Add( m_buttonCancel, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxRIGHT, 5 ); + + + bSizer54->Add( bSizerStdButtons, 0, wxALIGN_RIGHT, 5 ); + + + this->SetSizer( bSizer54 ); + this->Layout(); + bSizer54->Fit( this ); + + // Connect Events + this->Connect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( ActivationDlgGenerated::onClose ) ); + m_buttonActivateOnline->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ActivationDlgGenerated::onActivateOnline ), NULL, this ); + m_buttonCopyUrl->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ActivationDlgGenerated::onCopyUrl ), NULL, this ); + m_textCtrlOfflineActivationKey->Connect( wxEVT_COMMAND_TEXT_ENTER, wxCommandEventHandler( ActivationDlgGenerated::onOfflineActivationEnter ), NULL, this ); + m_buttonActivateOffline->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ActivationDlgGenerated::onActivateOffline ), NULL, this ); + m_buttonCancel->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ActivationDlgGenerated::onCancel ), NULL, this ); +} + +ActivationDlgGenerated::~ActivationDlgGenerated() +{ +} + +WarnAccessRightsMissingDlgGenerated::WarnAccessRightsMissingDlgGenerated( wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style ) : wxDialog( parent, id, title, pos, size, style ) +{ + this->SetSizeHints( wxSize( -1, -1 ), wxDefaultSize ); + this->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) ); + + wxBoxSizer* bSizer330; + bSizer330 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapGrantAccess = new wxStaticBitmap( this, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer330->Add( m_bitmapGrantAccess, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 10 ); + + wxBoxSizer* bSizer95; + bSizer95 = new wxBoxSizer( wxVERTICAL ); + + m_staticTextDescr = new wxStaticText( this, wxID_ANY, _("FreeFileSync requires access rights to avoid \"Operation not permitted\" errors when synchronizing your data (e.g. Mail, Messages, Calendars)."), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_staticTextDescr->Wrap( -1 ); + bSizer95->Add( m_staticTextDescr, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL|wxALL, 10 ); + + wxStaticLine* m_staticline20; + m_staticline20 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer95->Add( m_staticline20, 0, wxEXPAND, 5 ); + + wxPanel* m_panel39; + m_panel39 = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panel39->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer166; + bSizer166 = new wxBoxSizer( wxVERTICAL ); + + wxFlexGridSizer* ffgSizer11; + ffgSizer11 = new wxFlexGridSizer( 0, 2, 5, 5 ); + ffgSizer11->SetFlexibleDirection( wxBOTH ); + ffgSizer11->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); + + m_staticTextStep1 = new wxStaticText( m_panel39, wxID_ANY, _("1."), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextStep1->Wrap( -1 ); + ffgSizer11->Add( m_staticTextStep1, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 ); + + m_buttonLocateBundle = new wxButton( m_panel39, wxID_ANY, _("Locate the FreeFileSync app"), wxDefaultPosition, wxDefaultSize, 0 ); + ffgSizer11->Add( m_buttonLocateBundle, 0, wxALIGN_CENTER_VERTICAL|wxEXPAND, 5 ); + + m_staticTextStep2 = new wxStaticText( m_panel39, wxID_ANY, _("2."), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextStep2->Wrap( -1 ); + ffgSizer11->Add( m_staticTextStep2, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 ); + + m_buttonOpenSecurity = new wxButton( m_panel39, wxID_ANY, _("Open Security && Privacy"), wxDefaultPosition, wxDefaultSize, 0 ); + ffgSizer11->Add( m_buttonOpenSecurity, 0, wxALIGN_CENTER_VERTICAL|wxEXPAND, 5 ); + + m_staticTextStep3 = new wxStaticText( m_panel39, wxID_ANY, _("3."), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextStep3->Wrap( -1 ); + ffgSizer11->Add( m_staticTextStep3, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 ); + + m_staticTextGrantAccess = new wxStaticText( m_panel39, wxID_ANY, _("Drag FreeFileSync into the panel."), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextGrantAccess->Wrap( -1 ); + ffgSizer11->Add( m_staticTextGrantAccess, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer166->Add( ffgSizer11, 0, wxALL|wxALIGN_CENTER_HORIZONTAL, 10 ); + + + m_panel39->SetSizer( bSizer166 ); + m_panel39->Layout(); + bSizer166->Fit( m_panel39 ); + bSizer95->Add( m_panel39, 1, wxEXPAND, 5 ); + + wxStaticLine* m_staticline36; + m_staticline36 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer95->Add( m_staticline36, 0, wxEXPAND, 5 ); + + m_checkBoxDontShowAgain = new wxCheckBox( this, wxID_ANY, _("&Don't show this dialog again"), wxDefaultPosition, wxDefaultSize, 0 ); + bSizer95->Add( m_checkBoxDontShowAgain, 0, wxALIGN_CENTER_HORIZONTAL|wxALL, 5 ); + + bSizerStdButtons = new wxBoxSizer( wxHORIZONTAL ); + + m_buttonClose = new wxButton( this, wxID_OK, _("Close"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + + m_buttonClose->SetDefault(); + bSizerStdButtons->Add( m_buttonClose, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 ); + + + bSizer95->Add( bSizerStdButtons, 0, wxALIGN_RIGHT, 5 ); + + + bSizer330->Add( bSizer95, 1, wxEXPAND, 5 ); + + + this->SetSizer( bSizer330 ); + this->Layout(); + bSizer330->Fit( this ); + + // Connect Events + this->Connect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( WarnAccessRightsMissingDlgGenerated::onClose ) ); + m_buttonLocateBundle->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( WarnAccessRightsMissingDlgGenerated::onShowAppBundle ), NULL, this ); + m_buttonOpenSecurity->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( WarnAccessRightsMissingDlgGenerated::onOpenSecuritySettings ), NULL, this ); + m_checkBoxDontShowAgain->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( WarnAccessRightsMissingDlgGenerated::onCheckBoxClick ), NULL, this ); + m_buttonClose->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( WarnAccessRightsMissingDlgGenerated::onOkay ), NULL, this ); +} + +WarnAccessRightsMissingDlgGenerated::~WarnAccessRightsMissingDlgGenerated() +{ +} diff --git a/FreeFileSync/Source/ui/gui_generated.h b/FreeFileSync/Source/ui/gui_generated.h new file mode 100644 index 0000000..25152be --- /dev/null +++ b/FreeFileSync/Source/ui/gui_generated.h @@ -0,0 +1,1275 @@ +/////////////////////////////////////////////////////////////////////////// +// C++ code generated with wxFormBuilder (version 3.10.1-0-g8feb16b3) +// http://www.wxformbuilder.org/ +// +// PLEASE DO *NOT* EDIT THIS FILE! +/////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +namespace zen { class BitmapTextButton; } +namespace zen { class ToggleButton; } + +#include "wx+/bitmap_button.h" +#include "folder_history_box.h" +#include "wx+/grid.h" +#include "triple_splitter.h" +#include "wx+/toggle_button.h" +#include "command_box.h" +#include "wx+/graph.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "zen/i18n.h" + +/////////////////////////////////////////////////////////////////////////// + + +/////////////////////////////////////////////////////////////////////////////// +/// Class MainDialogGenerated +/////////////////////////////////////////////////////////////////////////////// +class MainDialogGenerated : public wxFrame +{ +private: + +protected: + wxMenuBar* m_menubar; + wxMenu* m_menuFile; + wxMenuItem* m_menuItemNew; + wxMenuItem* m_menuItemLoad; + wxMenuItem* m_menuItemSave; + wxMenuItem* m_menuItemSaveAs; + wxMenuItem* m_menuItemSaveAsBatch; + wxMenuItem* m_menuItemQuit; + wxMenu* m_menuActions; + wxMenuItem* m_menuItemShowLog; + wxMenuItem* m_menuItemCompare; + wxMenuItem* m_menuItemCompSettings; + wxMenuItem* m_menuItemFilter; + wxMenuItem* m_menuItemSyncSettings; + wxMenuItem* m_menuItemSynchronize; + wxMenu* m_menuTools; + wxMenuItem* m_menuItemOptions; + wxMenu* m_menuLanguages; + wxMenuItem* m_menuItemFind; + wxMenuItem* m_menuItemExportList; + wxMenuItem* m_menuItemResetLayout; + wxMenuItem* m_menuItemShowMain; + wxMenuItem* m_menuItemShowFolders; + wxMenuItem* m_menuItemShowViewFilter; + wxMenuItem* m_menuItemShowConfig; + wxMenuItem* m_menuItemShowOverview; + wxMenu* m_menuHelp; + wxMenuItem* m_menuItemHelp; + wxMenuItem* m_menuItemCheckVersionNow; + wxMenuItem* m_menuItemAbout; + wxBoxSizer* bSizerPanelHolder; + wxPanel* m_panelTopButtons; + wxBoxSizer* bSizerTopButtons; + wxButton* m_buttonCancel; + zen::BitmapTextButton* m_buttonCompare; + wxBitmapButton* m_bpButtonCmpConfig; + wxBitmapButton* m_bpButtonCmpContext; + wxBitmapButton* m_bpButtonFilter; + wxBitmapButton* m_bpButtonFilterContext; + wxBitmapButton* m_bpButtonSyncConfig; + wxBitmapButton* m_bpButtonSyncContext; + zen::BitmapTextButton* m_buttonSync; + wxPanel* m_panelDirectoryPairs; + wxPanel* m_panelTopLeft; + wxStaticText* m_staticTextResolvedPathL; + wxBitmapButton* m_bpButtonAddPair; + fff::FolderHistoryBox* m_folderPathLeft; + wxButton* m_buttonSelectFolderLeft; + wxBitmapButton* m_bpButtonSelectAltFolderLeft; + wxPanel* m_panelTopCenter; + wxBitmapButton* m_bpButtonSwapSides; + wxPanel* m_panelTopRight; + wxStaticText* m_staticTextResolvedPathR; + fff::FolderHistoryBox* m_folderPathRight; + wxButton* m_buttonSelectFolderRight; + wxBitmapButton* m_bpButtonSelectAltFolderRight; + wxScrolledWindow* m_scrolledWindowFolderPairs; + wxBoxSizer* bSizerAddFolderPairs; + zen::Grid* m_gridOverview; + wxPanel* m_panelCenter; + fff::TripleSplitter* m_splitterMain; + zen::Grid* m_gridMainL; + zen::Grid* m_gridMainC; + zen::Grid* m_gridMainR; + wxPanel* m_panelStatusBar; + wxBoxSizer* bSizerStatusLeftDirectories; + wxStaticBitmap* m_bitmapSmallDirectoryLeft; + wxStaticText* m_staticTextStatusLeftDirs; + wxBoxSizer* bSizerStatusLeftFiles; + wxStaticBitmap* m_bitmapSmallFileLeft; + wxStaticText* m_staticTextStatusLeftFiles; + wxStaticText* m_staticTextStatusLeftBytes; + wxStaticText* m_staticTextStatusCenter; + wxBoxSizer* bSizerStatusRightDirectories; + wxStaticBitmap* m_bitmapSmallDirectoryRight; + wxStaticText* m_staticTextStatusRightDirs; + wxBoxSizer* bSizerStatusRightFiles; + wxStaticBitmap* m_bitmapSmallFileRight; + wxStaticText* m_staticTextStatusRightFiles; + wxStaticText* m_staticTextStatusRightBytes; + wxPanel* m_panelSearch; + wxBitmapButton* m_bpButtonHideSearch; + wxTextCtrl* m_textCtrlSearchTxt; + wxCheckBox* m_checkBoxMatchCase; + wxPanel* m_panelLog; + wxBoxSizer* bSizerLog; + wxStaticBitmap* m_bitmapSyncResult; + wxStaticText* m_staticTextSyncResult; + wxStaticText* m_staticTextProcessed; + wxStaticText* m_staticTextRemaining; + wxPanel* m_panelItemStats; + wxStaticBitmap* m_bitmapItemStat; + wxStaticText* m_staticTextItemsProcessed; + wxStaticText* m_staticTextBytesProcessed; + wxStaticText* m_staticTextItemsRemaining; + wxStaticText* m_staticTextBytesRemaining; + wxPanel* m_panelTimeStats; + wxStaticBitmap* m_bitmapTimeStat; + wxStaticText* m_staticTextTimeElapsed; + wxPanel* m_panelConfig; + wxBoxSizer* bSizerConfig; + wxBoxSizer* bSizerCfgHistoryButtons; + wxBitmapButton* m_bpButtonNew; + wxBitmapButton* m_bpButtonOpen; + wxBitmapButton* m_bpButtonSave; + wxBoxSizer* bSizerSaveAs; + wxBitmapButton* m_bpButtonSaveAs; + wxBitmapButton* m_bpButtonSaveAsBatch; + zen::Grid* m_gridCfgHistory; + wxPanel* m_panelViewFilter; + wxBoxSizer* bSizerViewFilter; + zen::ToggleButton* m_bpButtonToggleLog; + wxBoxSizer* bSizerViewButtons; + zen::ToggleButton* m_bpButtonViewType; + zen::ToggleButton* m_bpButtonShowExcluded; + zen::ToggleButton* m_bpButtonShowDeleteLeft; + zen::ToggleButton* m_bpButtonShowUpdateLeft; + zen::ToggleButton* m_bpButtonShowCreateLeft; + zen::ToggleButton* m_bpButtonShowLeftOnly; + zen::ToggleButton* m_bpButtonShowLeftNewer; + zen::ToggleButton* m_bpButtonShowEqual; + zen::ToggleButton* m_bpButtonShowDoNothing; + zen::ToggleButton* m_bpButtonShowDifferent; + zen::ToggleButton* m_bpButtonShowRightNewer; + zen::ToggleButton* m_bpButtonShowRightOnly; + zen::ToggleButton* m_bpButtonShowCreateRight; + zen::ToggleButton* m_bpButtonShowUpdateRight; + zen::ToggleButton* m_bpButtonShowDeleteRight; + zen::ToggleButton* m_bpButtonShowConflict; + wxBitmapButton* m_bpButtonViewFilterContext; + wxPanel* m_panelStatistics; + wxBoxSizer* bSizerStatistics; + wxStaticBitmap* m_bitmapDeleteLeft; + wxStaticText* m_staticTextDeleteLeft; + wxStaticBitmap* m_bitmapUpdateLeft; + wxStaticText* m_staticTextUpdateLeft; + wxStaticBitmap* m_bitmapCreateLeft; + wxStaticText* m_staticTextCreateLeft; + wxStaticBitmap* m_bitmapData; + wxStaticText* m_staticTextData; + wxStaticBitmap* m_bitmapCreateRight; + wxStaticText* m_staticTextCreateRight; + wxStaticBitmap* m_bitmapUpdateRight; + wxStaticText* m_staticTextUpdateRight; + wxStaticBitmap* m_bitmapDeleteRight; + wxStaticText* m_staticTextDeleteRight; + + // Virtual event handlers, override them in your derived class + virtual void onClose( wxCloseEvent& event ) { event.Skip(); } + virtual void onConfigNew( wxCommandEvent& event ) { event.Skip(); } + virtual void onConfigLoad( wxCommandEvent& event ) { event.Skip(); } + virtual void onConfigSave( wxCommandEvent& event ) { event.Skip(); } + virtual void onConfigSaveAs( wxCommandEvent& event ) { event.Skip(); } + virtual void onSaveAsBatchJob( wxCommandEvent& event ) { event.Skip(); } + virtual void onMenuQuit( wxCommandEvent& event ) { event.Skip(); } + virtual void onToggleLog( wxCommandEvent& event ) { event.Skip(); } + virtual void onCompare( wxCommandEvent& event ) { event.Skip(); } + virtual void onCmpSettings( wxCommandEvent& event ) { event.Skip(); } + virtual void onConfigureFilter( wxCommandEvent& event ) { event.Skip(); } + virtual void onSyncSettings( wxCommandEvent& event ) { event.Skip(); } + virtual void onStartSync( wxCommandEvent& event ) { event.Skip(); } + virtual void onMenuOptions( wxCommandEvent& event ) { event.Skip(); } + virtual void onMenuFindItem( wxCommandEvent& event ) { event.Skip(); } + virtual void onMenuExportFileList( wxCommandEvent& event ) { event.Skip(); } + virtual void onMenuResetLayout( wxCommandEvent& event ) { event.Skip(); } + virtual void onShowHelp( wxCommandEvent& event ) { event.Skip(); } + virtual void onMenuCheckVersion( wxCommandEvent& event ) { event.Skip(); } + virtual void onMenuAbout( wxCommandEvent& event ) { event.Skip(); } + virtual void onCompSettingsContextMouse( wxMouseEvent& event ) { event.Skip(); } + virtual void onCompSettingsContext( wxCommandEvent& event ) { event.Skip(); } + virtual void onGlobalFilterContextMouse( wxMouseEvent& event ) { event.Skip(); } + virtual void onGlobalFilterContext( wxCommandEvent& event ) { event.Skip(); } + virtual void onSyncSettingsContextMouse( wxMouseEvent& event ) { event.Skip(); } + virtual void onSyncSettingsContext( wxCommandEvent& event ) { event.Skip(); } + virtual void onTopFolderPairAdd( wxCommandEvent& event ) { event.Skip(); } + virtual void onTopFolderPairRemove( wxCommandEvent& event ) { event.Skip(); } + virtual void onSwapSides( wxCommandEvent& event ) { event.Skip(); } + virtual void onTopLocalCompCfg( wxCommandEvent& event ) { event.Skip(); } + virtual void onTopLocalFilterCfg( wxCommandEvent& event ) { event.Skip(); } + virtual void onTopLocalSyncCfg( wxCommandEvent& event ) { event.Skip(); } + virtual void onHideSearchPanel( wxCommandEvent& event ) { event.Skip(); } + virtual void onSearchGridEnter( wxCommandEvent& event ) { event.Skip(); } + virtual void onToggleViewType( wxCommandEvent& event ) { event.Skip(); } + virtual void onViewTypeContextMouse( wxMouseEvent& event ) { event.Skip(); } + virtual void onToggleViewButton( wxCommandEvent& event ) { event.Skip(); } + virtual void onViewFilterContextMouse( wxMouseEvent& event ) { event.Skip(); } + virtual void onViewFilterContext( wxCommandEvent& event ) { event.Skip(); } + + +public: + wxBitmapButton* m_bpButtonRemovePair; + wxBitmapButton* m_bpButtonLocalCompCfg; + wxBitmapButton* m_bpButtonLocalFilter; + wxBitmapButton* m_bpButtonLocalSyncCfg; + + MainDialogGenerated( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = _("dummy"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize( -1, -1 ), long style = wxDEFAULT_FRAME_STYLE|wxTAB_TRAVERSAL ); + + ~MainDialogGenerated(); + +}; + +/////////////////////////////////////////////////////////////////////////////// +/// Class FolderPairPanelGenerated +/////////////////////////////////////////////////////////////////////////////// +class FolderPairPanelGenerated : public wxPanel +{ +private: + +protected: + wxButton* m_buttonSelectFolderLeft; + wxBitmapButton* m_bpButtonSelectAltFolderLeft; + wxPanel* m_panelRight; + wxButton* m_buttonSelectFolderRight; + wxBitmapButton* m_bpButtonSelectAltFolderRight; + +public: + wxPanel* m_panelLeft; + wxBitmapButton* m_bpButtonFolderPairOptions; + wxBitmapButton* m_bpButtonRemovePair; + fff::FolderHistoryBox* m_folderPathLeft; + wxBitmapButton* m_bpButtonLocalCompCfg; + wxBitmapButton* m_bpButtonLocalFilter; + wxBitmapButton* m_bpButtonLocalSyncCfg; + fff::FolderHistoryBox* m_folderPathRight; + + FolderPairPanelGenerated( wxWindow* parent, wxWindowID id = wxID_ANY, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize( 698, 67 ), long style = 0, const wxString& name = wxEmptyString ); + + ~FolderPairPanelGenerated(); + +}; + +/////////////////////////////////////////////////////////////////////////////// +/// Class ConfigDlgGenerated +/////////////////////////////////////////////////////////////////////////////// +class ConfigDlgGenerated : public wxDialog +{ +private: + +protected: + wxStaticText* m_staticTextFolderPairLabel; + wxListBox* m_listBoxFolderPair; + wxNotebook* m_notebook; + wxPanel* m_panelCompSettingsTab; + wxBoxSizer* bSizerHeaderCompSettings; + wxStaticText* m_staticTextMainCompSettings; + wxCheckBox* m_checkBoxUseLocalCmpOptions; + wxStaticLine* m_staticlineCompHeader; + wxPanel* m_panelComparisonSettings; + zen::ToggleButton* m_buttonByTimeSize; + zen::ToggleButton* m_buttonByContent; + zen::ToggleButton* m_buttonBySize; + wxStaticBitmap* m_bitmapCompVariant; + wxStaticText* m_staticTextCompVarDescription; + wxCheckBox* m_checkBoxSymlinksInclude; + wxRadioButton* m_radioBtnSymlinksFollow; + wxRadioButton* m_radioBtnSymlinksDirect; + wxTextCtrl* m_textCtrlTimeShift; + wxBoxSizer* bSizerCompMisc; + wxStaticBitmap* m_bitmapIgnoreErrors; + wxCheckBox* m_checkBoxIgnoreErrors; + wxStaticBitmap* m_bitmapRetryErrors; + wxCheckBox* m_checkBoxAutoRetry; + wxFlexGridSizer* fgSizerAutoRetry; + wxStaticText* m_staticTextAutoRetryDelay; + wxSpinCtrl* m_spinCtrlAutoRetryCount; + wxSpinCtrl* m_spinCtrlAutoRetryDelay; + wxBoxSizer* bSizerPerformance; + wxPanel* m_panel57; + wxStaticBitmap* m_bitmapPerf; + wxHyperlinkCtrl* m_hyperlinkPerfDeRequired; + wxBoxSizer* bSizer260; + wxStaticText* m_staticTextPerfParallelOps; + wxScrolledWindow* m_scrolledWindowPerf; + wxFlexGridSizer* fgSizerPerf; + wxPanel* m_panelFilterSettingsTab; + wxBoxSizer* bSizerHeaderFilterSettings; + wxStaticText* m_staticTextMainFilterSettings; + wxStaticText* m_staticTextLocalFilterSettings; + wxStaticLine* m_staticlineFilterHeader; + wxStaticBitmap* m_bitmapInclude; + wxTextCtrl* m_textCtrlInclude; + wxStaticBitmap* m_bitmapExclude; + wxTextCtrl* m_textCtrlExclude; + wxStaticBitmap* m_bitmapFilterSize; + wxSpinCtrl* m_spinCtrlMinSize; + wxChoice* m_choiceUnitMinSize; + wxSpinCtrl* m_spinCtrlMaxSize; + wxChoice* m_choiceUnitMaxSize; + wxStaticBitmap* m_bitmapFilterDate; + wxChoice* m_choiceUnitTimespan; + wxSpinCtrl* m_spinCtrlTimespan; + wxStaticText* m_staticTextFilterDescr; + wxButton* m_buttonDefault; + wxBitmapButton* m_bpButtonDefaultContext; + wxButton* m_buttonClear; + wxPanel* m_panelSyncSettingsTab; + wxBoxSizer* bSizerHeaderSyncSettings; + wxStaticText* m_staticTextMainSyncSettings; + wxCheckBox* m_checkBoxUseLocalSyncOptions; + wxStaticLine* m_staticlineSyncHeader; + wxPanel* m_panelSyncSettings; + zen::ToggleButton* m_buttonTwoWay; + zen::ToggleButton* m_buttonMirror; + zen::ToggleButton* m_buttonUpdate; + zen::ToggleButton* m_buttonCustom; + wxStaticBitmap* m_bitmapDatabase; + wxCheckBox* m_checkBoxUseDatabase; + wxStaticText* m_staticTextSyncVarDescription; + wxStaticBitmap* m_bitmapMoveLeft; + wxStaticBitmap* m_bitmapMoveRight; + wxStaticText* m_staticTextDetectMove; + wxBoxSizer* bSizerSyncDirHolder; + wxBoxSizer* bSizerSyncDirsDiff; + wxStaticBitmap* m_bitmapLeftOnly; + wxStaticBitmap* m_bitmapLeftNewer; + wxStaticBitmap* m_bitmapDifferent; + wxStaticBitmap* m_bitmapRightNewer; + wxStaticBitmap* m_bitmapRightOnly; + wxBitmapButton* m_bpButtonLeftOnly; + wxBitmapButton* m_bpButtonLeftNewer; + wxBitmapButton* m_bpButtonDifferent; + wxBitmapButton* m_bpButtonRightNewer; + wxBitmapButton* m_bpButtonRightOnly; + wxBoxSizer* bSizerSyncDirsChanges; + wxBitmapButton* m_bpButtonLeftCreate; + wxBitmapButton* m_bpButtonRightCreate; + wxBitmapButton* m_bpButtonLeftUpdate; + wxBitmapButton* m_bpButtonRightUpdate; + wxBitmapButton* m_bpButtonLeftDelete; + wxBitmapButton* m_bpButtonRightDelete; + zen::ToggleButton* m_buttonRecycler; + zen::ToggleButton* m_buttonPermanent; + zen::ToggleButton* m_buttonVersioning; + wxBoxSizer* bSizerVersioningHolder; + wxStaticBitmap* m_bitmapDeletionType; + wxStaticText* m_staticTextDeletionTypeDescription; + wxPanel* m_panelVersioning; + wxStaticBitmap* m_bitmapVersioning; + fff::FolderHistoryBox* m_versioningFolderPath; + wxButton* m_buttonSelectVersioningFolder; + wxBitmapButton* m_bpButtonSelectVersioningAltFolder; + wxChoice* m_choiceVersioningStyle; + wxStaticText* m_staticTextNamingCvtPart1; + wxStaticText* m_staticTextNamingCvtPart2Bold; + wxStaticText* m_staticTextNamingCvtPart3; + wxStaticText* m_staticTextLimitVersions; + wxCheckBox* m_checkBoxVersionMaxDays; + wxCheckBox* m_checkBoxVersionCountMin; + wxCheckBox* m_checkBoxVersionCountMax; + wxSpinCtrl* m_spinCtrlVersionMaxDays; + wxSpinCtrl* m_spinCtrlVersionCountMin; + wxSpinCtrl* m_spinCtrlVersionCountMax; + wxBoxSizer* bSizerSyncMisc; + wxStaticBitmap* m_bitmapEmail; + wxCheckBox* m_checkBoxSendEmail; + fff::CommandBox* m_comboBoxEmail; + wxBitmapButton* m_bpButtonEmailAlways; + wxBitmapButton* m_bpButtonEmailErrorWarning; + wxBitmapButton* m_bpButtonEmailErrorOnly; + wxHyperlinkCtrl* m_hyperlinkPerfDeRequired2; + wxPanel* m_panelLogfile; + wxStaticBitmap* m_bitmapLogFile; + wxCheckBox* m_checkBoxOverrideLogPath; + wxBitmapButton* m_bpButtonShowLogFolder; + fff::FolderHistoryBox* m_logFolderPath; + wxButton* m_buttonSelectLogFolder; + wxBitmapButton* m_bpButtonSelectAltLogFolder; + wxStaticText* m_staticTextPostSync; + wxChoice* m_choicePostSyncCondition; + fff::CommandBox* m_comboBoxPostSyncCommand; + wxPanel* m_panelNotes; + wxStaticBitmap* m_bitmapNotes; + wxTextCtrl* m_textCtrNotes; + wxBoxSizer* bSizerStdButtons; + zen::BitmapTextButton* m_buttonAddNotes; + wxButton* m_buttonOK; + wxButton* m_buttonCancel; + + // Virtual event handlers, override them in your derived class + virtual void onClose( wxCloseEvent& event ) { event.Skip(); } + virtual void onListBoxKeyEvent( wxKeyEvent& event ) { event.Skip(); } + virtual void onSelectFolderPair( wxCommandEvent& event ) { event.Skip(); } + virtual void onToggleLocalCompSettings( wxCommandEvent& event ) { event.Skip(); } + virtual void onCompByTimeSize( wxCommandEvent& event ) { event.Skip(); } + virtual void onCompByTimeSizeDouble( wxMouseEvent& event ) { event.Skip(); } + virtual void onCompByContent( wxCommandEvent& event ) { event.Skip(); } + virtual void onCompByContentDouble( wxMouseEvent& event ) { event.Skip(); } + virtual void onCompBySize( wxCommandEvent& event ) { event.Skip(); } + virtual void onCompBySizeDouble( wxMouseEvent& event ) { event.Skip(); } + virtual void onChangeCompOption( wxCommandEvent& event ) { event.Skip(); } + virtual void onToggleIgnoreErrors( wxCommandEvent& event ) { event.Skip(); } + virtual void onToggleAutoRetry( wxCommandEvent& event ) { event.Skip(); } + virtual void onChangeFilterOption( wxCommandEvent& event ) { event.Skip(); } + virtual void onFilterDefault( wxCommandEvent& event ) { event.Skip(); } + virtual void onFilterDefaultContextMouse( wxMouseEvent& event ) { event.Skip(); } + virtual void onFilterDefaultContext( wxCommandEvent& event ) { event.Skip(); } + virtual void onFilterClear( wxCommandEvent& event ) { event.Skip(); } + virtual void onToggleLocalSyncSettings( wxCommandEvent& event ) { event.Skip(); } + virtual void onSyncTwoWay( wxCommandEvent& event ) { event.Skip(); } + virtual void onSyncTwoWayDouble( wxMouseEvent& event ) { event.Skip(); } + virtual void onSyncMirror( wxCommandEvent& event ) { event.Skip(); } + virtual void onSyncMirrorDouble( wxMouseEvent& event ) { event.Skip(); } + virtual void onSyncUpdate( wxCommandEvent& event ) { event.Skip(); } + virtual void onSyncUpdateDouble( wxMouseEvent& event ) { event.Skip(); } + virtual void onSyncCustom( wxCommandEvent& event ) { event.Skip(); } + virtual void onSyncCustomDouble( wxMouseEvent& event ) { event.Skip(); } + virtual void onToggleUseDatabase( wxCommandEvent& event ) { event.Skip(); } + virtual void onLeftOnly( wxCommandEvent& event ) { event.Skip(); } + virtual void onLeftNewer( wxCommandEvent& event ) { event.Skip(); } + virtual void onDifferent( wxCommandEvent& event ) { event.Skip(); } + virtual void onRightNewer( wxCommandEvent& event ) { event.Skip(); } + virtual void onRightOnly( wxCommandEvent& event ) { event.Skip(); } + virtual void onLeftCreate( wxCommandEvent& event ) { event.Skip(); } + virtual void onRightCreate( wxCommandEvent& event ) { event.Skip(); } + virtual void onLeftUpdate( wxCommandEvent& event ) { event.Skip(); } + virtual void onRightUpdate( wxCommandEvent& event ) { event.Skip(); } + virtual void onLeftDelete( wxCommandEvent& event ) { event.Skip(); } + virtual void onRightDelete( wxCommandEvent& event ) { event.Skip(); } + virtual void onDeletionRecycler( wxCommandEvent& event ) { event.Skip(); } + virtual void onDeletionPermanent( wxCommandEvent& event ) { event.Skip(); } + virtual void onDeletionVersioning( wxCommandEvent& event ) { event.Skip(); } + virtual void onChangeVersioningStyle( wxCommandEvent& event ) { event.Skip(); } + virtual void onToggleVersioningLimit( wxCommandEvent& event ) { event.Skip(); } + virtual void onToggleMiscEmail( 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 onToggleMiscOption( wxCommandEvent& event ) { event.Skip(); } + virtual void onShowLogFolder( wxCommandEvent& event ) { event.Skip(); } + virtual void onAddNotes( wxCommandEvent& event ) { event.Skip(); } + virtual void onOkay( wxCommandEvent& event ) { event.Skip(); } + virtual void onCancel( wxCommandEvent& event ) { event.Skip(); } + + +public: + + ConfigDlgGenerated( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = _("Synchronization Settings"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize( -1, -1 ), long style = wxDEFAULT_DIALOG_STYLE|wxMAXIMIZE_BOX|wxRESIZE_BORDER ); + + ~ConfigDlgGenerated(); + +}; + +/////////////////////////////////////////////////////////////////////////////// +/// Class CloudSetupDlgGenerated +/////////////////////////////////////////////////////////////////////////////// +class CloudSetupDlgGenerated : public wxDialog +{ +private: + +protected: + wxStaticBitmap* m_bitmapCloud; + wxToggleButton* m_toggleBtnGdrive; + wxToggleButton* m_toggleBtnSftp; + wxToggleButton* m_toggleBtnFtp; + wxBoxSizer* bSizerGdrive; + wxStaticBitmap* m_bitmapGdriveUser; + wxListBox* m_listBoxGdriveUsers; + zen::BitmapTextButton* m_buttonGdriveAddUser; + zen::BitmapTextButton* m_buttonGdriveRemoveUser; + wxStaticBitmap* m_bitmapGdriveDrive; + wxListBox* m_listBoxGdriveDrives; + wxBoxSizer* bSizerServer; + wxStaticBitmap* m_bitmapServer; + wxTextCtrl* m_textCtrlServer; + wxTextCtrl* m_textCtrlPort; + wxBoxSizer* bSizerAuth; + wxBoxSizer* bSizerAuthInner; + wxBoxSizer* bSizerFtpEncrypt; + wxRadioButton* m_radioBtnEncryptNone; + wxRadioButton* m_radioBtnEncryptSsl; + wxBoxSizer* bSizerSftpAuth; + wxRadioButton* m_radioBtnPassword; + wxRadioButton* m_radioBtnKeyfile; + wxRadioButton* m_radioBtnAgent; + wxPanel* m_panelAuth; + wxTextCtrl* m_textCtrlUserName; + wxStaticText* m_staticTextKeyfile; + wxBoxSizer* bSizerKeyFile; + wxTextCtrl* m_textCtrlKeyfilePath; + wxButton* m_buttonSelectKeyfile; + wxStaticText* m_staticTextPassword; + wxBoxSizer* bSizerPassword; + wxTextCtrl* m_textCtrlPasswordVisible; + wxTextCtrl* m_textCtrlPasswordHidden; + wxCheckBox* m_checkBoxShowPassword; + wxCheckBox* m_checkBoxPasswordPrompt; + wxStaticBitmap* m_bitmapServerDir; + wxStaticText* m_staticTextTimeout; + wxSpinCtrl* m_spinCtrlTimeout; + wxTextCtrl* m_textCtrlServerPath; + wxButton* m_buttonSelectFolder; + wxStaticBitmap* m_bitmapPerf; + wxBoxSizer* bSizerConnectionsLabel; + wxStaticText* m_staticTextConnectionsLabel; + wxStaticText* m_staticTextConnectionsLabelSub; + wxSpinCtrl* m_spinCtrlConnectionCount; + wxStaticText* m_staticTextConnectionCountDescr; + wxHyperlinkCtrl* m_hyperlinkDeRequired; + wxStaticText* m_staticTextChannelCountSftp; + wxSpinCtrl* m_spinCtrlChannelCountSftp; + wxButton* m_buttonChannelCountSftp; + wxCheckBox* m_checkBoxAllowZlib; + wxStaticText* m_staticTextZlibDescr; + wxBoxSizer* bSizerStdButtons; + wxButton* m_buttonOK; + wxButton* m_buttonCancel; + + // Virtual event handlers, override them in your derived class + virtual void onClose( wxCloseEvent& event ) { event.Skip(); } + virtual void onConnectionGdrive( wxCommandEvent& event ) { event.Skip(); } + virtual void onConnectionSftp( wxCommandEvent& event ) { event.Skip(); } + virtual void onConnectionFtp( wxCommandEvent& event ) { event.Skip(); } + virtual void onGdriveUserSelect( wxCommandEvent& event ) { event.Skip(); } + virtual void onGdriveUserAdd( wxCommandEvent& event ) { event.Skip(); } + virtual void onGdriveUserRemove( wxCommandEvent& event ) { event.Skip(); } + virtual void onAuthPassword( wxCommandEvent& event ) { event.Skip(); } + virtual void onAuthKeyfile( wxCommandEvent& event ) { event.Skip(); } + virtual void onAuthAgent( wxCommandEvent& event ) { event.Skip(); } + virtual void onSelectKeyfile( wxCommandEvent& event ) { event.Skip(); } + virtual void onTypingPassword( wxCommandEvent& event ) { event.Skip(); } + virtual void onToggleShowPassword( wxCommandEvent& event ) { event.Skip(); } + virtual void onTogglePasswordPrompt( wxCommandEvent& event ) { event.Skip(); } + virtual void onBrowseCloudFolder( wxCommandEvent& event ) { event.Skip(); } + virtual void onDetectServerChannelLimit( wxCommandEvent& event ) { event.Skip(); } + virtual void onOkay( wxCommandEvent& event ) { event.Skip(); } + virtual void onCancel( wxCommandEvent& event ) { event.Skip(); } + + +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|wxRESIZE_BORDER ); + + ~CloudSetupDlgGenerated(); + +}; + +/////////////////////////////////////////////////////////////////////////////// +/// Class AbstractFolderPickerGenerated +/////////////////////////////////////////////////////////////////////////////// +class AbstractFolderPickerGenerated : public wxDialog +{ +private: + +protected: + wxStaticText* m_staticTextStatus; + wxTreeCtrl* m_treeCtrlFileSystem; + wxBoxSizer* bSizerStdButtons; + wxButton* m_buttonOK; + wxButton* m_buttonCancel; + + // Virtual event handlers, override them in your derived class + virtual void onClose( wxCloseEvent& event ) { event.Skip(); } + virtual void onExpandNode( wxTreeEvent& event ) { event.Skip(); } + virtual void onOkay( wxCommandEvent& event ) { event.Skip(); } + virtual void onCancel( wxCommandEvent& event ) { event.Skip(); } + + +public: + + AbstractFolderPickerGenerated( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = _("Select a folder"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize( -1, -1 ), long style = wxDEFAULT_DIALOG_STYLE|wxMAXIMIZE_BOX|wxRESIZE_BORDER ); + + ~AbstractFolderPickerGenerated(); + +}; + +/////////////////////////////////////////////////////////////////////////////// +/// Class SyncConfirmationDlgGenerated +/////////////////////////////////////////////////////////////////////////////// +class SyncConfirmationDlgGenerated : public wxDialog +{ +private: + +protected: + wxStaticBitmap* m_bitmapSync; + wxStaticText* m_staticTextCaption; + wxPanel* m_panelStatistics; + wxStaticText* m_staticTextSyncVar; + wxStaticBitmap* m_bitmapSyncVar; + wxStaticBitmap* m_bitmapDeleteLeft; + wxStaticBitmap* m_bitmapUpdateLeft; + wxStaticBitmap* m_bitmapCreateLeft; + wxStaticBitmap* m_bitmapData; + wxStaticBitmap* m_bitmapCreateRight; + wxStaticBitmap* m_bitmapUpdateRight; + wxStaticBitmap* m_bitmapDeleteRight; + wxStaticText* m_staticTextDeleteLeft; + wxStaticText* m_staticTextUpdateLeft; + wxStaticText* m_staticTextCreateLeft; + wxStaticText* m_staticTextData; + wxStaticText* m_staticTextCreateRight; + wxStaticText* m_staticTextUpdateRight; + wxStaticText* m_staticTextDeleteRight; + wxCheckBox* m_checkBoxDontShowAgain; + wxBoxSizer* bSizerStdButtons; + wxButton* m_buttonOK; + wxButton* m_buttonCancel; + + // Virtual event handlers, override them in your derived class + virtual void onClose( wxCloseEvent& event ) { event.Skip(); } + virtual void onStartSync( wxCommandEvent& event ) { event.Skip(); } + virtual void onCancel( wxCommandEvent& event ) { event.Skip(); } + + +public: + + SyncConfirmationDlgGenerated( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = wxEmptyString, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = wxDEFAULT_DIALOG_STYLE ); + + ~SyncConfirmationDlgGenerated(); + +}; + +/////////////////////////////////////////////////////////////////////////////// +/// Class CompareProgressDlgGenerated +/////////////////////////////////////////////////////////////////////////////// +class CompareProgressDlgGenerated : public wxPanel +{ +private: + +protected: + wxStaticText* m_staticTextStatus; + wxStaticText* m_staticTextProcessed; + wxStaticText* m_staticTextRemaining; + wxPanel* m_panelItemStats; + wxStaticBitmap* m_bitmapItemStat; + wxStaticText* m_staticTextItemsProcessed; + wxStaticText* m_staticTextBytesProcessed; + wxStaticText* m_staticTextItemsRemaining; + wxStaticText* m_staticTextBytesRemaining; + wxPanel* m_panelTimeStats; + wxStaticBitmap* m_bitmapTimeStat; + wxStaticText* m_staticTextTimeElapsed; + wxStaticText* m_staticTextTimeRemaining; + wxStaticText* m_staticTextErrors; + wxStaticText* m_staticTextWarnings; + wxPanel* m_panelErrorStats; + wxStaticBitmap* m_bitmapErrors; + wxStaticText* m_staticTextErrorCount; + wxStaticBitmap* m_bitmapWarnings; + wxStaticText* m_staticTextWarningCount; + wxBoxSizer* bSizerErrorsRetry; + wxStaticBitmap* m_bitmapRetryErrors; + wxStaticText* m_staticTextRetryCount; + wxBoxSizer* bSizerErrorsIgnore; + wxStaticBitmap* m_bitmapIgnoreErrors; + wxBoxSizer* bSizerProgressGraph; + zen::Graph2D* m_panelProgressGraph; + +public: + + CompareProgressDlgGenerated( wxWindow* parent, wxWindowID id = wxID_ANY, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize( -1, -1 ), long style = wxBORDER_RAISED, const wxString& name = wxEmptyString ); + + ~CompareProgressDlgGenerated(); + +}; + +/////////////////////////////////////////////////////////////////////////////// +/// Class SyncProgressPanelGenerated +/////////////////////////////////////////////////////////////////////////////// +class SyncProgressPanelGenerated : public wxPanel +{ +private: + +protected: + +public: + wxBoxSizer* bSizerRoot; + wxStaticBitmap* m_bitmapStatus; + wxStaticText* m_staticTextPhase; + wxStaticText* m_staticTextPercentTotal; + wxBitmapButton* m_bpButtonMinimizeToTray; + wxBoxSizer* bSizerStatusText; + wxStaticText* m_staticTextStatus; + wxPanel* m_panelProgress; + zen::Graph2D* m_panelGraphBytes; + wxStaticBitmap* m_bitmapGraphKeyBytes; + wxStaticBitmap* m_bitmapGraphKeyItems; + wxStaticText* m_staticTextProcessed; + wxStaticText* m_staticTextRemaining; + wxPanel* m_panelItemStats; + wxStaticBitmap* m_bitmapItemStat; + wxStaticText* m_staticTextItemsProcessed; + wxStaticText* m_staticTextBytesProcessed; + wxStaticText* m_staticTextItemsRemaining; + wxStaticText* m_staticTextBytesRemaining; + wxPanel* m_panelTimeStats; + wxStaticBitmap* m_bitmapTimeStat; + wxStaticText* m_staticTextTimeElapsed; + wxStaticText* m_staticTextTimeRemaining; + wxStaticText* m_staticTextErrors; + wxStaticText* m_staticTextWarnings; + wxPanel* m_panelErrorStats; + wxStaticBitmap* m_bitmapErrors; + wxStaticText* m_staticTextErrorCount; + wxStaticBitmap* m_bitmapWarnings; + wxStaticText* m_staticTextWarningCount; + wxBoxSizer* bSizerDynSpace; + zen::Graph2D* m_panelGraphItems; + wxBoxSizer* bSizerProgressFooter; + wxBoxSizer* bSizerErrorsRetry; + wxStaticBitmap* m_bitmapRetryErrors; + wxStaticText* m_staticTextRetryCount; + wxBoxSizer* bSizerErrorsIgnore; + wxStaticBitmap* m_bitmapIgnoreErrors; + wxChoice* m_choicePostSyncAction; + wxNotebook* m_notebookResult; + wxStaticLine* m_staticlineFooter; + wxBoxSizer* bSizerStdButtons; + wxCheckBox* m_checkBoxAutoClose; + wxButton* m_buttonClose; + wxButton* m_buttonPause; + wxButton* m_buttonStop; + + SyncProgressPanelGenerated( wxWindow* parent, wxWindowID id = wxID_ANY, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize( -1, -1 ), long style = wxTAB_TRAVERSAL, const wxString& name = wxEmptyString ); + + ~SyncProgressPanelGenerated(); + +}; + +/////////////////////////////////////////////////////////////////////////////// +/// Class LogPanelGenerated +/////////////////////////////////////////////////////////////////////////////// +class LogPanelGenerated : public wxPanel +{ +private: + +protected: + zen::ToggleButton* m_bpButtonErrors; + zen::ToggleButton* m_bpButtonWarnings; + zen::ToggleButton* m_bpButtonInfo; + zen::Grid* m_gridMessages; + + // Virtual event handlers, override them in your derived class + virtual void onErrors( wxCommandEvent& event ) { event.Skip(); } + virtual void onWarnings( wxCommandEvent& event ) { event.Skip(); } + virtual void onInfo( wxCommandEvent& event ) { event.Skip(); } + + +public: + + LogPanelGenerated( wxWindow* parent, wxWindowID id = wxID_ANY, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = wxTAB_TRAVERSAL, const wxString& name = wxEmptyString ); + + ~LogPanelGenerated(); + +}; + +/////////////////////////////////////////////////////////////////////////////// +/// Class BatchDlgGenerated +/////////////////////////////////////////////////////////////////////////////// +class BatchDlgGenerated : public wxDialog +{ +private: + +protected: + wxStaticBitmap* m_bitmapBatchJob; + wxStaticText* m_staticTextHeader; + wxStaticBitmap* m_bitmapMinimizeToTray; + wxCheckBox* m_checkBoxRunMinimized; + wxCheckBox* m_checkBoxAutoClose; + wxStaticBitmap* m_bitmapIgnoreErrors; + wxCheckBox* m_checkBoxIgnoreErrors; + wxRadioButton* m_radioBtnErrorDialogShow; + wxRadioButton* m_radioBtnErrorDialogCancel; + wxChoice* m_choicePostSyncAction; + wxBoxSizer* bSizerStdButtons; + wxButton* m_buttonSaveAs; + wxButton* m_buttonCancel; + + // Virtual event handlers, override them in your derived class + virtual void onClose( wxCloseEvent& event ) { event.Skip(); } + virtual void onToggleRunMinimized( wxCommandEvent& event ) { event.Skip(); } + virtual void onToggleIgnoreErrors( wxCommandEvent& event ) { event.Skip(); } + virtual void onSaveBatchJob( wxCommandEvent& event ) { event.Skip(); } + virtual void onCancel( wxCommandEvent& event ) { event.Skip(); } + + +public: + + BatchDlgGenerated( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = _("Save as a Batch Job"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER ); + + ~BatchDlgGenerated(); + +}; + +/////////////////////////////////////////////////////////////////////////////// +/// Class DeleteDlgGenerated +/////////////////////////////////////////////////////////////////////////////// +class DeleteDlgGenerated : public wxDialog +{ +private: + +protected: + wxStaticBitmap* m_bitmapDeleteType; + wxStaticText* m_staticTextHeader; + wxTextCtrl* m_textCtrlFileList; + wxBoxSizer* bSizerStdButtons; + wxCheckBox* m_checkBoxUseRecycler; + wxButton* m_buttonOK; + wxButton* m_buttonCancel; + + // Virtual event handlers, override them in your derived class + virtual void onClose( wxCloseEvent& event ) { event.Skip(); } + virtual void onUseRecycler( wxCommandEvent& event ) { event.Skip(); } + virtual void onOkay( wxCommandEvent& event ) { event.Skip(); } + virtual void onCancel( wxCommandEvent& event ) { event.Skip(); } + + +public: + + DeleteDlgGenerated( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = _("Delete Items"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize( -1, -1 ), long style = wxDEFAULT_DIALOG_STYLE|wxMAXIMIZE_BOX|wxRESIZE_BORDER ); + + ~DeleteDlgGenerated(); + +}; + +/////////////////////////////////////////////////////////////////////////////// +/// Class CopyToDlgGenerated +/////////////////////////////////////////////////////////////////////////////// +class CopyToDlgGenerated : public wxDialog +{ +private: + +protected: + wxStaticBitmap* m_bitmapCopyTo; + wxStaticText* m_staticTextHeader; + wxTextCtrl* m_textCtrlFileList; + fff::FolderHistoryBox* m_targetFolderPath; + wxButton* m_buttonSelectTargetFolder; + wxBitmapButton* m_bpButtonSelectAltTargetFolder; + wxBoxSizer* bSizerStdButtons; + wxCheckBox* m_checkBoxKeepRelPath; + wxCheckBox* m_checkBoxOverwriteIfExists; + wxButton* m_buttonOK; + wxButton* m_buttonCancel; + + // Virtual event handlers, override them in your derived class + virtual void onClose( wxCloseEvent& event ) { event.Skip(); } + virtual void onOkay( wxCommandEvent& event ) { event.Skip(); } + virtual void onCancel( wxCommandEvent& event ) { event.Skip(); } + + +public: + + CopyToDlgGenerated( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = _("Copy Items"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize( -1, -1 ), long style = wxDEFAULT_DIALOG_STYLE|wxMAXIMIZE_BOX|wxRESIZE_BORDER ); + + ~CopyToDlgGenerated(); + +}; + +/////////////////////////////////////////////////////////////////////////////// +/// Class RenameDlgGenerated +/////////////////////////////////////////////////////////////////////////////// +class RenameDlgGenerated : public wxDialog +{ +private: + +protected: + wxStaticBitmap* m_bitmapRename; + wxStaticText* m_staticTextHeader; + zen::Grid* m_gridRenamePreview; + wxStaticLine* m_staticlinePreview; + wxStaticText* m_staticTextPlaceholderDescription; + wxTextCtrl* m_textCtrlNewName; + wxBoxSizer* bSizerStdButtons; + wxButton* m_buttonOK; + wxButton* m_buttonCancel; + + // Virtual event handlers, override them in your derived class + virtual void onClose( wxCloseEvent& event ) { event.Skip(); } + virtual void onOkay( wxCommandEvent& event ) { event.Skip(); } + virtual void onCancel( wxCommandEvent& event ) { event.Skip(); } + + +public: + + RenameDlgGenerated( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = _("Rename Items"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize( -1, -1 ), long style = wxDEFAULT_DIALOG_STYLE|wxMAXIMIZE_BOX|wxRESIZE_BORDER ); + + ~RenameDlgGenerated(); + +}; + +/////////////////////////////////////////////////////////////////////////////// +/// Class OptionsDlgGenerated +/////////////////////////////////////////////////////////////////////////////// +class OptionsDlgGenerated : public wxDialog +{ +private: + +protected: + wxStaticBitmap* m_bitmapSettings; + wxCheckBox* m_checkBoxFailSafe; + wxBoxSizer* bSizerLockedFiles; + wxCheckBox* m_checkBoxCopyLocked; + wxCheckBox* m_checkBoxCopyPermissions; + wxBoxSizer* bSizerColorTheme; + wxStaticBitmap* m_bitmapColorTheme; + wxChoice* m_choiceColorTheme; + wxStaticBitmap* m_bitmapWarnings; + wxStaticText* m_staticTextHiddenDialogsCount; + wxButton* m_buttonShowHiddenDialogs; + wxCheckListBox* m_checkListHiddenDialogs; + wxStaticBitmap* m_bitmapLogFile; + wxBitmapButton* m_bpButtonShowLogFolder; + wxPanel* m_panelLogfile; + fff::FolderHistoryBox* m_logFolderPath; + wxButton* m_buttonSelectLogFolder; + wxBitmapButton* m_bpButtonSelectAltLogFolder; + wxCheckBox* m_checkBoxLogFilesMaxAge; + wxSpinCtrl* m_spinCtrlLogFilesMaxAge; + wxRadioButton* m_radioBtnLogHtml; + wxRadioButton* m_radioBtnLogText; + wxStaticBitmap* m_bitmapNotificationSounds; + wxStaticBitmap* m_bitmapCompareDone; + wxBitmapButton* m_bpButtonPlayCompareDone; + wxTextCtrl* m_textCtrlSoundPathCompareDone; + wxButton* m_buttonSelectSoundCompareDone; + wxStaticBitmap* m_bitmapSyncDone; + wxBitmapButton* m_bpButtonPlaySyncDone; + wxTextCtrl* m_textCtrlSoundPathSyncDone; + wxButton* m_buttonSelectSoundSyncDone; + wxStaticBitmap* m_bitmapAlertPending; + wxBitmapButton* m_bpButtonPlayAlertPending; + wxTextCtrl* m_textCtrlSoundPathAlertPending; + wxButton* m_buttonSelectSoundAlertPending; + wxStaticBitmap* m_bitmapConsole; + wxButton* m_buttonShowCtxCustomize; + wxBoxSizer* bSizerContextCustomize; + wxBitmapButton* m_bpButtonAddRow; + wxBitmapButton* m_bpButtonRemoveRow; + wxGrid* m_gridCustomCommand; + wxBoxSizer* bSizerStdButtons; + wxButton* m_buttonDefault; + wxButton* m_buttonOK; + wxButton* m_buttonCancel; + + // Virtual event handlers, override them in your derived class + virtual void onClose( wxCloseEvent& event ) { event.Skip(); } + virtual void onChangeColorTheme( wxCommandEvent& event ) { event.Skip(); } + virtual void onShowHiddenDialogs( wxCommandEvent& event ) { event.Skip(); } + virtual void onToggleHiddenDialog( wxCommandEvent& event ) { event.Skip(); } + virtual void onShowLogFolder( wxCommandEvent& event ) { event.Skip(); } + virtual void onToggleLogfilesLimit( wxCommandEvent& event ) { event.Skip(); } + virtual void onPlayCompareDone( wxCommandEvent& event ) { event.Skip(); } + virtual void onChangeSoundFilePath( wxCommandEvent& event ) { event.Skip(); } + virtual void onSelectSoundCompareDone( wxCommandEvent& event ) { event.Skip(); } + virtual void onPlaySyncDone( wxCommandEvent& event ) { event.Skip(); } + virtual void onSelectSoundSyncDone( wxCommandEvent& event ) { event.Skip(); } + virtual void onPlayAlertPending( wxCommandEvent& event ) { event.Skip(); } + virtual void onSelectSoundAlertPending( wxCommandEvent& event ) { event.Skip(); } + virtual void onShowContextCustomize( wxCommandEvent& event ) { event.Skip(); } + virtual void onAddRow( wxCommandEvent& event ) { event.Skip(); } + virtual void onRemoveRow( wxCommandEvent& event ) { event.Skip(); } + virtual void onDefault( wxCommandEvent& event ) { event.Skip(); } + virtual void onOkay( wxCommandEvent& event ) { event.Skip(); } + virtual void onCancel( wxCommandEvent& event ) { event.Skip(); } + + +public: + + OptionsDlgGenerated( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = _("Options"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER ); + + ~OptionsDlgGenerated(); + +}; + +/////////////////////////////////////////////////////////////////////////////// +/// Class SelectTimespanDlgGenerated +/////////////////////////////////////////////////////////////////////////////// +class SelectTimespanDlgGenerated : public wxDialog +{ +private: + +protected: + wxCalendarCtrl* m_calendarFrom; + wxCalendarCtrl* m_calendarTo; + wxBoxSizer* bSizerStdButtons; + wxButton* m_buttonOK; + wxButton* m_buttonCancel; + + // Virtual event handlers, override them in your derived class + virtual void onClose( wxCloseEvent& event ) { event.Skip(); } + virtual void onChangeSelectionFrom( wxCalendarEvent& event ) { event.Skip(); } + virtual void onChangeSelectionTo( wxCalendarEvent& event ) { event.Skip(); } + virtual void onOkay( wxCommandEvent& event ) { event.Skip(); } + virtual void onCancel( wxCommandEvent& event ) { event.Skip(); } + + +public: + + SelectTimespanDlgGenerated( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = _("Select Time Span"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = wxDEFAULT_DIALOG_STYLE ); + + ~SelectTimespanDlgGenerated(); + +}; + +/////////////////////////////////////////////////////////////////////////////// +/// Class AboutDlgGenerated +/////////////////////////////////////////////////////////////////////////////// +class AboutDlgGenerated : public wxDialog +{ +private: + +protected: + wxStaticBitmap* m_bitmapLogoLeft; + wxBoxSizer* bSizerMainSection; + wxStaticBitmap* m_bitmapLogo; + wxStaticText* m_staticFfsTextVersion; + wxStaticText* m_staticTextFfsVariant; + wxBoxSizer* bSizerDonate; + wxPanel* m_panelDonate; + wxStaticBitmap* m_bitmapAnimalSmall; + wxStaticText* m_staticTextDonate; + zen::BitmapTextButton* m_buttonDonate1; + wxStaticBitmap* m_bitmapAnimalBig; + wxBitmapButton* m_bpButtonForum; + wxBitmapButton* m_bpButtonEmail; + wxStaticText* m_staticTextThanksForLoc; + wxScrolledWindow* m_scrolledWindowTranslators; + wxFlexGridSizer* fgSizerTranslators; + wxBoxSizer* bSizerStdButtons; + wxButton* m_buttonShowSupporterDetails; + zen::BitmapTextButton* m_buttonDonate2; + wxButton* m_buttonClose; + + // Virtual event handlers, override them in your derived class + virtual void onClose( wxCloseEvent& event ) { event.Skip(); } + virtual void onDonate( wxCommandEvent& event ) { event.Skip(); } + virtual void onOpenForum( wxCommandEvent& event ) { event.Skip(); } + virtual void onSendEmail( wxCommandEvent& event ) { event.Skip(); } + virtual void onShowSupporterDetails( wxCommandEvent& event ) { event.Skip(); } + virtual void onOkay( wxCommandEvent& event ) { event.Skip(); } + + +public: + + AboutDlgGenerated( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = _("About"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = wxDEFAULT_DIALOG_STYLE ); + + ~AboutDlgGenerated(); + +}; + +/////////////////////////////////////////////////////////////////////////////// +/// Class DownloadProgressDlgGenerated +/////////////////////////////////////////////////////////////////////////////// +class DownloadProgressDlgGenerated : public wxDialog +{ +private: + +protected: + wxStaticBitmap* m_bitmapDownloading; + wxStaticText* m_staticTextHeader; + wxGauge* m_gaugeProgress; + wxStaticText* m_staticTextDetails; + wxBoxSizer* bSizerStdButtons; + wxButton* m_buttonCancel; + + // Virtual event handlers, override them in your derived class + virtual void onCancel( wxCommandEvent& event ) { event.Skip(); } + + +public: + + DownloadProgressDlgGenerated( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = wxEmptyString, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = 0 ); + + ~DownloadProgressDlgGenerated(); + +}; + +/////////////////////////////////////////////////////////////////////////////// +/// Class CfgHighlightDlgGenerated +/////////////////////////////////////////////////////////////////////////////// +class CfgHighlightDlgGenerated : public wxDialog +{ +private: + +protected: + wxStaticText* m_staticTextHighlight; + wxSpinCtrl* m_spinCtrlOverdueDays; + wxBoxSizer* bSizerStdButtons; + wxButton* m_buttonOK; + wxButton* m_buttonCancel; + + // Virtual event handlers, override them in your derived class + virtual void onClose( wxCloseEvent& event ) { event.Skip(); } + virtual void onOkay( wxCommandEvent& event ) { event.Skip(); } + virtual void onCancel( wxCommandEvent& event ) { event.Skip(); } + + +public: + + CfgHighlightDlgGenerated( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = _("Highlight Configurations"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = wxDEFAULT_DIALOG_STYLE ); + + ~CfgHighlightDlgGenerated(); + +}; + +/////////////////////////////////////////////////////////////////////////////// +/// Class PasswordPromptDlgGenerated +/////////////////////////////////////////////////////////////////////////////// +class PasswordPromptDlgGenerated : public wxDialog +{ +private: + +protected: + wxStaticText* m_staticTextMain; + wxStaticText* m_staticTextPassword; + wxTextCtrl* m_textCtrlPasswordVisible; + wxTextCtrl* m_textCtrlPasswordHidden; + wxCheckBox* m_checkBoxShowPassword; + wxBoxSizer* bSizerError; + wxStaticBitmap* m_bitmapError; + wxStaticText* m_staticTextError; + wxBoxSizer* bSizerStdButtons; + wxButton* m_buttonOK; + wxButton* m_buttonCancel; + + // Virtual event handlers, override them in your derived class + virtual void onClose( wxCloseEvent& event ) { event.Skip(); } + virtual void onTypingPassword( wxCommandEvent& event ) { event.Skip(); } + virtual void onToggleShowPassword( wxCommandEvent& event ) { event.Skip(); } + virtual void onOkay( wxCommandEvent& event ) { event.Skip(); } + virtual void onCancel( wxCommandEvent& event ) { event.Skip(); } + + +public: + + PasswordPromptDlgGenerated( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = _("dummy"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER ); + + ~PasswordPromptDlgGenerated(); + +}; + +/////////////////////////////////////////////////////////////////////////////// +/// Class ActivationDlgGenerated +/////////////////////////////////////////////////////////////////////////////// +class ActivationDlgGenerated : public wxDialog +{ +private: + +protected: + wxStaticBitmap* m_bitmapActivation; + wxRichTextCtrl* m_richTextLastError; + wxStaticText* m_staticTextMain; + wxStaticText* m_staticTextMain1; + wxButton* m_buttonActivateOnline; + wxButton* m_buttonCopyUrl; + wxRichTextCtrl* m_richTextManualActivationUrl; + wxTextCtrl* m_textCtrlOfflineActivationKey; + wxButton* m_buttonActivateOffline; + wxBoxSizer* bSizerStdButtons; + wxButton* m_buttonCancel; + + // Virtual event handlers, override them in your derived class + virtual void onClose( wxCloseEvent& event ) { event.Skip(); } + virtual void onActivateOnline( wxCommandEvent& event ) { event.Skip(); } + virtual void onCopyUrl( wxCommandEvent& event ) { event.Skip(); } + virtual void onOfflineActivationEnter( wxCommandEvent& event ) { event.Skip(); } + virtual void onActivateOffline( wxCommandEvent& event ) { event.Skip(); } + virtual void onCancel( wxCommandEvent& event ) { event.Skip(); } + + +public: + + ActivationDlgGenerated( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = _("dummy"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER ); + + ~ActivationDlgGenerated(); + +}; + +/////////////////////////////////////////////////////////////////////////////// +/// Class WarnAccessRightsMissingDlgGenerated +/////////////////////////////////////////////////////////////////////////////// +class WarnAccessRightsMissingDlgGenerated : public wxDialog +{ +private: + +protected: + wxStaticBitmap* m_bitmapGrantAccess; + wxStaticText* m_staticTextDescr; + wxStaticText* m_staticTextStep1; + wxButton* m_buttonLocateBundle; + wxStaticText* m_staticTextStep2; + wxButton* m_buttonOpenSecurity; + wxStaticText* m_staticTextStep3; + wxStaticText* m_staticTextGrantAccess; + wxCheckBox* m_checkBoxDontShowAgain; + wxBoxSizer* bSizerStdButtons; + wxButton* m_buttonClose; + + // Virtual event handlers, override them in your derived class + virtual void onClose( wxCloseEvent& event ) { event.Skip(); } + virtual void onShowAppBundle( wxCommandEvent& event ) { event.Skip(); } + virtual void onOpenSecuritySettings( wxCommandEvent& event ) { event.Skip(); } + virtual void onCheckBoxClick( wxCommandEvent& event ) { event.Skip(); } + virtual void onOkay( wxCommandEvent& event ) { event.Skip(); } + + +public: + + WarnAccessRightsMissingDlgGenerated( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = _("Grant Full Disk Access"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = wxDEFAULT_DIALOG_STYLE ); + + ~WarnAccessRightsMissingDlgGenerated(); + +}; + diff --git a/FreeFileSync/Source/ui/gui_status_handler.cpp b/FreeFileSync/Source/ui/gui_status_handler.cpp new file mode 100644 index 0000000..f3a3df3 --- /dev/null +++ b/FreeFileSync/Source/ui/gui_status_handler.cpp @@ -0,0 +1,716 @@ +// ***************************************************************************** +// * 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 "gui_status_handler.h" +#include +#include +#include +#include + +using namespace zen; +using namespace fff; + +namespace +{ +constexpr std::chrono::seconds TEMP_PANEL_DISPLAY_DELAY(1); +} + +StatusHandlerTemporaryPanel::StatusHandlerTemporaryPanel(MainDialog& dlg, + const std::chrono::system_clock::time_point& startTime, + bool ignoreErrors, + size_t autoRetryCount, + std::chrono::seconds autoRetryDelay, + const Zstring& soundFileAlertPending) : + mainDlg_(dlg), + ignoreErrors_(ignoreErrors), + autoRetryCount_(autoRetryCount), + autoRetryDelay_(autoRetryDelay), + soundFileAlertPending_(soundFileAlertPending), + startTime_(startTime) +{ + mainDlg_.compareStatus_->init(*this, ignoreErrors_, autoRetryCount_); //clear old values before showing panel + + //showStatsPanel(); => delay and avoid GUI distraction for short-lived tasks + + mainDlg_.Update(); //don't wait until idle event! + + //register keys + mainDlg_. Bind(wxEVT_CHAR_HOOK, &StatusHandlerTemporaryPanel::onLocalKeyEvent, this); + mainDlg_.m_buttonCancel->Bind(wxEVT_COMMAND_BUTTON_CLICKED, &StatusHandlerTemporaryPanel::onAbortCompare, this); +} + + +void StatusHandlerTemporaryPanel::showStatsPanel() +{ + assert(!mainDlg_.auiMgr_.GetPane(mainDlg_.compareStatus_->getAsWindow()).IsShown()); + { + //------------------------------------------------------------------ + const wxAuiPaneInfo& topPanel = mainDlg_.auiMgr_.GetPane(mainDlg_.m_panelTopButtons); + wxAuiPaneInfo& statusPanel = mainDlg_.auiMgr_.GetPane(mainDlg_.compareStatus_->getAsWindow()); + + //determine best status panel row near top panel + switch (topPanel.dock_direction) + { + case wxAUI_DOCK_TOP: + case wxAUI_DOCK_BOTTOM: + statusPanel.Layer (topPanel.dock_layer); + statusPanel.Direction(topPanel.dock_direction); + statusPanel.Row (topPanel.dock_row + 1); + break; + + case wxAUI_DOCK_LEFT: + case wxAUI_DOCK_RIGHT: + statusPanel.Layer (std::max(0, topPanel.dock_layer - 1)); + statusPanel.Direction(wxAUI_DOCK_TOP); + statusPanel.Row (0); + break; + //case wxAUI_DOCK_CENTRE: + } + + const bool statusRowTaken = [&] + { + for (wxAuiPaneInfo& paneInfo : mainDlg_.auiMgr_.GetAllPanes()) + //doesn't matter if paneInfo.IsShown() or not! => move down in either case! + if (&paneInfo != &statusPanel && + paneInfo.dock_layer == statusPanel.dock_layer && + paneInfo.dock_direction == statusPanel.dock_direction && + paneInfo.dock_row == statusPanel.dock_row) + return true; + + return false; + }(); + + //move all rows that are in the way one step further + if (statusRowTaken) + for (wxAuiPaneInfo& paneInfo : mainDlg_.auiMgr_.GetAllPanes()) + if (&paneInfo != &statusPanel && + paneInfo.dock_layer == statusPanel.dock_layer && + paneInfo.dock_direction == statusPanel.dock_direction && + paneInfo.dock_row >= statusPanel.dock_row) + ++paneInfo.dock_row; + //------------------------------------------------------------------ + + statusPanel.Show(); + mainDlg_.auiMgr_.Update(); + mainDlg_.compareStatus_->getAsWindow()->Refresh(); //macOS: fix background corruption for the statistics boxes (call *after* wxAuiManager::Update() + } +} + + +StatusHandlerTemporaryPanel::~StatusHandlerTemporaryPanel() +{ + if (!errorLog_.empty()) //prepareResult() was not called! + std::abort(); + + //Workaround wxAuiManager crash when starting panel resizing during comparison and holding button until after comparison has finished: + //- unlike regular window resizing, wxAuiManager does not run a dedicated event loop while the mouse button is held + //- wxAuiManager internally stores the panel index that is currently resized + //- our hiding of the compare status panel invalidates this index + // => the next mouse move will have wxAuiManager crash => another fine piece of "wxQuality" code + // => mitigate: + wxMouseCaptureLostEvent dummy; + mainDlg_.ProcessEvent(dummy); //trigger wxAuiManager::OnCaptureLost(); should be no-op if no mouse buttons are pressed + if (wxWindow::GetCapture() == &mainDlg_) + mainDlg_.ReleaseMouse(); + + mainDlg_.auiMgr_.GetPane(mainDlg_.compareStatus_->getAsWindow()).Hide(); + mainDlg_.auiMgr_.Update(); + + //unregister keys + [[maybe_unused]] bool ubOk1 = mainDlg_. Unbind(wxEVT_CHAR_HOOK, &StatusHandlerTemporaryPanel::onLocalKeyEvent, this); + [[maybe_unused]] bool ubOk2 = mainDlg_.m_buttonCancel->Unbind(wxEVT_COMMAND_BUTTON_CLICKED, &StatusHandlerTemporaryPanel::onAbortCompare, this); + assert(ubOk1 && ubOk2); + + mainDlg_.compareStatus_->teardown(); +} + + +StatusHandlerTemporaryPanel::Result StatusHandlerTemporaryPanel::prepareResult() //noexcept!! +{ + const std::chrono::milliseconds totalTime = mainDlg_.compareStatus_->pauseAndGetTotalTime(); + + //append "extra" log for sync errors that could not otherwise be reported: + if (const ErrorLog extraLog = fetchExtraLog(); + !extraLog.empty()) + { + append(errorLog_, extraLog); + std::stable_sort(errorLog_.begin(), errorLog_.end(), [](const LogEntry& lhs, const LogEntry& rhs) { return lhs.time < rhs.time; }); + } + + //determine post-sync status irrespective of further errors during tear-down + const TaskResult syncResult = [&] + { + if (taskCancelled()) + { + logMsg(errorLog_, _("Stopped"), MSG_TYPE_ERROR); //= user cancel + return TaskResult::cancelled; + } + + const ErrorLogStats logCount = getStats(errorLog_); + if (logCount.errors > 0) + return TaskResult::error; + else if (logCount.warnings > 0) + return TaskResult::warning; + else + return TaskResult::success; + }(); + + const ProcessSummary summary + { + startTime_, syncResult, {} /*jobNames*/, + getCurrentStats(), + getTotalStats (), + totalTime + }; + + return {summary, makeSharedRef(std::exchange(errorLog_, {}))}; //see check in ~StatusHandlerTemporaryPanel() +} + + +void StatusHandlerTemporaryPanel::initNewPhase(int itemsTotal, int64_t bytesTotal, ProcessPhase phaseID) +{ + StatusHandler::initNewPhase(itemsTotal, bytesTotal, phaseID); + + mainDlg_.compareStatus_->initNewPhase(); //call after "StatusHandler::initNewPhase" + + //macOS needs a full yield to update GUI and get rid of "dummy" texts + requestUiUpdate(true /*force*/); //throw CancelProcess +} + + +void StatusHandlerTemporaryPanel::logMessage(const std::wstring& msg, MsgType type) +{ + logMsg(errorLog_, msg, [&] + { + switch (type) + { + case MsgType::info: return MSG_TYPE_INFO; + case MsgType::warning: return MSG_TYPE_WARNING; + case MsgType::error: return MSG_TYPE_ERROR; + } + assert(false); + return MSG_TYPE_ERROR; + }()); + requestUiUpdate(false /*force*/); //throw CancelProcess +} + + +void StatusHandlerTemporaryPanel::reportWarning(const std::wstring& msg, bool& warningActive) +{ + PauseTimers dummy(*mainDlg_.compareStatus_); + + logMsg(errorLog_, msg, MSG_TYPE_WARNING); + + if (!warningActive) //if errors are ignored, then warnings should also + return; + + if (!mainDlg_.compareStatus_->getOptionIgnoreErrors()) + { + forceUiUpdateNoThrow(); //noexcept! => don't throw here when error occurs during clean up! + + bool dontWarnAgain = false; + switch (showConfirmationDialog(&mainDlg_, DialogInfoType::warning, + PopupDialogCfg().setDetailInstructions(msg). + alertWhenPending(soundFileAlertPending_). + setCheckBox(dontWarnAgain, _("&Don't show this warning again")), + _("&Ignore"))) + { + case ConfirmationButton::accept: + warningActive = !dontWarnAgain; + break; + case ConfirmationButton::cancel: + cancelProcessNow(CancelReason::user); //throw CancelProcess + break; + } + } + //else: if errors are ignored, then warnings should also +} + + +ProcessCallback::Response StatusHandlerTemporaryPanel::reportError(const ErrorInfo& errorInfo) +{ + PauseTimers dummy(*mainDlg_.compareStatus_); + + //log actual fail time (not "now"!) + const time_t failTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now() - + std::chrono::duration_cast(std::chrono::steady_clock::now() - errorInfo.failTime)); + //auto-retry + if (errorInfo.retryNumber < autoRetryCount_) + { + logMsg(errorLog_, errorInfo.msg + L"\n-> " + _("Automatic retry"), MSG_TYPE_INFO, failTime); + delayAndCountDown(errorInfo.failTime + autoRetryDelay_ - std::chrono::steady_clock::now(), + [&, statusPrefix = _("Automatic retry") + + (errorInfo.retryNumber == 0 ? L"" : L' ' + formatNumber(errorInfo.retryNumber + 1)) + SPACED_DASH, + statusPostfix = SPACED_DASH + _("Error") + L": " + replaceCpy(errorInfo.msg, L'\n', L' ')](const std::wstring& timeRemMsg) + { this->updateStatus(statusPrefix + timeRemMsg + statusPostfix); }); //throw CancelProcess + return ProcessCallback::retry; + } + + //always, except for "retry": + auto guardWriteLog = makeGuard([&] { logMsg(errorLog_, errorInfo.msg, MSG_TYPE_ERROR, failTime); }); + + if (!mainDlg_.compareStatus_->getOptionIgnoreErrors()) + { + forceUiUpdateNoThrow(); //noexcept! => don't throw here when error occurs during clean up! + + switch (showConfirmationDialog(&mainDlg_, DialogInfoType::error, + PopupDialogCfg().setDetailInstructions(errorInfo.msg). + alertWhenPending(soundFileAlertPending_), + _("&Ignore"), _("Ignore &all"), _("&Retry"))) + { + case ConfirmationButton3::accept: //ignore + return ProcessCallback::ignore; + + case ConfirmationButton3::accept2: //ignore all + mainDlg_.compareStatus_->setOptionIgnoreErrors(true); + return ProcessCallback::ignore; + + case ConfirmationButton3::decline: //retry + guardWriteLog.dismiss(); + logMsg(errorLog_, errorInfo.msg + L"\n-> " + _("Retrying operation..."), //explain why there are duplicate "doing operation X" info messages in the log! + MSG_TYPE_INFO, failTime); + return ProcessCallback::retry; + + case ConfirmationButton3::cancel: + cancelProcessNow(CancelReason::user); //throw CancelProcess + break; + } + } + else + return ProcessCallback::ignore; + + assert(false); + return ProcessCallback::ignore; //dummy return value +} + + +void StatusHandlerTemporaryPanel::reportFatalError(const std::wstring& msg) +{ + PauseTimers dummy(*mainDlg_.compareStatus_); + + logMsg(errorLog_, msg, MSG_TYPE_ERROR); + + if (!mainDlg_.compareStatus_->getOptionIgnoreErrors()) + { + forceUiUpdateNoThrow(); //noexcept! => don't throw here when error occurs during clean up! + + switch (showConfirmationDialog(&mainDlg_, DialogInfoType::error, + PopupDialogCfg().setDetailInstructions(msg). + alertWhenPending(soundFileAlertPending_), + _("&Ignore"), _("Ignore &all"))) + { + case ConfirmationButton2::accept: //ignore + break; + + case ConfirmationButton2::accept2: //ignore all + mainDlg_.compareStatus_->setOptionIgnoreErrors(true); + break; + + case ConfirmationButton2::cancel: + cancelProcessNow(CancelReason::user); //throw CancelProcess + break; + } + } +} + + +Statistics::ErrorStats StatusHandlerTemporaryPanel::getErrorStats() const +{ + //errorLog_ is an "append only" structure, so we can make getErrorStats() complexity "constant time": + std::for_each(errorLog_.begin() + errorStatsRowsChecked_, errorLog_.end(), [&](const LogEntry& entry) + { + switch (entry.type) + { + case MSG_TYPE_INFO: + break; + case MSG_TYPE_WARNING: + ++errorStatsBuf_.warningCount; + break; + case MSG_TYPE_ERROR: + ++errorStatsBuf_.errorCount; + break; + } + }); + errorStatsRowsChecked_ = errorLog_.size(); + + return errorStatsBuf_; +} + + +void StatusHandlerTemporaryPanel::forceUiUpdateNoThrow() +{ + if (!mainDlg_.auiMgr_.GetPane(mainDlg_.compareStatus_->getAsWindow()).IsShown() && + std::chrono::steady_clock::now() > panelInitTime_ + TEMP_PANEL_DISPLAY_DELAY) + showStatsPanel(); + + mainDlg_.compareStatus_->updateGui(); +} + + +void StatusHandlerTemporaryPanel::onLocalKeyEvent(wxKeyEvent& event) +{ + const int keyCode = event.GetKeyCode(); + if (keyCode == WXK_ESCAPE) + return userRequestCancel(); + + event.Skip(); +} + + +void StatusHandlerTemporaryPanel::onAbortCompare(wxCommandEvent& event) +{ + userRequestCancel(); +} + +//######################################################################################################## + +StatusHandlerFloatingDialog::StatusHandlerFloatingDialog(wxFrame* parentDlg, + const std::vector& jobNames, + const std::chrono::system_clock::time_point& startTime, + bool ignoreErrors, + size_t autoRetryCount, + std::chrono::seconds autoRetryDelay, + const Zstring& soundFileSyncComplete, + const Zstring& soundFileAlertPending, + const WindowLayout::Dimensions& dim, + bool autoCloseDialog) : + jobNames_(jobNames), + startTime_(startTime), + autoRetryCount_(autoRetryCount), + autoRetryDelay_(autoRetryDelay), + soundFileSyncComplete_(soundFileSyncComplete), + soundFileAlertPending_(soundFileAlertPending) +{ + //set *after* initializer list => callbacks during construction to getErrorStats()! + progressDlg_ = SyncProgressDialog::create(dim, [this] { userRequestCancel(); }, *this, parentDlg, true /*showProgress*/, autoCloseDialog, + jobNames, std::chrono::system_clock::to_time_t(startTime), ignoreErrors, autoRetryCount, PostSyncAction::none); +} + + +StatusHandlerFloatingDialog::~StatusHandlerFloatingDialog() +{ + if (progressDlg_) //prepareResult() was not called! + std::abort(); +} + + +StatusHandlerFloatingDialog::Result StatusHandlerFloatingDialog::prepareResult() +{ + //keep correct summary window stats considering count down timer, system sleep + const std::chrono::milliseconds totalTime = progressDlg_->pauseAndGetTotalTime(); + + //append "extra" log for sync errors that could not otherwise be reported: + if (const ErrorLog extraLog = fetchExtraLog(); + !extraLog.empty()) + { + append(errorLog_.ref(), extraLog); + std::stable_sort(errorLog_.ref().begin(), errorLog_.ref().end(), [](const LogEntry& lhs, const LogEntry& rhs) { return lhs.time < rhs.time; }); + } + + //determine post-sync status irrespective of further errors during tear-down + assert(!syncResult_); + syncResult_ = [&] + { + if (taskCancelled()) //= user cancel + { + assert(*taskCancelled() == CancelReason::user); //"stop on first error" is ffs_batch-only + logMsg(errorLog_.ref(), _("Stopped"), MSG_TYPE_ERROR); + return TaskResult::cancelled; + } + + const ErrorLogStats logCount = getStats(errorLog_.ref()); + if (logCount.errors > 0) + return TaskResult::error; + else if (logCount.warnings > 0) + return TaskResult::warning; + + if (getTotalStats() == ProgressStats()) + logMsg(errorLog_.ref(), _("Nothing to synchronize"), MSG_TYPE_INFO); + return TaskResult::success; + }(); + + assert(*syncResult_ == TaskResult::cancelled || currentPhase() == ProcessPhase::sync); + + const ProcessSummary summary + { + startTime_, *syncResult_, jobNames_, + getCurrentStats(), + getTotalStats (), + totalTime + }; + + return {summary, errorLog_}; +} + + +StatusHandlerFloatingDialog::DlgOptions StatusHandlerFloatingDialog::showResult() +{ + bool autoClose = false; + bool suspend = false; + FinalRequest finalRequest = FinalRequest::none; + + if (taskCancelled()) + assert(*taskCancelled() == CancelReason::user); //"stop on first error" is only for ffs_batch + else + { + //--------------------- post sync actions ---------------------- + //give user chance to cancel shutdown; do *not* consider the sync itself cancelled + auto proceedWithShutdown = [&](const std::wstring& operationName) + { + if (progressDlg_->getWindowIfVisible()) + try + { + assert(!endsWith(operationName, L".")); + auto notifyStatus = [&](const std::wstring& timeRemMsg) { updateStatus(operationName + L"... " + timeRemMsg); /*throw CancelProcess*/ }; + + delayAndCountDown(std::chrono::seconds(10), notifyStatus); //throw CancelProcess + } + catch (CancelProcess&) { return false; } + + return true; + }; + + switch (progressDlg_->getAndFreezePostSyncAction()) + { + case PostSyncAction::none: + autoClose = progressDlg_->getOptionAutoCloseDialog(); + break; + case PostSyncAction::exit: + autoClose = true; + finalRequest = FinalRequest::exit; //program exit must be handled by calling context! + break; + case PostSyncAction::sleep: + if (proceedWithShutdown(_("System: Sleep"))) + { + autoClose = progressDlg_->getOptionAutoCloseDialog(); + suspend = true; + } + break; + case PostSyncAction::shutdown: + if (proceedWithShutdown(_("System: Shut down"))) + { + autoClose = true; + finalRequest = FinalRequest::shutdown; //system shutdown must be handled by calling context! + } + break; + } + } + + if (suspend) //*before* showing results dialog + try + { + suspendSystem(); //throw FileError + } + catch (const FileError& e) { logMsg(errorLog_.ref(), e.toString(), MSG_TYPE_ERROR); } + + //--------------------- sound notification ---------------------- + if (!taskCancelled() && !suspend && !autoClose && //only play when actually showing results dialog + !soundFileSyncComplete_.empty()) + { + //wxWidgets shows modal error dialog by default => "no, wxWidgets, NO!" + wxLog* oldLogTarget = wxLog::SetActiveTarget(new wxLogStderr); //transfer and receive ownership! + ZEN_ON_SCOPE_EXIT(delete wxLog::SetActiveTarget(oldLogTarget)); + + wxSound::Play(utfTo(soundFileSyncComplete_), wxSOUND_ASYNC); + } + //if (::GetForegroundWindow() != GetHWND()) + // RequestUserAttention(); -> probably too much since task bar is already colorized with Taskbar::Status::error or Status::normal + + const auto [autoCloseSelected, dim] = progressDlg_->destroy(autoClose, + finalRequest == FinalRequest::none /*restoreParentFrame*/, + *syncResult_, errorLog_); + //caveat: calls back to getErrorStats() => *share* (and not move) errorLog_ + progressDlg_ = nullptr; + + return {autoCloseSelected, dim, finalRequest}; +} + + +void StatusHandlerFloatingDialog::initNewPhase(int itemsTotal, int64_t bytesTotal, ProcessPhase phaseID) +{ + assert(phaseID == ProcessPhase::sync); + StatusHandler::initNewPhase(itemsTotal, bytesTotal, phaseID); + progressDlg_->initNewPhase(); //call after "StatusHandler::initNewPhase" + + //macOS needs a full yield to update GUI and get rid of "dummy" texts + requestUiUpdate(true /*force*/); //throw CancelProcess +} + + + +void StatusHandlerFloatingDialog::logMessage(const std::wstring& msg, MsgType type) +{ + logMsg(errorLog_.ref(), msg, [&] + { + switch (type) + { + case MsgType::info: return MSG_TYPE_INFO; + case MsgType::warning: return MSG_TYPE_WARNING; + case MsgType::error: return MSG_TYPE_ERROR; + } + assert(false); + return MSG_TYPE_ERROR; + }()); + requestUiUpdate(false /*force*/); //throw CancelProcess +} + + +void StatusHandlerFloatingDialog::reportWarning(const std::wstring& msg, bool& warningActive) +{ + PauseTimers dummy(*progressDlg_); + + logMsg(errorLog_.ref(), msg, MSG_TYPE_WARNING); + + if (!warningActive) + return; + + if (!progressDlg_->getOptionIgnoreErrors()) + { + forceUiUpdateNoThrow(); //noexcept! => don't throw here when error occurs during clean up! + + bool dontWarnAgain = false; + switch (showConfirmationDialog(progressDlg_->getWindowIfVisible(), DialogInfoType::warning, + PopupDialogCfg().setDetailInstructions(msg). + alertWhenPending(soundFileAlertPending_). + setCheckBox(dontWarnAgain, _("&Don't show this warning again")), + _("&Ignore"))) + { + case ConfirmationButton::accept: + warningActive = !dontWarnAgain; + break; + case ConfirmationButton::cancel: + cancelProcessNow(CancelReason::user); //throw CancelProcess + break; + } + } + //else: if errors are ignored, then warnings should be, too +} + + +ProcessCallback::Response StatusHandlerFloatingDialog::reportError(const ErrorInfo& errorInfo) +{ + PauseTimers dummy(*progressDlg_); + + //log actual fail time (not "now"!) + const time_t failTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now() - + std::chrono::duration_cast(std::chrono::steady_clock::now() - errorInfo.failTime)); + //auto-retry + if (errorInfo.retryNumber < autoRetryCount_) + { + logMsg(errorLog_.ref(), errorInfo.msg + L"\n-> " + _("Automatic retry"), MSG_TYPE_INFO, failTime); + delayAndCountDown(errorInfo.failTime + autoRetryDelay_ - std::chrono::steady_clock::now(), + [&, statusPrefix = _("Automatic retry") + + (errorInfo.retryNumber == 0 ? L"" : L' ' + formatNumber(errorInfo.retryNumber + 1)) + SPACED_DASH, + statusPostfix = SPACED_DASH + _("Error") + L": " + replaceCpy(errorInfo.msg, L'\n', L' ')](const std::wstring& timeRemMsg) + { this->updateStatus(statusPrefix + timeRemMsg + statusPostfix); }); //throw CancelProcess + return ProcessCallback::retry; + } + + //always, except for "retry": + auto guardWriteLog = makeGuard([&] { logMsg(errorLog_.ref(), errorInfo.msg, MSG_TYPE_ERROR, failTime); }); + + if (!progressDlg_->getOptionIgnoreErrors()) + { + forceUiUpdateNoThrow(); //noexcept! => don't throw here when error occurs during clean up! + + switch (showConfirmationDialog(progressDlg_->getWindowIfVisible(), DialogInfoType::error, + PopupDialogCfg().setDetailInstructions(errorInfo.msg). + alertWhenPending(soundFileAlertPending_), + _("&Ignore"), _("Ignore &all"), _("&Retry"))) + { + case ConfirmationButton3::accept: //ignore + return ProcessCallback::ignore; + + case ConfirmationButton3::accept2: //ignore all + progressDlg_->setOptionIgnoreErrors(true); + return ProcessCallback::ignore; + + case ConfirmationButton3::decline: //retry + guardWriteLog.dismiss(); + logMsg(errorLog_.ref(), errorInfo.msg + L"\n-> " + _("Retrying operation..."), //explain why there are duplicate "doing operation X" info messages in the log! + MSG_TYPE_INFO, failTime); + return ProcessCallback::retry; + + case ConfirmationButton3::cancel: + cancelProcessNow(CancelReason::user); //throw CancelProcess + break; + } + } + else + return ProcessCallback::ignore; + + assert(false); + return ProcessCallback::ignore; //dummy value +} + + +void StatusHandlerFloatingDialog::reportFatalError(const std::wstring& msg) +{ + PauseTimers dummy(*progressDlg_); + + logMsg(errorLog_.ref(), msg, MSG_TYPE_ERROR); + + if (!progressDlg_->getOptionIgnoreErrors()) + { + forceUiUpdateNoThrow(); //noexcept! => don't throw here when error occurs during clean up! + + switch (showConfirmationDialog(progressDlg_->getWindowIfVisible(), DialogInfoType::error, + PopupDialogCfg().setDetailInstructions(msg). + alertWhenPending(soundFileAlertPending_), + _("&Ignore"), _("Ignore &all"))) + { + case ConfirmationButton2::accept: //ignore + break; + + case ConfirmationButton2::accept2: //ignore all + progressDlg_->setOptionIgnoreErrors(true); + break; + + case ConfirmationButton2::cancel: + cancelProcessNow(CancelReason::user); //throw CancelProcess + break; + } + } +} + + +Statistics::ErrorStats StatusHandlerFloatingDialog::getErrorStats() const +{ + //errorLog_ is an "append only" structure, so we can make getErrorStats() complexity "constant time": + std::for_each(errorLog_.ref().begin() + errorStatsRowsChecked_, errorLog_.ref().end(), [&](const LogEntry& entry) + { + switch (entry.type) + { + case MSG_TYPE_INFO: + break; + case MSG_TYPE_WARNING: + ++errorStatsBuf_.warningCount; + break; + case MSG_TYPE_ERROR: + ++errorStatsBuf_.errorCount; + break; + } + }); + errorStatsRowsChecked_ = errorLog_.ref().size(); + + return errorStatsBuf_; +} + + +void StatusHandlerFloatingDialog::updateDataProcessed(int itemsDelta, int64_t bytesDelta) //noexcept! +{ + StatusHandler::updateDataProcessed(itemsDelta, bytesDelta); + + //note: this method should NOT throw in order to properly allow undoing setting of statistics! + progressDlg_->notifyProgressChange(); //noexcept + //for "curveDataBytes_->addRecord()" +} + + +void StatusHandlerFloatingDialog::forceUiUpdateNoThrow() +{ + progressDlg_->updateGui(); +} diff --git a/FreeFileSync/Source/ui/gui_status_handler.h b/FreeFileSync/Source/ui/gui_status_handler.h new file mode 100644 index 0000000..252bd1c --- /dev/null +++ b/FreeFileSync/Source/ui/gui_status_handler.h @@ -0,0 +1,129 @@ +// ***************************************************************************** +// * 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 GUI_STATUS_HANDLER_H_0183247018545 +#define GUI_STATUS_HANDLER_H_0183247018545 + +#include +#include +#include "progress_indicator.h" +#include "main_dlg.h" +#include "../status_handler.h" + + +namespace fff +{ +//classes handling sync and compare errors as well as status feedback + +//internally pumps window messages => disable GUI controls to avoid unexpected callbacks! +class StatusHandlerTemporaryPanel : private wxEvtHandler, public StatusHandler +{ +public: + StatusHandlerTemporaryPanel(MainDialog& dlg, + const std::chrono::system_clock::time_point& startTime, + bool ignoreErrors, + size_t autoRetryCount, + std::chrono::seconds autoRetryDelay, + const Zstring& soundFileAlertPending); + ~StatusHandlerTemporaryPanel(); + + void initNewPhase (int itemsTotal, int64_t bytesTotal, ProcessPhase phaseID) override; // + void logMessage (const std::wstring& msg, MsgType type) override; // + void reportWarning (const std::wstring& msg, bool& warningActive) override; //throw CancelProcess + Response reportError (const ErrorInfo& errorInfo) override; // + void reportFatalError(const std::wstring& msg) override; // + ErrorStats getErrorStats() const override; + + void forceUiUpdateNoThrow() override; + + struct Result + { + ProcessSummary summary; + zen::SharedRef errorLog; + }; + Result prepareResult(); //noexcept!! + +private: + void onLocalKeyEvent(wxKeyEvent& event); + void onAbortCompare(wxCommandEvent& event); //handle abort button click + void showStatsPanel(); + + MainDialog& mainDlg_; + zen::ErrorLog errorLog_; + mutable Statistics::ErrorStats errorStatsBuf_{}; + mutable size_t errorStatsRowsChecked_ = 0; + const bool ignoreErrors_; + const size_t autoRetryCount_; + const std::chrono::seconds autoRetryDelay_; + const Zstring soundFileAlertPending_; + const std::chrono::system_clock::time_point startTime_; + const std::chrono::steady_clock::time_point panelInitTime_ = std::chrono::steady_clock::now(); +}; + + +//StatusHandlerFloatingDialog(SyncProgressDialog) will internally process Window messages! disable GUI controls to avoid unexpected callbacks! +class StatusHandlerFloatingDialog : public StatusHandler +{ +public: + StatusHandlerFloatingDialog(wxFrame* parentDlg, + const std::vector& jobNames, + const std::chrono::system_clock::time_point& startTime, + bool ignoreErrors, + size_t autoRetryCount, + std::chrono::seconds autoRetryDelay, + const Zstring& soundFileSyncComplete, + const Zstring& soundFileAlertPending, + const zen::WindowLayout::Dimensions& dim, + bool autoCloseDialog); //noexcept! + ~StatusHandlerFloatingDialog(); + + void initNewPhase (int itemsTotal, int64_t bytesTotal, ProcessPhase phaseID) override; // + void logMessage (const std::wstring& msg, MsgType type) override; // + void reportWarning (const std::wstring& msg, bool& warningActive) override; //throw CancelProcess + Response reportError (const ErrorInfo& errorInfo) override; // + void reportFatalError(const std::wstring& msg) override; // + ErrorStats getErrorStats() const override; + + void updateDataProcessed(int itemsDelta, int64_t bytesDelta) override; //noexcept!! + void forceUiUpdateNoThrow() override; // + + struct Result + { + ProcessSummary summary; + zen::SharedRef errorLog; + }; + Result prepareResult(); + + enum class FinalRequest + { + none, + exit, + shutdown + }; + struct DlgOptions + { + bool autoCloseSelected; + zen::WindowLayout::Dimensions dim; + FinalRequest finalRequest; + }; + DlgOptions showResult(); + +private: + const std::vector jobNames_; + const std::chrono::system_clock::time_point startTime_; + const size_t autoRetryCount_; + const std::chrono::seconds autoRetryDelay_; + const Zstring soundFileSyncComplete_; + const Zstring soundFileAlertPending_; + SyncProgressDialog* progressDlg_; //managed to have the same lifetime as this handler! + zen::SharedRef errorLog_ = zen::makeSharedRef(); + mutable Statistics::ErrorStats errorStatsBuf_{}; + mutable size_t errorStatsRowsChecked_ = 0; + std::optional syncResult_; +}; +} + +#endif //GUI_STATUS_HANDLER_H_0183247018545 diff --git a/FreeFileSync/Source/ui/log_panel.cpp b/FreeFileSync/Source/ui/log_panel.cpp new file mode 100644 index 0000000..3a0a456 --- /dev/null +++ b/FreeFileSync/Source/ui/log_panel.cpp @@ -0,0 +1,545 @@ +// ***************************************************************************** +// * 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 "log_panel.h" +#include +#include +#include +#include +#include + +using namespace zen; +using namespace fff; + + +namespace +{ +inline wxColor getColorGridLine() { return {192, 192, 192}; } //light grey + + +inline +wxImage getImageButtonPressed(const char* imageName) +{ + return layOver(loadImage("msg_button_pressed"), + loadImage(imageName)); +} + + +inline +wxImage getImageButtonReleased(const char* imageName) +{ + return greyScale(loadImage(imageName)); + //loadImage(utfTo(imageName)).ConvertToGreyscale(1.0/3, 1.0/3, 1.0/3); //treat all channels equally! + //brighten(output, 30); +} + + +enum class ColumnTypeLog +{ + time, + severity, + text, +}; +} + + +//a vector-view on ErrorLog considering multi-line messages: prepare consumption by Grid +class fff::MessageView +{ +public: + explicit MessageView(const SharedRef& log) : log_(log) {} + + size_t rowsOnView() const { return viewRef_.size(); } + + struct LogEntryView + { + time_t time = 0; + MessageType type = MSG_TYPE_INFO; + std::string_view messageLine; + bool firstLine = false; //if LogEntry::message spans multiple rows + }; + + std::optional getEntry(size_t row) const + { + if (row < viewRef_.size()) + { + const Line& line = viewRef_[row]; + + LogEntryView output; + output.time = line.logIt->time; + output.type = line.logIt->type; + output.messageLine = extractLine(line.logIt->message, line.row); + output.firstLine = line.row == 0; //this is virtually always correct, unless first line of the original message is empty! + return output; + } + return {}; + } + + void updateView(int includedTypes) //MSG_TYPE_INFO | MSG_TYPE_WARNING, etc. see error_log.h + { + viewRef_.clear(); + + for (auto it = log_.ref().begin(); it != log_.ref().end(); ++it) + if (it->type & includedTypes) + { + assert(!startsWith(it->message, '\n')); + + size_t rowNumber = 0; + bool lastCharNewline = true; + for (const char c : it->message) + if (c == '\n') + { + if (!lastCharNewline) //do not reference empty lines! + viewRef_.push_back({it, rowNumber}); + ++rowNumber; + lastCharNewline = true; + } + else + lastCharNewline = false; + + if (!lastCharNewline) + viewRef_.push_back({it, rowNumber}); + } + } + +private: + static std::string_view extractLine(const Zstringc& message, size_t textRow) + { + auto it1 = message.begin(); + for (;;) + { + auto it2 = std::find_if(it1, message.end(), [](const char c) { return c == '\n'; }); + if (textRow == 0) + return makeStringView(it1, it2 - it1); + + if (it2 == message.end()) + { + assert(false); + return makeStringView(it1, 0); + } + + it1 = it2 + 1; //skip newline + --textRow; + } + } + + struct Line + { + ErrorLog::const_iterator logIt; //always bound! + size_t row; //LogEntry::message may span multiple rows + }; + + std::vector viewRef_; //partial view on log_ + /* /|\ + | updateView() + | */ + const SharedRef log_; +}; + +//----------------------------------------------------------------------------- +namespace +{ +//Grid data implementation referencing MessageView +class GridDataMessages : public GridData +{ +public: + explicit GridDataMessages(const SharedRef& log) : msgView_(log) {} + + MessageView& getDataView() { return msgView_; } + + size_t getRowCount() const override { return msgView_.rowsOnView(); } + + std::wstring getValue(size_t row, ColumnType colType) const override + { + if (const std::optional entry = msgView_.getEntry(row)) + switch (static_cast(colType)) + { + case ColumnTypeLog::time: + if (entry->firstLine) + return utfTo(formatTime(formatTimeTag, getLocalTime(entry->time))); //empty string on error + break; + + case ColumnTypeLog::severity: + if (entry->firstLine) + switch (entry->type) + { + case MSG_TYPE_INFO: + return _("Info"); + case MSG_TYPE_WARNING: + return _("Warning"); + case MSG_TYPE_ERROR: + return _("Error"); + } + break; + + case ColumnTypeLog::text: + return utfTo(entry->messageLine); + } + return std::wstring(); + } + + void renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected, HoverArea rowHover) override + { + if (!enabled || !selected) + ; //clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); -> already the default + else + GridData::renderRowBackgound(dc, rect, row, true /*enabled*/, true /*selected*/, rowHover); + + //-------------- draw item separation line ----------------- + const bool drawBottomLine = [&] //don't separate multi-line messages + { + if (std::optional nextEntry = msgView_.getEntry(row + 1)) + return nextEntry->firstLine; + return true; + }(); + + if (drawBottomLine) + clearArea(dc, {rect.x, rect.y + rect.height - dipToWxsize(1), rect.width, dipToWxsize(1)}, getColorGridLine()); + //-------------------------------------------------------- + } + + void renderCell(wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover) override + { + wxDCTextColourChanger textColor(dc); + if (enabled && selected) //accessibility: always set *both* foreground AND background colors! + textColor.Set(*wxBLACK); + + wxRect rectTmp = rect; + + if (std::optional entry = msgView_.getEntry(row)) + switch (static_cast(colType)) + { + case ColumnTypeLog::time: + drawCellText(dc, rectTmp, getValue(row, colType), wxALIGN_CENTER); + break; + + case ColumnTypeLog::severity: + if (entry->firstLine) + { + wxImage msgTypeIcon = [&] + { + switch (entry->type) + { + case MSG_TYPE_INFO: + return loadImage("msg_info", dipToScreen(getMenuIconDipSize())); + case MSG_TYPE_WARNING: + return loadImage("msg_warning", dipToScreen(getMenuIconDipSize())); + case MSG_TYPE_ERROR: + return loadImage("msg_error", dipToScreen(getMenuIconDipSize())); + } + assert(false); + return wxNullImage; + }(); + drawBitmapRtlNoMirror(dc, enabled ? msgTypeIcon : msgTypeIcon.ConvertToDisabled(), rectTmp, wxALIGN_CENTER); + } + break; + + case ColumnTypeLog::text: + rectTmp.x += getColumnGapLeft(); + rectTmp.width -= getColumnGapLeft(); + drawCellText(dc, rectTmp, getValue(row, colType)); + break; + } + } + + int getBestSize(const wxReadOnlyDC& dc, size_t row, ColumnType colType) override + { + // -> synchronize renderCell() <-> getBestSize() + + if (msgView_.getEntry(row)) + switch (static_cast(colType)) + { + case ColumnTypeLog::time: + return 2 * getColumnGapLeft() + dc.GetTextExtent(getValue(row, colType)).GetWidth(); + + case ColumnTypeLog::severity: + return dipToWxsize(getMenuIconDipSize()); + + case ColumnTypeLog::text: + return getColumnGapLeft() + dc.GetTextExtent(getValue(row, colType)).GetWidth(); + } + return 0; + } + + static int getColumnTimeDefaultWidth(Grid& grid) + { + wxInfoDC dc(&grid.getMainWin()); + dc.SetFont(grid.getMainWin().GetFont()); + return 2 * getColumnGapLeft() + dc.GetTextExtent(utfTo(formatTime(formatTimeTag))).GetWidth(); + } + + static int getColumnSeverityDefaultWidth() + { + return dipToWxsize(getMenuIconDipSize()); + } + + static int getRowDefaultHeight(const Grid& grid) + { + return std::max(dipToWxsize(getMenuIconDipSize()), grid.getMainWin().GetCharHeight() + dipToWxsize(2) /*extra space*/) + dipToWxsize(1) /*bottom border*/; + } + + std::wstring getToolTip(size_t row, ColumnType colType, HoverArea rowHover) override + { + switch (static_cast(colType)) + { + case ColumnTypeLog::time: + case ColumnTypeLog::text: + break; + + case ColumnTypeLog::severity: + return getValue(row, colType); + } + return std::wstring(); + } + + std::wstring getColumnLabel(ColumnType colType) const override { return std::wstring(); } + +private: + MessageView msgView_; +}; +} + +//######################################################################################## + +LogPanel::LogPanel(wxWindow* parent) : LogPanelGenerated(parent) +{ + const int rowHeight = GridDataMessages::getRowDefaultHeight(*m_gridMessages); + const int colMsgTimeWidth = GridDataMessages::getColumnTimeDefaultWidth(*m_gridMessages); + const int colMsgSeverityWidth = GridDataMessages::getColumnSeverityDefaultWidth(); + + m_gridMessages->setColumnLabelHeight(0); + m_gridMessages->showRowLabel(false); + m_gridMessages->setRowHeight(rowHeight); + m_gridMessages->setColumnConfig( + { + {static_cast(ColumnTypeLog::time ), colMsgTimeWidth, 0, true}, + {static_cast(ColumnTypeLog::severity), colMsgSeverityWidth, 0, true}, + {static_cast(ColumnTypeLog::text ), -colMsgTimeWidth - colMsgSeverityWidth, 1, true}, + }); + + //support for CTRL + C + m_gridMessages->Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onGridKeyEvent(event); }); + + m_gridMessages->Bind(EVENT_GRID_CONTEXT_MENU, [this](GridContextMenuEvent& event) { onMsgGridContext(event); }); + + Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onLocalKeyEvent(event); }); //enable dialog-specific key events + + setLog(nullptr); +} + + +void LogPanel::setLog(const std::shared_ptr& log) +{ + SharedRef newLog = [&] + { + if (log) + return SharedRef(log); + + ErrorLog dummyLog; + logMsg(dummyLog, _("No log entries"), MSG_TYPE_INFO); + return makeSharedRef(std::move(dummyLog)); + }(); + + const ErrorLogStats logCount = getStats(newLog.ref()); + + auto initButton = [](ToggleButton& btn, const char* imgName, const wxString& tooltip) + { + btn.init(getImageButtonPressed(imgName), getImageButtonReleased(imgName)); + btn.SetToolTip(tooltip); + }; + + initButton(*m_bpButtonErrors, "msg_error", _("Error" ) + L" (" + formatNumber(logCount.errors) + L')'); + initButton(*m_bpButtonWarnings, "msg_warning", _("Warning") + L" (" + formatNumber(logCount.warnings) + L')'); + initButton(*m_bpButtonInfo, "msg_info", _("Info" ) + L" (" + formatNumber(logCount.infos) + L')'); + + m_bpButtonErrors ->setActive(true); + m_bpButtonWarnings->setActive(true); + m_bpButtonInfo ->setActive(logCount.warnings + logCount.errors == 0); + + m_bpButtonErrors ->Show(logCount.errors != 0); + m_bpButtonWarnings->Show(logCount.warnings != 0); + m_bpButtonInfo ->Show(logCount.infos != 0); + + m_gridMessages->setDataProvider(std::make_shared(newLog)); + + updateGrid(); +} + + +MessageView& LogPanel::getDataView() +{ + if (auto* prov = dynamic_cast(m_gridMessages->getDataProvider())) + return prov->getDataView(); + throw std::runtime_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] m_gridMessages was not initialized."); +} + + +void LogPanel::updateGrid() +{ + int includedTypes = 0; + if (m_bpButtonErrors->isActive()) + includedTypes |= MSG_TYPE_ERROR; + + if (m_bpButtonWarnings->isActive()) + includedTypes |= MSG_TYPE_WARNING; + + if (m_bpButtonInfo->isActive()) + includedTypes |= MSG_TYPE_INFO; + + getDataView().updateView(includedTypes); //update MVC "model" + m_gridMessages->Refresh(); //update MVC "view" +} + +void LogPanel::onErrors(wxCommandEvent& event) +{ + m_bpButtonErrors->toggle(); + updateGrid(); +} + + +void LogPanel::onWarnings(wxCommandEvent& event) +{ + m_bpButtonWarnings->toggle(); + updateGrid(); +} + + +void LogPanel::onInfo(wxCommandEvent& event) +{ + m_bpButtonInfo->toggle(); + updateGrid(); +} + + +void LogPanel::onMsgGridContext(GridContextMenuEvent& event) +{ + const std::vector selection = m_gridMessages->getSelectedRows(); + + const size_t rowCount = [&]() -> size_t + { + if (auto prov = m_gridMessages->getDataProvider()) + return prov->getRowCount(); + return 0; + }(); + + ContextMenu menu; + menu.addItem(_("&Copy") + L"\tCtrl+C", [this] { copySelectionToClipboard(); }, loadImage("item_copy_sicon"), !selection.empty()); + menu.addSeparator(); + + menu.addItem(_("Select all") + L"\tCtrl+A", [this] { m_gridMessages->selectAllRows(GridEventPolicy::allow); }, wxNullImage, rowCount > 0); + + menu.popup(*m_gridMessages, event.mousePos_); +} + + +void LogPanel::onGridKeyEvent(wxKeyEvent& event) +{ + int keyCode = event.GetKeyCode(); + + if (event.ControlDown()) + switch (keyCode) + { + case 'C': + case WXK_INSERT: //CTRL + C || CTRL + INS + case WXK_NUMPAD_INSERT: + copySelectionToClipboard(); + return; // -> swallow event! don't allow default grid commands! + } + + //else + //switch (keyCode) + //{ + // case WXK_RETURN: + // case WXK_NUMPAD_ENTER: + // return; + //} + + event.Skip(); //unknown keypress: propagate +} + + +void LogPanel::onLocalKeyEvent(wxKeyEvent& event) //process key events without explicit menu entry :) +{ + if (!processingKeyEventHandler_) //avoid recursion + { + processingKeyEventHandler_ = true; + ZEN_ON_SCOPE_EXIT(processingKeyEventHandler_ = false); + + const int keyCode = event.GetKeyCode(); + + if (event.ControlDown()) + switch (keyCode) + { + case 'A': + m_gridMessages->SetFocus(); + m_gridMessages->selectAllRows(GridEventPolicy::allow); + return; // -> swallow event! don't allow default grid commands! + } + else + switch (keyCode) + { + //redirect certain (unhandled) keys directly to grid! + case WXK_UP: + case WXK_DOWN: + case WXK_LEFT: + case WXK_RIGHT: + case WXK_PAGEUP: + case WXK_PAGEDOWN: + case WXK_HOME: + case WXK_END: + + case WXK_NUMPAD_UP: + case WXK_NUMPAD_DOWN: + case WXK_NUMPAD_LEFT: + case WXK_NUMPAD_RIGHT: + case WXK_NUMPAD_PAGEUP: + case WXK_NUMPAD_PAGEDOWN: + case WXK_NUMPAD_HOME: + case WXK_NUMPAD_END: + if (!isComponentOf(wxWindow::FindFocus(), m_gridMessages) && //don't propagate keyboard commands if grid is already in focus + m_gridMessages->IsEnabled()) + { + m_gridMessages->SetFocus(); + + event.SetEventType(wxEVT_KEY_DOWN); //the grid event handler doesn't expect wxEVT_CHAR_HOOK! + m_gridMessages->getMainWin().GetEventHandler()->ProcessEvent(event); //propagating event catched at wxTheApp to child leads to recursion, but we prevented it... + event.Skip(false); //definitively handled now! + return; + } + break; + } + } + event.Skip(); +} + + +void LogPanel::copySelectionToClipboard() +{ + try + { + wxString clipBuf; //perf: old wxString didn't model exponential growth, but now it's std::string-based: + static_assert(std::is_same_v); + + if (auto prov = m_gridMessages->getDataProvider()) + { + std::vector colAttr = m_gridMessages->getColumnConfig(); + std::erase_if(colAttr, [](const Grid::ColAttributes& ca) { return !ca.visible; }); + + for (size_t row : m_gridMessages->getSelectedRows()) + for (auto it = colAttr.begin(); it != colAttr.end(); ++it) + { + clipBuf += prov->getValue(row, it->type); + clipBuf += it == colAttr.end() - 1 ? L'\n' : L'\t'; + } + } + + setClipboardText(clipBuf); + } + catch (const std::bad_alloc& e) + { + showNotificationDialog(nullptr, DialogInfoType::error, PopupDialogCfg().setMainInstructions(_("Out of memory.") + L' ' + utfTo(e.what()))); + } +} diff --git a/FreeFileSync/Source/ui/log_panel.h b/FreeFileSync/Source/ui/log_panel.h new file mode 100644 index 0000000..6bfeff9 --- /dev/null +++ b/FreeFileSync/Source/ui/log_panel.h @@ -0,0 +1,43 @@ +// ***************************************************************************** +// * 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 LOG_PANEL_3218470817450193 +#define LOG_PANEL_3218470817450193 + +#include +#include "gui_generated.h" +#include + + +namespace fff +{ +class MessageView; + +class LogPanel : public LogPanelGenerated +{ +public: + explicit LogPanel(wxWindow* parent); + + void setLog(const std::shared_ptr& log); + +private: + MessageView& getDataView(); + void updateGrid(); + + void onErrors (wxCommandEvent& event) override; + void onWarnings(wxCommandEvent& event) override; + void onInfo (wxCommandEvent& event) override; + void onMsgGridContext (zen::GridContextMenuEvent& event); + void onGridKeyEvent (wxKeyEvent& event); + void onLocalKeyEvent(wxKeyEvent& event); + + void copySelectionToClipboard(); + + bool processingKeyEventHandler_ = false; +}; +} + +#endif //LOG_PANEL_3218470817450193 diff --git a/FreeFileSync/Source/ui/main_dlg.cpp b/FreeFileSync/Source/ui/main_dlg.cpp new file mode 100644 index 0000000..62b7252 --- /dev/null +++ b/FreeFileSync/Source/ui/main_dlg.cpp @@ -0,0 +1,6497 @@ +// ***************************************************************************** +// * 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 "main_dlg.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "cfg_grid.h" +#include "folder_selector.h" +#include "tree_grid.h" +#include "version_check.h" +#include "gui_status_handler.h" +#include "small_dlgs.h" +#include "rename_dlg.h" +#include "folder_pair.h" +#include "search_grid.h" +#include "batch_config.h" +#include "app_icon.h" +#include "../base_tools.h" +#include "../afs/concrete.h" +#include "../afs/native.h" +#include "../base/comparison.h" +#include "../base/algorithm.h" +#include "../base/lock_holder.h" +#include "../base/icon_loader.h" +#include "../ffs_paths.h" +#include "../localization.h" +#include "../version/version.h" +#include "../afs/gdrive.h" + +using namespace zen; +using namespace fff; + + +namespace +{ +const size_t EXT_APP_MASS_INVOKE_THRESHOLD = 10; //more is likely a user mistake (Explorer uses limit of 15) +const size_t EXT_APP_MAX_TOTAL_WAIT_TIME_MS = 1000; + +const int TOP_BUTTON_OPTIMAL_WIDTH_DIP = 170; +constexpr std::chrono::milliseconds LAST_USED_CFG_EXISTENCE_CHECK_TIME_MAX(500); +constexpr std::chrono::milliseconds FILE_GRID_POST_UPDATE_DELAY(400); + +const ZstringView macroNameItemPath = Zstr("%item_path%"); +const ZstringView macroNameItemPath2 = Zstr("%item_path2%"); +const ZstringView macroNameItemPaths = Zstr("%item_paths%"); +const ZstringView macroNameLocalPath = Zstr("%local_path%"); +const ZstringView macroNameLocalPath2 = Zstr("%local_path2%"); +const ZstringView macroNameLocalPaths = Zstr("%local_paths%"); +const ZstringView macroNameItemName = Zstr("%item_name%"); +const ZstringView macroNameItemName2 = Zstr("%item_name2%"); +const ZstringView macroNameItemNames = Zstr("%item_names%"); +const ZstringView macroNameParentPath = Zstr("%parent_path%"); +const ZstringView macroNameParentPath2 = Zstr("%parent_path2%"); +const ZstringView macroNameParentPaths = Zstr("%parent_paths%"); + +bool containsFileItemMacro(const Zstring& commandLinePhrase) +{ + return contains(commandLinePhrase, macroNameItemPath ) || + contains(commandLinePhrase, macroNameItemPath2 ) || + contains(commandLinePhrase, macroNameItemPaths ) || + contains(commandLinePhrase, macroNameLocalPath ) || + contains(commandLinePhrase, macroNameLocalPath2 ) || + contains(commandLinePhrase, macroNameLocalPaths ) || + contains(commandLinePhrase, macroNameItemName ) || + contains(commandLinePhrase, macroNameItemName2 ) || + contains(commandLinePhrase, macroNameItemNames ) || + contains(commandLinePhrase, macroNameParentPath ) || + contains(commandLinePhrase, macroNameParentPath2) || + contains(commandLinePhrase, macroNameParentPaths); +} + +//let's NOT create wxWidgets objects statically: +wxColor getColorHighlightCompareButton() { return wxSystemSettings::GetAppearance().IsDark() ? wxColor{0, 0, 0x80} : wxColor{236, 236, 255}; } //dark + light blue +wxColor getColorHighlightSyncButton () { return wxSystemSettings::GetAppearance().IsDark() ? wxColor{0, 0x40, 0} : wxColor{230, 255, 215}; } //dark + light green + +wxColor getColorAuiPanelCaptionText() { return wxSystemSettings::GetAppearance().IsDark() ? 0xdadada : 0xffffff; } +wxColor getColorAuiPanelCaptionBack() { return wxSystemSettings::GetAppearance().IsDark() ? wxColor{0, 0x4f, 0x8e} : wxColor{51, 147, 223}; } //dark + medium blue +wxColor getColorAuiPanelCaptionBackGradient() { return wxSystemSettings::GetAppearance().IsDark() ? wxColor{0, 0x3d, 0x6e} : wxColor{ 0, 120, 215}; } //dark + medium blue + +wxColor getColorFlashStatusInfo() +{ + return enhanceContrast({31, 57, 226} /*blue*/, wxWindow::GetClassDefaultAttributes(wxWindowVariant::wxWINDOW_VARIANT_NORMAL).colBg, + 5 /*contrastRatioMin*/); //W3C recommends >= 4.5 +} + +IconBuffer::IconSize convert(GridIconSize isize) +{ + switch (isize) + { + case GridIconSize::small: + return IconBuffer::IconSize::small; + case GridIconSize::medium: + return IconBuffer::IconSize::medium; + case GridIconSize::large: + return IconBuffer::IconSize::large; + } + return IconBuffer::IconSize::small; +} + + +bool acceptDialogFileDrop(const std::vector& shellItemPaths) +{ + return std::any_of(shellItemPaths.begin(), shellItemPaths.end(), [](const Zstring& shellItemPath) + { + const Zstring ext = getFileExtension(shellItemPath); + return equalAsciiNoCase(ext, "ffs_gui") || + equalAsciiNoCase(ext, "ffs_batch"); + }); +} + + + +FfsGuiConfig getDefaultGuiConfig(const FilterConfig& defaultFilter) +{ + FfsGuiConfig defaultCfg; + + //set default file filter: this is only ever relevant when creating new configurations! + //a default FfsGuiConfig does not need these user-specific exclusions! + defaultCfg.mainCfg.globalFilter = defaultFilter; + + return defaultCfg; +} +} + +//------------------------------------------------------------------ +/* class hierarchy: + + template<> + FolderPairPanelBasic + /|\ + | + template<> + FolderPairCallback FolderPairPanelGenerated + /|\ /|\ + _________|_________ ________| + | | | + FolderPairFirst FolderPairPanel +*/ + +template +class fff::FolderPairCallback : public FolderPairPanelBasic //implements callback functionality to MainDialog as imposed by FolderPairPanelBasic +{ +public: + FolderPairCallback(GuiPanel& basicPanel, MainDialog& mainDlg, + + wxPanel& dropWindow1L, + wxPanel& dropWindow1R, + wxButton& selectFolderButtonL, + wxButton& selectFolderButtonR, + wxButton& selectSftpButtonL, + wxButton& selectSftpButtonR, + FolderHistoryBox& dirpathL, + FolderHistoryBox& dirpathR, + Zstring& folderLastSelectedL, + Zstring& folderLastSelectedR, + Zstring& sftpKeyFileLastSelected, + wxStaticText* staticTextL, + wxStaticText* staticTextR, + wxWindow* dropWindow2L, + wxWindow* dropWindow2R) : + FolderPairPanelBasic(basicPanel), //pass FolderPairPanelGenerated part... + mainDlg_(mainDlg), + folderSelectorLeft_ (&mainDlg, dropWindow1L, selectFolderButtonL, selectSftpButtonL, dirpathL, folderLastSelectedL, + sftpKeyFileLastSelected, staticTextL, dropWindow2L, droppedPathsFilter_, getDeviceParallelOps_, setDeviceParallelOps_), + folderSelectorRight_(&mainDlg, dropWindow1R, selectFolderButtonR, selectSftpButtonR, dirpathR, folderLastSelectedR, + sftpKeyFileLastSelected, staticTextR, dropWindow2R, droppedPathsFilter_, getDeviceParallelOps_, setDeviceParallelOps_) + { + folderSelectorLeft_ .setSiblingSelector(&folderSelectorRight_); + folderSelectorRight_.setSiblingSelector(&folderSelectorLeft_); + + folderSelectorLeft_ .Bind(EVENT_ON_FOLDER_SELECTED, [&mainDlg](wxCommandEvent& event) { mainDlg.onFolderSelected(event); }); + folderSelectorRight_.Bind(EVENT_ON_FOLDER_SELECTED, [&mainDlg](wxCommandEvent& event) { mainDlg.onFolderSelected(event); }); + } + + void setValues(const LocalPairConfig& lpc) + { + this->setConfig(lpc.localCmpCfg, lpc.localSyncCfg, lpc.localFilter); + folderSelectorLeft_ .setPath(lpc.folderPathPhraseLeft); + folderSelectorRight_.setPath(lpc.folderPathPhraseRight); + } + + LocalPairConfig getValues() const + { + return + { + folderSelectorLeft_ .getPath(), + folderSelectorRight_.getPath(), + this->getCompConfig(), + this->getSyncConfig(), + this->getFilterConfig() + }; + } + +private: + MainConfiguration getMainConfig() const override { return mainDlg_.getConfig().mainCfg; } + wxWindow* getParentWindow() override { return &mainDlg_; } + + void onLocalCompCfgChange () override { mainDlg_.applyCompareConfig(false /*setDefaultViewType*/); } + void onLocalSyncCfgChange () override { mainDlg_.applySyncDirections(); } + void onLocalFilterCfgChange() override { mainDlg_.applyFilterConfig(); } //re-apply filter + + const std::function& shellItemPaths)> droppedPathsFilter_ = [&](const std::vector& shellItemPaths) + { + if (acceptDialogFileDrop(shellItemPaths)) + { + assert(!shellItemPaths.empty()); + mainDlg_.loadConfiguration(shellItemPaths); + return false; //don't set dropped paths + } + return true; //do set dropped paths + }; + + const std::function getDeviceParallelOps_ = [&](const Zstring& folderPathPhrase) + { + return getDeviceParallelOps(mainDlg_.currentCfg_.mainCfg.deviceParallelOps, folderPathPhrase); + }; + + const std::function setDeviceParallelOps_ = [&](const Zstring& folderPathPhrase, size_t parallelOps) + { + setDeviceParallelOps(mainDlg_.currentCfg_.mainCfg.deviceParallelOps, folderPathPhrase, parallelOps); + mainDlg_.updateUnsavedCfgStatus(); + }; + + MainDialog& mainDlg_; + FolderSelector folderSelectorLeft_; + FolderSelector folderSelectorRight_; +}; + + +class fff::FolderPairPanel : + public FolderPairPanelGenerated, //FolderPairPanel "owns" FolderPairPanelGenerated! + public FolderPairCallback +{ +public: + FolderPairPanel(wxWindow* parent, + MainDialog& mainDlg, + Zstring& folderLastSelectedL, + Zstring& folderLastSelectedR, + Zstring& sftpKeyFileLastSelected) : + FolderPairPanelGenerated(parent), + FolderPairCallback(static_cast(*this), mainDlg, + + *m_panelLeft, + *m_panelRight, + *m_buttonSelectFolderLeft, + *m_buttonSelectFolderRight, + *m_bpButtonSelectAltFolderLeft, + *m_bpButtonSelectAltFolderRight, + *m_folderPathLeft, + *m_folderPathRight, + folderLastSelectedL, + folderLastSelectedR, + sftpKeyFileLastSelected, + nullptr /*staticText*/, nullptr /*staticText*/, + nullptr /*dropWindow2*/, nullptr /*dropWindow2*/) {} +}; + + +class fff::FolderPairFirst : public FolderPairCallback +{ +public: + FolderPairFirst(MainDialog& mainDlg, + Zstring& folderLastSelectedL, + Zstring& folderLastSelectedR, + Zstring& sftpKeyFileLastSelected) : + FolderPairCallback(mainDlg, mainDlg, + + *mainDlg.m_panelTopLeft, + *mainDlg.m_panelTopRight, + *mainDlg.m_buttonSelectFolderLeft, + *mainDlg.m_buttonSelectFolderRight, + *mainDlg.m_bpButtonSelectAltFolderLeft, + *mainDlg.m_bpButtonSelectAltFolderRight, + *mainDlg.m_folderPathLeft, + *mainDlg.m_folderPathRight, + folderLastSelectedL, + folderLastSelectedR, + sftpKeyFileLastSelected, + mainDlg.m_staticTextResolvedPathL, + mainDlg.m_staticTextResolvedPathR, + &mainDlg.m_gridMainL->getMainWin(), + &mainDlg.m_gridMainR->getMainWin()) {} +}; + + + +//--------------------------------------------------------------------------------------------- +class MainDialog::UiInputDisabler +{ +public: + UiInputDisabler(MainDialog& mainDlg, bool enableAbort) : mainDlg_(mainDlg) + { + disableGuiElementsImpl(enableAbort); + } + + ~UiInputDisabler() + { + if (!dismissed_ ) + { + wxTheApp->Yield(); //GUI update before enabling buttons again: prevent strange behaviour of delayed button clicks + enableGuiElementsImpl(); + } + } + + void dismiss() { dismissed_ = true; } + +private: + UiInputDisabler (const UiInputDisabler&) = delete; + UiInputDisabler& operator=(const UiInputDisabler&) = delete; + + void disableGuiElementsImpl(bool enableAbort); //dis-/enable all elements (except abort button) that might receive unwanted user input + void enableGuiElementsImpl(); //during long-running processes: comparison, deletion + + MainDialog& mainDlg_; + bool dismissed_ = false; +}; + + +void MainDialog::UiInputDisabler::disableGuiElementsImpl(bool enableAbort) +{ + //disables all elements (except abort button) that might receive user input during long-running processes: + //when changing consider: comparison, synchronization, manual deletion + + //OS X: wxWidgets portability promise is again a mess: http://wxwidgets.10942.n7.nabble.com/Disable-panel-and-appropriate-children-windows-linux-macos-td35357.html + + mainDlg_.EnableCloseButton(false); //closing main dialog is not allowed during synchronization! crash! + //EnableCloseButton(false) just does not work reliably! + //- Windows: dialog can still be closed by clicking the task bar preview window with the middle mouse button or by pressing ALT+F4! + //- OS X: Quit/Preferences menu items still enabled during sync, + // ([[m_macWindow standardWindowButton:NSWindowCloseButton] setEnabled:enable]) does not stick after calling Maximize() ([m_macWindow zoom:nil]) + //- Linux: it just works! :) + + + for (size_t pos = 0; pos < mainDlg_.m_menubar->GetMenuCount(); ++pos) + mainDlg_.m_menubar->EnableTop(pos, false); + + if (enableAbort) + { + mainDlg_.m_buttonCancel->Enable(); + mainDlg_.m_buttonCancel->Show(); + //if (m_buttonCancel->IsShownOnScreen()) -> needed? + mainDlg_.m_buttonCancel->SetFocus(); + mainDlg_.m_buttonCompare->Disable(); + mainDlg_.m_buttonCompare->Hide(); + mainDlg_.m_panelTopButtons->Layout(); + + mainDlg_.m_bpButtonCmpConfig ->Disable(); + mainDlg_.m_bpButtonCmpContext ->Disable(); + mainDlg_.m_bpButtonFilter ->Disable(); + mainDlg_.m_bpButtonFilterContext->Disable(); + mainDlg_.m_bpButtonSyncConfig ->Disable(); + mainDlg_.m_bpButtonSyncContext->Disable(); + mainDlg_.m_buttonSync ->Disable(); + } + else + mainDlg_.m_panelTopButtons->Disable(); + + mainDlg_.m_panelDirectoryPairs->Disable(); + mainDlg_.m_gridOverview ->Disable(); + mainDlg_.m_panelCenter ->Disable(); + mainDlg_.m_panelSearch ->Disable(); + mainDlg_.m_panelLog ->Disable(); + mainDlg_.m_panelConfig ->Disable(); + mainDlg_.m_panelViewFilter ->Disable(); + + mainDlg_.Refresh(); //wxWidgets fails to do this automatically for child items of disabled windows +} + + +void MainDialog::UiInputDisabler::enableGuiElementsImpl() +{ + //wxGTK, yet another QOI issue: some stupid bug keeps moving main dialog to top!! + mainDlg_.EnableCloseButton(true); + + for (size_t pos = 0; pos < mainDlg_.m_menubar->GetMenuCount(); ++pos) + mainDlg_.m_menubar->EnableTop(pos, true); + + mainDlg_.m_buttonCancel->Disable(); + mainDlg_.m_buttonCancel->Hide(); + mainDlg_.m_buttonCompare->Enable(); + mainDlg_.m_buttonCompare->Show(); + mainDlg_.m_panelTopButtons->Layout(); + + mainDlg_.m_bpButtonCmpConfig ->Enable(); + mainDlg_.m_bpButtonCmpContext ->Enable(); + mainDlg_.m_bpButtonFilter ->Enable(); + mainDlg_.m_bpButtonFilterContext->Enable(); + mainDlg_.m_bpButtonSyncConfig ->Enable(); + mainDlg_.m_bpButtonSyncContext->Enable(); + mainDlg_.m_buttonSync ->Enable(); + + mainDlg_.m_panelTopButtons->Enable(); + + mainDlg_.m_panelDirectoryPairs->Enable(); + mainDlg_.m_gridOverview ->Enable(); + mainDlg_.m_panelCenter ->Enable(); + mainDlg_.m_panelSearch ->Enable(); + mainDlg_.m_panelLog ->Enable(); + mainDlg_.m_panelConfig ->Enable(); + mainDlg_.m_panelViewFilter ->Enable(); + + mainDlg_.Refresh(); + //mainDlg_.auiMgr_.Update(); needed on macOS; 2021-02-01: apparently not anymore! +} +//--------------------------------------------------------------------------------------------- + + +namespace +{ +void updateTopButton(wxBitmapButton& btn, + const wxImage& img, + const wxString& varName, const char* varIconName /*optional*/, + const char* extraIconName /*optional*/, + const wxColor& highlightCol /*optional*/) +{ + const wxColor backCol = highlightCol.IsOk() ? highlightCol : btn.GetBackgroundColour(); + wxImage iconImg = highlightCol.IsOk() ? img : greyScale(img); + + wxImage btnLabelImg = createImageFromText(btn.GetLabelText(), btn.GetFont(), + enhanceContrast(wxSystemSettings::GetColour(wxSYS_COLOUR_BTNTEXT), backCol, 4.5 /*contrastRatioMin*/)); + + wxImage varLabelImg = createImageFromText(varName, wxNORMAL_FONT->Bold(), + enhanceContrast(wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT), backCol, 4.5 /*contrastRatioMin*/)); + wxImage varImg = varLabelImg; + if (varIconName) + { + wxImage varIcon = mirrorIfRtl(loadImage(varIconName, -1 /*maxWidth*/, dipToScreen(getMenuIconDipSize()))); + + varImg = btn.GetLayoutDirection() != wxLayout_RightToLeft ? + stackImages(varLabelImg, varIcon, ImageStackLayout::horizontal, ImageStackAlignment::center, dipToScreen(5)) : + stackImages(varIcon, varLabelImg, ImageStackLayout::horizontal, ImageStackAlignment::center, dipToScreen(5)); + } + + wxImage btnImg = stackImages(btnLabelImg, varImg, ImageStackLayout::vertical, ImageStackAlignment::center); + + btnImg = btn.GetLayoutDirection() != wxLayout_RightToLeft ? + stackImages(iconImg, btnImg, ImageStackLayout::horizontal, ImageStackAlignment::center, dipToScreen(5)) : + stackImages(btnImg, iconImg, ImageStackLayout::horizontal, ImageStackAlignment::center, dipToScreen(5)); + + if (extraIconName) + { + const wxImage exImg = loadImage(extraIconName, dipToScreen(20)); + + btnImg = btn.GetLayoutDirection() != wxLayout_RightToLeft ? + stackImages(btnImg, exImg, ImageStackLayout::horizontal, ImageStackAlignment::center, dipToScreen(5)) : + stackImages(exImg, btnImg, ImageStackLayout::horizontal, ImageStackAlignment::center, dipToScreen(5)); + } + + wxSize btnSize = btnImg.GetSize() + wxSize(dipToScreen(5 + 5), 0) /*border space*/; + btnSize.x = std::max(btnSize.x, dipToScreen(TOP_BUTTON_OPTIMAL_WIDTH_DIP)); + btnSize.y += dipToScreen(2 + 2); //border space + btnImg = resizeCanvas(btnImg, btnSize, wxALIGN_CENTER); + + if (highlightCol.IsOk()) + btnImg = layOver(rectangleImage(btnImg.GetSize(), highlightCol), btnImg, wxALIGN_CENTER); + + setImage(btn, btnImg); +} +} + +//################################################################################################################################## + +void MainDialog::create(const GlobalConfig& globalCfg, const Zstring& globalCfgFilePath) +{ + std::vector cfgFilePaths = globalCfg.mainDlg.config.lastUsedFiles; + + //------------------------------------------------------------------------------------------ + //check existence of all files in parallel: + AsyncFirstResult firstUnavailableFile; + + for (const Zstring& filePath : cfgFilePaths) + firstUnavailableFile.addJob([filePath]() -> std::optional + { + try + { + assert(!filePath.empty()); + getItemType(filePath); //throw FileError + return {}; + } + catch (FileError&) { return std::false_type(); } + }); + + //potentially slow network access: give all checks 500ms to finish + const bool allFilesAvailable = firstUnavailableFile.timedWait(LAST_USED_CFG_EXISTENCE_CHECK_TIME_MAX) && //false: time elapsed + !firstUnavailableFile.get(); //no missing + if (!allFilesAvailable) + cfgFilePaths.clear(); //we do NOT want to show an error due to last config file missing on application start! + //------------------------------------------------------------------------------------------ + + if (cfgFilePaths.empty()) + try //3. ...to load auto-save config (should not block) + { + const Zstring lastRunConfigFilePath = getLastRunConfigPath(); + + getItemType(lastRunConfigFilePath); //throw FileError + cfgFilePaths.push_back(lastRunConfigFilePath); + } + catch (FileError&) {} //not-existing/access error? => user may click on [Last session] later + + + FfsGuiConfig guiCfg = getDefaultGuiConfig(globalCfg.defaultFilter); + + if (!cfgFilePaths.empty()) + try + { + std::wstring warningMsg; + std::tie(guiCfg, warningMsg) = readAnyConfig(cfgFilePaths); //throw FileError + + if (!warningMsg.empty()) + showNotificationDialog(nullptr, DialogInfoType::warning, PopupDialogCfg().setDetailInstructions(warningMsg)); + //what about showing as changed config on parsing errors???? + } + catch (const FileError& e) + { + showNotificationDialog(nullptr, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); + } + //------------------------------------------------------------------------------------------ + + create(guiCfg, cfgFilePaths, globalCfg, globalCfgFilePath, false /*startComparison*/); +} + + +void MainDialog::create(const FfsGuiConfig& guiCfg, const std::vector& cfgFilePaths, + const GlobalConfig& globalCfg, const Zstring& globalCfgFilePath, + bool startComparison) +{ + MainDialog* mainDlg = new MainDialog(guiCfg, cfgFilePaths, globalCfg, globalCfgFilePath); + + //avoid Windows 10 white flash when showing dark mode window: https://chromium-review.googlesource.com/c/chromium/src/+/6092335 +#if 0 //variant 1: works, but no fade-in animation +#include +#pragma comment(lib, "dwmapi.lib") + + BOOL cloak = true; //requires Windows 8 and later + bool cloaked = SUCCEEDED(::DwmSetWindowAttribute(mainDlg->GetHWND(), DWMWA_CLOAK, &cloak, sizeof(cloak))); + + mainDlg->Show(); + + if (cloaked) + { + /*BOOL success = */ ::UpdateWindow(mainDlg->GetHWND()); + BOOL cloak = false; + /*HRESULT hr = */::DwmSetWindowAttribute(mainDlg->GetHWND(), DWMWA_CLOAK, &cloak, sizeof(cloak)); + } +#endif +#if 0 //variant 2: works, but different fade-in animation + mainDlg->Iconize(); + mainDlg->Show(); + mainDlg->Iconize(false); +#endif + + mainDlg->Show(); + + //------------------------------------------------------------------------------------------ + //construction complete! trigger special events: + //------------------------------------------------------------------------------------------ + + //show welcome dialog after FreeFileSync update => show *before* any other dialogs + if (mainDlg->globalCfg_.welcomeDialogLastVersion != ffsVersion) + { + mainDlg->globalCfg_.welcomeDialogLastVersion = ffsVersion; + + //showAboutDialog(mainDlg); => dialog centered incorrectly (Centos) + //mainDlg->CallAfter([mainDlg] { showAboutDialog(mainDlg); }); => dialog centered incorrectly (Windows, Centos) + mainDlg->guiQueue_.processAsync([] {}, [mainDlg]() { showAboutDialog(mainDlg); }); //apparently oh-kay? + } + + + //if FFS is started with a *.ffs_gui file as commandline parameter AND all directories contained exist, comparison shall be started right away + if (startComparison) + { + const MainConfiguration currMainCfg = mainDlg->getConfig().mainCfg; + + //------------------------------------------------------------------------------------------ + //harmonize checks with comparison.cpp:: checkForIncompleteInput() + //we're really doing two checks: 1. check directory existence 2. check config validity -> don't mix them! + bool havePartialPair = false; + bool haveFullPair = false; + + std::vector folderPathsToCheck; + + auto addFolderCheck = [&](const LocalPairConfig& lpc) + { + const AbstractPath folderPathL = createAbstractPath(lpc.folderPathPhraseLeft); + const AbstractPath folderPathR = createAbstractPath(lpc.folderPathPhraseRight); + + if (AFS::isNullPath(folderPathL) != AFS::isNullPath(folderPathR)) //only skip check if both sides are empty! + havePartialPair = true; + else if (!AFS::isNullPath(folderPathL)) + haveFullPair = true; + + if (!AFS::isNullPath(folderPathL)) + folderPathsToCheck.push_back(folderPathL); //noexcept + if (!AFS::isNullPath(folderPathR)) + folderPathsToCheck.push_back(folderPathR); //noexcept + }; + + addFolderCheck(currMainCfg.firstPair); + for (const LocalPairConfig& lpc : currMainCfg.additionalPairs) + addFolderCheck(lpc); + //------------------------------------------------------------------------------------------ + + if (havePartialPair != haveFullPair) //either all pairs full or all half-filled -> validity check! + { + //check existence of all directories in parallel! + AsyncFirstResult firstMissingDir; + for (const AbstractPath& folderPath : folderPathsToCheck) + firstMissingDir.addJob([folderPath]() -> std::optional + { + try + { + if (AFS::getItemType(folderPath) != AFS::ItemType::file) //throw FileError + return {}; + } + catch (FileError&) {} + return std::false_type(); + }); + + const bool startComparisonNow = !firstMissingDir.timedWait(std::chrono::milliseconds(500)) || //= no result yet => start comparison anyway! + !firstMissingDir.get(); //= all directories exist + + if (startComparisonNow) //simulate click on "compare" + { + wxCommandEvent dummy2(wxEVT_COMMAND_BUTTON_CLICKED); + mainDlg->m_buttonCompare->Command(dummy2); + } + } + } +} + + +MainDialog::MainDialog(const FfsGuiConfig& guiCfg, const std::vector& cfgFilePaths, + const GlobalConfig& globalCfg, const Zstring& globalCfgFilePath) : + MainDialogGenerated(nullptr), + globalCfgFilePath_(globalCfgFilePath), + folderHistoryLeft_ (std::make_shared(globalCfg.mainDlg.folderHistoryLeft, globalCfg.folderHistoryMax)), + folderHistoryRight_(std::make_shared(globalCfg.mainDlg.folderHistoryRight, globalCfg.folderHistoryMax)), + imgTrashSmall_([] +{ + try { return extractWxImage(fff::getTrashIcon(dipToScreen(getMenuIconDipSize()))); /*throw SysError*/ } + catch (SysError&) { assert(false); return loadImage("delete_recycler", dipToScreen(getMenuIconDipSize())); } +} +()), + +imgFileManagerSmall_([] +{ + try { return extractWxImage(fff::getFileManagerIcon(dipToScreen(getMenuIconDipSize()))); /*throw SysError*/ } + catch (SysError&) { assert(false); return loadImage("file_manager", dipToScreen(getMenuIconDipSize())); } +}()) +{ + SetSizeHints(dipToWxsize(640), dipToWxsize(400)); + + //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); + + + setRelativeFontSize(*m_buttonCompare, 1.4); + setRelativeFontSize(*m_buttonSync, 1.4); + setRelativeFontSize(*m_buttonCancel, 1.4); + + SetIcon(getFfsIcon()); //set application icon + + auto generateSaveAsImage = [](const char* layoverName) + { + const wxSize oldSize = loadImage("cfg_save").GetSize(); + + wxImage backImg = loadImage("cfg_save", oldSize.GetWidth() * 9 / 10); + backImg = resizeCanvas(backImg, oldSize, wxALIGN_BOTTOM | wxALIGN_LEFT); + + return layOver(backImg, loadImage(layoverName, backImg.GetWidth() * 7 / 10), wxALIGN_TOP | wxALIGN_RIGHT); + }; + + setImage(*m_bpButtonCmpConfig, loadImage("options_compare")); + setImage(*m_bpButtonSyncConfig, loadImage("options_sync")); + + setImage(*m_bpButtonCmpContext, mirrorIfRtl(loadImage("button_arrow_right"))); + setImage(*m_bpButtonFilterContext, mirrorIfRtl(loadImage("button_arrow_right"))); + setImage(*m_bpButtonSyncContext, mirrorIfRtl(loadImage("button_arrow_right"))); + setImage(*m_bpButtonViewFilterContext, mirrorIfRtl(loadImage("button_arrow_right"))); + + //m_bpButtonNew ->set dynamically + setImage(*m_bpButtonOpen, loadImage("cfg_load")); + //m_bpButtonSave ->set dynamically + setImage(*m_bpButtonSaveAs, generateSaveAsImage("start_sync")); + setImage(*m_bpButtonSaveAsBatch, generateSaveAsImage("cfg_batch")); + + setImage(*m_bpButtonAddPair, loadImage("item_add")); + setImage(*m_bpButtonHideSearch, loadImage("close_panel")); + //setImage(*m_bpButtonToggleLog, loadImage("log_file")); + + m_bpButtonFilter ->SetMinSize({screenToWxsize(loadImage("options_filter").GetWidth()) + dipToWxsize(27), -1}); //make the filter button wider + m_textCtrlSearchTxt->SetMinSize({dipToWxsize(220), -1}); + + //---------------------------------------------------------------------------------------- + wxImage labelImage = createImageFromText(_("Select view:"), m_bpButtonViewType->GetFont(), wxSystemSettings::GetColour(wxSYS_COLOUR_BTNTEXT)); + + labelImage = resizeCanvas(labelImage, labelImage.GetSize() + wxSize(dipToScreen(10), 0), wxALIGN_CENTER); //add border space + + auto generateViewTypeImage = [&](const char* imgName) + { + return stackImages(labelImage, mirrorIfRtl(loadImage(imgName)), ImageStackLayout::vertical, ImageStackAlignment::center); + }; + m_bpButtonViewType->init(generateViewTypeImage("viewtype_sync_action"), + generateViewTypeImage("viewtype_cmp_result")); + //tooltip is updated dynamically in setViewTypeSyncAction() + //---------------------------------------------------------------------------------------- + m_bpButtonShowExcluded ->SetToolTip(_("Show filtered or temporarily excluded files")); + m_bpButtonShowEqual ->SetToolTip(_("Show files that are equal")); + m_bpButtonShowConflict ->SetToolTip(_("Show conflicts")); + + m_bpButtonShowCreateLeft ->SetToolTip(_("Show files that will be created on the left side")); + m_bpButtonShowCreateRight->SetToolTip(_("Show files that will be created on the right side")); + m_bpButtonShowDeleteLeft ->SetToolTip(_("Show files that will be deleted on the left side")); + m_bpButtonShowDeleteRight->SetToolTip(_("Show files that will be deleted on the right side")); + m_bpButtonShowUpdateLeft ->SetToolTip(_("Show files that will be updated on the left side")); + m_bpButtonShowUpdateRight->SetToolTip(_("Show files that will be updated on the right side")); + m_bpButtonShowDoNothing ->SetToolTip(_("Show files that won't be copied")); + + m_bpButtonShowLeftOnly ->SetToolTip(_("Show files that exist on left side only")); + m_bpButtonShowRightOnly ->SetToolTip(_("Show files that exist on right side only")); + m_bpButtonShowLeftNewer ->SetToolTip(_("Show files that are newer on left")); + m_bpButtonShowRightNewer->SetToolTip(_("Show files that are newer on right")); + m_bpButtonShowDifferent ->SetToolTip(_("Show files that are different")); + //---------------------------------------------------------------------------------------- + + const wxImage& imgFile = IconBuffer::genericFileIcon(IconBuffer::IconSize::small); + const wxImage& imgDir = IconBuffer::genericDirIcon (IconBuffer::IconSize::small); + + //init log panel + setRelativeFontSize(*m_staticTextSyncResult, 1.5); + + setImage(*m_bitmapItemStat, imgFile); + + wxImage imgTime = loadImage("time", -1 /*maxWidth*/, imgFile.GetHeight()); + setImage(*m_bitmapTimeStat, imgTime); + m_bitmapTimeStat->SetMinSize({-1, screenToWxsize(imgFile.GetHeight())}); + + logPanel_ = new LogPanel(m_panelLog); //pass ownership + bSizerLog->Add(logPanel_, 1, wxEXPAND); + + setLastOperationLog(ProcessSummary(), nullptr /*errorLog*/); + + //we have to use the OS X naming convention by default, because wxMac permanently populates the display menu when the wxMenuItem is created for the first time! + //=> other wx ports are not that badly programmed; therefore revert: + assert(m_menuItemOptions->GetItemLabel() == _("&Preferences") + L"\tCtrl+,"); //"Ctrl" is automatically mapped to command button! + m_menuItemOptions->SetItemLabel(_("&Options")); + + //---------------- support for dockable gui style -------------------------------- + bSizerPanelHolder->Detach(m_panelTopButtons); + bSizerPanelHolder->Detach(m_panelLog); + bSizerPanelHolder->Detach(m_panelDirectoryPairs); + bSizerPanelHolder->Detach(m_gridOverview); + bSizerPanelHolder->Detach(m_panelCenter); + bSizerPanelHolder->Detach(m_panelConfig); + bSizerPanelHolder->Detach(m_panelViewFilter); + + auiMgr_.SetDockSizeConstraint(1 /*width_pct*/, 1 /*height_pct*/); //get rid: interferes with programmatic layout changes + doesn't limit what user can do + + auiMgr_.SetManagedWindow(this); + auiMgr_.SetFlags(wxAUI_MGR_DEFAULT | wxAUI_MGR_LIVE_RESIZE); + + auiMgr_.Bind(wxEVT_AUI_PANE_CLOSE, [](wxAuiManagerEvent& event) + { + //wxAuiManager::ClosePane already calls wxAuiManager::RestorePane if wxAuiPaneInfo::IsMaximized + if (wxAuiPaneInfo* pi = event.GetPane()) + if (!pi->IsMaximized()) + pi->best_size = pi->rect.GetSize(); //ensure current window sizes will be used when pane is shown again: + + assert(event.GetPane()->rect != wxSize()); + }); + + //daily WTF: wxAuiManager ignores old directory pane size in wxAuiPaneInfo::rect + //and calculates new window sizes based on best_size/min_size during wxEVT_AUI_PANE_RESTORE! + auiMgr_.Bind(wxEVT_AUI_PANE_MAXIMIZE, [this](wxAuiManagerEvent& event) + { + wxAuiPaneInfo& dirPane = auiMgr_.GetPane(m_panelDirectoryPairs); + wxAuiPaneInfo& logPane = auiMgr_.GetPane(m_panelLog); + assert(event.GetPane() == &logPane); + + //ensure current window sizes will be used during wxEVT_AUI_PANE_RESTORE: + dirPane.best_size = dirPane.rect.GetSize(); + logPane.best_size = logPane.rect.GetSize(); + + assert(dirPane.rect != wxSize()); + assert(logPane.rect != wxSize()); + }); + + auiMgr_.Bind(wxEVT_AUI_PANE_CLOSE, [this](wxAuiManagerEvent& event) + { + if (event.GetPane() == &auiMgr_.GetPane(m_panelLog)) + { + event.Veto(); + showLogPanel(false); + } + }); + + compareStatus_.emplace(*this); //integrate the compare status panel (in hidden state) + + //caption required for all panes that can be manipulated by the users => used by context menu + auiMgr_.AddPane(m_panelCenter, + wxAuiPaneInfo().Name(L"CenterPanel").CenterPane().PaneBorder(false)); + + //set comparison button label tentatively for m_panelTopButtons to receive final height: + updateTopButton(*m_buttonCompare, loadImage("compare"), getVariantName(CompareVariant::timeSize), "cmp_time", nullptr /*extraIconName*/, wxNullColour); + m_panelTopButtons->GetSizer()->SetSizeHints(m_panelTopButtons); //~=Fit() + SetMinSize() + + m_buttonCancel->SetMinSize({std::max(m_buttonCancel->GetSize().x, dipToWxsize(TOP_BUTTON_OPTIMAL_WIDTH_DIP)), + std::max(m_buttonCancel->GetSize().y, m_buttonCompare->GetSize().y) + }); + + auiMgr_.AddPane(m_panelTopButtons, + wxAuiPaneInfo().Name(L"TopPanel").Layer(2).Top().Row(1).Caption(_("Main Bar")).CaptionVisible(false). + PaneBorder(false).Gripper(). + //BestSize(-1, m_panelTopButtons->GetSize().GetHeight() + dipToWxsize(10)). + MinSize(dipToWxsize(TOP_BUTTON_OPTIMAL_WIDTH_DIP), m_panelTopButtons->GetSize().GetHeight())); + //note: min height is calculated incorrectly by wxAuiManager if panes with and without caption are in the same row => use smaller min-size + + auiMgr_.AddPane(compareStatus_->getAsWindow(), + wxAuiPaneInfo().Name(L"ProgressPanel").Layer(2).Top().Row(2).CaptionVisible(false).PaneBorder(false).Hide(). + //wxAui does not consider the progress panel's wxRAISED_BORDER and set's too small a panel height! => use correct value from wxWindow::GetSize() + MinSize(-1, compareStatus_->getAsWindow()->GetSize().GetHeight())); //bonus: minimal height isn't a bad idea anyway + + m_panelDirectoryPairs->GetSizer()->SetSizeHints(m_panelDirectoryPairs); //~=Fit() + SetMinSize() + auiMgr_.AddPane(m_panelDirectoryPairs, + wxAuiPaneInfo().Name(L"FoldersPanel").Layer(2).Top().Row(3).Caption(_("Folder Pairs")).CaptionVisible(false).PaneBorder(false).Gripper(). + /* yes, m_panelDirectoryPairs's min height is overwritten in updateGuiForFolderPair(), but the default height might be wrong + after increasing text size (Win10 Settings -> Accessibility -> Text size), e.g. to 150%: + auiMgr_.LoadPerspective will load a too small "dock_size", so m_panelTopLeft/m_panelTopCenter will have squashed height */ + MinSize(dipToWxsize(100), m_panelDirectoryPairs->GetSize().y).CloseButton(false)); + + m_panelSearch->GetSizer()->SetSizeHints(m_panelSearch); //~=Fit() + SetMinSize() + auiMgr_.AddPane(m_panelSearch, + wxAuiPaneInfo().Name(L"SearchPanel").Layer(2).Bottom().Row(3).Caption(_("Find")).CaptionVisible(false).PaneBorder(false).Gripper(). + MinSize(dipToWxsize(100), m_panelSearch->GetSize().y).Hide()); + + auiMgr_.AddPane(m_panelLog, + wxAuiPaneInfo().Name(L"LogPanel").Layer(2).Bottom().Row(2).Caption(_("Log")).MaximizeButton().Hide(). + MinSize (dipToWxsize(100), dipToWxsize(100)). + BestSize(dipToWxsize(600), dipToWxsize(300))); + + m_panelViewFilter->GetSizer()->SetSizeHints(m_panelViewFilter); //~=Fit() + SetMinSize() + auiMgr_.AddPane(m_panelViewFilter, + wxAuiPaneInfo().Name(L"ViewFilterPanel").Layer(2).Bottom().Row(1).Caption(_("View Settings")).CaptionVisible(false). + PaneBorder(false).Gripper().MinSize(dipToWxsize(80), m_panelViewFilter->GetSize().y)); + + m_panelConfig->GetSizer()->SetSizeHints(m_panelConfig); //~=Fit() + SetMinSize() + auiMgr_.AddPane(m_panelConfig, + wxAuiPaneInfo().Name(L"ConfigPanel").Layer(3).Left().Position(1).Caption(_("Configuration")).MinSize(bSizerCfgHistoryButtons->GetSize())); + + auiMgr_.AddPane(m_gridOverview, + wxAuiPaneInfo().Name(L"OverviewPanel").Layer(3).Left().Position(2).Caption(_("Overview")). + MinSize (dipToWxsize(100), dipToWxsize(100)). + BestSize(dipToWxsize(300), -1)); + { + wxAuiDockArt* artProvider = auiMgr_.GetArtProvider(); + + wxFont font = artProvider->GetFont(wxAUI_DOCKART_CAPTION_FONT); + font.SetWeight(wxFONTWEIGHT_BOLD); + font.SetPointSize(wxNORMAL_FONT->GetPointSize()); //= larger than the wxAuiDockArt default; looks better on OS X + artProvider->SetFont(wxAUI_DOCKART_CAPTION_FONT, font); + artProvider->SetMetric(wxAUI_DOCKART_CAPTION_SIZE, font.GetPixelSize().GetHeight() + dipToWxsize(2 + 2)); + + //- fix wxWidgets 3.1.0 insane color scheme + artProvider->SetColor(wxAUI_DOCKART_INACTIVE_CAPTION_TEXT_COLOUR, getColorAuiPanelCaptionText()); //accessibility: always set both foreground AND background colors! + artProvider->SetColor(wxAUI_DOCKART_INACTIVE_CAPTION_COLOUR, getColorAuiPanelCaptionBack()); + artProvider->SetColor(wxAUI_DOCKART_INACTIVE_CAPTION_GRADIENT_COLOUR, getColorAuiPanelCaptionBackGradient()); + } + //auiMgr_.Update(); -> redundant; called by setGlobalCfgOnInit() below + + defaultPerspective_ = auiMgr_.SavePerspective(); //does not need wxAuiManager::Update()! + //---------------------------------------------------------------------------------- + //register view layout context menu + m_panelTopButtons->Bind(wxEVT_RIGHT_DOWN, [this](wxMouseEvent& event) { onSetLayoutContext(event); }); + m_panelConfig ->Bind(wxEVT_RIGHT_DOWN, [this](wxMouseEvent& event) { onSetLayoutContext(event); }); + m_panelViewFilter->Bind(wxEVT_RIGHT_DOWN, [this](wxMouseEvent& event) { onSetLayoutContext(event); }); + m_panelStatusBar ->Bind(wxEVT_RIGHT_DOWN, [this](wxMouseEvent& event) { onSetLayoutContext(event); }); + //---------------------------------------------------------------------------------- + + //file grid: sorting + m_gridMainL->Bind(EVENT_GRID_COL_LABEL_MOUSE_LEFT, [this](GridLabelClickEvent& event) { onGridLabelLeftClickRim(event, true /*leftSide*/); }); + m_gridMainR->Bind(EVENT_GRID_COL_LABEL_MOUSE_LEFT, [this](GridLabelClickEvent& event) { onGridLabelLeftClickRim(event, false /*leftSide*/); }); + m_gridMainC->Bind(EVENT_GRID_COL_LABEL_MOUSE_LEFT, [this](GridLabelClickEvent& event) { onGridLabelLeftClickC(event); }); + + m_gridMainL->Bind(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, [this](GridLabelClickEvent& event) { onGridLabelContextRim(event, true /*leftSide*/); }); + m_gridMainR->Bind(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, [this](GridLabelClickEvent& event) { onGridLabelContextRim(event, false /*leftSide*/); }); + m_gridMainC->Bind(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, [this](GridLabelClickEvent& event) { onGridLabelContextC(event); }); + + //file grid: context menu + m_gridMainL->Bind(EVENT_GRID_CONTEXT_MENU, [this](GridContextMenuEvent& event) { onGridContextRim(event, true /*leftSide*/); }); + m_gridMainR->Bind(EVENT_GRID_CONTEXT_MENU, [this](GridContextMenuEvent& event) { onGridContextRim(event, false /*leftSide*/); }); + + m_gridMainL->Bind(EVENT_GRID_MOUSE_RIGHT_DOWN, [this](GridClickEvent& event) { onGridGroupContextRim(event, true /*leftSide*/); }); + m_gridMainR->Bind(EVENT_GRID_MOUSE_RIGHT_DOWN, [this](GridClickEvent& event) { onGridGroupContextRim(event, false /*leftSide*/); }); + + m_gridMainL->Bind(EVENT_GRID_MOUSE_LEFT_DOUBLE, [this](GridClickEvent& event) { onGridDoubleClickRim(event, true /*leftSide*/); }); + m_gridMainR->Bind(EVENT_GRID_MOUSE_LEFT_DOUBLE, [this](GridClickEvent& event) { onGridDoubleClickRim(event, false /*leftSide*/); }); + + //tree grid: + m_gridOverview->Bind(EVENT_GRID_CONTEXT_MENU, [this](GridContextMenuEvent& event) { onTreeGridContext (event); }); + m_gridOverview->Bind(EVENT_GRID_SELECT_RANGE, [this](GridSelectEvent& event) { onTreeGridSelection(event); }); + + //cfg grid: + m_gridCfgHistory->Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onCfgGridKeyEvent(event); }); + m_gridCfgHistory->Bind(EVENT_GRID_SELECT_RANGE, [this](GridSelectEvent& event) { onCfgGridSelection (event); }); + m_gridCfgHistory->Bind(EVENT_GRID_MOUSE_LEFT_DOUBLE, [this](GridClickEvent& event) { onCfgGridDoubleClick (event); }); + m_gridCfgHistory->Bind(EVENT_GRID_CONTEXT_MENU, [this](GridContextMenuEvent& event) { onCfgGridContext (event); }); + m_gridCfgHistory->Bind(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, [this](GridLabelClickEvent& event) { onCfgGridLabelContext (event); }); + m_gridCfgHistory->Bind(EVENT_GRID_COL_LABEL_MOUSE_LEFT, [this](GridLabelClickEvent& event) { onCfgGridLabelLeftClick(event); }); + //---------------------------------------------------------------------------------- + + m_panelSearch->Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onSearchPanelKeyPressed(event); }); + + + //set tool tips with (non-translated!) short cut hint + auto setCommandToolTip = [](wxButton& btn, const wxString& label, wxString shortcut) + { + wxString tooltip = wxControl::RemoveMnemonics(label); + if (!shortcut.empty()) + { + tooltip += L" (" + shortcut + L')'; + } + btn.SetToolTip(tooltip); + }; + setCommandToolTip(*m_bpButtonNew, _("&New"), L"Ctrl+N"); // + setCommandToolTip(*m_bpButtonOpen, _("&Open..."), L"Ctrl+O"); // + setCommandToolTip(*m_bpButtonSave, _("&Save"), L"Ctrl+S"); //reuse texts from GUI builder + setCommandToolTip(*m_bpButtonSaveAs, _("Save &as..."), L""); // + setCommandToolTip(*m_bpButtonSaveAsBatch, _("Save as &batch job..."), L""); // + + setCommandToolTip(*m_bpButtonToggleLog, _("Show &log"), L"F4"); // + setCommandToolTip(*m_buttonCompare, _("Start &comparison"), L"F5"); // + setCommandToolTip(*m_bpButtonCmpConfig, _("C&omparison settings"), L"F6"); // + setCommandToolTip(*m_bpButtonSyncConfig, _("S&ynchronization settings"), L"F8"); // + setCommandToolTip(*m_buttonSync, _("Start &synchronization"), L"F9"); // + setCommandToolTip(*m_bpButtonSwapSides, _("Swap sides"), L"Ctrl+Tab"); + + //m_bpButtonCmpContext ->SetToolTip(m_bpButtonCmpConfig ->GetToolTipText()); + //m_bpButtonSyncContext->SetToolTip(m_bpButtonSyncConfig->GetToolTipText()); + + + setImage(*m_bitmapSmallDirectoryLeft, imgDir); + setImage(*m_bitmapSmallFileLeft, imgFile); + setImage(*m_bitmapSmallDirectoryRight, imgDir); + setImage(*m_bitmapSmallFileRight, imgFile); + + //---------------------- menu bar---------------------------- + setImage(*m_menuItemNew, loadImage("cfg_new", dipToScreen(getMenuIconDipSize()))); + setImage(*m_menuItemLoad, loadImage("cfg_load", dipToScreen(getMenuIconDipSize()))); + setImage(*m_menuItemSave, loadImage("cfg_save", dipToScreen(getMenuIconDipSize()))); + setImage(*m_menuItemSaveAsBatch, loadImage("cfg_batch", dipToScreen(getMenuIconDipSize()))); + + setImage(*m_menuItemShowLog, loadImage("log_file", dipToScreen(getMenuIconDipSize()))); + setImage(*m_menuItemCompare, loadImage("compare", dipToScreen(getMenuIconDipSize()))); + setImage(*m_menuItemCompSettings, loadImage("options_compare", dipToScreen(getMenuIconDipSize()))); + setImage(*m_menuItemFilter, loadImage("options_filter", dipToScreen(getMenuIconDipSize()))); + setImage(*m_menuItemSyncSettings, loadImage("options_sync", dipToScreen(getMenuIconDipSize()))); + setImage(*m_menuItemSynchronize, loadImage("start_sync", dipToScreen(getMenuIconDipSize()))); + + setImage(*m_menuItemOptions, loadImage("settings", dipToScreen(getMenuIconDipSize()))); + setImage(*m_menuItemFind, loadImage("find_sicon")); + setImage(*m_menuItemResetLayout, loadImage("reset_sicon")); + + setImage(*m_menuItemHelp, loadImage("help", dipToScreen(getMenuIconDipSize()))); + setImage(*m_menuItemAbout, loadImage("about", dipToScreen(getMenuIconDipSize()))); + setImage(*m_menuItemCheckVersionNow, loadImage("update_check", dipToScreen(getMenuIconDipSize()))); + + fixMenuIcons(*m_menuFile); + fixMenuIcons(*m_menuActions); + fixMenuIcons(*m_menuTools); + fixMenuIcons(*m_menuHelp); + + //create language selection menu + for (const TranslationInfo& ti : getAvailableTranslations()) + { + wxMenuItem* newItem = new wxMenuItem(m_menuLanguages, wxID_ANY, ti.languageName); + setImage(*newItem, loadImage(ti.languageFlag)); //GTK: set *before* inserting into menu + + m_menuLanguages->Bind(wxEVT_COMMAND_MENU_SELECTED, [this, langId = ti.languageID](wxCommandEvent&) { switchProgramLanguage(langId); }, newItem->GetId()); + m_menuLanguages->Append(newItem); //pass ownership + } + + //set up layout items to toggle showing hidden panels + m_menuItemShowMain ->SetItemLabel(replaceCpy(_("Show \"%x\""), L"%x", _("Main Bar"))); + m_menuItemShowFolders ->SetItemLabel(replaceCpy(_("Show \"%x\""), L"%x", _("Folder Pairs"))); + m_menuItemShowViewFilter->SetItemLabel(replaceCpy(_("Show \"%x\""), L"%x", _("View Settings"))); + m_menuItemShowConfig ->SetItemLabel(replaceCpy(_("Show \"%x\""), L"%x", _("Configuration"))); + m_menuItemShowOverview ->SetItemLabel(replaceCpy(_("Show \"%x\""), L"%x", _("Overview"))); + + auto setupLayoutMenuEvent = [&](wxMenuItem* menuItem, wxWindow* panelWindow) + { + m_menuTools->Bind(wxEVT_COMMAND_MENU_SELECTED, [this, panelWindow](wxCommandEvent&) + { + this->auiMgr_.GetPane(panelWindow).Show(); + this->auiMgr_.Update(); + }, menuItem->GetId()); + + //"hide" menu items by default + detachedMenuItems_.insert(m_menuTools->Remove(menuItem)); //pass ownership + }; + setupLayoutMenuEvent(m_menuItemShowMain, m_panelTopButtons); + setupLayoutMenuEvent(m_menuItemShowFolders, m_panelDirectoryPairs); + setupLayoutMenuEvent(m_menuItemShowViewFilter, m_panelViewFilter); + setupLayoutMenuEvent(m_menuItemShowConfig, m_panelConfig); + setupLayoutMenuEvent(m_menuItemShowOverview, m_gridOverview); + + m_menuTools->Bind(wxEVT_MENU_OPEN, [this](wxMenuEvent& event) { onOpenMenuTools(event); }); + + //notify about (logical) application main window => program won't quit, but stay on this dialog + wxTheApp->SetTopWindow(this); + wxTheApp->SetExitOnFrameDelete(true); + + //init handling of first folder pair + firstFolderPair_ = std::make_unique(*this, + globalCfg_.mainDlg.folderLastSelectedLeft, + globalCfg_.mainDlg.folderLastSelectedRight, + globalCfg_.sftpKeyFileLastSelected); + + //init grid settings + filegrid::init(*m_gridMainL, *m_gridMainC, *m_gridMainR); + treegrid::init(*m_gridOverview); + cfggrid ::init(*m_gridCfgHistory); + + + //initialize and load configuration + setGlobalCfgOnInit(globalCfg); //calls auiMgr_.Update() + setConfig(guiCfg, cfgFilePaths); //expects auiMgr_.Update(): e.g. recalcMaxFolderPairsVisible() + + //support for CTRL + C and DEL on grids + m_gridMainL->Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onGridKeyEvent(event, *m_gridMainL, true /*leftSide*/); }); + m_gridMainC->Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onGridKeyEvent(event, *m_gridMainC, true /*leftSide*/); }); + m_gridMainR->Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onGridKeyEvent(event, *m_gridMainR, false /*leftSide*/); }); + + m_gridOverview->Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onTreeKeyEvent(event); }); + + Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onLocalKeyEvent(event); }); //enable dialog-specific key events + + //drag and drop .ffs_gui and .ffs_batch on main dialog + setupFileDrop(*this); + Bind(EVENT_DROP_FILE, [this](FileDropEvent& event) { onDialogFilesDropped(event); }); + + //calculate witdh of folder pair manually (if scrollbars are visible) + m_panelTopLeft->Bind(wxEVT_SIZE, [this](wxSizeEvent& event) { onResizeLeftFolderWidth(event); }); + + m_panelTopLeft ->Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onTopFolderPairKeyEvent(event); }); + m_panelTopCenter->Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onTopFolderPairKeyEvent(event); }); + m_panelTopRight ->Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onTopFolderPairKeyEvent(event); }); + + //dynamically change sizer direction depending on size + m_panelTopButtons->Bind(wxEVT_SIZE, [this](wxSizeEvent& event) { onResizeTopButtonPanel(event); }); + m_panelConfig ->Bind(wxEVT_SIZE, [this](wxSizeEvent& event) { onResizeConfigPanel (event); }); + m_panelViewFilter->Bind(wxEVT_SIZE, [this](wxSizeEvent& event) { onResizeViewPanel (event); }); + wxSizeEvent dummy3; + onResizeTopButtonPanel(dummy3); // + onResizeConfigPanel (dummy3); //call once on window creation + onResizeViewPanel (dummy3); // + + const int scrollDelta = m_buttonSelectFolderLeft->GetSize().y; //more approriate than GetCharHeight() here + m_scrolledWindowFolderPairs->SetScrollRate(scrollDelta, scrollDelta); + + //event handler for manual (un-)checking of rows and setting of sync direction + m_gridMainC->Bind(EVENT_GRID_CHECK_ROWS, [this](CheckRowsEvent& event) { onCheckRows (event); }); + m_gridMainC->Bind(EVENT_GRID_SYNC_DIRECTION, [this](SyncDirectionEvent& event) { onSetSyncDirection(event); }); + + //mainly to update row label sizes... + updateGui(); + + //register regular check for update on next idle event + Bind(wxEVT_IDLE, &MainDialog::onStartupUpdateCheck, this); + + //asynchronous call to wxWindow::Dimensions(): fix superfluous frame on right and bottom when FFS is started in fullscreen mode + Bind(wxEVT_IDLE, &MainDialog::onLayoutWindowAsync, this); + wxCommandEvent evtDummy; //call once before onLayoutWindowAsync() + onResizeLeftFolderWidth(evtDummy); // + + + onSystemShutdownRegister(onBeforeSystemShutdownCookie_); + + //show and clear "extra" log in case of startup errors: + guiQueue_.processAsync([] { std::this_thread::sleep_for(std::chrono::milliseconds(500)); }, [this] //give worker threads some time to (potentially) log extra errors + { + if (!operationInProgress_ && folderCmp_.empty()) //don't show if main dialog is otherwise busy! + { + ErrorLog extraLog = fetchExtraLog(); + + try //clean up remnant logs from previous FFS runs: + { + traverseFolder(getConfigDirPath(), [&](const FileInfo& fi) //"ErrorLog 2023-07-05 105207.073.xml" + { + if (startsWith(fi.itemName, Zstr("ErrorLog ")) && endsWith(fi.itemName, Zstr(".xml"))) //case-sensitive + { + append(extraLog, loadErrorLog(fi.fullPath)); //throw FileError + removeFilePlain(fi.fullPath); //throw FileError + //yeah, "read + delete" is a bit racy... + } + }, nullptr, nullptr); //throw FileError + } + catch (const FileError& e) { logMsg(extraLog, e.toString(), MessageType::MSG_TYPE_ERROR); } + + std::stable_sort(extraLog.begin(), extraLog.end(), [](const LogEntry& lhs, const LogEntry& rhs) { return lhs.time < rhs.time; }); + + if (!extraLog.empty()) + { + const ErrorLogStats logCount = getStats(extraLog); + const TaskResult taskResult = logCount.errors > 0 ? TaskResult::error : (logCount.warnings > 0 ? TaskResult::warning : TaskResult::success); + setLastOperationLog({.result = taskResult}, make_shared(std::move(extraLog))); + showLogPanel(true); + } + } + }); + + + //scroll cfg history to last used position. We cannot do this earlier e.g. in setGlobalCfgOnInit() + //1. setConfig() indirectly calls cfggrid::addAndSelect() which changes cfg history scroll position + //2. Grid::makeRowVisible() requires final window height! => do this after window resizing is complete + if (m_gridCfgHistory->getRowCount() > 0) + m_gridCfgHistory->scrollTo(std::clamp(globalCfg.mainDlg.config.topRowPos, //must be set *after* wxAuiManager::LoadPerspective() to have any effect + 0, m_gridCfgHistory->getRowCount() - 1)); + //first selected item should *always* be visible: + const std::vector selectedRows = m_gridCfgHistory->getSelectedRows(); + if (!selectedRows.empty()) + { + m_gridCfgHistory->setGridCursor(selectedRows[0], GridEventPolicy::deny); + //= Grid::makeRowVisible() + set grid cursor (+ select cursor row => undo:) + cfggrid::addAndSelect(*m_gridCfgHistory, activeConfigFiles_, false /*scrollToSelection*/); + } + //start up: user most likely wants to change config, or start comparison by pressing ENTER + m_gridCfgHistory->SetFocus(); +} + + +MainDialog::~MainDialog() +{ + std::wstring errorMsg; + try //LastRun.ffs_gui + { + writeConfig(getConfig(), lastRunConfigPath_); //throw FileError + } + catch (const FileError& e) { errorMsg += e.toString() + L"\n\n"; } + + try //GlobalSettings.xml + { + writeConfig(getGlobalCfgBeforeExit(), globalCfgFilePath_); //throw FileError + } + catch (const FileError& e) { errorMsg += e.toString() + L"\n\n"; } + + //don't annoy users on read-only drives: it's enough to show a single error message + if (!errorMsg.empty()) + showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(trimCpy(errorMsg))); + + //auiMgr_.UnInit(); - "since wxWidgets 3.1.4 [...] it will be called automatically when this window is destroyed, as well as when the manager itself is." + + for (wxMenuItem* item : detachedMenuItems_) + delete item; //something's got to give + + //no need for wxEventHandler::Unbind(): event sources are components of this window and are destroyed, too +} + +//------------------------------------------------------------------------------------------------------------------------------------- + +void MainDialog::onBeforeSystemShutdown() +{ + try { writeConfig(getConfig(), lastRunConfigPath_); } + catch (const FileError& e) { logExtraError(e.toString()); } + + try { writeConfig(getGlobalCfgBeforeExit(), globalCfgFilePath_); } + catch (const FileError& e) { logExtraError(e.toString()); } +} + + +void MainDialog::onClose(wxCloseEvent& event) +{ + //wxEVT_END_SESSION is already handled by application.cpp::onSystemShutdown()! + + //regular destruction handling + if (event.CanVeto()) + { + //=> veto all attempts to close the main window while comparison or synchronization are running: + if (operationInProgress_) + { + event.Veto(); + Raise(); //=what Windows does when vetoing a close (via middle mouse on taskbar preview) while showing a modal dialog + SetFocus(); // + return; + } + + const bool cancelled = !saveOldConfig(); //notify user about changed settings + if (cancelled) //...or error + { + event.Veto(); + return; + } + } + + Destroy(); +} + + +void MainDialog::setGlobalCfgOnInit(const GlobalConfig& globalCfg) +{ + globalCfg_ = globalCfg; + + DpiLayout layout; + if (auto it = globalCfg.dpiLayouts.find(getDpiScalePercent()); + it != globalCfg.dpiLayouts.end()) + layout = it->second; + + //caveat: set/get language asymmmetry! setLanguage(globalCfg.programLanguage); //throw FileError + //we need to set language before creating this class! + + WindowLayout::setInitial(*this, {layout.mainDlg.size, layout.mainDlg.pos, layout.mainDlg.isMaximized}, {dipToWxsize(900), dipToWxsize(600)} /*defaultSize*/); + + //set column attributes + m_gridMainL ->setColumnConfig(convertColAttributes(layout.fileColumnAttribsLeft, getFileGridDefaultColAttribsLeft())); + m_gridMainR ->setColumnConfig(convertColAttributes(layout.fileColumnAttribsRight, getFileGridDefaultColAttribsLeft())); + m_splitterMain->setSashOffset(globalCfg.mainDlg.sashOffset); + + m_gridOverview->setColumnConfig(convertColAttributes(layout.overviewColumnAttribs, getOverviewDefaultColAttribs())); + treegrid::setShowPercentage(*m_gridOverview, globalCfg.mainDlg.overview.showPercentBar); + + treegrid::getDataView(*m_gridOverview).setSortDirection(globalCfg.mainDlg.overview.lastSortColumn, globalCfg.mainDlg.overview.lastSortAscending); + + //-------------------------------------------------------------------------------- + //load list of configuration files + cfggrid::getDataView(*m_gridCfgHistory).set(globalCfg.mainDlg.config.fileHistory); + + //globalCfg.mainDlg.cfgGridTopRowPos => defer evaluation until later within MainDialog constructor + m_gridCfgHistory->setColumnConfig(convertColAttributes(layout.configColumnAttribs, getCfgGridDefaultColAttribs())); + cfggrid::getDataView(*m_gridCfgHistory).setSortDirection(globalCfg.mainDlg.config.lastSortColumn, globalCfg.mainDlg.config.lastSortAscending); + cfggrid::setSyncOverdueDays(*m_gridCfgHistory, globalCfg.mainDlg.config.syncOverdueDays); + //m_gridCfgHistory->Refresh(); <- implicit in last call + + //remove non-existent items: sufficient to call once at startup + std::vector cfgFilePaths; + for (const ConfigFileItem& item : globalCfg.mainDlg.config.fileHistory) + cfgFilePaths.push_back(item.cfgFilePath); + + cfgHistoryRemoveObsolete(cfgFilePaths); + + //are we spawning too many async jobs, considering cfgHistoryRemoveObsolete()!? + cfgHistoryUpdateNotes(cfgFilePaths); + //-------------------------------------------------------------------------------- + + //load list of last used folders + m_folderPathLeft ->setHistory(folderHistoryLeft_); + m_folderPathRight->setHistory(folderHistoryRight_); + + //show/hide file icons + filegrid::setupIcons(*m_gridMainL, *m_gridMainC, *m_gridMainR, globalCfg.mainDlg.showIcons, convert(globalCfg.mainDlg.iconSize)); + + filegrid::setItemPathForm(*m_gridMainL, globalCfg.mainDlg.itemPathFormatLeftGrid); + filegrid::setItemPathForm(*m_gridMainR, globalCfg.mainDlg.itemPathFormatRightGrid); + + //-------------------------------------------------------------------------------- + m_checkBoxMatchCase->SetValue(globalCfg_.mainDlg.textSearchRespectCase); + + //work around wxAuiManager::LoadPerspective overwriting pane captions with old values (might be different language!) + std::vector> paneCaptions; + for (wxAuiPaneInfo& paneInfo : auiMgr_.GetAllPanes()) + paneCaptions.emplace_back(&paneInfo, paneInfo.caption); + + //compare progress dialog minimum sizes are layout-dependent + can't be changed by user => don't load stale values from config + std::vector> paneConstraints; + auto preserveConstraint = [&paneConstraints](wxAuiPaneInfo& pane) { paneConstraints.emplace_back(&pane, pane.min_size, pane.best_size); }; + + wxAuiPaneInfo& progPane = auiMgr_.GetPane(compareStatus_->getAsWindow()); + preserveConstraint(progPane); + preserveConstraint(auiMgr_.GetPane(m_panelTopButtons)); + preserveConstraint(auiMgr_.GetPane(m_panelDirectoryPairs)); + preserveConstraint(auiMgr_.GetPane(m_panelSearch)); + preserveConstraint(auiMgr_.GetPane(m_panelViewFilter)); + preserveConstraint(auiMgr_.GetPane(m_panelConfig)); + + auiMgr_.LoadPerspective(layout.panelLayout, false /*update: don't call wxAuiManager::Update() yet*/); + + //restore original captions + for (const auto& [paneInfo, caption] : paneCaptions) + paneInfo->Caption(caption); + + //restore pane layout constraints + for (auto& [pane, minSize, bestSize] : paneConstraints) + { + pane->min_size = minSize; + pane->best_size = bestSize; + } + //-------------------------------------------------------------------------------- + + //if MainDialog::onBeforeSystemShutdown() is called while comparison is active, this panel is saved and restored as "visible" + progPane.Hide(); + + auiMgr_.GetPane(m_panelSearch).Hide(); //no need to show it on startup + auiMgr_.GetPane(m_panelLog ).Hide(); // + + auiMgr_.Update(); +} + + +GlobalConfig MainDialog::getGlobalCfgBeforeExit() +{ + Freeze(); //no need to Thaw() again!! + recalcMaxFolderPairsVisible(); + //-------------------------------------------------------------------------------- + GlobalConfig globalSettings = globalCfg_; + + globalSettings.programLanguage = getLanguage(); + + //retrieve column attributes + globalSettings.dpiLayouts[getDpiScalePercent()].fileColumnAttribsLeft = convertColAttributes(m_gridMainL->getColumnConfig()); + globalSettings.dpiLayouts[getDpiScalePercent()].fileColumnAttribsRight = convertColAttributes(m_gridMainR->getColumnConfig()); + globalSettings.mainDlg.sashOffset = m_splitterMain->getSashOffset(); + + globalSettings.dpiLayouts[getDpiScalePercent()].overviewColumnAttribs = convertColAttributes(m_gridOverview->getColumnConfig()); + globalSettings.mainDlg.overview.showPercentBar = treegrid::getShowPercentage(*m_gridOverview); + + const auto [sortCol, ascending] = treegrid::getDataView(*m_gridOverview).getSortConfig(); + globalSettings.mainDlg.overview.lastSortColumn = sortCol; + globalSettings.mainDlg.overview.lastSortAscending = ascending; + + //-------------------------------------------------------------------------------- + //write list of configuration files + std::vector cfgHistory + { + //make sure [Last session] is always part of history list + ConfigFileItem(lastRunConfigPath_, LastRunStats{}, wxSystemSettings::GetAppearance().IsDark() ? 0xb7b7b7 : 0xdddddd /*grey from onCfgGridContext()*/) + }; + + for (const ConfigFileItem& item : cfggrid::getDataView(*m_gridCfgHistory).get()) + if (equalNativePath(item.cfgFilePath, lastRunConfigPath_)) + cfgHistory[0] = item; //preserve users's background color choice + else + cfgHistory.push_back(item); + + //trim excess elements (oldest first) + if (cfgHistory.size() > globalSettings.mainDlg.config.histItemsMax) + cfgHistory.resize(globalSettings.mainDlg.config.histItemsMax); + + globalSettings.mainDlg.config.fileHistory = std::move(cfgHistory); + globalSettings.mainDlg.config.topRowPos = m_gridCfgHistory->getRowAtWinPos(0); + globalSettings.dpiLayouts[getDpiScalePercent()].configColumnAttribs = convertColAttributes(m_gridCfgHistory->getColumnConfig()); + globalSettings.mainDlg.config.syncOverdueDays = cfggrid::getSyncOverdueDays(*m_gridCfgHistory); + + std::tie(globalSettings.mainDlg.config.lastSortColumn, + globalSettings.mainDlg.config.lastSortAscending) = cfggrid::getDataView(*m_gridCfgHistory).getSortDirection(); + //-------------------------------------------------------------------------------- + globalSettings.mainDlg.config.lastUsedFiles = activeConfigFiles_; + + //write list of last used folders + globalSettings.mainDlg.folderHistoryLeft = folderHistoryLeft_ ->getList(); + globalSettings.mainDlg.folderHistoryRight = folderHistoryRight_->getList(); + + globalSettings.mainDlg.textSearchRespectCase = m_checkBoxMatchCase->GetValue(); + + wxAuiPaneInfo& logPane = auiMgr_.GetPane(m_panelLog); + assert(m_bpButtonToggleLog->isActive() == logPane.IsShown()); + + if (logPane.IsShown()) + { + if (logPane.IsMaximized()) + auiMgr_.RestorePane(logPane); //!= wxAuiPaneInfo::Restore() which does not un-hide other panels (WTF!?) + else //ensure current window sizes will be used when pane is shown again: + logPane.best_size = logPane.rect.GetSize(); + } + //else: logPane.best_size already contains non-maximized value + + //auiMgr_.Update(); //[!] not needed + globalSettings.dpiLayouts[getDpiScalePercent()].panelLayout = auiMgr_.SavePerspective(); //does not need wxAuiManager::Update()! + + const auto& [size, pos, isMaximized] = WindowLayout::getBeforeClose(*this); //call *after* wxAuiManager::SavePerspective()! + globalSettings.dpiLayouts[getDpiScalePercent()].mainDlg = {size, pos, isMaximized}; + + return globalSettings; +} + + +namespace +{ +//user expectations for partial sync: +// 1. selected folder implies also processing child items +// 2. to-be-moved item requires also processing target item +std::vector expandSelectionForPartialSync(const std::vector& selection) +{ + std::vector output; + + for (FileSystemObject* fsObj : selection) + visitFSObjectRecursively(*fsObj, [&](FolderPair& folder) { output.push_back(&folder); }, + [&](FilePair& file) + { + output.push_back(&file); + switch (file.getSyncOperation()) //evaluate comparison result and sync direction + { + case SO_MOVE_LEFT_FROM: + case SO_MOVE_LEFT_TO: + case SO_MOVE_RIGHT_FROM: + case SO_MOVE_RIGHT_TO: + if (FilePair* refFile = file.getMovePair()) + output.push_back(refFile); + else assert(false); + break; + + case SO_CREATE_LEFT: + case SO_CREATE_RIGHT: + case SO_DELETE_LEFT: + case SO_DELETE_RIGHT: + case SO_OVERWRITE_LEFT: + case SO_OVERWRITE_RIGHT: + case SO_RENAME_LEFT: + case SO_RENAME_RIGHT: + case SO_UNRESOLVED_CONFLICT: + case SO_DO_NOTHING: + case SO_EQUAL: + break; + } + }, + [&](SymlinkPair& symlink) { output.push_back(&symlink); }); + + removeDuplicates(output); + return output; +} + + +bool selectionIncludesNonEqualItem(const std::vector& selection) +{ + struct ItemFound {}; + try + { + auto onFsItem = [](FileSystemObject& fsObj) { if (fsObj.getSyncOperation() != SO_EQUAL) throw ItemFound(); }; + + for (FileSystemObject* fsObj : selection) + visitFSObjectRecursively(*fsObj, onFsItem, onFsItem, onFsItem); + return false; + } + catch (ItemFound&) { return true;} +} +} + + +void MainDialog::setSyncDirManually(const std::vector& selection, SyncDirection direction) +{ + if (!selectionIncludesNonEqualItem(selection)) + return; //harmonize with onGridContextRim(): this function should be a no-op iff context menu option is disabled! + + for (FileSystemObject* fsObj : selection) + { + setSyncDirectionRec(direction, *fsObj); //set new direction (recursively) + setActiveStatus(true, *fsObj); //works recursively for directories + } + updateGui(); +} + + +void MainDialog::setIncludedManually(const std::vector& selection, bool setActive) +{ + //if hidefiltered is active, there should be no filtered elements on screen => current element was filtered out + assert(m_bpButtonShowExcluded->isActive() || !setActive); + + if (selection.empty()) + return; //harmonize with onGridContextRim(): this function should be a no-op iff context menu option is disabled! + + for (FileSystemObject* fsObj : selection) + setActiveStatus(setActive, *fsObj); //works recursively for directories + + updateGuiDelayedIf(!m_bpButtonShowExcluded->isActive()); //show update GUI before removing rows +} + + +void MainDialog::copyGridSelectionToClipboard(const zen::Grid& grid) +{ + try + { + wxString clipBuf; //perf: old wxString didn't model exponential growth, but now it's std::string-based: + static_assert(std::is_same_v); + + if (auto prov = grid.getDataProvider()) + { + std::vector colAttr = grid.getColumnConfig(); + std::erase_if(colAttr, [](const Grid::ColAttributes& ca) { return !ca.visible; }); + + for (size_t row : grid.getSelectedRows()) + for (auto it = colAttr.begin(); it != colAttr.end(); ++it) + { + clipBuf += prov->getValue(row, it->type); + clipBuf += it == colAttr.end() - 1 ? L'\n' : L'\t'; + } + } + + setClipboardText(clipBuf); + } + catch (const std::bad_alloc& e) + { + showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setMainInstructions(_("Out of memory.") + L' ' + utfTo(e.what()))); + } +} + + +void MainDialog::copyPathsToClipboard(const std::vector& selectionL, + const std::vector& selectionR) +{ + try + { + wxString clipBuf; //perf: old wxString didn't model exponential growth, but now it's std::string-based: + static_assert(std::is_same_v); + + auto appendPath = [&](const AbstractPath& itemPath) + { + if (!clipBuf.empty()) + clipBuf += L'\n'; + clipBuf += AFS::getDisplayPath(itemPath); + }; + + for (const FileSystemObject* fsObj : selectionL) + //if (!fsObj->isEmpty()) + appendPath(fsObj->getAbstractPath()); + + for (const FileSystemObject* fsObj : selectionR) + //if (!fsObj->isEmpty()) + appendPath(fsObj->getAbstractPath()); + + setClipboardText(clipBuf); + } + catch (const std::bad_alloc& e) + { + showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setMainInstructions(_("Out of memory.") + L' ' + utfTo(e.what()))); + } +} + + +std::vector MainDialog::getGridSelection(bool fromLeft, bool fromRight) const +{ + std::vector selectedRows; + + if (fromLeft) + append(selectedRows, m_gridMainL->getSelectedRows()); + + if (fromRight) + append(selectedRows, m_gridMainR->getSelectedRows()); + + removeDuplicates(selectedRows); + assert(std::is_sorted(selectedRows.begin(), selectedRows.end())); + + return filegrid::getDataView(*m_gridMainC).getAllFileRef(selectedRows); +} + + +std::vector MainDialog::getTreeSelection() const +{ + std::vector output; + + for (size_t row : m_gridOverview->getSelectedRows()) + if (std::unique_ptr node = treegrid::getDataView(*m_gridOverview).getLine(row)) + { + if (auto root = dynamic_cast(node.get())) + { + //selecting root means "select everything", *ignoring* current view filter! + for (FileSystemObject& fsObj : root->baseFolder.subfolders()) //no need to explicitly add child elements! + output.push_back(&fsObj); + for (FileSystemObject& fsObj : root->baseFolder.files()) + output.push_back(&fsObj); + for (FileSystemObject& fsObj : root->baseFolder.symlinks()) + output.push_back(&fsObj); + } + else if (auto dir = dynamic_cast(node.get())) + output.push_back(&(dir->folder)); + else if (auto file = dynamic_cast(node.get())) + append(output, file->filesAndLinks); + else assert(false); + } + return output; +} + + +void MainDialog::copyToAlternateFolder(const std::vector& selectionL, + const std::vector& selectionR) +{ + if (std::exchange(operationInProgress_, true)) + return; + ZEN_ON_SCOPE_EXIT(operationInProgress_ = false); + + std::vector copyLeft; + std::vector copyRight; + + for (const FileSystemObject* fsObj : selectionL) + if (!fsObj->isEmpty()) + copyLeft.push_back(fsObj); + + for (const FileSystemObject* fsObj : selectionR) + if (!fsObj->isEmpty()) + copyRight.push_back(fsObj); + + if (copyLeft.empty() && copyRight.empty()) + return; //harmonize with onGridContextRim(): this function should be a no-op iff context menu option is disabled! + + const int itemCount = static_cast(copyLeft.size() + copyRight.size()); + std::wstring itemList; + + for (const FileSystemObject* fsObj : copyLeft) + itemList += AFS::getDisplayPath(fsObj->getAbstractPath()) + L'\n'; + + for (const FileSystemObject* fsObj : copyRight) + itemList += AFS::getDisplayPath(fsObj->getAbstractPath()) + L'\n'; + //------------------------------------------------------------------ + + FocusPreserver fp; + + if (showCopyToDialog(this, + itemList, itemCount, + globalCfg_.mainDlg.copyToCfg.targetFolderPath, + globalCfg_.mainDlg.copyToCfg.targetFolderLastSelected, + globalCfg_.mainDlg.copyToCfg.folderHistory, globalCfg_.folderHistoryMax, + globalCfg_.sftpKeyFileLastSelected, + globalCfg_.mainDlg.copyToCfg.keepRelPaths, + globalCfg_.mainDlg.copyToCfg.overwriteIfExists) != ConfirmationButton::accept) + return; + + const auto& guiCfg = getConfig(); + + UiInputDisabler uiBlock(*this, true /*enableAbort*/); //StatusHandlerTemporaryPanel calls wxApp::Yield(), so avoid unexpected callbacks! + + StatusHandlerTemporaryPanel statusHandler(*this, std::chrono::system_clock::now() /*startTime*/, + false /*ignoreErrors*/, + guiCfg.mainCfg.autoRetryCount, + guiCfg.mainCfg.autoRetryDelay, + globalCfg_.soundFileAlertPending); + try + { + fff::copyToAlternateFolder(copyLeft, copyRight, + globalCfg_.mainDlg.copyToCfg.targetFolderPath, + globalCfg_.mainDlg.copyToCfg.keepRelPaths, + globalCfg_.mainDlg.copyToCfg.overwriteIfExists, + globalCfg_.warnDlgs, + statusHandler); //throw CancelProcess + + //"clearSelection" not needed/desired + } + catch (CancelProcess&) {} + + const StatusHandlerTemporaryPanel::Result r = statusHandler.prepareResult(); //noexcept + setLastOperationLog(r.summary, r.errorLog.ptr()); + + //updateGui(); -> not needed +} + + +void MainDialog::deleteSelectedFiles(const std::vector& selectionL, + const std::vector& selectionR, bool moveToRecycler) +{ + if (std::exchange(operationInProgress_, true)) + return; + ZEN_ON_SCOPE_EXIT(operationInProgress_ = false); + + std::vector deleteLeft = selectionL; + std::vector deleteRight = selectionR; + + std::erase_if(deleteLeft, [](const FileSystemObject* fsObj) { return fsObj->isEmpty(); }); + std::erase_if(deleteRight, [](const FileSystemObject* fsObj) { return fsObj->isEmpty(); }); + + if (deleteLeft.empty() && deleteRight.empty()) + return; //harmonize with onGridContextRim(): this function should be a no-op iff context menu option is disabled! + + const int itemCount = static_cast(deleteLeft.size() + deleteRight.size()); + std::wstring itemList; + + for (const FileSystemObject* fsObj : deleteLeft) + itemList += AFS::getDisplayPath(fsObj->getAbstractPath()) + L'\n'; + + for (const FileSystemObject* fsObj : deleteRight) + itemList += AFS::getDisplayPath(fsObj->getAbstractPath()) + L'\n'; + //------------------------------------------------------------------ + + FocusPreserver fp; + + if (showDeleteDialog(this, itemList, itemCount, + moveToRecycler) != ConfirmationButton::accept) + return; + + //wxBusyCursor dummy; -> redundant: progress already shown in status bar! + const auto& guiCfg = getConfig(); + + UiInputDisabler uiBlock(*this, true /*enableAbort*/); //StatusHandlerTemporaryPanel calls wxApp::Yield(), so avoid unexpected callbacks! + + StatusHandlerTemporaryPanel statusHandler(*this, std::chrono::system_clock::now() /*startTime*/, + false /*ignoreErrors*/, + guiCfg.mainCfg.autoRetryCount, + guiCfg.mainCfg.autoRetryDelay, + globalCfg_.soundFileAlertPending); + try + { + deleteFiles(deleteLeft, deleteRight, + extractDirectionCfg(folderCmp_, getConfig().mainCfg), + moveToRecycler, + globalCfg_.warnDlgs.warnRecyclerMissing, + statusHandler); //throw CancelProcess + } + catch (CancelProcess&) {} + + const StatusHandlerTemporaryPanel::Result r = statusHandler.prepareResult(); //noexcept + setLastOperationLog(r.summary, r.errorLog.ptr()); + + append(fullSyncLog_->log, r.errorLog.ref()); + fullSyncLog_->totalTime += r.summary.totalTime; + + //remove rows that are empty: just a beautification, invalid rows shouldn't cause issues + filegrid::getDataView(*m_gridMainC).removeInvalidRows(); + + updateGui(); +} + + +void MainDialog::renameSelectedFiles(const std::vector& selectionL, + const std::vector& selectionR) +{ + if (std::exchange(operationInProgress_, true)) + return; + ZEN_ON_SCOPE_EXIT(operationInProgress_ = false); + + std::vector renameLeft = selectionL; + std::vector renameRight = selectionR; + + std::erase_if(renameLeft, [](const FileSystemObject* fsObj) { return fsObj->isEmpty(); }); + std::erase_if(renameRight, [](const FileSystemObject* fsObj) { return fsObj->isEmpty(); }); + + if (renameLeft.empty() && renameRight.empty()) + return; //harmonize with onGridContextRim(): this function should be a no-op iff context menu option is disabled! + //------------------------------------------------------------------ + + std::vector fileNamesOld; + for (const FileSystemObject* fsObj : renameLeft) + fileNamesOld.push_back(fsObj->getItemName()); + + for (const FileSystemObject* fsObj : renameRight) + fileNamesOld.push_back(fsObj->getItemName()); + + FocusPreserver fp; + + std::vector fileNamesNew; + if (showRenameDialog(this, fileNamesOld, fileNamesNew) != ConfirmationButton::accept) + return; + + //wxBusyCursor dummy; -> redundant: progress already shown in status bar! + const auto& guiCfg = getConfig(); + + UiInputDisabler uiBlock(*this, true /*enableAbort*/); //StatusHandlerTemporaryPanel calls wxApp::Yield(), so avoid unexpected callbacks! + + StatusHandlerTemporaryPanel statusHandler(*this, std::chrono::system_clock::now() /*startTime*/, + false /*ignoreErrors*/, + guiCfg.mainCfg.autoRetryCount, + guiCfg.mainCfg.autoRetryDelay, + globalCfg_.soundFileAlertPending); + try + { + renameItems(renameLeft, {fileNamesNew.data(), renameLeft.size()}, + renameRight, {fileNamesNew.data() + renameLeft.size(), fileNamesNew.size() - renameLeft.size()}, + extractDirectionCfg(folderCmp_, getConfig().mainCfg), + statusHandler); //throw CancelProcess + } + catch (CancelProcess&) {} + + const StatusHandlerTemporaryPanel::Result r = statusHandler.prepareResult(); //noexcept + setLastOperationLog(r.summary, r.errorLog.ptr()); + + append(fullSyncLog_->log, r.errorLog.ref()); + fullSyncLog_->totalTime += r.summary.totalTime; + + updateGui(); +} + + +namespace +{ +template +AbstractPath getExistingParentFolder(const FileSystemObject& fsObj) +{ + auto folder = dynamic_cast(&fsObj); + if (!folder) + folder = dynamic_cast(&fsObj.parent()); + + while (folder) + { + if (!folder->isEmpty()) + return folder->getAbstractPath(); + + folder = dynamic_cast(&folder->parent()); + } + return fsObj.base().getAbstractPath(); +} + + +template +void extractFileDescriptor(const FileSystemObject& fsObj, Function onDescriptor) +{ + if (!fsObj.isEmpty()) + visitFSObject(fsObj, [](const FolderPair& folder) {}, + [&](const FilePair& file) + { + onDescriptor(FileDescriptor{file.getAbstractPath(), file.getAttributes()}); + }, + [](const SymlinkPair& symlink) {}); +} + + +template +void collectNonNativeFiles(const std::vector& selectedRows, const TempFileBuffer& tempFileBuf, + std::set& workLoad) +{ + for (const FileSystemObject* fsObj : selectedRows) + extractFileDescriptor(*fsObj, [&](const FileDescriptor& descr) + { + if (getNativeItemPath(descr.path).empty() && + tempFileBuf.getTempPath(descr).empty()) //TempFileBuffer::createTempFiles() contract! + workLoad.insert(descr); + }); +} + + +struct ItemPathInfo +{ + Zstring itemPath; + Zstring itemPath2; + Zstring itemName; + Zstring itemName2; + Zstring parentPath; + Zstring parentPath2; + Zstring localPath; + Zstring localPath2; +}; +template +std::vector getItemPathInfo(const std::vector& selection, const TempFileBuffer& tempFileBuf) +{ + constexpr SelectSide side2 = getOtherSide; + + std::vector pathInfos; + + for (const FileSystemObject* fsObj : selection) //context menu calls this function only if selection is not empty! + { + const AbstractPath basePath = fsObj->base().getAbstractPath(); + const AbstractPath basePath2 = fsObj->base().getAbstractPath(); + + //return paths, even if item is not (yet) existing: + const Zstring itemPath = AFS::isNullPath(basePath ) ? Zstr("") : utfTo(AFS::getDisplayPath(fsObj-> getAbstractPath())); + const Zstring itemPath2 = AFS::isNullPath(basePath2) ? Zstr("") : utfTo(AFS::getDisplayPath(fsObj-> getAbstractPath())); + const Zstring itemName = AFS::isNullPath(basePath ) ? Zstr("") : AFS::getItemName (fsObj-> getAbstractPath()); + const Zstring itemName2 = AFS::isNullPath(basePath2) ? Zstr("") : AFS::getItemName (fsObj-> getAbstractPath()); + const Zstring parentPath = AFS::isNullPath(basePath ) ? Zstr("") : utfTo(AFS::getDisplayPath(fsObj->parent().getAbstractPath())); + const Zstring parentPath2 = AFS::isNullPath(basePath2) ? Zstr("") : utfTo(AFS::getDisplayPath(fsObj->parent().getAbstractPath())); + + Zstring localPath; + Zstring localPath2; + + if (const Zstring& nativePath = getNativeItemPath(fsObj->getAbstractPath()); + !nativePath.empty()) + localPath = nativePath; //no matter if item exists or not + else //returns empty if not available (item not existing, error during copy): + extractFileDescriptor(*fsObj, [&](const FileDescriptor& descr) { localPath = tempFileBuf.getTempPath(descr); }); + + if (const Zstring& nativePath = getNativeItemPath(fsObj->getAbstractPath()); + !nativePath.empty()) + localPath2 = nativePath; + else + extractFileDescriptor(*fsObj, [&](const FileDescriptor& descr) { localPath2 = tempFileBuf.getTempPath(descr); }); + + if (localPath .empty()) localPath = replaceCpy(utfTo(L"<" + _("Local path not available for %x.") + L">"), Zstr("%x"), itemPath ); + if (localPath2.empty()) localPath2 = replaceCpy(utfTo(L"<" + _("Local path not available for %x.") + L">"), Zstr("%x"), itemPath2); + + pathInfos.push_back( + { + itemPath, + itemPath2, + itemName, + itemName2, + parentPath, + parentPath2, + localPath, + localPath2, + }); + } + return pathInfos; +} +} + + +void MainDialog::openExternalApplication(const Zstring& commandLinePhrase, bool leftSide, + const std::vector& selectionL, + const std::vector& selectionR) +{ + //do not open more than one Explorer instance! + if (commandLinePhrase == extCommandFileManager.cmdLine) + if (selectionL.size() + selectionR.size() > 1) + { + if (( leftSide && !selectionL.empty()) || + (!leftSide && selectionR.empty())) + return openExternalApplication(commandLinePhrase, leftSide, {selectionL[0]}, {}); + else + return openExternalApplication(commandLinePhrase, leftSide, {}, {selectionR[0]}); + } + + //---------------------------------------------------------------- + if (std::exchange(operationInProgress_, true)) + return; + ZEN_ON_SCOPE_EXIT(operationInProgress_ = false); + + try + { + //support fallback instead of an error in this special case + if (commandLinePhrase == extCommandFileManager.cmdLine) + { + //either left or right selection is filled with exactly one item (or no selection at all) + AbstractPath itemPath = getNullPath(); + if (!selectionL.empty()) + { + if (selectionL[0]->isEmpty()) + return openFolderInFileBrowser(getExistingParentFolder(*selectionL[0])); //throw FileError + + itemPath = selectionL[0]->getAbstractPath(); + } + else if (!selectionR.empty()) + { + if (selectionR[0]->isEmpty()) + return openFolderInFileBrowser(getExistingParentFolder(*selectionR[0])); //throw FileError + + itemPath = selectionR[0]->getAbstractPath(); + } + else + return openFolderInFileBrowser(leftSide ? //throw FileError + createAbstractPath(firstFolderPair_->getValues().folderPathPhraseLeft) : + createAbstractPath(firstFolderPair_->getValues().folderPathPhraseRight)); + + //itemPath != base folder in this context + if (const Zstring& gdriveUrl = getGoogleDriveFolderUrl(*AFS::getParentPath(itemPath)); //throw FileError + !gdriveUrl.empty()) + return openWithDefaultApp(gdriveUrl); //throw FileError + } + + std::vector cmdLines; + if (containsFileItemMacro(commandLinePhrase)) + { + //regular command evaluation: + const size_t invokeCount = selectionL.size() + selectionR.size(); + assert(invokeCount > 0); + if (invokeCount > EXT_APP_MASS_INVOKE_THRESHOLD) + if (globalCfg_.confirmDlgs.confirmCommandMassInvoke) + { + bool dontAskAgain = false; + switch (showConfirmationDialog(this, DialogInfoType::warning, PopupDialogCfg().setTitle(_("Confirm")). + setMainInstructions(replaceCpy(_P("Do you really want to execute the command %y for one item?", + "Do you really want to execute the command %y for %x items?", invokeCount), + L"%y", fmtPath(commandLinePhrase))). + setCheckBox(dontAskAgain, _("&Don't show this warning again")), + _("&Execute"))) + { + case ConfirmationButton::accept: + globalCfg_.confirmDlgs.confirmCommandMassInvoke = !dontAskAgain; + break; + case ConfirmationButton::cancel: + return; + } + } + + std::set nonNativeFiles; + if (contains(commandLinePhrase, macroNameLocalPath) || + contains(commandLinePhrase, macroNameLocalPaths)) + { + collectNonNativeFiles(selectionL, tempFileBuf_, nonNativeFiles); + collectNonNativeFiles(selectionR, tempFileBuf_, nonNativeFiles); + } + if (contains(commandLinePhrase, macroNameLocalPath2)) + { + collectNonNativeFiles(selectionL, tempFileBuf_, nonNativeFiles); + collectNonNativeFiles(selectionR, tempFileBuf_, nonNativeFiles); + } + + //##################### create temporary files for non-native paths ###################### + if (!nonNativeFiles.empty()) + { + const auto& guiCfg = getConfig(); + + FocusPreserver fp; + + UiInputDisabler uiBlock(*this, true /*enableAbort*/); //StatusHandlerTemporaryPanel calls wxApp::Yield(), so avoid unexpected callbacks! + + StatusHandlerTemporaryPanel statusHandler(*this, std::chrono::system_clock::now() /*startTime*/, + false /*ignoreErrors*/, + guiCfg.mainCfg.autoRetryCount, + guiCfg.mainCfg.autoRetryDelay, + globalCfg_.soundFileAlertPending); + try + { + tempFileBuf_.createTempFiles(nonNativeFiles, statusHandler); //throw CancelProcess + //"clearSelection" not needed/desired + } + catch (CancelProcess&) {} + + const StatusHandlerTemporaryPanel::Result r = statusHandler.prepareResult(); //noexcept + setLastOperationLog(r.summary, r.errorLog.ptr()); + + if (r.summary.result == TaskResult::cancelled) + return; + + //updateGui(); -> not needed + } + //######################################################################################## + + std::vector pathInfos; + append(pathInfos, getItemPathInfo(selectionL, tempFileBuf_)); + append(pathInfos, getItemPathInfo(selectionR, tempFileBuf_)); + + Zstring cmdLineTmp = expandMacros(commandLinePhrase); + + //support path lists for a single command line: https://freefilesync.org/forum/viewtopic.php?t=10328#p39305 + auto replaceListMacro = [&](const ZstringView macroName, const Zstring ItemPathInfo::*itemPath) + { + replace(cmdLineTmp, Zstring() + Zstr('"') + macroName + Zstr('"'), macroName); //get rid of quotes if existing + + if (contains(cmdLineTmp, macroName)) + { + Zstring pathList; + for (const ItemPathInfo& pathInfo : pathInfos) + { + if (!pathList.empty()) + pathList += Zstr(' '); + pathList += escapeCommandArg(pathInfo.*itemPath); + } + replace(cmdLineTmp, macroName, pathList); + } + }; + replaceListMacro(macroNameItemPaths, &ItemPathInfo::itemPath); + replaceListMacro(macroNameLocalPaths, &ItemPathInfo::localPath); + replaceListMacro(macroNameItemNames, &ItemPathInfo::itemName); + replaceListMacro(macroNameParentPaths, &ItemPathInfo::parentPath); + + //generate multiple command lines per each selected item + for (const ItemPathInfo& pathInfo : pathInfos) + if (commandLinePhrase == extCommandOpenDefault.cmdLine) + //not strictly needed, but: 1. better error reporting (Windows) 2. not async => avoid zombies (Linux/macOS) + openWithDefaultApp(pathInfo.localPath); //throw FileError + else + { + Zstring cmdLineItem = cmdLineTmp; + + auto replaceMacro = [&](const ZstringView macroName, const Zstring& value) + { + replace(cmdLineItem, Zstring() + Zstr('"') + macroName + Zstr('"'), macroName); //get rid of quotes if existing + replace(cmdLineItem, macroName, escapeCommandArg(value)); + }; + + replaceMacro(macroNameItemPath, pathInfo.itemPath); + replaceMacro(macroNameItemPath2, pathInfo.itemPath2); + replaceMacro(macroNameLocalPath, pathInfo.localPath); + replaceMacro(macroNameLocalPath2, pathInfo.localPath2); + replaceMacro(macroNameItemName, pathInfo.itemName); + replaceMacro(macroNameItemName2, pathInfo.itemName2); + replaceMacro(macroNameParentPath, pathInfo.parentPath); + replaceMacro(macroNameParentPath2, pathInfo.parentPath2); + + cmdLines.push_back(std::move(cmdLineItem)); + } + + removeDuplicatesStable(cmdLines); + } + else + cmdLines.push_back(expandMacros(commandLinePhrase)); //add single entry (even if selection is empty!) + + for (const Zstring& cmdLine : cmdLines) + try + { + std::optional timeoutMs; + if (cmdLines.size() <= EXT_APP_MASS_INVOKE_THRESHOLD) + timeoutMs = EXT_APP_MAX_TOTAL_WAIT_TIME_MS / cmdLines.size(); //run async, but give consoleExecute() some "time to fail" + //else: run synchronously + + if (const auto& [exitCode, output] = consoleExecute(cmdLine, timeoutMs); //throw SysError, SysErrorTimeOut + exitCode != 0) + throw SysError(formatSystemError(utfTo(commandLinePhrase), + replaceCpy(_("Exit code %x"), L"%x", numberTo(exitCode)), utfTo(output))); + } + catch (SysErrorTimeOut&) {} //child process not failed yet => probably fine :> + catch (const SysError& e) { throw FileError(replaceCpy(_("Command %x failed."), L"%x", fmtPath(cmdLine)), e.toString()); } + } + catch (const FileError& e) { showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); } +} + + +void MainDialog::setStatusInfo(const wxString& text, bool highlight) +{ + if (statusTxts_.empty()) + { + m_staticTextStatusCenter->SetFont((m_staticTextStatusCenter->GetFont().*(highlight ? &wxFont::Bold : &wxFont::GetBaseFont))()); + m_staticTextStatusCenter->SetForegroundColour(highlight ? getColorFlashStatusInfo() : wxNullColour); + + setText(*m_staticTextStatusCenter, text); + m_panelStatusBar->Layout(); + } + else + statusTxts_.front() = text; + + statusTxtHighlightFirst_ = highlight; +} + + +void MainDialog::flashStatusInfo(const wxString& text) +{ + if (statusTxts_.empty()) + { + statusTxts_.push_back(m_staticTextStatusCenter->GetLabelText()); + statusTxts_.push_back(text); + + m_staticTextStatusCenter->SetForegroundColour(getColorFlashStatusInfo()); + m_staticTextStatusCenter->SetFont(m_staticTextStatusCenter->GetFont().Bold()); + + popStatusInfo(); + } + else + statusTxts_.insert(statusTxts_.begin() + 1, text); +} + + +void MainDialog::popStatusInfo() +{ + assert(!statusTxts_.empty()); + if (!statusTxts_.empty()) + { + const wxString statusTxt = std::move(statusTxts_.back()); + statusTxts_.pop_back(); + + if (statusTxts_.empty()) + setStatusInfo(statusTxt, statusTxtHighlightFirst_); + else + { + guiQueue_.processAsync([] { std::this_thread::sleep_for(std::chrono::seconds(3)); }, [this] { popStatusInfo(); }); + + setText(*m_staticTextStatusCenter, statusTxt); + m_panelStatusBar->Layout(); + } + } +} + + +void MainDialog::onResizeTopButtonPanel(wxEvent& event) +{ + const double horizontalWeight = 0.3; + const int newOrientation = m_panelTopButtons->GetSize().GetWidth() * horizontalWeight > + m_panelTopButtons->GetSize().GetHeight() ? wxHORIZONTAL : wxVERTICAL; //check window, NOT sizer width! + + assert(m_buttonCompare->GetContainingSizer()->GetItem(static_cast(0))->IsSpacer()); + + if (bSizerTopButtons->GetOrientation() != newOrientation) + { + bSizerTopButtons->SetOrientation(newOrientation); + + m_buttonCompare->GetContainingSizer()->GetItem(static_cast(0))->SetProportion(newOrientation == wxHORIZONTAL ? 1 : 0); + m_buttonCancel ->GetContainingSizer()->GetItem(m_buttonCancel) ->SetProportion(newOrientation == wxHORIZONTAL ? 0 : 1); + m_buttonCompare->GetContainingSizer()->GetItem(m_buttonCompare) ->SetProportion(newOrientation == wxHORIZONTAL ? 0 : 1); + m_buttonSync ->GetContainingSizer()->GetItem(m_buttonSync) ->SetProportion(newOrientation == wxHORIZONTAL ? 0 : 1); + + m_panelTopButtons->Layout(); + } + event.Skip(); +} + + +void MainDialog::onResizeConfigPanel(wxEvent& event) +{ + const double horizontalWeight = 0.75; + const int newOrientation = m_panelConfig->GetSize().GetWidth() * horizontalWeight > + m_panelConfig->GetSize().GetHeight() ? wxHORIZONTAL : wxVERTICAL; //check window, NOT sizer width! + if (bSizerConfig->GetOrientation() != newOrientation) + { + //hide button labels for horizontal layout + for (wxSizerItem* szItem : bSizerCfgHistoryButtons->GetChildren()) + if (auto sizerChild = dynamic_cast(szItem->GetSizer())) + for (wxSizerItem* szItem2 : sizerChild->GetChildren()) + if (auto btnLabel = dynamic_cast(szItem2->GetWindow())) + btnLabel->Show(newOrientation == wxVERTICAL); + + bSizerConfig->SetOrientation(newOrientation); + bSizerCfgHistoryButtons->SetOrientation(newOrientation == wxHORIZONTAL ? wxVERTICAL : wxHORIZONTAL); + bSizerSaveAs ->SetOrientation(newOrientation == wxHORIZONTAL ? wxVERTICAL : wxHORIZONTAL); + m_panelConfig->Layout(); + } + event.Skip(); +} + + +void MainDialog::onResizeViewPanel(wxEvent& event) +{ + const int newOrientation = m_panelViewFilter->GetSize().GetWidth() > + m_panelViewFilter->GetSize().GetHeight() ? wxHORIZONTAL : wxVERTICAL; //check window, NOT sizer width! + if (bSizerViewFilter->GetOrientation() != newOrientation) + { + bSizerStatistics ->SetOrientation(newOrientation); + bSizerViewButtons->SetOrientation(newOrientation); + bSizerViewFilter ->SetOrientation(newOrientation); + + //apply opposite orientation for child sizers + const int childOrient = newOrientation == wxHORIZONTAL ? wxVERTICAL : wxHORIZONTAL; + + for (wxSizerItem* szItem : bSizerStatistics->GetChildren()) + if (auto sizerChild = dynamic_cast(szItem->GetSizer())) + if (sizerChild->GetOrientation() != childOrient) + sizerChild->SetOrientation(childOrient); + + m_panelViewFilter->Layout(); + m_panelStatistics->Layout(); + } + event.Skip(); +} + + +void MainDialog::onResizeLeftFolderWidth(wxEvent& event) +{ + //adapt left-shift display distortion caused by scrollbars for multiple folder pairs + const int width = m_panelTopLeft->GetSize().GetWidth(); + for (FolderPairPanel* panel : additionalFolderPairs_) + panel->m_panelLeft->SetMinSize({width, -1}); + + event.Skip(); +} + + +void MainDialog::onTreeKeyEvent(wxKeyEvent& event) +{ + const std::vector selection = getTreeSelection(); + + int keyCode = event.GetKeyCode(); + if (m_gridOverview->GetLayoutDirection() == wxLayout_RightToLeft) + { + if (keyCode == WXK_LEFT || keyCode == WXK_NUMPAD_LEFT) + keyCode = WXK_RIGHT; + else if (keyCode == WXK_RIGHT || keyCode == WXK_NUMPAD_RIGHT) + keyCode = WXK_LEFT; + } + + if (event.ControlDown()) + switch (keyCode) + { + case 'C': + case WXK_INSERT: //CTRL + C || CTRL + INS + case WXK_NUMPAD_INSERT: + copyGridSelectionToClipboard(*m_gridOverview); + return; + } + + else if (event.AltDown()) + switch (keyCode) + { + case WXK_NUMPAD_LEFT: + case WXK_LEFT: //ALT + + setSyncDirManually(selection, SyncDirection::left); + return; + + case WXK_NUMPAD_RIGHT: + case WXK_RIGHT: //ALT + + setSyncDirManually(selection, SyncDirection::right); + return; + + case WXK_NUMPAD_UP: + case WXK_NUMPAD_DOWN: + case WXK_UP: //ALT + + case WXK_DOWN: //ALT + + setSyncDirManually(selection, SyncDirection::none); + return; + } + + else + switch (keyCode) + { + case WXK_F2: + case WXK_NUMPAD_F2: + renameSelectedFiles(selection, selection); + return; + + case WXK_RETURN: + case WXK_NUMPAD_ENTER: + startSyncForSelecction(selection); + return; + + case WXK_SPACE: + case WXK_NUMPAD_SPACE: + if (!selection.empty()) + setIncludedManually(selection, m_bpButtonShowExcluded->isActive() && !selection[0]->isActive()); + //always exclude items if "m_bpButtonShowExcluded is unchecked" => yes, it's possible to have already unchecked items in selection, so we need to overwrite: + //e.g. select root node while the first item returned is not shown on grid! + return; + + case WXK_DELETE: + case WXK_NUMPAD_DELETE: + deleteSelectedFiles(selection, selection, !event.ShiftDown() /*moveToRecycler*/); + return; + } + + event.Skip(); //unknown keypress: propagate +} + + +void MainDialog::onGridKeyEvent(wxKeyEvent& event, Grid& grid, bool leftSide) +{ + const std::vector selection = getGridSelection(); + const std::vector selectionL = getGridSelection(true, false); + const std::vector selectionR = getGridSelection(false, true); + + int keyCode = event.GetKeyCode(); + if (grid.GetLayoutDirection() == wxLayout_RightToLeft) + { + if (keyCode == WXK_LEFT || keyCode == WXK_NUMPAD_LEFT) + keyCode = WXK_RIGHT; + else if (keyCode == WXK_RIGHT || keyCode == WXK_NUMPAD_RIGHT) + keyCode = WXK_LEFT; + } + + if (event.ControlDown()) + switch (keyCode) + { + case 'C': + case WXK_INSERT: //CTRL + C || CTRL + INS + case WXK_NUMPAD_INSERT: + copyPathsToClipboard(selectionL, selectionR); + return; // -> swallow event! don't allow default grid commands! + + case 'T': //CTRL + T + copyToAlternateFolder(selectionL, selectionR); + return; + } + + else if (event.AltDown()) + switch (keyCode) + { + case WXK_NUMPAD_LEFT: + case WXK_LEFT: //ALT + + setSyncDirManually(selection, SyncDirection::left); + return; + + case WXK_NUMPAD_RIGHT: + case WXK_RIGHT: //ALT + + setSyncDirManually(selection, SyncDirection::right); + return; + + case WXK_NUMPAD_UP: + case WXK_NUMPAD_DOWN: + case WXK_UP: //ALT + + case WXK_DOWN: //ALT + + setSyncDirManually(selection, SyncDirection::none); + return; + } + + else + { + //0 ... 9 + const size_t extAppPos = [&]() -> size_t + { + if ('0' <= keyCode && keyCode <= '9') + return keyCode - '0'; + if (WXK_NUMPAD0 <= keyCode && keyCode <= WXK_NUMPAD9) + return keyCode - WXK_NUMPAD0; + return static_cast(-1); + }(); + + if (extAppPos < globalCfg_.externalApps.size()) + { + openExternalApplication(globalCfg_.externalApps[extAppPos].cmdLine, leftSide, selectionL, selectionR); + return; + } + + switch (keyCode) + { + case WXK_F2: + case WXK_NUMPAD_F2: + renameSelectedFiles(selectionL, selectionR); + return; + + case WXK_RETURN: + case WXK_NUMPAD_ENTER: + startSyncForSelecction(selection); + return; + + case WXK_SPACE: + case WXK_NUMPAD_SPACE: + if (!selection.empty()) + setIncludedManually(selection, m_bpButtonShowExcluded->isActive() && !selection[0]->isActive()); + return; + + case WXK_DELETE: + case WXK_NUMPAD_DELETE: + deleteSelectedFiles(selectionL, selectionR, !event.ShiftDown() /*moveToRecycler*/); + return; + } + } + + event.Skip(); //unknown keypress: propagate +} + + +void MainDialog::onLocalKeyEvent(wxKeyEvent& event) //process key events without explicit menu entry :) +{ + if (localKeyEventsEnabled_) //avoid recursion + { + localKeyEventsEnabled_ = false; + ZEN_ON_SCOPE_EXIT(localKeyEventsEnabled_ = true); + + const int keyCode = event.GetKeyCode(); + + //CTRL + X + /* if (event.ControlDown()) + switch (keyCode) + { + case 'F': //CTRL + F + showFindPanel(); + return; //-> swallow event! + } */ + + if (event.ControlDown()) + switch (keyCode) + { + case WXK_TAB: //CTRL + TAB + case WXK_NUMPAD_TAB: //don't use F10: avoid accidental clicks: https://freefilesync.org/forum/viewtopic.php?t=1663 + swapSides(); + return; //-> swallow event! + } + + switch (keyCode) + { + case WXK_F3: + case WXK_NUMPAD_F3: + startFindNext(!event.ShiftDown() /*searchAscending*/); + return; //-> swallow event! + + //case WXK_F6: + //{ + // wxCommandEvent dummy2(wxEVT_COMMAND_BUTTON_CLICKED); + // m_bpButtonCmpConfig->Command(dummy2); //simulate click + //} + //return; //-> swallow event! + + //case WXK_F7: + //{ + // wxCommandEvent dummy2(wxEVT_COMMAND_BUTTON_CLICKED); + // m_bpButtonFilter->Command(dummy2); //simulate click + //} + //return; //-> swallow event! + + //case WXK_F8: + //{ + // wxCommandEvent dummy2(wxEVT_COMMAND_BUTTON_CLICKED); + // m_bpButtonSyncConfig->Command(dummy2); //simulate click + //} + //return; //-> swallow event! + + case WXK_F11: + setGridViewType(m_bpButtonViewType->isActive() ? GridViewType::difference : GridViewType::action); + return; //-> swallow event! + + //redirect certain (unhandled) keys directly to grid! + case WXK_UP: + case WXK_DOWN: + case WXK_LEFT: + case WXK_RIGHT: + case WXK_PAGEUP: + case WXK_PAGEDOWN: + case WXK_HOME: + case WXK_END: + + case WXK_NUMPAD_UP: + case WXK_NUMPAD_DOWN: + case WXK_NUMPAD_LEFT: + case WXK_NUMPAD_RIGHT: + case WXK_NUMPAD_PAGEUP: + case WXK_NUMPAD_PAGEDOWN: + case WXK_NUMPAD_HOME: + case WXK_NUMPAD_END: + { + const wxWindow* focus = wxWindow::FindFocus(); + if (!isComponentOf(focus, m_gridMainL ) && // + !isComponentOf(focus, m_gridMainC ) && //don't propagate keyboard commands if grid is already in focus + !isComponentOf(focus, m_gridMainR ) && // + !isComponentOf(focus, m_gridOverview ) && + !isComponentOf(focus, m_gridCfgHistory) && //don't propagate if selecting config + !isComponentOf(focus, m_panelSearch ) && + !isComponentOf(focus, m_panelLog ) && + !isComponentOf(focus, m_panelDirectoryPairs) && //don't propagate if changing directory fields + m_gridMainL->IsEnabled()) + { + m_gridMainL->SetFocus(); + + event.SetEventType(wxEVT_KEY_DOWN); //the grid event handler doesn't expect wxEVT_CHAR_HOOK! + m_gridMainL->getMainWin().GetEventHandler()->ProcessEvent(event); //propagating event to child lead to recursion with old key_event.h handling => still an issue? + event.Skip(false); //definitively handled now! + return; + } + } + break; + + case WXK_ESCAPE: //let's do something useful and hide the log panel + if (!isComponentOf(wxWindow::FindFocus(), m_panelSearch) && //search panel also handles ESC! + m_panelLog->IsEnabled()) + { + if (auiMgr_.GetPane(m_panelLog).IsShown()) //else: let it "ding" + return showLogPanel(false /*show*/); + } + break; + } + } + event.Skip(); +} + + +void MainDialog::onTreeGridSelection(GridSelectEvent& event) +{ + //scroll m_gridMain to user's new selection on m_gridOverview + ptrdiff_t leadRow = -1; + if (event.positive_ && event.rowFirst_ != event.rowLast_) + if (std::unique_ptr node = treegrid::getDataView(*m_gridOverview).getLine(event.rowFirst_)) + { + if (const TreeView::RootNode* root = dynamic_cast(node.get())) + leadRow = filegrid::getDataView(*m_gridMainC).findRowFirstChild(&(root->baseFolder)); + else if (const TreeView::DirNode* dir = dynamic_cast(node.get())) + { + leadRow = filegrid::getDataView(*m_gridMainC).findRowDirect(&(dir->folder)); + if (leadRow < 0) //directory was filtered out! still on tree view (but NOT on grid view) + leadRow = filegrid::getDataView(*m_gridMainC).findRowFirstChild(&(dir->folder)); + } + else if (const TreeView::FilesNode* files = dynamic_cast(node.get())) + { + assert(!files->filesAndLinks.empty()); + if (!files->filesAndLinks.empty()) + leadRow = filegrid::getDataView(*m_gridMainC).findRowDirect(files->filesAndLinks[0]); + } + } + + if (leadRow >= 0) + { + leadRow = std::max(0, leadRow - 1); //scroll one more row + + m_gridMainL->scrollTo(leadRow); // + m_gridMainC->scrollTo(leadRow); //scroll all of them (including "scroll master") + m_gridMainR->scrollTo(leadRow); // + + m_gridOverview->getMainWin().Update(); //draw cursor immediately rather than on next idle event (required for slow CPUs, netbook) + } + + //get selection on overview panel and set corresponding markers on main grid + std::unordered_set markedFilesAndLinks; //mark files/symlinks directly + std::unordered_set markedContainer; //mark full container including child-objects + + for (size_t row : m_gridOverview->getSelectedRows()) + if (std::unique_ptr node = treegrid::getDataView(*m_gridOverview).getLine(row)) + { + if (const TreeView::RootNode* root = dynamic_cast(node.get())) + markedContainer.insert(&(root->baseFolder)); + else if (const TreeView::DirNode* dir = dynamic_cast(node.get())) + markedContainer.insert(&(dir->folder)); + else if (const TreeView::FilesNode* files = dynamic_cast(node.get())) + markedFilesAndLinks.insert(files->filesAndLinks.begin(), files->filesAndLinks.end()); + } + + filegrid::setNavigationMarker(*m_gridMainL, *m_gridMainR, + std::move(markedFilesAndLinks), std::move(markedContainer)); + + //selecting overview should clear main grid selection (if any) but not the other way around: + m_gridMainL->clearSelection(GridEventPolicy::deny); + m_gridMainC->clearSelection(GridEventPolicy::deny); + m_gridMainR->clearSelection(GridEventPolicy::deny); + + event.Skip(); +} + + +namespace +{ +template +std::vector getFilterPhrasesRel(const std::vector& selection) +{ + std::vector output; + for (const FileSystemObject* fsObj : selection) + { + //#pragma warning(suppress: 6011) -> fsObj bound in this context! + Zstring phrase = FILE_NAME_SEPARATOR + fsObj->getRelativePath(); + + const bool isFolder = dynamic_cast(fsObj) != nullptr; + if (isFolder) + phrase += FILE_NAME_SEPARATOR; + + output.push_back(std::move(phrase)); + } + return output; +} + + +Zstring getFilterPhraseRel(const std::vector& selectionL, + const std::vector& selectionR) +{ + std::vector phrases; + append(phrases, getFilterPhrasesRel(selectionL)); + append(phrases, getFilterPhrasesRel(selectionR)); + + removeDuplicatesStable(phrases, [](const Zstring& lhs, const Zstring& rhs) { return compareNoCase(lhs, rhs) < 0; }); + //ignore case, just like path filter + + Zstring relPathPhrase; + for (const Zstring& phrase : phrases) + { + relPathPhrase += phrase; + relPathPhrase += Zstr('\n'); + } + + return trimCpy(relPathPhrase); +} +} + + +void MainDialog::onTreeGridContext(GridContextMenuEvent& event) +{ + const std::vector& selection = getTreeSelection(); //referenced by lambdas! + ContextMenu menu; + + //---------------------------------------------------------------------------------------------------- + auto getImage = [&](SyncDirection dir, SyncOperation soDefault) + { + return mirrorIfRtl(getSyncOpImage(!selection.empty() && selection[0]->getSyncOperation() != SO_EQUAL ? + selection[0]->testSyncOperation(dir) : soDefault)); + }; + const wxImage opRight = getImage(SyncDirection::right, SO_OVERWRITE_RIGHT); + const wxImage opNone = getImage(SyncDirection::none, SO_DO_NOTHING ); + const wxImage opLeft = getImage(SyncDirection::left, SO_OVERWRITE_LEFT ); + + wxString shortcutLeft = L"\tAlt+Left"; + wxString shortcutRight = L"\tAlt+Right"; + if (m_gridOverview->GetLayoutDirection() == wxLayout_RightToLeft) + std::swap(shortcutLeft, shortcutRight); + + const bool nonEqualSelected = selectionIncludesNonEqualItem(selection); + menu.addItem(_("Set direction:") + L" ->" + shortcutRight, [this, &selection] { setSyncDirManually(selection, SyncDirection::right); }, opRight, nonEqualSelected); + menu.addItem(_("Set direction:") + L" -" L"\tAlt+Down", [this, &selection] { setSyncDirManually(selection, SyncDirection::none); }, opNone, nonEqualSelected); + menu.addItem(_("Set direction:") + L" <-" + shortcutLeft, [this, &selection] { setSyncDirManually(selection, SyncDirection::left); }, opLeft, nonEqualSelected); + //Gtk needs a direction, "<-", because it has no context menu icons! + //Gtk requires "no spaces" for shortcut identifiers! + menu.addSeparator(); + //---------------------------------------------------------------------------------------------------- + auto addFilterMenu = [&](const std::wstring& label, const wxImage& img, bool include) + { + if (selection.empty()) + menu.addItem(label, nullptr, img, false /*enabled*/); + else if (selection.size() == 1) + { + ContextMenu submenu; + + const bool isFolder = dynamic_cast(selection[0]) != nullptr; + + const Zstring& relPathL = selection[0]->getRelativePath(); + const Zstring& relPathR = selection[0]->getRelativePath(); + + //by extension + const Zstring extensionL = getFileExtension(relPathL); + const Zstring extensionR = getFileExtension(relPathR); + if (!extensionL.empty()) + submenu.addItem(L"*." + utfTo(extensionL), + [this, extensionL, include] { addFilterPhrase(Zstr("*.") + extensionL, include, false /*requireNewLine*/); }); + + if (!extensionR.empty() && !equalNoCase(extensionL, extensionR)) //rare, but possible (e.g. after manual rename) + submenu.addItem(L"*." + utfTo(extensionR), + [this, extensionR, include] { addFilterPhrase(Zstr("*.") + extensionR, include, false /*requireNewLine*/); }); + + //by file name + Zstring filterPhraseNameL = Zstring(Zstr("*")) + FILE_NAME_SEPARATOR + getItemName(relPathL); + Zstring filterPhraseNameR = Zstring(Zstr("*")) + FILE_NAME_SEPARATOR + getItemName(relPathR); + if (isFolder) + { + filterPhraseNameL += FILE_NAME_SEPARATOR; + filterPhraseNameR += FILE_NAME_SEPARATOR; + } + + submenu.addItem(utfTo(filterPhraseNameL), + [this, filterPhraseNameL, include] { addFilterPhrase(filterPhraseNameL, include, true /*requireNewLine*/); }); + + if (!equalNoCase(filterPhraseNameL, filterPhraseNameR)) //rare, but possible (ignore case, just like path filter) + submenu.addItem(utfTo(filterPhraseNameR), + [this, filterPhraseNameR, include] { addFilterPhrase(filterPhraseNameR, include, true /*requireNewLine*/); }); + + //by relative path + Zstring filterPhraseRelL = FILE_NAME_SEPARATOR + relPathL; + Zstring filterPhraseRelR = FILE_NAME_SEPARATOR + relPathR; + if (isFolder) + { + filterPhraseRelL += FILE_NAME_SEPARATOR; + filterPhraseRelR += FILE_NAME_SEPARATOR; + } + submenu.addItem(utfTo(filterPhraseRelL), [this, filterPhraseRelL, include] { addFilterPhrase(filterPhraseRelL, include, true /*requireNewLine*/); }); + + if (!equalNoCase(filterPhraseRelL, filterPhraseRelR)) //rare, but possible + submenu.addItem(utfTo(filterPhraseRelR), [this, filterPhraseRelR, include] { addFilterPhrase(filterPhraseRelR, include, true /*requireNewLine*/); }); + + menu.addSubmenu(label, submenu, img); + } + else //by relative path + menu.addItem(label + L" <" + _("multiple selection") + L">", + [this, &selection, include] { addFilterPhrase(getFilterPhraseRel(selection, selection), include, true /*requireNewLine*/); }, img); + }; + addFilterMenu(_("&Include via filter:"), loadImage("filter_include", dipToScreen(getMenuIconDipSize())), true); + addFilterMenu(_("&Exclude via filter:"), loadImage("filter_exclude", dipToScreen(getMenuIconDipSize())), false); + //---------------------------------------------------------------------------------------------------- + if (m_bpButtonShowExcluded->isActive() && !selection.empty() && !selection[0]->isActive()) + menu.addItem(_("Include temporarily") + L"\tSpace", [this, &selection] { setIncludedManually(selection, true); }, loadImage("checkbox_true")); + else + menu.addItem(_("Exclude temporarily") + L"\tSpace", [this, &selection] { setIncludedManually(selection, false); }, loadImage("checkbox_false"), !selection.empty()); + //---------------------------------------------------------------------------------------------------- + const bool selectionContainsItemsToSync = [&] + { + for (FileSystemObject* fsObj : expandSelectionForPartialSync(selection)) + if (getEffectiveSyncDir(fsObj->getSyncOperation()) != SyncDirection::none) + return true; + return false; + }(); + menu.addSeparator(); + menu.addItem(_("&Synchronize selection") + L"\tEnter", [&] { startSyncForSelecction(selection); }, + loadImage("start_sync_selection", dipToScreen(getMenuIconDipSize())), selectionContainsItemsToSync); + //---------------------------------------------------------------------------------------------------- + const ptrdiff_t itemsSelected = + std::count_if(selection.begin(), selection.end(), [](const FileSystemObject* fsObj) { return !fsObj->isEmpty(); }) + + std::count_if(selection.begin(), selection.end(), [](const FileSystemObject* fsObj) { return !fsObj->isEmpty(); }); + + //menu.addSeparator(); + //menu.addItem(_("&Copy to...") + L"\tCtrl+T", [&] { copyToAlternateFolder(selection, selection); }, wxNullImage, itemsSelected > 0); + //---------------------------------------------------------------------------------------------------- + menu.addSeparator(); + + menu.addItem((itemsSelected > 1 ? _("Multi-&Rename") : _("&Rename")) + L"\tF2", + [&] { renameSelectedFiles(selection, selection); }, loadImage("rename", dipToScreen(getMenuIconDipSize())), itemsSelected > 0); + + menu.addItem(_("&Delete") + L"\t(Shift+)Del", [&] { deleteSelectedFiles(selection, selection, true /*moveToRecycler*/); }, imgTrashSmall_, itemsSelected > 0); + + menu.popup(*m_gridOverview, event.mousePos_); +} + + +void MainDialog::onGridContextRim(GridContextMenuEvent& event, bool leftSide) +{ + const std::vector selection = getGridSelection(); //referenced by lambdas! + const std::vector selectionL = getGridSelection(true, false); + const std::vector selectionR = getGridSelection(false, true); + + onGridContextRim(getGridSelection(), + getGridSelection(true, false), + getGridSelection(false, true), leftSide, event.mousePos_); +} + + +void MainDialog::onGridGroupContextRim(GridClickEvent& event, bool leftSide) +{ + if (static_cast(event.hoverArea_) == HoverAreaGroup::groupName) + if (const FileView::PathDrawInfo pdi = filegrid::getDataView(*m_gridMainC).getDrawInfo(event.row_); + pdi.folderGroupObj) + { + m_gridMainL->clearSelection(GridEventPolicy::deny); + m_gridMainC->clearSelection(GridEventPolicy::deny); + m_gridMainR->clearSelection(GridEventPolicy::deny); + + std::vector selectionL; + std::vector selectionR; + (leftSide ? selectionL : selectionR).push_back(pdi.folderGroupObj); + + onGridContextRim({pdi.folderGroupObj}, + selectionL, selectionR, leftSide, event.mousePos_); + return; //"swallow" event => suppress default context menu handling + } + + assert(static_cast(event.hoverArea_) != HoverAreaGroup::groupName); + event.Skip(); +} + + +void MainDialog::onGridContextRim(const std::vector& selection, + const std::vector& selectionL, + const std::vector& selectionR, bool leftSide, wxPoint mousePos) +{ + ContextMenu menu; + + auto getImage = [&](SyncDirection dir, SyncOperation soDefault) + { + return mirrorIfRtl(getSyncOpImage(!selection.empty() && selection[0]->getSyncOperation() != SO_EQUAL ? + selection[0]->testSyncOperation(dir) : soDefault)); + }; + const wxImage opLeft = getImage(SyncDirection::left, SO_OVERWRITE_LEFT ); + const wxImage opRight = getImage(SyncDirection::right, SO_OVERWRITE_RIGHT); + const wxImage opNone = getImage(SyncDirection::none, SO_DO_NOTHING ); + + wxString shortcutLeft = L"\tAlt+Left"; + wxString shortcutRight = L"\tAlt+Right"; + if (m_gridMainL->GetLayoutDirection() == wxLayout_RightToLeft) + std::swap(shortcutLeft, shortcutRight); + + const bool nonEqualSelected = selectionIncludesNonEqualItem(selection); + menu.addItem(_("Set direction:") + L" ->" + shortcutRight, [this, &selection] { setSyncDirManually(selection, SyncDirection::right); }, opRight, nonEqualSelected); + menu.addItem(_("Set direction:") + L" -" L"\tAlt+Down", [this, &selection] { setSyncDirManually(selection, SyncDirection::none); }, opNone, nonEqualSelected); + menu.addItem(_("Set direction:") + L" <-" + shortcutLeft, [this, &selection] { setSyncDirManually(selection, SyncDirection::left); }, opLeft, nonEqualSelected); + //GTK needs a direction, "<-", because it has no context menu icons! + //GTK does not allow spaces in shortcut identifiers! + menu.addSeparator(); + //---------------------------------------------------------------------------------------------------- + auto addFilterMenu = [&](const wxString& label, const wxImage& img, bool include) + { + if (selectionL.empty() && selectionR.empty()) + menu.addItem(label, nullptr, img, false /*enabled*/); + else if (selectionL.size() + selectionR.size() == 1) + { + ContextMenu submenu; + + const bool isFolder = dynamic_cast((!selectionL.empty() ? selectionL : selectionR)[0]) != nullptr; + + const Zstring& relPath = !selectionL.empty() ? + selectionL[0]->getRelativePath() : + selectionR[0]->getRelativePath(); + //by extension + if (const Zstring extension = getFileExtension(relPath); + !extension.empty()) + submenu.addItem(L"*." + utfTo(extension), [this, extension, include] + { + addFilterPhrase(Zstr("*.") + extension, include, false /*requireNewLine*/); + }); + + //by file name + Zstring filterPhraseName = Zstring(Zstr("*")) + FILE_NAME_SEPARATOR + getItemName(relPath); + if (isFolder) + filterPhraseName += FILE_NAME_SEPARATOR; + + submenu.addItem(utfTo(filterPhraseName), [this, filterPhraseName, include] + { + addFilterPhrase(filterPhraseName, include, true /*requireNewLine*/); + }); + + //by relative path + Zstring filterPhraseRel = FILE_NAME_SEPARATOR + relPath; + if (isFolder) + filterPhraseRel += FILE_NAME_SEPARATOR; + submenu.addItem(utfTo(filterPhraseRel), [this, filterPhraseRel, include] { addFilterPhrase(filterPhraseRel, include, true /*requireNewLine*/); }); + + menu.addSubmenu(label, submenu, img); + } + else //by relative path + menu.addItem(label + L" <" + _("multiple selection") + L">", + [this, &selectionL, &selectionR, include] { addFilterPhrase(getFilterPhraseRel(selectionL, selectionR), include, true /*requireNewLine*/); }, img); + }; + addFilterMenu(_("&Include via filter:"), loadImage("filter_include", dipToScreen(getMenuIconDipSize())), true); + addFilterMenu(_("&Exclude via filter:"), loadImage("filter_exclude", dipToScreen(getMenuIconDipSize())), false); + //---------------------------------------------------------------------------------------------------- + if (m_bpButtonShowExcluded->isActive() && !selection.empty() && !selection[0]->isActive()) + menu.addItem(_("Include temporarily") + L"\tSpace", [this, &selection] { setIncludedManually(selection, true); }, loadImage("checkbox_true")); + else + menu.addItem(_("Exclude temporarily") + L"\tSpace", [this, &selection] { setIncludedManually(selection, false); }, loadImage("checkbox_false"), !selection.empty()); + //---------------------------------------------------------------------------------------------------- + const bool selectionContainsItemsToSync = [&] + { + for (FileSystemObject* fsObj : expandSelectionForPartialSync(selection)) + if (getEffectiveSyncDir(fsObj->getSyncOperation()) != SyncDirection::none) + return true; + return false; + }(); + menu.addSeparator(); + menu.addItem(_("&Synchronize selection") + L"\tEnter", [&] { startSyncForSelecction(selection); }, + loadImage("start_sync_selection", dipToScreen(getMenuIconDipSize())), selectionContainsItemsToSync); + //---------------------------------------------------------------------------------------------------- + if (!globalCfg_.externalApps.empty()) + { + menu.addSeparator(); + + for (auto it = globalCfg_.externalApps.begin(); + it != globalCfg_.externalApps.end(); + ++it) + { + //translate default external apps on the fly: 1. "Show in Explorer" 2. "Open with default application" + wxString description = translate(it->description); + + if (const size_t pos = it - globalCfg_.externalApps.begin(); + pos == 0) + description += L"\tD-Click, 0"; + else if (pos < 9) + description += L"\t" + numberTo(pos); + + auto openApp = [this, command = it->cmdLine, leftSide, &selectionL, &selectionR] { openExternalApplication(command, leftSide, selectionL, selectionR); }; + + menu.addItem(description, openApp, it->cmdLine == extCommandFileManager.cmdLine ? imgFileManagerSmall_ : wxNullImage, + it->cmdLine == extCommandFileManager.cmdLine || + !containsFileItemMacro(it->cmdLine) || + !selectionL.empty() || !selectionR.empty()); + } + } + //---------------------------------------------------------------------------------------------------- + const ptrdiff_t itemsSelected = + std::count_if(selectionL.begin(), selectionL.end(), [](const FileSystemObject* fsObj) { return !fsObj->isEmpty(); }) + + std::count_if(selectionR.begin(), selectionR.end(), [](const FileSystemObject* fsObj) { return !fsObj->isEmpty(); }); + + menu.addSeparator(); + menu.addItem(_("&Copy to...") + L"\tCtrl+T", [&] { copyToAlternateFolder(selectionL, selectionR); }, wxNullImage, itemsSelected > 0); + //---------------------------------------------------------------------------------------------------- + menu.addSeparator(); + + menu.addItem((itemsSelected > 1 ? _("Multi-&Rename") : _("&Rename")) + L"\tF2", + [&] { renameSelectedFiles(selectionL, selectionR); }, loadImage("rename", dipToScreen(getMenuIconDipSize())), itemsSelected > 0); + + menu.addItem(_("&Delete") + L"\t(Shift+)Del", [&] { deleteSelectedFiles(selectionL, selectionR, true /*moveToRecycler*/); }, imgTrashSmall_, itemsSelected > 0); + + menu.popup(leftSide ? *m_gridMainL : *m_gridMainR, mousePos); +} + + +void MainDialog::addFilterPhrase(const Zstring& phrase, bool include, bool requireNewLine) +{ + Zstring& filterString = [&]() -> Zstring& + { + if (include) + { + Zstring& includeFilter = currentCfg_.mainCfg.globalFilter.includeFilter; + if (NameFilter::isNull(includeFilter, Zstring())) //fancy way of checking for "*" include + includeFilter.clear(); + return includeFilter; + } + else + return currentCfg_.mainCfg.globalFilter.excludeFilter; + }(); + + if (requireNewLine) + { + trim(filterString, TrimSide::right, [](Zchar c) { return c == FILTER_ITEM_SEPARATOR || c == Zstr('\n') || c == Zstr(' '); }); + if (!filterString.empty()) + filterString += Zstr('\n'); + filterString += phrase; + } + else + { + trim(filterString, TrimSide::right, [](Zchar c) { return c == Zstr('\n') || c == Zstr(' '); }); + + if (contains(afterLast(filterString, Zstr('\n'), IfNotFoundReturn::all), FILTER_ITEM_SEPARATOR)) + { + if (!endsWith(filterString, FILTER_ITEM_SEPARATOR)) + filterString += Zstring() + Zstr(' ') + FILTER_ITEM_SEPARATOR; + + filterString += Zstr(' ') + phrase; + } + else + { + if (!filterString.empty()) + filterString += Zstr('\n'); + + filterString += phrase + Zstr(' ') + FILTER_ITEM_SEPARATOR; //append FILTER_ITEM_SEPARATOR to 'mark' that next extension exclude should write to same line + } + } + + updateGlobalFilterButton(); + if (include) + applyFilterConfig(); //user's temporary exclusions lost! + else //do not fully apply filter, just exclude new items: preserve user's temporary exclusions + { + for (BaseFolderPair& baseFolder : asRange(folderCmp_)) + addHardFiltering(baseFolder, phrase); + updateGui(); + } +} + + +void MainDialog::onGridLabelContextC(GridLabelClickEvent& event) +{ + ContextMenu menu; + + const GridViewType viewType = m_bpButtonViewType->isActive() ? GridViewType::action : GridViewType::difference; + menu.addItem(_("Difference") + (viewType != GridViewType::difference ? L"\tF11" : L""), + [&] { setGridViewType(GridViewType::difference); }, greyScaleIfDisabled(loadImage("compare", dipToScreen(getMenuIconDipSize())), viewType == GridViewType::difference)); + + menu.addItem(_("Action") + (viewType != GridViewType::action ? L"\tF11" : L""), + [&] { setGridViewType(GridViewType::action); }, greyScaleIfDisabled(loadImage("start_sync", dipToScreen(getMenuIconDipSize())), viewType == GridViewType::action)); + menu.popup(*m_gridMainC, {event.mousePos_.x, m_gridMainC->getColumnLabelHeight()}); +} + + +void MainDialog::onGridLabelContextRim(GridLabelClickEvent& event, bool leftSide) +{ + ContextMenu menu; + //-------------------------------------------------------------------------------------------------------- + Grid& grid = leftSide ? *m_gridMainL : *m_gridMainR; + //const ColumnTypeRim colType = static_cast(event.colType_); + + auto toggleColumn = [&](ColumnType ct) + { + auto colAttr = grid.getColumnConfig(); + + Grid::ColAttributes* caItemPath = nullptr; + Grid::ColAttributes* caToggle = nullptr; + + for (Grid::ColAttributes& ca : colAttr) + if (ca.type == static_cast(ColumnTypeRim::path)) + caItemPath = &ca; + else if (ca.type == ct) + caToggle = &ca; + + assert(caItemPath && caItemPath->stretch > 0 && caItemPath->visible); + assert(caToggle && caToggle ->stretch == 0); + + if (caItemPath && caToggle) + { + caToggle->visible = !caToggle->visible; + + //take width of newly visible column from stretched item path column + caItemPath->offset -= caToggle->visible ? caToggle->offset : -caToggle->offset; + + grid.setColumnConfig(colAttr); + } + }; + + if (const GridData* prov = grid.getDataProvider()) + for (const Grid::ColAttributes& ca : grid.getColumnConfig()) + menu.addCheckBox(prov->getColumnLabel(ca.type), [ct = ca.type, toggleColumn] { toggleColumn(ct); }, + ca.visible, ca.type != static_cast(ColumnTypeRim::path)); //do not allow user to hide this column! + //---------------------------------------------------------------------------------------------- + menu.addSeparator(); + + auto& itemPathFormat = leftSide ? globalCfg_.mainDlg.itemPathFormatLeftGrid : globalCfg_.mainDlg.itemPathFormatRightGrid; + + auto setItemPathFormat = [&](ItemPathFormat fmt) + { + itemPathFormat = fmt; + filegrid::setItemPathForm(grid, fmt); + }; + auto addFormatEntry = [&](const wxString& label, ItemPathFormat fmt) + { + menu.addRadio(label, [fmt, &setItemPathFormat] { setItemPathFormat(fmt); }, itemPathFormat == fmt); + }; + addFormatEntry(_("Item name" ), ItemPathFormat::name); + addFormatEntry(_("Relative path"), ItemPathFormat::relative); + addFormatEntry(_("Full path" ), ItemPathFormat::full); + + //---------------------------------------------------------------------------------------------- + auto setIconSize = [&](GridIconSize sz, bool showIcons) + { + globalCfg_.mainDlg.iconSize = sz; + globalCfg_.mainDlg.showIcons = showIcons; + filegrid::setupIcons(*m_gridMainL, *m_gridMainC, *m_gridMainR, globalCfg_.mainDlg.showIcons, convert(globalCfg_.mainDlg.iconSize)); + }; + + menu.addSeparator(); + menu.addCheckBox(_("Show icons:"), [&] { setIconSize(globalCfg_.mainDlg.iconSize, !globalCfg_.mainDlg.showIcons); }, globalCfg_.mainDlg.showIcons); + + auto addSizeEntry = [&](const wxString& label, GridIconSize sz) + { + menu.addRadio(label, [sz, &setIconSize] { setIconSize(sz, true /*showIcons*/); }, globalCfg_.mainDlg.iconSize == sz, globalCfg_.mainDlg.showIcons); + }; + addSizeEntry(TAB_SPACE + _("Small" ), GridIconSize::small ); + addSizeEntry(TAB_SPACE + _("Medium"), GridIconSize::medium); + addSizeEntry(TAB_SPACE + _("Large" ), GridIconSize::large ); + + //---------------------------------------------------------------------------------------------- + auto setDefault = [&] + { + grid.setColumnConfig(convertColAttributes(leftSide ? getFileGridDefaultColAttribsLeft() : getFileGridDefaultColAttribsRight(), getFileGridDefaultColAttribsLeft())); + + const GlobalConfig defaultCfg; + setItemPathFormat(leftSide ? defaultCfg.mainDlg.itemPathFormatLeftGrid : defaultCfg.mainDlg.itemPathFormatRightGrid); + setIconSize(defaultCfg.mainDlg.iconSize, defaultCfg.mainDlg.showIcons); + }; + + menu.addSeparator(); + menu.addItem(_("&Default"), setDefault, loadImage("reset_sicon")); + + // if (type == ColumnTypeRim::date) + { + auto selectTimeSpan = [&] + { + if (showSelectTimespanDlg(this, manualTimeSpanFrom_, manualTimeSpanTo_) == ConfirmationButton::accept) + { + applyTimeSpanFilter(folderCmp_, manualTimeSpanFrom_, manualTimeSpanTo_); //overwrite current active/inactive settings + //updateGuiDelayedIf(!m_bpButtonShowExcluded->isActive()); //show update GUI before removing rows + updateGui(); + } + }; + + menu.addSeparator(); + menu.addItem(_("Select time span..."), selectTimeSpan); + } + //-------------------------------------------------------------------------------------------------------- + menu.popup(grid, {event.mousePos_.x, grid.getColumnLabelHeight()}); + //event.Skip(); +} + + +void MainDialog::onOpenMenuTools(wxMenuEvent& event) +{ + //each layout menu item is either shown and owned by m_menuTools OR detached from m_menuTools and owned by detachedMenuItems_: + auto filterLayoutItems = [&](wxMenuItem* menuItem, wxWindow* panelWindow) + { + wxAuiPaneInfo& paneInfo = this->auiMgr_.GetPane(panelWindow); + if (paneInfo.IsShown()) + { + if (!detachedMenuItems_.contains(menuItem)) + detachedMenuItems_.insert(m_menuTools->Remove(menuItem)); //pass ownership + } + else if (detachedMenuItems_.contains(menuItem)) + { + detachedMenuItems_.erase(menuItem); //pass ownership + m_menuTools->Append(menuItem); // + } + }; + filterLayoutItems(m_menuItemShowMain, m_panelTopButtons); + filterLayoutItems(m_menuItemShowFolders, m_panelDirectoryPairs); + filterLayoutItems(m_menuItemShowViewFilter, m_panelViewFilter); + filterLayoutItems(m_menuItemShowConfig, m_panelConfig); + filterLayoutItems(m_menuItemShowOverview, m_gridOverview); + event.Skip(); +} + + +void MainDialog::resetLayout() +{ + m_splitterMain->setSashOffset(0); + auiMgr_.LoadPerspective(defaultPerspective_, false /*don't call wxAuiManager::Update() => already done in updateGuiForFolderPair() */); + updateGuiForFolderPair(); + + //progress dialog size: + globalCfg_.dpiLayouts[getDpiScalePercent()].progressDlg.size = std::nullopt; + globalCfg_.dpiLayouts[getDpiScalePercent()].progressDlg.isMaximized = false; +} + + +void MainDialog::onSetLayoutContext(wxMouseEvent& event) +{ + ContextMenu menu; + + menu.addItem(_("&Reset layout"), [&] { resetLayout(); }, loadImage("reset_sicon")); + //---------------------------------------------------------------------------------------- + + bool addedSeparator = false; + + for (wxAuiPaneInfo& paneInfo : auiMgr_.GetAllPanes()) + if (!paneInfo.IsShown() && + paneInfo.window != compareStatus_->getAsWindow() && + paneInfo.window != m_panelLog && + paneInfo.window != m_panelSearch) + { + if (!addedSeparator) + { + menu.addSeparator(); + addedSeparator = true; + } + + menu.addItem(replaceCpy(_("Show \"%x\""), L"%x", paneInfo.caption), [this, &paneInfo] + { + paneInfo.Show(); + this->auiMgr_.Update(); + }); + } + + menu.popup(*this); +} + + +void MainDialog::onCompSettingsContext(wxEvent& event) +{ + ContextMenu menu; + + auto setVariant = [&](CompareVariant var) + { + currentCfg_.mainCfg.cmpCfg.compareVar = var; + applyCompareConfig(true /*setDefaultViewType*/); + }; + + const CompareVariant activeCmpVar = getConfig().mainCfg.cmpCfg.compareVar; + + auto addVariantItem = [&](CompareVariant cmpVar, const char* iconName) + { + const wxImage imgSel = loadImage(iconName, -1 /*maxWidth*/, dipToScreen(getMenuIconDipSize())); + + menu.addItem(getVariantName(cmpVar), [&setVariant, cmpVar] { setVariant(cmpVar); }, greyScaleIfDisabled(imgSel, activeCmpVar == cmpVar)); + }; + addVariantItem(CompareVariant::timeSize, "cmp_time"); + addVariantItem(CompareVariant::content, "cmp_content"); + addVariantItem(CompareVariant::size, "cmp_size"); + + menu.popup(*m_bpButtonCmpContext, {m_bpButtonCmpContext->GetSize().x, 0}); +} + + +void MainDialog::onSyncSettingsContext(wxEvent& event) +{ + ContextMenu menu; + + auto setVariant = [&](SyncVariant var) + { + currentCfg_.mainCfg.syncCfg.directionCfg = getDefaultSyncCfg(var); + applySyncDirections(); + }; + + const SyncVariant activeSyncVar = getSyncVariant(getConfig().mainCfg.syncCfg.directionCfg); + + auto addVariantItem = [&](SyncVariant syncVar, const char* iconName) + { + const wxImage imgSel = mirrorIfRtl(loadImage(iconName, -1 /*maxWidth*/, dipToScreen(getMenuIconDipSize()))); + + menu.addItem(getVariantName(syncVar), [&setVariant, syncVar] { setVariant(syncVar); }, greyScaleIfDisabled(imgSel, activeSyncVar == syncVar)); + }; + addVariantItem(SyncVariant::twoWay, "sync_twoway"); + addVariantItem(SyncVariant::mirror, "sync_mirror"); + addVariantItem(SyncVariant::update, "sync_update"); + //addVariantItem(SyncVariant::custom, "sync_custom"); -> doesn't make sense, does it? + + menu.popup(*m_bpButtonSyncContext, {m_bpButtonSyncContext->GetSize().x, 0}); +} + + +void MainDialog::onDialogFilesDropped(FileDropEvent& event) +{ + assert(!event.itemPaths_.empty()); + loadConfiguration(event.itemPaths_); + //event.Skip(); +} + + +void MainDialog::onFolderSelected(wxCommandEvent& event) +{ + if (!folderCmp_.empty()) + clearGrid(); //+ update GUI! + else + updateUnsavedCfgStatus(); + + event.Skip(); +} + + +void MainDialog::cfgHistoryRemoveObsolete(const std::vector& filePaths) +{ + auto getUnavailableCfgFilesAsync = [filePaths] //don't use wxString: NOT thread-safe! (e.g. non-atomic ref-count) + { + std::vector> keepFile; //check all config files in parallel! + + for (const Zstring& filePath : filePaths) + keepFile.push_back(runAsync([=] + { + try + { + getItemType(filePath); //throw FileError + return true; + } + catch (FileError&) { return false; } //not-existing/access error? e.g. not accessible network share or USB stick => remove cfg + })); + + //potentially slow network access => limit maximum wait time! + waitForAllTimed(keepFile.begin(), keepFile.end(), std::chrono::seconds(2)); + + std::vector pathsToRemove; + + auto itFut = keepFile.begin(); + for (auto it = filePaths.begin(); it != filePaths.end(); ++it, ++itFut) + if (isReady(*itFut) && !itFut->get()) //not ready? maybe HDD that is just spinning up => better keep it + pathsToRemove.push_back(*it); + + return pathsToRemove; + }; + + guiQueue_.processAsync(getUnavailableCfgFilesAsync, [this](const std::vector& filePaths2) + { + if (!filePaths2.empty()) + { + cfggrid::getDataView(*m_gridCfgHistory).removeItems(filePaths2); + + //restore grid selection (after rows were removed) + cfggrid::addAndSelect(*m_gridCfgHistory, activeConfigFiles_, false /*scrollToSelection*/); + } + }); +} + + +void MainDialog::cfgHistoryUpdateNotes(const std::vector& filePaths) +{ + //load per-config user notes (let's not keep stale copy in GlobalSettings.xml) + for (const Zstring& filePath : filePaths) + { + auto getCfgNotes = [filePath] + { + try + { + const auto& [newGuiCfg, warningMsg] = readAnyConfig({filePath}); //throw FileError + return newGuiCfg.notes; + } + catch (FileError&) { return std::wstring(); } + }; + + guiQueue_.processAsync(getCfgNotes, [this, filePath](const std::wstring& notes) + { + if (const auto& [item, row] = cfggrid::getDataView(*m_gridCfgHistory).getItem(filePath); + item) + if (item->notes != notes) + { + cfggrid::getDataView(*m_gridCfgHistory).setNotes(filePath, notes); + m_gridCfgHistory->Refresh(); + } + }); + } +} + + +std::vector MainDialog::getJobNames() const +{ + std::vector jobNames; + for (const Zstring& cfgFilePath : activeConfigFiles_) + jobNames.push_back(equalNativePath(cfgFilePath, lastRunConfigPath_) ? + L'[' + _("Last session") + L']' : + extractJobName(cfgFilePath)); + return jobNames; +} + + +void MainDialog::updateUnsavedCfgStatus() +{ + const FfsGuiConfig guiCfg = getConfig(); + + auto makeBrightGrey = [](wxImage img) + { + img = img.ConvertToGreyscale(1.0/3, 1.0/3, 1.0/3); //treat all channels equally! + brighten(img, 80); + return img; + }; + + //update new config button + const bool allowNew = guiCfg != getDefaultGuiConfig(globalCfg_.defaultFilter); + + if (m_bpButtonNew->IsEnabled() != allowNew || !m_bpButtonNew->GetBitmap().IsOk()) //support polling + { + setImage(*m_bpButtonNew, allowNew ? loadImage("cfg_new") : makeBrightGrey(loadImage("cfg_new"))); + m_bpButtonNew->Enable(allowNew); + m_menuItemNew->Enable(allowNew); + } + + //update save config button + const bool haveUnsavedCfg = lastSavedCfg_ != guiCfg; + + const bool allowSave = haveUnsavedCfg || + activeConfigFiles_.size() > 1; + + const Zstring activeCfgFilePath = activeConfigFiles_.size() == 1 && !equalNativePath(activeConfigFiles_[0], lastRunConfigPath_) ? activeConfigFiles_[0] : Zstring(); + + if (m_bpButtonSave->IsEnabled() != allowSave || !m_bpButtonSave->GetBitmap().IsOk()) //support polling + { + setImage(*m_bpButtonSave, allowSave ? loadImage("cfg_save") : makeBrightGrey(loadImage("cfg_save"))); + m_bpButtonSave->Enable(allowSave); + m_menuItemSave->Enable(allowSave); //bitmap is automatically greyscaled on Win7 (introducing a crappy looking shift), but not on XP + } + + //set main dialog title + wxString title; + if (haveUnsavedCfg) + title += L'*'; + bool showingConfigName = true; + if (!activeCfgFilePath.empty()) + { + title += extractJobName(activeCfgFilePath); + if (const std::optional& parentPath = getParentFolderPath(activeCfgFilePath)) + title += L" [" + utfTo(*parentPath) + L']'; + } + else if (activeConfigFiles_.size() > 1) + { + for (const std::wstring& jobName : getJobNames()) + title += jobName + L" + "; + if (endsWith(title, L" + ")) + title.resize(title.size() - 3); + } + else + showingConfigName = false; + + if (showingConfigName) + title += SPACED_DASH; + + title += L"FreeFileSync " + utfTo(ffsVersion); + try + { + if (runningElevated()) //throw FileError + title += L" (root)"; + } + catch (FileError&) { assert(false); } + + if (!showingConfigName) + title += SPACED_DASH + _("Folder Comparison and Synchronization"); + + + SetTitle(title); + + //macOS-only: + OSXSetModified(haveUnsavedCfg); + SetRepresentedFilename(utfTo(activeCfgFilePath)); +} + + +void MainDialog::onConfigSave(wxCommandEvent& event) +{ + const Zstring activeCfgFilePath = activeConfigFiles_.size() == 1 && !equalNativePath(activeConfigFiles_[0], lastRunConfigPath_) ? activeConfigFiles_[0] : Zstring(); + + //if we work on a single named configuration document: save directly if changed + //else: always show file dialog + if (activeCfgFilePath.empty()) + trySaveConfig(nullptr); + else + { + if (endsWithAsciiNoCase(activeCfgFilePath, Zstr(".ffs_gui"))) + trySaveConfig(&activeCfgFilePath); + else if (endsWithAsciiNoCase(activeCfgFilePath, Zstr(".ffs_batch"))) + trySaveBatchConfig(&activeCfgFilePath); + else + showNotificationDialog(this, DialogInfoType::error, + PopupDialogCfg().setDetailInstructions(replaceCpy(_("Cannot open file %x."), L"%x", fmtPath(activeCfgFilePath)) + + L"\n\n" + _("Unexpected file extension:") + L' ' + fmtPath(getFileExtension(activeCfgFilePath)) + L'\n' + + _("Expected:") + L" ffs_gui, ffs_batch")); + } +} + + +bool MainDialog::trySaveConfig(const Zstring* guiCfgPath) //"false": error/cancel +{ + Zstring cfgFilePath; + + if (guiCfgPath) + { + cfgFilePath = *guiCfgPath; + assert(endsWithAsciiNoCase(cfgFilePath, Zstr(".ffs_gui"))); + } + else + { + const Zstring activeCfgFilePath = activeConfigFiles_.size() == 1 && !equalNativePath(activeConfigFiles_[0], lastRunConfigPath_) ? activeConfigFiles_[0] : Zstring(); + + const std::optional defaultFolderPath = !activeCfgFilePath.empty() ? + getParentFolderPath(activeCfgFilePath) : + getParentFolderPath(globalCfg_.mainDlg.config.lastSelectedFile); + + Zstring defaultFileName = !activeCfgFilePath.empty() ? + getItemName(activeCfgFilePath) : + Zstr("SyncSettings.ffs_gui"); + + //attention: activeConfigFiles_ may be an imported ffs_batch file! We don't want to overwrite it with a GUI config! + defaultFileName = beforeLast(defaultFileName, Zstr('.'), IfNotFoundReturn::all) + Zstr(".ffs_gui"); + + wxFileDialog fileSelector(this, wxString() /*message*/, utfTo(defaultFolderPath ? *defaultFolderPath : Zstr("")), utfTo(defaultFileName), + wxString(L"FreeFileSync (*.ffs_gui)|*.ffs_gui") + L"|" +_("All files") + L" (*.*)|*", + wxFD_SAVE | wxFD_OVERWRITE_PROMPT); + if (fileSelector.ShowModal() != wxID_OK) + return false; + + cfgFilePath = utfTo(fileSelector.GetPath()); + if (!endsWithAsciiNoCase(cfgFilePath, Zstr(".ffs_gui"))) //no weird shit! + cfgFilePath += Zstr(".ffs_gui"); //https://freefilesync.org/forum/viewtopic.php?t=9451#p34724 + + globalCfg_.mainDlg.config.lastSelectedFile = cfgFilePath; + } + + const FfsGuiConfig guiCfg = getConfig(); + + try + { + writeConfig(guiCfg, cfgFilePath); //throw FileError + + setLastUsedConfig(guiCfg, {cfgFilePath}); + + flashStatusInfo(_("Configuration saved")); + return true; + } + catch (const FileError& e) + { + showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); + return false; + } +} + + +bool MainDialog::trySaveBatchConfig(const Zstring* batchCfgPath) //"false": error/cancel +{ + //essentially behave like trySaveConfig(): the collateral damage of not saving GUI-only settings "m_bpButtonViewType" is negligible + + const Zstring activeCfgFilePath = activeConfigFiles_.size() == 1 && !equalNativePath(activeConfigFiles_[0], lastRunConfigPath_) ? activeConfigFiles_[0] : Zstring(); + + //prepare batch config: reuse existing batch-specific settings from file if available + BatchExclusiveConfig batchExCfg; + try + { + Zstring referenceBatchFile; + if (batchCfgPath) + referenceBatchFile = *batchCfgPath; + else if (!activeCfgFilePath.empty() && endsWithAsciiNoCase(activeCfgFilePath, Zstr(".ffs_batch"))) + referenceBatchFile = activeCfgFilePath; + + if (!referenceBatchFile.empty()) + batchExCfg = readBatchConfig(referenceBatchFile).first.batchExCfg; //throw FileError + //=> ignore warnings altogether: user has seen them already when loading the config file! + } + catch (const FileError& e) + { + showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); + return false; + } + + Zstring cfgFilePath; + if (batchCfgPath) + { + cfgFilePath = *batchCfgPath; + assert(endsWithAsciiNoCase(cfgFilePath, Zstr(".ffs_batch"))); + } + else + { + //let user update batch config: this should change batch-exclusive settings only, else the "setLastUsedConfig" below would be somewhat of a lie + if (showBatchConfigDialog(this, + batchExCfg, + currentCfg_.mainCfg.ignoreErrors) != ConfirmationButton::accept) + return false; + updateUnsavedCfgStatus(); //nothing else to update on GUI! + + const std::optional defaultFolderPath = !activeCfgFilePath.empty() ? + getParentFolderPath(activeCfgFilePath) : + getParentFolderPath(globalCfg_.mainDlg.config.lastSelectedFile); + + Zstring defaultFileName = !activeCfgFilePath.empty() ? + getItemName(activeCfgFilePath) : + Zstr("BatchRun.ffs_batch"); + + //attention: activeConfigFiles_ may be an ffs_gui file! We don't want to overwrite it with a BATCH config! + defaultFileName = beforeLast(defaultFileName, Zstr('.'), IfNotFoundReturn::all) + Zstr(".ffs_batch"); + + wxFileDialog fileSelector(this, wxString() /*message*/, utfTo(defaultFolderPath ? *defaultFolderPath : Zstr("")), utfTo(defaultFileName), + _("FreeFileSync batch") + L" (*.ffs_batch)|*.ffs_batch" + L"|" +_("All files") + L" (*.*)|*", + wxFD_SAVE | wxFD_OVERWRITE_PROMPT); + if (fileSelector.ShowModal() != wxID_OK) + return false; + + cfgFilePath = utfTo(fileSelector.GetPath()); + if (!endsWithAsciiNoCase(cfgFilePath, Zstr(".ffs_batch"))) //no weird shit! + cfgFilePath += Zstr(".ffs_batch"); //https://freefilesync.org/forum/viewtopic.php?t=9451#p34724 + + globalCfg_.mainDlg.config.lastSelectedFile = cfgFilePath; + } + + const FfsGuiConfig guiCfg = getConfig(); + try + { + writeConfig({guiCfg, batchExCfg}, cfgFilePath); //throw FileError + + setLastUsedConfig(guiCfg, {cfgFilePath}); //[!] behave as if we had saved guiCfg + + flashStatusInfo(_("Configuration saved")); + return true; + } + catch (const FileError& e) + { + showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); + return false; + } +} + + +bool MainDialog::saveOldConfig() //"false": error/cancel +{ + const FfsGuiConfig guiCfg = getConfig(); + + if (lastSavedCfg_ != guiCfg) + { + const Zstring activeCfgFilePath = activeConfigFiles_.size() == 1 && !equalNativePath(activeConfigFiles_[0], lastRunConfigPath_) ? activeConfigFiles_[0] : Zstring(); + + //notify user about changed settings + if (globalCfg_.confirmDlgs.confirmSaveConfig) + if (!activeCfgFilePath.empty()) + //only if check is active and non-default config file loaded + { + bool neverSaveChanges = false; + switch (showQuestionDialog(this, DialogInfoType::info, PopupDialogCfg().setTitle(utfTo(activeCfgFilePath)). + setMainInstructions(replaceCpy(_("Do you want to save changes to %x?"), L"%x", fmtPath(getItemName(activeCfgFilePath)))). + setCheckBox(neverSaveChanges, _("Never save &changes"), static_cast(QuestionButton2::yes)), + _("&Save"), _("Do&n't save"))) + { + case QuestionButton2::yes: //save + if (endsWithAsciiNoCase(activeCfgFilePath, Zstr(".ffs_gui"))) + return trySaveConfig(&activeCfgFilePath); //"false": error/cancel + else if (endsWithAsciiNoCase(activeCfgFilePath, Zstr(".ffs_batch"))) + return trySaveBatchConfig(&activeCfgFilePath); //"false": error/cancel + else + { + showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg(). + setDetailInstructions(replaceCpy(_("Cannot open file %x."), L"%x", fmtPath(activeCfgFilePath)) + + L"\n\n" + _("Unexpected file extension:") + L' ' + fmtPath(getFileExtension(activeCfgFilePath)) + L'\n' + + _("Expected:") + L" ffs_gui, ffs_batch")); + return false; + } + break; + + case QuestionButton2::no: //don't save + globalCfg_.confirmDlgs.confirmSaveConfig = !neverSaveChanges; + break; + + case QuestionButton2::cancel: + return false; + } + } + //user doesn't save changes => + //discard current reference file(s), this ensures next app start will load [Last session] instead of the original non-modified config selection + setLastUsedConfig(guiCfg, {} /*cfgFilePaths*/); + //this seems to make theoretical sense also: the job of this function is to make sure, current (volatile) config and reference file name are in sync + // => if user does not save cfg, it is not attached to a physical file anymore! + } + return true; +} + + +void MainDialog::onConfigLoad(wxCommandEvent& event) +{ + std::optional defaultFolderPath = getParentFolderPath(globalCfg_.mainDlg.config.lastSelectedFile); + + wxFileDialog fileSelector(this, wxString() /*message*/, utfTo(defaultFolderPath ? *defaultFolderPath : Zstr("")), wxString() /*default file name*/, + wxString(L"FreeFileSync (*.ffs_gui; *.ffs_batch)|*.ffs_gui;*.ffs_batch") + L"|" +_("All files") + L" (*.*)|*", + wxFD_OPEN | wxFD_MULTIPLE); + if (fileSelector.ShowModal() != wxID_OK) + return; + + wxArrayString tmp; + fileSelector.GetPaths(tmp); + + std::vector filePaths; + for (const wxString& path : tmp) + filePaths.push_back(utfTo(path)); + + if (!filePaths.empty()) + globalCfg_.mainDlg.config.lastSelectedFile = filePaths[0]; + + assert(!filePaths.empty()); + loadConfiguration(filePaths); +} + + +void MainDialog::onCfgGridSelection(GridSelectEvent& event) +{ + std::vector filePaths; + for (size_t row : m_gridCfgHistory->getSelectedRows()) + if (const ConfigView::Details* cfg = cfggrid::getDataView(*m_gridCfgHistory).getItem(row)) + filePaths.push_back(cfg->cfgItem.cfgFilePath); + else + assert(false); + + //clicking on already selected config should not clear comparison results: + const bool skipSelection = [&] //what about multi-selection? a second selection probably *should* clear results + { + return filePaths.size() == 1 && activeConfigFiles_.size() == 1 && + filePaths[0] == activeConfigFiles_[0]; + }(); + + if (!skipSelection) + if (filePaths.empty() || //ignore accidental clicks in empty space of configuration panel + !loadConfiguration(filePaths, true /*ignoreBrokenConfig*/)) //=> allow user to delete broken config entry! + //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 + cfggrid::addAndSelect(*m_gridCfgHistory, activeConfigFiles_, false /*scrollToSelection*/); + + event.Skip(); +} + + +void MainDialog::onCfgGridDoubleClick(GridClickEvent& event) +{ + if (!activeConfigFiles_.empty()) + { + wxCommandEvent dummy(wxEVT_COMMAND_BUTTON_CLICKED); + m_buttonCompare->Command(dummy); //simulate click + } +} + + +void MainDialog::onConfigNew(wxCommandEvent& event) +{ + loadConfiguration({}); +} + + +bool MainDialog::loadConfiguration(const std::vector& filePaths, bool ignoreBrokenConfig) //"false": error/cancel +{ + FfsGuiConfig newGuiCfg = getDefaultGuiConfig(globalCfg_.defaultFilter); + std::wstring warningMsg; + + if (!filePaths.empty()) //empty cfg file list means "use default" + try + { + std::tie(newGuiCfg, warningMsg) = readAnyConfig(filePaths); //throw FileError + //allow reading batch configurations, too + } + catch (const FileError& e) + { + showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); + if (!ignoreBrokenConfig) + return false; + } + + if (!saveOldConfig()) //=> error/cancel + return false; + + setConfig(newGuiCfg, filePaths); + + if (!warningMsg.empty()) + { + showNotificationDialog(this, DialogInfoType::warning, PopupDialogCfg().setDetailInstructions(warningMsg)); + setLastUsedConfig(FfsGuiConfig(), filePaths); //simulate changed config due to parsing errors + } + + //flashStatusInfo("Configuration loaded"); -> irrelevant!? + return true; +} + + +void MainDialog::removeSelectedCfgHistoryItems(bool deleteFromDisk) +{ + if (std::exchange(operationInProgress_, true)) + return; + ZEN_ON_SCOPE_EXIT(operationInProgress_ = false); + + const std::vector selectedRows = m_gridCfgHistory->getSelectedRows(); + if (!selectedRows.empty()) + { + std::vector filePaths; + for (size_t row : selectedRows) + if (const ConfigView::Details* cfg = cfggrid::getDataView(*m_gridCfgHistory).getItem(row)) + filePaths.push_back(cfg->cfgItem.cfgFilePath); + else + assert(false); + + if (deleteFromDisk) + { + //=========================================================================== + std::wstring fileList; + for (const Zstring& filePath : filePaths) + fileList += utfTo(filePath) + L'\n'; + + FocusPreserver fp; + + bool moveToRecycler = true; + if (showDeleteDialog(this, fileList, static_cast(filePaths.size()), + moveToRecycler) != ConfirmationButton::accept) + return; + + UiInputDisabler uiBlock(*this, true /*enableAbort*/); //StatusHandlerTemporaryPanel calls wxApp::Yield(), so avoid unexpected callbacks! + + StatusHandlerTemporaryPanel statusHandler(*this, std::chrono::system_clock::now() /*startTime*/, + false /*ignoreErrors*/, + 0 /*autoRetryCount*/, + std::chrono::seconds(0) /*autoRetryDelay*/, + globalCfg_.soundFileAlertPending); + std::vector deletedPaths; + try + { + deleteListOfFiles(filePaths, deletedPaths, moveToRecycler, globalCfg_.warnDlgs.warnRecyclerMissing, statusHandler); //throw CancelProcess + } + catch (CancelProcess&) {} + + const StatusHandlerTemporaryPanel::Result r = statusHandler.prepareResult(); //noexcept + setLastOperationLog(r.summary, r.errorLog.ptr()); + + filePaths = deletedPaths; + //=========================================================================== + } + + cfggrid::getDataView(*m_gridCfgHistory).removeItems(filePaths); + m_gridCfgHistory->Refresh(); //grid size changed => clears selection! + + //discard unsaved changes => no point in saving before loading next config, right? + //- bonus: clear activeConfigFiles_ if loadConfiguration() fails so that old configs don't reappear after restart + setLastUsedConfig(getConfig(), {} /*cfgFilePaths*/); + + //set active selection on next item to allow "batch-deletion" by holding down DEL key + //user expects that selected config is also loaded: https://freefilesync.org/forum/viewtopic.php?t=5723 + // => deleteFromDisk failed? still select selectedRows.front()! + std::vector nextCfgPaths; + if (m_gridCfgHistory->getRowCount() > 0) + { + const size_t nextRow = std::min(selectedRows.front(), m_gridCfgHistory->getRowCount() - 1); + if (const ConfigView::Details* cfg = cfggrid::getDataView(*m_gridCfgHistory).getItem(nextRow)) + { + nextCfgPaths.push_back(cfg->cfgItem.cfgFilePath); + + m_gridCfgHistory->setGridCursor(nextRow, GridEventPolicy::deny); + //= Grid::makeRowVisible(redundant) + set grid cursor + select cursor row(redundant) + } + } + + loadConfiguration(nextCfgPaths); //=> error/(cancel) + } +} + + +void MainDialog::renameSelectedCfgHistoryItem() +{ + const std::vector selectedRows = m_gridCfgHistory->getSelectedRows(); + if (!selectedRows.empty()) + { + const ConfigView::Details* cfg = cfggrid::getDataView(*m_gridCfgHistory).getItem(selectedRows[0]); + assert(cfg); + if (!cfg) + return; + + if (cfg->isLastRunCfg) + return showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions( + replaceCpy(_("%x cannot be renamed."), L"%x", fmtPath(cfg->name)))); + + const Zstring cfgPathOld = cfg->cfgItem.cfgFilePath; + + //FIRST: 1. consolidate unsaved changes using the *old* config file name, if any! + //2. get rid of multiple-selection if exists 3. load cfg to allow non-failing(!) setLastUsedConfig() below + if (!loadConfiguration({cfgPathOld})) //=> error/cancel + return; + + const Zstring fileName = getItemName(cfgPathOld); + /**/ Zstring folderPathPf = beforeLast(cfgPathOld, FILE_NAME_SEPARATOR, IfNotFoundReturn::none); + if (!folderPathPf.empty()) + folderPathPf += FILE_NAME_SEPARATOR; + + const Zstring cfgNameOld = beforeLast(fileName, Zstr('.'), IfNotFoundReturn::all); + /**/ Zstring cfgDotExt = afterLast(fileName, Zstr('.'), IfNotFoundReturn::none); + if (!cfgDotExt.empty()) + cfgDotExt = Zstr('.') + cfgDotExt; + + wxString cfgNameTmp = utfTo(cfgNameOld); + for (;;) + { + wxTextEntryDialog cfgRenameDlg(this, _("New name:"), _("Rename Configuration"), cfgNameTmp); + + wxTextValidator inputValidator(wxFILTER_EXCLUDE_CHAR_LIST); + inputValidator.SetCharExcludes(L"/\\"); //let's not silently forbid "fileNameForbiddenChars", but let it fail explicitly! + cfgRenameDlg.SetTextValidator(inputValidator); + + if (cfgRenameDlg.ShowModal() != wxID_OK) + return; + cfgNameTmp = cfgRenameDlg.GetValue(); + + const Zstring cfgNameNew = utfTo(trimCpy(cfgNameTmp)); + if (cfgNameNew == cfgNameOld) + return; + + const Zstring cfgPathNew = folderPathPf + cfgNameNew + cfgDotExt; + try + { + if (cfgNameNew.empty()) //better error message + check than wxFILTER_EMPTY, e.g. trimCpy()! + throw FileError(_("Configuration name must not be empty.")); + + moveAndRenameItem(cfgPathOld, cfgPathNew, false /*replaceExisting*/); //throw FileError, (ErrorMoveUnsupported), ErrorTargetExisting + } + catch (const FileError& e) + { + showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); + continue; + } + + cfggrid::getDataView(*m_gridCfgHistory).renameItem(cfgPathOld, cfgPathNew); + m_gridCfgHistory->Refresh(); //grid size changed => clears selection! + + const auto& [item, row] = cfggrid::getDataView(*m_gridCfgHistory).getItem(cfgPathNew); + assert(item); + m_gridCfgHistory->setGridCursor(row, GridEventPolicy::deny); + //= Grid::makeRowVisible(redundant) + set grid cursor + select cursor row(redundant) + // + //keep current cfg and just swap the file name: see previous "loadConfiguration({cfgPathOld}"! + setLastUsedConfig(lastSavedCfg_, {cfgPathNew}); + return; + } + } +} + + +void MainDialog::onCfgGridKeyEvent(wxKeyEvent& event) +{ + const int keyCode = event.GetKeyCode(); + switch (keyCode) + { + case WXK_RETURN: + case WXK_NUMPAD_ENTER: + if (!activeConfigFiles_.empty()) + { + wxCommandEvent dummy(wxEVT_COMMAND_BUTTON_CLICKED); + (folderCmp_.empty() ? m_buttonCompare : m_buttonSync)->Command(dummy); //simulate click + } + break; + + case WXK_DELETE: + case WXK_NUMPAD_DELETE: + removeSelectedCfgHistoryItems(event.ShiftDown() /*deleteFromDisk*/); + return; //"swallow" event + + case WXK_F2: + case WXK_NUMPAD_F2: + renameSelectedCfgHistoryItem(); + return; //"swallow" event + } + event.Skip(); +} + + +void MainDialog::onCfgGridContext(GridContextMenuEvent& event) +{ + ContextMenu menu; + const std::vector selectedRows = m_gridCfgHistory->getSelectedRows(); + + std::vector cfgFilePaths; + for (size_t row : selectedRows) + if (const ConfigView::Details* cfg = cfggrid::getDataView(*m_gridCfgHistory).getItem(row)) + cfgFilePaths.push_back(cfg->cfgItem.cfgFilePath); + else + assert(false); + + //-------------------------------------------------------------------------------------------------------- + ContextMenu submenu; + + auto applyBackColor = [this, &cfgFilePaths](const wxColor& col) + { + cfggrid::getDataView(*m_gridCfgHistory).setBackColor(cfgFilePaths, col); + + //re-apply selection (after sorting by color tags): + cfggrid::addAndSelect(*m_gridCfgHistory, activeConfigFiles_, false /*scrollToSelection*/); + //m_gridCfgHistory->Refresh(); <- implicit in last call + }; + + const wxSize colSize{this->GetCharHeight(), this->GetCharHeight()}; + + auto addColorOption = [&](const wxColor& col, const wxString& name) + { + submenu.addItem(name, [&, col] { applyBackColor(col); }, + rectangleImage({wxsizeToScreen(colSize.x), + wxsizeToScreen(colSize.y)}, + col.Ok() ? col : wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW), + {0xdd, 0xdd, 0xdd} /*light grey*/, dipToScreen(1)), + !selectedRows.empty()); + }; + const auto defaultColors = []() -> std::vector> + { + if (wxSystemSettings::GetAppearance().IsDark()) //=> offer darker colors + return {{wxNullColour /*=> !wxColor::IsOk()*/, L'(' + _("&Default") + L')'}, //meta options should be enclosed in parentheses + {{0xfe, 0x59, 0x48}, _("Red")}, + {{0xfe, 0xff, 0x31}, _("Yellow")}, + {{0x5a, 0xff, 0x00}, _("Green")}, + {{0x5a, 0xff, 0xff}, _("Cyan")}, + {{0x48, 0x47, 0xff}, _("Blue")}, + {{0xc1, 0x7e, 0xfe}, _("Purple")}, + {{0xb7, 0xb7, 0xb7}, _("Gray")}, + }; + else //=> offer lighter colors + return {{wxNullColour /*=> !wxColor::IsOk()*/, L'(' + _("&Default") + L')'}, //meta options should be enclosed in parentheses + {{0xff, 0xd8, 0xcb}, _("Red")}, + {{0xff, 0xf9, 0x99}, _("Yellow")}, + {{0xcc, 0xff, 0x99}, _("Green")}, + {{0xcc, 0xff, 0xff}, _("Cyan")}, + {{0xcc, 0xcc, 0xff}, _("Blue")}, + {{0xf2, 0xcb, 0xff}, _("Purple")}, + {{0xdd, 0xdd, 0xdd}, _("Gray")}, + }; + }(); + + std::unordered_set addedColorCodes; + + //add default colors + for (const auto& [color, name] : defaultColors) + { + addColorOption(color, name); + if (color.IsOk()) + addedColorCodes.insert(color.GetRGBA()); + } + + //add user-defined colors + for (const ConfigFileItem& item : cfggrid::getDataView(*m_gridCfgHistory).get()) + if (item.backColor.IsOk()) + if (const auto [it, inserted] = addedColorCodes.insert(item.backColor.GetRGBA()); + inserted) + addColorOption(item.backColor, item.backColor.GetAsString(wxC2S_HTML_SYNTAX)); //#RRGGBB + + //show color picker + wxBitmap bmpColorPicker(wxsizeToScreen(colSize.x), + wxsizeToScreen(colSize.y)); //seems we don't need to pass 24-bit depth here even for high-contrast color schemes + bmpColorPicker.SetScaleFactor(getScreenDpiScale()); + { + wxMemoryDC dc(bmpColorPicker); + const wxColor borderCol(0xdd, 0xdd, 0xdd); //light grey + drawFilledRectangle(dc, wxRect(colSize), wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW), borderCol, dipToWxsize(1)); + + dc.SetFont(dc.GetFont().Bold()); + dc.SetTextForeground(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); + dc.DrawText(L"?", wxPoint() + (colSize - dc.GetTextExtent(L"?")) / 2); + } + + submenu.addItem(_("Different color..."), [&] + { + wxColourData colCfg; + colCfg.SetChooseFull(true); + colCfg.SetChooseAlpha(false); + colCfg.SetColour(defaultColors[1].first); //tentative + + if (const ConfigView::Details* cfg = cfggrid::getDataView(*m_gridCfgHistory).getItem(selectedRows[0])) + if (cfg->cfgItem.backColor.IsOk()) + colCfg.SetColour(cfg->cfgItem.backColor); + + int i = 0; + for (const auto& [color, name] : defaultColors) + if (color.IsOk() && i < static_cast(wxColourData::NUM_CUSTOM)) + colCfg.SetCustomColour(i++, color); + + auto fixColorPickerColor = [](const wxColor& col) + { + assert(col.Alpha() == 255); + return col; + }; + wxColourDialog dlg(this, &colCfg); + dlg.Center(); + + dlg.Bind(wxEVT_COLOUR_CHANGED, [&](wxColourDialogEvent& event2) + { + //show preview during color selection (Windows-only atm) + cfggrid::getDataView(*m_gridCfgHistory).setBackColor(cfgFilePaths, fixColorPickerColor(event2.GetColour()), true /*previewOnly*/); + m_gridCfgHistory->Refresh(); + }); + + if (dlg.ShowModal() == wxID_OK) + applyBackColor(fixColorPickerColor(dlg.GetColourData().GetColour())); + else //shut off color preview + { + cfggrid::getDataView(*m_gridCfgHistory).setBackColor(cfgFilePaths, wxNullColour, true /*previewOnly*/); + m_gridCfgHistory->Refresh(); + } + }, bmpColorPicker.ConvertToImage()); + + menu.addSubmenu(_("Background color"), submenu, loadImage("color", dipToScreen(getMenuIconDipSize())), !selectedRows.empty()); + menu.addSeparator(); + //-------------------------------------------------------------------------------------------------------- + + auto showInFileManager = [&] + { + if (!selectedRows.empty()) + if (const ConfigView::Details* cfg = cfggrid::getDataView(*m_gridCfgHistory).getItem(selectedRows[0])) + { + const Zstring cmdLine = replaceCpy(expandMacros(extCommandFileManager.cmdLine), Zstr("%local_path%"), escapeCommandArg(cfg->cfgItem.cfgFilePath)); + try + { + if (const auto& [exitCode, output] = consoleExecute(cmdLine, EXT_APP_MAX_TOTAL_WAIT_TIME_MS); //throw SysError, SysErrorTimeOut + exitCode != 0) + throw SysError(formatSystemError(utfTo(extCommandFileManager.cmdLine), + replaceCpy(_("Exit code %x"), L"%x", numberTo(exitCode)), utfTo(output))); + } + catch (SysErrorTimeOut&) {} //child process not failed yet => probably fine :> + catch (const SysError& e) + { + const std::wstring errorMsg = replaceCpy(_("Command %x failed."), L"%x", fmtPath(cmdLine)) + L"\n\n" + e.toString(); + showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(errorMsg)); + } + return; + } + assert(false); + }; + menu.addItem(translate(extCommandFileManager.description), //translate default external apps on the fly: "Show in Explorer" + showInFileManager, imgFileManagerSmall_, !selectedRows.empty()); + menu.addSeparator(); + //-------------------------------------------------------------------------------------------------------- + const bool renameEnabled = [&] + { + if (!selectedRows.empty()) + if (const ConfigView::Details* cfg = cfggrid::getDataView(*m_gridCfgHistory).getItem(selectedRows[0])) + return !cfg->isLastRunCfg; + return false; + }(); + menu.addItem(_("&Rename") + L"\tF2", [this] { renameSelectedCfgHistoryItem (); }, loadImage("rename", dipToScreen(getMenuIconDipSize())), renameEnabled); + + //-------------------------------------------------------------------------------------------------------- + menu.addItem(_("&Hide") + L"\tDel", [this] { removeSelectedCfgHistoryItems(false /*deleteFromDisk*/); }, wxNullImage, !selectedRows.empty()); + menu.addItem(_("&Delete") + L"\tShift+Del", [this] { removeSelectedCfgHistoryItems(true /*deleteFromDisk*/); }, imgTrashSmall_, !selectedRows.empty()); + //-------------------------------------------------------------------------------------------------------- + menu.popup(*m_gridCfgHistory, event.mousePos_); + //event.Skip(); +} + + +void MainDialog::onCfgGridLabelContext(GridLabelClickEvent& event) +{ + ContextMenu menu; + //-------------------------------------------------------------------------------------------------------- + auto toggleColumn = [&](ColumnType ct) + { + auto colAttr = m_gridCfgHistory->getColumnConfig(); + + Grid::ColAttributes* caName = nullptr; + Grid::ColAttributes* caToggle = nullptr; + + for (Grid::ColAttributes& ca : colAttr) + if (ca.type == static_cast(ColumnTypeCfg::name)) + caName = &ca; + else if (ca.type == ct) + caToggle = &ca; + + assert(caName && caName->stretch > 0 && caName->visible); + assert(caToggle && caToggle->stretch == 0); + + if (caName && caToggle) + { + caToggle->visible = !caToggle->visible; + + //take width of newly visible column from stretched folder name column + caName->offset -= caToggle->visible ? caToggle->offset : -caToggle->offset; + + m_gridCfgHistory->setColumnConfig(colAttr); + } + }; + + if (auto prov = m_gridCfgHistory->getDataProvider()) + for (const Grid::ColAttributes& ca : m_gridCfgHistory->getColumnConfig()) + menu.addCheckBox(prov->getColumnLabel(ca.type), [ct = ca.type, toggleColumn] { toggleColumn(ct); }, + ca.visible, ca.type != static_cast(ColumnTypeCfg::name)); //do not allow user to hide name column! + else assert(false); + //-------------------------------------------------------------------------------------------------------- + menu.addSeparator(); + + auto setDefault = [&] + { + const DpiLayout defaultLayout; + m_gridCfgHistory->setColumnConfig(convertColAttributes(defaultLayout.configColumnAttribs, getCfgGridDefaultColAttribs())); + }; + menu.addItem(_("&Default"), setDefault, loadImage("reset_sicon")); //'&' -> reuse text from "default" buttons elsewhere + //-------------------------------------------------------------------------------------------------------- + menu.addSeparator(); + + auto setCfgHighlight = [&] + { + int cfgGridSyncOverdueDays = cfggrid::getSyncOverdueDays(*m_gridCfgHistory); + + if (showCfgHighlightDlg(this, cfgGridSyncOverdueDays) == ConfirmationButton::accept) + cfggrid::setSyncOverdueDays(*m_gridCfgHistory, cfgGridSyncOverdueDays); + }; + menu.addItem(_("Highlight..."), setCfgHighlight); + //-------------------------------------------------------------------------------------------------------- + + menu.popup(*m_gridCfgHistory, {event.mousePos_.x, m_gridCfgHistory->getColumnLabelHeight()}); + //event.Skip(); +} + + +void MainDialog::onCfgGridLabelLeftClick(GridLabelClickEvent& event) +{ + const auto colType = static_cast(event.colType_); + bool sortAscending = getDefaultSortDirection(colType); + + const auto [sortCol, ascending] = cfggrid::getDataView(*m_gridCfgHistory).getSortDirection(); + if (sortCol == colType) + sortAscending = !ascending; + + cfggrid::getDataView(*m_gridCfgHistory).setSortDirection(colType, sortAscending); + m_gridCfgHistory->Refresh(); + + //re-apply selection: + cfggrid::addAndSelect(*m_gridCfgHistory, activeConfigFiles_, false /*scrollToSelection*/); +} + + +void MainDialog::onCheckRows(CheckRowsEvent& event) +{ + std::vector selectedRows; + + const size_t rowLast = std::min(event.rowLast_, filegrid::getDataView(*m_gridMainC).rowsOnView()); //consider dummy rows + for (size_t i = event.rowFirst_; i < rowLast; ++i) + selectedRows.push_back(i); + + if (!selectedRows.empty()) + { + std::vector objects = filegrid::getDataView(*m_gridMainC).getAllFileRef(selectedRows); + setIncludedManually(objects, event.setActive_); + } +} + + +void MainDialog::onSetSyncDirection(SyncDirectionEvent& event) +{ + std::vector selectedRows; + + const size_t rowLast = std::min(event.rowLast_, filegrid::getDataView(*m_gridMainC).rowsOnView()); //consider dummy rows + for (size_t i = event.rowFirst_; i < rowLast; ++i) + selectedRows.push_back(i); + + if (!selectedRows.empty()) + { + std::vector objects = filegrid::getDataView(*m_gridMainC).getAllFileRef(selectedRows); + setSyncDirManually(objects, event.direction_); + } +} + + +void MainDialog::setLastUsedConfig(const FfsGuiConfig& guiConfig, const std::vector& cfgFilePaths) +{ + activeConfigFiles_ = cfgFilePaths; + lastSavedCfg_ = guiConfig; + + cfggrid::addAndSelect(*m_gridCfgHistory, activeConfigFiles_, true /*scrollToSelection*/); //put file paths on list of last used config files + + //update notes after save + for newly loaded files => BUT: superfluous when loading already known config files! + cfgHistoryUpdateNotes(cfgFilePaths); + + updateUnsavedCfgStatus(); +} + + +void MainDialog::setConfig(const FfsGuiConfig& newGuiCfg, const std::vector& cfgFilePaths) +{ + currentCfg_ = newGuiCfg; + + //(re-)set view filter buttons + setViewFilterDefault(); + + updateGlobalFilterButton(); + + //set first folder pair + firstFolderPair_->setValues(currentCfg_.mainCfg.firstPair); + + setAddFolderPairs(currentCfg_.mainCfg.additionalPairs); + + setGridViewType(currentCfg_.gridViewType); + + //clearGrid(); //+ update GUI! -> already called by setAddFolderPairs() + + setLastUsedConfig(newGuiCfg, cfgFilePaths); +} + + +FfsGuiConfig MainDialog::getConfig() const +{ + FfsGuiConfig guiCfg = currentCfg_; + + //load settings whose ownership lies not in currentCfg: + + //first folder pair + guiCfg.mainCfg.firstPair = firstFolderPair_->getValues(); + + //add additional pairs + guiCfg.mainCfg.additionalPairs.clear(); + + for (const FolderPairPanel* panel : additionalFolderPairs_) + guiCfg.mainCfg.additionalPairs.push_back(panel->getValues()); + + //sync preview + guiCfg.gridViewType = m_bpButtonViewType->isActive() ? GridViewType::action : GridViewType::difference; + + return guiCfg; +} + + +void MainDialog::updateGuiDelayedIf(bool condition) +{ + if (condition) + { + filegrid::refresh(*m_gridMainL, *m_gridMainC, *m_gridMainR); + m_gridMainL->Update(); + m_gridMainC->Update(); + m_gridMainR->Update(); + + //some delay to show the changed GUI before removing rows from sight + std::this_thread::sleep_for(FILE_GRID_POST_UPDATE_DELAY); + } + + updateGui(); +} + + +void MainDialog::showConfigDialog(SyncConfigPanel panelToShow, int localPairIndexToShow) +{ + GlobalPairConfig globalPairCfg; + globalPairCfg.cmpCfg = currentCfg_.mainCfg.cmpCfg; + globalPairCfg.syncCfg = currentCfg_.mainCfg.syncCfg; + globalPairCfg.filter = currentCfg_.mainCfg.globalFilter; + + globalPairCfg.miscCfg.deviceParallelOps = currentCfg_.mainCfg.deviceParallelOps; + globalPairCfg.miscCfg.ignoreErrors = currentCfg_.mainCfg.ignoreErrors; + globalPairCfg.miscCfg.autoRetryCount = currentCfg_.mainCfg.autoRetryCount; + globalPairCfg.miscCfg.autoRetryDelay = currentCfg_.mainCfg.autoRetryDelay; + globalPairCfg.miscCfg.postSyncCommand = currentCfg_.mainCfg.postSyncCommand; + globalPairCfg.miscCfg.postSyncCondition = currentCfg_.mainCfg.postSyncCondition; + globalPairCfg.miscCfg.altLogFolderPathPhrase = currentCfg_.mainCfg.altLogFolderPathPhrase; + globalPairCfg.miscCfg.emailNotifyAddress = currentCfg_.mainCfg.emailNotifyAddress; + globalPairCfg.miscCfg.emailNotifyCondition = currentCfg_.mainCfg.emailNotifyCondition; + globalPairCfg.miscCfg.notes = currentCfg_.notes; + + //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! + const bool showMultipleCfgs = m_bpButtonLocalCompCfg->IsShown(); + //harmonize with MainDialog::updateGuiForFolderPair()! + + assert(showMultipleCfgs || localPairIndexToShow == -1); + assert(m_bpButtonLocalCompCfg->IsShown() == m_bpButtonLocalSyncCfg->IsShown() && + m_bpButtonLocalCompCfg->IsShown() == m_bpButtonLocalFilter ->IsShown()); + + std::vector localCfgs; //showSyncConfigDlg() needs *all* folder pairs for deviceParallelOps update + localCfgs.push_back(firstFolderPair_->getValues()); + + for (const FolderPairPanel* panel : additionalFolderPairs_) + localCfgs.push_back(panel->getValues()); + + //------------------------------------------------------------------------------------ + const GlobalPairConfig globalPairCfgOld = globalPairCfg; + const std::vector localPairCfgOld = localCfgs; + + if (showSyncConfigDlg(this, + panelToShow, + showMultipleCfgs ? localPairIndexToShow : -1, + showMultipleCfgs, + globalPairCfg, + localCfgs, + globalCfg_.defaultFilter, + globalCfg_.versioningFolderHistory, globalCfg_.versioningFolderLastSelected, + globalCfg_.logFolderHistory, globalCfg_.logFolderLastSelected, globalCfg_.logFolderPhrase, + globalCfg_.folderHistoryMax, + globalCfg_.sftpKeyFileLastSelected, + globalCfg_.emailHistory, globalCfg_.emailHistoryMax, + globalCfg_.commandHistory, globalCfg_.commandHistoryMax) == ConfirmationButton::accept) + { + assert(localCfgs.size() == localPairCfgOld.size()); + + currentCfg_.mainCfg.cmpCfg = globalPairCfg.cmpCfg; + currentCfg_.mainCfg.syncCfg = globalPairCfg.syncCfg; + currentCfg_.mainCfg.globalFilter = globalPairCfg.filter; + + currentCfg_.mainCfg.deviceParallelOps = globalPairCfg.miscCfg.deviceParallelOps; + currentCfg_.mainCfg.ignoreErrors = globalPairCfg.miscCfg.ignoreErrors; + currentCfg_.mainCfg.autoRetryCount = globalPairCfg.miscCfg.autoRetryCount; + currentCfg_.mainCfg.autoRetryDelay = globalPairCfg.miscCfg.autoRetryDelay; + currentCfg_.mainCfg.postSyncCommand = globalPairCfg.miscCfg.postSyncCommand; + currentCfg_.mainCfg.postSyncCondition = globalPairCfg.miscCfg.postSyncCondition; + currentCfg_.mainCfg.altLogFolderPathPhrase = globalPairCfg.miscCfg.altLogFolderPathPhrase; + currentCfg_.mainCfg.emailNotifyAddress = globalPairCfg.miscCfg.emailNotifyAddress; + currentCfg_.mainCfg.emailNotifyCondition = globalPairCfg.miscCfg.emailNotifyCondition; + currentCfg_.notes = globalPairCfg.miscCfg.notes; + + firstFolderPair_->setValues(localCfgs[0]); + + for (size_t i = 1; i < localCfgs.size(); ++i) + additionalFolderPairs_[i - 1]->setValues(localCfgs[i]); + + //------------------------------------------------------------------------------------ + + const bool cmpConfigChanged = globalPairCfg.cmpCfg != globalPairCfgOld.cmpCfg || [&] + { + for (size_t i = 0; i < localCfgs.size(); ++i) + if (localCfgs[i].localCmpCfg != localPairCfgOld[i].localCmpCfg) + return true; + return false; + }(); + + //[!] don't redetermine sync directions if only options for deletion handling or versioning are changed!!! + const bool syncDirectionsChanged = globalPairCfg.syncCfg.directionCfg != globalPairCfgOld.syncCfg.directionCfg || [&] + { + for (size_t i = 0; i < localCfgs.size(); ++i) + if (static_cast(localCfgs[i].localSyncCfg) != static_cast(localPairCfgOld[i].localSyncCfg) || + (localCfgs[i].localSyncCfg && localCfgs[i].localSyncCfg->directionCfg != localPairCfgOld[i].localSyncCfg->directionCfg)) + return true; + return false; + }(); + + const bool filterConfigChanged = globalPairCfg.filter != globalPairCfgOld.filter || [&] + { + for (size_t i = 0; i < localCfgs.size(); ++i) + if (localCfgs[i].localFilter != localPairCfgOld[i].localFilter) + return true; + return false; + }(); + + //const bool miscConfigChanged = globalPairCfg.miscCfg.deviceParallelOps != globalPairCfgOld.miscCfg.deviceParallelOps || + // globalPairCfg.miscCfg.ignoreErrors != globalPairCfgOld.miscCfg.ignoreErrors || + // globalPairCfg.miscCfg.autoRetryCount != globalPairCfgOld.miscCfg.autoRetryCount || + // globalPairCfg.miscCfg.autoRetryDelay != globalPairCfgOld.miscCfg.autoRetryDelay || + // globalPairCfg.miscCfg.postSyncCommand != globalPairCfgOld.miscCfg.postSyncCommand || + // globalPairCfg.miscCfg.postSyncCondition != globalPairCfgOld.miscCfg.postSyncCondition || + // globalPairCfg.miscCfg.altLogFolderPathPhrase != globalPairCfgOld.miscCfg.altLogFolderPathPhrase || + // globalPairCfg.miscCfg.emailNotifyAddress != globalPairCfgOld.miscCfg.emailNotifyAddress || + // globalPairCfg.miscCfg.emailNotifyCondition != globalPairCfgOld.miscCfg.emailNotifyCondition; + // globalPairCfg.miscCfg.notes != globalPairCfgOld.miscCfg.notes; + + if (cmpConfigChanged) + applyCompareConfig(globalPairCfg.cmpCfg.compareVar != globalPairCfgOld.cmpCfg.compareVar /*setDefaultViewType*/); + + if (syncDirectionsChanged) + applySyncDirections(); + + if (filterConfigChanged) + { + updateGlobalFilterButton(); //refresh global filter icon + applyFilterConfig(); //re-apply filter + } + } + //else: possible but obscure: default filter changed => impact on "New config" enabled/disabled! + + updateUnsavedCfgStatus(); //also included by updateGui(); +} + + +void MainDialog::onGlobalFilterContext(wxEvent& event) +{ + std::optional filterCfgOnClipboard; + if (std::optional clipTxt = getClipboardText()) + filterCfgOnClipboard = parseFilterBuf(utfTo(*clipTxt)); + + auto cutFilter = [&] + { + setClipboardText(utfTo(serializeFilter(currentCfg_.mainCfg.globalFilter))); + currentCfg_.mainCfg.globalFilter = FilterConfig(); + updateGlobalFilterButton(); + applyFilterConfig(); + }; + + auto copyFilter = [&] { setClipboardText(utfTo(serializeFilter(currentCfg_.mainCfg.globalFilter))); }; + + auto pasteFilter = [&] + { + currentCfg_.mainCfg.globalFilter = *filterCfgOnClipboard; + updateGlobalFilterButton(); + applyFilterConfig(); + }; + + ContextMenu menu; + menu.addItem( _("&Copy"), copyFilter, loadImage("item_copy_sicon"), !isNullFilter(currentCfg_.mainCfg.globalFilter)); + menu.addItem( _("&Paste"), pasteFilter, loadImage("item_paste_sicon"), filterCfgOnClipboard.has_value()); + menu.addSeparator(); + menu.addItem( _("Cu&t"), cutFilter, loadImage("item_cut_sicon"), !isNullFilter(currentCfg_.mainCfg.globalFilter)); + + menu.popup(*m_bpButtonFilterContext, {m_bpButtonFilterContext->GetSize().x, 0}); +} + + +void MainDialog::onToggleViewType(wxCommandEvent& event) +{ + setGridViewType(m_bpButtonViewType->isActive() ? GridViewType::difference : GridViewType::action); +} + + +void MainDialog::onToggleViewButton(wxCommandEvent& event) +{ + if (auto button = dynamic_cast(event.GetEventObject())) + { + button->toggle(); + updateGui(); + + //consistency: toggling view buttons should *always* clear selections, not only implicitly when row count changes: + // + //m_gridMainL->clearSelection(GridEventPolicy::deny); + //m_gridMainC->clearSelection(GridEventPolicy::deny); -> implicitly called by onTreeGridSelection() + //m_gridMainR->clearSelection(GridEventPolicy::deny); + m_gridOverview->clearSelection(GridEventPolicy::allow); + } + else + assert(false); +} + + +void MainDialog::setViewFilterDefault() +{ + auto setButton = [](ToggleButton& tb, bool value) { tb.setActive(value); }; + + const auto& def = globalCfg_.mainDlg.viewFilterDefault; + setButton(*m_bpButtonShowExcluded, def.excluded); + setButton(*m_bpButtonShowEqual, def.equal); + setButton(*m_bpButtonShowConflict, def.conflict); + + setButton(*m_bpButtonShowLeftOnly, def.leftOnly); + setButton(*m_bpButtonShowRightOnly, def.rightOnly); + setButton(*m_bpButtonShowLeftNewer, def.leftNewer); + setButton(*m_bpButtonShowRightNewer, def.rightNewer); + setButton(*m_bpButtonShowDifferent, def.different); + + setButton(*m_bpButtonShowCreateLeft, def.createLeft); + setButton(*m_bpButtonShowCreateRight, def.createRight); + setButton(*m_bpButtonShowUpdateLeft, def.updateLeft); + setButton(*m_bpButtonShowUpdateRight, def.updateRight); + setButton(*m_bpButtonShowDeleteLeft, def.deleteLeft); + setButton(*m_bpButtonShowDeleteRight, def.deleteRight); + setButton(*m_bpButtonShowDoNothing, def.doNothing); +} + + +void MainDialog::onViewTypeContextMouse(wxMouseEvent& event) +{ + ContextMenu menu; + + const GridViewType viewType = m_bpButtonViewType->isActive() ? GridViewType::action : GridViewType::difference; + + menu.addItem(_("Difference") + (viewType != GridViewType::difference ? L"\tF11" : L""), + [&] { setGridViewType(GridViewType::difference); }, greyScaleIfDisabled(loadImage("compare", dipToScreen(getMenuIconDipSize())), viewType == GridViewType::difference)); + + menu.addItem(_("Action") + (viewType != GridViewType::action ? L"\tF11" : L""), + [&] { setGridViewType(GridViewType::action); }, greyScaleIfDisabled(loadImage("start_sync", dipToScreen(getMenuIconDipSize())), viewType == GridViewType::action)); + + menu.popup(*m_bpButtonViewType, {m_bpButtonViewType->GetSize().x, 0}); +} + + +void MainDialog::onViewFilterContext(wxEvent& event) +{ + ContextMenu menu; + + auto saveButtonDefault = [](const ToggleButton& tb, bool& defaultValue) + { + if (tb.IsShown()) + defaultValue = tb.isActive(); + }; + + auto saveDefault = [&] + { + auto& def = globalCfg_.mainDlg.viewFilterDefault; + saveButtonDefault(*m_bpButtonShowExcluded, def.excluded); + saveButtonDefault(*m_bpButtonShowEqual, def.equal); + saveButtonDefault(*m_bpButtonShowConflict, def.conflict); + + saveButtonDefault(*m_bpButtonShowLeftOnly, def.leftOnly); + saveButtonDefault(*m_bpButtonShowRightOnly, def.rightOnly); + saveButtonDefault(*m_bpButtonShowLeftNewer, def.leftNewer); + saveButtonDefault(*m_bpButtonShowRightNewer, def.rightNewer); + saveButtonDefault(*m_bpButtonShowDifferent, def.different); + + saveButtonDefault(*m_bpButtonShowCreateLeft, def.createLeft); + saveButtonDefault(*m_bpButtonShowCreateRight, def.createRight); + saveButtonDefault(*m_bpButtonShowDeleteLeft, def.deleteLeft); + saveButtonDefault(*m_bpButtonShowDeleteRight, def.deleteRight); + saveButtonDefault(*m_bpButtonShowUpdateLeft, def.updateLeft); + saveButtonDefault(*m_bpButtonShowUpdateRight, def.updateRight); + saveButtonDefault(*m_bpButtonShowDoNothing, def.doNothing); + + flashStatusInfo(_("View settings saved")); + }; + + menu.addItem(_("&Save as default"), saveDefault, loadImage("cfg_save", dipToScreen(getMenuIconDipSize()))); + menu.popup(*m_bpButtonViewFilterContext, {m_bpButtonViewFilterContext->GetSize().x, 0}); +} + + +void MainDialog::updateGlobalFilterButton() +{ + //global filter: test for Null-filter + setImage(*m_bpButtonFilter, greyScaleIfDisabled(loadImage("options_filter"), !isNullFilter(currentCfg_.mainCfg.globalFilter))); + + m_bpButtonFilter->SetToolTip(_("Filter") + L" (F7)" + getFilterSummaryForTooltip(currentCfg_.mainCfg.globalFilter)); + //m_bpButtonFilterContext->SetToolTip(m_bpButtonFilter->GetToolTipText()); +} + + +void MainDialog::onCompare(wxCommandEvent& event) +{ + /* mitigate unwanted reentrancy caused by wxApp::Yield(): + disabling GUI elements is NOT enough! e.g. reentrancy when there's a second click event *already* in the Windows message queue + + CAVEAT: This doesn't block all theoretically possible Window events that were queued *before* disableGuiElementsImpl() takes effect, + but at least the 90% case of (rare!) crashes caused by a duplicate click event on comparison or sync button. */ + if (std::exchange(operationInProgress_, true)) + return; + ZEN_ON_SCOPE_EXIT(operationInProgress_ = false); + + //wxBusyCursor dummy; -> redundant: progress already shown in progress dialog! + + FocusPreserver fp; //e.g. keep focus on config panel after pressing F5 + + //give nice hint on what's next to do if user manually clicked on compare + assert(m_buttonCompare->GetId() != wxID_ANY); + if (fp.getFocusId() == m_buttonCompare->GetId()) + fp.setFocus(m_buttonSync); + + int scrollPosX = 0; + int scrollPosY = 0; + m_gridMainL->GetViewStart(&scrollPosX, &scrollPosY); //preserve current scroll position + ZEN_ON_SCOPE_EXIT(m_gridMainL->Scroll(scrollPosX, scrollPosY); // + m_gridMainR->Scroll(scrollPosX, scrollPosY); //restore + m_gridMainC->Scroll(-1, scrollPosY); ); // + + clearGrid(); //avoid memory peak by clearing old data first + + const auto& guiCfg = getConfig(); + + const std::vector& fpCfgList = extractCompareCfg(guiCfg.mainCfg); + + UiInputDisabler uiBlock(*this, true /*enableAbort*/); //StatusHandlerTemporaryPanel calls wxApp::Yield(), so avoid unexpected callbacks! + + //handle status display and error messages + StatusHandlerTemporaryPanel statusHandler(*this, std::chrono::system_clock::now(), + guiCfg.mainCfg.ignoreErrors, + guiCfg.mainCfg.autoRetryCount, + guiCfg.mainCfg.autoRetryDelay, + globalCfg_.soundFileAlertPending); + + auto requestPassword = [&, password = Zstring()](const std::wstring& msg, const std::wstring& lastErrorMsg) mutable //throw CancelProcess + { + assert(runningOnMainThread()); + if (showPasswordPrompt(this, msg, lastErrorMsg, password) != ConfirmationButton::accept) + statusHandler.cancelProcessNow(CancelReason::user); //throw CancelProcess + + return password; + }; + try + { + //GUI mode: place directory locks on directories isolated(!) during both comparison and synchronization + + std::unique_ptr dirLocks; + folderCmp_ = compare(globalCfg_.warnDlgs, + globalCfg_.fileTimeTolerance, + requestPassword, + globalCfg_.runWithBackgroundPriority, + globalCfg_.createLockFile, + dirLocks, + fpCfgList, + statusHandler); //throw CancelProcess + } + catch (CancelProcess&) {} + + const StatusHandlerTemporaryPanel::Result r = statusHandler.prepareResult(); //noexcept + //--------------------------------------------------------------------------- + setLastOperationLog(r.summary, r.errorLog.ptr()); + + fullSyncLog_ = {r.errorLog.ref(), r.summary.startTime, r.summary.totalTime}; + + if (r.summary.result == TaskResult::cancelled) + return updateGui(); //refresh grid in ANY case! (also on abort) + + + filegrid::setData(*m_gridMainC, folderCmp_); // + treegrid::setData(*m_gridOverview, folderCmp_); //update view on data + updateGui(); // + + //play (optional) sound notification + if (!globalCfg_.soundFileCompareFinished.empty()) + { + //wxWidgets shows modal error dialog by default => "no, wxWidgets, NO!" + wxLog* oldLogTarget = wxLog::SetActiveTarget(new wxLogStderr); //transfer and receive ownership! + ZEN_ON_SCOPE_EXIT(delete wxLog::SetActiveTarget(oldLogTarget)); + + wxSound::Play(utfTo(globalCfg_.soundFileCompareFinished), wxSOUND_ASYNC); + } + + if (!IsActive()) + RequestUserAttention(); //this == toplevel win, so we also get the taskbar flash! + + //remember folder history (except when cancelled by user) + for (const FolderPairCfg& fpCfg : fpCfgList) + { + folderHistoryLeft_ ->addItem(fpCfg.folderPathPhraseLeft_); + folderHistoryRight_->addItem(fpCfg.folderPathPhraseRight_); + } + + //mark selected cfg files as "in sync" when there is nothing to do: https://freefilesync.org/forum/viewtopic.php?t=4991 + if (r.summary.result == TaskResult::success) + if (getCUD(SyncStatistics(folderCmp_)) == 0) + { + setStatusInfo(_("No files to synchronize"), true /*highlight*/); //user might be AFK: don't flashStatusInfo() + //overwrites status info already set in updateGui() above + + cfggrid::getDataView(*m_gridCfgHistory).setLastInSyncTime(activeConfigFiles_, std::chrono::system_clock::to_time_t(r.summary.startTime)); + //re-apply selection: sort order changed if sorted by last sync time, or log + cfggrid::addAndSelect(*m_gridCfgHistory, activeConfigFiles_, false /*scrollToSelection*/); + //m_gridCfgHistory->Refresh(); <- implicit in last call + } + + //reset icon cache (IconBuffer) after *each* comparison! + filegrid::setupIcons(*m_gridMainL, *m_gridMainC, *m_gridMainR, globalCfg_.mainDlg.showIcons, convert(globalCfg_.mainDlg.iconSize)); +} + + +void MainDialog::updateGui() +{ + updateGridViewData(); //update gridDataView and write status information + + const SyncStatistics st(folderCmp_); + updateStatistics(st); + + updateUnsavedCfgStatus(); + + const auto& mainCfg = getConfig().mainCfg; + const std::optional cmpVar = getCommonCompVariant(mainCfg); + const std::optional syncVar = getCommonSyncVariant(mainCfg); + + const char* cmpVarIconName = nullptr; + if (cmpVar) + switch (*cmpVar) + { + case CompareVariant::timeSize: cmpVarIconName = "cmp_time"; break; + case CompareVariant::content: cmpVarIconName = "cmp_content"; break; + case CompareVariant::size: cmpVarIconName = "cmp_size"; break; + } + const char* syncVarIconName = nullptr; + if (syncVar) + switch (*syncVar) + { + case SyncVariant::twoWay: syncVarIconName = "sync_twoway"; break; + case SyncVariant::mirror: syncVarIconName = "sync_mirror"; break; + case SyncVariant::update: syncVarIconName = "sync_update"; break; + case SyncVariant::custom: syncVarIconName = "sync_custom"; break; + } + + const bool useDbFile = [&] + { + for (const FolderPairCfg& fpCfg : extractCompareCfg(mainCfg)) + if (std::get_if(&fpCfg.directionCfg.dirs)) + return true; + return false; + }(); + + updateTopButton(*m_buttonCompare, loadImage("compare"), + getVariantName(cmpVar), cmpVarIconName, + nullptr /*extraIconName*/, + folderCmp_.empty() ? getColorHighlightCompareButton() : wxNullColour); + + updateTopButton(*m_buttonSync, loadImage("start_sync"), + getVariantName(syncVar), syncVarIconName, + useDbFile ? "database" : nullptr, + getCUD(st) != 0 ? getColorHighlightSyncButton() : wxNullColour); + + m_panelTopButtons->Layout(); + + m_menuItemExportList->Enable(!folderCmp_.empty()); //empty CSV confuses users: https://freefilesync.org/forum/viewtopic.php?t=4787 + + //auiMgr_.Update(); -> doesn't seem to be needed +} + + +void MainDialog::clearGrid(ptrdiff_t pos) +{ + if (!folderCmp_.empty()) + { + assert(pos < makeSigned(folderCmp_.size())); + if (pos < 0) + folderCmp_.clear(); + else + folderCmp_.erase(folderCmp_.begin() + pos); + } + + if (folderCmp_.empty()) + fullSyncLog_.reset(); + + filegrid::setData(*m_gridMainC, folderCmp_); + treegrid::setData(*m_gridOverview, folderCmp_); + updateGui(); +} + + +void MainDialog::updateStatistics(const SyncStatistics& st) +{ + auto setValue = [](wxStaticText& txtControl, bool isZeroValue, const wxString& valueAsString, wxStaticBitmap& bmpControl, const char* imageName) + { + if (txtControl.GetLabel() != valueAsString) + { + wxFont fnt = txtControl.GetFont(); + fnt.SetWeight(isZeroValue ? wxFONTWEIGHT_NORMAL : wxFONTWEIGHT_BOLD); + txtControl.SetFont(fnt); + + txtControl.SetLabelText(valueAsString); + setImage(bmpControl, greyScaleIfDisabled(mirrorIfRtl(loadImage(imageName)), !isZeroValue)); + } + }; + + auto setIntValue = [&setValue](wxStaticText& txtControl, int value, wxStaticBitmap& bmpControl, const char* imageName) + { + setValue(txtControl, value == 0, formatNumber(value), bmpControl, imageName); + }; + + //update preview of item count and bytes to be transferred: + setValue(*m_staticTextData, st.getBytesToProcess() == 0, formatFilesizeShort(st.getBytesToProcess()), *m_bitmapData, "data"); + setIntValue(*m_staticTextCreateLeft, st.createCount(), *m_bitmapCreateLeft, "so_create_left_sicon"); + setIntValue(*m_staticTextUpdateLeft, st.updateCount(), *m_bitmapUpdateLeft, "so_update_left_sicon"); + setIntValue(*m_staticTextDeleteLeft, st.deleteCount(), *m_bitmapDeleteLeft, "so_delete_left_sicon"); + setIntValue(*m_staticTextCreateRight, st.createCount(), *m_bitmapCreateRight, "so_create_right_sicon"); + setIntValue(*m_staticTextUpdateRight, st.updateCount(), *m_bitmapUpdateRight, "so_update_right_sicon"); + setIntValue(*m_staticTextDeleteRight, st.deleteCount(), *m_bitmapDeleteRight, "so_delete_right_sicon"); + + m_panelViewFilter->Layout(); //[!] statistics panel size changed, so this is needed + m_panelStatistics->Layout(); + m_panelStatistics->Refresh(); //fix small mess up on RTL layout +} + + +void MainDialog::applyCompareConfig(bool setDefaultViewType) +{ + clearGrid(); //+ GUI update + + //convenience: change sync view + if (setDefaultViewType) + switch (currentCfg_.mainCfg.cmpCfg.compareVar) + { + case CompareVariant::timeSize: + case CompareVariant::size: + setGridViewType(GridViewType::action); + break; + + case CompareVariant::content: + setGridViewType(GridViewType::difference); + break; + } +} + + +void MainDialog::onStartSync(wxCommandEvent& event) +{ + FocusPreserver fp; //e.g. keep focus on config panel after pressing F9 + + if (folderCmp_.empty()) + { + //quick sync: simulate button click on "compare" + wxCommandEvent dummy(wxEVT_COMMAND_BUTTON_CLICKED); + m_buttonCompare->Command(dummy); //simulate click + + if (folderCmp_.empty()) //check if user aborted or error occurred, etc... + return; + } + + if (std::exchange(operationInProgress_, true)) //*after* simluated comparison button click! + return; + ZEN_ON_SCOPE_EXIT(operationInProgress_ = false); + //------------------------------------------------------------------ + + const auto& guiCfg = getConfig(); + + //show sync preview/confirmation dialog + if (globalCfg_.confirmDlgs.confirmSyncStart) + { + bool dontShowAgain = false; + + if (showSyncConfirmationDlg(this, false /*syncSelection*/, + getCommonSyncVariant(guiCfg.mainCfg), + SyncStatistics(folderCmp_), + dontShowAgain) != ConfirmationButton::accept) + return; + globalCfg_.confirmDlgs.confirmSyncStart = !dontShowAgain; + } + + const std::chrono::system_clock::time_point syncStartTime = std::chrono::system_clock::now(); + + const WindowLayout::Dimensions progressDim + { + globalCfg_.dpiLayouts[getDpiScalePercent()].progressDlg.size, + std::nullopt /*pos*/, + globalCfg_.dpiLayouts[getDpiScalePercent()].progressDlg.isMaximized + }; + + UiInputDisabler uiBlock(*this, false /*enableAbort*/); //StatusHandlerFloatingDialog will internally process Window messages, so avoid unexpected callbacks! + + //class handling status updates and error messages + StatusHandlerFloatingDialog statusHandler(this, getJobNames(), syncStartTime, + guiCfg.mainCfg.ignoreErrors, + guiCfg.mainCfg.autoRetryCount, + guiCfg.mainCfg.autoRetryDelay, + globalCfg_.soundFileSyncFinished, + globalCfg_.soundFileAlertPending, + progressDim, + globalCfg_.progressDlgAutoClose); + try + { + //PERF_START; + + //let's report here rather than before comparison (user might have changed global settings in the meantime!) + logNonDefaultSettings(globalCfg_, statusHandler); //throw CancelProcess + + //wxBusyCursor dummy; -> redundant: progress already shown in progress dialog! + + //GUI mode: end directory lock lifetime after comparion and start new locking right before sync + std::unique_ptr dirLocks; + if (globalCfg_.createLockFile) + { + std::set folderPathsToLock; + for (const BaseFolderPair& baseFolder : asRange(folderCmp_)) + { + if (baseFolder.getFolderStatus() == BaseFolderStatus::existing) //do NOT check directory existence again! + if (const Zstring& nativePath = getNativeItemPath(baseFolder.getAbstractPath()); //restrict directory locking to native paths until further + !nativePath.empty()) + folderPathsToLock.insert(nativePath); + + if (baseFolder.getFolderStatus() == BaseFolderStatus::existing) + if (const Zstring& nativePath = getNativeItemPath(baseFolder.getAbstractPath()); + !nativePath.empty()) + folderPathsToLock.insert(nativePath); + } + dirLocks = std::make_unique(folderPathsToLock, globalCfg_.warnDlgs.warnDirectoryLockFailed, statusHandler); //throw CancelProcess + } + + synchronize(syncStartTime, + globalCfg_.verifyFileCopy, + globalCfg_.copyLockedFiles, + globalCfg_.copyFilePermissions, + globalCfg_.failSafeFileCopy, + globalCfg_.runWithBackgroundPriority, + extractSyncCfg(guiCfg.mainCfg), + folderCmp_, + globalCfg_.warnDlgs, + statusHandler); //throw CancelProcess + } + catch (CancelProcess&) { assert(statusHandler.taskCancelled() == CancelReason::user); } + + //------------------------------------------------------------------- + StatusHandlerFloatingDialog::Result r = statusHandler.prepareResult(); + + //merge logs of comparison, manual operations, sync + append(fullSyncLog_->log, r.errorLog.ref()); + fullSyncLog_->totalTime += r.summary.totalTime; + + + //"consume" fullSyncLog_, but don't reset: there may be items remaining for manual operations or re-sync! + ProcessSummary fullSummary = r.summary; + fullSummary.startTime = std::exchange(fullSyncLog_->startTime, std::chrono::system_clock::now()); + fullSummary.totalTime = std::exchange(fullSyncLog_->totalTime, {}); + //let's *not* redetermine "ProcessSummary::result", even if errors occured during manual operations! + + ErrorLog fullLog = std::exchange(fullSyncLog_->log, {}); + + auto logMsg2 =[&](const std::wstring& msg, MessageType type) + { + logMsg(fullLog, msg, type); + logMsg(r.errorLog.ref(), msg, type); + }; + + AbstractPath logFolderPath = createAbstractPath(guiCfg.mainCfg.altLogFolderPathPhrase); //optional + if (AFS::isNullPath(logFolderPath)) + logFolderPath = createAbstractPath(globalCfg_.logFolderPhrase); + assert(!AFS::isNullPath(logFolderPath)); //mandatory! but still: let's include fall back + if (AFS::isNullPath(logFolderPath)) + logFolderPath = createAbstractPath(getLogFolderDefaultPath()); + + AbstractPath logFilePath = AFS::appendRelPath(logFolderPath, generateLogFileName(globalCfg_.logFormat, fullSummary)); + //e.g. %AppData%\FreeFileSync\Logs\Backup FreeFileSync 2013-09-15 015052.123 [Error].log + + auto notifyStatusNoThrow = [&](std::wstring&& msg) { try { statusHandler.updateStatus(std::move(msg)); /*throw CancelProcess*/ } catch (CancelProcess&) {} }; + + + if (statusHandler.taskCancelled()) + /* user cancelled => don't run post sync command + => don't run post sync action + => don't send email notification + => don't play sound notification + (=> DO save log file: sync attempt is more than just a "manual operation") + (=> DO update last sync stats for the selected cfg files) */ + assert(statusHandler.taskCancelled() == CancelReason::user); //"stop on first error" is only for ffs_batch + else + { + //--------------------- post sync command ---------------------- + if (const Zstring cmdLine = trimCpy(expandMacros(guiCfg.mainCfg.postSyncCommand)); + !cmdLine.empty()) + if (guiCfg.mainCfg.postSyncCondition == PostSyncCondition::completion || + (guiCfg.mainCfg.postSyncCondition == PostSyncCondition::errors) == (r.summary.result == TaskResult::cancelled || + r.summary.result == TaskResult::error)) + try + { + //give consoleExecute() some "time to fail", but not too long to hang our process + const int DEFAULT_APP_TIMEOUT_MS = 100; + + if (const auto& [exitCode, output] = consoleExecute(cmdLine, DEFAULT_APP_TIMEOUT_MS); //throw SysError, SysErrorTimeOut + exitCode != 0) + throw SysError(formatSystemError("", replaceCpy(_("Exit code %x"), L"%x", numberTo(exitCode)), utfTo(output))); + + logMsg2(_("Executing command:") + L' ' + utfTo(cmdLine) + L" [" + replaceCpy(_("Exit code %x"), L"%x", L"0") + L']', MSG_TYPE_INFO); + } + catch (SysErrorTimeOut&) //child process not failed yet => probably fine :> + { + logMsg2(_("Executing command:") + L' ' + utfTo(cmdLine), MSG_TYPE_INFO); + } + catch (const SysError& e) + { + logMsg2(replaceCpy(_("Command %x failed."), L"%x", fmtPath(cmdLine)) + L"\n\n" + e.toString(), MSG_TYPE_ERROR); + } + + //--------------------- email notification ---------------------- + if (const std::string notifyEmail = trimCpy(guiCfg.mainCfg.emailNotifyAddress); + !notifyEmail.empty()) + if (guiCfg.mainCfg.emailNotifyCondition == ResultsNotification::always || + (guiCfg.mainCfg.emailNotifyCondition == ResultsNotification::errorWarning && (fullSummary.result == TaskResult::cancelled || + fullSummary.result == TaskResult::error || + fullSummary.result == TaskResult::warning)) || + (guiCfg.mainCfg.emailNotifyCondition == ResultsNotification::errorOnly && (fullSummary.result == TaskResult::cancelled || + fullSummary.result == TaskResult::error))) + try + { + logMsg2(replaceCpy(_("Sending email notification to %x"), L"%x", utfTo(notifyEmail)), MSG_TYPE_INFO); + sendLogAsEmail(notifyEmail, fullSummary, fullLog, logFilePath, notifyStatusNoThrow); //throw FileError + } + catch (const FileError& e) { logMsg2(e.toString(), MSG_TYPE_ERROR); } + } + + //--------------------- save log file ---------------------- + std::set logsToKeepPaths; + { + const std::set activeCfgSorted(activeConfigFiles_.begin(), activeConfigFiles_.end()); + + for (const ConfigFileItem& cfi : cfggrid::getDataView(*m_gridCfgHistory).get()) + if (!activeCfgSorted.contains(cfi.cfgFilePath)) //exception: don't keep old logs for the selected cfg files! + logsToKeepPaths.insert(cfi.lastRunStats.logFilePath); + } + 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! + saveLogFile(logFilePath, fullSummary, fullLog, globalCfg_.logfilesMaxAgeDays, globalCfg_.logFormat, logsToKeepPaths, notifyStatusNoThrow); //throw FileError + } + catch (const FileError& e) + { + try //fallback: log file *must* be saved no matter what! + { + const AbstractPath logFileDefaultPath = AFS::appendRelPath(createAbstractPath(getLogFolderDefaultPath()), generateLogFileName(globalCfg_.logFormat, fullSummary)); + if (logFilePath == logFileDefaultPath) + throw; + + logMsg2(e.toString(), MSG_TYPE_ERROR); + + logFilePath = logFileDefaultPath; + saveLogFile(logFileDefaultPath, fullSummary, fullLog, globalCfg_.logfilesMaxAgeDays, globalCfg_.logFormat, logsToKeepPaths, notifyStatusNoThrow); //throw FileError + } + catch (const FileError& e2) { logMsg2(e2.toString(), MSG_TYPE_ERROR); logExtraError(e2.toString()); } //should never happen!!! + } + + //--------- update last sync stats for the selected cfg files --------- + const ErrorLogStats& fullLogStats = getStats(fullLog); + + cfggrid::getDataView(*m_gridCfgHistory).setLastRunStats(activeConfigFiles_, + { + std::chrono::system_clock::to_time_t(fullSummary.startTime), + logFilePath, + fullSummary.result, + fullSummary.statsProcessed.items, + fullSummary.statsProcessed.bytes, + fullSummary.totalTime, + fullLogStats.errors, + fullLogStats.warnings, + }); + //re-apply selection: sort order changed if sorted by last sync time + cfggrid::addAndSelect(*m_gridCfgHistory, activeConfigFiles_, false /*scrollToSelection*/); + //m_gridCfgHistory->Refresh(); <- implicit in last call + + //--------------------------------------------------------------------------- + setLastOperationLog(r.summary, r.errorLog.ptr()); + + //remove empty rows: just a beautification, invalid rows shouldn't cause issues + filegrid::getDataView(*m_gridMainC).removeInvalidRows(); + + //--------------------------------------------------------------------------- + const StatusHandlerFloatingDialog::DlgOptions dlgOpt = statusHandler.showResult(); + + globalCfg_.progressDlgAutoClose = dlgOpt.autoCloseSelected; + globalCfg_.dpiLayouts[getDpiScalePercent()].progressDlg.size = dlgOpt.dim.size; //=> ignore dim.pos + globalCfg_.dpiLayouts[getDpiScalePercent()].progressDlg.isMaximized = dlgOpt.dim.isMaximized; + + updateGui(); //let's update *after* showResult(): some users are interested in seeing the old statistics dialog even after sync + + //--------------------------------------------------------------------------- + //run shutdown *after* last sync stats were updated! they will be saved via onBeforeSystemShutdownCookie_: https://freefilesync.org/forum/viewtopic.php?t=5761 + using FinalRequest = StatusHandlerFloatingDialog::FinalRequest; + switch (dlgOpt.finalRequest) + { + case FinalRequest::none: + break; + + case FinalRequest::exit: + //don't Close() which prompts to save current config in onClose() + Destroy(); //for top-level windows this employs delayed destruction (wxPendingDelete) + uiBlock.dismiss(); //...or else: crash when ~UiInputDisabler() calls Yield() + enableGuiElementsImpl()! + fp .dismiss(); + break; + + case FinalRequest::shutdown: + try + { + shutdownSystem(); //throw FileError + terminateProcess(static_cast(FfsExitCode::success)); + //no point in continuing and saving cfg again in ~MainDialog()/onBeforeSystemShutdown() while the OS will kill us any time! + } + catch (const FileError& e) { showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); } + //[!] ignores current error handling setting, BUT this is not a sync error! + break; + } +} + + +namespace +{ +void appendInactive(ContainerObject& conObj, std::vector& inactiveItems) +{ + for (FilePair& file : conObj.files()) + if (!file.isActive()) + inactiveItems.push_back(&file); + for (SymlinkPair& symlink : conObj.symlinks()) + if (!symlink.isActive()) + inactiveItems.push_back(&symlink); + for (FolderPair& folder : conObj.subfolders()) + { + if (!folder.isActive()) + inactiveItems.push_back(&folder); + appendInactive(folder, inactiveItems); //recurse + } +} +} + + +void MainDialog::startSyncForSelecction(const std::vector& selection) +{ + if (std::exchange(operationInProgress_, true)) + return; + ZEN_ON_SCOPE_EXIT(operationInProgress_ = false); + + //------------------ analyze selection ------------------ + std::unordered_set basePairsSelect; + std::vector selectedActive; + + for (FileSystemObject* fsObj : expandSelectionForPartialSync(selection)) + { + switch (fsObj->getSyncOperation()) + { + case SO_CREATE_LEFT: + case SO_CREATE_RIGHT: + case SO_DELETE_LEFT: + case SO_DELETE_RIGHT: + case SO_MOVE_LEFT_FROM: + case SO_MOVE_LEFT_TO: + case SO_MOVE_RIGHT_FROM: + case SO_MOVE_RIGHT_TO: + case SO_OVERWRITE_LEFT: + case SO_OVERWRITE_RIGHT: + case SO_RENAME_LEFT: + case SO_RENAME_RIGHT: + basePairsSelect.insert(&fsObj->base()); + break; + + case SO_UNRESOLVED_CONFLICT: + case SO_DO_NOTHING: + case SO_EQUAL: + break; + } + if (fsObj->isActive()) + selectedActive.push_back(fsObj); + } + + if (basePairsSelect.empty()) + return; //harmonize with onGridContextRim(): this function should be a no-op iff context menu option is disabled! + + FocusPreserver fp; + { + //--------------------------------------------------------------- + //simulate partial sync by temporarily excluding all other items: + std::vector inactiveItems; //remember inactive (assuming a smaller number than active items) + for (BaseFolderPair& baseFolder : asRange(folderCmp_)) + appendInactive(baseFolder, inactiveItems); + + setActiveStatus(false, folderCmp_); //limit to folderCmpSelect? => no, let's also activate non-participating folder pairs, if only to visually match user selection + + for (FileSystemObject* fsObj : selectedActive) + fsObj->setActive(true); + + //don't run a full updateGui() (which would remove excluded rows) since we're only temporarily excluding: + filegrid::refresh(*m_gridMainL, *m_gridMainC, *m_gridMainR); + m_gridOverview->Refresh(); + + ZEN_ON_SCOPE_EXIT( + setActiveStatus(true, folderCmp_); + + //inactive items are expected to still exist after sync! => no need for FileSystemObject::ObjectId + for (FileSystemObject* fsObj : inactiveItems) + fsObj->setActive(false); + + filegrid::refresh(*m_gridMainL, *m_gridMainC, *m_gridMainR); //e.g. if user cancels confirmation popup + m_gridOverview->Refresh(); + ); + //--------------------------------------------------------------- + const auto& guiCfg = getConfig(); + const std::vector fpCfg = extractSyncCfg(guiCfg.mainCfg); + + //only apply partial sync to base pairs that contain at least one item to sync (e.g. avoid needless sync.ffs_db updates) + std::vector> folderCmpSelect; + std::vector fpCfgSelect; + + for (size_t i = 0; i < folderCmp_.size(); ++i) + if (basePairsSelect.contains(&folderCmp_[i].ref())) + { + folderCmpSelect.push_back(folderCmp_[i]); + fpCfgSelect .push_back( fpCfg[i]); + } + + //show sync preview/confirmation dialog + if (globalCfg_.confirmDlgs.confirmSyncStart) + { + bool dontShowAgain = false; + + if (showSyncConfirmationDlg(this, + true /*syncSelection*/, + getCommonSyncVariant(guiCfg.mainCfg), + SyncStatistics(folderCmpSelect), + dontShowAgain) != ConfirmationButton::accept) + return; + globalCfg_.confirmDlgs.confirmSyncStart = !dontShowAgain; + } + + const std::chrono::system_clock::time_point syncStartTime = std::chrono::system_clock::now(); + + //last sync log file? => let's go without; same behavior as manual deletion + + UiInputDisabler uiBlock(*this, true /*enableAbort*/); //StatusHandlerTemporaryPanel calls wxApp::Yield(), so avoid unexpected callbacks! + + StatusHandlerTemporaryPanel statusHandler(*this, syncStartTime, + guiCfg.mainCfg.ignoreErrors, + guiCfg.mainCfg.autoRetryCount, + guiCfg.mainCfg.autoRetryDelay, + globalCfg_.soundFileAlertPending); + try + { + //let's report here rather than before comparison (user might have changed global settings in the meantime!) + logNonDefaultSettings(globalCfg_, statusHandler); //throw CancelProcess + + //LockHolder? => let's go without; same behavior as manual deletion + + synchronize(syncStartTime, + globalCfg_.verifyFileCopy, + globalCfg_.copyLockedFiles, + globalCfg_.copyFilePermissions, + globalCfg_.failSafeFileCopy, + globalCfg_.runWithBackgroundPriority, + fpCfgSelect, + folderCmpSelect, + globalCfg_.warnDlgs, + statusHandler); //throw CancelProcess + } + catch (CancelProcess&) {} + + const StatusHandlerTemporaryPanel::Result r = statusHandler.prepareResult(); //noexcept + + setLastOperationLog(r.summary, r.errorLog.ptr()); + + append(fullSyncLog_->log, r.errorLog.ref()); + fullSyncLog_->totalTime += r.summary.totalTime; + + } //run updateGui() *after* reverting our temporary exclusions + + //remove empty rows: just a beautification, invalid rows shouldn't cause issues + filegrid::getDataView(*m_gridMainC).removeInvalidRows(); + + updateGui(); +} + + +void MainDialog::setLastOperationLog(const ProcessSummary& summary, const std::shared_ptr& errorLog) +{ + const wxImage syncResultImage = [&] + { + switch (summary.result) + { + case TaskResult::success: + return loadImage("result_success"); + case TaskResult::warning: + return loadImage("result_warning"); + case TaskResult::error: + case TaskResult::cancelled: + return loadImage("result_error"); + } + assert(false); + return wxNullImage; + }(); + + const wxImage logOverlayImage = [&] + { + //don't use "syncResult": There may be errors after sync, e.g. failure to save log file/send email! + if (errorLog) + { + const ErrorLogStats logCount = getStats(*errorLog); + if (logCount.errors > 0) + return loadImage("msg_error", dipToScreen(getMenuIconDipSize())); + if (logCount.warnings > 0) + return loadImage("msg_warning", dipToScreen(getMenuIconDipSize())); + + //return loadImage("msg_success", dipToScreen(getMenuIconDipSize())); -> too noisy? + } + return wxNullImage; + }(); + + setImage(*m_bitmapSyncResult, syncResultImage); + m_staticTextSyncResult->SetLabelText(getSyncResultLabel(summary.result)); + + + m_staticTextItemsProcessed->SetLabelText(formatNumber(summary.statsProcessed.items)); + m_staticTextBytesProcessed->SetLabelText(L'(' + formatFilesizeShort(summary.statsProcessed.bytes) + L')'); + + const bool hideRemainingStats = (summary.statsTotal.items < 0 && summary.statsTotal.bytes < 0) || //no total items/bytes: e.g. for pure folder comparison + summary.statsProcessed == summary.statsTotal; //...if everything was processed successfully + + m_staticTextProcessed ->Show(!hideRemainingStats); + m_staticTextRemaining ->Show(!hideRemainingStats); + m_staticTextItemsRemaining->Show(!hideRemainingStats); + m_staticTextBytesRemaining->Show(!hideRemainingStats); + + if (!hideRemainingStats) + { + m_staticTextItemsRemaining->SetLabelText( formatNumber(summary.statsTotal.items - summary.statsProcessed.items)); + m_staticTextBytesRemaining->SetLabelText(L'(' + formatFilesizeShort(summary.statsTotal.bytes - summary.statsProcessed.bytes) + L')'); + } + + const int64_t totalTimeSec = std::chrono::duration_cast(summary.totalTime).count(); + m_staticTextTimeElapsed->SetLabelText(utfTo(formatTimeSpan(totalTimeSec, true /*hourRequired*/))); + //include "hour" => let's use full precision for max. clarity: https://freefilesync.org/forum/viewtopic.php?t=6308 + + logPanel_->setLog(errorLog); + + m_panelLog->Layout(); + //m_panelItemStats->Dimensions(); //needed? + //m_panelTimeStats->Dimensions(); // + + const wxImage& logBtnImg = layOver(loadImage("log_file"), logOverlayImage, wxALIGN_BOTTOM | wxALIGN_RIGHT); + m_bpButtonToggleLog->init(layOver(generatePressedButtonBack(logBtnImg.GetSize() + wxSize(dipToScreen(10), dipToScreen(10))), logBtnImg), logBtnImg); + + const int logBtnSize = m_bpButtonViewType->GetSize().GetHeight(); + m_bpButtonToggleLog->SetMinSize({logBtnSize, logBtnSize}); + + m_bpButtonToggleLog->Show(static_cast(errorLog)); +} + + +void MainDialog::onToggleLog(wxCommandEvent& event) +{ + showLogPanel(!m_bpButtonToggleLog->isActive()); +} + + +void MainDialog::showLogPanel(bool show) +{ + m_bpButtonToggleLog->setActive(show); + + if (wxAuiPaneInfo& logPane = auiMgr_.GetPane(m_panelLog); + logPane.IsShown() != show) + { + if (!show) + { + if (logPane.IsMaximized()) + auiMgr_.RestorePane(logPane); //!= wxAuiPaneInfo::Restore() which does not un-hide other panels (WTF!?) + else //ensure current window sizes will be used when pane is shown again: + logPane.best_size = logPane.rect.GetSize(); + } + + logPane.Show(show); + auiMgr_.Update(); + m_panelLog->Refresh(); //macOS: fix background corruption for the statistics boxes; call *after* wxAuiManager::Update() + } + + if (show) + { + if (wxWindow* focus = wxWindow::FindFocus()) //restore when closing panel! + if (!isComponentOf(focus, m_panelLog)) + focusAfterCloseLog_ = focus->GetId(); + + logPanel_->SetFocus(); + } + else + { + if (isComponentOf(wxWindow::FindFocus(), m_panelLog)) + if (wxWindow* oldFocusWin = wxWindow::FindWindowById(focusAfterCloseLog_)) + oldFocusWin->SetFocus(); + focusAfterCloseLog_ = wxID_ANY; + } +} + + +void MainDialog::onGridDoubleClickRim(GridClickEvent& event, bool leftSide) +{ + if (!globalCfg_.externalApps.empty()) + { + std::vector selectionL; + std::vector selectionR; + if (FileSystemObject* fsObj = filegrid::getDataView(*m_gridMainC).getFsObject(event.row_)) //selection must be a list of BOUND pointers! + (leftSide ? selectionL: selectionR) = {fsObj}; + + openExternalApplication(globalCfg_.externalApps[0].cmdLine, leftSide, selectionL, selectionR); + } +} + + +void MainDialog::onGridLabelLeftClickRim(GridLabelClickEvent& event, bool leftSide) +{ + const ColumnTypeRim colType = static_cast(event.colType_); + + bool sortAscending = getDefaultSortDirection(colType); + + if (auto sortInfo = filegrid::getDataView(*m_gridMainC).getSortConfig()) + if (const ColumnTypeRim* sortType = std::get_if(&sortInfo->sortCol)) + if (*sortType == colType && sortInfo->onLeft == leftSide) + sortAscending = !sortInfo->ascending; + + const ItemPathFormat itemPathFormat = leftSide ? globalCfg_.mainDlg.itemPathFormatLeftGrid : globalCfg_.mainDlg.itemPathFormatRightGrid; + + filegrid::getDataView(*m_gridMainC).sortView(colType, itemPathFormat, leftSide, sortAscending); + updateGui(); //refresh gridDataView + + m_gridMainL->clearSelection(GridEventPolicy::deny); //call *after* updateGui/updateGridViewData() has restored FileView::viewRef_ + m_gridMainC->clearSelection(GridEventPolicy::deny); + m_gridMainR->clearSelection(GridEventPolicy::deny); +} + + +void MainDialog::onGridLabelLeftClickC(GridLabelClickEvent& event) +{ + const ColumnTypeCenter colType = static_cast(event.colType_); + if (colType != ColumnTypeCenter::checkbox) + { + bool sortAscending = getDefaultSortDirection(colType); + + if (auto sortInfo = filegrid::getDataView(*m_gridMainC).getSortConfig()) + if (const ColumnTypeCenter* sortType = std::get_if(&sortInfo->sortCol)) + if (*sortType == colType) + sortAscending = !sortInfo->ascending; + + filegrid::getDataView(*m_gridMainC).sortView(colType, sortAscending); + updateGui(); //refresh gridDataView + + m_gridMainL->clearSelection(GridEventPolicy::deny); + m_gridMainC->clearSelection(GridEventPolicy::deny); + m_gridMainR->clearSelection(GridEventPolicy::deny); + } +} + + +void MainDialog::swapSides() +{ + if (std::exchange(operationInProgress_, true)) + return; + ZEN_ON_SCOPE_EXIT(operationInProgress_ = false); + + FocusPreserver fp; + + if (!folderCmp_.empty() && //require confirmation only *after* comparison + globalCfg_.confirmDlgs.confirmSwapSides) + { + bool dontWarnAgain = false; + switch (showConfirmationDialog(this, DialogInfoType::info, + PopupDialogCfg().setMainInstructions(_("Please confirm you want to swap sides.")). + setCheckBox(dontWarnAgain, _("&Don't show this dialog again")), + _("&Swap"))) + { + case ConfirmationButton::accept: //swap + globalCfg_.confirmDlgs.confirmSwapSides = !dontWarnAgain; + break; + case ConfirmationButton::cancel: + return; + } + } + //------------------------------------------------------ + + //swap directory names: + LocalPairConfig lpc1st = firstFolderPair_->getValues(); + std::swap(lpc1st.folderPathPhraseLeft, lpc1st.folderPathPhraseRight); + firstFolderPair_->setValues(lpc1st); + + for (FolderPairPanel* panel : additionalFolderPairs_) + { + LocalPairConfig lpc = panel->getValues(); + std::swap(lpc.folderPathPhraseLeft, lpc.folderPathPhraseRight); + panel->setValues(lpc); + } + + //swap view filter + bool tmp = m_bpButtonShowLeftOnly->isActive(); + m_bpButtonShowLeftOnly->setActive(m_bpButtonShowRightOnly->isActive()); + m_bpButtonShowRightOnly->setActive(tmp); + + tmp = m_bpButtonShowLeftNewer->isActive(); + m_bpButtonShowLeftNewer->setActive(m_bpButtonShowRightNewer->isActive()); + m_bpButtonShowRightNewer->setActive(tmp); + + /* for sync preview and "mirror" variant swapping may create strange effect: + tmp = m_bpButtonShowCreateLeft->isActive(); + m_bpButtonShowCreateLeft->setActive(m_bpButtonShowCreateRight->isActive()); + m_bpButtonShowCreateRight->setActive(tmp); + + tmp = m_bpButtonShowDeleteLeft->isActive(); + m_bpButtonShowDeleteLeft->setActive(m_bpButtonShowDeleteRight->isActive()); + m_bpButtonShowDeleteRight->setActive(tmp); + + tmp = m_bpButtonShowUpdateLeft->isActive(); + m_bpButtonShowUpdateLeft->setActive(m_bpButtonShowUpdateRight->isActive()); + m_bpButtonShowUpdateRight->setActive(tmp); + */ + //---------------------------------------------------------------------- + + if (!folderCmp_.empty()) + { + const auto& guiCfg = getConfig(); + + UiInputDisabler uiBlock(*this, true /*enableAbort*/); //StatusHandlerTemporaryPanel calls wxApp::Yield(), so avoid unexpected callbacks! + + StatusHandlerTemporaryPanel statusHandler(*this, std::chrono::system_clock::now() /*startTime*/, + false /*ignoreErrors*/, + guiCfg.mainCfg.autoRetryCount, + guiCfg.mainCfg.autoRetryDelay, + Zstr("") /*soundFileAlertPending*/); + try + { + statusHandler.initNewPhase(-1, -1, ProcessPhase::none); + swapGrids(getConfig().mainCfg, folderCmp_, + statusHandler); //throw CancelProcess + } + catch (CancelProcess&) {} + + const StatusHandlerTemporaryPanel::Result r = statusHandler.prepareResult(); //noexcept + setLastOperationLog(r.summary, r.errorLog.ptr()); + } + + updateGui(); //e.g. unsaved changes + + flashStatusInfo(_("Left and right sides have been swapped")); +} + + +void MainDialog::updateGridViewData() +{ + auto updateFilterButton = [&](ToggleButton& btn, const char* imgName, int itemCount) + { + const bool show = itemCount > 0; + if (show) + { + int& itemCountDrawn = buttonLabelItemCount_[&btn]; + assert(itemCount != 0); //itemCountDrawn defaults to 0! + if (itemCountDrawn != itemCount) //perf: only regenerate button labels when needed! + { + itemCountDrawn = itemCount; + + //accessibility: always set both foreground AND background colors! + wxImage imgCountPressed = mirrorIfRtl(createImageFromText(formatNumber(itemCount), btn.GetFont().Bold(), wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT))); + wxImage imgCountReleased = mirrorIfRtl(createImageFromText(formatNumber(itemCount), btn.GetFont(), wxSystemSettings::GetColour(wxSYS_COLOUR_BTNTEXT))); + imgCountReleased = resizeCanvas(imgCountReleased, imgCountPressed.GetSize(), wxALIGN_CENTER); //match with imgCountPressed's bold font + + //add bottom/right border space + imgCountPressed = resizeCanvas(imgCountPressed, imgCountPressed .GetSize() + wxSize(dipToScreen(5), dipToScreen(5)), wxALIGN_TOP | wxALIGN_LEFT); + imgCountReleased = resizeCanvas(imgCountReleased, imgCountReleased.GetSize() + wxSize(dipToScreen(5), dipToScreen(5)), wxALIGN_TOP | wxALIGN_LEFT); + + wxImage imgCategory = loadImage(imgName); + imgCategory = resizeCanvas(imgCategory, imgCategory.GetSize() + wxSize(dipToScreen(5), dipToScreen(2)), wxALIGN_CENTER); + + wxImage imgIconReleased = imgCategory.ConvertToGreyscale(1.0/3, 1.0/3, 1.0/3); //treat all channels equally! + brighten(imgIconReleased, 80); + + wxImage imgButtonPressed = stackImages(imgCategory, imgCountPressed, ImageStackLayout::horizontal, ImageStackAlignment::bottom); + wxImage imgButtonReleased = stackImages(imgIconReleased, imgCountReleased, ImageStackLayout::horizontal, ImageStackAlignment::bottom); + + btn.init(mirrorIfRtl(layOver(generatePressedButtonBack(imgButtonPressed.GetSize()), imgButtonPressed)), + mirrorIfRtl(imgButtonReleased)); + } + } + + if (btn.IsShown() != show) + btn.Show(show); + }; + + FileView::FileStats fileStatsLeft; + FileView::FileStats fileStatsRight; + + if (m_bpButtonViewType->isActive()) + { + const FileView::ActionViewStats viewStats = filegrid::getDataView(*m_gridMainC).applyActionFilter(m_bpButtonShowExcluded->isActive(), + m_bpButtonShowCreateLeft ->isActive(), + m_bpButtonShowCreateRight->isActive(), + m_bpButtonShowDeleteLeft ->isActive(), + m_bpButtonShowDeleteRight->isActive(), + m_bpButtonShowUpdateLeft ->isActive(), + m_bpButtonShowUpdateRight->isActive(), + m_bpButtonShowDoNothing ->isActive(), + m_bpButtonShowEqual ->isActive(), + m_bpButtonShowConflict ->isActive()); + fileStatsLeft = viewStats.fileStatsLeft; + fileStatsRight = viewStats.fileStatsRight; + + //sync preview buttons + updateFilterButton(*m_bpButtonShowExcluded, "cat_excluded", viewStats.excluded); + updateFilterButton(*m_bpButtonShowEqual, "cat_equal", viewStats.equal); + updateFilterButton(*m_bpButtonShowConflict, "cat_conflict", viewStats.conflict); + + updateFilterButton(*m_bpButtonShowCreateLeft, "so_create_left", viewStats.createLeft); + updateFilterButton(*m_bpButtonShowCreateRight, "so_create_right", viewStats.createRight); + updateFilterButton(*m_bpButtonShowDeleteLeft, "so_delete_left", viewStats.deleteLeft); + updateFilterButton(*m_bpButtonShowDeleteRight, "so_delete_right", viewStats.deleteRight); + updateFilterButton(*m_bpButtonShowUpdateLeft, "so_update_left", viewStats.updateLeft); + updateFilterButton(*m_bpButtonShowUpdateRight, "so_update_right", viewStats.updateRight); + updateFilterButton(*m_bpButtonShowDoNothing, "so_none", viewStats.updateNone); + + m_bpButtonShowLeftOnly ->Hide(); + m_bpButtonShowRightOnly ->Hide(); + m_bpButtonShowLeftNewer ->Hide(); + m_bpButtonShowRightNewer->Hide(); + m_bpButtonShowDifferent ->Hide(); + } + else + { + const FileView::DifferenceViewStats viewStats = filegrid::getDataView(*m_gridMainC).applyDifferenceFilter(m_bpButtonShowExcluded->isActive(), + m_bpButtonShowLeftOnly ->isActive(), + m_bpButtonShowRightOnly ->isActive(), + m_bpButtonShowLeftNewer ->isActive(), + m_bpButtonShowRightNewer->isActive(), + m_bpButtonShowDifferent ->isActive(), + m_bpButtonShowEqual ->isActive(), + m_bpButtonShowConflict ->isActive()); + fileStatsLeft = viewStats.fileStatsLeft; + fileStatsRight = viewStats.fileStatsRight; + + //comparison result view buttons + updateFilterButton(*m_bpButtonShowExcluded, "cat_excluded", viewStats.excluded); + updateFilterButton(*m_bpButtonShowEqual, "cat_equal", viewStats.equal); + updateFilterButton(*m_bpButtonShowConflict, "cat_conflict", viewStats.conflict); + + m_bpButtonShowCreateLeft ->Hide(); + m_bpButtonShowCreateRight->Hide(); + m_bpButtonShowDeleteLeft ->Hide(); + m_bpButtonShowDeleteRight->Hide(); + m_bpButtonShowUpdateLeft ->Hide(); + m_bpButtonShowUpdateRight->Hide(); + m_bpButtonShowDoNothing ->Hide(); + + updateFilterButton(*m_bpButtonShowLeftOnly, "cat_left_only", viewStats.leftOnly); + updateFilterButton(*m_bpButtonShowRightOnly, "cat_right_only", viewStats.rightOnly); + updateFilterButton(*m_bpButtonShowLeftNewer, "cat_left_newer", viewStats.leftNewer); + updateFilterButton(*m_bpButtonShowRightNewer, "cat_right_newer", viewStats.rightNewer); + updateFilterButton(*m_bpButtonShowDifferent, "cat_different", viewStats.different); + } + + const bool anyViewButtonShown = m_bpButtonShowExcluded ->IsShown() || + m_bpButtonShowEqual ->IsShown() || + m_bpButtonShowConflict ->IsShown() || + + m_bpButtonShowCreateLeft ->IsShown() || + m_bpButtonShowCreateRight->IsShown() || + m_bpButtonShowDeleteLeft ->IsShown() || + m_bpButtonShowDeleteRight->IsShown() || + m_bpButtonShowUpdateLeft ->IsShown() || + m_bpButtonShowUpdateRight->IsShown() || + m_bpButtonShowDoNothing ->IsShown() || + + m_bpButtonShowLeftOnly ->IsShown() || + m_bpButtonShowRightOnly ->IsShown() || + m_bpButtonShowLeftNewer ->IsShown() || + m_bpButtonShowRightNewer->IsShown() || + m_bpButtonShowDifferent ->IsShown(); + + m_bpButtonViewType ->Show(anyViewButtonShown); + m_bpButtonViewFilterContext->Show(anyViewButtonShown); + + //m_panelViewFilter->Dimensions(); -> yes, needed, but will also be called in updateStatistics(); + + //all three grids retrieve their data directly via gridDataView + filegrid::refresh(*m_gridMainL, *m_gridMainC, *m_gridMainR); + + //overview panel + if (m_bpButtonViewType->isActive()) + treegrid::getDataView(*m_gridOverview).applyActionFilter(m_bpButtonShowExcluded ->isActive(), + m_bpButtonShowCreateLeft ->isActive(), + m_bpButtonShowCreateRight->isActive(), + m_bpButtonShowDeleteLeft ->isActive(), + m_bpButtonShowDeleteRight->isActive(), + m_bpButtonShowUpdateLeft ->isActive(), + m_bpButtonShowUpdateRight->isActive(), + m_bpButtonShowDoNothing ->isActive(), + m_bpButtonShowEqual ->isActive(), + m_bpButtonShowConflict ->isActive()); + else + treegrid::getDataView(*m_gridOverview).applyDifferenceFilter(m_bpButtonShowExcluded ->isActive(), + m_bpButtonShowLeftOnly ->isActive(), + m_bpButtonShowRightOnly ->isActive(), + m_bpButtonShowLeftNewer ->isActive(), + m_bpButtonShowRightNewer->isActive(), + m_bpButtonShowDifferent ->isActive(), + m_bpButtonShowEqual ->isActive(), + m_bpButtonShowConflict ->isActive()); + m_gridOverview->Refresh(); + + //update status bar information + setStatusBarFileStats(fileStatsLeft, fileStatsRight); +} + + +void MainDialog::setStatusBarFileStats(FileView::FileStats statsLeft, + FileView::FileStats statsRight) +{ + //update status information + bSizerStatusLeftDirectories->Show(statsLeft.folderCount > 0); + bSizerStatusLeftFiles ->Show(statsLeft.fileCount > 0); + + setText(*m_staticTextStatusLeftDirs, _P("1 directory", "%x directories", statsLeft.folderCount)); + setText(*m_staticTextStatusLeftFiles, _P("1 file", "%x files", statsLeft.fileCount)); + setText(*m_staticTextStatusLeftBytes, L'(' + formatFilesizeShort(statsLeft.bytes) + L')'); + //------------------------------------------------------------------------------ + bSizerStatusRightDirectories->Show(statsRight.folderCount > 0); + bSizerStatusRightFiles ->Show(statsRight.fileCount > 0); + + setText(*m_staticTextStatusRightDirs, _P("1 directory", "%x directories", statsRight.folderCount)); + setText(*m_staticTextStatusRightFiles, _P("1 file", "%x files", statsRight.fileCount)); + setText(*m_staticTextStatusRightBytes, L'(' + formatFilesizeShort(statsRight.bytes) + L')'); + //------------------------------------------------------------------------------ + wxString statusCenterNew; + if (filegrid::getDataView(*m_gridMainC).rowsTotal() > 0) + { + statusCenterNew = _P("Showing %y of 1 item", "Showing %y of %x items", filegrid::getDataView(*m_gridMainC).rowsTotal()); + replace(statusCenterNew, L"%y", formatNumber(filegrid::getDataView(*m_gridMainC).rowsOnView())); //%x used as plural form placeholder! + } + + setStatusInfo(statusCenterNew, false /*highlight*/); +} + + +void MainDialog::applyFilterConfig() +{ + applyFiltering(folderCmp_, getConfig().mainCfg); + updateGui(); + //updateGuiDelayedIf(currentCfg.hideExcludedItems); //show update GUI before removing rows +} + + +void MainDialog::applySyncDirections() +{ + if (!folderCmp_.empty()) + { + if (std::exchange(operationInProgress_, true)) + //can't just skip:t now's a really bad time! Hopefully never happens!? + throw std::runtime_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Sync direction changed while other operation running."); + ZEN_ON_SCOPE_EXIT(operationInProgress_ = false); + + FocusPreserver fp; + + UiInputDisabler uiBlock(*this, true /*enableAbort*/); //StatusHandlerTemporaryPanel calls wxApp::Yield(), so avoid unexpected callbacks! + + const auto& guiCfg = getConfig(); + const auto& directCfgs = extractDirectionCfg(folderCmp_, getConfig().mainCfg); + + StatusHandlerTemporaryPanel statusHandler(*this, std::chrono::system_clock::now() /*startTime*/, + false /*ignoreErrors*/, + guiCfg.mainCfg.autoRetryCount, + guiCfg.mainCfg.autoRetryDelay, + Zstr("") /*soundFileAlertPending*/); + try + { + statusHandler.initNewPhase(-1, -1, ProcessPhase::none); + redetermineSyncDirection(directCfgs, + statusHandler); //throw CancelProcess + } + catch (CancelProcess&) {} + + const StatusHandlerTemporaryPanel::Result r = statusHandler.prepareResult(); //noexcept + setLastOperationLog(r.summary, r.errorLog.ptr()); + } + + updateGui(); //e.g. unsaved changes +} + + +void MainDialog::onSearchGridEnter(wxCommandEvent& event) +{ + startFindNext(true /*searchAscending*/); +} + + +void MainDialog::onHideSearchPanel(wxCommandEvent& event) +{ + showFindPanel(false /*show*/); +} + + +void MainDialog::onSearchPanelKeyPressed(wxKeyEvent& event) +{ + switch (event.GetKeyCode()) + { + case WXK_RETURN: + case WXK_NUMPAD_ENTER: //catches ENTER keys while focus is on *any* part of m_panelSearch! Seems to obsolete onSearchGridEnter()! + startFindNext(true /*searchAscending*/); + return; + case WXK_ESCAPE: + showFindPanel(false /*show*/); + return; + } + event.Skip(); +} + + +void MainDialog::showFindPanel(bool show) //CTRL + F or F3 with empty search phrase +{ + if (auiMgr_.GetPane(m_panelSearch).IsShown() != show) + { + auiMgr_.GetPane(m_panelSearch).Show(show); + auiMgr_.Update(); + } + + if (show) + { + m_textCtrlSearchTxt->SelectAll(); + + if (wxWindow* focus = wxWindow::FindFocus()) //restore when closing panel! + if (!isComponentOf(focus, m_panelSearch)) + focusAfterCloseSearch_ = focus->GetId(); + + m_textCtrlSearchTxt->SetFocus(); + } + else + { + if (isComponentOf(wxWindow::FindFocus(), m_panelSearch)) + if (wxWindow* oldFocusWin = wxWindow::FindWindowById(focusAfterCloseSearch_)) + oldFocusWin->SetFocus(); + + focusAfterCloseSearch_ = wxID_ANY; + } +} + + +void MainDialog::startFindNext(bool searchAscending) //F3 or ENTER in m_textCtrlSearchTxt +{ + const std::wstring& searchString = utfTo(trimCpy(m_textCtrlSearchTxt->GetValue())); + + if (searchString.empty()) + showFindPanel(true /*show*/); + else + { + Grid* grid1 = m_gridMainL; + Grid* grid2 = m_gridMainR; + + wxWindow* focus = wxWindow::FindFocus(); + if ((isComponentOf(focus, m_panelSearch) ? focusAfterCloseSearch_ : focus->GetId()) == m_gridMainR->getMainWin().GetId()) + std::swap(grid1, grid2); //select side to start search at grid cursor position + + wxBeginBusyCursor(wxHOURGLASS_CURSOR); + const std::pair result = findGridMatch(*grid1, *grid2, utfTo(searchString), + m_checkBoxMatchCase->GetValue(), searchAscending); + //parameter owned by GUI, *not* globalCfg structure! => we should better implement a getGlocalCfg()! + wxEndBusyCursor(); + + if (Grid* grid = const_cast(result.first)) //grid wasn't const when passing to findAndSelectNext(), so this is legal + { + assert(result.second >= 0); + + filegrid::setScrollMaster(*grid); + grid->setGridCursor(result.second, GridEventPolicy::allow); + + focusAfterCloseSearch_ = grid->getMainWin().GetId(); + + if (!isComponentOf(wxWindow::FindFocus(), m_panelSearch)) + grid->getMainWin().SetFocus(); + } + else + { + showFindPanel(true /*show*/); + showNotificationDialog(this, DialogInfoType::info, PopupDialogCfg().setTitle(_("Find")). + setMainInstructions(replaceCpy(_("Cannot find %x"), L"%x", fmtPath(searchString)))); + } + } +} + + +void MainDialog::onTopFolderPairAdd(wxCommandEvent& event) +{ + insertAddFolderPair({LocalPairConfig()}, 0); + moveAddFolderPairUp(0); +} + + +void MainDialog::onTopFolderPairRemove(wxCommandEvent& event) +{ + assert(!additionalFolderPairs_.empty()); + if (!additionalFolderPairs_.empty()) + { + moveAddFolderPairUp(0); + removeAddFolderPair(0); + } +} + + +void MainDialog::onLocalCompCfg(wxCommandEvent& event) +{ + const wxObject* const eventObj = event.GetEventObject(); //find folder pair originating the event + for (auto it = additionalFolderPairs_.begin(); it != additionalFolderPairs_.end(); ++it) + if (eventObj == (*it)->m_bpButtonLocalCompCfg) + { + showConfigDialog(SyncConfigPanel::compare, (it - additionalFolderPairs_.begin()) + 1); + break; + } +} + + +void MainDialog::onLocalSyncCfg(wxCommandEvent& event) +{ + const wxObject* const eventObj = event.GetEventObject(); //find folder pair originating the event + for (auto it = additionalFolderPairs_.begin(); it != additionalFolderPairs_.end(); ++it) + if (eventObj == (*it)->m_bpButtonLocalSyncCfg) + { + showConfigDialog(SyncConfigPanel::sync, (it - additionalFolderPairs_.begin()) + 1); + break; + } +} + + +void MainDialog::onLocalFilterCfg(wxCommandEvent& event) +{ + const wxObject* const eventObj = event.GetEventObject(); //find folder pair originating the event + for (auto it = additionalFolderPairs_.begin(); it != additionalFolderPairs_.end(); ++it) + if (eventObj == (*it)->m_bpButtonLocalFilter) + { + showConfigDialog(SyncConfigPanel::filter, (it - additionalFolderPairs_.begin()) + 1); + break; + } +} + + +void MainDialog::onRemoveFolderPair(wxCommandEvent& event) +{ + const wxObject* const eventObj = event.GetEventObject(); //find folder pair originating the event + for (auto it = additionalFolderPairs_.begin(); it != additionalFolderPairs_.end(); ++it) + if (eventObj == (*it)->m_bpButtonRemovePair) + { + removeAddFolderPair(it - additionalFolderPairs_.begin()); + break; + } +} + + +void MainDialog::onShowFolderPairOptions(wxEvent& event) +{ + const wxObject* const eventObj = event.GetEventObject(); //find folder pair originating the event + for (auto it = additionalFolderPairs_.begin(); it != additionalFolderPairs_.end(); ++it) + if (eventObj == (*it)->m_bpButtonFolderPairOptions) + { + const ptrdiff_t pos = it - additionalFolderPairs_.begin(); + + ContextMenu menu; + menu.addItem(_("Add folder pair"), [this, pos] { insertAddFolderPair({LocalPairConfig()}, pos); }, loadImage("item_add_sicon")); + menu.addSeparator(); + menu.addItem(_("Move up" ) + L"\tAlt+Page Up", [this, pos] { moveAddFolderPairUp(pos); }, loadImage("move_up_sicon")); + menu.addItem(_("Move down") + L"\tAlt+Page Down", [this, pos] { moveAddFolderPairUp(pos + 1); }, loadImage("move_down_sicon"), + pos + 1 < makeSigned(additionalFolderPairs_.size())); + + menu.popup(*(*it)->m_bpButtonFolderPairOptions, {(*it)->m_bpButtonFolderPairOptions->GetSize().x, 0}); + break; + } +} + + +void MainDialog::onTopFolderPairKeyEvent(wxKeyEvent& event) +{ + const int keyCode = event.GetKeyCode(); + + if (event.AltDown()) + switch (keyCode) + { + case WXK_PAGEDOWN: //Alt + Page Down + case WXK_NUMPAD_PAGEDOWN: + if (!additionalFolderPairs_.empty()) + { + moveAddFolderPairUp(0); + additionalFolderPairs_[0]->m_folderPathLeft->SetFocus(); + } + return; + } + + event.Skip(); +} + + +void MainDialog::onAddFolderPairKeyEvent(wxKeyEvent& event) +{ + const int keyCode = event.GetKeyCode(); + + auto getAddFolderPairPos = [&]() -> ptrdiff_t //find folder pair originating the event + { + if (auto eventObj = dynamic_cast(event.GetEventObject())) + for (auto it = additionalFolderPairs_.begin(); it != additionalFolderPairs_.end(); ++it) + if (isComponentOf(eventObj, *it)) + return it - additionalFolderPairs_.begin(); + return -1; + }; + + if (event.AltDown()) + switch (keyCode) + { + case WXK_PAGEUP: //Alt + Page Up + case WXK_NUMPAD_PAGEUP: + if (const ptrdiff_t pos = getAddFolderPairPos(); + pos >= 0) + { + moveAddFolderPairUp(pos); + (pos == 0 ? m_folderPathLeft : additionalFolderPairs_[pos - 1]->m_folderPathLeft)->SetFocus(); + } + return; + + case WXK_PAGEDOWN: //Alt + Page Down + case WXK_NUMPAD_PAGEDOWN: + if (const ptrdiff_t pos = getAddFolderPairPos(); + 0 <= pos && pos + 1 < makeSigned(additionalFolderPairs_.size())) + { + moveAddFolderPairUp(pos + 1); + additionalFolderPairs_[pos + 1]->m_folderPathLeft->SetFocus(); + } + return; + } + + event.Skip(); +} + + +void MainDialog::updateGuiForFolderPair() +{ + recalcMaxFolderPairsVisible(); + + //adapt delete top folder pair button + m_bpButtonRemovePair->Show(!additionalFolderPairs_.empty()); + m_panelTopLeft->Layout(); + + //adapt local filter and sync cfg for first folder pair + const bool showLocalCfgFirstPair = !additionalFolderPairs_.empty() || + firstFolderPair_->getCompConfig() || + firstFolderPair_->getSyncConfig() || + !isNullFilter(firstFolderPair_->getFilterConfig()); + //harmonize with MainDialog::showConfigDialog()! + + m_bpButtonLocalCompCfg->Show(showLocalCfgFirstPair); + m_bpButtonLocalSyncCfg->Show(showLocalCfgFirstPair); + m_bpButtonLocalFilter ->Show(showLocalCfgFirstPair); + setImage(*m_bpButtonSwapSides, loadImage(showLocalCfgFirstPair ? "swap_slim" : "swap")); + + //update sub-panel sizes for calculations below!!! + m_panelTopCenter->GetSizer()->SetSizeHints(m_panelTopCenter); //~=Fit() + SetMinSize() + + const int firstPairHeight = std::max(m_panelDirectoryPairs->ClientToWindowSize(m_panelTopLeft ->GetSize()).y, //include m_panelDirectoryPairs window borders! + m_panelDirectoryPairs->ClientToWindowSize(m_panelTopCenter->GetSize()).y); // + const int addPairHeight = !additionalFolderPairs_.empty() ? additionalFolderPairs_[0]->GetSize().y : 0; + + const double addPairCountMax = std::max(globalCfg_.mainDlg.folderPairsVisibleMax - 1 + 0.5, 1.5); + + const double addPairCountMin = std::min(1.5, additionalFolderPairs_.size()); //add 0.5 to indicate additional folders + const double addPairCountOpt = std::min(addPairCountMax, additionalFolderPairs_.size()); // + addPairCountLast_ = addPairCountOpt; + + wxAuiPaneInfo& dirPane = auiMgr_.GetPane(m_panelDirectoryPairs); + + //make sure user cannot fully shrink additional folder pairs + dirPane.MinSize(dipToWxsize(100), firstPairHeight + addPairCountMin * addPairHeight); + dirPane.BestSize(-1, firstPairHeight + addPairCountOpt * addPairHeight); + + //######################################################################################################################## + //wxAUI hack: call wxAuiPaneInfo::Fixed() to apply best size: + dirPane.Fixed(); + auiMgr_.Update(); + + //now make resizable again + dirPane.Resizable(); + auiMgr_.Update(); + //alternative: dirPane.Hide() + .Show() seems to work equally well + + //######################################################################################################################## + + //it seems there is no GetSizer()->SetSizeHints(this)/Fit() required due to wxAui "magic" + //=> *massive* perf improvement on OS X! +} + + +void MainDialog::recalcMaxFolderPairsVisible() +{ + const int firstPairHeight = std::max(m_panelDirectoryPairs->ClientToWindowSize(m_panelTopLeft ->GetSize()).y, //include m_panelDirectoryPairs window borders! + m_panelDirectoryPairs->ClientToWindowSize(m_panelTopCenter->GetSize()).y); // + const int addPairHeight = !additionalFolderPairs_.empty() ? additionalFolderPairs_[0]->GetSize().y : + m_bpButtonAddPair->GetSize().y; //an educated guess + + //assert(firstPairHeight > 0 && addPairHeight > 0); -> wxWindows::GetSize() returns 0 if main window is minimized during sync! Test with "When finished: Exit" + + if (addPairCountLast_ && firstPairHeight > 0 && addPairHeight > 0) + { + const double addPairCountCurrent = (m_panelDirectoryPairs->GetSize().y - firstPairHeight) / (1.0 * addPairHeight); //include m_panelDirectoryPairs window borders! + + if (std::abs(addPairCountCurrent - *addPairCountLast_) > 0.4) //=> presumely changed by user! + { + globalCfg_.mainDlg.folderPairsVisibleMax = std::round(addPairCountCurrent) + 1; + } + } +} + + +void MainDialog::insertAddFolderPair(const std::vector& newPairs, size_t pos) +{ + assert(pos <= additionalFolderPairs_.size() && additionalFolderPairs_.size() == bSizerAddFolderPairs->GetItemCount()); + pos = std::min(pos, additionalFolderPairs_.size()); + + for (size_t i = 0; i < newPairs.size(); ++i) + { + FolderPairPanel* newPair = nullptr; + if (!folderPairScrapyard_.empty()) //construct cheaply from "spare parts" + { + newPair = folderPairScrapyard_.back().release(); //transfer ownership + folderPairScrapyard_.pop_back(); + newPair->Show(); + } + else + { + newPair = new FolderPairPanel(m_scrolledWindowFolderPairs, *this, + globalCfg_.mainDlg.folderLastSelectedLeft, + globalCfg_.mainDlg.folderLastSelectedRight, + globalCfg_.sftpKeyFileLastSelected); + + //setHistory dropdown history + newPair->m_folderPathLeft ->setHistory(folderHistoryLeft_ ); + newPair->m_folderPathRight->setHistory(folderHistoryRight_); + + const wxSize optionsIconSize = loadImage("item_add").GetSize(); + setImage(*(newPair->m_bpButtonFolderPairOptions), resizeCanvas(mirrorIfRtl(loadImage("button_arrow_right")), optionsIconSize, wxALIGN_CENTER)); + + //set width of left folder panel + const int width = m_panelTopLeft->GetSize().GetWidth(); + newPair->m_panelLeft->SetMinSize({width, -1}); + + //register events + newPair->m_bpButtonFolderPairOptions->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](wxCommandEvent& event) { onShowFolderPairOptions(event); }); + newPair->m_bpButtonFolderPairOptions->Bind(wxEVT_RIGHT_DOWN, [this](wxMouseEvent& event) { onShowFolderPairOptions(event); }); + newPair->m_bpButtonRemovePair ->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](wxCommandEvent& event) { onRemoveFolderPair (event); }); + + static_cast(newPair)->Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onAddFolderPairKeyEvent(event); }); + + newPair->m_bpButtonLocalCompCfg->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](wxCommandEvent& event) { onLocalCompCfg (event); }); + newPair->m_bpButtonLocalSyncCfg->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](wxCommandEvent& event) { onLocalSyncCfg (event); }); + newPair->m_bpButtonLocalFilter ->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](wxCommandEvent& event) { onLocalFilterCfg(event); }); + + //important: make sure panel has proper default height! + newPair->GetSizer()->SetSizeHints(newPair); //~=Fit() + SetMinSize() + } + + bSizerAddFolderPairs->Insert(pos + i, newPair, 0, wxEXPAND); + additionalFolderPairs_.insert(additionalFolderPairs_.begin() + pos + i, newPair); + + //wxComboBox screws up miserably if width/height is smaller than the magic number 4! Problem occurs when trying to set tooltip + //so we have to update window sizes before setting configuration: + newPair->setValues(newPairs[i]); + } + + updateGuiForFolderPair(); + + clearGrid(); //+ GUI update +} + + +void MainDialog::moveAddFolderPairUp(size_t pos) +{ + assert(pos < additionalFolderPairs_.size()); + if (pos < additionalFolderPairs_.size()) + { + const LocalPairConfig cfgTmp = additionalFolderPairs_[pos]->getValues(); + if (pos == 0) + { + additionalFolderPairs_[pos]->setValues(firstFolderPair_->getValues()); + firstFolderPair_->setValues(cfgTmp); + } + else + { + additionalFolderPairs_[pos]->setValues(additionalFolderPairs_[pos - 1]->getValues()); + additionalFolderPairs_[pos - 1]->setValues(cfgTmp); + } + + //move comparison results, too! + if (!folderCmp_.empty()) + std::swap(folderCmp_[pos], folderCmp_[pos + 1]); //invariant: folderCmp is empty or matches number of all folder pairs + + filegrid::setData(*m_gridMainC, folderCmp_); + treegrid::setData(*m_gridOverview, folderCmp_); + updateGui(); + } +} + + +void MainDialog::removeAddFolderPair(size_t pos) +{ + assert(pos < additionalFolderPairs_.size()); + if (pos < additionalFolderPairs_.size()) + { + FolderPairPanel* panel = additionalFolderPairs_[pos]; + + additionalFolderPairs_.erase(additionalFolderPairs_.begin() + pos); + bSizerAddFolderPairs->Detach(panel); //Remove() does not work on wxWindow*, so do it manually + //more (non-portable) wxWidgets bullshit: on OS X wxWindow::Destroy() screws up and calls "operator delete" directly rather than + //the deferred deletion it is expected to do (and which is implemented correctly on Windows and Linux) + //http://bb10.com/python-wxpython-devel/2012-09/msg00004.html + //=> since we're in a mouse button callback of a sub-component of "panel" we need to delay deletion ourselves: + panel->Hide(); + folderPairScrapyard_.emplace_back(panel); //transfer ownership + + updateGuiForFolderPair(); + clearGrid(pos + 1); //+ GUI update + } +} + + +void MainDialog::setAddFolderPairs(const std::vector& newPairs) +{ + //FolderPairPanel are too expensive to casually throw away and recreate! + for (FolderPairPanel* panel : additionalFolderPairs_) + { + panel->Hide(); + folderPairScrapyard_.emplace_back(panel); //transfer ownership + } + additionalFolderPairs_.clear(); + bSizerAddFolderPairs->Clear(false /*delete_windows*/); //release ownership + + insertAddFolderPair(newPairs, 0); +} + + +//######################################################################################################## + + +void MainDialog::onMenuOptions(wxCommandEvent& event) +{ + const ColorTheme colorThemeOld = globalCfg_.appColorTheme; + + showOptionsDlg(this, globalCfg_); + + if (!equalAppearance(globalCfg_.appColorTheme, colorThemeOld)) + { + if (!folderCmp_.empty()) //otherwise: why bother the user? + switch (showConfirmationDialog(this, DialogInfoType::warning, PopupDialogCfg().setTitle(_("Confirm")). + setMainInstructions(_("The application must restart to change the color theme.") + L"\n\n" + + _("Restart now?")), _("&Restart"))) + { + case ConfirmationButton::accept: + break; + case ConfirmationButton::cancel: + return; + } + try + { + changeColorTheme(globalCfg_.appColorTheme); //throw FileError + //should work on macOS/Linux, but not on Windows (until wxWidgets fixes their s...) + + //show new dialog, then delete old one + MainDialog::create(getConfig(), activeConfigFiles_, getGlobalCfgBeforeExit(), globalCfgFilePath_, false /*startComparison*/); + Destroy(); + } + catch (FileError&) //changing color scheme failed => restart app + { + onSystemShutdownRunTasks(); //LastRun.ffs_gui + GlobalSettings.xml +... + try + { + const Zstring ffsLaunchPath = getProcessPath(); //throw FileError + try + { + //run async, but give consoleExecute() some "time to fail" + const auto& [exitCode, output] = consoleExecute(ffsLaunchPath, 100 /*timeoutMs*/); //throw SysError, SysErrorTimeOut + if (exitCode != 0) + throw SysError(formatSystemError("", replaceCpy(_("Exit code %x"), L"%x", numberTo(exitCode)), utfTo(output))); + } + catch (SysErrorTimeOut&) {} + catch (const SysError& e) { throw FileError(replaceCpy(_("Command %x failed."), L"%x", fmtPath(ffsLaunchPath)), e.toString()); } + } + catch (const FileError& e) { showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); } + + //don't continue after having called onSystemShutdownRunTasks() + // => also avoid ~MainDialog() calling getGlobalCfgBeforeExit() a second time and saving cfg needlessly + terminateProcess(static_cast(FfsExitCode::success)); + } + } +} + + +void MainDialog::onMenuExportFileList(wxCommandEvent& event) +{ + wxBusyCursor dummy; + + //https://en.wikipedia.org/wiki/Comma-separated_values + const lconv* localInfo = ::localeconv(); //always bound according to doc + const bool haveCommaAsDecimalSep = std::string(localInfo->decimal_point) == ","; + + const char CSV_SEP = haveCommaAsDecimalSep ? ';' : ','; + + auto fmtValue = [&](const std::wstring& val) -> std::string + { + std::string&& tmp = utfTo(val); + + if (contains(tmp, CSV_SEP)) + return '"' + tmp + '"'; + else + return std::move(tmp); + }; + + //generate header + std::string header; + header += BYTE_ORDER_MARK_UTF8; + + header += fmtValue(_("Folder Pairs")) + LINE_BREAK; + for (const BaseFolderPair& baseFolder : asRange(folderCmp_)) + { + header += fmtValue(AFS::getDisplayPath(baseFolder.getAbstractPath())) + CSV_SEP; + header += fmtValue(AFS::getDisplayPath(baseFolder.getAbstractPath())) + LINE_BREAK; + } + header += LINE_BREAK; + + auto provLeft = m_gridMainL->getDataProvider(); + auto provCenter = m_gridMainC->getDataProvider(); + auto provRight = m_gridMainR->getDataProvider(); + + auto colAttrLeft = m_gridMainL->getColumnConfig(); + auto colAttrCenter = m_gridMainC->getColumnConfig(); + auto colAttrRight = m_gridMainR->getColumnConfig(); + + std::erase_if(colAttrLeft, [](const Grid::ColAttributes& ca) { return !ca.visible; }); + std::erase_if(colAttrCenter, [](const Grid::ColAttributes& ca) { return !ca.visible || static_cast(ca.type) == ColumnTypeCenter::checkbox; }); + std::erase_if(colAttrRight, [](const Grid::ColAttributes& ca) { return !ca.visible; }); + + if (provLeft && provCenter && provRight) + { + for (const Grid::ColAttributes& ca : colAttrLeft) + { + header += fmtValue(provLeft->getColumnLabel(ca.type)); + header += CSV_SEP; + } + for (const Grid::ColAttributes& ca : colAttrCenter) + { + header += fmtValue(provCenter->getColumnLabel(ca.type)); + header += CSV_SEP; + } + if (!colAttrRight.empty()) + { + std::for_each(colAttrRight.begin(), colAttrRight.end() - 1, + [&](const Grid::ColAttributes& ca) + { + header += fmtValue(provRight->getColumnLabel(ca.type)); + header += CSV_SEP; + }); + header += fmtValue(provRight->getColumnLabel(colAttrRight.back().type)); + } + header += LINE_BREAK; + + try + { + Zstring title = Zstr("FreeFileSync"); + if (const std::vector& jobNames = getJobNames(); + !jobNames.empty()) + { + title = utfTo(jobNames[0]); + std::for_each(jobNames.begin() + 1, jobNames.end(), [&](const std::wstring& jobName) + { title += Zstr(" + ") + utfTo(jobName); }); + } + + const Zstring shortGuid = printNumber(Zstr("%04x"), static_cast(getCrc16(generateGUID()))); + const Zstring csvFilePath = appendPath(tempFileBuf_.getAndCreateFolderPath(), //throw FileError + title + Zstr('~') + shortGuid + Zstr(".csv")); + + const Zstring tmpFilePath = getPathWithTempName(csvFilePath); + + FileOutputBuffered tmpFile(tmpFilePath, nullptr /*notifyUnbufferedIO*/); //throw FileError, (ErrorTargetExisting) + + auto writeString = [&](const std::string& str) { tmpFile.write(str.data(), str.size()); }; //throw FileError + + //main grid: write rows one after the other instead of creating one big string: memory allocation might fail; think 1 million rows! + writeString(header); //throw FileError + + const size_t rowCount = m_gridMainL->getRowCount(); + for (size_t row = 0; row < rowCount; ++row) + { + for (const Grid::ColAttributes& ca : colAttrLeft) + writeString(fmtValue(provLeft->getValue(row, ca.type)) += CSV_SEP); //throw FileError + + for (const Grid::ColAttributes& ca : colAttrCenter) + writeString(fmtValue(provCenter->getValue(row, ca.type)) += CSV_SEP); //throw FileError + + for (const Grid::ColAttributes& ca : colAttrRight) + writeString(fmtValue(provRight->getValue(row, ca.type)) += CSV_SEP); //throw FileError + + writeString(LINE_BREAK); //throw FileError + } + + tmpFile.finalize(); //throw FileError + //take over ownership: + ZEN_ON_SCOPE_FAIL( try { removeFilePlain(tmpFilePath); } + catch (const FileError& e) { logExtraError(e.toString()); }); + + //operation finished: move temp file transactionally + moveAndRenameItem(tmpFilePath, csvFilePath, true /*replaceExisting*/); //throw FileError, (ErrorMoveUnsupported), (ErrorTargetExisting) + + openWithDefaultApp(csvFilePath); //throw FileError + + flashStatusInfo(_("File list exported")); + } + catch (const FileError& e) + { + showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); + } + } +} + + +void MainDialog::onMenuCheckVersion(wxCommandEvent& event) +{ + checkForUpdateNow(*this, globalCfg_.lastOnlineVersion); +} + + +void MainDialog::onStartupUpdateCheck(wxIdleEvent& event) +{ + //execute just once per startup! + [[maybe_unused]] bool ubOk = Unbind(wxEVT_IDLE, &MainDialog::onStartupUpdateCheck, this); + assert(ubOk); + + auto showNewVersionReminder = [this] + { + if (haveNewerVersionOnline(globalCfg_.lastOnlineVersion)) + { + auto menu = new wxMenu(); + wxMenuItem* newItem = new wxMenuItem(menu, wxID_ANY, _("&Show details")); + Bind(wxEVT_COMMAND_MENU_SELECTED, [this](wxCommandEvent&) { checkForUpdateNow(*this, globalCfg_.lastOnlineVersion); }, newItem->GetId()); + //show changelog + handle Supporter Edition auto-updater (including expiration) + menu->Append(newItem); //pass ownership + + const std::wstring& blackStar = utfTo("★"); + m_menubar->Append(menu, blackStar + L' ' + replaceCpy(_("FreeFileSync %x is available!"), L"%x", utfTo(globalCfg_.lastOnlineVersion)) + L' ' + blackStar); + } + }; + + if (automaticUpdateCheckDue(globalCfg_.lastUpdateCheck)) + { + flashStatusInfo(_("Searching for software updates...")); + + guiQueue_.processAsync([resultPrep = automaticUpdateCheckPrepare(*this) /*prepare on main thread*/] + { return automaticUpdateCheckRunAsync(resultPrep.ref()); }, //run on worker thread: (long-running part of the check) + [this, showNewVersionReminder] (SharedRef&& resultAsync) + { + const time_t lastUpdateCheckOld = globalCfg_.lastUpdateCheck; + + automaticUpdateCheckEval(*this, globalCfg_.lastUpdateCheck, globalCfg_.lastOnlineVersion, + resultAsync.ref()); //run on main thread: + showNewVersionReminder(); + + if (globalCfg_.lastUpdateCheck == lastUpdateCheckOld) + flashStatusInfo(_("Software update check failed!")); + }); + } + else + showNewVersionReminder(); +} + + +void MainDialog::onLayoutWindowAsync(wxIdleEvent& event) +{ + //execute just once per startup! + [[maybe_unused]] bool ubOk = Unbind(wxEVT_IDLE, &MainDialog::onLayoutWindowAsync, this); + assert(ubOk); + + //adjust folder pair distortion on startup + for (FolderPairPanel* panel : additionalFolderPairs_) + panel->Layout(); + + Layout(); //strangely this layout call works if called in next idle event only + m_panelTopButtons->Layout(); + + //auiMgr_.Update(); fix view filter distortion; 2021-02-01: apparently not needed anymore! +} + + +void MainDialog::onMenuAbout(wxCommandEvent& event) +{ + showAboutDialog(this); +} + + +void MainDialog::switchProgramLanguage(wxLanguage langId) +{ + try + { + //set language *before* creating MainDialog! + setLanguage(langId); //throw FileError + } + catch (const FileError& e) + { + showNotificationDialog(nullptr, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); + return; + } + + //create new dialog with respect to new language + GlobalConfig newGlobalCfg = getGlobalCfgBeforeExit(); + newGlobalCfg.programLanguage = langId; + + //show new dialog, then delete old one + MainDialog::create(getConfig(), activeConfigFiles_, newGlobalCfg, globalCfgFilePath_, false /*startComparison*/); + + //don't use Close(): + //1. we don't want to show the prompt to save current config in onClose() + //2. after getGlobalCfgBeforeExit() the old main dialog is invalid so we want to force deletion + Destroy(); //alternative: Close(true /*force*/) +} + + +void MainDialog::setGridViewType(GridViewType vt) +{ + //if (m_bpButtonViewType->isActive() == value) return; support polling -> what about initialization? + + m_bpButtonViewType->setActive(vt == GridViewType::action); + m_bpButtonViewType->SetToolTip((vt == GridViewType::action ? _("Action") : _("Difference")) + L" (F11)"); + + //toggle display of sync preview in middle grid + filegrid::setViewType(*m_gridMainC, vt); + + updateGui(); +} diff --git a/FreeFileSync/Source/ui/main_dlg.h b/FreeFileSync/Source/ui/main_dlg.h new file mode 100644 index 0000000..2ceb5a6 --- /dev/null +++ b/FreeFileSync/Source/ui/main_dlg.h @@ -0,0 +1,370 @@ +// ***************************************************************************** +// * 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 MAIN_DLG_H_8910481324545644545 +#define MAIN_DLG_H_8910481324545644545 + +//#include +#include +#include +#include +#include "gui_generated.h" +#include "file_grid.h" +#include "progress_indicator.h" +#include "sync_cfg.h" +#include "log_panel.h" +#include "folder_history_box.h" +#include "../config.h" +//#include "../status_handler.h" +#include "../base/algorithm.h" +#include "../base/synchronization.h" + + +namespace fff +{ +class FolderPairFirst; +class FolderPairPanel; +template class FolderPairCallback; + + +class MainDialog : public MainDialogGenerated +{ +public: + //default behavior, application start, restores last used config + static void create(const GlobalConfig& globalCfg, const Zstring& globalCfgFilePath); + + //- when loading dynamically assembled config + //- when switching language + //- switching from batch run to GUI on warnings dialog + static void create(const FfsGuiConfig& guiCfg, const std::vector& cfgFilePaths, + const GlobalConfig& globalCfg, const Zstring& globalCfgFilePath, //take over ownership => save on exit + bool startComparison); + +private: + MainDialog(const FfsGuiConfig& guiCfg, const std::vector& cfgFilePaths, + const GlobalConfig& globalCfg, const Zstring& globalCfgFilePath); //take over ownership => save on exit + ~MainDialog(); + + void onBeforeSystemShutdown(); //last chance to do something useful before killing the application! + + friend class StatusHandlerTemporaryPanel; + friend class StatusHandlerFloatingDialog; + friend class FolderPairFirst; + friend class FolderPairPanel; + template + friend class FolderPairCallback; + friend class PanelMoveWindow; + + class UiInputDisabler; + + //configuration load/save + void setLastUsedConfig(const FfsGuiConfig& guiConfig, const std::vector& cfgFilePaths); + + FfsGuiConfig getConfig() const; + void setConfig(const FfsGuiConfig& newGuiCfg, const std::vector& cfgFilePaths); + + void setGlobalCfgOnInit(const GlobalConfig& globalCfg); //messes with Maximize(), window sizes, so call just once! + GlobalConfig getGlobalCfgBeforeExit(); //destructive "get" thanks to "Iconize(false), Maximize(false)" + + bool loadConfiguration(const std::vector& filepaths, bool ignoreBrokenConfig = false); //"false": error/cancel + + bool trySaveConfig (const Zstring* guiCfgPath); // + bool trySaveBatchConfig(const Zstring* batchCfgPath); //"false": error/cancel + bool saveOldConfig(); // + + void updateGlobalFilterButton(); + + void setViewFilterDefault(); + + void cfgHistoryRemoveObsolete(const std::vector& filepaths); + void cfgHistoryUpdateNotes (const std::vector& filepaths); + + void insertAddFolderPair(const std::vector& newPairs, size_t pos); + void moveAddFolderPairUp(size_t pos); + void removeAddFolderPair(size_t pos); + void setAddFolderPairs(const std::vector& newPairs); + + void updateGuiForFolderPair(); //helper method: add usability by showing/hiding buttons related to folder pairs + void recalcMaxFolderPairsVisible(); + + //main method for putting gridDataView on UI: updates data respecting current view settings + void updateGui(); //kitchen-sink update + void updateGuiDelayedIf(bool condition); //400 ms delay + + void updateGridViewData(); // + void updateStatistics(const SyncStatistics& st); // more fine-grained updaters + void updateUnsavedCfgStatus(); // + + std::vector getJobNames() const; + + //context menu functions + std::vector getGridSelection(bool fromLeft = true, bool fromRight = true) const; + std::vector getTreeSelection() const; + + void setSyncDirManually (const std::vector& selection, SyncDirection direction); + void setIncludedManually(const std::vector& selection, bool setIncluded); + void copyGridSelectionToClipboard(const zen::Grid& grid); + void copyPathsToClipboard(const std::vector& selectionL, + const std::vector& selectionR); + + void copyToAlternateFolder(const std::vector& selectionL, + const std::vector& selectionR); + + void deleteSelectedFiles(const std::vector& selectionL, + const std::vector& selectionR, bool moveToRecycler); + + void renameSelectedFiles(const std::vector& selectionL, + const std::vector& selectionR); + + void openExternalApplication(const Zstring& commandLinePhrase, bool leftSide, + const std::vector& selectionL, + const std::vector& selectionR); //selection may be empty + + void setStatusBarFileStats(FileView::FileStats statsLeft, FileView::FileStats statsRight); + + void setStatusInfo(const wxString& text, bool highlight); //(permanently) set status bar center text + void flashStatusInfo(const wxString& text); //temporarily show different status + void popStatusInfo(); + + //events + void onGridKeyEvent(wxKeyEvent& event, zen::Grid& grid, bool leftSide); + + void onTreeKeyEvent (wxKeyEvent& event); + void onSetLayoutContext(wxMouseEvent& event); + void onLocalKeyEvent (wxKeyEvent& event); + + void applyCompareConfig(bool setDefaultViewType); + + //context menu handler methods + void onGridContextRim(zen::GridContextMenuEvent& event, bool leftSide); + + void onGridGroupContextRim(zen::GridClickEvent& event, bool leftSide); + + void onGridContextRim(const std::vector& selection, + const std::vector& selectionL, + const std::vector& selectionR, bool leftSide, wxPoint mousePos); + + void onTreeGridContext(zen::GridContextMenuEvent& event); + + void onTreeGridSelection(zen::GridSelectEvent& event); + + void onDialogFilesDropped(zen::FileDropEvent& event); + + void onFolderSelected(wxCommandEvent& event); + + void onCheckRows (CheckRowsEvent& event); + void onSetSyncDirection(SyncDirectionEvent& event); + + void swapSides(); + + void onGridDoubleClickRim(zen::GridClickEvent& event, bool leftSide); + + void onGridLabelLeftClickRim(zen::GridLabelClickEvent& event, bool onLeft); + void onGridLabelLeftClickC (zen::GridLabelClickEvent& event); + + void onGridLabelContextRim(zen::GridLabelClickEvent& event, bool leftSide); + void onGridLabelContextC (zen::GridLabelClickEvent& event); + + void onToggleViewType (wxCommandEvent& event) override; + void onToggleViewButton(wxCommandEvent& event) override; + + void onViewTypeContextMouse (wxMouseEvent& event) override; + void onViewFilterContext (wxCommandEvent& event) override { onViewFilterContext(static_cast(event)); } + void onViewFilterContextMouse(wxMouseEvent& event) override { onViewFilterContext(static_cast(event)); } + void onViewFilterContext(wxEvent& event); + + void onConfigNew (wxCommandEvent& event) override; + void onConfigSave (wxCommandEvent& event) override; + void onConfigSaveAs (wxCommandEvent& event) override { trySaveConfig(nullptr); } + void onSaveAsBatchJob(wxCommandEvent& event) override { trySaveBatchConfig(nullptr); } + void onConfigLoad (wxCommandEvent& event) override; + + void onCfgGridSelection (zen::GridSelectEvent& event); + void onCfgGridDoubleClick(zen::GridClickEvent& event); + void onCfgGridKeyEvent (wxKeyEvent& event); + void onCfgGridContext (zen::GridContextMenuEvent& event); + void onCfgGridLabelContext (zen::GridLabelClickEvent& event); + void onCfgGridLabelLeftClick(zen::GridLabelClickEvent& event); + + void removeSelectedCfgHistoryItems(bool removeFromDisk); + void renameSelectedCfgHistoryItem(); + + void onStartupUpdateCheck(wxIdleEvent& event); + void onLayoutWindowAsync (wxIdleEvent& event); + + void onResizeLeftFolderWidth(wxEvent& event); + void onResizeTopButtonPanel (wxEvent& event); + void onResizeConfigPanel (wxEvent& event); + void onResizeViewPanel (wxEvent& event); + void onToggleLog (wxCommandEvent& event) override; + void onCompare (wxCommandEvent& event) override; + void onStartSync (wxCommandEvent& event) override; + void onClose (wxCloseEvent& event) override; + void onSwapSides (wxCommandEvent& event) override { swapSides(); } + + void startSyncForSelecction(const std::vector& selection); + + void onCmpSettings (wxCommandEvent& event) override { showConfigDialog(SyncConfigPanel::compare, -1); } + void onSyncSettings (wxCommandEvent& event) override { showConfigDialog(SyncConfigPanel::sync, -1); } + void onConfigureFilter(wxCommandEvent& event) override { showConfigDialog(SyncConfigPanel::filter, -1); } + + void onCompSettingsContext (wxCommandEvent& event) override { onCompSettingsContext(static_cast(event)); } + void onCompSettingsContextMouse(wxMouseEvent& event) override { onCompSettingsContext(static_cast(event)); } + void onSyncSettingsContext (wxCommandEvent& event) override { onSyncSettingsContext(static_cast(event)); } + void onSyncSettingsContextMouse(wxMouseEvent& event) override { onSyncSettingsContext(static_cast(event)); } + void onGlobalFilterContext (wxCommandEvent& event) override { onGlobalFilterContext(static_cast(event)); } + void onGlobalFilterContextMouse(wxMouseEvent& event) override { onGlobalFilterContext(static_cast(event)); } + + void onCompSettingsContext(wxEvent& event); + void onSyncSettingsContext(wxEvent& event); + void onGlobalFilterContext(wxEvent& event); + + void showConfigDialog(SyncConfigPanel panelToShow, int localPairIndexToShow); + + void setLastOperationLog(const ProcessSummary& summary, const std::shared_ptr& errorLog); + void showLogPanel(bool show); + + void addFilterPhrase(const Zstring& phrase, bool include, bool requireNewLine); + + void onTopFolderPairAdd (wxCommandEvent& event) override; + void onTopFolderPairRemove(wxCommandEvent& event) override; + void onRemoveFolderPair (wxCommandEvent& event); + void onShowFolderPairOptions(wxEvent& event); + + void onTopLocalCompCfg (wxCommandEvent& event) override { showConfigDialog(SyncConfigPanel::compare, 0); } + void onTopLocalSyncCfg (wxCommandEvent& event) override { showConfigDialog(SyncConfigPanel::sync, 0); } + void onTopLocalFilterCfg(wxCommandEvent& event) override { showConfigDialog(SyncConfigPanel::filter, 0); } + + void onLocalCompCfg (wxCommandEvent& event); + void onLocalSyncCfg (wxCommandEvent& event); + void onLocalFilterCfg(wxCommandEvent& event); + + void onTopFolderPairKeyEvent(wxKeyEvent& event); + void onAddFolderPairKeyEvent(wxKeyEvent& event); + + void applyFilterConfig(); + void applySyncDirections(); + + void showFindPanel(bool show); //CTRL + F + void startFindNext(bool searchAscending); //F3 + + void resetLayout(); + + void onSearchGridEnter(wxCommandEvent& event) override; + void onHideSearchPanel(wxCommandEvent& event) override; + void onSearchPanelKeyPressed(wxKeyEvent& event); + + //menu events + void onOpenMenuTools(wxMenuEvent& event); + void onMenuOptions (wxCommandEvent& event) override; + void onMenuExportFileList (wxCommandEvent& event) override; + void onMenuResetLayout (wxCommandEvent& event) override { resetLayout(); } + void onMenuFindItem (wxCommandEvent& event) override { showFindPanel(true /*show*/); } //CTRL + F + void onMenuCheckVersion (wxCommandEvent& event) override; + void onMenuAbout (wxCommandEvent& event) override; + void onShowHelp (wxCommandEvent& event) override { wxLaunchDefaultBrowser(L"https://freefilesync.org/manual.php?topic=freefilesync"); } + void onMenuQuit (wxCommandEvent& event) override { Close(); } + + void switchProgramLanguage(wxLanguage langId); + + std::set detachedMenuItems_; //owning pointers!!! + //alternatives: 1. std::set>? key is const => no support for moving items out! 2. std::map>: redundant info, inconvenient use + + void clearGrid(ptrdiff_t pos = -1); + + //*********************************************** + //application variables are stored here: + + //global settings shared by GUI and batch mode + GlobalConfig globalCfg_; + + const Zstring globalCfgFilePath_; + + //------------------------------------- + //program configuration + FfsGuiConfig currentCfg_; //caveat: some parts are owned by GUI controls! see setConfig() + + //used when saving configuration + std::vector activeConfigFiles_; //name of currently loaded config files: NOT owned by m_gridCfgHistory, see onCfgGridSelection() + + FfsGuiConfig lastSavedCfg_; //support for: "Save changed configuration?" dialog + + const Zstring lastRunConfigPath_ = getLastRunConfigPath(); //let's not use another global... + //------------------------------------- + + //the prime data structure of this tool *bling*: + FolderComparison folderCmp_; //optional!: sync button not available if empty + + //merge logs of individual steps (comparison, manual operations, sync) into a combined result (just as for ffs_batch jobs) + struct FullSyncLog + { + zen::ErrorLog log; + std::chrono::system_clock::time_point startTime; + std::chrono::milliseconds totalTime{}; + }; + std::optional fullSyncLog_; + + //folder pairs: + std::unique_ptr firstFolderPair_; //always bound!!! + std::vector additionalFolderPairs_; //additional pairs to the first pair + + std::optional addPairCountLast_; + + //------------------------------------- + //fight sluggish GUI: FolderPairPanel are too expensive to casually throw away and recreate! + struct DeleteWxWindow { void operator()(wxWindow* win) const { win->Destroy(); } }; + + std::vector> folderPairScrapyard_; + //------------------------------------- + + //*********************************************** + //status bar center text + std::vector statusTxts_; //the first one is the original/non-flash status message + bool statusTxtHighlightFirst_ = false; + + //compare status panel (hidden on start, shown during comparison) + std::optional compareStatus_; //always bound + + LogPanel* logPanel_ = nullptr; + + //toggle to display configuration preview instead of comparison result: + //for read access use: m_bpButtonViewType->isActive() + //when changing value use: + void setGridViewType(GridViewType vt); + + wxAuiManager auiMgr_; //implement dockable GUI design + + wxString defaultPerspective_; + + time_t manualTimeSpanFrom_ = 0; + time_t manualTimeSpanTo_ = 0; //buffer manual time span selection at session level + + //recreate view filter button labels only when necessary: + std::unordered_map buttonLabelItemCount_; + + const std::shared_ptr folderHistoryLeft_; //shared by all wxComboBox dropdown controls + const std::shared_ptr folderHistoryRight_; // + + zen::AsyncGuiQueue guiQueue_; //schedule and run long-running tasks asynchronously, but process results on GUI queue + + wxWindowID focusAfterCloseLog_ = wxID_ANY; // + wxWindowID focusAfterCloseSearch_ = wxID_ANY; //restore focus after panel is closed + //don't save wxWindow* to arbitrary window: might not exist anymore when hideFindPanel() uses it!!! (e.g. some folder pair panel) + + //mitigate reentrancy: + bool localKeyEventsEnabled_ = true; + bool operationInProgress_ = false; //see SingleOperationBlocker; e.g. do NOT allow dialog exit while sync is running => crash!!! + + TempFileBuffer tempFileBuf_; //buffer temporary copies of non-native files for %local_path% + + const wxImage imgTrashSmall_; + const wxImage imgFileManagerSmall_; + + const zen::SharedRef> onBeforeSystemShutdownCookie_ = zen::makeSharedRef>([this] { onBeforeSystemShutdown(); }); +}; +} + +#endif //MAIN_DLG_H_8910481324545644545 diff --git a/FreeFileSync/Source/ui/progress_indicator.cpp b/FreeFileSync/Source/ui/progress_indicator.cpp new file mode 100644 index 0000000..64e16dd --- /dev/null +++ b/FreeFileSync/Source/ui/progress_indicator.cpp @@ -0,0 +1,1746 @@ +// ***************************************************************************** +// * 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 "progress_indicator.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "wx+/taskbar.h" +#include "gui_generated.h" +#include "tray_icon.h" +#include "log_panel.h" +#include "app_icon.h" +#include "../icon_buffer.h" +#include "../base/speed_test.h" + + +using namespace zen; +using namespace fff; + + +namespace +{ +constexpr std::chrono::seconds PERF_WINDOW_BYTES_PER_SEC (4); //window size used for statistics +constexpr std::chrono::seconds PERF_WINDOW_REMAINING_TIME(60); //USB memory stick can have 40-second-hangs +constexpr std::chrono::seconds SPEED_ESTIMATE_SAMPLE_SKIP(1); +constexpr std::chrono::milliseconds SPEED_ESTIMATE_UPDATE_INTERVAL(500); +constexpr std::chrono::seconds GRAPH_TOTAL_TIME_UPDATE_INTERVAL(2); + +const size_t PROGRESS_GRAPH_SAMPLE_SIZE_MAX = 2'500'000; //sizeof(CurveDataStatistics::Sample) == 16 byte key/value + +wxColor getColorBytes () { return wxSystemSettings::GetAppearance().IsDark() ? wxColor{0x16, 0xd2, 0x02} /*medium green*/ : wxColor{111, 255, 99} /*light green*/; } +wxColor getColorItems () { return wxSystemSettings::GetAppearance().IsDark() ? wxColor{0x53, 0x71, 0xfb} /*medium blue*/ : wxColor{127, 147, 255} /*light blue*/; } +wxColor getColorEstimate() { return wxSystemSettings::GetAppearance().IsDark() ? 0xc4c4c4 /*medium grey*/ : 0xf0f0f0 /*light grey*/; } +wxColor getColorEstimateText() { return 0x000000UL; } + + +std::wstring getDialogPhaseText(const Statistics& syncStat, bool paused) +{ + if (paused) + return _("Paused"); + + if (syncStat.taskCancelled()) + return _("Stop requested..."); + + switch (syncStat.currentPhase()) + { + case ProcessPhase::none: + return _("Initializing..."); //dialog is shown *before* sync starts, so this text may be visible! + case ProcessPhase::scan: + return _("Scanning..."); + case ProcessPhase::binaryCompare: + return _("Comparing content..."); + case ProcessPhase::sync: + return _("Synchronizing..."); + } + assert(false); + return std::wstring(); +} + + +class CurveDataProgressBar : public CurveData +{ +public: + CurveDataProgressBar(bool drawTop) : drawTop_(drawTop) {} + + void setFraction(double fraction) { fraction_ = fraction; } //value between [0, 1] + +private: + std::pair getRangeX() const override { return {0, 1}; } + + std::vector getPoints(double minX, double maxX, const wxSize& areaSizePx) const override + { + const double yLow = drawTop_ ? 1 : -1; //draw partially out of vertical bounds to not render top/bottom borders of the bars + const double yHigh = drawTop_ ? 3 : 1; // + + return + { + {0, yHigh}, + {fraction_, yHigh}, + {fraction_, yLow }, + {0, yLow }, + }; + } + + double fraction_ = 0; + const bool drawTop_; +}; + +class CurveDataProgressSeparatorLine : public CurveData +{ + std::pair getRangeX() const override { return {0, 1}; } + + std::vector getPoints(double minX, double maxX, const wxSize& areaSizePx) const override + { + return + { + {0, 1}, + {1, 1}, + }; + } +}; +} + + +class CompareProgressPanel::Impl : public CompareProgressDlgGenerated +{ +public: + explicit Impl(wxFrame& parentWindow); + + void init(const Statistics& syncStat, bool ignoreErrors, size_t autoRetryCount); //constructor/destructor semantics, but underlying Window is reused + void teardown(); // + + void initNewPhase(); + void updateProgressGui(bool allowYield); + + bool getOptionIgnoreErrors() const { return ignoreErrors_; } + void setOptionIgnoreErrors(bool ignoreErrors) { ignoreErrors_ = ignoreErrors; updateStaticGui(); } + + void timerSetStatus(bool active) + { + if (active) + stopWatch_.resume(); + else + stopWatch_.pause(); + } + bool timerIsRunning() const { return !stopWatch_.isPaused(); } + + std::chrono::milliseconds pauseAndGetTotalTime() + { + stopWatch_.pause(); + return std::chrono::duration_cast(stopWatch_.elapsed()); + } + +private: + //void onToggleIgnoreErrors(wxCommandEvent& event) override { updateStaticGui(); } + + void updateStaticGui(); + + wxFrame& parentWindow_; + wxString parentTitleBackup_; + + StopWatch stopWatch_; + std::chrono::nanoseconds phaseStart_{}; //begin of current phase + + const Statistics* syncStat_ = nullptr; //only bound while sync is running + + std::optional taskbar_; + SpeedTest remTimeTest_{PERF_WINDOW_REMAINING_TIME}; + SpeedTest speedTest_ {PERF_WINDOW_BYTES_PER_SEC}; + + std::chrono::nanoseconds timeLastSpeedEstimate_ = std::chrono::seconds(-100); //used for calculating intervals between showing and collecting perf samples + //initial value: just some big number + + SharedRef curveDataBytes_{makeSharedRef(true /*drawTop*/)}; + SharedRef curveDataItems_{makeSharedRef(false /*drawTop*/)}; + + bool ignoreErrors_ = false; +}; + + +CompareProgressPanel::Impl::Impl(wxFrame& parentWindow) : + CompareProgressDlgGenerated(&parentWindow), + parentWindow_(parentWindow) +{ + setImage(*m_bitmapItemStat, IconBuffer::genericFileIcon(IconBuffer::IconSize::small)); + setImage(*m_bitmapTimeStat, loadImage("time", -1 /*maxWidth*/, IconBuffer::getPixSize(IconBuffer::IconSize::small))); + m_bitmapTimeStat->SetMinSize({-1, screenToWxsize(IconBuffer::getPixSize(IconBuffer::IconSize::small))}); + + setImage(*m_bitmapErrors, loadImage("msg_error", dipToScreen(getMenuIconDipSize()))); + setImage(*m_bitmapWarnings, loadImage("msg_warning", dipToScreen(getMenuIconDipSize()))); + + setImage(*m_bitmapIgnoreErrors, loadImage("error_ignore_active", dipToScreen(getMenuIconDipSize()))); + setImage(*m_bitmapRetryErrors, loadImage("error_retry", dipToScreen(getMenuIconDipSize()))); + + //make sure standard height matches ProcessPhase::binaryCompare statistics layout (== largest) + + //init graph + m_panelProgressGraph->setAttributes(Graph2D::MainAttributes().setMinY(0).setMaxY(2). + setLabelX(XLabelPos::none). + setLabelY(YLabelPos::none). + setBaseColors(getColorEstimateText(), getColorEstimate()). + setSelectionMode(GraphSelMode::none)); + + const wxColor gridLineColor = m_panelProgressGraph->getAttributes().getGridLineColor(); + m_panelProgressGraph->addCurve(curveDataBytes_, Graph2D::CurveAttributes().setLineWidth(dipToWxsize(1)).fillPolygonArea(getColorBytes()).setColor(gridLineColor)); + m_panelProgressGraph->addCurve(curveDataItems_, Graph2D::CurveAttributes().setLineWidth(dipToWxsize(1)).fillPolygonArea(getColorItems()).setColor(gridLineColor)); + + m_panelProgressGraph->addCurve(makeSharedRef(), Graph2D::CurveAttributes().setLineWidth(dipToWxsize(1)).setColor(gridLineColor)); + + Layout(); + m_panelItemStats->Layout(); + m_panelTimeStats->Layout(); + m_panelErrorStats->Layout(); + + GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() +#ifdef __WXGTK3__ + //Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 + //Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404 +#endif +} + + +void CompareProgressPanel::Impl::init(const Statistics& syncStat, bool ignoreErrors, size_t autoRetryCount) +{ + assert(!syncStat_); + syncStat_ = &syncStat; + parentTitleBackup_ = parentWindow_.GetTitle(); + + try //try to get access to Windows 7/Ubuntu taskbar + { + taskbar_.emplace(this); //throw TaskbarNotAvailable + } + catch (const TaskbarNotAvailable&) {} + + stopWatch_ = StopWatch(); //reset to measure total time + + setText(*m_staticTextRetryCount, L'(' + formatNumber(autoRetryCount) + MULT_SIGN + L')'); + bSizerErrorsRetry->Show(autoRetryCount > 0); + + //allow changing a few options dynamically during sync + ignoreErrors_ = ignoreErrors; + + updateStaticGui(); + + initNewPhase(); +} + + +void CompareProgressPanel::Impl::teardown() +{ + assert(stopWatch_.isPaused()); //why wasn't pauseAndGetTotalTime() called? + + syncStat_ = nullptr; + parentWindow_.SetTitle(parentTitleBackup_); + taskbar_.reset(); +} + + +void CompareProgressPanel::Impl::initNewPhase() +{ + //start new measurement + remTimeTest_.clear(); + speedTest_ .clear(); + timeLastSpeedEstimate_ = std::chrono::seconds(-100); //make sure estimate is updated upon next check + phaseStart_ = stopWatch_.elapsed(); + + const int itemsTotal = syncStat_->getTotalStats().items; + const int64_t bytesTotal = syncStat_->getTotalStats().bytes; + + const bool haveTotalStats = itemsTotal >= 0 || bytesTotal >= 0; + + if (taskbar_) taskbar_->setStatus(haveTotalStats ? Taskbar::Status::normal : Taskbar::Status::indeterminate); + + m_staticTextProcessed ->Show(haveTotalStats); + m_staticTextRemaining ->Show(haveTotalStats); + m_staticTextItemsRemaining->Show(haveTotalStats); + m_staticTextBytesRemaining->Show(haveTotalStats); + m_staticTextTimeRemaining ->Show(haveTotalStats); + bSizerProgressGraph ->Show(haveTotalStats); + + Layout(); // + m_panelItemStats->Layout(); //redundant? can we trust updateProgressGui() to do the same after detecting "layoutChanged"? + m_panelTimeStats->Layout(); // + + updateProgressGui(false /*allowYield*/); +} + + +void CompareProgressPanel::Impl::updateStaticGui() +{ + bSizerErrorsIgnore->Show(ignoreErrors_); + Layout(); +} + + +void CompareProgressPanel::Impl::updateProgressGui(bool allowYield) +{ + assert(syncStat_); + if (!syncStat_) //no comparison running!? + return; + + auto setTitle = [&](const wxString& title) + { + if (parentWindow_.GetTitle() != title) + parentWindow_.SetTitle(title); + }; + + bool layoutChanged = false; //avoid screen flicker by calling layout() only if necessary + const std::chrono::nanoseconds timeElapsed = stopWatch_.elapsed(); + + const int itemsCurrent = syncStat_->getCurrentStats().items; + const int64_t bytesCurrent = syncStat_->getCurrentStats().bytes; + const int itemsTotal = syncStat_->getTotalStats ().items; + const int64_t bytesTotal = syncStat_->getTotalStats ().bytes; + + const bool haveTotalStats = itemsTotal >= 0 || bytesTotal >= 0; + + //status texts + setText(*m_staticTextStatus, replaceCpy(syncStat_->currentStatusText(), L'\n', L' ')); //no layout update for status texts! + + if (!haveTotalStats) + { + //dialog caption, taskbar + setTitle(formatNumber(itemsCurrent) + L' ' + getDialogPhaseText(*syncStat_, false /*paused*/)); + + //progress indicators + //taskbar_ already set to STATUS_INDETERMINATE by initNewPhase() + } + else + { + //add both bytes + item count, to handle "deletion-only" cases + const double fractionTotal = bytesTotal + itemsTotal == 0 ? 0 : 1.0 * (bytesCurrent + itemsCurrent) / (bytesTotal + itemsTotal); + const double fractionBytes = bytesTotal == 0 ? 0 : 1.0 * bytesCurrent / bytesTotal; + const double fractionItems = itemsTotal == 0 ? 0 : 1.0 * itemsCurrent / itemsTotal; + + //dialog caption, taskbar + setTitle(formatProgressPercent(fractionTotal) + L' ' + getDialogPhaseText(*syncStat_, false /*paused*/)); + + //progress indicators + if (taskbar_) taskbar_->setProgress(fractionTotal); + + curveDataBytes_.ref().setFraction(fractionBytes); + curveDataItems_.ref().setFraction(fractionItems); + } + + //item and data stats + if (!haveTotalStats) + { + setText(*m_staticTextItemsProcessed, formatNumber(itemsCurrent), &layoutChanged); + setText(*m_staticTextBytesProcessed, L"", &layoutChanged); + } + else + { + setText(*m_staticTextItemsProcessed, formatNumber(itemsCurrent), &layoutChanged); + setText(*m_staticTextBytesProcessed, L'(' + formatFilesizeShort(bytesCurrent) + L')', &layoutChanged); + + setText(*m_staticTextItemsRemaining, formatNumber(itemsTotal - itemsCurrent), &layoutChanged); + setText(*m_staticTextBytesRemaining, L'(' + formatFilesizeShort(bytesTotal - bytesCurrent) + L')', &layoutChanged); + } + + auto showIfNeeded = [&](wxWindow& wnd, bool show) + { + if (wnd.IsShown() != show) + { + wnd.Show(show); + layoutChanged = true; + } + }; + + //errors and warnings (pop up dynamically) + const Statistics::ErrorStats errorStats = syncStat_->getErrorStats(); + + showIfNeeded(*m_staticTextErrors, errorStats.errorCount != 0); + showIfNeeded(*m_staticTextWarnings, errorStats.warningCount != 0); + showIfNeeded(*m_panelErrorStats, errorStats.errorCount != 0 || errorStats.warningCount != 0); + + if (m_panelErrorStats->IsShown()) + { + showIfNeeded(*m_bitmapErrors, errorStats.errorCount != 0); + showIfNeeded(*m_staticTextErrorCount, errorStats.errorCount != 0); + + if (m_staticTextErrorCount->IsShown()) + setText(*m_staticTextErrorCount, formatNumber(errorStats.errorCount), &layoutChanged); + + showIfNeeded(*m_bitmapWarnings, errorStats.warningCount != 0); + showIfNeeded(*m_staticTextWarningCount, errorStats.warningCount != 0); + + if (m_staticTextWarningCount->IsShown()) + setText(*m_staticTextWarningCount, formatNumber(errorStats.warningCount), &layoutChanged); + } + + //current time elapsed + const int64_t timeElapSec = std::chrono::duration_cast(timeElapsed).count(); + + setText(*m_staticTextTimeElapsed, utfTo(formatTimeSpan(timeElapSec, false /*hourRequired*/)), &layoutChanged); + + if (haveTotalStats) //remaining time and speed: only visible during binary comparison + if (numeric::dist(timeLastSpeedEstimate_, timeElapsed) >= SPEED_ESTIMATE_UPDATE_INTERVAL) + { + timeLastSpeedEstimate_ = timeElapsed; + + if (numeric::dist(phaseStart_, timeElapsed) >= SPEED_ESTIMATE_SAMPLE_SKIP) //discard stats for first second: probably messy + { + remTimeTest_.addSample(timeElapsed, itemsCurrent, bytesCurrent); + speedTest_ .addSample(timeElapsed, itemsCurrent, bytesCurrent); + } + + //current speed -> Win 7 copy uses 1 sec update interval instead + m_panelProgressGraph->setAttributes(m_panelProgressGraph->getAttributes().setCornerText(speedTest_.getBytesPerSecFmt(), GraphCorner::topL)); + m_panelProgressGraph->setAttributes(m_panelProgressGraph->getAttributes().setCornerText(speedTest_.getItemsPerSecFmt(), GraphCorner::bottomL)); + + //remaining time: display with relative error of 10% - based on samples taken every 0.5 sec only + //-> call more often than once per second to correctly show last few seconds countdown, but don't call too often to avoid occasional jitter + std::optional remTimeSec = remTimeTest_.getRemainingSec(itemsTotal - itemsCurrent, bytesTotal - bytesCurrent); + setText(*m_staticTextTimeRemaining, remTimeSec ? formatRemainingTime(*remTimeSec) : std::wstring(1, EM_DASH), &layoutChanged); + } + + if (haveTotalStats) + m_panelProgressGraph->Refresh(); + + //adapt layout after content changes above + if (layoutChanged) + { + Layout(); + m_panelItemStats->Layout(); + m_panelTimeStats->Layout(); + if (m_panelErrorStats->IsShown()) + m_panelErrorStats->Layout(); + } + + //do the ui update + if (allowYield) + wxTheApp->Yield(); //pump GUI messages + else + this->Update(); //don't wait until next idle event (who knows what blocking process comes next?) +} + +//######################################################################################## + +//redirect to implementation +CompareProgressPanel::CompareProgressPanel(wxFrame& parentWindow) : pimpl_(new Impl(parentWindow)) {} //owned by parentWindow +wxWindow* CompareProgressPanel::getAsWindow() { return pimpl_; } +void CompareProgressPanel::init(const Statistics& syncStat, bool ignoreErrors, size_t autoRetryCount) { pimpl_->init(syncStat, ignoreErrors, autoRetryCount); } +void CompareProgressPanel::teardown() { pimpl_->teardown(); } +void CompareProgressPanel::initNewPhase() { pimpl_->initNewPhase(); } +void CompareProgressPanel::updateGui() { pimpl_->updateProgressGui(true /*allowYield*/); } +bool CompareProgressPanel::getOptionIgnoreErrors() const { return pimpl_->getOptionIgnoreErrors(); } +void CompareProgressPanel::setOptionIgnoreErrors(bool ignoreErrors) { pimpl_->setOptionIgnoreErrors(ignoreErrors); } +void CompareProgressPanel::timerSetStatus(bool active) { pimpl_->timerSetStatus(active); } +bool CompareProgressPanel::timerIsRunning() const { return pimpl_->timerIsRunning(); } +std::chrono::milliseconds CompareProgressPanel::pauseAndGetTotalTime() { return pimpl_->pauseAndGetTotalTime(); } +//######################################################################################## + +namespace +{ +class CurveDataStatistics : public SparseCurveData +{ +public: + CurveDataStatistics() : SparseCurveData(true /*addSteps*/) {} + + void clear() { samples_.clear(); lastSample_ = {}; } + + void addSample(double timeElapsed /*[sec]*/, double value /*[items|bytes]*/) + { + assert(( samples_.empty() && lastSample_.x == 0 && lastSample_.y == 0) || + (!samples_.empty() && samples_.back().x <= lastSample_.x)); + + if (timeElapsed < lastSample_.x) //time *required* to be monotonously ascending for std::partition_point + { + assert(false); + return; + } + + lastSample_ = {timeElapsed, value}; + + //allow for at most one sample per 100ms (handles duplicate inserts, too!) => unrelated to UI_UPDATE_INTERVAL! + if (!samples_.empty() && timeElapsed - samples_.back().x < 0.1) + return; + + samples_.push_back(CurvePoint{timeElapsed, value}); + + if (samples_.size() > PROGRESS_GRAPH_SAMPLE_SIZE_MAX) //limit buffer size + samples_.pop_front(); + } + +private: + std::pair getRangeX() const override + { + if (samples_.empty()) + return {}; + /* + //report some additional width by 5% elapsed time to make graph recalibrate before hitting the right border + //caveat: graph for batch mode binary comparison does NOT start at elapsed time 0!! ProcessPhase::binaryCompare and ProcessPhase::sync! + //=> consider width of current sample set! + upperEndMs += 0.05 *(upperEndMs - samples.begin()->first); + */ + return {samples_.front().x, //need not start with 0, e.g. "binary comparison, graph reset, followed by sync" + lastSample_.x}; + } + + std::optional getLessEq(double x) const override //x: seconds since begin + { + //--------- add artifical last sample value -------- + if (!samples_.empty() && lastSample_.x <= x) + return lastSample_; + //-------------------------------------------------- + + //find first item > x, then go one step back: + auto it = std::partition_point(samples_.begin(), samples_.end(), + /*find first item for which "!pred"*/ [x](const CurvePoint& p) { return p.x <= x; }); + if (it == samples_.begin()) + return std::nullopt; + --it; //bound! + return *it; + } + + std::optional getGreaterEq(double x) const override + { + //find first item >= x + const auto it = std::partition_point(samples_.begin(), samples_.end(), + /*find first item for which "!pred"*/ [x](const CurvePoint& p) { return p.x < x; }); + if (it != samples_.end()) + return *it; + + //--------- add artifical last sample value -------- + if (!samples_.empty() && x <= lastSample_.x) + return lastSample_; + //-------------------------------------------------- + return std::nullopt; + } + + RingBuffer samples_; //x: monotonously ascending with time! + CurvePoint lastSample_; //artificial record after end of samples to visualize current time! +}; + + +class CurveDataEstimate : public CurveData +{ +public: + void setValue(double x1, double x2, double y1, double y2) { x1_ = x1; x2_ = x2; y1_ = y1; y2_ = y2; } + void setTotalTime(double x2) { x2_ = x2; } + double getTotalTime() const { return x2_; } + +private: + std::pair getRangeX() const override { return {x1_, x2_}; } + + std::vector getPoints(double minX, double maxX, const wxSize& areaSizePx) const override + { + return + { + {x1_, y1_}, + {x2_, y2_}, + }; + } + + double x1_ = 0; //elapsed time [s] + double x2_ = 0; //total time [s] (estimated) + double y1_ = 0; //items/bytes processed + double y2_ = 0; //items/bytes total +}; + + +class CurveDataTimeMarker : public CurveData +{ +public: + void setValue(double x, double y) { x_ = x; y_ = y; } + void setTime(double x) { x_ = x; } + +private: + std::pair getRangeX() const override { return {x_, x_}; } + + std::vector getPoints(double minX, double maxX, const wxSize& areaSizePx) const override + { + return + { + {x_, y_}, + {x_, 0 }, + }; + } + + double x_ = 0; //time [s] + double y_ = 0; //items/bytes +}; + + +const double stretchDefaultBlockSize = 1.4; //enlarge block default size + + +struct LabelFormatterBytes : public LabelFormatter +{ + double getOptimalBlockSize(double bytesProposed) const override + { + bytesProposed *= stretchDefaultBlockSize; //enlarge block default size + + if (bytesProposed <= 1) //never smaller than 1 byte + return 1; + + //round to next number which is a convenient to read block size + const double k = std::floor(std::log(bytesProposed) / std::numbers::ln2); + const double e = std::pow(2.0, k); + if (numeric::isNull(e)) + return 0; + const double a = bytesProposed / e; //bytesProposed = a * 2^k with a in [1, 2) + assert(1 <= a && a < 2); + const double steps[] = {1, 2}; + return e * numeric::roundToGrid(a, std::begin(steps), std::end(steps)); + } + + wxString formatText(double value, double optimalBlockSize) const override { return formatFilesizeShort(static_cast(value)); } +}; + + +struct LabelFormatterItemCount : public LabelFormatter +{ + double getOptimalBlockSize(double itemsProposed) const override + { + itemsProposed *= stretchDefaultBlockSize; //enlarge block default size + + const double steps[] = {1, 2, 5, 10}; + if (itemsProposed <= 10) + return numeric::roundToGrid(itemsProposed, std::begin(steps), std::end(steps)); //like nextNiceNumber(), but without the 2.5 step! + return nextNiceNumber(itemsProposed); + } + + wxString formatText(double value, double optimalBlockSize) const override + { + return formatNumber(std::round(value)); //not enough room for a "%x items" representation + } +}; + + +struct LabelFormatterTimeElapsed : public LabelFormatter +{ + double getOptimalBlockSize(double secProposed) const override + { + //5 sec minimum block size + const double stepsSec[] = {5, 10, 20, 30, 60}; //nice numbers for seconds + if (secProposed <= 60) + return numeric::roundToGrid(secProposed, std::begin(stepsSec), std::end(stepsSec)); + + const double stepsMin[] = {1, 2, 5, 10, 15, 20, 30, 60}; //nice numbers for minutes + if (secProposed <= 3600) + return 60 * numeric::roundToGrid(secProposed / 60, std::begin(stepsMin), std::end(stepsMin)); + + if (secProposed <= 3600 * 24) + return 3600 * nextNiceNumber(secProposed / 3600); //round to full hours + + return 24 * 3600 * nextNiceNumber(secProposed / (24 * 3600)); //round to full days + } + + wxString formatText(double timeElapsed, double optimalBlockSize) const override + { + const int64_t timeElapsedSec = std::round(timeElapsed); + if (timeElapsedSec < 60) + return _P("1 sec", "%x sec", timeElapsedSec); + + return utfTo(formatTimeSpan(timeElapsedSec, false /*hourRequired*/)); + } +}; +} + + +template //can be a wxFrame or wxDialog +class SyncProgressDialogImpl : public TopLevelDialog, public SyncProgressDialog +/* we need derivation, not composition: + 1. SyncProgressDialogImpl IS a wxFrame/wxDialog + 2. implement virtual ~wxFrame() + 3. event handling below assumes lifetime is larger-equal than wxFrame's */ +{ +public: + SyncProgressDialogImpl(long style, //wxFrame/wxDialog style + const WindowLayout::Dimensions& dim, + const std::function& userRequestCancel, + const Statistics& syncStat, + wxFrame* parentFrame, + bool showProgress, + bool autoCloseDialog, + const std::vector& jobNames, + time_t syncStartTime, + bool ignoreErrors, + size_t autoRetryCount, + PostSyncAction postSyncAction); + + Result destroy(bool autoClose, bool restoreParentFrame, TaskResult syncResult, const SharedRef& log) override; + + wxWindow* getWindowIfVisible() override { return this->IsShown() ? this : nullptr; } + //workaround macOS bug: if "this" is used as parent window for a modal dialog then this dialog will erroneously un-hide its parent! + + void initNewPhase () override; + void notifyProgressChange() override; + void updateGui () override { updateProgressGui(true /*allowYield*/); } + + bool getOptionIgnoreErrors() const override { return ignoreErrors_; } + void setOptionIgnoreErrors(bool ignoreErrors) override { ignoreErrors_ = ignoreErrors; updateStaticGui(); } + PostSyncAction getAndFreezePostSyncAction() const override + { + pnl_.m_choicePostSyncAction->Disable(); + return enumPostSyncAction_.get(); + } + bool getOptionAutoCloseDialog() const override { return pnl_.m_checkBoxAutoClose->GetValue(); } + + void timerSetStatus(bool active) override + { + if (active) + stopWatch_.resume(); + else + stopWatch_.pause(); + } + + bool timerIsRunning() const override { return !stopWatch_.isPaused(); } + + std::chrono::milliseconds pauseAndGetTotalTime() override + { + stopWatch_.pause(); + return std::chrono::duration_cast(stopWatch_.elapsed()); + } + +private: + void onLocalKeyEvent (wxKeyEvent& event); + void onParentKeyEvent(wxKeyEvent& event); + void onPause (wxCommandEvent& event); + void onCancel (wxCommandEvent& event); + void onClose(wxCloseEvent& event); + void onIconize(wxIconizeEvent& event); + //void onToggleIgnoreErrors(wxCommandEvent& event) { updateStaticGui(); } + + void showSummary(TaskResult syncResult, const SharedRef& log); + + void minimizeToTray(); + void resumeFromSystray(bool userRequested); + + void updateStaticGui(); + void updateProgressGui(bool allowYield); + + void setExternalStatus(const wxString& status, const wxString& progress); //progress may be empty! + + SyncProgressPanelGenerated& pnl_; //wxPanel containing the GUI controls of *this + + const TimeComp syncStartTime_; + const wxString jobName_; + StopWatch stopWatch_; + + wxFrame* parentFrame_; //optional + + const std::function userRequestAbort_; //cancel button or dialog close + + //status variables + const Statistics* syncStat_; //valid only while sync is running + bool paused_ = false; + bool closePressed_ = false; + + //remaining time + SpeedTest remTimeTest_{PERF_WINDOW_REMAINING_TIME}; + SpeedTest speedTest_ {PERF_WINDOW_BYTES_PER_SEC}; + std::chrono::nanoseconds timeLastSpeedEstimate_ = std::chrono::seconds(-100); //used for calculating intervals between collecting perf samples + std::chrono::nanoseconds timeLastGraphTotalUpdate_ = std::chrono::seconds(-100); + + //help calculate total speed + std::chrono::nanoseconds phaseStart_{}; //begin of current phase + + SharedRef curveBytes_ = makeSharedRef(); + SharedRef curveItems_ = makeSharedRef(); + SharedRef curveBytesEstim_ = makeSharedRef(); + SharedRef curveItemsEstim_ = makeSharedRef(); + SharedRef curveBytesTimeNow_ = makeSharedRef(); + SharedRef curveItemsTimeNow_ = makeSharedRef(); + SharedRef curveBytesTimeEstim_ = makeSharedRef(); + SharedRef curveItemsTimeEstim_ = makeSharedRef(); + + const wxColor colorBytesRim_ = enhanceContrast(getColorBytes(), + wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW), 4.5 /*contrastRatioMin*/); //W3C recommends >= 4.5 for text + const wxColor colorItemsRim_ = enhanceContrast(getColorItems(), + wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW), 4.5 /*contrastRatioMin*/); //W3C recommends >= 4.5 for text + const wxColor colorEstimRim_ = enhanceContrast(getColorEstimate(), + wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW), 4.5 /*contrastRatioMin*/); //W3C recommends >= 4.5 for text + const wxColor colorBytesNow_ = enhanceContrast(getColorBytes(), getColorEstimate(), 4.5 /*contrastRatioMin*/); + const wxColor colorItemsNow_ = enhanceContrast(getColorItems(), getColorEstimate(), 4.5 /*contrastRatioMin*/); + + wxString parentTitleBackup_; + std::optional trayIcon_; //optional: if filled all other windows should be hidden and conversely + std::optional taskbar_; + + bool ignoreErrors_ = false; + EnumDescrList enumPostSyncAction_ + { + *pnl_.m_choicePostSyncAction, [this] + { + std::vector::DescrItem> descr; + descr.push_back({PostSyncAction::none, L"", {}}); + if (parentFrame_) //enable EXIT option for gui mode sync + descr.push_back({PostSyncAction::exit, wxControl::RemoveMnemonics(_("E&xit")), {}}); + descr.push_back({PostSyncAction::sleep, _("System: Sleep"), {}}); + descr.push_back({PostSyncAction::shutdown, _("System: Shut down"), {}}); + return descr; + }() + }; +}; + + +template +SyncProgressDialogImpl::SyncProgressDialogImpl(long style, //wxFrame/wxDialog style + const WindowLayout::Dimensions& dim, + const std::function& userRequestCancel, + const Statistics& syncStat, + wxFrame* parentFrame, + bool showProgress, + bool autoCloseDialog, + const std::vector& jobNames, + time_t syncStartTime, + bool ignoreErrors, + size_t autoRetryCount, + PostSyncAction postSyncAction) : + TopLevelDialog(parentFrame, wxID_ANY, wxString(), wxDefaultPosition, wxDefaultSize, style), //title is overwritten anyway in setExternalStatus() + pnl_(*new SyncProgressPanelGenerated(this)), //ownership passed to "this" + syncStartTime_(getLocalTime(syncStartTime)), //returns TimeComp() on error + 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; +} +()), +parentFrame_(parentFrame), +userRequestAbort_(userRequestCancel), +syncStat_(&syncStat) +{ + static_assert(std::is_same_v || + std::is_same_v); + assert((std::is_same_v == !parentFrame)); + //finish construction of this dialog: + this->pnl_.m_panelProgress->SetMinSize({dipToWxsize(550), dipToWxsize(340)}); + + wxBoxSizer* bSizer170 = new wxBoxSizer(wxVERTICAL); + bSizer170->Add(&pnl_, 1, wxEXPAND); + this->SetSizer(bSizer170); //pass ownership + + //lifetime of event sources is subset of this instance's lifetime => no wxEvtHandler::Unbind() needed + this->Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent& event) { onClose(event); }); + this->Bind(wxEVT_ICONIZE, [this](wxIconizeEvent& event) { onIconize(event); }); + this->Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onLocalKeyEvent(event); }); + + pnl_.m_buttonClose ->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](wxCommandEvent& event) { closePressed_ = true; }); + pnl_.m_buttonPause ->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](wxCommandEvent& event) { onPause(event); }); + pnl_.m_buttonStop ->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](wxCommandEvent& event) { onCancel(event); }); + pnl_.m_bpButtonMinimizeToTray->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](wxCommandEvent& event) { minimizeToTray(); }); + + if (parentFrame_) + parentFrame_->Bind(wxEVT_CHAR_HOOK, &SyncProgressDialogImpl::onParentKeyEvent, this); + + + assert(pnl_.m_buttonClose->GetId() == wxID_OK); //we cannot use wxID_CLOSE else ESC key won't work: yet another wxWidgets bug?? + + setRelativeFontSize(*pnl_.m_staticTextPhase, 1.5); + setRelativeFontSize(*pnl_.m_staticTextPercentTotal, 1.5); + + if (parentFrame_) + parentTitleBackup_ = parentFrame_->GetTitle(); //save old title (will be used as progress indicator) + + //pnl.m_animCtrlSyncing->SetAnimation(getResourceAnimation(L"working")); + //pnl.m_animCtrlSyncing->Play(); + + //this->EnableCloseButton(false); //this is NOT honored on OS X or with ALT+F4 on Windows! -> why disable button at all?? + + try //try to get access to Windows 7/Ubuntu taskbar + { + taskbar_.emplace(this); //throw TaskbarNotAvailable + } + catch (const TaskbarNotAvailable&) {} + + //hide until end of process: + pnl_.m_notebookResult ->Hide(); + pnl_.m_buttonClose ->Show(false); + //set std order after button visibility was set + setStandardButtonLayout(*pnl_.bSizerStdButtons, StdButtons().setAffirmative(pnl_.m_buttonPause).setCancel(pnl_.m_buttonStop)); + + setImage(*pnl_.m_bpButtonMinimizeToTray, loadImage("minimize_to_tray")); + + setImage(*pnl_.m_bitmapItemStat, IconBuffer::genericFileIcon(IconBuffer::IconSize::small)); + setImage(*pnl_.m_bitmapTimeStat, loadImage("time", -1 /*maxWidth*/, IconBuffer::getPixSize(IconBuffer::IconSize::small))); + pnl_.m_bitmapTimeStat->SetMinSize({-1, screenToWxsize(IconBuffer::getPixSize(IconBuffer::IconSize::small))}); + + setImage(*pnl_.m_bitmapErrors, loadImage("msg_error", dipToScreen(getMenuIconDipSize()))); + setImage(*pnl_.m_bitmapWarnings, loadImage("msg_warning", dipToScreen(getMenuIconDipSize()))); + + setImage(*pnl_.m_bitmapIgnoreErrors, loadImage("error_ignore_active", dipToScreen(getMenuIconDipSize()))); + setImage(*pnl_.m_bitmapRetryErrors, loadImage("error_retry", dipToScreen(getMenuIconDipSize()))); + + //init graph + const int xLabelHeight = this->GetCharHeight() + dipToWxsize(2) /*margin*/; //use same height for both graphs to make sure they stretch evenly + const int yLabelWidth = dipToWxsize(70); + pnl_.m_panelGraphBytes->setAttributes(Graph2D::MainAttributes(). + setLabelX(XLabelPos::top, xLabelHeight, std::make_shared()). + setLabelY(YLabelPos::right, yLabelWidth, std::make_shared()). + setBaseColors(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT), wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)). + setSelectionMode(GraphSelMode::none)); + + pnl_.m_panelGraphItems->setAttributes(Graph2D::MainAttributes(). + setLabelX(XLabelPos::bottom, xLabelHeight, std::make_shared()). + setLabelY(YLabelPos::right, yLabelWidth, std::make_shared()). + setBaseColors(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT), wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)). + setSelectionMode(GraphSelMode::none)); + + pnl_.m_panelGraphBytes->addCurve(curveBytes_, Graph2D::CurveAttributes().setLineWidth(dipToWxsize(1)).fillCurveArea(getColorBytes()).setColor(colorBytesRim_)); + pnl_.m_panelGraphItems->addCurve(curveItems_, Graph2D::CurveAttributes().setLineWidth(dipToWxsize(1)).fillCurveArea(getColorItems()).setColor(colorItemsRim_)); + + pnl_.m_panelGraphBytes->addCurve(curveBytesEstim_, Graph2D::CurveAttributes().setLineWidth(dipToWxsize(1)).fillCurveArea(getColorEstimate()).setColor(colorEstimRim_)); + pnl_.m_panelGraphItems->addCurve(curveItemsEstim_, Graph2D::CurveAttributes().setLineWidth(dipToWxsize(1)).fillCurveArea(getColorEstimate()).setColor(colorEstimRim_)); + + pnl_.m_panelGraphBytes->addCurve(curveBytesTimeNow_, Graph2D::CurveAttributes().setLineWidth(dipToWxsize(2)).setColor(colorBytesNow_)); + pnl_.m_panelGraphItems->addCurve(curveItemsTimeNow_, Graph2D::CurveAttributes().setLineWidth(dipToWxsize(2)).setColor(colorItemsNow_)); + + pnl_.m_panelGraphBytes->addCurve(curveBytesTimeEstim_, Graph2D::CurveAttributes().setLineWidth(dipToWxsize(2)).setColor(colorEstimRim_)); + pnl_.m_panelGraphItems->addCurve(curveItemsTimeEstim_, Graph2D::CurveAttributes().setLineWidth(dipToWxsize(2)).setColor(colorEstimRim_)); + + //graph legend: + const wxSize squareSize{this->GetCharHeight(), this->GetCharHeight()}; + setImage(*pnl_.m_bitmapGraphKeyBytes, rectangleImage({wxsizeToScreen(squareSize.x), wxsizeToScreen(squareSize.y)}, getColorBytes(), colorBytesRim_, dipToScreen(1))); + setImage(*pnl_.m_bitmapGraphKeyItems, rectangleImage({wxsizeToScreen(squareSize.x), wxsizeToScreen(squareSize.y)}, getColorItems(), colorItemsRim_, dipToScreen(1))); + + pnl_.bSizerDynSpace->SetMinSize(yLabelWidth, -1); //ensure item/time stats are nicely centered + + setText(*pnl_.m_staticTextRetryCount, L'(' + formatNumber(autoRetryCount) + MULT_SIGN + L')'); + pnl_.bSizerErrorsRetry->Show(autoRetryCount > 0); + + //allow changing a few options dynamically during sync + ignoreErrors_ = ignoreErrors; + + enumPostSyncAction_.set(postSyncAction); + + pnl_.m_checkBoxAutoClose->SetValue(autoCloseDialog); + + updateStaticGui(); //null-status will be shown while waiting for dir locks + + //make sure that standard height matches ProcessPhase::binaryCompare statistics layout (== largest) + + this->GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() +#ifdef __WXGTK3__ + this->Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 + //Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404 +#endif + pnl_.Layout(); + this->Center(); //call *after* dialog layout update and *before* wxWindow::Show()! + + WindowLayout::setInitial(*this, dim, this->GetSize() /*defaultSize*/); + + pnl_.m_buttonStop->SetDefault(); + + if (showProgress) + { + this->Show(); + //clear gui flicker, remove dummy texts: window must be visible to make this work! + updateProgressGui(true /*allowYield*/); //at least on OS X a real Yield() is required to flush pending GUI updates; Update() is not enough + + setFocusIfActive(*pnl_.m_buttonStop); //don't steal focus when starting in sys-tray! + } + else + minimizeToTray(); +} + + +template +void SyncProgressDialogImpl::onLocalKeyEvent(wxKeyEvent& event) +{ + switch (event.GetKeyCode()) + { + case WXK_ESCAPE: + { + wxButton& activeButton = pnl_.m_buttonStop->IsShown() ? *pnl_.m_buttonStop : *pnl_.m_buttonClose; + + wxCommandEvent dummy(wxEVT_COMMAND_BUTTON_CLICKED); + activeButton.Command(dummy); //simulate click + return; + } + } + + event.Skip(); +} + + +template +void SyncProgressDialogImpl::onParentKeyEvent(wxKeyEvent& event) +{ + //redirect keys from main dialog to progress dialog + switch (event.GetKeyCode()) + { + case WXK_ESCAPE: + this->SetFocus(); + this->onLocalKeyEvent(event); //event will be handled => no event recursion to parent dialog! + return; + } + + event.Skip(); +} + + +template +void SyncProgressDialogImpl::initNewPhase() +{ + updateStaticGui(); //evaluates "syncStat_->currentPhase()" + + //reset graphs (e.g. after binary comparison) + curveBytes_ .ref().clear(); + curveItems_ .ref().clear(); + curveBytesEstim_ .ref().setValue(0, 0, 0, 0); + curveItemsEstim_ .ref().setValue(0, 0, 0, 0); + curveBytesTimeNow_ .ref().setValue(0, 0); + curveItemsTimeNow_ .ref().setValue(0, 0); + curveBytesTimeEstim_.ref().setValue(0, 0); + curveItemsTimeEstim_.ref().setValue(0, 0); + + notifyProgressChange(); //make sure graphs get initial values + + //start new measurement + remTimeTest_.clear(); + speedTest_ .clear(); + timeLastGraphTotalUpdate_ = timeLastSpeedEstimate_ = std::chrono::seconds(-100); //make sure estimate is updated upon next check + phaseStart_ = stopWatch_.elapsed(); + + updateProgressGui(false /*allowYield*/); +} + + +template +void SyncProgressDialogImpl::notifyProgressChange() //noexcept! +{ + if (syncStat_) //sync running + { + const double timeElapsedDouble = std::chrono::duration(stopWatch_.elapsed()).count(); + const ProgressStats stats = syncStat_->getCurrentStats(); + curveBytes_.ref().addSample(timeElapsedDouble, stats.bytes); + curveItems_.ref().addSample(timeElapsedDouble, stats.items); + } +} + + +namespace +{ +} + + +template +void SyncProgressDialogImpl::setExternalStatus(const wxString& status, const wxString& progress) //progress may be empty! +{ + //sys tray: order "top-down": jobname, status, progress + wxString tooltip = L"FreeFileSync"; + if (!jobName_.empty()) + tooltip += SPACED_DASH + jobName_; + + tooltip += L'\n' + status; + + if (!progress.empty()) + tooltip += L' ' + progress; + + //window caption/taskbar; inverse order: progress, status, jobname + wxString title; + if (!progress.empty()) + title += progress + L' '; + + title += status; + + if (!jobName_.empty() && !parentFrame_ /*job name already visible in sync config panel, unlike with batch jobs*/) + title += SPACED_DASH + jobName_; + +#if 0 //why again does start time have to be visible in the title!? + const Zchar* format = [&tc = syncStartTime_] + { + if (const TimeComp& tcNow = getLocalTime(); + tc.day == tcNow.day && + tc.month == tcNow.month && + tc.year == tcNow.year) + return formatTimeTag; + return formatDateTimeTag; + }(); + title += SPACED_DASH + utfTo(formatTime(format, syncStartTime_)); +#endif + //--------------------------------------------------------------------------- + + //systray tooltip, if window is minimized + if (trayIcon_) + trayIcon_->setToolTip(tooltip); + + //top level dialog title also shows in Windows taskbar! + if (parentFrame_) + { + if (parentFrame_->GetTitle() != title) + parentFrame_->SetTitle(title); + } + else if (this->GetTitle() != title) + this->SetTitle(title); +} + + +template +void SyncProgressDialogImpl::updateProgressGui(bool allowYield) +{ + assert(syncStat_); + if (!syncStat_) //sync not running!? + return; + + //normally we don't update the "static" GUI components here, but we have to make an exception + //if sync is cancelled (by user or error handling option) + if (syncStat_->taskCancelled()) + updateStaticGui(); //called more than once after cancel... ok + + + const std::chrono::nanoseconds timeElapsed = stopWatch_.elapsed(); + const double timeElapsedDouble = std::chrono::duration(timeElapsed).count(); + + const int itemsCurrent = syncStat_->getCurrentStats().items; + const int64_t bytesCurrent = syncStat_->getCurrentStats().bytes; + const int itemsTotal = syncStat_->getTotalStats ().items; + const int64_t bytesTotal = syncStat_->getTotalStats ().bytes; + + const bool haveTotalStats = itemsTotal >= 0 || bytesTotal >= 0; + + bool headerLayoutChanged = false; + + //status texts + setText(*pnl_.m_staticTextStatus, replaceCpy(syncStat_->currentStatusText(), L'\n', L' ')); //no layout update for status texts! + + if (!haveTotalStats) + { + //dialog caption, taskbar, systray tooltip + setExternalStatus(getDialogPhaseText(*syncStat_, paused_), formatNumber(itemsCurrent)); //status text may be "paused"! + + //progress indicators + setText(*pnl_.m_staticTextPercentTotal, L"", &headerLayoutChanged); + + if (trayIcon_) trayIcon_->setProgress(1); //100% = fully visible FFS logo + //taskbar_ already set to STATUS_INDETERMINATE by initNewPhase() + } + else + { + //dialog caption, taskbar, systray tooltip + + const double fractionTotal = bytesTotal + itemsTotal == 0 ? 0 : 1.0 * (bytesCurrent + itemsCurrent) / (bytesTotal + itemsTotal); + //add both data + obj-count, to handle "deletion-only" cases + + const std::wstring percentTotal = formatProgressPercent(fractionTotal); + + setExternalStatus(getDialogPhaseText(*syncStat_, paused_), percentTotal); //status text may be "paused"! + + //progress indicators + setText(*pnl_.m_staticTextPercentTotal, L' ' + percentTotal, &headerLayoutChanged); + + if (trayIcon_) trayIcon_->setProgress(fractionTotal); + if (taskbar_ ) taskbar_ ->setProgress(fractionTotal); + + const double timeTotalSecTentative = bytesCurrent == bytesTotal ? timeElapsedDouble : std::max(curveBytesEstim_.ref().getTotalTime(), timeElapsedDouble); + + curveBytesEstim_.ref().setValue(timeElapsedDouble, timeTotalSecTentative, bytesCurrent, bytesTotal); + curveItemsEstim_.ref().setValue(timeElapsedDouble, timeTotalSecTentative, itemsCurrent, itemsTotal); + + //tentatively update total time, may be improved on below: + curveBytesTimeNow_.ref().setValue(timeElapsedDouble, bytesCurrent); + curveItemsTimeNow_.ref().setValue(timeElapsedDouble, itemsCurrent); + + curveBytesTimeEstim_.ref().setValue(timeTotalSecTentative, bytesTotal); + curveItemsTimeEstim_.ref().setValue(timeTotalSecTentative, itemsTotal); + } + + //even though notifyProgressChange() already set the latest data, let's add another sample to have all curves consider "timeNowMs" + //no problem with adding too many records: CurveDataStatistics will remove duplicate entries! + curveBytes_.ref().addSample(timeElapsedDouble, bytesCurrent); + curveItems_.ref().addSample(timeElapsedDouble, itemsCurrent); + + bool layoutChanged = false; //avoid screen flicker by calling layout() only if necessary + auto showIfNeeded = [&](wxWindow& wnd, bool show) + { + if (wnd.IsShown() != show) + { + wnd.Show(show); + layoutChanged = true; + } + }; + + //item and data stats + if (!haveTotalStats) + { + setText(*pnl_.m_staticTextItemsProcessed, formatNumber(itemsCurrent), &layoutChanged); + setText(*pnl_.m_staticTextBytesProcessed, L"", &layoutChanged); + + setText(*pnl_.m_staticTextItemsRemaining, std::wstring(1, EM_DASH), &layoutChanged); + setText(*pnl_.m_staticTextBytesRemaining, L"", &layoutChanged); + } + else + { + setText(*pnl_.m_staticTextItemsProcessed, formatNumber(itemsCurrent), &layoutChanged); + setText(*pnl_.m_staticTextBytesProcessed, L'(' + formatFilesizeShort(bytesCurrent) + L')', &layoutChanged); + + setText(*pnl_.m_staticTextItemsRemaining, formatNumber(itemsTotal - itemsCurrent), &layoutChanged); + setText(*pnl_.m_staticTextBytesRemaining, L'(' + formatFilesizeShort(bytesTotal - bytesCurrent) + L')', &layoutChanged); + //it's possible data remaining becomes shortly negative if last file synced has ADS data and the bytesTotal was not yet corrected! + } + + + //errors and warnings (pop up dynamically) + const Statistics::ErrorStats errorStats = syncStat_->getErrorStats(); + + showIfNeeded(*pnl_.m_staticTextErrors, errorStats.errorCount != 0); + showIfNeeded(*pnl_.m_staticTextWarnings, errorStats.warningCount != 0); + showIfNeeded(*pnl_.m_panelErrorStats, errorStats.errorCount != 0 || errorStats.warningCount != 0); + + if (pnl_.m_panelErrorStats->IsShown()) + { + showIfNeeded(*pnl_.m_bitmapErrors, errorStats.errorCount != 0); + showIfNeeded(*pnl_.m_staticTextErrorCount, errorStats.errorCount != 0); + + if (pnl_.m_staticTextErrorCount->IsShown()) + setText(*pnl_.m_staticTextErrorCount, formatNumber(errorStats.errorCount), &layoutChanged); + + showIfNeeded(*pnl_.m_bitmapWarnings, errorStats.warningCount != 0); + showIfNeeded(*pnl_.m_staticTextWarningCount, errorStats.warningCount != 0); + + if (pnl_.m_staticTextWarningCount->IsShown()) + setText(*pnl_.m_staticTextWarningCount, formatNumber(errorStats.warningCount), &layoutChanged); + } + + //current time elapsed + const int64_t timeElapSec = std::chrono::duration_cast(timeElapsed).count(); + + setText(*pnl_.m_staticTextTimeElapsed, utfTo(formatTimeSpan(timeElapSec, false /*hourRequired*/)), &layoutChanged); + + //remaining time and speed + if (numeric::dist(timeLastSpeedEstimate_, timeElapsed) >= SPEED_ESTIMATE_UPDATE_INTERVAL) + { + timeLastSpeedEstimate_ = timeElapsed; + + if (numeric::dist(phaseStart_, timeElapsed) >= SPEED_ESTIMATE_SAMPLE_SKIP) //discard stats for first second: probably messy + { + remTimeTest_.addSample(timeElapsed, itemsCurrent, bytesCurrent); + speedTest_ .addSample(timeElapsed, itemsCurrent, bytesCurrent); + } + + //current speed -> Win 7 copy uses 1 sec update interval instead + pnl_.m_panelGraphBytes->setAttributes(pnl_.m_panelGraphBytes->getAttributes().setCornerText(speedTest_.getBytesPerSecFmt(), GraphCorner::topL)); + pnl_.m_panelGraphItems->setAttributes(pnl_.m_panelGraphItems->getAttributes().setCornerText(speedTest_.getItemsPerSecFmt(), GraphCorner::topL)); + + //remaining time + if (!haveTotalStats) + { + setText(*pnl_.m_staticTextTimeRemaining, std::wstring(1, EM_DASH), &layoutChanged); + //ignore graphs: should already have been cleared in initNewPhase() + } + else + { + //remaining time: display with relative error of 10% - based on samples taken every 0.5 sec only + //-> call more often than once per second to correctly show last few seconds countdown, but don't call too often to avoid occasional jitter + std::optional remTimeSec = remTimeTest_.getRemainingSec(itemsTotal - itemsCurrent, bytesTotal - bytesCurrent); + setText(*pnl_.m_staticTextTimeRemaining, remTimeSec ? formatRemainingTime(*remTimeSec) : std::wstring(1, EM_DASH), &layoutChanged); + + const double timeRemainingSec = remTimeSec ? *remTimeSec : 0; + const double timeTotalSec = timeElapsedDouble + timeRemainingSec; + //update estimated total time marker only with precision of "20% remaining time" to avoid needless jumping around: + if (numeric::dist(curveBytesEstim_.ref().getTotalTime(), timeTotalSec) > 0.2 * timeRemainingSec) + { + //avoid needless flicker and don't update total time graph too often: + static_assert(std::chrono::duration_cast(GRAPH_TOTAL_TIME_UPDATE_INTERVAL).count() % SPEED_ESTIMATE_UPDATE_INTERVAL.count() == 0); + if (numeric::dist(timeLastGraphTotalUpdate_, timeElapsed) >= GRAPH_TOTAL_TIME_UPDATE_INTERVAL) + { + timeLastGraphTotalUpdate_ = timeElapsed; + + curveBytesEstim_.ref().setTotalTime(timeTotalSec); + curveItemsEstim_.ref().setTotalTime(timeTotalSec); + + curveBytesTimeEstim_.ref().setTime(timeTotalSec); + curveItemsTimeEstim_.ref().setTime(timeTotalSec); + } + } + } + } + + pnl_.m_panelGraphBytes->Refresh(); + pnl_.m_panelGraphItems->Refresh(); + + //adapt layout after content changes above + if (headerLayoutChanged) + pnl_.Layout(); + + if (layoutChanged) + { + pnl_.m_panelProgress->Layout(); + //small statistics panels: + pnl_.m_panelItemStats->Layout(); + pnl_.m_panelTimeStats->Layout(); + if (pnl_.m_panelErrorStats->IsShown()) + pnl_.m_panelErrorStats->Layout(); + } + + + if (allowYield) + { + if (paused_) //support for pause button + { + PauseTimers dummy(*this); + + while (paused_) + { + wxTheApp->Yield(); //receive UI message that ends pause + //*first* refresh GUI (removing flicker) before sleeping! + std::this_thread::sleep_for(UI_UPDATE_INTERVAL); + } + } + else + /* + /|\ + | keep this sequence to ensure one full progress update before entering pause mode! + \|/ + */ + wxTheApp->Yield(); //receive UI message that sets pause status OR forceful termination! + } + else + this->Update(); //don't wait until next idle event (who knows what blocking process comes next?) +} + + +template +void SyncProgressDialogImpl::updateStaticGui() //depends on "syncStat_, paused_" +{ + assert(syncStat_); + if (!syncStat_) + return; + + pnl_.m_staticTextPhase->SetLabelText(getDialogPhaseText(*syncStat_, paused_)); + //pnl_.m_bitmapStatus->SetToolTip(); -> redundant + + const wxImage statusImage = [&] + { + if (paused_) + return loadImage("status_pause"); + + if (syncStat_->taskCancelled()) + return loadImage("result_error"); + + switch (syncStat_->currentPhase()) + { + case ProcessPhase::none: + case ProcessPhase::scan: + return loadImage("status_scanning"); + case ProcessPhase::binaryCompare: + return loadImage("status_binary_compare"); + case ProcessPhase::sync: + return loadImage("status_syncing"); + } + assert(false); + return wxNullImage; + }(); + setImage(*pnl_.m_bitmapStatus, statusImage); + + //show status on Windows 7 taskbar + if (taskbar_) + { + if (paused_) + taskbar_->setStatus(Taskbar::Status::paused); + else + { + const int itemsTotal = syncStat_->getTotalStats().items; + const int64_t bytesTotal = syncStat_->getTotalStats().bytes; + + const bool haveTotalStats = itemsTotal >= 0 || bytesTotal >= 0; + + taskbar_->setStatus(haveTotalStats ? Taskbar::Status::normal : Taskbar::Status::indeterminate); + } + } + + //pause button + pnl_.m_buttonPause->SetLabel(paused_ ? _("&Continue") : _("&Pause")); + + pnl_.bSizerErrorsIgnore->Show(ignoreErrors_); + + pnl_.Layout(); + pnl_.m_panelProgress->Layout(); //for bSizerErrorsIgnore + //this->Refresh(); //a few pixels below the status text need refreshing -> still needed? +} + + +template +void SyncProgressDialogImpl::showSummary(TaskResult syncResult, const SharedRef& log) +{ + assert(syncStat_); + //at the LATEST(!) to prevent access to currentStatusHandler + //enable okay and close events; may be set in this method ONLY + + paused_ = false; //you never know? + + //update numbers one last time (as if sync were still running) + notifyProgressChange(); //make one last graph entry at the *current* time + updateProgressGui(false /*allowYield*/); + //=================================================================================== + + const int itemsProcessed = syncStat_->getCurrentStats().items; + const int64_t bytesProcessed = syncStat_->getCurrentStats().bytes; + const int itemsTotal = syncStat_->getTotalStats ().items; + const int64_t bytesTotal = syncStat_->getTotalStats ().bytes; + + //set overall speed (instead of current speed) + const double timeDelta = std::chrono::duration(stopWatch_.elapsed() - phaseStart_).count(); + //we need to consider "time within current phase" not total "timeElapsed"! + + const wxString overallBytesPerSecond = numeric::isNull(timeDelta) ? std::wstring() : + replaceCpy(_("%x/sec"), L"%x", formatFilesizeShort(std::round(bytesProcessed / timeDelta))); + const wxString overallItemsPerSecond = numeric::isNull(timeDelta) ? std::wstring() : + replaceCpy(_("%x/sec"), L"%x", replaceCpy(_("%x items"), L"%x", formatThreeDigitPrecision(itemsProcessed / timeDelta))); + + pnl_.m_panelGraphBytes->setAttributes(pnl_.m_panelGraphBytes->getAttributes().setCornerText(overallBytesPerSecond, GraphCorner::topL)); + pnl_.m_panelGraphItems->setAttributes(pnl_.m_panelGraphItems->getAttributes().setCornerText(overallItemsPerSecond, GraphCorner::topL)); + + //...if everything was processed successfully + if (itemsTotal >= 0 && bytesTotal >= 0 && //itemsTotal < 0 && bytesTotal < 0 => e.g. cancel during folder comparison + itemsProcessed == itemsTotal && + bytesProcessed == bytesTotal) + { + pnl_.m_staticTextPercentTotal->Hide(); + + pnl_.m_staticTextProcessed ->Hide(); + pnl_.m_staticTextRemaining ->Hide(); + pnl_.m_staticTextItemsRemaining->Hide(); + pnl_.m_staticTextBytesRemaining->Hide(); + pnl_.m_staticTextTimeRemaining ->Hide(); + } + + //generally not interesting anymore (e.g. items > 0 due to skipped errors) + pnl_.m_staticTextTimeRemaining->Hide(); + + const int64_t totalTimeSec = std::chrono::duration_cast(stopWatch_.elapsed()).count(); + pnl_.m_staticTextTimeElapsed->SetLabelText(utfTo(formatTimeSpan(totalTimeSec))); + //hourOptional? -> let's use full precision for max. clarity: https://freefilesync.org/forum/viewtopic.php?t=6308 + + + resumeFromSystray(false /*userRequested*/); //if in tray mode... + + //------- change class state ------- + syncStat_ = nullptr; + //---------------------------------- + + const wxImage statusImage = [&] + { + switch (syncResult) + { + case TaskResult::success: + return loadImage("result_success"); + case TaskResult::warning: + return loadImage("result_warning"); + case TaskResult::error: + case TaskResult::cancelled: + return loadImage("result_error"); + } + assert(false); + return wxNullImage; + }(); + setImage(*pnl_.m_bitmapStatus, statusImage); + + pnl_.m_staticTextPhase->SetLabelText(getSyncResultLabel(syncResult)); + + //pnl_.m_bitmapStatus->SetToolTip(); -> redundant + + //show status on Windows 7 taskbar + if (taskbar_) + switch (syncResult) + { + case TaskResult::success: + taskbar_->setStatus(Taskbar::Status::normal); + break; + + case TaskResult::warning: + taskbar_->setStatus(Taskbar::Status::warning); + break; + + case TaskResult::error: + case TaskResult::cancelled: + taskbar_->setStatus(Taskbar::Status::error); + break; + } + //---------------------------------- + + setExternalStatus(getSyncResultLabel(syncResult), wxString()); + + //this->EnableCloseButton(true); + + pnl_.m_bpButtonMinimizeToTray->Hide(); + pnl_.m_buttonStop->Disable(); + pnl_.m_buttonStop->Hide(); + pnl_.m_buttonPause->Disable(); + pnl_.m_buttonPause->Hide(); + pnl_.m_buttonClose->Show(); + pnl_.m_buttonClose->Enable(); + + pnl_.bSizerProgressFooter->Show(false); + + if (!parentFrame_) //hide checkbox for batch mode sync (where value won't be retrieved after close) + pnl_.m_checkBoxAutoClose->Hide(); + + //set std order after button visibility was set + setStandardButtonLayout(*pnl_.bSizerStdButtons, StdButtons().setAffirmative(pnl_.m_buttonClose)); + + //hide current operation status + pnl_.bSizerStatusText->Show(false); + + pnl_.m_staticlineFooter->Hide(); //win: m_notebookResult already has a window frame + + //------------------------------------------------------------- + + pnl_.m_notebookResult->SetPadding(wxSize(dipToWxsize(2), 0)); //height cannot be changed + + //1. re-arrange graph into results listbook + const size_t pagePosProgress = 0; + const size_t pagePosLog = 1; + + [[maybe_unused]] const bool wasDetached = pnl_.bSizerRoot->Detach(pnl_.m_panelProgress); + assert(wasDetached); + pnl_.m_panelProgress->Reparent(pnl_.m_notebookResult); + pnl_.m_notebookResult->AddPage(pnl_.m_panelProgress, _("Progress"), true /*bSelect*/); + + //2. log file + assert(pnl_.m_notebookResult->GetPageCount() == 1); + LogPanel* logPanel = new LogPanel(pnl_.m_notebookResult); //owned by m_notebookResult + logPanel->setLog(log.ptr()); + pnl_.m_notebookResult->AddPage(logPanel, _("Log"), false /*bSelect*/); + + //show log instead of graph if errors occurred! (not required for ignored warnings) + const ErrorLogStats logCount = getStats(log.ref()); + if (logCount.errors > 0) + pnl_.m_notebookResult->ChangeSelection(pagePosLog); + + //fill image list to cope with wxNotebook image setting design desaster... + const int imgListSize = dipToWxsize(16); //also required by GTK => don't use getMenuIconDipSize() + auto imgList = std::make_unique(imgListSize, imgListSize); + + imgList->Add(toScaledBitmap(loadImage("progress", wxsizeToScreen(imgListSize)))); + imgList->Add(toScaledBitmap(loadImage("log_file", wxsizeToScreen(imgListSize)))); + + pnl_.m_notebookResult->AssignImageList(imgList.release()); //pass ownership + + pnl_.m_notebookResult->SetPageImage(pagePosProgress, pagePosProgress); + pnl_.m_notebookResult->SetPageImage(pagePosLog, pagePosLog); + + //Caveat: we need "Show()" *after" the above wxNotebook::ChangeSelection() to get the correct selection on Linux + pnl_.m_notebookResult->Show(); + + //GetSizer()->SetSizeHints(this); //~=Fit() //not a good idea: will shrink even if window is maximized or was enlarged by the user + pnl_.Layout(); + + pnl_.m_panelProgress->Layout(); + //small statistics panels: + pnl_.m_panelItemStats->Layout(); + pnl_.m_panelTimeStats->Layout(); + if (pnl_.m_panelErrorStats->IsShown()) + pnl_.m_panelErrorStats->Layout(); + + //this->Raise(); -> don't! user may be watching a movie in the meantime ;) + + pnl_.m_buttonClose->SetDefault(); + setFocusIfActive(*pnl_.m_buttonClose); +} + + +template +auto SyncProgressDialogImpl::destroy(bool autoClose, bool restoreParentFrame, TaskResult syncResult, const SharedRef& log) -> Result +{ + assert(stopWatch_.isPaused()); //why wasn't pauseAndGetTotalTime() called? + + if (autoClose) + { + assert(syncStat_); + + //ATTENTION: dialog may live a little longer, so watch callbacks! + //e.g. wxGTK calls onIconize after wxWindow::Close() (better not ask why) and before physical destruction! => indirectly calls updateStaticGui(), which reads syncStat_!!! + syncStat_ = nullptr; + } + else + { + showSummary(syncResult, log); + + //wait until user closes the dialog by pressing "Close" + while (!closePressed_) + { + wxTheApp->Yield(); //refresh GUI *first* before sleeping! (remove flicker) + std::this_thread::sleep_for(UI_UPDATE_INTERVAL); + } + restoreParentFrame = true; + } + //------------------------------------------------------------------------ + + if (parentFrame_) + { + [[maybe_unused]] bool ubOk = parentFrame_->Unbind(wxEVT_CHAR_HOOK, &SyncProgressDialogImpl::onParentKeyEvent, this); + assert(ubOk); + + parentFrame_->SetTitle(parentTitleBackup_); //restore title text + + if (restoreParentFrame) + { + //make sure main dialog is shown again if still "minimized to systray"! + parentFrame_->Show(); + //if (parentFrame_->IsIconized()) //caveat: if window is maximized calling Iconize(false) will erroneously un-maximize! + // parentFrame_->Iconize(false); + } + } + //else: don't call transformAppType(): consider "switch to main dialog" option during silent batch run + + //------------------------------------------------------------------------ + const bool autoCloseDialog = getOptionAutoCloseDialog(); + + const WindowLayout::Dimensions dims = WindowLayout::getBeforeClose(*this); + + this->Destroy(); //wxWidgets macOS: simple "delete"!!!!!!! + + return {autoCloseDialog, dims}; +} + + +template +void SyncProgressDialogImpl::onClose(wxCloseEvent& event) +{ + assert(event.CanVeto()); //this better be true: if "this" is parent of a modal error dialog, there is NO way (in hell) we allow destruction here!!! + //wxEVT_END_SESSION is already handled by application.cpp::onSystemShutdown()! + event.Veto(); + + closePressed_ = true; //"temporary" auto-close: preempt closing results dialog + + if (syncStat_) + { + //user closing dialog => cancel sync + auto-close dialog + userRequestAbort_(); + + paused_ = false; //[!] we could be pausing here! + updateStaticGui(); //update status + pause button + } +} + + +template +void SyncProgressDialogImpl::onCancel(wxCommandEvent& event) +{ + userRequestAbort_(); + + paused_ = false; + updateStaticGui(); //update status + pause button + //no UI-update here to avoid cascaded Yield()-call! +} + + +template +void SyncProgressDialogImpl::onPause(wxCommandEvent& event) +{ + paused_ = !paused_; + updateStaticGui(); //update status + pause button +} + + +template +void SyncProgressDialogImpl::onIconize(wxIconizeEvent& event) +{ + /* propagate progress dialog minimize/maximize to parent + ----------------------------------------------------- + Fedora/Debian/Ubuntu: + - wxDialog cannot be minimized + - worse, wxGTK sends stray iconize events *after* wxDialog::Destroy() + - worse, on Fedora an iconize event is issued directly after calling Close() + - worse, even wxDialog::Hide() causes iconize event! + => nothing to do + SUSE: + - wxDialog can be minimized (it just vanishes!) and in general also minimizes parent: except for our progress wxDialog!!! + - worse, wxDialog::Hide() causes iconize event + - probably the same issues with stray iconize events like Fedora/Debian/Ubuntu + - minimize button is always shown, even if wxMINIMIZE_BOX is omitted! + => nothing to do + macOS: + - wxDialog can be minimized but does not also minimize parent + => propagate event to parent + Windows: + - wxDialog can be minimized but does not also minimize parent + - iconize events only seen for manual minimize + => propagate event to parent */ + event.Skip(); +} + + +template +void SyncProgressDialogImpl::minimizeToTray() +{ + if (!trayIcon_) + { + trayIcon_.emplace([this] { this->resumeFromSystray(true /*userRequested*/); }); //FfsTrayIcon lifetime is a subset of "this"'s lifetime! + //we may destroy FfsTrayIcon even while in the FfsTrayIcon callback!!!! + + updateProgressGui(false /*allowYield*/); //set tray tooltip + progress: e.g. no updates while paused + + +#warning("need delay for minimize animation to play out?") + //std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + + this->Hide(); + if (parentFrame_) + parentFrame_->Hide(); + } +} + + +template +void SyncProgressDialogImpl::resumeFromSystray(bool userRequested) +{ + if (trayIcon_) + { + trayIcon_.reset(); + + if (parentFrame_) + parentFrame_->Show(); + this->Show(); + + updateStaticGui(); //restore Windows 7 task bar status (e.g. required in pause mode) + updateProgressGui(false /*allowYield*/); //restore Windows 7 task bar progress (e.g. required in pause mode) + + if (userRequested) + { + if (parentFrame_) + parentFrame_->Raise(); + this->Raise(); + pnl_.m_bpButtonMinimizeToTray->SetFocus(); + } + } +} + +//######################################################################################## + +SyncProgressDialog* SyncProgressDialog::create(const WindowLayout::Dimensions& dim, + const std::function& userRequestCancel, + const Statistics& syncStat, + wxFrame* parentWindow, //may be nullptr + bool showProgress, + bool autoCloseDialog, + const std::vector& jobNames, + time_t syncStartTime, + bool ignoreErrors, + size_t autoRetryCount, + PostSyncAction postSyncAction) +{ + if (parentWindow) //FFS GUI sync + return new SyncProgressDialogImpl(wxDEFAULT_DIALOG_STYLE | wxMAXIMIZE_BOX | wxMINIMIZE_BOX | wxRESIZE_BORDER, + dim, userRequestCancel, syncStat, parentWindow, showProgress, + autoCloseDialog, jobNames, syncStartTime, ignoreErrors, autoRetryCount, postSyncAction); + else //FFS batch job + { + auto dlg = new SyncProgressDialogImpl(wxDEFAULT_FRAME_STYLE, + dim, userRequestCancel, syncStat, parentWindow, showProgress, + autoCloseDialog, jobNames, syncStartTime, ignoreErrors, autoRetryCount, postSyncAction); + dlg->SetIcon(getFfsIcon()); //only top level windows should have an icon + return dlg; + } +} diff --git a/FreeFileSync/Source/ui/progress_indicator.h b/FreeFileSync/Source/ui/progress_indicator.h new file mode 100644 index 0000000..5fae12f --- /dev/null +++ b/FreeFileSync/Source/ui/progress_indicator.h @@ -0,0 +1,110 @@ +// ***************************************************************************** +// * 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 PROGRESS_INDICATOR_H_8037493452348 +#define PROGRESS_INDICATOR_H_8037493452348 + +#include +#include +#include "wx+/window_tools.h" +#include "../status_handler.h" + + +namespace fff +{ +class CompareProgressPanel +{ +public: + explicit CompareProgressPanel(wxFrame& parentWindow); //CompareProgressPanel will be owned by parentWindow! + + wxWindow* getAsWindow(); //convenience! don't abuse! + + void init(const Statistics& syncStat, bool ignoreErrors, size_t autoRetryCount); //begin of sync: make visible, set pointer to "syncStat", initialize all status values + void teardown(); //end of sync: hide again, clear pointer to "syncStat" + + void initNewPhase(); //call after "StatusHandler::initNewPhase" + + void updateGui(); + + //allow changing a few options dynamically during sync + bool getOptionIgnoreErrors() const; + void setOptionIgnoreErrors(bool ignoreError); + + void timerSetStatus(bool active); //start/stop all internal timers! + bool timerIsRunning() const; + std::chrono::milliseconds pauseAndGetTotalTime(); + +private: + class Impl; + Impl* const pimpl_; +}; + + +//StatusHandlerFloatingDialog will internally process Window messages => disable GUI controls to avoid unexpected callbacks! + +enum class PostSyncAction +{ + none, + exit, + sleep, + shutdown +}; + +struct SyncProgressDialog +{ + static SyncProgressDialog* create(const zen::WindowLayout::Dimensions& dim, + const std::function& userRequestCancel, + const Statistics& syncStat, + wxFrame* parentWindow, //may be nullptr + bool showProgress, + bool autoCloseDialog, + const std::vector& jobNames, + time_t syncStartTime, + bool ignoreErrors, + size_t autoRetryCount, + PostSyncAction postSyncAction); + struct Result + { + bool autoCloseDialog; + zen::WindowLayout::Dimensions dim; + }; + virtual Result destroy(bool autoClose, bool restoreParentFrame, TaskResult syncResult, const zen::SharedRef& log) = 0; + //--------------------------------------------------------------------------- + + virtual wxWindow* getWindowIfVisible() = 0; //may be nullptr; don't abuse, use as parent for modal dialogs only! + + virtual void initNewPhase () = 0; //call after "StatusHandler::initNewPhase" + virtual void notifyProgressChange() = 0; //noexcept, required by graph! + virtual void updateGui () = 0; //update GUI and process Window messages + + //allow changing a few options dynamically during sync + virtual bool getOptionIgnoreErrors() const = 0; + virtual void setOptionIgnoreErrors(bool ignoreError) = 0; + virtual PostSyncAction getAndFreezePostSyncAction() const = 0; + virtual bool getOptionAutoCloseDialog() const = 0; + + virtual void timerSetStatus(bool active) = 0; //start/stop all internal timers! + virtual bool timerIsRunning() const = 0; + virtual std::chrono::milliseconds pauseAndGetTotalTime() = 0; + +protected: + ~SyncProgressDialog() {} +}; + + +template +class PauseTimers +{ +public: + explicit PauseTimers(ProgressDlg& ss) : ss_(ss), timerWasRunning_(ss.timerIsRunning()) { ss_.timerSetStatus(false); } + ~PauseTimers() { ss_.timerSetStatus(timerWasRunning_); } //restore previous state: support recursive calls +private: + ProgressDlg& ss_; + const bool timerWasRunning_; +}; +} + +#endif //PROGRESS_INDICATOR_H_8037493452348 diff --git a/FreeFileSync/Source/ui/rename_dlg.cpp b/FreeFileSync/Source/ui/rename_dlg.cpp new file mode 100644 index 0000000..64051a6 --- /dev/null +++ b/FreeFileSync/Source/ui/rename_dlg.cpp @@ -0,0 +1,437 @@ +// ***************************************************************************** +// * 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 "rename_dlg.h" +#include +//#include +#include +#include +#include +#include "gui_generated.h" +#include "../base/multi_rename.h" + + +using namespace zen; +using namespace fff; + + +namespace +{ +enum class ColumnTypeRename +{ + oldName, + newName, +}; + + +class GridDataRename : public GridData +{ +public: + GridDataRename(const std::vector& fileNamesOld, + const SharedRef& renameBuf) : + fileNamesOld_(fileNamesOld), + renameBuf_(renameBuf) {} + + bool updatePreview(std::wstring_view renamePhrase, size_t selectBegin, size_t selectEnd) //support polling + { + //normalize input: trim and adapt selection + { + const std::wstring_view renamePhraseTrm = trimCpy(renamePhrase); + + if (selectBegin <= selectEnd && selectEnd <= renamePhrase.size()) + { + selectBegin -= std::min(selectBegin, makeUnsigned(renamePhraseTrm.data() - renamePhrase.data())); //careful: + selectEnd -= std::min(selectEnd, makeUnsigned(renamePhraseTrm.data() - renamePhrase.data())); //avoid underflow + + selectBegin = std::min(selectBegin, renamePhraseTrm.size()); + selectEnd = std::min(selectEnd, renamePhraseTrm.size()); + } + else + { + assert(false); + selectBegin = selectEnd = 0; + } + + renamePhrase = renamePhraseTrm; + } + + auto currentPhrase = std::make_tuple(renamePhrase, selectBegin, selectEnd); + if (currentPhrase != lastUsedPhrase_) //only update when needed + { + lastUsedPhrase_ = currentPhrase; + + fileNamesNewSelectBefore_ = resolvePlaceholderPhrase(renamePhrase.substr(0, selectBegin), renameBuf_.ref()); + fileNamesNewSelected_ = resolvePlaceholderPhrase(renamePhrase.substr(selectBegin, selectEnd - selectBegin), renameBuf_.ref()); + fileNamesNewSelectAfter_ = resolvePlaceholderPhrase(renamePhrase.substr(selectEnd), renameBuf_.ref()); + + assert(fileNamesNewSelectBefore_.size() == fileNamesOld_.size()); + assert(fileNamesNewSelected_ .size() == fileNamesOld_.size()); + assert(fileNamesNewSelectAfter_ .size() == fileNamesOld_.size()); + + previewChangeTime_ = std::chrono::steady_clock::now(); + return true; + } + else + return false; + } + + std::vector getNewNames() const { return resolvePlaceholderPhrase(std::get(lastUsedPhrase_), renameBuf_.ref()); } + + size_t getRowCount() const override { return fileNamesOld_.size(); } + + std::wstring getValue(size_t row, ColumnType colType) const override + { + if (row < fileNamesOld_.size()) + switch (static_cast(colType)) + { + case ColumnTypeRename::oldName: + return fileNamesOld_[row]; + + case ColumnTypeRename::newName: + return fileNamesNewSelectBefore_[row] + fileNamesNewSelected_[row] + fileNamesNewSelectAfter_[row]; + } + return std::wstring(); + } + + void renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected, HoverArea rowHover) override + { + //clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); -> already the default + } + + void renderCell(wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover) override + { + //draw border on right + clearArea(dc, {rect.x + rect.width - dipToWxsize(1), rect.y, dipToWxsize(1), rect.height}, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNSHADOW)); + + wxRect rectTmp = rect; + rectTmp.x += getColumnGapLeft(); + rectTmp.width -= getColumnGapLeft() + dipToWxsize(1); + + switch (static_cast(colType)) + { + case ColumnTypeRename::oldName: + drawCellText(dc, rectTmp, getValue(row, colType)); + break; + + case ColumnTypeRename::newName: + { + const std::wstring& fulltext = fileNamesNewSelectBefore_[row] + fileNamesNewSelected_[row] + fileNamesNewSelectAfter_[row]; + //macOS: drawCellText() is not accurate for partial strings => draw full text + calculate deltas: + const wxSize extentBefore = dc.GetTextExtent(fileNamesNewSelectBefore_[row]); + const wxSize extentFullText = dc.GetTextExtent(fulltext); + + drawCellText(dc, rectTmp, fulltext, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, &extentFullText); + + if (!fileNamesNewSelected_[row].empty()) //highlight text selection: + { + const wxSize extentBeforeAndSel = dc.GetTextExtent(fileNamesNewSelectBefore_[row] + fileNamesNewSelected_[row]); + + const wxRect rectSel{rectTmp.x + extentBefore.GetWidth(), + rectTmp.y, + extentBeforeAndSel.GetWidth() - extentBefore.GetWidth(), + rectTmp.height}; + + clearArea(dc, rectSel, wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHT)); + + RecursiveDcClipper dummy(dc, rectSel); + + wxDCTextColourChanger textColor(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHTTEXT)); //accessibility: always set both foreground AND background colors! + drawCellText(dc, rectTmp, fulltext, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, &extentFullText); //draw everything: might fix partially cleared character + } + else //draw input cursor + if (showCursor_ || std::chrono::steady_clock::now() < previewChangeTime_ + std::chrono::milliseconds(400)) + { + const wxRect rectLine{rectTmp.x + extentBefore.GetWidth(), + rectTmp.y, + dipToWxsize(1), + rectTmp.height}; + clearArea(dc, rectLine, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); + } + } + break; + } + } + + int getBestSize(const wxReadOnlyDC& dc, size_t row, ColumnType colType) override + { + // -> synchronize renderCell() <-> getBestSize() + return dc.GetTextExtent(getValue(row, colType)).GetWidth() + 2 * getColumnGapLeft() + dipToWxsize(1); //gap on left and right side + border + } + + std::wstring getToolTip(size_t row, ColumnType colType, HoverArea rowHover) override { return std::wstring(); } + + std::wstring getColumnLabel(ColumnType colType) const override + { + switch (static_cast(colType)) + { + case ColumnTypeRename::oldName: + return _("Old name"); + case ColumnTypeRename::newName: + return _("New name"); + } + //assert(false); may be ColumnType::none + return std::wstring(); + } + + void setCursorShown(bool show) { showCursor_ = show; } + +private: + const std::vector fileNamesOld_; + + std::tuple lastUsedPhrase_; + + std::vector fileNamesNewSelectBefore_{fileNamesOld_.size()}; + std::vector fileNamesNewSelected_ {fileNamesOld_.size()}; + std::vector fileNamesNewSelectAfter_ {fileNamesOld_.size()}; + + bool showCursor_ = false; + std::chrono::steady_clock::time_point previewChangeTime_ = std::chrono::steady_clock::now(); + + const SharedRef renameBuf_; +}; + + +class RenameDialog : public RenameDlgGenerated +{ +public: + RenameDialog(wxWindow* parent, const std::vector& fileNamesOld, std::vector& fileNamesNew); + +private: + void onOkay (wxCommandEvent& event) override; + void onCancel(wxCommandEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } + void onClose (wxCloseEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } + + void onLocalKeyEvent(wxKeyEvent& event); + + void updatePreview() + { + const std::wstring renamePhrase = copyStringTo(m_textCtrlNewName->GetValue()); + + long selectBegin = 0; + long selectEnd = 0; + m_textCtrlNewName->GetSelection(&selectBegin, &selectEnd); + + assert(selectBegin == m_textCtrlNewName->GetInsertionPoint()); //apparently this is true for all Win/macOS/Linux + + if (getDataView().updatePreview(renamePhrase, selectBegin, selectEnd)) + m_gridRenamePreview->Refresh(); + } + + GridDataRename& getDataView() + { + if (auto* prov = dynamic_cast(m_gridRenamePreview->getDataProvider())) + return *prov; + throw std::runtime_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] m_gridRenamePreview was not initialized."); + } + + wxTimer timer_; //poll for text selection changes + wxTimer timerCursor_; //second timer just for cursor blinking + + //output-only parameters: + std::vector& fileNamesNewOut_; +}; + + +RenameDialog::RenameDialog(wxWindow* parent, + const std::vector& fileNamesOld, + std::vector& fileNamesNew) : + RenameDlgGenerated(parent), + fileNamesNewOut_(fileNamesNew) +{ + setStandardButtonLayout(*bSizerStdButtons, StdButtons().setAffirmative(m_buttonOK).setCancel(m_buttonCancel)); + + setMainInstructionFont(*m_staticTextHeader); + + setImage(*m_bitmapRename, loadImage("rename")); + + m_staticTextHeader->SetLabelText(_P("Do you really want to rename the following item?", + "Do you really want to rename the following %x items?", fileNamesOld.size())); + + m_buttonOK->SetLabelText(wxControl::RemoveMnemonics(_("&Rename"))); //no access key needed: use ENTER! + + auto [renamePhrase, renameBuf] = getPlaceholderPhrase(fileNamesOld); + const std::wstring renamePhraseOld = renamePhrase; //save copy *before* trimming + + trim(renamePhrase); //leading/trailing whitespace makes no sense for file names + + + std::wstring placeholders; + for (const wchar_t c : renamePhrase) + if (isRenamePlaceholderChar(c)) + placeholders += c; + + m_staticTextPlaceholderDescription->SetLabelText(placeholders + L": " + m_staticTextPlaceholderDescription->GetLabelText()); + + //----------------------------------------------------------- + m_gridRenamePreview->setDataProvider(std::make_shared(fileNamesOld, renameBuf)); + m_gridRenamePreview->showRowLabel(false); + m_gridRenamePreview->setRowHeight(m_gridRenamePreview->getMainWin().GetCharHeight() + dipToWxsize(1) /*extra space*/); + + //----------------------------------------------------------- + if (fileNamesOld.size() > 1) //calculate reasonable default preview grid size + { + //quick and dirty: get (likely) maximum string width while avoiding excessive wxDC::GetTextExtent() calls + std::vector longNames = fileNamesOld; + if (longNames.size() > 10) //find the 10 longest strings according to std::wstring::size() + { + std::nth_element(longNames.begin(), longNames.begin() + 9, longNames.end(), + /**/[](const std::wstring& lhs, const std::wstring& rhs) { return lhs.size() > rhs.size(); }); //complexity: O(n) + longNames.resize(10); + } + + wxInfoDC infoDc(m_gridRenamePreview); + infoDc.SetFont(m_gridRenamePreview->GetFont()); //the font parameter of GetTextExtent() is not evaluated on OS X, wxWidgets 2.9.5, so apply it to the DC directly! + + int maxStringWidth = 0; + for (const std::wstring& str : longNames) + maxStringWidth = std::max(maxStringWidth, infoDc.GetTextExtent(str).GetWidth()); + + const int defaultColWidthOld = maxStringWidth + 2 * GridData::getColumnGapLeft() + dipToWxsize(1) /*border*/ + dipToWxsize(10) /*extra space: less cramped*/; + const int defaultColWidthNew = maxStringWidth + 2 * GridData::getColumnGapLeft() + dipToWxsize(1) /*border*/ + dipToWxsize(50) /*extra space: for longer new name*/; + + m_gridRenamePreview->setColumnConfig( + { + {static_cast(ColumnTypeRename::oldName), defaultColWidthOld, 0, true}, //"old name" is fixed => + {static_cast(ColumnTypeRename::newName), -defaultColWidthOld, 1, true}, //stretch "new name" only + }); + + const int previewDefaultWidth = std::min(defaultColWidthOld + defaultColWidthNew + dipToWxsize(25), //scroll bar width (guess!) + dipToWxsize(900)); + + const int previewDefaultHeight = std::min(m_gridRenamePreview->getColumnLabelHeight() + + static_cast(fileNamesOld.size()) * m_gridRenamePreview->getRowHeight(), + dipToWxsize(400)); + + m_gridRenamePreview->SetMinSize({previewDefaultWidth, previewDefaultHeight}); + + m_staticTextHeader->Wrap(std::max(previewDefaultWidth, dipToWxsize(400))); //needs to be reapplied after SetLabel() + } + else //renaming single file + { + m_gridRenamePreview ->Hide(); + m_staticlinePreview ->Hide(); + m_staticTextPlaceholderDescription->Hide(); + + wxInfoDC infoDc(m_textCtrlNewName); + infoDc.SetFont(m_textCtrlNewName->GetFont()); //the font parameter of GetTextExtent() is not evaluated on OS X, wxWidgets 2.9.5, so apply it to the DC directly! + + const int textCtrlDefaultWidth = std::min(infoDc.GetTextExtent(renamePhrase).GetWidth() + 20 /*borders (non-DIP!)*/ + + dipToWxsize(50) /*extra space: for longer new name*/, + dipToWxsize(900)); + m_textCtrlNewName->SetMinSize({textCtrlDefaultWidth, -1}); + + m_staticTextHeader->Wrap(std::max(textCtrlDefaultWidth, dipToWxsize(400))); //needs to be reapplied after SetLabel() + } + //----------------------------------------------------------- + + m_textCtrlNewName->Bind(wxEVT_COMMAND_TEXT_UPDATED, [this, renamePhraseOld, needPreview = fileNamesOld.size() > 1](wxCommandEvent& event) + { + if (needPreview) + updatePreview(); //(almost?) redundant, considering timer_ is doing the same!? + + //disable OK button, until user changes input + const std::wstring renamePhraseNew = trimCpy(copyStringTo(m_textCtrlNewName->GetValue())); + m_buttonOK->Enable(!renamePhraseNew.empty() && renamePhraseNew != renamePhraseOld); //supports polling + }); + + wxTextValidator inputValidator(wxFILTER_EXCLUDE_CHAR_LIST); + + inputValidator.SetCharExcludes(L"/\\"); //let's not silently forbid "fileNameForbiddenChars", but let it fail explicitly! + m_textCtrlNewName->SetValidator(inputValidator); + m_textCtrlNewName->SetValue(renamePhrase); //SetValue() generates a text change event, unlike ChangeValue() + + + if (fileNamesOld.size() > 1) + { + timer_.Bind(wxEVT_TIMER, [this](wxTimerEvent& event) { updatePreview(); }); //poll to detect text selection changes + timer_.Start(100 /*unit: [ms]*/); + + timerCursor_.Bind(wxEVT_TIMER, [this, show = true](wxTimerEvent& event) mutable //trigger blinking cursor + { + getDataView().setCursorShown(show); + m_gridRenamePreview->Refresh(); + show = !show; + }); + timerCursor_.Start(wxCaret::GetBlinkTime() /*unit: [ms]*/); + } + + Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onLocalKeyEvent(event); }); //enable dialog-specific key events + + //----------------------------------------------------------- + GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() +#ifdef __WXGTK3__ + Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 + //Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404 +#endif + Center(); //apply *after* dialog size change! + + m_textCtrlNewName->SetFocus(); //[!] required *before* SetSelection() on wxGTK + //----------------------------------------------------------- + + //macOS issue: the *whole* text control is selected by default, unless we SetSelection() *after* wxDialog::Show()! + CallAfter([this, nameCount = fileNamesOld.size(), renamePhrase = renamePhrase] + { + //pre-select name part that user will most likely change + //assert(contains(renamePhrase, L'\u2776') == nameCount > 1); -> fails, if user selects same item on left and right grid + auto it = std::find_if(renamePhrase.begin(), renamePhrase.end(), isRenamePlaceholderChar); //❶ + if (it == renamePhrase.end()) + it = findLast(renamePhrase.begin(), renamePhrase.end(), L'.'); //select everything except file extension + + if (it == renamePhrase.end()) + m_textCtrlNewName->SelectAll(); + else + { + const long selectEnd = static_cast(it - renamePhrase.begin()); + m_textCtrlNewName->SetSelection(0, selectEnd); + } + + updatePreview(); //consider new selection + }); +} + + +void RenameDialog::onLocalKeyEvent(wxKeyEvent& event) +{ + switch (event.GetKeyCode()) + { + case WXK_RETURN: + case WXK_NUMPAD_ENTER: + if (event.ControlDown()) //Ctrl+Enter or on macOS: Command+Enter + { + wxCommandEvent dummy(wxEVT_COMMAND_BUTTON_CLICKED); + m_buttonOK->Command(dummy); //simulate click + return; + } + break; + } + event.Skip(); +} + + +void RenameDialog::onOkay(wxCommandEvent& event) +{ + updatePreview(); //ensure GridDataRename::getNewNames() is current + + fileNamesNewOut_.clear(); + for (const std::wstring& newName : getDataView().getNewNames()) + fileNamesNewOut_.push_back(utfTo(newName)); + + EndModal(static_cast(ConfirmationButton::accept)); +} +} + + +ConfirmationButton fff::showRenameDialog(wxWindow* parent, + const std::vector& fileNamesOld, + std::vector& fileNamesNew) +{ + std::vector namesOld; + for (const Zstring& name : fileNamesOld) + namesOld.push_back(utfTo(getUnicodeNormalForm(name))); //[!] don't care about Normalization form differences! + + RenameDialog dlg(parent, namesOld, fileNamesNew); + return static_cast(dlg.ShowModal()); +} diff --git a/FreeFileSync/Source/ui/rename_dlg.h b/FreeFileSync/Source/ui/rename_dlg.h new file mode 100644 index 0000000..b6be9cd --- /dev/null +++ b/FreeFileSync/Source/ui/rename_dlg.h @@ -0,0 +1,20 @@ +// ***************************************************************************** +// * 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 RENAME_DLG_H_23487982347324 +#define RENAME_DLG_H_23487982347324 + +#include + + +namespace fff +{ +zen::ConfirmationButton showRenameDialog(wxWindow* parent, + const std::vector& fileNamesOld, + std::vector& fileNamesNew); +} + +#endif //RENAME_DLG_H_23487982347324 diff --git a/FreeFileSync/Source/ui/search_grid.cpp b/FreeFileSync/Source/ui/search_grid.cpp new file mode 100644 index 0000000..e17238e --- /dev/null +++ b/FreeFileSync/Source/ui/search_grid.cpp @@ -0,0 +1,145 @@ +// ***************************************************************************** +// * 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 "search_grid.h" +#include +#include + +using namespace zen; +using namespace fff; + + +namespace +{ +template +void normalizeForSearch(std::wstring& str); + +template <> inline +void normalizeForSearch(std::wstring& str) +{ + for (wchar_t& c : str) + if (!isAsciiChar(c)) + { + str = utfTo(getUnicodeNormalForm(utfTo(str))); + replace(str, L'\\', L'/'); + return; + } + else if (c == L'\\') + c = L'/'; +} + +template <> inline +void normalizeForSearch(std::wstring& str) +{ + for (wchar_t& c : str) + if (!isAsciiChar(c)) + { + str = utfTo(getUpperCase(utfTo(str))); //getUnicodeNormalForm() is implied by getUpperCase() + replace(str, L'\\', L'/'); + return; + } + else if (c == L'\\') + c = L'/'; + else + c = asciiToUpper(c); //caveat, decomposed Unicode form! c might be followed by combining character! Still, should be fine... +} + + +template +class MatchFound +{ +public: + explicit MatchFound(const std::wstring& textToFind) : textToFind_(textToFind) + { + normalizeForSearch(textToFind_); + } + + bool operator()(std::wstring&& phrase) const + { + normalizeForSearch(phrase); + return contains(phrase, textToFind_); + } + +private: + std::wstring textToFind_; +}; + +//########################################################################################### + +template +ptrdiff_t findRow(const Grid& grid, //return -1 if no matching row found + const std::wstring& searchString, + bool searchAscending, + size_t rowFirst, //range to search: + size_t rowLast) // [rowFirst, rowLast) +{ + if (auto prov = grid.getDataProvider()) + { + std::vector colAttr = grid.getColumnConfig(); + std::erase_if(colAttr, [](const Grid::ColAttributes& ca) { return !ca.visible; }); + if (!colAttr.empty()) + { + const MatchFound matchFound(searchString); + + if (searchAscending) + { + for (size_t row = rowFirst; row < rowLast; ++row) + for (const Grid::ColAttributes& ca : colAttr) + if (matchFound(prov->getValue(row, ca.type))) + return row; + } + else + for (size_t row = rowLast; row-- > rowFirst;) + for (const Grid::ColAttributes& ca : colAttr) + if (matchFound(prov->getValue(row, ca.type))) + return row; + } + } + return -1; +} +} + + +std::pair fff::findGridMatch(const Grid& grid1, const Grid& grid2, const std::wstring& searchString, bool respectCase, bool searchAscending) +{ + //PERF_START + + const size_t rowCount1 = grid1.getRowCount(); + const size_t rowCount2 = grid2.getRowCount(); + + size_t cursorRow1 = grid1.getGridCursor(); + if (cursorRow1 >= rowCount1) + cursorRow1 = 0; + + std::pair result(nullptr, -1); + + auto finishSearch = [&](const Grid& grid, size_t rowFirst, size_t rowLast) + { + const ptrdiff_t targetRow = respectCase ? + findRow(grid, searchString, searchAscending, rowFirst, rowLast) : + findRow(grid, searchString, searchAscending, rowFirst, rowLast); + if (targetRow >= 0) + { + result = {&grid, targetRow}; + return true; + } + return false; + }; + + if (searchAscending) + { + if (!finishSearch(grid1, cursorRow1 + 1, rowCount1)) + if (!finishSearch(grid2, 0, rowCount2)) + finishSearch(grid1, 0, cursorRow1 + 1); + } + else + { + if (!finishSearch(grid1, 0, cursorRow1)) + if (!finishSearch(grid2, 0, rowCount2)) + finishSearch(grid1, cursorRow1, rowCount1); + } + return result; +} diff --git a/FreeFileSync/Source/ui/search_grid.h b/FreeFileSync/Source/ui/search_grid.h new file mode 100644 index 0000000..81560f7 --- /dev/null +++ b/FreeFileSync/Source/ui/search_grid.h @@ -0,0 +1,19 @@ +// ***************************************************************************** +// * 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 SEARCH_H_423905762345342526587 +#define SEARCH_H_423905762345342526587 + +#include + + +namespace fff +{ +std::pair findGridMatch(const zen::Grid& grid1, const zen::Grid& grid2, const std::wstring& searchString, bool respectCase, bool searchAscending); +//returns (grid/row) where the value was found, (nullptr, -1) if not found +} + +#endif //SEARCH_H_423905762345342526587 diff --git a/FreeFileSync/Source/ui/small_dlgs.cpp b/FreeFileSync/Source/ui/small_dlgs.cpp new file mode 100644 index 0000000..b50cdae --- /dev/null +++ b/FreeFileSync/Source/ui/small_dlgs.cpp @@ -0,0 +1,2446 @@ +// ***************************************************************************** +// * 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 "small_dlgs.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "gui_generated.h" +#include "folder_selector.h" +#include "abstract_folder_picker.h" +#include "../afs/concrete.h" +#include "../afs/gdrive.h" +#include "../afs/ftp.h" +#include "../afs/sftp.h" +#include "../base/synchronization.h" +#include "../base/icon_loader.h" +#include "../status_handler.h" //uiUpdateDue() +#include "../version/version.h" +#include "../ffs_paths.h" +#include "../icon_buffer.h" + + + +using namespace zen; +using namespace fff; + + +namespace +{ +class AboutDlg : public AboutDlgGenerated +{ +public: + AboutDlg(wxWindow* parent); + +private: + void onOkay (wxCommandEvent& event) override { EndModal(static_cast(ConfirmationButton::accept)); } + void onClose(wxCloseEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } + void onOpenForum(wxCommandEvent& event) override { wxLaunchDefaultBrowser(L"https://freefilesync.org/forum"); } + void onDonate (wxCommandEvent& event) override { wxLaunchDefaultBrowser(L"https://freefilesync.org/donate"); } + void onSendEmail(wxCommandEvent& event) override + { + wxLaunchDefaultBrowser(wxString() + L"mailto:zenju" + L'@' + /*don't leave full email in either source or binary*/ L"freefilesync.org"); + } + + void onLocalKeyEvent(wxKeyEvent& event); +}; + + +AboutDlg::AboutDlg(wxWindow* parent) : AboutDlgGenerated(parent) +{ + setStandardButtonLayout(*bSizerStdButtons, StdButtons().setAffirmative(m_buttonClose)); + + assert(m_buttonClose->GetId() == wxID_OK); //we cannot use wxID_CLOSE else ESC key won't work: yet another wxWidgets bug?? + + const bool darkAppearance = wxSystemSettings::GetAppearance().IsDark(); //not "dark mode" necessarily! + + setImage(*m_bitmapLogo, loadImage(darkAppearance ? "ffs-header-dark" : "ffs-header-light")); + setImage(*m_bitmapLogoLeft, loadImage(darkAppearance ? "ffs-logo-dark" : "ffs-logo-light")); + + setBitmapTextLabel(*m_bpButtonForum, loadImage("ffs_forum"), L"FreeFileSync Forum"); + setBitmapTextLabel(*m_bpButtonEmail, loadImage("ffs_email"), wxString() + L"zenju" + L'@' + /*don't leave full email in either source or binary*/ L"freefilesync.org"); + m_bpButtonEmail->SetToolTip( wxString() + L"mailto:zenju" + L'@' + /*don't leave full email in either source or binary*/ L"freefilesync.org"); + + wxString build = utfTo(ffsVersion); + + const wchar_t* const SPACED_BULLET = L" \u2022 "; + build += SPACED_BULLET; + + build += LTR_MARK; //fix Arabic + build += utfTo(cpuArchName); + + build += SPACED_BULLET; + build += utfTo(formatTime(formatDateTag, getCompileTime())); + + m_staticFfsTextVersion->SetLabelText(replaceCpy(_("Version: %x"), L"%x", build)); + + wxString variantName; + m_staticTextFfsVariant->SetLabelText(variantName); + +#ifndef wxUSE_UNICODE +#error what is going on? +#endif + + { + m_bitmapAnimalBig->Hide(); + + setRelativeFontSize(*m_staticTextDonate, 1.20); + m_staticTextDonate->Hide(); //temporarily! => avoid impact to dialog width + + setRelativeFontSize(*m_buttonDonate1, 1.25); + setBitmapTextLabel(*m_buttonDonate1, loadImage("ffs_heart", dipToScreen(28)), m_buttonDonate1->GetLabelText()); + + m_buttonShowSupporterDetails->Hide(); + m_buttonDonate2->Hide(); + } + + //-------------------------------------------------------------------------- + m_staticTextThanksForLoc->SetMinSize({dipToWxsize(200), -1}); + m_staticTextThanksForLoc->Wrap(dipToWxsize(200)); + + const int scrollDelta = GetCharHeight(); + m_scrolledWindowTranslators->SetScrollRate(scrollDelta, scrollDelta); + + for (const TranslationInfo& ti : getAvailableTranslations()) + { + //country flag + wxStaticBitmap* staticBitmapFlag = new wxStaticBitmap(m_scrolledWindowTranslators, wxID_ANY, toScaledBitmap(loadImage(ti.languageFlag))); + fgSizerTranslators->Add(staticBitmapFlag, 0, wxALIGN_CENTER); + + //translator name + wxStaticText* staticTextTranslator = new wxStaticText(m_scrolledWindowTranslators, wxID_ANY, ti.translatorName, wxDefaultPosition, wxDefaultSize, 0); + fgSizerTranslators->Add(staticTextTranslator, 0, wxALIGN_CENTER_VERTICAL); + + staticBitmapFlag ->SetToolTip(ti.languageName); + staticTextTranslator->SetToolTip(ti.languageName); + } + fgSizerTranslators->Fit(m_scrolledWindowTranslators); + //-------------------------------------------------------------------------- + + wxImage::AddHandler(new wxJPEGHandler /*ownership passed*/); //activate support for .jpg files + + wxImage animalImg(utfTo(appendPath(getResourceDirPath(), Zstr("Animal.dat"))), wxBITMAP_TYPE_JPEG); + convertToVanillaImage(animalImg); + assert(animalImg.IsOk()); + + //-------------------------------------------------------------------------- + //have animal + text match *final* dialog width + GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() +#ifdef __WXGTK3__ + Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 + //Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404 +#endif + + { + const int imageWidth = (m_panelDonate->GetSize().GetWidth() - 5 - 5 - 5 /* grey border*/) / 2; + const int textWidth = m_panelDonate->GetSize().GetWidth() - 5 - 5 - 5 - imageWidth; + + setImage(*m_bitmapAnimalSmall, shrinkImage(animalImg, wxsizeToScreen(imageWidth), -1 /*maxHeight*/)); + + m_staticTextDonate->Show(); + m_staticTextDonate->Wrap(textWidth - 10 /*left gap*/); //wrap *after* changing font size + } + //-------------------------------------------------------------------------- + + Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onLocalKeyEvent(event); }); //enable dialog-specific key events + + GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() +#ifdef __WXGTK3__ + Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 + //Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404 +#endif + Center(); //apply *after* dialog size change! + + m_buttonClose->SetFocus(); //on GTK ESC is only associated with wxID_OK correctly if we set at least *any* focus at all!!! +} + + +void AboutDlg::onLocalKeyEvent(wxKeyEvent& event) //process key events without explicit menu entry :) +{ + switch (event.GetKeyCode()) + { + case WXK_RETURN: + case WXK_NUMPAD_ENTER: + if (event.ControlDown()) //Ctrl+Enter or on macOS: Command+Enter + { + wxCommandEvent dummy(wxEVT_COMMAND_BUTTON_CLICKED); + m_buttonClose->Command(dummy); //simulate click + return; + } + break; + } + event.Skip(); +} +} + +void fff::showAboutDialog(wxWindow* parent) +{ + AboutDlg dlg(parent); + dlg.ShowModal(); +} + +//######################################################################################## + +namespace +{ +class CloudSetupDlg : public CloudSetupDlgGenerated +{ +public: + CloudSetupDlg(wxWindow* parent, Zstring& folderPathPhrase, Zstring& sftpKeyFileLastSelected, size_t& parallelOps, bool canChangeParallelOp); + +private: + void onOkay (wxCommandEvent& event) override; + void onCancel(wxCommandEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } + void onClose (wxCloseEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } + + void onGdriveUserAdd (wxCommandEvent& event) override; + void onGdriveUserRemove(wxCommandEvent& event) override; + void onGdriveUserSelect(wxCommandEvent& event) override; + void gdriveUpdateDrivesAndSelect(const std::string& accountEmail, const Zstring& locationToSelect); + + void onDetectServerChannelLimit(wxCommandEvent& event) override; + void onTypingPassword(wxCommandEvent& event) override; + void onToggleShowPassword(wxCommandEvent& event) override; + void onTogglePasswordPrompt(wxCommandEvent& event) override { updateGui(); } + void onBrowseCloudFolder (wxCommandEvent& event) override; + + void onConnectionGdrive(wxCommandEvent& event) override { type_ = CloudType::gdrive; updateGui(); } + 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::keyFile; updateGui(); } + void onAuthAgent (wxCommandEvent& event) override { sftpAuthType_ = SftpAuthType::agent; updateGui(); } + + void onSelectKeyfile(wxCommandEvent& event) override; + + void updateGui(); + + //work around defunct keyboard focus on macOS (or is it wxMac?) => not needed for this dialog! + //void onLocalKeyEvent(wxKeyEvent& event); + + static bool acceptFileDrop(const std::vector& shellItemPaths); + void onKeyFileDropped(FileDropEvent& event); + + bool validateParameters(); + AbstractPath getFolderPath() const; + + enum class CloudType + { + gdrive, + sftp, + ftp, + }; + CloudType type_ = CloudType::gdrive; + + const wxString txtLoading_ = L'(' + _("Loading...") + L')'; + const wxString txtMyDrive_ = _("My Drive"); + + const SftpLogin sftpDefault_; + + SftpAuthType sftpAuthType_ = sftpDefault_.authType; + + AsyncGuiQueue guiQueue_; + + Zstring& sftpKeyFileLastSelected_; + + //output-only parameters: + Zstring& folderPathPhraseOut_; + size_t& parallelOpsOut_; +}; + + +CloudSetupDlg::CloudSetupDlg(wxWindow* parent, Zstring& folderPathPhrase, Zstring& sftpKeyFileLastSelected, size_t& parallelOps, bool canChangeParallelOp) : + CloudSetupDlgGenerated(parent), + sftpKeyFileLastSelected_(sftpKeyFileLastSelected), + folderPathPhraseOut_(folderPathPhrase), + parallelOpsOut_(parallelOps) +{ + setStandardButtonLayout(*bSizerStdButtons, StdButtons().setAffirmative(m_buttonOK).setCancel(m_buttonCancel)); + + setImage(*m_toggleBtnGdrive, loadImage("google_drive")); + + setRelativeFontSize(*m_toggleBtnGdrive, 1.25); + setRelativeFontSize(*m_toggleBtnSftp, 1.25); + setRelativeFontSize(*m_toggleBtnFtp, 1.25); + + setBitmapTextLabel(*m_buttonGdriveAddUser, loadImage("user_add", dipToScreen(20)), m_buttonGdriveAddUser ->GetLabelText()); + setBitmapTextLabel(*m_buttonGdriveRemoveUser, loadImage("user_remove", dipToScreen(20)), m_buttonGdriveRemoveUser->GetLabelText()); + + setImage(*m_bitmapGdriveUser, loadImage("user", dipToScreen(20))); + setImage(*m_bitmapGdriveDrive, loadImage("drive", dipToScreen(20))); + setImage(*m_bitmapServer, loadImage("server", dipToScreen(20))); + setImage(*m_bitmapCloud, loadImage("cloud")); + setImage(*m_bitmapPerf, loadImage("speed")); + setImage(*m_bitmapServerDir, IconBuffer::genericDirIcon(IconBuffer::IconSize::small)); + m_checkBoxShowPassword ->SetValue(false); + m_checkBoxPasswordPrompt->SetValue(false); + + m_textCtrlServer->SetHint(_("Example:") + L" website.com 66.198.240.22"); + m_textCtrlServer->SetMinSize({dipToWxsize(260), -1}); + + m_textCtrlPort->SetMinSize({dipToWxsize(60), -1}); + setDefaultWidth(*m_spinCtrlConnectionCount); + setDefaultWidth(*m_spinCtrlChannelCountSftp); + setDefaultWidth(*m_spinCtrlTimeout); + + setupFileDrop(*m_panelAuth); + m_panelAuth->Bind(EVENT_DROP_FILE, [this](FileDropEvent& event) { onKeyFileDropped(event); }); + + m_staticTextConnectionsLabelSub->SetLabelText(L'(' + _("Connections") + L')'); + + //use spacer to keep dialog height stable, no matter if key file options are visible + bSizerAuthInner->Add(0, m_panelAuth->GetSize().y); + + //--------------------------------------------------------- + std::vector gdriveAccounts; + try + { + for (const std::string& loginEmail : gdriveListAccounts()) //throw FileError + gdriveAccounts.push_back(utfTo(loginEmail)); + } + catch (const FileError& e) + { + showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); + } + m_listBoxGdriveUsers->Append(gdriveAccounts); + + //set default values for Google Drive: use first item of m_listBoxGdriveUsers + if (!gdriveAccounts.empty() && !acceptsItemPathPhraseGdrive(folderPathPhrase)) + { + m_listBoxGdriveUsers->SetSelection(0); + gdriveUpdateDrivesAndSelect(utfTo(gdriveAccounts[0]), Zstring() /*My Drive*/); + } + + m_spinCtrlTimeout->SetValue(sftpDefault_.timeoutSec); + assert(sftpDefault_.timeoutSec == FtpLogin().timeoutSec); //make sure the default values are in sync + + //--------------------------------------------------------- + if (acceptsItemPathPhraseGdrive(folderPathPhrase)) + { + type_ = CloudType::gdrive; + const AbstractPath folderPath = createItemPathGdrive(folderPathPhrase); + const GdriveLogin login = extractGdriveLogin(folderPath.afsDevice); //noexcept + + if (const int selPos = m_listBoxGdriveUsers->FindString(utfTo(login.email), false /*caseSensitive*/); + selPos != wxNOT_FOUND) + { + m_listBoxGdriveUsers->EnsureVisible(selPos); + m_listBoxGdriveUsers->SetSelection(selPos); + gdriveUpdateDrivesAndSelect(login.email, login.locationName); + } + else + { + m_listBoxGdriveUsers->DeselectAll(); + m_listBoxGdriveDrives->Clear(); + } + + m_textCtrlServerPath->ChangeValue(utfTo(FILE_NAME_SEPARATOR + folderPath.afsPath.value)); + m_spinCtrlTimeout->SetValue(login.timeoutSec); + } + else if (acceptsItemPathPhraseSftp(folderPathPhrase)) + { + type_ = CloudType::sftp; + const AbstractPath folderPath = createItemPathSftp(folderPathPhrase); + const SftpLogin login = extractSftpLogin(folderPath.afsDevice); //noexcept + + if (login.portCfg > 0) + m_textCtrlPort->ChangeValue(numberTo(login.portCfg)); + m_textCtrlServer ->ChangeValue(utfTo(login.server)); + m_textCtrlUserName ->ChangeValue(utfTo(login.username)); + sftpAuthType_ = login.authType; + if (login.password) + m_textCtrlPasswordHidden->ChangeValue(utfTo(*login.password)); + else + m_checkBoxPasswordPrompt->SetValue(true); + m_textCtrlKeyfilePath ->ChangeValue(utfTo(login.privateKeyFilePath)); + m_textCtrlServerPath ->ChangeValue(utfTo(FILE_NAME_SEPARATOR + folderPath.afsPath.value)); + m_checkBoxAllowZlib ->SetValue(login.allowZlib); + m_spinCtrlTimeout ->SetValue(login.timeoutSec); + m_spinCtrlChannelCountSftp->SetValue(login.traverserChannelsPerConnection); + } + else if (acceptsItemPathPhraseFtp(folderPathPhrase)) + { + type_ = CloudType::ftp; + const AbstractPath folderPath = createItemPathFtp(folderPathPhrase); + const FtpLogin login = extractFtpLogin(folderPath.afsDevice); //noexcept + + if (login.portCfg > 0) + m_textCtrlPort->ChangeValue(numberTo(login.portCfg)); + m_textCtrlServer ->ChangeValue(utfTo(login.server)); + m_textCtrlUserName->ChangeValue(utfTo(login.username)); + if (login.password) + m_textCtrlPasswordHidden ->ChangeValue(utfTo(*login.password)); + else + m_checkBoxPasswordPrompt->SetValue(true); + m_textCtrlServerPath->ChangeValue(utfTo(FILE_NAME_SEPARATOR + folderPath.afsPath.value)); + (login.useTls ? m_radioBtnEncryptSsl : m_radioBtnEncryptNone)->SetValue(true); + m_spinCtrlTimeout->SetValue(login.timeoutSec); + } + + m_spinCtrlConnectionCount->SetValue(parallelOps); + + m_spinCtrlConnectionCount->Disable(); + m_staticTextConnectionCountDescr->Hide(); + + m_spinCtrlChannelCountSftp->Disable(); + m_buttonChannelCountSftp ->Disable(); + //--------------------------------------------------------- + + //set up default view for dialog size calculation + bSizerGdrive->Show(false); + bSizerFtpEncrypt->Show(false); + m_textCtrlPasswordVisible->Hide(); + m_checkBoxPasswordPrompt->Hide(); + + GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() + //=> works like a charm for GTK with window resizing problems and title bar corruption; e.g. Debian!!! +#ifdef __WXGTK3__ + Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 + //Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404 +#endif + Center(); //apply *after* dialog size change! + + updateGui(); //*after* SetSizeHints when standard dialog height has been calculated + + m_buttonOK->SetFocus(); +} + + +void CloudSetupDlg::onGdriveUserAdd(wxCommandEvent& event) +{ + guiQueue_.processAsync([timeoutSec = extractGdriveLogin(getFolderPath().afsDevice).timeoutSec]() -> std::variant + { + try + { + return gdriveAddUser(nullptr /*updateGui*/, timeoutSec); //throw FileError + } + catch (const FileError& e) { return e; } + }, + [this](const std::variant& result) + { + if (const FileError* e = std::get_if(&result)) + showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e->toString())); + else + { + const std::string& loginEmail = std::get(result); + + int selPos = m_listBoxGdriveUsers->FindString(utfTo(loginEmail), false /*caseSensitive*/); + if (selPos == wxNOT_FOUND) + selPos = m_listBoxGdriveUsers->Append(utfTo(loginEmail)); + + m_listBoxGdriveUsers->EnsureVisible(selPos); + m_listBoxGdriveUsers->SetSelection(selPos); + updateGui(); //enable remove user button + gdriveUpdateDrivesAndSelect(loginEmail, Zstring() /*My Drive*/); + } + }); +} + + +void CloudSetupDlg::onGdriveUserRemove(wxCommandEvent& event) +{ + const int selPos = m_listBoxGdriveUsers->GetSelection(); + assert(selPos != wxNOT_FOUND); + if (selPos != wxNOT_FOUND) + try + { + const std::string& loginEmail = utfTo(m_listBoxGdriveUsers->GetString(selPos)); + if (showConfirmationDialog(this, DialogInfoType::warning, PopupDialogCfg(). + setTitle(_("Confirm")). + setMainInstructions(replaceCpy(_("Do you really want to disconnect from user account %x?"), L"%x", utfTo(loginEmail))), + _("&Disconnect")) != ConfirmationButton::accept) + return; + + gdriveRemoveUser(loginEmail, extractGdriveLogin(getFolderPath().afsDevice).timeoutSec); //throw FileError + m_listBoxGdriveUsers->Delete(selPos); + updateGui(); //disable remove user button + m_listBoxGdriveDrives->Clear(); + } + catch (const FileError& e) + { + showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); + } +} + + +void CloudSetupDlg::onGdriveUserSelect(wxCommandEvent& event) +{ + const int selPos = m_listBoxGdriveUsers->GetSelection(); + assert(selPos != wxNOT_FOUND); + if (selPos != wxNOT_FOUND) + { + const std::string& loginEmail = utfTo(m_listBoxGdriveUsers->GetString(selPos)); + updateGui(); //enable remove user button + gdriveUpdateDrivesAndSelect(loginEmail, Zstring() /*My Drive*/); + } +} + + +void CloudSetupDlg::gdriveUpdateDrivesAndSelect(const std::string& accountEmail, const Zstring& locationToSelect) +{ + m_listBoxGdriveDrives->Clear(); + m_listBoxGdriveDrives->Append(txtLoading_); + + guiQueue_.processAsync([accountEmail, timeoutSec = extractGdriveLogin(getFolderPath().afsDevice).timeoutSec]() -> + std::variant, FileError> + { + try + { + return gdriveListLocations(accountEmail, timeoutSec); //throw FileError + } + catch (const FileError& e) { return e; } + }, + [this, accountEmail, locationToSelect](std::variant, FileError>&& result) + { + if (const int selPos = m_listBoxGdriveUsers->GetSelection(); + selPos == wxNOT_FOUND || utfTo(m_listBoxGdriveUsers->GetString(selPos)) != accountEmail) + return; //different accountEmail selected in the meantime! + + m_listBoxGdriveDrives->Clear(); + + if (const FileError* e = std::get_if(&result)) + showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e->toString())); + else + { + auto& locationNames = std::get>(result); + std::sort(locationNames.begin(), locationNames.end(), LessNaturalSort()); + + m_listBoxGdriveDrives->Append(txtMyDrive_); //sort locations, but keep "My Drive" at top + + for (const Zstring& itemLabel : locationNames) + m_listBoxGdriveDrives->Append(utfTo(itemLabel)); + + const wxString labelToSelect = locationToSelect.empty() ? txtMyDrive_ : utfTo(locationToSelect); + + if (const int selPos = m_listBoxGdriveDrives->FindString(labelToSelect, true /*caseSensitive*/); + selPos != wxNOT_FOUND) + { + m_listBoxGdriveDrives->EnsureVisible(selPos); + m_listBoxGdriveDrives->SetSelection (selPos); + } + } + }); +} + + +void CloudSetupDlg::onDetectServerChannelLimit(wxCommandEvent& event) +{ + assert (type_ == CloudType::sftp); + try + { + m_spinCtrlChannelCountSftp->SetSelection(0, 0); //some visual feedback: clear selection + m_spinCtrlChannelCountSftp->Refresh(); //both needed for wxGTK: meh! + m_spinCtrlChannelCountSftp->Update(); // + + AbstractPath folderPath = getFolderPath(); //noexcept + //------------------------------------------------------------------- + auto requestPassword = [&, password = Zstring()](const std::wstring& msg, const std::wstring& lastErrorMsg) mutable + { + assert(runningOnMainThread()); + if (showPasswordPrompt(this, msg, lastErrorMsg, password) != ConfirmationButton::accept) + throw CancelProcess(); + return password; + }; + AFS::authenticateAccess(folderPath.afsDevice, requestPassword); //throw FileError, CancelProcess + //------------------------------------------------------------------- + + const int channelCountMax = getServerMaxChannelsPerConnection(extractSftpLogin(folderPath.afsDevice)); //throw FileError + m_spinCtrlChannelCountSftp->SetValue(channelCountMax); + + m_spinCtrlChannelCountSftp->SetFocus(); //[!] otherwise selection is lost + m_spinCtrlChannelCountSftp->SetSelection(-1, -1); //some visual feedback: select all + } + catch (CancelProcess&) { return; } + catch (const FileError& e) + { + showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); + } +} + + +void CloudSetupDlg::onToggleShowPassword(wxCommandEvent& event) +{ + assert(type_ != CloudType::gdrive); + if (m_checkBoxShowPassword->GetValue()) + m_textCtrlPasswordVisible->ChangeValue(m_textCtrlPasswordHidden->GetValue()); + else + m_textCtrlPasswordHidden->ChangeValue(m_textCtrlPasswordVisible->GetValue()); + + updateGui(); + + wxTextCtrl& textCtrl = *(m_checkBoxShowPassword->GetValue() ? m_textCtrlPasswordVisible : m_textCtrlPasswordHidden); + textCtrl.SetFocus(); //macOS: selects text as unwanted side effect => *before* SetInsertionPointEnd() + textCtrl.SetInsertionPointEnd(); +} + + +void CloudSetupDlg::onTypingPassword(wxCommandEvent& event) +{ + assert(m_staticTextPassword->IsShown()); + const wxString password = (m_checkBoxShowPassword->GetValue() ? m_textCtrlPasswordVisible : m_textCtrlPasswordHidden)->GetValue(); + if (m_checkBoxShowPassword ->IsShown() != !password.empty() || //let's avoid some minor flicker + m_checkBoxPasswordPrompt->IsShown() != password.empty()) //in updateGui() Dimensions() + updateGui(); +} + + +bool CloudSetupDlg::acceptFileDrop(const std::vector& shellItemPaths) +{ + if (shellItemPaths.empty()) + return false; + + const Zstring ext = getFileExtension(shellItemPaths[0]); + return ext.empty() || + equalAsciiNoCase(ext, "pem") || + equalAsciiNoCase(ext, "ppk"); +} + + +void CloudSetupDlg::onKeyFileDropped(FileDropEvent& event) +{ + //assert (type_ == CloudType::SFTP); -> no big deal if false + if (!event.itemPaths_.empty()) + { + m_textCtrlKeyfilePath->ChangeValue(utfTo(event.itemPaths_[0])); + + sftpAuthType_ = SftpAuthType::keyFile; + updateGui(); + } +} + + +void CloudSetupDlg::onSelectKeyfile(wxCommandEvent& event) +{ + assert (type_ == CloudType::sftp && sftpAuthType_ == SftpAuthType::keyFile); + + std::optional defaultFolderPath = getParentFolderPath(sftpKeyFileLastSelected_); + + wxFileDialog fileSelector(this, wxString() /*message*/, utfTo(defaultFolderPath ? *defaultFolderPath : Zstr("")), wxString() /*default file name*/, + _("All files") + L" (*.*)|*" + + L"|" + L"OpenSSL PEM (*.pem)|*.pem" + + L"|" + L"PuTTY Private Key (*.ppk)|*.ppk", + wxFD_OPEN); + if (fileSelector.ShowModal() != wxID_OK) + return; + m_textCtrlKeyfilePath->ChangeValue(fileSelector.GetPath()); + sftpKeyFileLastSelected_ = utfTo(fileSelector.GetPath()); +} + + +void CloudSetupDlg::updateGui() +{ + m_toggleBtnGdrive->SetValue(type_ == CloudType::gdrive); + m_toggleBtnSftp ->SetValue(type_ == CloudType::sftp); + m_toggleBtnFtp ->SetValue(type_ == CloudType::ftp); + + bSizerGdrive->Show(type_ == CloudType::gdrive); + bSizerServer->Show(type_ == CloudType::ftp || type_ == CloudType::sftp); + bSizerAuth ->Show(type_ == CloudType::ftp || type_ == CloudType::sftp); + + bSizerFtpEncrypt->Show(type_ == CloudType:: ftp); + bSizerSftpAuth ->Show(type_ == CloudType::sftp); + + 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)); + if (m_staticTextPassword->IsShown()) + { + m_textCtrlPasswordVisible->Show( m_checkBoxShowPassword->GetValue()); + m_textCtrlPasswordHidden ->Show(!m_checkBoxShowPassword->GetValue()); + + m_textCtrlPasswordVisible->Enable(!m_checkBoxPasswordPrompt->GetValue()); + m_textCtrlPasswordHidden ->Enable(!m_checkBoxPasswordPrompt->GetValue()); + + const wxString password = (m_checkBoxShowPassword->GetValue() ? m_textCtrlPasswordVisible : m_textCtrlPasswordHidden)->GetValue(); + m_checkBoxShowPassword ->Show(!password.empty()); + m_checkBoxPasswordPrompt->Show( password.empty()); + } + + switch (type_) + { + case CloudType::gdrive: + m_buttonGdriveRemoveUser->Enable(m_listBoxGdriveUsers->GetSelection() != wxNOT_FOUND); + break; + + case CloudType::sftp: + m_radioBtnPassword->SetValue(false); + m_radioBtnKeyfile ->SetValue(false); + m_radioBtnAgent ->SetValue(false); + + m_textCtrlPort->SetHint(numberTo(DEFAULT_PORT_SFTP)); + + switch (sftpAuthType_) //*not* owned by GUI controls + { + case SftpAuthType::password: + m_radioBtnPassword->SetValue(true); + m_staticTextPassword->SetLabelText(_("Password:")); + break; + case SftpAuthType::keyFile: + m_radioBtnKeyfile->SetValue(true); + m_staticTextPassword->SetLabelText(_("Key passphrase:")); + break; + case SftpAuthType::agent: + m_radioBtnAgent->SetValue(true); + break; + } + break; + + case CloudType::ftp: + m_textCtrlPort->SetHint(numberTo(DEFAULT_PORT_FTP)); + m_staticTextPassword->SetLabelText(_("Password:")); + break; + } + + m_staticTextChannelCountSftp->Show(type_ == CloudType::sftp); + m_spinCtrlChannelCountSftp ->Show(type_ == CloudType::sftp); + m_buttonChannelCountSftp ->Show(type_ == CloudType::sftp); + m_checkBoxAllowZlib ->Show(type_ == CloudType::sftp); + m_staticTextZlibDescr ->Show(type_ == CloudType::sftp); + + Layout(); //needed! hidden items are not considered during resize + Refresh(); +} + + +bool CloudSetupDlg::validateParameters() +{ + if (type_ == CloudType::sftp || + type_ == CloudType::ftp) + { + if (trimCpy(m_textCtrlServer->GetValue()).empty()) + { + showNotificationDialog(this, DialogInfoType::info, PopupDialogCfg().setMainInstructions(_("Server name must not be empty."))); + m_textCtrlServer->SetFocus(); + return false; + } + } + + switch (type_) + { + case CloudType::gdrive: + if (m_listBoxGdriveUsers->GetSelection() == wxNOT_FOUND) + { + showNotificationDialog(this, DialogInfoType::info, PopupDialogCfg().setMainInstructions(_("Please select a user account first."))); + return false; + } + break; + + case CloudType::sftp: + //username *required* for SFTP, but optional for FTP: libcurl will use "anonymous" + if (trimCpy(m_textCtrlUserName->GetValue()).empty()) + { + showNotificationDialog(this, DialogInfoType::info, PopupDialogCfg().setMainInstructions(_("Please enter a username."))); + m_textCtrlUserName->SetFocus(); + return false; + } + + if (sftpAuthType_ == SftpAuthType::keyFile) + if (trimCpy(m_textCtrlKeyfilePath->GetValue()).empty()) + { + showNotificationDialog(this, DialogInfoType::info, PopupDialogCfg().setMainInstructions(_("Please enter a file path."))); + //don't show error icon to follow "Windows' encouraging tone" + m_textCtrlKeyfilePath->SetFocus(); + return false; + } + break; + + case CloudType::ftp: + break; + } + return true; +} + + +AbstractPath CloudSetupDlg::getFolderPath() const +{ + //clean up (messy) user input, but no trim: support folders with trailing blanks! + const AfsPath serverRelPath = sanitizeDeviceRelativePath(utfTo(m_textCtrlServerPath->GetValue())); + + switch (type_) + { + case CloudType::gdrive: + { + GdriveLogin login; + if (const int selPos = m_listBoxGdriveUsers->GetSelection(); + selPos != wxNOT_FOUND) + { + login.email = utfTo(m_listBoxGdriveUsers->GetString(selPos)); + + if (const int selPos2 = m_listBoxGdriveDrives->GetSelection(); + selPos2 != wxNOT_FOUND) + { + if (const wxString& locationName = m_listBoxGdriveDrives->GetString(selPos2); + locationName != txtMyDrive_ && + locationName != txtLoading_) + login.locationName = utfTo(locationName); + } + } + login.timeoutSec = m_spinCtrlTimeout->GetValue(); + return AbstractPath(condenseToGdriveDevice(login), serverRelPath); //noexcept + } + + case CloudType::sftp: + { + SftpLogin login; + login.server = utfTo(m_textCtrlServer ->GetValue()); + login.portCfg = stringTo (m_textCtrlPort ->GetValue()); //0 if empty + login.username = utfTo(m_textCtrlUserName->GetValue()); + login.authType = sftpAuthType_; + login.privateKeyFilePath = utfTo(m_textCtrlKeyfilePath->GetValue()); + if (m_checkBoxPasswordPrompt->GetValue()) + login.password = std::nullopt; + else + login.password = utfTo((m_checkBoxShowPassword->GetValue() ? m_textCtrlPasswordVisible : m_textCtrlPasswordHidden)->GetValue()); + login.allowZlib = m_checkBoxAllowZlib->GetValue(); + login.timeoutSec = m_spinCtrlTimeout->GetValue(); + login.traverserChannelsPerConnection = m_spinCtrlChannelCountSftp->GetValue(); + return AbstractPath(condenseToSftpDevice(login), serverRelPath); //noexcept + } + + case CloudType::ftp: + { + FtpLogin login; + login.server = utfTo(m_textCtrlServer ->GetValue()); + login.portCfg = stringTo (m_textCtrlPort ->GetValue()); //0 if empty + login.username = utfTo(m_textCtrlUserName->GetValue()); + if (m_checkBoxPasswordPrompt->GetValue()) + login.password = std::nullopt; + else + login.password = utfTo((m_checkBoxShowPassword->GetValue() ? m_textCtrlPasswordVisible : m_textCtrlPasswordHidden)->GetValue()); + login.useTls = m_radioBtnEncryptSsl->GetValue(); + login.timeoutSec = m_spinCtrlTimeout->GetValue(); + return AbstractPath(condenseToFtpDevice(login), serverRelPath); //noexcept + } + } + assert(false); + return createAbstractPath(Zstr("")); +} + + +void CloudSetupDlg::onBrowseCloudFolder(wxCommandEvent& event) +{ + if (!validateParameters()) + return; + + AbstractPath folderPath = getFolderPath(); //noexcept + try + { + //------------------------------------------------------------------- + auto requestPassword = [&, password = Zstring()](const std::wstring& msg, const std::wstring& lastErrorMsg) mutable + { + assert(runningOnMainThread()); + if (showPasswordPrompt(this, msg, lastErrorMsg, password) != ConfirmationButton::accept) + throw CancelProcess(); + return password; + }; + AFS::authenticateAccess(folderPath.afsDevice, requestPassword); //throw FileError, CancelProcess + //caveat: this could block *indefinitely* for Google Drive, but luckily already authenticated in this context + //------------------------------------------------------------------- + // + //for (S)FTP it makes more sense to start with the home directory rather than root (which often denies access!) + if (!AFS::getParentPath(folderPath)) + { + if (type_ == CloudType::sftp) + folderPath.afsPath = getSftpHomePath(extractSftpLogin(folderPath.afsDevice)); //throw FileError + + if (type_ == CloudType::ftp) + folderPath.afsPath = getFtpHomePath(extractFtpLogin(folderPath.afsDevice)); //throw FileError + } + } + catch (CancelProcess&) { return; } + catch (const FileError& e) + { + showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); + return; + } + + if (showAbstractFolderPicker(this, folderPath) == ConfirmationButton::accept) + m_textCtrlServerPath->ChangeValue(utfTo(FILE_NAME_SEPARATOR + folderPath.afsPath.value)); +} + + +void CloudSetupDlg::onOkay(wxCommandEvent& event) +{ + //------- parameter validation (BEFORE writing output!) ------- + if (!validateParameters()) + return; + //------------------------------------------------------------- + + folderPathPhraseOut_ = AFS::getInitPathPhrase(getFolderPath()); + parallelOpsOut_ = m_spinCtrlConnectionCount->GetValue(); + + EndModal(static_cast(ConfirmationButton::accept)); +} +} + +ConfirmationButton fff::showCloudSetupDialog(wxWindow* parent, Zstring& folderPathPhrase, Zstring& sftpKeyFileLastSelected, size_t& parallelOps, bool canChangeParallelOp) +{ + CloudSetupDlg dlg(parent, folderPathPhrase, sftpKeyFileLastSelected, parallelOps, canChangeParallelOp); + return static_cast(dlg.ShowModal()); +} + +//######################################################################################## + +namespace +{ +class CopyToDialog : public CopyToDlgGenerated +{ +public: + CopyToDialog(wxWindow* parent, + const std::wstring& itemList, int itemCount, + Zstring& targetFolderPath, Zstring& targetFolderLastSelected, + std::vector& folderHistory, size_t folderHistoryMax, + Zstring& sftpKeyFileLastSelected, + bool& keepRelPaths, + bool& overwriteIfExists); + +private: + void onOkay (wxCommandEvent& event) override; + void onCancel(wxCommandEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } + void onClose (wxCloseEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } + + void onLocalKeyEvent(wxKeyEvent& event); + + std::optional targetFolder; //always bound + + //output-only parameters: + Zstring& targetFolderPathOut_; + bool& keepRelPathsOut_; + bool& overwriteIfExistsOut_; + std::vector& folderHistoryOut_; +}; + + +CopyToDialog::CopyToDialog(wxWindow* parent, + const std::wstring& itemList, int itemCount, + Zstring& targetFolderPath, Zstring& targetFolderLastSelected, + std::vector& folderHistory, size_t folderHistoryMax, + Zstring& sftpKeyFileLastSelected, + bool& keepRelPaths, + bool& overwriteIfExists) : + CopyToDlgGenerated(parent), + targetFolderPathOut_(targetFolderPath), + keepRelPathsOut_(keepRelPaths), + overwriteIfExistsOut_(overwriteIfExists), + folderHistoryOut_(folderHistory) +{ + setStandardButtonLayout(*bSizerStdButtons, StdButtons().setAffirmative(m_buttonOK).setCancel(m_buttonCancel)); + + setMainInstructionFont(*m_staticTextHeader); + + setImage(*m_bitmapCopyTo, loadImage("copy_to")); + + targetFolder.emplace(this, *this, *m_buttonSelectTargetFolder, *m_bpButtonSelectAltTargetFolder, *m_targetFolderPath, + targetFolderLastSelected, sftpKeyFileLastSelected, nullptr /*staticText*/, nullptr /*wxWindow*/, nullptr /*droppedPathsFilter*/, + [](const Zstring& folderPathPhrase) { return 1; } /*getDeviceParallelOps*/, nullptr /*setDeviceParallelOps*/); + + m_targetFolderPath->setHistory(std::make_shared(folderHistory, folderHistoryMax)); + + m_textCtrlFileList->SetMinSize({dipToWxsize(500), dipToWxsize(200)}); + + /* There is a nasty bug on wxGTK under Ubuntu: If a multi-line wxTextCtrl contains so many lines that scrollbars are shown, + it re-enables all windows that are supposed to be disabled during the current modal loop! + This only affects Ubuntu/wxGTK! No such issue on Debian/wxGTK or Suse/wxGTK + => another Unity problem like the following? + https://github.com/wxWidgets/wxWidgets/issues/14823 "Menu not disabled when showing modal dialogs in wxGTK under Unity" */ + + m_staticTextHeader->SetLabelText(_P("Copy the following item to another folder?", + "Copy the following %x items to another folder?", itemCount)); + m_staticTextHeader->Wrap(dipToWxsize(460)); //needs to be reapplied after SetLabel() + + m_textCtrlFileList->ChangeValue(itemList); + + //----------------- set config --------------------------------- + targetFolder ->setPath(targetFolderPath); + m_checkBoxKeepRelPath ->SetValue(keepRelPaths); + m_checkBoxOverwriteIfExists->SetValue(overwriteIfExists); + //----------------- /set config -------------------------------- + + Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onLocalKeyEvent(event); }); //enable dialog-specific key events + + GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() +#ifdef __WXGTK3__ + Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 + //Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404 +#endif + Center(); //apply *after* dialog size change! + + m_buttonOK->SetFocus(); +} + + +void CopyToDialog::onLocalKeyEvent(wxKeyEvent& event) //process key events without explicit menu entry :) +{ + switch (event.GetKeyCode()) + { + case WXK_RETURN: + case WXK_NUMPAD_ENTER: + if (event.ControlDown()) //Ctrl+Enter or on macOS: Command+Enter + { + wxCommandEvent dummy(wxEVT_COMMAND_BUTTON_CLICKED); + m_buttonOK->Command(dummy); //simulate click + return; + } + break; + } + event.Skip(); +} + + +void CopyToDialog::onOkay(wxCommandEvent& event) +{ + //------- parameter validation (BEFORE writing output!) ------- + if (trimCpy(targetFolder->getPath()).empty()) + { + showNotificationDialog(this, DialogInfoType::info, PopupDialogCfg().setMainInstructions(_("Please enter a target folder."))); + //don't show error icon to follow "Windows' encouraging tone" + m_targetFolderPath->SetFocus(); + return; + } + m_targetFolderPath->getHistory()->addItem(targetFolder->getPath()); + //------------------------------------------------------------- + + targetFolderPathOut_ = targetFolder->getPath(); + keepRelPathsOut_ = m_checkBoxKeepRelPath->GetValue(); + overwriteIfExistsOut_ = m_checkBoxOverwriteIfExists->GetValue(); + folderHistoryOut_ = m_targetFolderPath->getHistory()->getList(); + + EndModal(static_cast(ConfirmationButton::accept)); +} +} + +ConfirmationButton fff::showCopyToDialog(wxWindow* parent, + const std::wstring& itemList, int itemCount, + Zstring& targetFolderPath, Zstring& targetFolderLastSelected, + std::vector& folderHistory, size_t folderHistoryMax, + Zstring& sftpKeyFileLastSelected, + bool& keepRelPaths, + bool& overwriteIfExists) +{ + CopyToDialog dlg(parent, itemList, itemCount, targetFolderPath, targetFolderLastSelected, folderHistory, folderHistoryMax, sftpKeyFileLastSelected, keepRelPaths, overwriteIfExists); + return static_cast(dlg.ShowModal()); +} + +//######################################################################################## + +namespace +{ +class DeleteDialog : public DeleteDlgGenerated +{ +public: + DeleteDialog(wxWindow* parent, + const std::wstring& itemList, int itemCount, + bool& useRecycleBin); + +private: + void onUseRecycler(wxCommandEvent& event) override { updateGui(); } + void onOkay (wxCommandEvent& event) override; + void onCancel (wxCommandEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } + void onClose (wxCloseEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } + + void onLocalKeyEvent(wxKeyEvent& event); + + void updateGui(); + + const int itemCount_ = 0; + const std::chrono::steady_clock::time_point dlgStartTime_ = std::chrono::steady_clock::now(); + + const wxImage imgTrash_ = [] + { + wxImage imgDefault = loadImage("delete_recycler"); + + //use system icon if available (can fail on Linux??) + try { return extractWxImage(fff::getTrashIcon(imgDefault.GetHeight())); /*throw SysError*/ } + catch (SysError&) { assert(false); return imgDefault; } + }(); + + //output-only parameters: + bool& useRecycleBinOut_; +}; + + +DeleteDialog::DeleteDialog(wxWindow* parent, + const std::wstring& itemList, int itemCount, + bool& useRecycleBin) : + DeleteDlgGenerated(parent), + itemCount_(itemCount), + useRecycleBinOut_(useRecycleBin) +{ + setStandardButtonLayout(*bSizerStdButtons, StdButtons().setAffirmative(m_buttonOK).setCancel(m_buttonCancel)); + + setMainInstructionFont(*m_staticTextHeader); + + m_textCtrlFileList->SetMinSize({dipToWxsize(500), dipToWxsize(200)}); + + wxString itemList2(itemList); + trim(itemList2); //remove trailing newline + m_textCtrlFileList->ChangeValue(itemList2); + /* There is a nasty bug on wxGTK under Ubuntu: If a multi-line wxTextCtrl contains so many lines that scrollbars are shown, + it re-enables all windows that are supposed to be disabled during the current modal loop! + This only affects Ubuntu/wxGTK! No such issue on Debian/wxGTK or Suse/wxGTK + => another Unity problem like the following? + https://github.com/wxWidgets/wxWidgets/issues/14823 "Menu not disabled when showing modal dialogs in wxGTK under Unity" */ + + m_checkBoxUseRecycler->SetValue(useRecycleBin); + + updateGui(); + + Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onLocalKeyEvent(event); }); //enable dialog-specific key events + + GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() +#ifdef __WXGTK3__ + Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 + //Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404 +#endif + Center(); //apply *after* dialog size change! + + m_buttonOK->SetFocus(); +} + + +void DeleteDialog::updateGui() +{ + if (m_checkBoxUseRecycler->GetValue()) + { + setImage(*m_bitmapDeleteType, imgTrash_); + m_staticTextHeader->SetLabelText(_P("Do you really want to move the following item to the recycle bin?", + "Do you really want to move the following %x items to the recycle bin?", itemCount_)); + m_buttonOK->SetLabelText(_("Move")); //no access key needed: use ENTER! + } + else + { + setImage(*m_bitmapDeleteType, loadImage("delete_permanently")); + m_staticTextHeader->SetLabelText(_P("Do you really want to delete the following item?", + "Do you really want to delete the following %x items?", itemCount_)); + m_buttonOK->SetLabelText(wxControl::RemoveMnemonics(_("&Delete"))); //no access key needed: use ENTER! + } + m_staticTextHeader->Wrap(dipToWxsize(460)); //needs to be reapplied after SetLabel() + + Layout(); + Refresh(); //needed after m_buttonOK label change +} + + +void DeleteDialog::onLocalKeyEvent(wxKeyEvent& event) +{ + switch (event.GetKeyCode()) + { + case WXK_RETURN: + case WXK_NUMPAD_ENTER: + if (event.ControlDown()) //Ctrl+Enter or on macOS: Command+Enter + { + wxCommandEvent dummy(wxEVT_COMMAND_BUTTON_CLICKED); + m_buttonOK->Command(dummy); //simulate click + return; + } + break; + } + event.Skip(); +} + + +void DeleteDialog::onOkay(wxCommandEvent& event) +{ + //additional safety net, similar to File Explorer: time delta between DEL and ENTER must be at least 50ms to avoid accidental deletion! + if (std::chrono::steady_clock::now() < dlgStartTime_ + std::chrono::milliseconds(50)) //considers chrono-wrap-around! + return; + + useRecycleBinOut_ = m_checkBoxUseRecycler->GetValue(); + + EndModal(static_cast(ConfirmationButton::accept)); +} +} + +ConfirmationButton fff::showDeleteDialog(wxWindow* parent, + const std::wstring& itemList, int itemCount, + bool& useRecycleBin) +{ + DeleteDialog dlg(parent, itemList, itemCount, useRecycleBin); + return static_cast(dlg.ShowModal()); +} + +//######################################################################################## + +namespace +{ +class SyncConfirmationDlg : public SyncConfirmationDlgGenerated +{ +public: + SyncConfirmationDlg(wxWindow* parent, + bool syncSelection, + std::optional syncVar, + const SyncStatistics& st, + bool& dontShowAgain); +private: + void onStartSync(wxCommandEvent& event) override; + void onCancel (wxCommandEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } + void onClose (wxCloseEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } + + void onLocalKeyEvent(wxKeyEvent& event); + + //output-only parameters: + bool& dontShowAgainOut_; +}; + + +SyncConfirmationDlg::SyncConfirmationDlg(wxWindow* parent, + bool syncSelection, + std::optional syncVar, + const SyncStatistics& st, + bool& dontShowAgain) : + SyncConfirmationDlgGenerated(parent), + dontShowAgainOut_(dontShowAgain) +{ + setStandardButtonLayout(*bSizerStdButtons, StdButtons().setAffirmative(m_buttonOK).setCancel(m_buttonCancel)); + + setMainInstructionFont(*m_staticTextCaption); + setImage(*m_bitmapSync, loadImage(syncSelection ? "start_sync_selection" : "start_sync")); + + m_staticTextCaption->SetLabelText(syncSelection ?_("Start to synchronize the selection?") : _("Start synchronization now?")); + m_staticTextSyncVar->SetLabelText(getVariantName(syncVar)); + + const char* varImgName = nullptr; + if (syncVar) + switch (*syncVar) + { + case SyncVariant::twoWay: varImgName = "sync_twoway"; break; + case SyncVariant::mirror: varImgName = "sync_mirror"; break; + case SyncVariant::update: varImgName = "sync_update"; break; + case SyncVariant::custom: varImgName = "sync_custom"; break; + } + if (varImgName) + setImage(*m_bitmapSyncVar, loadImage(varImgName, -1 /*maxWidth*/, dipToScreen(getMenuIconDipSize()))); + + m_checkBoxDontShowAgain->SetValue(dontShowAgain); + + Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onLocalKeyEvent(event); }); + + //update preview of item count and bytes to be transferred: + auto setValue = [](wxStaticText& txtControl, bool isZeroValue, const wxString& valueAsString, wxStaticBitmap& bmpControl, const char* imageName) + { + wxFont fnt = txtControl.GetFont(); + fnt.SetWeight(isZeroValue ? wxFONTWEIGHT_NORMAL : wxFONTWEIGHT_BOLD); + txtControl.SetFont(fnt); + + setText(txtControl, valueAsString); + + setImage(bmpControl, greyScaleIfDisabled(mirrorIfRtl(loadImage(imageName)), !isZeroValue)); + }; + + auto setIntValue = [&setValue](wxStaticText& txtControl, int value, wxStaticBitmap& bmpControl, const char* imageName) + { + setValue(txtControl, value == 0, formatNumber(value), bmpControl, imageName); + }; + + setValue(*m_staticTextData, st.getBytesToProcess() == 0, formatFilesizeShort(st.getBytesToProcess()), *m_bitmapData, "data"); + setIntValue(*m_staticTextCreateLeft, st.createCount(), *m_bitmapCreateLeft, "so_create_left_sicon"); + setIntValue(*m_staticTextUpdateLeft, st.updateCount(), *m_bitmapUpdateLeft, "so_update_left_sicon"); + setIntValue(*m_staticTextDeleteLeft, st.deleteCount(), *m_bitmapDeleteLeft, "so_delete_left_sicon"); + setIntValue(*m_staticTextCreateRight, st.createCount(), *m_bitmapCreateRight, "so_create_right_sicon"); + setIntValue(*m_staticTextUpdateRight, st.updateCount(), *m_bitmapUpdateRight, "so_update_right_sicon"); + setIntValue(*m_staticTextDeleteRight, st.deleteCount(), *m_bitmapDeleteRight, "so_delete_right_sicon"); + + GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() +#ifdef __WXGTK3__ + Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 + //Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404 +#endif + Center(); //apply *after* dialog size change! + + m_buttonOK->SetFocus(); +} + + +void SyncConfirmationDlg::onLocalKeyEvent(wxKeyEvent& event) +{ + switch (event.GetKeyCode()) + { + case WXK_RETURN: + case WXK_NUMPAD_ENTER: + if (event.ControlDown()) //Ctrl+Enter or on macOS: Command+Enter + { + wxCommandEvent dummy(wxEVT_COMMAND_BUTTON_CLICKED); + m_buttonOK->Command(dummy); //simulate click + return; + } + break; + } + event.Skip(); +} + + +void SyncConfirmationDlg::onStartSync(wxCommandEvent& event) +{ + dontShowAgainOut_ = m_checkBoxDontShowAgain->GetValue(); + EndModal(static_cast(ConfirmationButton::accept)); +} +} + +ConfirmationButton fff::showSyncConfirmationDlg(wxWindow* parent, + bool syncSelection, + std::optional syncVar, + const SyncStatistics& statistics, + bool& dontShowAgain) +{ + SyncConfirmationDlg dlg(parent, + syncSelection, + syncVar, + statistics, + dontShowAgain); + return static_cast(dlg.ShowModal()); +} + +//######################################################################################## + +namespace +{ +class OptionsDlg : public OptionsDlgGenerated +{ +public: + OptionsDlg(wxWindow* parent, GlobalConfig& globalCfg); + +private: + void onOkay (wxCommandEvent& event) override; + void onShowHiddenDialogs (wxCommandEvent& event) override { expandConfigArea(ConfigArea::hidden); }; + void onShowContextCustomize(wxCommandEvent& event) override { expandConfigArea(ConfigArea::context); }; + void onDefault (wxCommandEvent& event) override; + void onCancel (wxCommandEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } + void onClose (wxCloseEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } + void onAddRow (wxCommandEvent& event) override; + void onRemoveRow (wxCommandEvent& event) override; + void onShowLogFolder (wxCommandEvent& event) override; + void onToggleLogfilesLimit(wxCommandEvent& event) override { updateGui(); } + void onToggleHiddenDialog (wxCommandEvent& event) override { updateGui(); } + + void onSelectSoundCompareDone (wxCommandEvent& event) override { selectSound(*m_textCtrlSoundPathCompareDone); } + void onSelectSoundSyncDone (wxCommandEvent& event) override { selectSound(*m_textCtrlSoundPathSyncDone); } + void onSelectSoundAlertPending(wxCommandEvent& event) override { selectSound(*m_textCtrlSoundPathAlertPending); } + void selectSound(wxTextCtrl& txtCtrl); + + void onChangeSoundFilePath(wxCommandEvent& event) override { updateGui(); } + void onChangeColorTheme (wxCommandEvent& event) override { updateGui(); } + + void onPlayCompareDone (wxCommandEvent& event) override { playSoundWithDiagnostics(trimCpy(m_textCtrlSoundPathCompareDone ->GetValue())); } + void onPlaySyncDone (wxCommandEvent& event) override { playSoundWithDiagnostics(trimCpy(m_textCtrlSoundPathSyncDone ->GetValue())); } + void onPlayAlertPending(wxCommandEvent& event) override { playSoundWithDiagnostics(trimCpy(m_textCtrlSoundPathAlertPending->GetValue())); } + void playSoundWithDiagnostics(const wxString& filePath); + + void onGridResize(wxEvent& event); + void onGridContext(wxGridEvent& event); + void copySelectionToClipboard() const; + + void updateGui(); + + void onLocalKeyEvent(wxKeyEvent& event); + + enum class ConfigArea + { + hidden, + context + }; + void expandConfigArea(ConfigArea area); + + //work around defunct keyboard focus on macOS (or is it wxMac?) => not needed for this dialog! + //void onLocalKeyEvent(wxKeyEvent& event); + + void setExtApp(const std::vector& extApp); + std::vector getExtApp() const; + + std::unordered_map descriptionTransToEng_; //mapping for external application config + + const GlobalConfig defaultCfg_; + + EnumDescrList enumColorTheme_ + { + *m_choiceColorTheme, + { + {ColorTheme::System, wxControl::RemoveMnemonics(_("&Default")), {}/*tooltip*/}, + {ColorTheme::Light, _("Light"), {}/*tooltip*/}, + {ColorTheme::Dark, _("Dark"), {}/*tooltip*/}, + } + }; + + std::optional colorThemeIcon_; + + std::vector /*get dialog shown status*/, + std::function /*set dialog shown status*/, + wxString /*dialog message*/>> hiddenDialogCfgMapping_ + { + //*INDENT-OFF* + {[](const GlobalConfig& cfg){ return cfg.confirmDlgs.confirmSyncStart; }, + []( GlobalConfig& cfg, bool show){ cfg.confirmDlgs.confirmSyncStart = show; }, _("Start synchronization now?")}, + {[](const GlobalConfig& cfg){ return cfg.confirmDlgs.confirmSaveConfig; }, + []( GlobalConfig& cfg, bool show){ cfg.confirmDlgs.confirmSaveConfig = show; }, _("Do you want to save changes to %x?")}, + {[](const GlobalConfig& cfg){ return !cfg.progressDlgAutoClose; }, + []( GlobalConfig& cfg, bool show){ cfg.progressDlgAutoClose = !show; }, _("Leave progress dialog open after synchronization. (don't auto-close)")}, + {[](const GlobalConfig& cfg){ return cfg.confirmDlgs.confirmSwapSides; }, + []( GlobalConfig& cfg, bool show){ cfg.confirmDlgs.confirmSwapSides = show; }, _("Please confirm you want to swap sides.")}, + {[](const GlobalConfig& cfg){ return cfg.confirmDlgs.confirmCommandMassInvoke; }, + []( GlobalConfig& cfg, bool show){ cfg.confirmDlgs.confirmCommandMassInvoke = show; }, _P("Do you really want to execute the command %y for one item?", + "Do you really want to execute the command %y for %x items?", 42)}, + {[](const GlobalConfig& cfg){ return cfg.warnDlgs.warnFolderNotExisting; }, + []( GlobalConfig& cfg, bool show){ cfg.warnDlgs.warnFolderNotExisting = show; }, _("The following folders do not yet exist:") + L" [...] " + _("The folders are created automatically when needed.")}, + {[](const GlobalConfig& cfg){ return cfg.warnDlgs.warnFoldersDifferInCase; }, + []( GlobalConfig& cfg, bool show){ cfg.warnDlgs.warnFoldersDifferInCase = show; }, _("The following folder paths differ in case. Please use a single form in order to avoid duplicate accesses.")}, + {[](const GlobalConfig& cfg){ return cfg.warnDlgs.warnDependentFolderPair; }, + []( GlobalConfig& cfg, bool show){ cfg.warnDlgs.warnDependentFolderPair = show; }, _("One folder of the folder pair is a subfolder of the other.") + L' ' + _("The folder should be excluded via filter.")}, + {[](const GlobalConfig& cfg){ return cfg.warnDlgs.warnDependentBaseFolders; }, + []( GlobalConfig& cfg, bool show){ cfg.warnDlgs.warnDependentBaseFolders = show; }, _("Some files will be synchronized as part of multiple folder pairs.") + L' ' + _("To avoid conflicts, set up exclude filters so that each updated file is included by only one folder pair.")}, + {[](const GlobalConfig& cfg){ return cfg.warnDlgs.warnSignificantDifference; }, + []( GlobalConfig& cfg, bool show){ cfg.warnDlgs.warnSignificantDifference = show; }, _("The following folders are significantly different. Please check that the correct folders are selected for synchronization.")}, + {[](const GlobalConfig& cfg){ return cfg.warnDlgs.warnNotEnoughDiskSpace; }, + []( GlobalConfig& cfg, bool show){ cfg.warnDlgs.warnNotEnoughDiskSpace = show; }, _("Not enough free disk space available in:")}, + {[](const GlobalConfig& cfg){ return cfg.warnDlgs.warnUnresolvedConflicts; }, + []( GlobalConfig& cfg, bool show){ cfg.warnDlgs.warnUnresolvedConflicts = show; }, _("The following items have unresolved conflicts and will not be synchronized:")}, + {[](const GlobalConfig& cfg){ return cfg.warnDlgs.warnRecyclerMissing; }, + []( GlobalConfig& cfg, bool show){ cfg.warnDlgs.warnRecyclerMissing = show; }, _("The recycle bin is not available for %x.") + L' ' + _("Ignore and delete permanently each time recycle bin is unavailable?")}, + {[](const GlobalConfig& cfg){ return cfg.warnDlgs.warnDirectoryLockFailed; }, + []( GlobalConfig& cfg, bool show){ cfg.warnDlgs.warnDirectoryLockFailed = show; }, _("Cannot set directory locks for the following folders:")}, + {[](const GlobalConfig& cfg){ return cfg.warnDlgs.warnVersioningFolderPartOfSync; }, + []( GlobalConfig& cfg, bool show){ cfg.warnDlgs.warnVersioningFolderPartOfSync = show; }, _("The versioning folder must not be part of the synchronization.") + L' ' + _("The folder should be excluded via filter.")}, + //*INDENT-ON* + }; + + FolderSelector logFolderSelector_; + + //output-only parameters: + GlobalConfig& globalCfgOut_; +}; + + +OptionsDlg::OptionsDlg(wxWindow* parent, GlobalConfig& globalCfg) : + OptionsDlgGenerated(parent), + + logFolderSelector_(this, *m_panelLogfile, *m_buttonSelectLogFolder, *m_bpButtonSelectAltLogFolder, *m_logFolderPath, globalCfg.logFolderLastSelected, globalCfg.sftpKeyFileLastSelected, + nullptr /*staticText*/, nullptr /*dropWindow2*/, nullptr /*droppedPathsFilter*/, + [](const Zstring& folderPathPhrase) { return 1; } /*getDeviceParallelOps_*/, nullptr /*setDeviceParallelOps_*/), + globalCfgOut_(globalCfg) +{ + setStandardButtonLayout(*bSizerStdButtons, StdButtons().setAffirmative(m_buttonOK).setCancel(m_buttonCancel)); + + //setMainInstructionFont(*m_staticTextHeader); + + const wxImage imgFileManagerSmall_([] + { + try { return extractWxImage(fff::getFileManagerIcon(dipToScreen(20))); /*throw SysError*/ } + catch (SysError&) { assert(false); return loadImage("file_manager", dipToScreen(20)); } + }()); + setImage(*m_bpButtonShowLogFolder, imgFileManagerSmall_); + m_bpButtonShowLogFolder->SetToolTip(translate(extCommandFileManager.description));//translate default external apps on the fly: "Show in Explorer" + + m_logFolderPath->SetHint(utfTo(defaultCfg_.logFolderPhrase)); + //1. no text shown when control is disabled! 2. apparently there's a refresh problem on GTK + + m_logFolderPath->setHistory(std::make_shared(globalCfg.logFolderHistory, globalCfg.folderHistoryMax)); + + logFolderSelector_.setPath(globalCfg.logFolderPhrase); + + setDefaultWidth(*m_spinCtrlLogFilesMaxAge); + + setImage(*m_bitmapSettings, loadImage("settings")); + setImage(*m_bitmapWarnings, loadImage("msg_warning", dipToScreen(20))); + setImage(*m_bitmapLogFile, loadImage("log_file", dipToScreen(20))); + setImage(*m_bitmapNotificationSounds, loadImage("notification_sounds")); + setImage(*m_bitmapConsole, loadImage("command_line", dipToScreen(20))); + setImage(*m_bitmapCompareDone, loadImage("compare", dipToScreen(20))); + setImage(*m_bitmapSyncDone, loadImage("start_sync", dipToScreen(20))); + setImage(*m_bitmapAlertPending, loadImage("msg_error", dipToScreen(20))); + setImage(*m_bpButtonPlayCompareDone, loadImage("play_sound")); + setImage(*m_bpButtonPlaySyncDone, loadImage("play_sound")); + setImage(*m_bpButtonPlayAlertPending, loadImage("play_sound")); + setImage(*m_bpButtonAddRow, loadImage("item_add")); + setImage(*m_bpButtonRemoveRow, loadImage("item_remove")); + + //-------------------------------------------------------------------------------- + m_checkListHiddenDialogs->Hide(); + m_buttonShowCtxCustomize->Hide(); + + //fix wxCheckListBox's stupid "per-item toggle" when multiple items are selected + m_checkListHiddenDialogs->Bind(wxEVT_KEY_DOWN, [&checklist = *m_checkListHiddenDialogs](wxKeyEvent& event) + { + switch (event.GetKeyCode()) + { + case WXK_SPACE: + case WXK_NUMPAD_SPACE: + assert(checklist.HasMultipleSelection()); + + if (wxArrayInt selection; + checklist.GetSelections(selection), !selection.empty()) + { + const bool checkedNew = !checklist.IsChecked(selection[0]); + + for (const int itemPos : selection) + checklist.Check(itemPos, checkedNew); + + wxCommandEvent chkEvent(wxEVT_CHECKLISTBOX); + chkEvent.SetInt(selection[0]); + checklist.GetEventHandler()->ProcessEvent(chkEvent); + } + return; + } + event.Skip(); + }); + + std::stable_partition(hiddenDialogCfgMapping_.begin(), hiddenDialogCfgMapping_.end(), [&](const auto& item) + { + const auto& [dlgShown, dlgSetShown, msg] = item; + return !dlgShown(globalCfg); //move hidden dialogs to the top + }); + + std::vector dialogMessages; + for (const auto& [dlgShown, dlgSetShown, msg] : hiddenDialogCfgMapping_) + dialogMessages.push_back(msg); + + m_checkListHiddenDialogs->Append(dialogMessages); + + unsigned int itemPos = 0; + for (const auto& [dlgShown, dlgSetShown, msg] : hiddenDialogCfgMapping_) + { + if (dlgShown(globalCfg)) + m_checkListHiddenDialogs->Check(itemPos); + ++itemPos; + } + + //-------------------------------------------------------------------------------- + m_checkBoxFailSafe ->SetValue(globalCfg.failSafeFileCopy); + m_checkBoxCopyLocked ->SetValue(globalCfg.copyLockedFiles); + m_checkBoxCopyPermissions->SetValue(globalCfg.copyFilePermissions); + + bSizerColorTheme->Show(darkModeAvailable()); + enumColorTheme_.set(globalCfg.appColorTheme); + + m_checkBoxLogFilesMaxAge->SetValue(globalCfg.logfilesMaxAgeDays > 0); + m_spinCtrlLogFilesMaxAge->SetValue(globalCfg.logfilesMaxAgeDays > 0 ? globalCfg.logfilesMaxAgeDays : GlobalConfig().logfilesMaxAgeDays); + + switch (globalCfg.logFormat) + { + case LogFileFormat::html: + m_radioBtnLogHtml->SetValue(true); + break; + case LogFileFormat::text: + m_radioBtnLogText->SetValue(true); + break; + } + + m_textCtrlSoundPathCompareDone ->ChangeValue(utfTo(globalCfg.soundFileCompareFinished)); + m_textCtrlSoundPathSyncDone ->ChangeValue(utfTo(globalCfg.soundFileSyncFinished)); + m_textCtrlSoundPathAlertPending->ChangeValue(utfTo(globalCfg.soundFileAlertPending)); + //-------------------------------------------------------------------------------- + + bSizerLockedFiles->Show(false); + m_gridCustomCommand->SetMargins(0, 0); //for onGridResize(): ensure GetClientSize() calculations are correct + m_gridCustomCommand->SetTabBehaviour(wxGrid::Tab_Leave); + m_gridCustomCommand->SetSelectionMode(wxGrid::wxGridSelectRows); + + //getting rid of column header highlight the stupid way but the alternative "wxGrid::DisableOverlaySelection()" looks like ass + class wxGridCellAttrProviderNoColHighlight : public wxGridCellAttrProvider + { + const wxGridColumnHeaderRenderer& GetColumnHeaderRenderer(int col) override { return colRenderNoHighlight_; } + + class : public wxGridColumnHeaderRendererDefault + { + void DrawHighlighted(const wxGrid& grid, wxDC& dc, wxRect& rect, int col, int flags) const override { DrawBorder(grid, dc, rect); } + } colRenderNoHighlight_; + }; + m_gridCustomCommand->GetTable()->SetAttrProvider(new wxGridCellAttrProviderNoColHighlight); + + m_gridCustomCommand->GetGridWindow()->Bind(wxEVT_SIZE, [this](wxSizeEvent& event) { onGridResize(event); }); + m_gridCustomCommand->Bind(wxEVT_GRID_CELL_RIGHT_CLICK, [this](wxGridEvent& event) { onGridContext(event); }); + m_gridCustomCommand->Bind(wxEVT_GRID_LABEL_RIGHT_CLICK, [this](wxGridEvent& event) { onGridContext(event); }); + + m_gridCustomCommand->Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) + { + switch (event.GetKeyCode()) + { + //fix \src\generic\grid.cpp calling wxGrid::MoveCursorDown() after pressing ENTER: 1. instead of showing edit control 2. after ending edit mode + case WXK_RETURN: + case WXK_NUMPAD_ENTER: + m_gridCustomCommand->EnableCellEditControl(!m_gridCustomCommand->IsCellEditControlEnabled()); + return; + + case WXK_HOME: //make wxGrid behave like a list instead of a spreadsheet: + case WXK_END: //=> add fake CTRL to move up/down instead of left/right + { + assert(!m_gridCustomCommand->IsCellEditControlEnabled()); //cell edit already handles this event + event.m_controlDown = true; + break; + } + case 'A': //CTRL + A - select all + if (event.ControlDown()) + { + assert(!m_gridCustomCommand->IsCellEditControlEnabled()); //cell edit already handles this event + m_gridCustomCommand->SelectAll(); + return; + } + break; + + case 'C': + case WXK_INSERT: //CTRL + C || CTRL + INS + case WXK_NUMPAD_INSERT: + if (event.ControlDown()) + { + copySelectionToClipboard(); + return; + } + break; + } + event.Skip(); + }); + + m_gridCustomCommand->Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) + { + switch (event.GetKeyCode()) + { + case WXK_ESCAPE: //exit cell edit mode + undo (instead of cancelling the whole dialog!!!) + if (m_gridCustomCommand->IsCellEditControlEnabled()) + { + const wxGridCellCoords coords = m_gridCustomCommand->GetGridCursorCoords(); + assert(coords != wxGridNoCellCoords); //otherwise what exactly are we editting??? + const wxString oldVal = m_gridCustomCommand->GetCellValue(coords); + + m_gridCustomCommand->DisableCellEditControl(); //saves editted value, unless wxEVT_GRID_CELL_CHANGED is vetoed + m_gridCustomCommand->SetCellValue(coords, oldVal); //=> instead of veto, restore old value manually + return; + } + break; + } + event.Skip(); + }); + + //temporarily set dummy value for window height calculations: + setExtApp(std::vector(globalCfg.externalApps.size() + 1)); + updateGui(); + + Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onLocalKeyEvent(event); }); //enable dialog-specific key events + + GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() +#ifdef __WXGTK3__ + Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 + //Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404 +#endif + Center(); //apply *after* dialog size change! + + //restore actual value: + setExtApp(globalCfg.externalApps); + updateGui(); + + m_buttonOK->SetFocus(); +} + + +void OptionsDlg::onLocalKeyEvent(wxKeyEvent& event) //process key events without explicit menu entry :) +{ + switch (event.GetKeyCode()) + { + case WXK_RETURN: + case WXK_NUMPAD_ENTER: + if (event.ControlDown()) //Ctrl+Enter or on macOS: Command+Enter + { + m_gridCustomCommand->DisableCellEditControl(); + + wxCommandEvent dummy(wxEVT_COMMAND_BUTTON_CLICKED); + m_buttonOK->Command(dummy); //simulate click + return; + } + break; + } + event.Skip(); +} + + +//automatically fit column width to match total grid width +void OptionsDlg::onGridResize(wxEvent& event) +{ + const int widthTotal = m_gridCustomCommand->GetGridWindow()->GetClientSize().GetWidth(); + assert(m_gridCustomCommand->GetNumberCols() == 2); + + const int w0 = widthTotal * 2 / 5; //ratio 2 : 3 + const int w1 = widthTotal - w0; + m_gridCustomCommand->SetColSize(0, w0); + m_gridCustomCommand->SetColSize(1, w1); + + m_gridCustomCommand->Refresh(); //required on Ubuntu + event.Skip(); +} + + +void OptionsDlg::onGridContext(wxGridEvent& event) +{ + m_gridCustomCommand->SetFocus(); //ensure cell cursor is highlighted + + ContextMenu menu; + + const bool canCopy = m_gridCustomCommand->IsSelection() || m_gridCustomCommand->GetGridCursorCoords() != wxGridNoCellCoords; + menu.addItem(_("&Copy") + L"\tCtrl+C", [this] { copySelectionToClipboard(); }, loadImage("item_copy_sicon"), canCopy); + menu.addSeparator(); + + const int rowCount = m_gridCustomCommand->GetNumberRows(); + menu.addItem(_("Select all") + L"\tCtrl+A", [this] { m_gridCustomCommand->SelectAll(); }, wxNullImage, rowCount > 0); + + menu.popup(*m_gridCustomCommand, event.GetPosition()); +} + + +//why the fuck does wxGrid even allow multi-block selection and then fail in wxGrid::CopySelection()????????????? => fix [t... s...] +void OptionsDlg::copySelectionToClipboard() const +{ + const wxGridBlocks& selBlocks = m_gridCustomCommand->GetSelectedBlocks(); + + std::vector blocks(selBlocks.begin(), selBlocks.end()); + if (blocks.empty()) //=> select cursor position instead + if (const wxGridCellCoords curPos = m_gridCustomCommand->GetGridCursorCoords(); + curPos != wxGridNoCellCoords) + blocks.emplace_back(curPos.GetRow(), curPos.GetCol(), + curPos.GetRow(), curPos.GetCol()); + + wxString clipBuf; //perf: old wxString didn't model exponential growth, but now it's std::string-based: + static_assert(std::is_same_v); + + for (const wxGridBlockCoords& block : blocks) + for (int row = block.GetTopRow(); row <= block.GetBottomRow(); ++row) + for (int col = block.GetLeftCol(); col <= block.GetRightCol(); ++col) + { + clipBuf += m_gridCustomCommand->GetCellValue(row, col); + clipBuf += col == block.GetRightCol() ? L'\n' : L'\t'; + } + + setClipboardText(clipBuf); +} + + +void OptionsDlg::updateGui() +{ + if (!colorThemeIcon_ || *colorThemeIcon_ != enumColorTheme_.get()) //perf? don't update icon unless needed + switch (enumColorTheme_.get()) + { + case ColorTheme::System: + setImage(*m_bitmapColorTheme, loadImage("theme-default")); + break; + case ColorTheme::Light: + setImage(*m_bitmapColorTheme, loadImage("theme-light")); + break; + case ColorTheme::Dark: + setImage(*m_bitmapColorTheme, loadImage("theme-dark")); + break; + } + colorThemeIcon_ = enumColorTheme_.get(); + + m_spinCtrlLogFilesMaxAge->Enable(m_checkBoxLogFilesMaxAge->GetValue()); + + m_bpButtonPlayCompareDone ->Enable(!trimCpy(m_textCtrlSoundPathCompareDone ->GetValue()).empty()); + m_bpButtonPlaySyncDone ->Enable(!trimCpy(m_textCtrlSoundPathSyncDone ->GetValue()).empty()); + m_bpButtonPlayAlertPending->Enable(!trimCpy(m_textCtrlSoundPathAlertPending->GetValue()).empty()); + + int hiddenDialogs = 0; + for (unsigned int itemPos = 0; itemPos < hiddenDialogCfgMapping_.size(); ++itemPos) + if (!m_checkListHiddenDialogs->IsChecked(itemPos)) + ++hiddenDialogs; + assert(hiddenDialogCfgMapping_.size() == m_checkListHiddenDialogs->GetCount()); + + setText(*m_staticTextHiddenDialogsCount, L'(' + (hiddenDialogs == 0 ? _("No dialogs hidden") : + _P("1 dialog hidden", "%x dialogs hidden", hiddenDialogs)) + L')'); + Layout(); +} + + +void OptionsDlg::expandConfigArea(ConfigArea area) +{ + //only show one expanded area at a time (wxGTK even crashes when showing both: not worth debugging) + m_buttonShowHiddenDialogs->Show(area != ConfigArea::hidden); + m_buttonShowCtxCustomize ->Show(area != ConfigArea::context); + + m_checkListHiddenDialogs->Show(area == ConfigArea::hidden); + bSizerContextCustomize ->Show(area == ConfigArea::context); + + Layout(); + Refresh(); //required on Windows +} + + +void OptionsDlg::selectSound(wxTextCtrl& txtCtrl) +{ + std::optional defaultFolderPath = getParentFolderPath(utfTo(txtCtrl.GetValue())); + if (!defaultFolderPath) + defaultFolderPath = getResourceDirPath(); + + wxFileDialog fileSelector(this, wxString() /*message*/, utfTo(*defaultFolderPath), wxString() /*default file name*/, + wxString(L"WAVE (*.wav)|*.wav") + L"|" + _("All files") + L" (*.*)|*", + wxFD_OPEN); + if (fileSelector.ShowModal() != wxID_OK) + return; + + txtCtrl.ChangeValue(fileSelector.GetPath()); + updateGui(); +} + + +void OptionsDlg::playSoundWithDiagnostics(const wxString& filePath) +{ + try + { + //::PlaySound() on Windows does not set last error! + //wxSound::Play(..., wxSOUND_SYNC) can return "false", but also without details! + //=> check file access manually: + [[maybe_unused]] const std::string& stream = getFileContent(utfTo(filePath), nullptr /*notifyUnbufferedIO*/); //throw FileError + + if (!wxSound::Play(filePath, wxSOUND_ASYNC)) + throw FileError(L"Sound playback failed. No further diagnostics available."); + } + catch (const FileError& e) { showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); } +} + + +void OptionsDlg::onDefault(wxCommandEvent& event) +{ + m_checkBoxFailSafe ->SetValue(defaultCfg_.failSafeFileCopy); + m_checkBoxCopyLocked ->SetValue(defaultCfg_.copyLockedFiles); + m_checkBoxCopyPermissions->SetValue(defaultCfg_.copyFilePermissions); + + enumColorTheme_.set(defaultCfg_.appColorTheme); + + unsigned int itemPos = 0; + for (const auto& [dlgShown, dlgSetShown, msg] : hiddenDialogCfgMapping_) + m_checkListHiddenDialogs->Check(itemPos++, dlgShown(defaultCfg_)); + + logFolderSelector_.setPath(defaultCfg_.logFolderPhrase); + + m_checkBoxLogFilesMaxAge->SetValue(defaultCfg_.logfilesMaxAgeDays > 0); + m_spinCtrlLogFilesMaxAge->SetValue(defaultCfg_.logfilesMaxAgeDays > 0 ? defaultCfg_.logfilesMaxAgeDays : 14); + + switch (defaultCfg_.logFormat) + { + case LogFileFormat::html: + m_radioBtnLogHtml->SetValue(true); + break; + case LogFileFormat::text: + m_radioBtnLogText->SetValue(true); + break; + } + + m_textCtrlSoundPathCompareDone ->ChangeValue(utfTo(defaultCfg_.soundFileCompareFinished)); + m_textCtrlSoundPathSyncDone ->ChangeValue(utfTo(defaultCfg_.soundFileSyncFinished)); + m_textCtrlSoundPathAlertPending->ChangeValue(utfTo(defaultCfg_.soundFileAlertPending)); + + setExtApp(defaultCfg_.externalApps); + + updateGui(); +} + + +void OptionsDlg::onOkay(wxCommandEvent& event) +{ + //------- parameter validation (BEFORE writing output!) ------- + Zstring logFolderPhrase = logFolderSelector_.getPath(); + if (AFS::isNullPath(createAbstractPath(logFolderPhrase))) //no need to show an error: just set default! + logFolderPhrase = defaultCfg_.logFolderPhrase; + //------------------------------------------------------------- + + //write settings only when okay-button is pressed (except hidden dialog reset)! + globalCfgOut_.failSafeFileCopy = m_checkBoxFailSafe->GetValue(); + globalCfgOut_.copyLockedFiles = m_checkBoxCopyLocked->GetValue(); + globalCfgOut_.copyFilePermissions = m_checkBoxCopyPermissions->GetValue(); + + globalCfgOut_.appColorTheme = enumColorTheme_.get(); + + globalCfgOut_.logFolderPhrase = logFolderPhrase; + m_logFolderPath->getHistory()->addItem(logFolderPhrase); + globalCfgOut_.logFolderHistory = m_logFolderPath->getHistory()->getList(); + globalCfgOut_.logfilesMaxAgeDays = m_checkBoxLogFilesMaxAge->GetValue() ? m_spinCtrlLogFilesMaxAge->GetValue() : -1; + globalCfgOut_.logFormat = m_radioBtnLogHtml->GetValue() ? LogFileFormat::html : LogFileFormat::text; + + globalCfgOut_.soundFileCompareFinished = utfTo(trimCpy(m_textCtrlSoundPathCompareDone ->GetValue())); + globalCfgOut_.soundFileSyncFinished = utfTo(trimCpy(m_textCtrlSoundPathSyncDone ->GetValue())); + globalCfgOut_.soundFileAlertPending = utfTo(trimCpy(m_textCtrlSoundPathAlertPending->GetValue())); + + globalCfgOut_.externalApps = getExtApp(); + + unsigned int itemPos = 0; + for (const auto& [dlgShown, dlgSetShown, msg] : hiddenDialogCfgMapping_) + dlgSetShown(globalCfgOut_, m_checkListHiddenDialogs->IsChecked(itemPos++)); + + EndModal(static_cast(ConfirmationButton::accept)); +} + + +void OptionsDlg::setExtApp(const std::vector& extApps) +{ + int rowDiff = static_cast(extApps.size()) - m_gridCustomCommand->GetNumberRows(); + ++rowDiff; //append empty row to facilitate insertions by user + + if (rowDiff >= 0) + m_gridCustomCommand->AppendRows(rowDiff); + else + m_gridCustomCommand->DeleteRows(0, -rowDiff); + + int row = 0; + for (const auto& [descriptionEng, cmdLine] : extApps) + { + const std::wstring description = translate(descriptionEng); + //remember english description to save in GlobalSettings.xml later rather than hard-code translation + descriptionTransToEng_[description] = descriptionEng; + + m_gridCustomCommand->SetCellValue(row, 0, description); + m_gridCustomCommand->SetCellValue(row, 1, utfTo(cmdLine)); + ++row; + } +} + + +std::vector OptionsDlg::getExtApp() const +{ + std::vector output; + for (int i = 0; i < m_gridCustomCommand->GetNumberRows(); ++i) + { + auto description = copyStringTo(m_gridCustomCommand->GetCellValue(i, 0)); + auto commandline = utfTo(m_gridCustomCommand->GetCellValue(i, 1)); + + //try to undo translation of description for GlobalSettings.xml + auto it = descriptionTransToEng_.find(description); + if (it != descriptionTransToEng_.end()) + description = it->second; + + if (!description.empty() || !commandline.empty()) + output.push_back({description, commandline}); + } + return output; +} + + +void OptionsDlg::onAddRow(wxCommandEvent& event) +{ + const int selectedRow = m_gridCustomCommand->GetGridCursorRow(); + if (0 <= selectedRow && selectedRow < m_gridCustomCommand->GetNumberRows()) + m_gridCustomCommand->InsertRows(selectedRow); + else + m_gridCustomCommand->AppendRows(); + + m_gridCustomCommand->SetFocus(); //make grid cursor visible +} + + +void OptionsDlg::onRemoveRow(wxCommandEvent& event) +{ + if (m_gridCustomCommand->GetNumberRows() > 0) + { + const int selectedRow = m_gridCustomCommand->GetGridCursorRow(); + if (0 <= selectedRow && selectedRow < m_gridCustomCommand->GetNumberRows()) + m_gridCustomCommand->DeleteRows(selectedRow); + else + m_gridCustomCommand->DeleteRows(m_gridCustomCommand->GetNumberRows() - 1); + + m_gridCustomCommand->SetFocus(); //make grid cursor visible + } +} + + +void OptionsDlg::onShowLogFolder(wxCommandEvent& event) +{ + try + { + AbstractPath logFolderPath = createAbstractPath(logFolderSelector_.getPath()); + if (AFS::isNullPath(logFolderPath)) + logFolderPath = createAbstractPath(defaultCfg_.logFolderPhrase); + + openFolderInFileBrowser(logFolderPath); //throw FileError + } + catch (const FileError& e) { showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); } +} +} + +ConfirmationButton fff::showOptionsDlg(wxWindow* parent, GlobalConfig& globalCfg) +{ + OptionsDlg dlg(parent, globalCfg); + return static_cast(dlg.ShowModal()); +} + +//######################################################################################## + +namespace +{ +class SelectTimespanDlg : public SelectTimespanDlgGenerated +{ +public: + SelectTimespanDlg(wxWindow* parent, time_t& timeFrom, time_t& timeTo); + +private: + void onOkay (wxCommandEvent& event) override; + void onCancel(wxCommandEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } + void onClose (wxCloseEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } + + void onChangeSelectionFrom(wxCalendarEvent& event) override + { + if (m_calendarFrom->GetDate() > m_calendarTo->GetDate()) + m_calendarTo->SetDate(m_calendarFrom->GetDate()); + } + void onChangeSelectionTo(wxCalendarEvent& event) override + { + if (m_calendarFrom->GetDate() > m_calendarTo->GetDate()) + m_calendarFrom->SetDate(m_calendarTo->GetDate()); + } + + void onLocalKeyEvent(wxKeyEvent& event); + + //output-only parameters: + time_t& timeFromOut_; + time_t& timeToOut_; +}; + + +SelectTimespanDlg::SelectTimespanDlg(wxWindow* parent, time_t& timeFrom, time_t& timeTo) : + SelectTimespanDlgGenerated(parent), + timeFromOut_(timeFrom), + timeToOut_(timeTo) +{ + setStandardButtonLayout(*bSizerStdButtons, StdButtons().setAffirmative(m_buttonOK).setCancel(m_buttonCancel)); + + assert(m_calendarFrom->GetWindowStyle() == m_calendarTo->GetWindowStyle()); + assert(m_calendarFrom->HasFlag(wxCAL_SHOW_HOLIDAYS)); //caveat: for some stupid reason this is not honored when set by SetWindowStyle() + assert(m_calendarFrom->HasFlag(wxCAL_SHOW_SURROUNDING_WEEKS)); + assert(!m_calendarFrom->HasFlag(wxCAL_MONDAY_FIRST) && + !m_calendarFrom->HasFlag(wxCAL_SUNDAY_FIRST)); //...because we set it in the following: + long style = m_calendarFrom->GetWindowStyle(); + + style |= getFirstDayOfWeek() == WeekDay::sunday ? wxCAL_SUNDAY_FIRST : wxCAL_MONDAY_FIRST; //seems to be ignored on CentOS + + m_calendarFrom->SetWindowStyle(style); + m_calendarTo ->SetWindowStyle(style); + + //set default values + time_t timeFromTmp = timeFrom; + time_t timeToTmp = timeTo; + + if (timeToTmp == 0) + timeToTmp = std::time(nullptr); // + if (timeFromTmp == 0) + timeFromTmp = timeToTmp - 7 * 24 * 3600; //default time span: one week from "now" + + //wxDateTime models local(!) time (in contrast to what documentation says), but it has a constructor taking time_t UTC + m_calendarFrom->SetDate(timeFromTmp); + m_calendarTo ->SetDate(timeToTmp ); + + Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onLocalKeyEvent(event); }); //enable dialog-specific key events + + GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() +#ifdef __WXGTK3__ + Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 + //Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404 +#endif + Center(); //apply *after* dialog size change! + + m_buttonOK->SetFocus(); +} + + +void SelectTimespanDlg::onLocalKeyEvent(wxKeyEvent& event) //process key events without explicit menu entry :) +{ + switch (event.GetKeyCode()) + { + case WXK_RETURN: + case WXK_NUMPAD_ENTER: + if (event.ControlDown()) //Ctrl+Enter or on macOS: Command+Enter + { + wxCommandEvent dummy(wxEVT_COMMAND_BUTTON_CLICKED); + m_buttonOK->Command(dummy); //simulate click + return; + } + break; + } + event.Skip(); +} + + +void SelectTimespanDlg::onOkay(wxCommandEvent& event) +{ + wxDateTime from = m_calendarFrom->GetDate(); + wxDateTime to = m_calendarTo ->GetDate(); + + //align to full days + from.ResetTime(); + to .ResetTime(); //reset local(!) time + to += wxTimeSpan::Day(); + to -= wxTimeSpan::Second(); //go back to end of previous day + + timeFromOut_ = from.GetTicks(); + timeToOut_ = to .GetTicks(); + + EndModal(static_cast(ConfirmationButton::accept)); +} +} + +ConfirmationButton fff::showSelectTimespanDlg(wxWindow* parent, time_t& timeFrom, time_t& timeTo) +{ + SelectTimespanDlg dlg(parent, timeFrom, timeTo); + return static_cast(dlg.ShowModal()); +} + +//######################################################################################## + +namespace +{ +class PasswordPromptDlg : public PasswordPromptDlgGenerated +{ +public: + PasswordPromptDlg(wxWindow* parent, const std::wstring& msg, const std::wstring& lastErrorMsg /*optional*/, Zstring& password); + +private: + void onOkay (wxCommandEvent& event) override; + void onCancel(wxCommandEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } + void onClose (wxCloseEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } + + void onToggleShowPassword(wxCommandEvent& event) override; + + void updateGui(); + + //work around defunct keyboard focus on macOS (or is it wxMac?) => not needed for this dialog! + //void onLocalKeyEvent(wxKeyEvent& event); + + //output-only parameters: + Zstring& passwordOut_; +}; + + +PasswordPromptDlg::PasswordPromptDlg(wxWindow* parent, const std::wstring& msg, const std::wstring& lastErrorMsg /*optional*/, Zstring& password) : + PasswordPromptDlgGenerated(parent), + passwordOut_(password) +{ + setStandardButtonLayout(*bSizerStdButtons, StdButtons().setAffirmative(m_buttonOK).setCancel(m_buttonCancel)); + + wxString titleTmp; + if (!parent || !parent->IsShownOnScreen()) + titleTmp = wxTheApp->GetAppDisplayName(); + SetTitle(titleTmp); + + const int maxWidthDip = 600; + + m_staticTextMain->SetLabelText(msg); + m_staticTextMain->Wrap(dipToWxsize(maxWidthDip)); + + m_checkBoxShowPassword->SetValue(false); + + m_textCtrlPasswordHidden->ChangeValue(utfTo(password)); + + bSizerError->Show(!lastErrorMsg.empty()); + if (!lastErrorMsg.empty()) + { + setImage(*m_bitmapError, loadImage("msg_error", dipToWxsize(32))); + + m_staticTextError->SetLabelText(lastErrorMsg); + m_staticTextError->Wrap(dipToWxsize(maxWidthDip) - m_bitmapError->GetSize().x - 10 /*border in non-DIP pixel*/); + } + + //set up default view for dialog size calculation + m_textCtrlPasswordVisible->Hide(); + + GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() +#ifdef __WXGTK3__ + Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 + //Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404 +#endif + Center(); //apply *after* dialog size change! + + updateGui(); //*after* SetSizeHints when standard dialog height has been calculated + + //m_textCtrlPasswordHidden->SelectAll(); -> apparantly implicitly caused by SetFocus!? + m_textCtrlPasswordHidden->SetFocus(); +} + + +void PasswordPromptDlg::onToggleShowPassword(wxCommandEvent& event) +{ + if (m_checkBoxShowPassword->GetValue()) + m_textCtrlPasswordVisible->ChangeValue(m_textCtrlPasswordHidden->GetValue()); + else + m_textCtrlPasswordHidden->ChangeValue(m_textCtrlPasswordVisible->GetValue()); + + updateGui(); + + wxTextCtrl& textCtrl = *(m_checkBoxShowPassword->GetValue() ? m_textCtrlPasswordVisible : m_textCtrlPasswordHidden); + textCtrl.SetFocus(); //macOS: selects text as unwanted side effect => *before* SetInsertionPointEnd() + textCtrl.SetInsertionPointEnd(); +} + + +void PasswordPromptDlg::updateGui() +{ + m_textCtrlPasswordVisible->Show( m_checkBoxShowPassword->GetValue()); + m_textCtrlPasswordHidden ->Show(!m_checkBoxShowPassword->GetValue()); + + Layout(); + Refresh(); +} + + +void PasswordPromptDlg::onOkay(wxCommandEvent& event) +{ + passwordOut_ = utfTo((m_checkBoxShowPassword->GetValue() ? m_textCtrlPasswordVisible : m_textCtrlPasswordHidden)->GetValue()); + EndModal(static_cast(ConfirmationButton::accept)); +} +} + +ConfirmationButton fff::showPasswordPrompt(wxWindow* parent, const std::wstring& msg, const std::wstring& lastErrorMsg /*optional*/, Zstring& password) +{ + PasswordPromptDlg dlg(parent, msg, lastErrorMsg, password); + return static_cast(dlg.ShowModal()); +} + +//######################################################################################## + +namespace +{ +class CfgHighlightDlg : public CfgHighlightDlgGenerated +{ +public: + CfgHighlightDlg(wxWindow* parent, int& cfgHistSyncOverdueDays); + +private: + void onOkay (wxCommandEvent& event) override; + void onCancel(wxCommandEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } + void onClose (wxCloseEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } + + //work around defunct keyboard focus on macOS (or is it wxMac?) => not needed for this dialog! + //void onLocalKeyEvent(wxKeyEvent& event); + + //output-only parameters: + int& cfgHistSyncOverdueDaysOut_; +}; + + +CfgHighlightDlg::CfgHighlightDlg(wxWindow* parent, int& cfgHistSyncOverdueDays) : + CfgHighlightDlgGenerated(parent), + cfgHistSyncOverdueDaysOut_(cfgHistSyncOverdueDays) +{ + setStandardButtonLayout(*bSizerStdButtons, StdButtons().setAffirmative(m_buttonOK).setCancel(m_buttonCancel)); + + m_staticTextHighlight->Wrap(dipToWxsize(300)); + + setDefaultWidth(*m_spinCtrlOverdueDays); + + m_spinCtrlOverdueDays->SetValue(cfgHistSyncOverdueDays); + + GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() +#ifdef __WXGTK3__ + Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 + //Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404 +#endif + Center(); //apply *after* dialog size change! + + m_spinCtrlOverdueDays->SetFocus(); +} + + +void CfgHighlightDlg::onOkay(wxCommandEvent& event) +{ + cfgHistSyncOverdueDaysOut_ = m_spinCtrlOverdueDays->GetValue(); + EndModal(static_cast(ConfirmationButton::accept)); +} +} + +ConfirmationButton fff::showCfgHighlightDlg(wxWindow* parent, int& cfgHistSyncOverdueDays) +{ + CfgHighlightDlg dlg(parent, cfgHistSyncOverdueDays); + return static_cast(dlg.ShowModal()); +} + +//######################################################################################## + +namespace +{ +class ActivationDlg : public ActivationDlgGenerated +{ +public: + ActivationDlg(wxWindow* parent, const std::wstring& lastErrorMsg, const std::wstring& manualActivationUrl, std::wstring& manualActivationKey); + +private: + void onActivateOnline (wxCommandEvent& event) override; + void onActivateOffline(wxCommandEvent& event) override; + void onOfflineActivationEnter(wxCommandEvent& event) override { onActivateOffline(event); } + void onCopyUrl (wxCommandEvent& event) override; + void onCancel(wxCommandEvent& event) override { EndModal(static_cast(ActivationDlgButton::cancel)); } + void onClose (wxCloseEvent& event) override { EndModal(static_cast(ActivationDlgButton::cancel)); } + + std::wstring& manualActivationKeyOut_; //in/out parameter +}; + + +ActivationDlg::ActivationDlg(wxWindow* parent, + const std::wstring& lastErrorMsg, + const std::wstring& manualActivationUrl, + std::wstring& manualActivationKey) : + ActivationDlgGenerated(parent), + manualActivationKeyOut_(manualActivationKey) +{ + setStandardButtonLayout(*bSizerStdButtons, StdButtons().setCancel(m_buttonCancel)); + + std::wstring title = L"FreeFileSync " + utfTo(ffsVersion); + SetTitle(title); + + //setMainInstructionFont(*m_staticTextMain); + + m_richTextLastError ->SetMinSize({-1, m_richTextLastError ->GetCharHeight() * 8}); + m_richTextManualActivationUrl ->SetMinSize({-1, m_richTextManualActivationUrl->GetCharHeight() * 4}); + m_textCtrlOfflineActivationKey->SetMinSize({dipToWxsize(260), -1}); + + setImage(*m_bitmapActivation, loadImage("internet")); + m_textCtrlOfflineActivationKey->ForceUpper(); + + setTextWithUrls(*m_richTextLastError, lastErrorMsg); + setTextWithUrls(*m_richTextManualActivationUrl, manualActivationUrl); + + m_textCtrlOfflineActivationKey->ChangeValue(manualActivationKey); + + GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() +#ifdef __WXGTK3__ + Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 + //Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404 +#endif + Center(); //apply *after* dialog size change! + + m_buttonActivateOnline->SetFocus(); +} + + +void ActivationDlg::onCopyUrl(wxCommandEvent& event) +{ + setClipboardText(m_richTextManualActivationUrl->GetValue()); + + m_richTextManualActivationUrl->SetFocus(); //[!] otherwise selection is lost + m_richTextManualActivationUrl->SelectAll(); //some visual feedback +} + + +void ActivationDlg::onActivateOnline(wxCommandEvent& event) +{ + manualActivationKeyOut_ = utfTo(m_textCtrlOfflineActivationKey->GetValue()); + EndModal(static_cast(ActivationDlgButton::activateOnline)); +} + + +void ActivationDlg::onActivateOffline(wxCommandEvent& event) +{ + manualActivationKeyOut_ = utfTo(m_textCtrlOfflineActivationKey->GetValue()); + if (trimCpy(manualActivationKeyOut_).empty()) //alternative: disable button? => user thinks option is not available! + { + showNotificationDialog(this, DialogInfoType::info, PopupDialogCfg().setMainInstructions(_("Please enter a key for offline activation."))); + m_textCtrlOfflineActivationKey->SetFocus(); + return; + } + + EndModal(static_cast(ActivationDlgButton::activateOffline)); +} +} + +ActivationDlgButton fff::showActivationDialog(wxWindow* parent, const std::wstring& lastErrorMsg, const std::wstring& manualActivationUrl, std::wstring& manualActivationKey) +{ + ActivationDlg dlg(parent, lastErrorMsg, manualActivationUrl, manualActivationKey); + return static_cast(dlg.ShowModal()); +} + +//######################################################################################## + +class DownloadProgressWindow::Impl : public DownloadProgressDlgGenerated +{ +public: + Impl(wxWindow* parent, int64_t fileSizeTotal); + + void notifyNewFile (const Zstring& filePath) { filePath_ = filePath; } + void notifyProgress(int64_t delta) { bytesCurrent_ += delta; } + + void requestUiUpdate() //throw CancelPressed + { + if (cancelled_) + throw CancelPressed(); + + if (uiUpdateDue()) + { + updateGui(); + //wxTheApp->Yield(); + ::wxSafeYield(this); //disables user input except for "this" (using wxWindowDisabler instead would move the FFS main dialog into the background: why?) + } + } + +private: + void onCancel(wxCommandEvent& event) override { cancelled_ = true; } + + void updateGui() + { + const double fraction = bytesTotal_ == 0 ? 0 : 1.0 * bytesCurrent_ / bytesTotal_; + m_staticTextHeader->SetLabelText(_("Downloading update...") + L' ' + formatProgressPercent(fraction) + L" (" + formatFilesizeShort(bytesCurrent_) + L')'); + m_gaugeProgress->SetValue(std::floor(fraction * GAUGE_FULL_RANGE)); + + m_staticTextDetails->SetLabelText(utfTo(filePath_)); + } + + bool cancelled_ = false; + int64_t bytesCurrent_ = 0; + const int64_t bytesTotal_; + Zstring filePath_; + const int GAUGE_FULL_RANGE = 1000'000; +}; + + +DownloadProgressWindow::Impl::Impl(wxWindow* parent, int64_t fileSizeTotal) : + DownloadProgressDlgGenerated(parent), + bytesTotal_(fileSizeTotal) +{ + + setStandardButtonLayout(*bSizerStdButtons, StdButtons().setCancel(m_buttonCancel)); + + setMainInstructionFont(*m_staticTextHeader); + m_staticTextHeader->Wrap(dipToWxsize(460)); //*after* font change! + + m_staticTextDetails->SetMinSize({dipToWxsize(550), -1}); + + setImage(*m_bitmapDownloading, loadImage("internet")); + + m_gaugeProgress->SetRange(GAUGE_FULL_RANGE); + + updateGui(); + + GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() +#ifdef __WXGTK3__ + Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 + //Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404 +#endif + Center(); //apply *after* dialog size change! + + Show(); + + //clear gui flicker: window must be visible to make this work! + ::wxSafeYield(); //at least on OS X a real Yield() is required to flush pending GUI updates; Update() is not enough + + m_buttonCancel->SetFocus(); +} + + +DownloadProgressWindow::DownloadProgressWindow(wxWindow* parent, int64_t fileSizeTotal) : + pimpl_(new DownloadProgressWindow::Impl(parent, fileSizeTotal)) {} + +DownloadProgressWindow::~DownloadProgressWindow() { pimpl_->Destroy(); } + +void DownloadProgressWindow::notifyNewFile(const Zstring& filePath) { pimpl_->notifyNewFile(filePath); } +void DownloadProgressWindow::notifyProgress(int64_t delta) { pimpl_->notifyProgress(delta); } +void DownloadProgressWindow::requestUiUpdate() { pimpl_->requestUiUpdate(); } //throw CancelPressed + +//######################################################################################## + diff --git a/FreeFileSync/Source/ui/small_dlgs.h b/FreeFileSync/Source/ui/small_dlgs.h new file mode 100644 index 0000000..f29b3a9 --- /dev/null +++ b/FreeFileSync/Source/ui/small_dlgs.h @@ -0,0 +1,78 @@ +// ***************************************************************************** +// * 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 SMALL_DLGS_H_8321790875018750245 +#define SMALL_DLGS_H_8321790875018750245 + +#include +#include "../base/synchronization.h" +#include "../config.h" + + +namespace fff +{ +//parent window, optional: support correct dialog placement above parent on multiple monitor systems + +void showAboutDialog(wxWindow* parent); + +zen::ConfirmationButton showCopyToDialog(wxWindow* parent, + const std::wstring& itemList, int itemCount, + Zstring& targetFolderPath, Zstring& targetFolderLastSelected, + std::vector& folderPathHistory, size_t folderPathHistoryMax, + Zstring& sftpKeyFileLastSelected, + bool& keepRelPaths, + bool& overwriteIfExists); + +zen::ConfirmationButton showDeleteDialog(wxWindow* parent, + const std::wstring& itemList, int itemCount, + bool& useRecycleBin); + +zen::ConfirmationButton showSyncConfirmationDlg(wxWindow* parent, + bool syncSelection, + std::optional syncVar, + const SyncStatistics& statistics, + bool& dontShowAgain); + +zen::ConfirmationButton showOptionsDlg(wxWindow* parent, GlobalConfig& globalCfg); + +zen::ConfirmationButton showSelectTimespanDlg(wxWindow* parent, time_t& timeFrom, time_t& timeTo); + +zen::ConfirmationButton showPasswordPrompt(wxWindow* parent, const std::wstring& msg, const std::wstring& lastErrorMsg /*optional*/, Zstring& password); + +zen::ConfirmationButton showCfgHighlightDlg(wxWindow* parent, int& cfgHistSyncOverdueDays); + +zen::ConfirmationButton showCloudSetupDialog(wxWindow* parent, Zstring& folderPathPhrase, Zstring& sftpKeyFileLastSelected, + size_t& parallelOps, bool canChangeParallelOp); + +enum class ActivationDlgButton +{ + cancel, + activateOnline, + activateOffline, +}; +ActivationDlgButton showActivationDialog(wxWindow* parent, const std::wstring& lastErrorMsg, const std::wstring& manualActivationUrl, std::wstring& manualActivationKey); + + +class DownloadProgressWindow //temporary progress info => life-time: stack +{ +public: + DownloadProgressWindow(wxWindow* parent, int64_t fileSizeTotal); + ~DownloadProgressWindow(); + + struct CancelPressed {}; + void notifyNewFile(const Zstring& filePath); + void notifyProgress(int64_t delta); + void requestUiUpdate(); //throw CancelPressed + +private: + class Impl; + Impl* const pimpl_; +}; + + +} + +#endif //SMALL_DLGS_H_8321790875018750245 diff --git a/FreeFileSync/Source/ui/sync_cfg.cpp b/FreeFileSync/Source/ui/sync_cfg.cpp new file mode 100644 index 0000000..110a847 --- /dev/null +++ b/FreeFileSync/Source/ui/sync_cfg.cpp @@ -0,0 +1,1859 @@ +// ***************************************************************************** +// * 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 "sync_cfg.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "gui_generated.h" +#include "folder_selector.h" +#include "../base/norm_filter.h" +#include "../base/file_hierarchy.h" +#include "../base/icon_loader.h" +#include "../afs/concrete.h" +#include "../base_tools.h" + + + +using namespace zen; +using namespace fff; + + +namespace +{ +const int CFG_DESCRIPTION_WIDTH_DIP = 250; +const wchar_t arrowRight[] = L"\u2192"; //"RIGHTWARDS ARROW" + + +void initBitmapRadioButtons(const std::vector>& buttons, bool alignLeft) +{ + const bool physicalLeft = alignLeft == (wxTheApp->GetLayoutDirection() != wxLayout_RightToLeft); + + auto generateSelectImage = [physicalLeft](wxButton& btn, const std::string& imgName, bool selected) + { + wxImage imgTxt = createImageFromText(btn.GetLabelText(), btn.GetFont(), + selected ? *wxBLACK : //accessibility: always set both foreground AND background colors! see renderSelectedButton() + btn.GetForegroundColour()); + + wxImage imgIco = mirrorIfRtl(loadImage(imgName, -1 /*maxWidth*/, dipToScreen(getMenuIconDipSize()))); + + if (imgName == "delete_recycler") //use system icon if available (can fail on Linux??) + try { imgIco = extractWxImage(fff::getTrashIcon(dipToScreen(getMenuIconDipSize()))); /*throw SysError*/ } + catch (SysError&) { assert(false); } + + if (!selected) + imgIco = greyScale(imgIco); + + wxImage imgStack = physicalLeft ? + stackImages(imgIco, imgTxt, ImageStackLayout::horizontal, ImageStackAlignment::center, dipToScreen(5)) : + stackImages(imgTxt, imgIco, ImageStackLayout::horizontal, ImageStackAlignment::center, dipToScreen(5)); + + return resizeCanvas(imgStack, imgStack.GetSize() + wxSize(dipToScreen(14), dipToScreen(12)), wxALIGN_CENTER); + }; + + wxSize maxExtent; + std::unordered_map labelsNotSel; + for (auto& [btn, imgName] : buttons) + { + wxImage img = generateSelectImage(*btn, imgName, false /*selected*/); + maxExtent.x = std::max(maxExtent.x, img.GetWidth()); + maxExtent.y = std::max(maxExtent.y, img.GetHeight()); + + labelsNotSel[btn] = std::move(img); + } + + for (auto& [btn, imgName] : buttons) + { + btn->init(layOver(rectangleImage(maxExtent, getColorToggleButtonFill(), getColorToggleButtonBorder(), dipToScreen(1)), + generateSelectImage(*btn, imgName, true /*selected*/), wxALIGN_CENTER_VERTICAL | (physicalLeft ? wxALIGN_LEFT : wxALIGN_RIGHT)), + resizeCanvas(labelsNotSel[btn], maxExtent, wxALIGN_CENTER_VERTICAL | (physicalLeft ? wxALIGN_LEFT : wxALIGN_RIGHT))); + + btn->SetMinSize({screenToWxsize(maxExtent.x), + screenToWxsize(maxExtent.y)}); //get rid of selection border on Windows + macOS :) + //SetMinSize() instead of SetSize() is needed here for wxWindows layout determination to work correctly + } +} + + +bool sanitizeFilter(FilterConfig& filterCfg, const std::vector& baseFolderPaths, wxWindow* parent) +{ + //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 + + + //replace full paths by relative ones: frequent user error => help out: https://freefilesync.org/forum/viewtopic.php?t=9225 + auto normalizeForSearch = [](Zstring str) + { + //1. ignore Unicode normalization form 2. ignore case 3. normalize path separator + str = getUpperCase(str); //getUnicodeNormalForm() is implied by getUpperCase() + + if constexpr (FILE_NAME_SEPARATOR != Zstr('/' )) std::replace(str.begin(), str.end(), Zstr('/'), FILE_NAME_SEPARATOR); + if constexpr (FILE_NAME_SEPARATOR != Zstr('\\')) std::replace(str.begin(), str.end(), Zstr('\\'), FILE_NAME_SEPARATOR); + + return str; + }; + + std::vector folderPathsPf; //normalized + postfix path separator + { + const Zstring includeFilterNorm = normalizeForSearch(filterCfg.includeFilter); + const Zstring excludeFilterNorm = normalizeForSearch(filterCfg.excludeFilter); + + for (const AbstractPath& folderPath : baseFolderPaths) + if (!AFS::isNullPath(folderPath)) + if (const std::wstring& displayPath = AFS::getDisplayPath(folderPath); + !displayPath.empty()) + if (displayPath != L"/") //Linux/macOS: https://freefilesync.org/forum/viewtopic.php?t=9713 + if (const Zstring pathNormPf = appendSeparator(normalizeForSearch(utfTo(displayPath))); + contains(includeFilterNorm, pathNormPf) || //perf!? + contains(excludeFilterNorm, pathNormPf)) // + folderPathsPf.push_back(pathNormPf); + + removeDuplicates(folderPathsPf); + } + + + std::vector> replacements; + + auto replaceFullPaths = [&](Zstring& filterPhrase) + { + Zstring filterPhraseNew; + const Zchar* itFilterOrig = filterPhrase.begin(); + + split2(filterPhrase, [](Zchar c) { return c == FILTER_ITEM_SEPARATOR || c == Zstr('\n'); }, //delimiters + [&](const ZstringView phrase) + { + const ZstringView phraseTrm = trimCpy(phrase); + if (!phraseTrm.empty()) + { + const Zstring phraseNorm = normalizeForSearch(Zstring{phraseTrm}); + + for (const Zstring& pathNormPf : folderPathsPf) + if (startsWith(phraseNorm, pathNormPf)) + { + //emulate a "normalized afterFirst()": + ptrdiff_t sepCount = std::count(pathNormPf.begin(), pathNormPf.end(), FILE_NAME_SEPARATOR); + assert(sepCount > 0); + + for (auto it = phraseTrm.begin(); it != phraseTrm.end(); ++it) + if (*it == Zstr('/') || + *it == Zstr('\\')) + if (--sepCount == 0) + { + const Zstring relPath(it, phraseTrm.end()); //include first path separator + + filterPhraseNew.append(itFilterOrig, phraseTrm.data()); + filterPhraseNew += relPath; + itFilterOrig = phraseTrm.data() + phraseTrm.size(); + + replacements.emplace_back(phraseTrm, relPath); + return; //... to next block + } + throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); + } + } + }); + + if (itFilterOrig != filterPhrase.begin()) //perf!? + { + filterPhraseNew.append(itFilterOrig, filterPhrase.cend()); + filterPhrase = std::move(filterPhraseNew); + } + }; + replaceFullPaths(filterCfg.includeFilter); + replaceFullPaths(filterCfg.excludeFilter); + + if (!replacements.empty()) + { + std::wstring detailsMsg; + for (const auto& [from, to] : replacements) + if (to.empty()) + detailsMsg += _("Remove:") + L' ' + utfTo(from) + L'\n'; + else + detailsMsg += utfTo(from) + L' ' + arrowRight + L' ' + utfTo(to) + L'\n'; + detailsMsg.pop_back(); + + switch (showConfirmationDialog(parent, DialogInfoType::info, PopupDialogCfg(). + setMainInstructions(_("Each filter item must be a path relative to the selected folder pairs. The following changes are suggested:")). + setDetailInstructions(detailsMsg), _("&Change"))) + { + case ConfirmationButton::accept: //change + break; + + case ConfirmationButton::cancel: + return false; + } + } + return true; +} + +//========================================================================== + +class ConfigDialog : public ConfigDlgGenerated +{ +public: + ConfigDialog(wxWindow* parent, + SyncConfigPanel panelToShow, + int localPairIndexToShow, bool showMultipleCfgs, + GlobalPairConfig& globalPairCfg, + std::vector& localPairCfg, + FilterConfig& defaultFilter, + std::vector& versioningFolderHistory, Zstring& versioningFolderLastSelected, + std::vector& logFolderHistory, Zstring& logFolderLastSelected, const Zstring& globalLogFolderPhrase, + size_t folderHistoryMax, Zstring& sftpKeyFileLastSelected, + std::vector& emailHistory, size_t emailHistoryMax, + std::vector& commandHistory, size_t commandHistoryMax); + + ~ConfigDialog(); + +private: + void onOkay (wxCommandEvent& event) override; + void onCancel(wxCommandEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } + void onClose (wxCloseEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } + + void onAddNotes(wxCommandEvent& event) override; + + void onLocalKeyEvent(wxKeyEvent& event); + void onListBoxKeyEvent(wxKeyEvent& event) override; + void onSelectFolderPair(wxCommandEvent& event) override; + + enum class ConfigTypeImage + { + compare = 0, //used as zero-based wxImageList index! + compareGrey, + filter, + filterGrey, + sync, + syncGrey, + }; + + //------------- comparison panel ---------------------- + void onToggleLocalCompSettings(wxCommandEvent& event) override { updateCompGui(); updateSyncGui(); /*affects sync settings, too!*/ } + void onToggleIgnoreErrors (wxCommandEvent& event) override { updateMiscGui(); } + void onToggleAutoRetry (wxCommandEvent& event) override { updateMiscGui(); } + + void onCompByTimeSize (wxCommandEvent& event) override { localCmpVar_ = CompareVariant::timeSize; updateCompGui(); updateSyncGui(); } // + void onCompByContent (wxCommandEvent& event) override { localCmpVar_ = CompareVariant::content; updateCompGui(); updateSyncGui(); } //affects sync settings, too! + void onCompBySize (wxCommandEvent& event) override { localCmpVar_ = CompareVariant::size; updateCompGui(); updateSyncGui(); } // + void onCompByTimeSizeDouble(wxMouseEvent& event) override; + void onCompByContentDouble (wxMouseEvent& event) override; + void onCompBySizeDouble (wxMouseEvent& event) override; + void onChangeCompOption (wxCommandEvent& event) override { updateCompGui(); } + + std::optional getCompConfig() const; + void setCompConfig(const CompConfig* compCfg); + + void updateCompGui(); + + CompareVariant localCmpVar_ = CompareVariant::timeSize; + + std::set devicesForEdit_; //helper data for deviceParallelOps + std::map deviceParallelOps_; // + + //------------- filter panel -------------------------- + void onChangeFilterOption(wxCommandEvent& event) override { updateFilterGui(); } + void onFilterClear (wxCommandEvent& event) override { setFilterConfig(FilterConfig()); } + void onFilterDefault (wxCommandEvent& event) override { setFilterConfig(defaultFilterOut_); } + + void onFilterDefaultContext (wxCommandEvent& event) override { onFilterDefaultContext(static_cast(event)); } + void onFilterDefaultContextMouse(wxMouseEvent& event) override { onFilterDefaultContext(static_cast(event)); } + void onFilterDefaultContext(wxEvent& event); + + FilterConfig getFilterConfig() const; + void setFilterConfig(const FilterConfig& filter); + + void updateFilterGui(); + + EnumDescrList enumTimeDescr_ + { + *m_choiceUnitTimespan, + { + {UnitTime::none, L'(' + _("None") + L')', {}}, //meta options should be enclosed in parentheses + {UnitTime::today, _("Today"), {}}, + //{UnitTime::THIS_WEEK, _("This week"), {}}, + {UnitTime::thisMonth, _("This month"), {}}, + {UnitTime::thisYear, _("This year"), {}}, + {UnitTime::lastDays, _("Last x days:"), {}}, + } + }; + EnumDescrList enumMinSizeDescr_ + { + *m_choiceUnitMinSize, + { + {UnitSize::none, L'(' + _("None") + L')', {}}, //meta options should be enclosed in parentheses + {UnitSize::byte, _("Byte"), {}}, + {UnitSize::kb, _("KB"), {}}, + {UnitSize::mb, _("MB"), {}}, + } + }; + + EnumDescrList enumMaxSizeDescr_{*m_choiceUnitMaxSize, enumMinSizeDescr_.getConfig()}; + + //------------- synchronization panel ----------------- + void onSyncTwoWay(wxCommandEvent& event) override { directionsCfg_ = getDefaultSyncCfg(SyncVariant::twoWay); updateSyncGui(); } + void onSyncMirror(wxCommandEvent& event) override { directionsCfg_ = getDefaultSyncCfg(SyncVariant::mirror); updateSyncGui(); } + void onSyncUpdate(wxCommandEvent& event) override { directionsCfg_ = getDefaultSyncCfg(SyncVariant::update); updateSyncGui(); } + void onSyncCustom(wxCommandEvent& event) override { directionsCfg_ = getDefaultSyncCfg(SyncVariant::custom); updateSyncGui(); } + + void onToggleLocalSyncSettings(wxCommandEvent& event) override { updateSyncGui(); } + void onToggleUseDatabase (wxCommandEvent& event) override; + void onChangeVersioningStyle (wxCommandEvent& event) override { updateSyncGui(); } + void onToggleVersioningLimit (wxCommandEvent& event) override { updateSyncGui(); } + + void onSyncTwoWayDouble(wxMouseEvent& event) override; + void onSyncMirrorDouble(wxMouseEvent& event) override; + void onSyncUpdateDouble(wxMouseEvent& event) override; + void onSyncCustomDouble(wxMouseEvent& event) override; + + void onLeftOnly (wxCommandEvent& event) override { toggleSyncDirButton(&DirectionByDiff::leftOnly); } + void onRightOnly (wxCommandEvent& event) override { toggleSyncDirButton(&DirectionByDiff::rightOnly); } + void onLeftNewer (wxCommandEvent& event) override; + void onRightNewer(wxCommandEvent& event) override; + void onDifferent (wxCommandEvent& event) override; + void toggleSyncDirButton(SyncDirection DirectionByDiff::* dir); + + void onLeftCreate (wxCommandEvent& event) override { toggleSyncDirButton(&DirectionByChange::left, &DirectionByChange::Changes::create); } + void onLeftUpdate (wxCommandEvent& event) override { toggleSyncDirButton(&DirectionByChange::left, &DirectionByChange::Changes::update); } + void onLeftDelete (wxCommandEvent& event) override { toggleSyncDirButton(&DirectionByChange::left, &DirectionByChange::Changes::delete_); } + void onRightCreate(wxCommandEvent& event) override { toggleSyncDirButton(&DirectionByChange::right, &DirectionByChange::Changes::create); } + void onRightUpdate(wxCommandEvent& event) override { toggleSyncDirButton(&DirectionByChange::right, &DirectionByChange::Changes::update); } + void onRightDelete(wxCommandEvent& event) override { toggleSyncDirButton(&DirectionByChange::right, &DirectionByChange::Changes::delete_); } + void toggleSyncDirButton(DirectionByChange::Changes DirectionByChange::* side, SyncDirection DirectionByChange::Changes::* dir); + + void onDeletionPermanent (wxCommandEvent& event) override { deletionVariant_ = DeletionVariant::permanent; updateSyncGui(); } + void onDeletionRecycler (wxCommandEvent& event) override { deletionVariant_ = DeletionVariant::recycler; updateSyncGui(); } + void onDeletionVersioning(wxCommandEvent& event) override { deletionVariant_ = DeletionVariant::versioning; updateSyncGui(); } + + void onToggleMiscOption(wxCommandEvent& event) override { updateMiscGui(); } + void onToggleMiscEmail (wxCommandEvent& event) override + { + onToggleMiscOption(event); + if (event.IsChecked()) //optimize UX + m_comboBoxEmail->SetFocus(); // + } + 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(); } + + void onShowLogFolder(wxCommandEvent& event) override; + + std::optional getSyncConfig() const; + void setSyncConfig(const SyncConfig* syncCfg); + + bool leftRightNewerCombined() const; + + void updateSyncGui(); + //----------------------------------------------------- + + //parameters with ownership NOT within GUI controls! + SyncDirectionConfig directionsCfg_; + DeletionVariant deletionVariant_ = DeletionVariant::recycler; //use Recycler, delete permanently or move to user-defined location + + const std::function getDeviceParallelOps_; + const std::function setDeviceParallelOps_; + + FolderSelector versioningFolder_; + EnumDescrList enumVersioningStyle_ + { + *m_choiceVersioningStyle, + { + {VersioningStyle::replace, _("Replace"), _("Move files and replace if existing")}, + {VersioningStyle::timestampFolder, _("Time stamp") + L" [" + _("Folder") + L']', _("Move files into a time-stamped subfolder")}, + {VersioningStyle::timestampFile, _("Time stamp") + L" [" + _("File") + L']', _("Append a time stamp to each file name")}, + } + }; + + ResultsNotification emailNotifyCondition_ = ResultsNotification::always; + + EnumDescrList enumPostSyncCondition_ + { + *m_choicePostSyncCondition, + { + {PostSyncCondition::completion, _("On completion:"), {}}, + {PostSyncCondition::errors, _("On errors:"), {}}, + {PostSyncCondition::success, _("On success:"), {}}, + } + }; + + FolderSelector logFolderSelector_; + //----------------------------------------------------- + + MiscSyncConfig getMiscSyncOptions() const; + void setMiscSyncOptions(const MiscSyncConfig& miscCfg); + + void updateMiscGui(); + + //----------------------------------------------------- + + void selectFolderPairConfig(int newPairIndexToShow); + bool unselectFolderPairConfig(bool validateParams); //returns false on error: shows message box! + + //output parameters (sync config) + GlobalPairConfig& globalPairCfgOut_; + std::vector& localPairCfgOut_; + //output parameters (global) -> ignores OK/Cancel + FilterConfig& defaultFilterOut_; + std::vector& versioningFolderHistoryOut_; + std::vector& logFolderHistoryOut_; + std::vector& emailHistoryOut_; + std::vector& commandHistoryOut_; + + //working copy of ALL config parameters: only one folder pair is selected at a time! + GlobalPairConfig globalPairCfg_; + std::vector localPairCfg_; + + int selectedPairIndexToShow_ = EMPTY_PAIR_INDEX_SELECTED; + static constexpr int EMPTY_PAIR_INDEX_SELECTED = -2; + + bool showNotesPanel_ = false; + + const bool enableExtraFeatures_; + const bool showMultipleCfgs_; + + const Zstring globalLogFolderPhrase_; +}; + +//################################################################################################################# + +std::wstring getCompVariantDescription(CompareVariant var) +{ + switch (var) + { + case CompareVariant::timeSize: + return _("Identify equal files by comparing modification time and size."); + case CompareVariant::content: + return _("Identify equal files by comparing the file content."); + case CompareVariant::size: + return _("Identify equal files by comparing their file size."); + } + assert(false); + return _("Error"); +} + + +std::wstring getSyncVariantDescription(SyncVariant var) +{ + switch (var) + { + case SyncVariant::twoWay: + return _("Identify and propagate changes on both sides. Deletions, moves and conflicts are detected automatically using a database."); + case SyncVariant::mirror: + return _("Create a mirror backup of the left folder by adapting the right folder to match."); + case SyncVariant::update: + return _("Copy new and updated files to the right folder."); + case SyncVariant::custom: + return _("Configure your own synchronization rules."); + } + assert(false); + return _("Error"); +} + +//========================================================================== + +ConfigDialog::ConfigDialog(wxWindow* parent, + SyncConfigPanel panelToShow, + int localPairIndexToShow, bool showMultipleCfgs, + GlobalPairConfig& globalPairCfg, + std::vector& localPairCfg, + FilterConfig& defaultFilter, + std::vector& versioningFolderHistory, Zstring& versioningFolderLastSelected, + std::vector& logFolderHistory, Zstring& logFolderLastSelected, + const Zstring& globalLogFolderPhrase, + size_t folderHistoryMax, Zstring& sftpKeyFileLastSelected, + std::vector& emailHistory, size_t emailHistoryMax, + std::vector& commandHistory, size_t commandHistoryMax) : + ConfigDlgGenerated(parent), + + getDeviceParallelOps_([this](const Zstring& folderPathPhrase) +{ + assert(selectedPairIndexToShow_ == -1 || makeUnsigned(selectedPairIndexToShow_) < localPairCfg_.size()); + const auto& deviceParallelOps = selectedPairIndexToShow_ < 0 ? getMiscSyncOptions().deviceParallelOps : globalPairCfg_.miscCfg.deviceParallelOps; //ternary-WTF! + + return getDeviceParallelOps(deviceParallelOps, folderPathPhrase); +}), + +setDeviceParallelOps_([this](const Zstring& folderPathPhrase, size_t parallelOps) //setDeviceParallelOps() +{ + assert(selectedPairIndexToShow_ == -1 || makeUnsigned(selectedPairIndexToShow_) < localPairCfg_.size()); + if (selectedPairIndexToShow_ < 0) + { + MiscSyncConfig miscCfg = getMiscSyncOptions(); + setDeviceParallelOps(miscCfg.deviceParallelOps, folderPathPhrase, parallelOps); + setMiscSyncOptions(miscCfg); + } + else + setDeviceParallelOps(globalPairCfg_.miscCfg.deviceParallelOps, folderPathPhrase, parallelOps); +}), + +versioningFolder_(this, *m_panelVersioning, *m_buttonSelectVersioningFolder, *m_bpButtonSelectVersioningAltFolder, *m_versioningFolderPath, versioningFolderLastSelected, sftpKeyFileLastSelected, + nullptr /*staticText*/, nullptr /*dropWindow2*/, nullptr /*droppedPathsFilter*/, getDeviceParallelOps_, setDeviceParallelOps_), + +logFolderSelector_(this, *m_panelLogfile, *m_buttonSelectLogFolder, *m_bpButtonSelectAltLogFolder, *m_logFolderPath, logFolderLastSelected, sftpKeyFileLastSelected, + nullptr /*staticText*/, nullptr /*dropWindow2*/, nullptr /*droppedPathsFilter*/, getDeviceParallelOps_, setDeviceParallelOps_), + +globalPairCfgOut_(globalPairCfg), +localPairCfgOut_(localPairCfg), +defaultFilterOut_(defaultFilter), +versioningFolderHistoryOut_(versioningFolderHistory), +logFolderHistoryOut_(logFolderHistory), +emailHistoryOut_(emailHistory), +commandHistoryOut_(commandHistory), +globalPairCfg_(globalPairCfg), +localPairCfg_(localPairCfg), +showNotesPanel_(!globalPairCfg.miscCfg.notes.empty()), + enableExtraFeatures_(false), +showMultipleCfgs_(showMultipleCfgs), +globalLogFolderPhrase_(globalLogFolderPhrase) +{ + assert(!AFS::isNullPath(createAbstractPath(globalLogFolderPhrase_))); + + setStandardButtonLayout(*bSizerStdButtons, StdButtons().setAffirmative(m_buttonOK).setCancel(m_buttonCancel)); + + + setBitmapTextLabel(*m_buttonAddNotes, loadImage("notes", dipToScreen(16)), m_buttonAddNotes->GetLabelText()); + + setImage(*m_bitmapNotes, loadImage("notes", dipToScreen(20))); + + //set reasonable default height for notes: simplistic algorithm neglecting line-wrap! + int notesRows = 1; + for (wchar_t c : trimCpy(globalPairCfg.miscCfg.notes)) + if (c == L'\n') + ++notesRows; + + double visibleRows = 5; + if (showNotesPanel_) + visibleRows = notesRows <= 10 ? notesRows : 10.5; //add half a row as visual hint + m_textCtrNotes->SetMinSize({-1, getTextCtrlHeight(*m_textCtrNotes, visibleRows)}); + + + m_notebook->SetPadding(wxSize(dipToWxsize(2), 0)); //height cannot be changed + + //fill image list to cope with wxNotebook image setting design desaster... + const int imgListSize = dipToWxsize(16); //also required by GTK => don't use getMenuIconDipSize() + auto imgList = std::make_unique(imgListSize, imgListSize); + + auto addToImageList = [&](const wxImage& img) + { + imgList->Add(toScaledBitmap(img)); + imgList->Add(toScaledBitmap(greyScale(img))); + }; + //add images in same sequence like ConfigTypeImage enum!!! + addToImageList(loadImage("options_compare", wxsizeToScreen(imgListSize))); + addToImageList(loadImage("options_filter", wxsizeToScreen(imgListSize))); + addToImageList(loadImage("options_sync", wxsizeToScreen(imgListSize))); + assert(imgList->GetImageCount() == static_cast(ConfigTypeImage::syncGrey) + 1); + + m_notebook->AssignImageList(imgList.release()); //pass ownership + + + m_notebook->SetPageText(static_cast(SyncConfigPanel::compare), _("Comparison") + L" (F6)"); + m_notebook->SetPageText(static_cast(SyncConfigPanel::filter ), _("Filter") + L" (F7)"); + m_notebook->SetPageText(static_cast(SyncConfigPanel::sync ), _("Synchronization") + L" (F8)"); + + m_notebook->ChangeSelection(static_cast(panelToShow)); + + //------------- comparison panel ---------------------- + setRelativeFontSize(*m_buttonByTimeSize, 1.25); + setRelativeFontSize(*m_buttonByContent, 1.25); + setRelativeFontSize(*m_buttonBySize, 1.25); + + initBitmapRadioButtons( + { + {m_buttonByTimeSize, "cmp_time" }, + {m_buttonByContent, "cmp_content"}, + {m_buttonBySize, "cmp_size" }, + }, true /*alignLeft*/); + + m_buttonByTimeSize->SetToolTip(getCompVariantDescription(CompareVariant::timeSize)); + m_buttonByContent ->SetToolTip(getCompVariantDescription(CompareVariant::content)); + m_buttonBySize ->SetToolTip(getCompVariantDescription(CompareVariant::size)); + + m_staticTextCompVarDescription->SetMinSize({dipToWxsize(CFG_DESCRIPTION_WIDTH_DIP), -1}); + + m_scrolledWindowPerf->SetMinSize({dipToWxsize(220), -1}); + setImage(*m_bitmapPerf, greyScaleIfDisabled(loadImage("speed"), enableExtraFeatures_)); + + const int scrollDelta = GetCharHeight(); + m_scrolledWindowPerf->SetScrollRate(scrollDelta, scrollDelta); + + setDefaultWidth(*m_spinCtrlAutoRetryCount); + setDefaultWidth(*m_spinCtrlAutoRetryDelay); + + //ignore invalid input for time shift control: + wxTextValidator inputValidator(wxFILTER_DIGITS | wxFILTER_INCLUDE_CHAR_LIST); + inputValidator.SetCharIncludes(L"+-;,: "); + m_textCtrlTimeShift->SetValidator(inputValidator); + + //------------- filter panel -------------------------- + m_textCtrlInclude->SetMinSize({dipToWxsize(280), -1}); + + assert(!contains(m_buttonClear->GetLabel(), L"&C") && !contains(m_buttonClear->GetLabel(), L"&c")); //gazillionth wxWidgets bug on OS X: Command + C mistakenly hits "&C" access key! + + setDefaultWidth(*m_spinCtrlMinSize); + setDefaultWidth(*m_spinCtrlMaxSize); + setDefaultWidth(*m_spinCtrlTimespan); + + setImage(*m_bpButtonDefaultContext, mirrorIfRtl(loadImage("button_arrow_right"))); + + //------------- synchronization panel ----------------- + m_buttonTwoWay->SetToolTip(getSyncVariantDescription(SyncVariant::twoWay)); + m_buttonMirror->SetToolTip(getSyncVariantDescription(SyncVariant::mirror)); + m_buttonUpdate->SetToolTip(getSyncVariantDescription(SyncVariant::update)); + m_buttonCustom->SetToolTip(getSyncVariantDescription(SyncVariant::custom)); + + const int catSizeMax = loadImage("cat_left_only").GetWidth() * 8 / 10; + setImage(*m_bitmapLeftOnly, mirrorIfRtl(greyScale(loadImage("cat_left_only", catSizeMax)))); + setImage(*m_bitmapRightOnly, mirrorIfRtl(greyScale(loadImage("cat_right_only", catSizeMax)))); + setImage(*m_bitmapLeftNewer, mirrorIfRtl(greyScale(loadImage("cat_left_newer", catSizeMax)))); + setImage(*m_bitmapRightNewer, mirrorIfRtl(greyScale(loadImage("cat_right_newer", catSizeMax)))); + setImage(*m_bitmapDifferent, mirrorIfRtl(greyScale(loadImage("cat_different", catSizeMax)))); + + setRelativeFontSize(*m_buttonTwoWay, 1.25); + setRelativeFontSize(*m_buttonMirror, 1.25); + setRelativeFontSize(*m_buttonUpdate, 1.25); + setRelativeFontSize(*m_buttonCustom, 1.25); + + initBitmapRadioButtons( + { + {m_buttonTwoWay, "sync_twoway"}, + {m_buttonMirror, "sync_mirror"}, + {m_buttonUpdate, "sync_update"}, + {m_buttonCustom, "sync_custom"}, + }, false /*alignLeft*/); + + m_staticTextSyncVarDescription->SetMinSize({dipToWxsize(CFG_DESCRIPTION_WIDTH_DIP), -1}); + + m_buttonRecycler ->SetToolTip(_("Retain deleted and overwritten files in the recycle bin")); + m_buttonPermanent ->SetToolTip(_("Delete and overwrite files permanently")); + m_buttonVersioning->SetToolTip(_("Move files to a user-defined folder")); + + initBitmapRadioButtons( + { + {m_buttonRecycler, "delete_recycler" }, + {m_buttonPermanent, "delete_permanently"}, + {m_buttonVersioning, "delete_versioning" }, + }, true /*alignLeft*/); + + setDefaultWidth(*m_spinCtrlVersionMaxDays ); + setDefaultWidth(*m_spinCtrlVersionCountMin); + setDefaultWidth(*m_spinCtrlVersionCountMax); + + m_versioningFolderPath->setHistory(std::make_shared(versioningFolderHistory, folderHistoryMax)); + + + const wxImage imgFileManagerSmall_([] + { + try { return extractWxImage(fff::getFileManagerIcon(dipToScreen(20))); /*throw SysError*/ } + catch (SysError&) { assert(false); return loadImage("file_manager", dipToScreen(20)); } + }()); + setImage(*m_bpButtonShowLogFolder, imgFileManagerSmall_); + m_bpButtonShowLogFolder->SetToolTip(translate(extCommandFileManager.description));//translate default external apps on the fly: "Show in Explorer" + + m_logFolderPath->SetHint(utfTo(globalLogFolderPhrase_)); + //1. no text shown when control is disabled! 2. apparently there's a refresh problem on GTK + + m_logFolderPath->setHistory(std::make_shared(logFolderHistory, folderHistoryMax)); + + m_comboBoxEmail->SetHint(/*_("Example:") + */ L"john.doe@example.com"); + m_comboBoxEmail->setHistory(emailHistory, emailHistoryMax); + + m_comboBoxEmail ->Enable(enableExtraFeatures_); + m_bpButtonEmailAlways ->Enable(enableExtraFeatures_); + m_bpButtonEmailErrorWarning ->Enable(enableExtraFeatures_); + m_bpButtonEmailErrorOnly ->Enable(enableExtraFeatures_); + + //m_staticTextPostSync->SetMinSize({dipToWxsize(180), -1}); + + m_comboBoxPostSyncCommand->SetHint(_("Example:") + L" systemctl poweroff"); + + m_comboBoxPostSyncCommand->setHistory(commandHistory, commandHistoryMax); + + //----------------------------------------------------- + // + //enable dialog-specific key events + Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onLocalKeyEvent(event); }); + + assert(!m_listBoxFolderPair->IsSorted()); + + m_listBoxFolderPair->Append(_("All folder pairs")); + for (const LocalPairConfig& lpc : localPairCfg) + { + std::wstring fpName = getShortDisplayNameForFolderPair(createAbstractPath(lpc.folderPathPhraseLeft), + createAbstractPath(lpc.folderPathPhraseRight)); + if (trimCpy(fpName).empty()) + fpName = L"<" + _("empty") + L">"; + + m_listBoxFolderPair->Append(TAB_SPACE + fpName); + } + + if (!showMultipleCfgs) + { + m_listBoxFolderPair->Hide(); + m_staticTextFolderPairLabel->Hide(); + } + + //temporarily set main config as reference for window min size calculations: + globalPairCfg_ = GlobalPairConfig(); + globalPairCfg_.syncCfg.directionCfg = getDefaultSyncCfg(SyncVariant::twoWay); + globalPairCfg_.syncCfg.deletionVariant = DeletionVariant::versioning; + globalPairCfg_.syncCfg.versioningFolderPhrase = Zstr("dummy"); + globalPairCfg_.syncCfg.versioningStyle = VersioningStyle::timestampFile; + globalPairCfg_.syncCfg.versionMaxAgeDays = 30; + globalPairCfg_.miscCfg.autoRetryCount = 1; + globalPairCfg_.miscCfg.altLogFolderPathPhrase = Zstr("dummy"); + globalPairCfg_.miscCfg.emailNotifyAddress = "dummy"; + + selectFolderPairConfig(-1); + + GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() +#ifdef __WXGTK3__ + Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 + //Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404 +#endif + Center(); //apply *after* dialog size change! + + //keep stable sizer height: change-based directions are taller than difference-based ones => init with SyncVariant::twoWay + bSizerSyncDirHolder ->SetMinSize(-1, bSizerSyncDirsChanges ->GetSize().y); + bSizerVersioningHolder->SetMinSize(-1, bSizerVersioningHolder->GetSize().y); + + unselectFolderPairConfig(false /*validateParams*/); + globalPairCfg_ = globalPairCfg; //restore proper value + + //set actual sync config + selectFolderPairConfig(localPairIndexToShow); + + //more useful and Enter is redirected to m_buttonOK anyway: + (m_listBoxFolderPair->IsShown() ? static_cast(m_listBoxFolderPair) : m_notebook)->SetFocus(); +} + + +void ConfigDialog::onLocalKeyEvent(wxKeyEvent& event) //process key events without explicit menu entry :) +{ + auto changeSelection = [&](SyncConfigPanel panel) + { + m_notebook->ChangeSelection(static_cast(panel)); + (m_listBoxFolderPair->IsShown() ? static_cast(m_listBoxFolderPair) : m_notebook)->SetFocus(); //GTK ignores F-keys if focus is on hidden item! + }; + + switch (event.GetKeyCode()) + { + case WXK_RETURN: + case WXK_NUMPAD_ENTER: + if (event.ControlDown()) //Ctrl+Enter or on macOS: Command+Enter + { + wxCommandEvent dummy(wxEVT_COMMAND_BUTTON_CLICKED); + m_buttonOK->Command(dummy); //simulate click + return; + } + break; + case WXK_F6: + changeSelection(SyncConfigPanel::compare); + return; //handled! + case WXK_F7: + changeSelection(SyncConfigPanel::filter); + return; + case WXK_F8: + changeSelection(SyncConfigPanel::sync); + return; + } + event.Skip(); +} + + +void ConfigDialog::onListBoxKeyEvent(wxKeyEvent& event) +{ + int keyCode = event.GetKeyCode(); + if (m_listBoxFolderPair->GetLayoutDirection() == wxLayout_RightToLeft) + { + if (keyCode == WXK_LEFT || keyCode == WXK_NUMPAD_LEFT) + keyCode = WXK_RIGHT; + else if (keyCode == WXK_RIGHT || keyCode == WXK_NUMPAD_RIGHT) + keyCode = WXK_LEFT; + } + + switch (keyCode) + { + case WXK_LEFT: + case WXK_NUMPAD_LEFT: + switch (static_cast(m_notebook->GetSelection())) + { + case SyncConfigPanel::compare: + break; + case SyncConfigPanel::filter: + m_notebook->ChangeSelection(static_cast(SyncConfigPanel::compare)); + break; + case SyncConfigPanel::sync: + m_notebook->ChangeSelection(static_cast(SyncConfigPanel::filter)); + break; + } + m_listBoxFolderPair->SetFocus(); //needed! wxNotebook::ChangeSelection() leads to focus change! + return; //handled! + + case WXK_RIGHT: + case WXK_NUMPAD_RIGHT: + switch (static_cast(m_notebook->GetSelection())) + { + case SyncConfigPanel::compare: + m_notebook->ChangeSelection(static_cast(SyncConfigPanel::filter)); + break; + case SyncConfigPanel::filter: + m_notebook->ChangeSelection(static_cast(SyncConfigPanel::sync)); + break; + case SyncConfigPanel::sync: + break; + } + m_listBoxFolderPair->SetFocus(); + return; //handled! + } + + event.Skip(); +} + + +void ConfigDialog::onSelectFolderPair(wxCommandEvent& event) +{ + assert(!m_listBoxFolderPair->HasMultipleSelection()); //single-choice! + const int selPos = event.GetSelection(); + assert(0 <= selPos && selPos < makeSigned(m_listBoxFolderPair->GetCount())); + + //m_listBoxFolderPair has no parameter ownership! => selectedPairIndexToShow has! + + if (!unselectFolderPairConfig(true /*validateParams*/)) + { + //restore old selection: + m_listBoxFolderPair->SetSelection(selectedPairIndexToShow_ + 1); + return; + } + selectFolderPairConfig(selPos - 1); +} + + +void ConfigDialog::onCompByTimeSizeDouble(wxMouseEvent& event) +{ + wxCommandEvent dummy; + onCompByTimeSize(dummy); + onOkay(dummy); +} + + +void ConfigDialog::onCompBySizeDouble(wxMouseEvent& event) +{ + wxCommandEvent dummy; + onCompBySize(dummy); + onOkay(dummy); +} + + +void ConfigDialog::onCompByContentDouble(wxMouseEvent& event) +{ + wxCommandEvent dummy; + onCompByContent(dummy); + onOkay(dummy); +} + + +std::optional ConfigDialog::getCompConfig() const +{ + if (!m_checkBoxUseLocalCmpOptions->GetValue()) + return {}; + + CompConfig compCfg; + compCfg.compareVar = localCmpVar_; + compCfg.handleSymlinks = !m_checkBoxSymlinksInclude->GetValue() ? SymLinkHandling::exclude : m_radioBtnSymlinksDirect->GetValue() ? SymLinkHandling::asLink : SymLinkHandling::follow; + compCfg.ignoreTimeShiftMinutes = fromTimeShiftPhrase(copyStringTo(m_textCtrlTimeShift->GetValue())); + + return compCfg; +} + + +void ConfigDialog::setCompConfig(const CompConfig* compCfg) +{ + m_checkBoxUseLocalCmpOptions->SetValue(compCfg); + + //when local settings are inactive, display (current) global settings instead: + const CompConfig tmpCfg = compCfg ? *compCfg : globalPairCfg_.cmpCfg; + + localCmpVar_ = tmpCfg.compareVar; + + switch (tmpCfg.handleSymlinks) + { + case SymLinkHandling::exclude: + m_checkBoxSymlinksInclude->SetValue(false); + m_radioBtnSymlinksFollow ->SetValue(true); + break; + case SymLinkHandling::follow: + m_checkBoxSymlinksInclude->SetValue(true); + m_radioBtnSymlinksFollow->SetValue(true); + break; + case SymLinkHandling::asLink: + m_checkBoxSymlinksInclude->SetValue(true); + m_radioBtnSymlinksDirect->SetValue(true); + break; + } + + m_textCtrlTimeShift->ChangeValue(toTimeShiftPhrase(tmpCfg.ignoreTimeShiftMinutes)); + + updateCompGui(); +} + + +void ConfigDialog::updateCompGui() +{ + const bool compOptionsEnabled = m_checkBoxUseLocalCmpOptions->GetValue(); + + m_panelComparisonSettings->Enable(compOptionsEnabled); + + m_notebook->SetPageImage(static_cast(SyncConfigPanel::compare), + static_cast(compOptionsEnabled ? ConfigTypeImage::compare : ConfigTypeImage::compareGrey)); + + //update toggle buttons -> they have no parameter-ownership at all! + m_buttonByTimeSize->setActive(CompareVariant::timeSize == localCmpVar_ && compOptionsEnabled); + m_buttonByContent ->setActive(CompareVariant::content == localCmpVar_ && compOptionsEnabled); + m_buttonBySize ->setActive(CompareVariant::size == localCmpVar_ && compOptionsEnabled); + //compOptionsEnabled: nudge wxWidgets to render inactive config state (needed on Windows, NOT on Linux!) + + switch (localCmpVar_) //unconditionally update image, including "local options off" + { + case CompareVariant::timeSize: + //help wxWidgets a little to render inactive config state (needed on Windows, NOT on Linux!) + setImage(*m_bitmapCompVariant, greyScaleIfDisabled(loadImage("cmp_time"), compOptionsEnabled)); + break; + case CompareVariant::content: + setImage(*m_bitmapCompVariant, greyScaleIfDisabled(loadImage("cmp_content"), compOptionsEnabled)); + break; + case CompareVariant::size: + setImage(*m_bitmapCompVariant, greyScaleIfDisabled(loadImage("cmp_size"), compOptionsEnabled)); + break; + } + + //active variant description: + setText(*m_staticTextCompVarDescription, getCompVariantDescription(localCmpVar_)); + m_staticTextCompVarDescription->Wrap(dipToWxsize(CFG_DESCRIPTION_WIDTH_DIP)); //needs to be reapplied after SetLabel() + + m_radioBtnSymlinksDirect->Enable(m_checkBoxSymlinksInclude->GetValue() && compOptionsEnabled); //help wxWidgets a little to render inactive config state (needed on Windows, NOT on Linux!) + m_radioBtnSymlinksFollow->Enable(m_checkBoxSymlinksInclude->GetValue() && compOptionsEnabled); // +} + + +void ConfigDialog::onFilterDefaultContext(wxEvent& event) +{ + const FilterConfig activeCfg = getFilterConfig(); + const FilterConfig defaultFilter = GlobalConfig().defaultFilter; + + ContextMenu menu; + menu.addItem(_("&Save"), [&] { defaultFilterOut_ = activeCfg; updateFilterGui(); }, + loadImage("cfg_save", dipToScreen(getMenuIconDipSize())), defaultFilterOut_ != activeCfg); + + menu.addItem(_("&Load factory default"), [&] { setFilterConfig(defaultFilter); }, wxNullImage, activeCfg != defaultFilter); + + menu.popup(*m_bpButtonDefaultContext, {m_bpButtonDefaultContext->GetSize().x, 0}); +} + + +FilterConfig ConfigDialog::getFilterConfig() const +{ + auto sanitizeFilter = [](wxString str) + { + //macOS: Ctrl+Enter inserts Unicode LINE_SEPARATOR which is indistinguishable from new line! + replace(str, LINE_SEPARATOR, L'\n'); + replace(str, PARAGRAPH_SEPARATOR, L'\n'); + + return utfTo(str); + }; + + return + { + sanitizeFilter(m_textCtrlInclude->GetValue()), sanitizeFilter(m_textCtrlExclude->GetValue()), + makeUnsigned(m_spinCtrlTimespan->GetValue()), + enumTimeDescr_.get(), + makeUnsigned(m_spinCtrlMinSize->GetValue()), enumMinSizeDescr_.get(), + makeUnsigned(m_spinCtrlMaxSize->GetValue()), enumMaxSizeDescr_.get()}; +} + + +void ConfigDialog::setFilterConfig(const FilterConfig& filter) +{ + m_textCtrlInclude->ChangeValue(utfTo(filter.includeFilter)); + m_textCtrlExclude->ChangeValue(utfTo(filter.excludeFilter)); + + enumTimeDescr_ .set(filter.unitTimeSpan); + enumMinSizeDescr_.set(filter.unitSizeMin); + enumMaxSizeDescr_.set(filter.unitSizeMax); + + m_spinCtrlTimespan->SetValue(static_cast(filter.timeSpan)); + m_spinCtrlMinSize ->SetValue(static_cast(filter.sizeMin)); + m_spinCtrlMaxSize ->SetValue(static_cast(filter.sizeMax)); + + updateFilterGui(); +} + + +void ConfigDialog::updateFilterGui() +{ + const FilterConfig activeCfg = getFilterConfig(); + + m_notebook->SetPageImage(static_cast(SyncConfigPanel::filter), + static_cast(!isNullFilter(activeCfg) ? ConfigTypeImage::filter: ConfigTypeImage::filterGrey)); + + setImage(*m_bitmapInclude, greyScaleIfDisabled(loadImage("filter_include"), !NameFilter::isNull(activeCfg.includeFilter, FilterConfig().excludeFilter))); + setImage(*m_bitmapExclude, greyScaleIfDisabled(loadImage("filter_exclude"), !NameFilter::isNull(FilterConfig().includeFilter, activeCfg.excludeFilter))); + setImage(*m_bitmapFilterDate, greyScaleIfDisabled(loadImage("cmp_time"), activeCfg.unitTimeSpan != UnitTime::none)); + setImage(*m_bitmapFilterSize, greyScaleIfDisabled(loadImage("cmp_size"), activeCfg.unitSizeMin != UnitSize::none || activeCfg.unitSizeMax != UnitSize::none)); + + m_spinCtrlTimespan->Enable(activeCfg.unitTimeSpan == UnitTime::lastDays); + m_spinCtrlMinSize ->Enable(activeCfg.unitSizeMin != UnitSize::none); + m_spinCtrlMaxSize ->Enable(activeCfg.unitSizeMax != UnitSize::none); + + m_buttonDefault->Enable(activeCfg != defaultFilterOut_); + m_buttonClear ->Enable(activeCfg != FilterConfig()); +} + + +void ConfigDialog::onToggleUseDatabase(wxCommandEvent& event) +{ + if (const DirectionByDiff* diffDirs = std::get_if(&directionsCfg_.dirs)) + directionsCfg_.dirs = getChangesDirDefault(*diffDirs); + else + { + const DirectionByChange& changeDirs = std::get(directionsCfg_.dirs); + directionsCfg_.dirs = getDiffDirDefault(changeDirs); + } + updateSyncGui(); +} + + +void ConfigDialog::onSyncTwoWayDouble(wxMouseEvent& event) +{ + wxCommandEvent dummy; + onSyncTwoWay(dummy); + onOkay(dummy); +} + + +void ConfigDialog::onSyncMirrorDouble(wxMouseEvent& event) +{ + wxCommandEvent dummy; + onSyncMirror(dummy); + onOkay(dummy); +} + + +void ConfigDialog::onSyncUpdateDouble(wxMouseEvent& event) +{ + wxCommandEvent dummy; + onSyncUpdate(dummy); + onOkay(dummy); +} + + +void ConfigDialog::onSyncCustomDouble(wxMouseEvent& event) +{ + wxCommandEvent dummy; + onSyncCustom(dummy); + onOkay(dummy); +} + + +void toggleSyncDirection(SyncDirection& current) +{ + switch (current) + { + case SyncDirection::right: + current = SyncDirection::left; + break; + case SyncDirection::left: + current = SyncDirection::none; + break; + case SyncDirection::none: + current = SyncDirection::right; + break; + } +} + + +void ConfigDialog::toggleSyncDirButton(SyncDirection DirectionByDiff::* dir) +{ + if (DirectionByDiff* diffDirs = std::get_if(&directionsCfg_.dirs)) + { + toggleSyncDirection(diffDirs->*dir); + updateSyncGui(); + } + else assert(false); +} + + +void ConfigDialog::onLeftNewer(wxCommandEvent& event) +{ + toggleSyncDirButton(&DirectionByDiff::leftNewer); + assert(!leftRightNewerCombined()); +} + + +void ConfigDialog::onRightNewer(wxCommandEvent& event) +{ + toggleSyncDirButton(&DirectionByDiff::rightNewer); + assert(!leftRightNewerCombined()); +} + + +void ConfigDialog::onDifferent(wxCommandEvent& event) +{ + toggleSyncDirButton(&DirectionByDiff::leftNewer); + + if (DirectionByDiff* diffDirs = std::get_if(&directionsCfg_.dirs)) + //simulate category "different" as leftNewer/rightNewer combined: + diffDirs->rightNewer = diffDirs->leftNewer; + else assert(false); + assert(leftRightNewerCombined()); +} + + +void ConfigDialog::toggleSyncDirButton(DirectionByChange::Changes DirectionByChange::* side, SyncDirection DirectionByChange::Changes::* dir) +{ + if (DirectionByChange* changeDirs = std::get_if(&directionsCfg_.dirs)) + { + toggleSyncDirection(changeDirs->*side.*dir); + updateSyncGui(); + } + else assert(false); +} + + +namespace +{ +auto updateDirButton(wxBitmapButton& button, SyncDirection dir, + const char* imgNameLeft, const char* imgNameNone, const char* imgNameRight, + SyncOperation opLeft, SyncOperation opNone, SyncOperation opRight) +{ + const char* imgName = nullptr; + switch (dir) + { + case SyncDirection::left: + imgName = imgNameLeft; + button.SetToolTip(getSyncOpDescription(opLeft)); + break; + case SyncDirection::none: + imgName = imgNameNone; + button.SetToolTip(getSyncOpDescription(opNone)); + break; + case SyncDirection::right: + imgName = imgNameRight; + button.SetToolTip(getSyncOpDescription(opRight)); + break; + } + wxImage img = mirrorIfRtl(loadImage(imgName)); + button.SetBitmapLabel (toScaledBitmap( img)); + button.SetBitmapDisabled(toScaledBitmap(greyScale(img))); //fix wxWidgets' all-too-clever multi-state! + //=> the disabled bitmap is generated during first SetBitmapLabel() call but never updated again by wxWidgets! +} + + +void updateDiffDirButtons(const DirectionByDiff& diffDirs, + wxBitmapButton& buttonLeftOnly, + wxBitmapButton& buttonRightOnly, + wxBitmapButton& buttonLeftNewer, + wxBitmapButton& buttonRightNewer, + wxBitmapButton& buttonDifferent) +{ + updateDirButton(buttonLeftOnly, diffDirs.leftOnly, "so_delete_left", "so_none", "so_create_right", SO_DELETE_LEFT, SO_DO_NOTHING, SO_CREATE_RIGHT); + updateDirButton(buttonRightOnly, diffDirs.rightOnly, "so_create_left", "so_none", "so_delete_right", SO_CREATE_LEFT, SO_DO_NOTHING, SO_DELETE_RIGHT); + updateDirButton(buttonLeftNewer, diffDirs.leftNewer, "so_update_left", "so_none", "so_update_right", SO_OVERWRITE_LEFT, SO_DO_NOTHING, SO_OVERWRITE_RIGHT); + updateDirButton(buttonRightNewer, diffDirs.rightNewer, "so_update_left", "so_none", "so_update_right", SO_OVERWRITE_LEFT, SO_DO_NOTHING, SO_OVERWRITE_RIGHT); + //simulate category "different" as leftNewer/rightNewer combined: + updateDirButton(buttonDifferent, diffDirs.leftNewer, "so_update_left", "so_none", "so_update_right", SO_OVERWRITE_LEFT, SO_DO_NOTHING, SO_OVERWRITE_RIGHT); +} + + +void updateChangeDirButtons(const DirectionByChange& changeDirs, + wxBitmapButton& buttonLeftCreate, + wxBitmapButton& buttonLeftUpdate, + wxBitmapButton& buttonLeftDelete, + wxBitmapButton& buttonRightCreate, + wxBitmapButton& buttonRightUpdate, + wxBitmapButton& buttonRightDelete) +{ + updateDirButton(buttonLeftCreate, changeDirs.left.create, "so_delete_left", "so_none", "so_create_right", SO_DELETE_LEFT, SO_DO_NOTHING, SO_CREATE_RIGHT); + updateDirButton(buttonLeftUpdate, changeDirs.left.update, "so_update_left", "so_none", "so_update_right", SO_OVERWRITE_LEFT, SO_DO_NOTHING, SO_OVERWRITE_RIGHT); + updateDirButton(buttonLeftDelete, changeDirs.left.delete_, "so_create_left", "so_none", "so_delete_right", SO_CREATE_LEFT, SO_DO_NOTHING, SO_DELETE_RIGHT); + + updateDirButton(buttonRightCreate, changeDirs.right.create, "so_create_left", "so_none", "so_delete_right", SO_CREATE_LEFT, SO_DO_NOTHING, SO_DELETE_RIGHT); + updateDirButton(buttonRightUpdate, changeDirs.right.update, "so_update_left", "so_none", "so_update_right", SO_OVERWRITE_LEFT, SO_DO_NOTHING, SO_OVERWRITE_RIGHT); + updateDirButton(buttonRightDelete, changeDirs.right.delete_, "so_delete_left", "so_none", "so_create_right", SO_DELETE_LEFT, SO_DO_NOTHING, SO_CREATE_RIGHT); +} +} + +void ConfigDialog::onShowLogFolder(wxCommandEvent& event) +{ + assert(selectedPairIndexToShow_ < 0); + if (selectedPairIndexToShow_ < 0) + try + { + AbstractPath logFolderPath = createAbstractPath(getMiscSyncOptions().altLogFolderPathPhrase); //optional + if (AFS::isNullPath(logFolderPath)) + logFolderPath = createAbstractPath(globalLogFolderPhrase_); + + openFolderInFileBrowser(logFolderPath); //throw FileError + } + catch (const FileError& e) { showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); } +} + + +bool ConfigDialog::leftRightNewerCombined() const +{ + assert(std::get_if(&directionsCfg_.dirs)); + const CompareVariant activeCmpVar = m_checkBoxUseLocalCmpOptions->GetValue() ? localCmpVar_ : globalPairCfg_.cmpCfg.compareVar; + return activeCmpVar == CompareVariant::content || activeCmpVar == CompareVariant::size; +} + + +std::optional ConfigDialog::getSyncConfig() const +{ + if (!m_checkBoxUseLocalSyncOptions->GetValue()) + return {}; + + SyncConfig syncCfg; + syncCfg.directionCfg = directionsCfg_; + syncCfg.deletionVariant = deletionVariant_; + syncCfg.versioningFolderPhrase = versioningFolder_.getPath(); + syncCfg.versioningStyle = enumVersioningStyle_.get(); + if (syncCfg.versioningStyle != VersioningStyle::replace) + { + syncCfg.versionMaxAgeDays = m_checkBoxVersionMaxDays ->GetValue() ? m_spinCtrlVersionMaxDays->GetValue() : 0; + syncCfg.versionCountMin = m_checkBoxVersionCountMin->GetValue() && m_checkBoxVersionMaxDays->GetValue() ? m_spinCtrlVersionCountMin->GetValue() : 0; + syncCfg.versionCountMax = m_checkBoxVersionCountMax->GetValue() ? m_spinCtrlVersionCountMax->GetValue() : 0; + } + + //simulate category "different" as leftNewer/rightNewer combined: + if (DirectionByDiff* diffDirs = std::get_if(&syncCfg.directionCfg.dirs)) + if (leftRightNewerCombined()) + diffDirs->rightNewer = diffDirs->leftNewer; + + return syncCfg; +} + + +void ConfigDialog::setSyncConfig(const SyncConfig* syncCfg) +{ + m_checkBoxUseLocalSyncOptions->SetValue(syncCfg); + + //when local settings are inactive, display (current) global settings instead: + const SyncConfig tmpCfg = syncCfg ? *syncCfg : globalPairCfg_.syncCfg; + + directionsCfg_ = tmpCfg.directionCfg; //make working copy; ownership *not* on GUI + deletionVariant_ = tmpCfg.deletionVariant; + versioningFolder_.setPath(tmpCfg.versioningFolderPhrase); + enumVersioningStyle_.set(tmpCfg.versioningStyle); + + const bool useVersionLimits = tmpCfg.versioningStyle != VersioningStyle::replace; + + m_checkBoxVersionMaxDays ->SetValue(useVersionLimits && tmpCfg.versionMaxAgeDays > 0); + m_checkBoxVersionCountMin->SetValue(useVersionLimits && tmpCfg.versionCountMin > 0 && tmpCfg.versionMaxAgeDays > 0); + m_checkBoxVersionCountMax->SetValue(useVersionLimits && tmpCfg.versionCountMax > 0); + + m_spinCtrlVersionMaxDays ->SetValue(m_checkBoxVersionMaxDays ->GetValue() ? tmpCfg.versionMaxAgeDays : 30); + m_spinCtrlVersionCountMin->SetValue(m_checkBoxVersionCountMin->GetValue() ? tmpCfg.versionCountMin : 1); + m_spinCtrlVersionCountMax->SetValue(m_checkBoxVersionCountMax->GetValue() ? tmpCfg.versionCountMax : 1); + + updateSyncGui(); +} + + +void ConfigDialog::updateSyncGui() +{ + const bool syncOptionsEnabled = m_checkBoxUseLocalSyncOptions->GetValue(); + + m_panelSyncSettings->Enable(syncOptionsEnabled); + + m_notebook->SetPageImage(static_cast(SyncConfigPanel::sync), + static_cast(syncOptionsEnabled ? ConfigTypeImage::sync: ConfigTypeImage::syncGrey)); + + const bool setDirsByDifferences = std::get_if(&directionsCfg_.dirs); + + m_checkBoxUseDatabase->SetValue(!setDirsByDifferences); + + //display only relevant sync options + bSizerSyncDirsDiff ->Show( setDirsByDifferences); + bSizerSyncDirsChanges->Show(!setDirsByDifferences); + + if (const DirectionByDiff* diffDirs = std::get_if(&directionsCfg_.dirs)) //sync directions by differences + { + updateDiffDirButtons(*diffDirs, + *m_bpButtonLeftOnly, + *m_bpButtonRightOnly, + *m_bpButtonLeftNewer, + *m_bpButtonRightNewer, + *m_bpButtonDifferent); + + //simulate category "different" as leftNewer/rightNewer combined: + const bool haveLeftRightNewerCombined = leftRightNewerCombined(); + m_bitmapLeftNewer ->Show(!haveLeftRightNewerCombined); + m_bpButtonLeftNewer ->Show(!haveLeftRightNewerCombined); + m_bitmapRightNewer ->Show(!haveLeftRightNewerCombined); + m_bpButtonRightNewer->Show(!haveLeftRightNewerCombined); + + m_bitmapDifferent ->Show(haveLeftRightNewerCombined); + m_bpButtonDifferent->Show(haveLeftRightNewerCombined); + } + else //sync directions by changes + { + const DirectionByChange& changeDirs = std::get(directionsCfg_.dirs); + + updateChangeDirButtons(changeDirs, + *m_bpButtonLeftCreate, + *m_bpButtonLeftUpdate, + *m_bpButtonLeftDelete, + *m_bpButtonRightCreate, + *m_bpButtonRightUpdate, + *m_bpButtonRightDelete); + } + + const bool useDatabaseFile = std::get_if(&directionsCfg_.dirs); + + setImage(*m_bitmapDatabase, greyScaleIfDisabled(loadImage("database", dipToScreen(22)), useDatabaseFile && syncOptionsEnabled)); + + //"detect move files" is always active iff database is used: + setImage(*m_bitmapMoveLeft, greyScaleIfDisabled(loadImage("so_move_left", dipToScreen(20)), useDatabaseFile && syncOptionsEnabled)); + setImage(*m_bitmapMoveRight, greyScaleIfDisabled(loadImage("so_move_right", dipToScreen(20)), useDatabaseFile && syncOptionsEnabled)); + m_staticTextDetectMove->Enable(useDatabaseFile); + + const SyncVariant syncVar = getSyncVariant(directionsCfg_); + + //active variant description: + setText(*m_staticTextSyncVarDescription, getSyncVariantDescription(syncVar)); + m_staticTextSyncVarDescription->Wrap(dipToWxsize(CFG_DESCRIPTION_WIDTH_DIP)); //needs to be reapplied after SetLabel() + + //update toggle buttons -> they have no parameter-ownership at all! + m_buttonTwoWay->setActive(SyncVariant::twoWay == syncVar && syncOptionsEnabled); + m_buttonMirror->setActive(SyncVariant::mirror == syncVar && syncOptionsEnabled); + m_buttonUpdate->setActive(SyncVariant::update == syncVar && syncOptionsEnabled); + m_buttonCustom->setActive(SyncVariant::custom == syncVar && syncOptionsEnabled); + //syncOptionsEnabled: nudge wxWidgets to render inactive config state (needed on Windows, NOT on Linux!) + + m_buttonRecycler ->setActive(DeletionVariant::recycler == deletionVariant_ && syncOptionsEnabled); + m_buttonPermanent ->setActive(DeletionVariant::permanent == deletionVariant_ && syncOptionsEnabled); + m_buttonVersioning->setActive(DeletionVariant::versioning == deletionVariant_ && syncOptionsEnabled); + + switch (deletionVariant_) //unconditionally update image, including "local options off" + { + case DeletionVariant::recycler: + { + wxImage imgTrash = loadImage("delete_recycler"); + //use system icon if available (can fail on Linux??) + try { imgTrash = extractWxImage(fff::getTrashIcon(imgTrash.GetHeight())); /*throw SysError*/ } + catch (SysError&) { assert(false); } + + setImage(*m_bitmapDeletionType, greyScaleIfDisabled(imgTrash, syncOptionsEnabled)); + setText(*m_staticTextDeletionTypeDescription, _("Retain deleted and overwritten files in the recycle bin")); + } + break; + case DeletionVariant::permanent: + setImage(*m_bitmapDeletionType, greyScaleIfDisabled(loadImage("delete_permanently"), syncOptionsEnabled)); + setText(*m_staticTextDeletionTypeDescription, _("Delete and overwrite files permanently")); + break; + case DeletionVariant::versioning: + setImage(*m_bitmapVersioning, greyScaleIfDisabled(loadImage("delete_versioning"), syncOptionsEnabled)); + break; + } + //m_staticTextDeletionTypeDescription->Wrap(dipToWxsize(200)); //needs to be reapplied after SetLabel() + + const bool versioningSelected = deletionVariant_ == DeletionVariant::versioning; + + m_bitmapDeletionType ->Show(!versioningSelected); + m_staticTextDeletionTypeDescription->Show(!versioningSelected); + m_panelVersioning ->Show( versioningSelected); + + if (versioningSelected) + { + enumVersioningStyle_.updateTooltip(); + + const VersioningStyle versioningStyle = enumVersioningStyle_.get(); + const std::wstring pathSep = utfTo(FILE_NAME_SEPARATOR); + + switch (versioningStyle) + { + case VersioningStyle::replace: + setText(*m_staticTextNamingCvtPart1, pathSep + _("Folder") + pathSep + _("File") + L".doc"); + setText(*m_staticTextNamingCvtPart2Bold, L""); + setText(*m_staticTextNamingCvtPart3, L""); + break; + + case VersioningStyle::timestampFolder: + setText(*m_staticTextNamingCvtPart1, pathSep); + setText(*m_staticTextNamingCvtPart2Bold, _("YYYY-MM-DD hhmmss")); + setText(*m_staticTextNamingCvtPart3, pathSep + _("Folder") + pathSep + _("File") + L".doc "); + break; + + case VersioningStyle::timestampFile: + setText(*m_staticTextNamingCvtPart1, pathSep + _("Folder") + pathSep + _("File") + L".doc "); + setText(*m_staticTextNamingCvtPart2Bold, _("YYYY-MM-DD hhmmss")); + setText(*m_staticTextNamingCvtPart3, L".doc"); + break; + } + + const bool enableLimitCtrls = syncOptionsEnabled && versioningStyle != VersioningStyle::replace; + const bool showLimitCtrls = m_checkBoxVersionMaxDays->GetValue() || m_checkBoxVersionCountMax->GetValue(); + //m_checkBoxVersionCountMin->GetValue() => irrelevant if !m_checkBoxVersionMaxDays->GetValue()! + + if (!m_checkBoxVersionMaxDays->GetValue() && m_checkBoxVersionCountMin->GetValue()) + m_checkBoxVersionCountMin->SetValue(false); //make this dependency cristal-clear (don't just disable) + + m_staticTextLimitVersions->Show(!showLimitCtrls); + + m_spinCtrlVersionMaxDays ->Show(showLimitCtrls); + m_spinCtrlVersionCountMin->Show(showLimitCtrls); + m_spinCtrlVersionCountMax->Show(showLimitCtrls); + + m_staticTextLimitVersions->Enable(enableLimitCtrls); + m_checkBoxVersionMaxDays ->Enable(enableLimitCtrls); + m_checkBoxVersionCountMin->Enable(enableLimitCtrls && m_checkBoxVersionMaxDays->GetValue()); + m_checkBoxVersionCountMax->Enable(enableLimitCtrls); + + m_spinCtrlVersionMaxDays ->Enable(enableLimitCtrls && m_checkBoxVersionMaxDays ->GetValue()); + m_spinCtrlVersionCountMin->Enable(enableLimitCtrls && m_checkBoxVersionMaxDays->GetValue() && m_checkBoxVersionCountMin->GetValue()); + m_spinCtrlVersionCountMax->Enable(enableLimitCtrls && m_checkBoxVersionCountMax->GetValue()); + } + + m_panelSyncSettings->Layout(); + + //Refresh(); //removes a few artifacts when toggling display of versioning folder +} + + +MiscSyncConfig ConfigDialog::getMiscSyncOptions() const +{ + MiscSyncConfig miscCfg; + + // Avoid "fake" changed configs! => + // - don't touch items corresponding to paths not currently used + // - don't store parallel ops == 1 + miscCfg.deviceParallelOps = deviceParallelOps_; + assert(fgSizerPerf->GetItemCount() == 2 * devicesForEdit_.size()); + int i = 0; + for (const AfsDevice& afsDevice : devicesForEdit_) + { + wxSpinCtrl* spinCtrlParallelOps = dynamic_cast(fgSizerPerf->GetItem(i * 2)->GetWindow()); + setDeviceParallelOps(miscCfg.deviceParallelOps, afsDevice, spinCtrlParallelOps->GetValue()); + ++i; + } + //---------------------------------------------------------------------------- + miscCfg.ignoreErrors = m_checkBoxIgnoreErrors->GetValue(); + miscCfg.autoRetryCount = m_checkBoxAutoRetry ->GetValue() ? m_spinCtrlAutoRetryCount->GetValue() : 0; + miscCfg.autoRetryDelay = std::chrono::seconds(m_spinCtrlAutoRetryDelay->GetValue()); + //---------------------------------------------------------------------------- + miscCfg.postSyncCommand = m_comboBoxPostSyncCommand->getValue(); + miscCfg.postSyncCondition = enumPostSyncCondition_.get(); + //---------------------------------------------------------------------------- + Zstring altLogFolderPhrase = logFolderSelector_.getPath(); + if (altLogFolderPhrase.empty()) //"empty" already means "unchecked" + altLogFolderPhrase = Zstr(' '); //=> trigger error message on dialog close + miscCfg.altLogFolderPathPhrase = m_checkBoxOverrideLogPath->GetValue() ? altLogFolderPhrase : Zstring(); + //---------------------------------------------------------------------------- + std::string emailAddress = utfTo(m_comboBoxEmail->getValue()); + if (emailAddress.empty()) + emailAddress = ' '; //trigger error message on dialog close + miscCfg.emailNotifyAddress = m_checkBoxSendEmail->GetValue() ? emailAddress : std::string(); + miscCfg.emailNotifyCondition = emailNotifyCondition_; + //---------------------------------------------------------------------------- + miscCfg.notes = trimCpy(utfTo(m_textCtrNotes->GetValue())); + + return miscCfg; +} + + +void ConfigDialog::setMiscSyncOptions(const MiscSyncConfig& miscCfg) +{ + // Avoid "fake" changed configs! => + //- when editting, consider only the deviceParallelOps items corresponding to the currently-used folder paths + //- keep parallel ops == 1 only temporarily during edit + deviceParallelOps_ = miscCfg.deviceParallelOps; + + assert(fgSizerPerf->GetItemCount() % 2 == 0); + const int rowsToCreate = static_cast(devicesForEdit_.size()) - static_cast(fgSizerPerf->GetItemCount() / 2); + if (rowsToCreate >= 0) + for (int i = 0; i < rowsToCreate; ++i) + { + wxSpinCtrl* spinCtrlParallelOps = new wxSpinCtrl(m_scrolledWindowPerf, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 1, 2000'000'000, 1); + setDefaultWidth(*spinCtrlParallelOps); + spinCtrlParallelOps->Enable(enableExtraFeatures_); + fgSizerPerf->Add(spinCtrlParallelOps, 0, wxALIGN_CENTER_VERTICAL); + + wxStaticText* staticTextDevice = new wxStaticText(m_scrolledWindowPerf, wxID_ANY, wxEmptyString); + staticTextDevice->Enable(enableExtraFeatures_); + fgSizerPerf->Add(staticTextDevice, 0, wxALIGN_CENTER_VERTICAL); + } + else + for (int i = 0; i < -rowsToCreate * 2; ++i) + fgSizerPerf->GetItem(size_t(0))->GetWindow()->Destroy(); + assert(fgSizerPerf->GetItemCount() == 2 * devicesForEdit_.size()); + + int i = 0; + for (const AfsDevice& afsDevice : devicesForEdit_) + { + wxSpinCtrl* spinCtrlParallelOps = dynamic_cast (fgSizerPerf->GetItem(i * 2 )->GetWindow()); + wxStaticText* staticTextDevice = dynamic_cast(fgSizerPerf->GetItem(i * 2 + 1)->GetWindow()); + + spinCtrlParallelOps->SetValue(static_cast(getDeviceParallelOps(deviceParallelOps_, afsDevice))); + staticTextDevice->SetLabelText(AFS::getDisplayPath(AbstractPath(afsDevice, AfsPath()))); + ++i; + } + m_staticTextPerfParallelOps->Enable(enableExtraFeatures_ && !devicesForEdit_.empty()); + + m_panelComparisonSettings->Layout(); //*after* setting text labels + + //---------------------------------------------------------------------------- + m_checkBoxIgnoreErrors ->SetValue(miscCfg.ignoreErrors); + m_checkBoxAutoRetry ->SetValue(miscCfg.autoRetryCount > 0); + m_spinCtrlAutoRetryCount->SetValue(std::max(miscCfg.autoRetryCount, 0)); + m_spinCtrlAutoRetryDelay->SetValue(miscCfg.autoRetryDelay.count()); + //---------------------------------------------------------------------------- + m_comboBoxPostSyncCommand->setValue(miscCfg.postSyncCommand); + enumPostSyncCondition_.set(miscCfg.postSyncCondition); + //---------------------------------------------------------------------------- + m_checkBoxOverrideLogPath->SetValue(!miscCfg.altLogFolderPathPhrase.empty()); //only "empty path" means unchecked! everything else (e.g. " "): "checked" + logFolderSelector_.setPath(m_checkBoxOverrideLogPath->GetValue() ? miscCfg.altLogFolderPathPhrase : globalLogFolderPhrase_); + //---------------------------------------------------------------------------- + Zstring defaultEmail; + if (const std::vector& history = m_comboBoxEmail->getHistory(); + !history.empty()) + defaultEmail = history[0]; + + m_checkBoxSendEmail->SetValue(!trimCpy(miscCfg.emailNotifyAddress).empty()); + m_comboBoxEmail->setValue(m_checkBoxSendEmail->GetValue() ? utfTo(miscCfg.emailNotifyAddress) : defaultEmail); + emailNotifyCondition_ = miscCfg.emailNotifyCondition; + //---------------------------------------------------------------------------- + m_textCtrNotes->ChangeValue(utfTo(miscCfg.notes)); + + updateMiscGui(); +} + + +void ConfigDialog::updateMiscGui() +{ + if (selectedPairIndexToShow_ == -1) + { + const MiscSyncConfig miscCfg = getMiscSyncOptions(); + + setImage(*m_bitmapIgnoreErrors, greyScaleIfDisabled(loadImage("error_ignore_active"), miscCfg.ignoreErrors)); + setImage(*m_bitmapRetryErrors, greyScaleIfDisabled(loadImage("error_retry"), miscCfg.autoRetryCount > 0 )); + + fgSizerAutoRetry->Show(miscCfg.autoRetryCount > 0); + + m_panelComparisonSettings->Layout(); //showing "retry count" can affect bSizerPerformance! + //---------------------------------------------------------------------------- + const bool sendEmailEnabled = m_checkBoxSendEmail->GetValue(); + setImage(*m_bitmapEmail, greyScaleIfDisabled(loadImage("email"), sendEmailEnabled)); + m_comboBoxEmail->Show(sendEmailEnabled); + + auto updateButton = [successIcon = loadImage("msg_success", dipToScreen(getMenuIconDipSize())), + warningIcon = loadImage("msg_warning", dipToScreen(getMenuIconDipSize())), + errorIcon = loadImage("msg_error", dipToScreen(getMenuIconDipSize())), + sendEmailEnabled, this](wxBitmapButton& button, ResultsNotification notifyCondition) + { + button.Show(sendEmailEnabled); + if (sendEmailEnabled) + { + wxString tooltip = _("Error"); + wxImage label = errorIcon; + + if (notifyCondition == ResultsNotification::always || + notifyCondition == ResultsNotification::errorWarning) + { + tooltip += (L" | ") + _("Warning"); + label = stackImages(label, warningIcon, ImageStackLayout::horizontal, ImageStackAlignment::center); + } + else + label = resizeCanvas(label, {label.GetWidth() + warningIcon.GetWidth(), label.GetHeight()}, wxALIGN_LEFT); + + if (notifyCondition == ResultsNotification::always) + { + tooltip += (L" | ") + _("Success"); + label = stackImages(label, successIcon, ImageStackLayout::horizontal, ImageStackAlignment::center); + } + else + label = resizeCanvas(label, {label.GetWidth() + successIcon.GetWidth(), label.GetHeight()}, wxALIGN_LEFT); + + button.SetToolTip(tooltip); + button.SetBitmapLabel (toScaledBitmap(notifyCondition == emailNotifyCondition_ && sendEmailEnabled ? label : greyScale(label))); + button.SetBitmapDisabled(toScaledBitmap(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_hyperlinkPerfDeRequired2->Show(!enableExtraFeatures_); //required after each bSizerSyncMisc->Show() + + //---------------------------------------------------------------------------- + setImage(*m_bitmapLogFile, greyScaleIfDisabled(loadImage("log_file", dipToScreen(20)), m_checkBoxOverrideLogPath->GetValue())); + 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_panelSyncSettings->Refresh(); //removes a few artifacts when toggling email notifications + m_panelLogfile ->Refresh();// + } + //---------------------------------------------------------------------------- + m_buttonAddNotes->Show(!showNotesPanel_); + m_panelNotes ->Show(showNotesPanel_); +} + + +void ConfigDialog::selectFolderPairConfig(int newPairIndexToShow) +{ + assert(selectedPairIndexToShow_ == EMPTY_PAIR_INDEX_SELECTED); + assert(newPairIndexToShow == -1 || makeUnsigned(newPairIndexToShow) < localPairCfg_.size()); + newPairIndexToShow = std::clamp(newPairIndexToShow, -1, static_cast(localPairCfg_.size()) - 1); + + selectedPairIndexToShow_ = newPairIndexToShow; + m_listBoxFolderPair->SetSelection(newPairIndexToShow + 1); + + //show/hide controls that are only relevant for main/local config + const bool mainConfigSelected = newPairIndexToShow < 0; + //comparison panel: + m_staticTextMainCompSettings->Show( mainConfigSelected && showMultipleCfgs_); + m_checkBoxUseLocalCmpOptions->Show(!mainConfigSelected && showMultipleCfgs_); + m_staticlineCompHeader->Show(showMultipleCfgs_); + //filter panel + m_staticTextMainFilterSettings ->Show( mainConfigSelected && showMultipleCfgs_); + m_staticTextLocalFilterSettings->Show(!mainConfigSelected && showMultipleCfgs_); + m_staticlineFilterHeader->Show(showMultipleCfgs_); + //sync panel: + m_staticTextMainSyncSettings ->Show( mainConfigSelected && showMultipleCfgs_); + m_checkBoxUseLocalSyncOptions->Show(!mainConfigSelected && showMultipleCfgs_); + m_staticlineSyncHeader->Show(showMultipleCfgs_); + //misc + bSizerPerformance->Show(mainConfigSelected); //caveat: recursively shows hidden child items! + bSizerCompMisc ->Show(mainConfigSelected); + bSizerSyncMisc ->Show(mainConfigSelected); + + if (mainConfigSelected) + { + m_hyperlinkPerfDeRequired->Show(!enableExtraFeatures_); //keep after bSizerPerformance->Show() + + //update the devices list for "parallel file operations" before calling setMiscSyncOptions(): + // => should be enough to do this when selecting the main config + // => to be "perfect" we'd have to update already when the user drags & drops a different versioning folder + devicesForEdit_.clear(); + auto addDevicePath = [&](const Zstring& folderPathPhrase) + { + const AfsDevice& afsDevice = createAbstractPath(folderPathPhrase).afsDevice; + if (!AFS::isNullDevice(afsDevice)) + devicesForEdit_.insert(afsDevice); + }; + for (const LocalPairConfig& fpCfg : localPairCfg_) + { + addDevicePath(fpCfg.folderPathPhraseLeft); + addDevicePath(fpCfg.folderPathPhraseRight); + + if (fpCfg.localSyncCfg && fpCfg.localSyncCfg->deletionVariant == DeletionVariant::versioning) + addDevicePath(fpCfg.localSyncCfg->versioningFolderPhrase); + } + if (globalPairCfg_.syncCfg.deletionVariant == DeletionVariant::versioning) //let's always add, even if *all* folder pairs use a local sync config (=> strange!) + addDevicePath(globalPairCfg_.syncCfg.versioningFolderPhrase); + //--------------------------------------------------------------------------------------------------------------- + + setCompConfig (&globalPairCfg_.cmpCfg); + setSyncConfig (&globalPairCfg_.syncCfg); + setFilterConfig(globalPairCfg_.filter); + } + else + { + setCompConfig(get(localPairCfg_[selectedPairIndexToShow_].localCmpCfg)); + setSyncConfig(get(localPairCfg_[selectedPairIndexToShow_].localSyncCfg)); + setFilterConfig (localPairCfg_[selectedPairIndexToShow_].localFilter); + } + setMiscSyncOptions(globalPairCfg_.miscCfg); + + m_panelCompSettingsTab ->Layout(); //fix comp panel glitch on Win 7 125% font size + perf panel + m_panelFilterSettingsTab->Layout(); + m_panelSyncSettingsTab ->Layout(); +} + + +bool ConfigDialog::unselectFolderPairConfig(bool validateParams) +{ + assert(selectedPairIndexToShow_ == -1 || makeUnsigned(selectedPairIndexToShow_) < localPairCfg_.size()); + + std::optional compCfg = getCompConfig(); + std::optional syncCfg = getSyncConfig(); + FilterConfig filterCfg = getFilterConfig(); + + MiscSyncConfig miscCfg = getMiscSyncOptions(); //some "misc" options are always visible, e.g. "notes" + + //------- parameter validation (BEFORE writing output!) ------- + if (validateParams) + { + //parameter validation and correction: + + std::vector baseFolderPaths; //display paths to fix filter if user pastes full folder paths + if (selectedPairIndexToShow_ < 0) + for (const LocalPairConfig& lpc : localPairCfg_) + { + baseFolderPaths.push_back(createAbstractPath(lpc.folderPathPhraseLeft)); + baseFolderPaths.push_back(createAbstractPath(lpc.folderPathPhraseRight)); + } + else + { + baseFolderPaths.push_back(createAbstractPath(localPairCfg_[selectedPairIndexToShow_].folderPathPhraseLeft)); + baseFolderPaths.push_back(createAbstractPath(localPairCfg_[selectedPairIndexToShow_].folderPathPhraseRight)); + } + if (!sanitizeFilter(filterCfg, baseFolderPaths, this)) + { + m_notebook->ChangeSelection(static_cast(SyncConfigPanel::filter)); + m_textCtrlExclude->SetFocus(); + return false; + } + + if (syncCfg && syncCfg->deletionVariant == DeletionVariant::versioning) + { + if (AFS::isNullPath(createAbstractPath(syncCfg->versioningFolderPhrase))) + { + m_notebook->ChangeSelection(static_cast(SyncConfigPanel::sync)); + showNotificationDialog(this, DialogInfoType::info, PopupDialogCfg().setMainInstructions(_("Please enter a target folder."))); + //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(SyncConfigPanel::sync)); + showNotificationDialog(this, DialogInfoType::info, PopupDialogCfg().setMainInstructions(_("Minimum version count must be smaller than maximum count."))); + m_spinCtrlVersionCountMin->SetFocus(); + return false; + } + } + + if (selectedPairIndexToShow_ < 0) + { + if (AFS::isNullPath(createAbstractPath(miscCfg.altLogFolderPathPhrase)) && + !miscCfg.altLogFolderPathPhrase.empty()) + { + m_notebook->ChangeSelection(static_cast(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(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(); + } + } + //------------------------------------------------------------- + + if (selectedPairIndexToShow_ < 0) + { + globalPairCfg_.cmpCfg = *compCfg; + globalPairCfg_.syncCfg = *syncCfg; + globalPairCfg_.filter = filterCfg; + } + else + { + localPairCfg_[selectedPairIndexToShow_].localCmpCfg = compCfg; + localPairCfg_[selectedPairIndexToShow_].localSyncCfg = syncCfg; + localPairCfg_[selectedPairIndexToShow_].localFilter = filterCfg; + } + globalPairCfg_.miscCfg = miscCfg; + + selectedPairIndexToShow_ = EMPTY_PAIR_INDEX_SELECTED; + //m_listBoxFolderPair->SetSelection(wxNOT_FOUND); not needed, selectedPairIndexToShow has parameter ownership + return true; +} + + +void ConfigDialog::onAddNotes(wxCommandEvent& event) +{ + showNotesPanel_ = true; + updateMiscGui(); + + //=> enlarge dialog height! + GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() + + m_textCtrNotes->SetFocus(); +} + + +void ConfigDialog::onOkay(wxCommandEvent& event) +{ + if (!unselectFolderPairConfig(true /*validateParams*/)) + return; + + globalPairCfgOut_ = globalPairCfg_; + localPairCfgOut_ = localPairCfg_; + + EndModal(static_cast(ConfirmationButton::accept)); +} + + +//save global settings: should NOT be impacted by OK/Cancel +ConfigDialog::~ConfigDialog() +{ + versioningFolderHistoryOut_ = m_versioningFolderPath->getHistory()->getList(); + logFolderHistoryOut_ = m_logFolderPath ->getHistory()->getList(); + + commandHistoryOut_ = m_comboBoxPostSyncCommand->getHistory(); + emailHistoryOut_ = m_comboBoxEmail ->getHistory(); +} +} + +//######################################################################################## + +ConfirmationButton fff::showSyncConfigDlg(wxWindow* parent, + SyncConfigPanel panelToShow, + int localPairIndexToShow, bool showMultipleCfgs, + + GlobalPairConfig& globalPairCfg, + std::vector& localPairCfg, + + FilterConfig& defaultFilter, + std::vector& versioningFolderHistory, Zstring& versioningFolderLastSelected, + std::vector& logFolderHistory, Zstring& logFolderLastSelected, const Zstring& globalLogFolderPhrase, + size_t folderHistoryMax, Zstring& sftpKeyFileLastSelected, + std::vector& emailHistory, size_t emailHistoryMax, + std::vector& commandHistory, size_t commandHistoryMax) +{ + + ConfigDialog syncDlg(parent, + panelToShow, + localPairIndexToShow, showMultipleCfgs, + globalPairCfg, + localPairCfg, + defaultFilter, + versioningFolderHistory, versioningFolderLastSelected, + logFolderHistory, logFolderLastSelected, globalLogFolderPhrase, + folderHistoryMax, sftpKeyFileLastSelected, + emailHistory, + emailHistoryMax, + commandHistory, + commandHistoryMax); + return static_cast(syncDlg.ShowModal()); +} diff --git a/FreeFileSync/Source/ui/sync_cfg.h b/FreeFileSync/Source/ui/sync_cfg.h new file mode 100644 index 0000000..6d32009 --- /dev/null +++ b/FreeFileSync/Source/ui/sync_cfg.h @@ -0,0 +1,66 @@ +// ***************************************************************************** +// * 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 SYNC_CFG_H_31289470134253425 +#define SYNC_CFG_H_31289470134253425 + +#include +#include "../base/structures.h" + + +namespace fff +{ +enum class SyncConfigPanel +{ + compare = 0, //used as zero-based notebook page index! + filter, + sync, +}; + +struct MiscSyncConfig +{ + std::map deviceParallelOps; + bool ignoreErrors = false; + size_t autoRetryCount = 0; + std::chrono::seconds autoRetryDelay{0}; + + Zstring postSyncCommand; + PostSyncCondition postSyncCondition = PostSyncCondition::completion; + + Zstring altLogFolderPathPhrase; + + std::string emailNotifyAddress; + ResultsNotification emailNotifyCondition = ResultsNotification::always; + + std::wstring notes; +}; + +struct GlobalPairConfig +{ + CompConfig cmpCfg; + SyncConfig syncCfg; + FilterConfig filter; + MiscSyncConfig miscCfg; +}; + + +zen::ConfirmationButton showSyncConfigDlg(wxWindow* parent, + SyncConfigPanel panelToShow, + int localPairIndexToShow, //< 0 to show global config + bool showMultipleCfgs, + + GlobalPairConfig& globalPairCfg, + std::vector& localPairCfg, + + FilterConfig& defaultFilter, + std::vector& versioningFolderHistory, Zstring& versioningFolderLastSelected, + std::vector& logFolderHistory, Zstring& logFolderLastSelected, const Zstring& globalLogFolderPhrase, + size_t folderHistoryMax, Zstring& sftpKeyFileLastSelected, + std::vector& emailHistory, size_t emailHistoryMax, + std::vector& commandHistory, size_t commandHistoryMax); +} + +#endif //SYNC_CFG_H_31289470134253425 diff --git a/FreeFileSync/Source/ui/tray_icon.cpp b/FreeFileSync/Source/ui/tray_icon.cpp new file mode 100644 index 0000000..53db435 --- /dev/null +++ b/FreeFileSync/Source/ui/tray_icon.cpp @@ -0,0 +1,198 @@ +// ***************************************************************************** +// * 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 "tray_icon.h" +#include +#include +#include +#include //req. by Linux +#include +#include +#include + +using namespace zen; +using namespace fff; + + +namespace +{ +void fillRange(wxImage& img, int pixelFirst, int pixelLast, const wxColor& col) //tolerant input range +{ + const int width = img.GetWidth (); + const int height = img.GetHeight(); + + if (width > 0 && height > 0) + { + pixelFirst = std::max(pixelFirst, 0); + pixelLast = std::min(pixelLast, width * height); + + if (pixelFirst < pixelLast) + { + const unsigned char r = col.Red (); // + const unsigned char g = col.Green(); //getting RGB involves virtual function calls! + const unsigned char b = col.Blue (); // + + unsigned char* rgb = img.GetData() + pixelFirst * 3; + for (int x = pixelFirst; x < pixelLast; ++x) + { + *rgb++ = r; + *rgb++ = g; + *rgb++ = b; + } + + if (img.HasAlpha()) //make progress indicator fully opaque: + std::fill(img.GetAlpha() + pixelFirst, img.GetAlpha() + pixelLast, wxIMAGE_ALPHA_OPAQUE); + } + } +} +} +//------------------------------------------------------------------------------------------------ + + +//generate icon with progress indicator +class FfsTrayIcon::ProgressIconGenerator +{ +public: + explicit ProgressIconGenerator(const wxImage& logo) : logo_(logo) {} + + wxBitmap get(double fraction); + +private: + const wxImage logo_; + wxBitmap iconBuf_; + int startPixBuf_ = -1; +}; + + +wxBitmap FfsTrayIcon::ProgressIconGenerator::get(double fraction) +{ + if (!logo_.IsOk() || logo_.GetWidth() <= 0 || logo_.GetHeight() <= 0) + return wxIcon(); + + const int pixelCount = logo_.GetWidth() * logo_.GetHeight(); + const int startFillPixel = std::clamp(std::floor(fraction * pixelCount), 0, pixelCount); + + if (startPixBuf_ != startFillPixel) + { + wxImage genImage(logo_.Copy()); //workaround wxWidgets' screwed-up design from hell: their copy-construction implements reference-counting WITHOUT copy-on-write! + + //gradually make FFS icon brighter while nearing completion + brighten(genImage, -200 * (1 - fraction)); + + //fill black border row + if (startFillPixel <= pixelCount - genImage.GetWidth()) + { + /* -------- + ---bbbbb + bbbbSyyy S : start yellow remainder + yyyyyyyy */ + + int bStart = startFillPixel - genImage.GetWidth(); + if (bStart % genImage.GetWidth() != 0) //add one more black pixel, see ascii-art + --bStart; + fillRange(genImage, bStart, startFillPixel, *wxBLACK); + } + else if (startFillPixel < pixelCount) + { + /* special handling for last row: + -------- + -------- + ---bbbbb + ---bSyyy S : start yellow remainder */ + + int bStart = startFillPixel - genImage.GetWidth() - 1; + int bEnd = (bStart / genImage.GetWidth() + 1) * genImage.GetWidth(); + + fillRange(genImage, bStart, bEnd, *wxBLACK); + fillRange(genImage, startFillPixel - 1, startFillPixel, *wxBLACK); + } + + //fill yellow remainder + fillRange(genImage, startFillPixel, pixelCount, wxColor(240, 200, 0)); + + iconBuf_ = toScaledBitmap(genImage); + startPixBuf_ = startFillPixel; + } + + return iconBuf_; +} + + +class FfsTrayIcon::TrayIconImpl : public wxTaskBarIcon +{ +public: + TrayIconImpl(const std::function& requestResume) : requestResume_(requestResume) + { + Bind(wxEVT_TASKBAR_LEFT_UP, [this](wxTaskBarIconEvent& event) { if (requestResume_) requestResume_(); }); + } + + void disconnectCallbacks() { requestResume_ = nullptr; } + +private: + wxMenu* CreatePopupMenu() override + { + if (!requestResume_) + return nullptr; + + wxMenu* contextMenu = new wxMenu; + + wxMenuItem* defaultItem = new wxMenuItem(contextMenu, wxID_ANY, _("&Restore")); + //wxWidgets font mess-up: + //1. font must be set *before* wxMenu::Append()! + //2. don't use defaultItem->GetFont(); making it bold creates a huge font size for some reason + contextMenu->Append(defaultItem); + + contextMenu->Bind(wxEVT_COMMAND_MENU_SELECTED, [this](wxCommandEvent& event) { if (requestResume_) requestResume_(); }, defaultItem->GetId()); + + return contextMenu; //ownership transferred to caller + } + + //void onLeftDownClick(wxEvent& event) + //{ + // //copied from wxTaskBarIconBase::OnRightButtonDown() + // if (wxMenu* menu = CreatePopupMenu()) + // { + // PopupMenu(menu); + // delete menu; + // } + //} + + std::function requestResume_; +}; + + +FfsTrayIcon::FfsTrayIcon(const std::function& requestResume) : + trayIcon_(new TrayIconImpl(requestResume)), + progressIcon_(std::make_unique(loadImage("start_sync", dipToScreen(24)))) +{ + [[maybe_unused]] const bool rv = trayIcon_->SetIcon(progressIcon_->get(activeFraction_), activeToolTip_); + assert(rv); //caveat wxTaskBarIcon::SetIcon() can return true, even if not wxTaskBarIcon::IsAvailable()!!! +} + + +FfsTrayIcon::~FfsTrayIcon() +{ + trayIcon_->disconnectCallbacks(); //TrayIconImpl has longer lifetime than FfsTrayIcon: avoid callback! + + trayIcon_->RemoveIcon(); + + //*schedule* for destruction: delete during next idle event (handle late window messages, e.g. when double-clicking) + trayIcon_->Destroy(); //uses wxPendingDelete +} + + +void FfsTrayIcon::setToolTip(const wxString& toolTip) +{ + activeToolTip_ = toolTip; + trayIcon_->SetIcon(progressIcon_->get(activeFraction_), activeToolTip_); //another wxWidgets design bug: non-orthogonal method! +} + + +void FfsTrayIcon::setProgress(double fraction) +{ + activeFraction_ = fraction; + trayIcon_->SetIcon(progressIcon_->get(activeFraction_), activeToolTip_); +} diff --git a/FreeFileSync/Source/ui/tray_icon.h b/FreeFileSync/Source/ui/tray_icon.h new file mode 100644 index 0000000..2106b18 --- /dev/null +++ b/FreeFileSync/Source/ui/tray_icon.h @@ -0,0 +1,50 @@ +// ***************************************************************************** +// * 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 TRAY_ICON_H_84217830427534285 +#define TRAY_ICON_H_84217830427534285 + +#include +#include +#include + + +/* show tray icon with progress during lifetime of this instance + + ATTENTION: wxWidgets never assumes that an object indirectly destroys itself while processing an event! + this includes wxEvtHandler-derived objects!!! + it seems wxTaskBarIcon::ProcessEvent() works (on Windows), but AddPendingEvent() will crash since it uses "this" after the event processing! + + => don't derive from wxEvtHandler or any other wxWidgets object here!!!!!! + => use simple std::function as callback instead => FfsTrayIcon instance may now be safely deleted in callback + while ~wxTaskBarIcon is delayed via wxPendingDelete */ +namespace fff +{ +class FfsTrayIcon +{ +public: + explicit FfsTrayIcon(const std::function& requestResume); //callback only held during lifetime of this instance + ~FfsTrayIcon(); + + void setToolTip(const wxString& toolTip); + void setProgress(double fraction); //number between [0, 1], for small progress indicator + +private: + FfsTrayIcon (const FfsTrayIcon&) = delete; + FfsTrayIcon& operator=(const FfsTrayIcon&) = delete; + + class TrayIconImpl; + TrayIconImpl* trayIcon_; + + class ProgressIconGenerator; + std::unique_ptr progressIcon_; + + wxString activeToolTip_ = L"FreeFileSync"; + double activeFraction_ = 1; +}; +} + +#endif //TRAY_ICON_H_84217830427534285 diff --git a/FreeFileSync/Source/ui/tree_grid.cpp b/FreeFileSync/Source/ui/tree_grid.cpp new file mode 100644 index 0000000..7bf9401 --- /dev/null +++ b/FreeFileSync/Source/ui/tree_grid.cpp @@ -0,0 +1,1218 @@ +// ***************************************************************************** +// * 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 "tree_grid.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../icon_buffer.h" + +using namespace zen; +using namespace fff; + + +namespace +{ +//let's NOT create wxWidgets objects statically: +const int PERCENTAGE_BAR_WIDTH_DIP = 60; +const int TREE_GRID_GAP_SIZE_DIP = 4; + +inline wxColor getColorPercentBorder () { return {198, 198, 198}; } +inline wxColor getColorPercentBackground() { return {0xf8, 0xf8, 0xf8}; } + + +Zstring getFolderPairName(const FolderPair& folder) +{ + if (folder.hasEquivalentItemNames()) + return folder.getItemName(); + else + return folder.getItemName() + Zstr(" | ") + + folder.getItemName(); +} +} + + +TreeView::TreeView(FolderComparison& folderCmp, const SortInfo& si) : currentSort_(si) +{ + for (SharedRef& baseObj : folderCmp) + //remove truly empty folder pairs as early as this: we want to distinguish single/multiple folder pair cases by looking at "folderCmp" + if (!AFS::isNullPath(baseObj.ref().getAbstractPath()) || + !AFS::isNullPath(baseObj.ref().getAbstractPath())) + folderCmp_.push_back(baseObj.ptr()); +} + + +inline +void TreeView::compressNode(Container& cont) //remove single-element sub-trees -> gain clarity + usability (call *after* inclusion check!!!) +{ + if (cont.subDirs.empty()) //single files node + cont.showFilesNode = false; + +#if 0 //let's not go overboard: empty folders should not be condensed => used for file exclusion filter; user expects to see them + if (!cont.showFilesNode && //single dir node... + cont.subDirs.size() == 1 && // + !cont.subDirs[0].showFilesNode && //...that is empty + cont.subDirs[0].subDirs.empty()) // + cont.subDirs.clear(); +#endif +} + + +template //(const FileSystemObject&) -> bool +void TreeView::extractVisibleSubtree(ContainerObject& conObj, //in + TreeView::Container& cont, //out + Function pred) +{ + auto getBytes = [](const FilePair& file) //MSVC screws up miserably if we put this lambda into std::for_each + { +#if 0 //give accumulated bytes the semantics of a sync preview? + switch (getEffectiveSyncDir(file.getSyncOperation())) + { + case SyncDirection::none: break; + case SyncDirection::left: return file.getFileSize(); + case SyncDirection::right: return file.getFileSize(); + } +#endif + //prefer file-browser semantics over sync preview (=> always show useful numbers, even for SyncDirection::none) + //discussion: https://freefilesync.org/forum/viewtopic.php?t=1595 + return std::max(file.isEmpty() ? 0 : file.getFileSize(), + file.isEmpty() ? 0 : file.getFileSize()); + }; + + for (FilePair& file : conObj.files()) + if (pred(file)) + { + cont.bytesNet += getBytes(file); + ++cont.itemCountNet; + } + + for (SymlinkPair& symlink : conObj.symlinks()) + if (pred(symlink)) + ++cont.itemCountNet; + + cont.showFilesNode = cont.itemCountNet > 0; + + cont.bytesGross += cont.bytesNet; + cont.itemCountGross += cont.itemCountNet; + + cont.subDirs.reserve(conObj.subfolders().size()); //avoid expensive reallocations! + + for (FolderPair& folder : conObj.subfolders()) + { + const bool included = pred(folder); + + cont.subDirs.emplace_back(); // + auto& subDirCont = cont.subDirs.back(); + TreeView::extractVisibleSubtree(folder, subDirCont, pred); + if (included) + ++subDirCont.itemCountGross; + + cont.bytesGross += subDirCont.bytesGross; + cont.itemCountGross += subDirCont.itemCountGross; + + if (!included && subDirCont.subDirs.empty() && !subDirCont.showFilesNode) + cont.subDirs.pop_back(); + else + { + subDirCont.containerRef = std::static_pointer_cast(folder.shared_from_this()); + compressNode(subDirCont); + } + } +} + + +namespace +{ +//generate "nice" percentage numbers which precisely add up to 100 +void calcPercentage(std::vector>& workList) +{ + uint64_t bytesTotal = 0; + for (const auto& [bytes, percentOut] : workList) + bytesTotal += bytes; + + if (bytesTotal == 0U) //this case doesn't work with the error minimizing algorithm below + { + for (auto& [bytes, percentOut] : workList) + *percentOut = 0; + return; + } + + int remainingPercent = 100; + for (auto& [bytes, percentOut] : workList) + { + *percentOut = static_cast(bytes * 100U / bytesTotal); //round down + remainingPercent -= *percentOut; + } + assert(remainingPercent >= 0); + assert(remainingPercent < std::ssize(workList)); + + //distribute remaining percent so that overall error is minimized as much as possible: + remainingPercent = std::min(remainingPercent, static_cast(workList.size())); + if (remainingPercent > 0) + { + std::nth_element(workList.begin(), workList.begin() + remainingPercent - 1, workList.end(), + [bytesTotal](const std::pair& lhs, const std::pair& rhs) + { + return lhs.first * 100U % bytesTotal > rhs.first * 100U % bytesTotal; + }); + + std::for_each(workList.begin(), workList.begin() + remainingPercent, [&](std::pair& pair) { ++*pair.second; }); + } +} +} + + +template +struct TreeView::LessShortName +{ + bool operator()(const TreeLine& lhs, const TreeLine& rhs) const + { + //files last (irrespective of sort direction) + if (lhs.type == NodeType::files) + return false; + else if (rhs.type == NodeType::files) + return true; + + if (lhs.type != rhs.type) // + return lhs.type < rhs.type; //shouldn't happen! root nodes not mixed with files or directories + + switch (lhs.type) + { + case NodeType::root: + return makeSortDirection(LessNaturalSort() /*even on Linux*/, + std::bool_constant())(utfTo(static_cast(lhs.node)->displayName), + utfTo(static_cast(rhs.node)->displayName)); + case NodeType::folder: + { + const auto* folderL = static_cast(lhs.node->containerRef.lock().get()); + const auto* folderR = static_cast(rhs.node->containerRef.lock().get()); + + if (!folderL) + return false; + else if (!folderR) + return true; + + return makeSortDirection(LessNaturalSort(), std::bool_constant())(getFolderPairName(*folderL), getFolderPairName(*folderR)); + } + + case NodeType::files: + break; + } + assert(false); + return false; //:= all equal + } +}; + + +template +void TreeView::sortSingleLevel(std::vector& items, ColumnTypeOverview columnType) +{ + auto getBytes = [](const TreeLine& line) -> uint64_t + { + switch (line.type) + { + case NodeType::root: + case NodeType::folder: + return line.node->bytesGross; + case NodeType::files: + return line.node->bytesNet; + } + assert(false); + return 0U; + }; + + auto getCount = [](const TreeLine& line) -> int + { + switch (line.type) + { + case NodeType::root: + case NodeType::folder: + return line.node->itemCountGross; + + case NodeType::files: + return line.node->itemCountNet; + } + assert(false); + return 0; + }; + + const auto lessBytes = [&](const TreeLine& lhs, const TreeLine& rhs) { return getBytes(lhs) < getBytes(rhs); }; + const auto lessCount = [&](const TreeLine& lhs, const TreeLine& rhs) { return getCount(lhs) < getCount(rhs); }; + + switch (columnType) + { + case ColumnTypeOverview::folder: + std::sort(items.begin(), items.end(), LessShortName()); + break; + case ColumnTypeOverview::itemCount: + std::sort(items.begin(), items.end(), makeSortDirection(lessCount, std::bool_constant())); + break; + case ColumnTypeOverview::bytes: + std::sort(items.begin(), items.end(), makeSortDirection(lessBytes, std::bool_constant())); + break; + } +} + + +void TreeView::getChildren(const Container& cont, unsigned int level, std::vector& output) +{ + output.clear(); + output.reserve(cont.subDirs.size() + 1); //keep pointers in "workList" valid + std::vector> workList; + + for (const Container& subDir : cont.subDirs) + { + output.push_back({level, 0, &subDir, NodeType::folder}); + workList.emplace_back(subDir.bytesGross, &output.back().percent); + } + + if (cont.showFilesNode) + { + output.push_back({level, 0, &cont, NodeType::files}); + workList.emplace_back(cont.bytesNet, &output.back().percent); + } + calcPercentage(workList); + + if (currentSort_.ascending) + sortSingleLevel(output, currentSort_.sortCol); + else + sortSingleLevel(output, currentSort_.sortCol); +} + + +void TreeView::applySubView(std::vector&& newView) +{ + //preserve current node expansion status + auto getContainer = [](const TreeView::TreeLine& tl) -> const ContainerObject* + { + switch (tl.type) + { + case NodeType::root: + case NodeType::folder: + return tl.node->containerRef.lock().get(); + + case NodeType::files: + break; //none!!! + } + return nullptr; + }; + + std::unordered_set expandedNodes; + if (!flatTree_.empty()) + { + auto it = flatTree_.begin(); + for (auto itNext = flatTree_.begin() + 1; itNext != flatTree_.end(); ++itNext, ++it) + if (it->level < itNext->level) + if (auto conObj = getContainer(*it)) + expandedNodes.insert(conObj); + } + + //update view on full data + folderCmpView_.swap(newView); //newView may be an alias for folderCmpView! see sorting! + + //set default flat tree + flatTree_.clear(); + + if (folderCmp_.size() == 1) //single folder pair case (empty pairs were already removed!) do NOT use folderCmpView for this check! + { + if (!folderCmpView_.empty()) //possibly empty! + getChildren(folderCmpView_[0], 0, flatTree_); //do not show root + } + else + { + //following is almost identical with TreeView::getChildren(): + // however we *cannot* reuse code here since "std::vector" is not a "Container"! + + flatTree_.reserve(folderCmpView_.size()); //keep pointers in "workList" valid + std::vector> workList; + + for (const RootNodeImpl& root : folderCmpView_) + { + flatTree_.push_back({0, 0, &root, NodeType::root}); + workList.emplace_back(root.bytesGross, &flatTree_.back().percent); + } + + calcPercentage(workList); + + if (currentSort_.ascending) + sortSingleLevel(flatTree_, currentSort_.sortCol); + else + sortSingleLevel(flatTree_, currentSort_.sortCol); + } + + //restore node expansion status + for (size_t row = 0; row < flatTree_.size(); ++row) //flatTree size changes during loop! + { + const TreeLine& line = flatTree_[row]; + + if (auto conObj = getContainer(line)) + if (expandedNodes.contains(conObj)) + { + std::vector newLines; + getChildren(*line.node, line.level + 1, newLines); + + flatTree_.insert(flatTree_.begin() + row + 1, newLines.begin(), newLines.end()); + } + } +} + + +template +void TreeView::updateView(Predicate pred) +{ + //update view on full data + std::vector newView; + newView.reserve(folderCmp_.size()); //avoid expensive reallocations! + + for (const std::weak_ptr& baseObjRef : folderCmp_) + if (BaseFolderPair* baseObj = baseObjRef.lock().get()) + { + newView.emplace_back(); + RootNodeImpl& root = newView.back(); + this->extractVisibleSubtree(*baseObj, root, pred); //"this->" is bogus for a static method, but GCC screws this one up + + //warning: the following lines are almost 1:1 copy from extractVisibleSubtree, adapted for BaseFolderPair: + if (root.subDirs.empty() && !root.showFilesNode) + newView.pop_back(); + else + { + root.containerRef = baseObjRef; + root.displayName = getShortDisplayNameForFolderPair(baseObj->getAbstractPath(), + baseObj->getAbstractPath()); + + this->compressNode(root); //"this->" required by two-pass lookup as enforced by GCC 4.7 + } + } + + lastViewFilterPred_ = pred; + applySubView(std::move(newView)); +} + + +void TreeView::setSortDirection(ColumnTypeOverview colType, bool ascending) //apply permanently! +{ + currentSort_ = SortInfo{colType, ascending}; + + //reapply current view + applySubView(std::move(folderCmpView_)); +} + + +TreeView::NodeStatus TreeView::getStatus(size_t row) const +{ + if (row < flatTree_.size()) + { + if (row + 1 < flatTree_.size() && flatTree_[row + 1].level > flatTree_[row].level) + return NodeStatus::expanded; + + //it's either reduced or empty + switch (flatTree_[row].type) + { + case NodeType::root: + case NodeType::folder: + return flatTree_[row].node->showFilesNode || !flatTree_[row].node->subDirs.empty() ? NodeStatus::reduced : NodeStatus::empty; + + case NodeType::files: + return NodeStatus::empty; + } + } + return NodeStatus::empty; +} + + +void TreeView::expandNode(size_t row) +{ + if (getStatus(row) != NodeStatus::reduced) + { + assert(false); + return; + } + + if (row < flatTree_.size()) + { + std::vector newLines; + + switch (flatTree_[row].type) + { + case NodeType::root: + case NodeType::folder: + getChildren(*flatTree_[row].node, flatTree_[row].level + 1, newLines); + break; + case NodeType::files: + break; + } + flatTree_.insert(flatTree_.begin() + row + 1, newLines.begin(), newLines.end()); + } +} + + +void TreeView::reduceNode(size_t row) +{ + if (row < flatTree_.size()) + { + const unsigned int parentLevel = flatTree_[row].level; + + bool done = false; + flatTree_.erase(std::remove_if(flatTree_.begin() + row + 1, flatTree_.end(), + [&](const TreeLine& line) -> bool + { + if (done) + return false; + if (line.level > parentLevel) + return true; + else + { + done = true; + return false; + } + }), flatTree_.end()); + } +} + + +ptrdiff_t TreeView::getParent(size_t row) const +{ + if (row < flatTree_.size()) + { + const auto level = flatTree_[row].level; + + while (row-- > 0) + if (flatTree_[row].level < level) + return row; + } + return -1; +} + + +void TreeView::applyDifferenceFilter(bool showExcluded, + bool leftOnlyFilesActive, + bool rightOnlyFilesActive, + bool leftNewerFilesActive, + bool rightNewerFilesActive, + bool differentFilesActive, + bool equalFilesActive, + bool conflictFilesActive) +{ + updateView([showExcluded, //make sure the predicate can be stored safely! + leftOnlyFilesActive, + rightOnlyFilesActive, + leftNewerFilesActive, + rightNewerFilesActive, + differentFilesActive, + equalFilesActive, + conflictFilesActive](const FileSystemObject& fsObj) -> bool + { + if (!fsObj.isActive() && !showExcluded) + return false; + + switch (fsObj.getCategory()) + { + case FILE_LEFT_ONLY: + return leftOnlyFilesActive; + case FILE_RIGHT_ONLY: + return rightOnlyFilesActive; + case FILE_LEFT_NEWER: + return leftNewerFilesActive; + case FILE_RIGHT_NEWER: + return rightNewerFilesActive; + case FILE_DIFFERENT_CONTENT: + return differentFilesActive; + case FILE_EQUAL: + return equalFilesActive; + case FILE_RENAMED: + case FILE_CONFLICT: + case FILE_TIME_INVALID: + return conflictFilesActive; + } + assert(false); + return true; + }); +} + + +void TreeView::applyActionFilter(bool showExcluded, + bool syncCreateLeftActive, + bool syncCreateRightActive, + bool syncDeleteLeftActive, + bool syncDeleteRightActive, + bool syncDirOverwLeftActive, + bool syncDirOverwRightActive, + bool syncDirNoneActive, + bool syncEqualActive, + bool conflictFilesActive) +{ + updateView([showExcluded, //make sure the predicate can be stored safely! + syncCreateLeftActive, + syncCreateRightActive, + syncDeleteLeftActive, + syncDeleteRightActive, + syncDirOverwLeftActive, + syncDirOverwRightActive, + syncDirNoneActive, + syncEqualActive, + conflictFilesActive](const FileSystemObject& fsObj) -> bool + { + if (!fsObj.isActive() && !showExcluded) + return false; + + switch (fsObj.getSyncOperation()) + { + case SO_CREATE_LEFT: + return syncCreateLeftActive; + case SO_CREATE_RIGHT: + return syncCreateRightActive; + case SO_DELETE_LEFT: + return syncDeleteLeftActive; + case SO_DELETE_RIGHT: + return syncDeleteRightActive; + case SO_OVERWRITE_RIGHT: + case SO_RENAME_RIGHT: + case SO_MOVE_RIGHT_FROM: + case SO_MOVE_RIGHT_TO: + return syncDirOverwRightActive; + case SO_OVERWRITE_LEFT: + case SO_RENAME_LEFT: + case SO_MOVE_LEFT_FROM: + case SO_MOVE_LEFT_TO: + return syncDirOverwLeftActive; + case SO_DO_NOTHING: + return syncDirNoneActive; + case SO_EQUAL: + return syncEqualActive; + case SO_UNRESOLVED_CONFLICT: + return conflictFilesActive; + } + assert(false); + return true; + }); +} + + +std::unique_ptr TreeView::getLine(size_t row) const +{ + if (row < flatTree_.size()) + { + const auto level = flatTree_[row].level; + const int percent = flatTree_[row].percent; + + switch (flatTree_[row].type) + { + case NodeType::root: + { + const auto& root = *static_cast(flatTree_[row].node); + + if (BaseFolderPair* baseFolder = static_cast(root.containerRef.lock().get())) + return std::make_unique(percent, root.bytesGross, root.itemCountGross, getStatus(row), *baseFolder, root.displayName); + } + break; + + case NodeType::folder: + { + const Container& contObj = *flatTree_[row].node; + + if (FolderPair* folder = static_cast(contObj.containerRef.lock().get())) + return std::make_unique(percent, contObj.bytesGross, contObj.itemCountGross, level, getStatus(row), *folder); + } + break; + + case NodeType::files: + { + const auto& parentFolder = *flatTree_[row].node; + + if (ContainerObject* conObj = parentFolder.containerRef.lock().get()) + { + std::vector filesAndLinks; + + //lazy evaluation: recheck "lastViewFilterPred" again rather than buffer and bloat "lastViewFilterPred" + for (FileSystemObject& fsObj : conObj->files()) + if (lastViewFilterPred_(fsObj)) + filesAndLinks.push_back(&fsObj); + + for (FileSystemObject& fsObj : conObj->symlinks()) + if (lastViewFilterPred_(fsObj)) + filesAndLinks.push_back(&fsObj); + + return std::make_unique(percent, parentFolder.bytesNet, parentFolder.itemCountNet, level, std::move(filesAndLinks)); + } + } + break; + } + } + return nullptr; +} + +//########################################################################################################## + +namespace +{ +wxColor getColorForLevel(size_t level) +{ + switch (level % 12) + { + case 0: return {0xcc, 0xcc, 0xff}; + case 1: return {0xcc, 0xff, 0xcc}; + case 2: return {0xff, 0xff, 0x99}; + case 3: return {0xdd, 0xdd, 0xdd}; + case 4: return {0xff, 0xcc, 0xff}; + case 5: return {0x99, 0xff, 0xcc}; + case 6: return {0xcc, 0xcc, 0x99}; + case 7: return {0xff, 0xcc, 0xcc}; + case 8: return {0xcc, 0xff, 0x99}; + case 9: return {0xff, 0xff, 0xcc}; + case 10: return {0xcc, 0xff, 0xff}; + case 11: return {0xff, 0xcc, 0x99}; + } + assert(false); + return *wxBLACK; +} + + +class GridDataTree : private wxEvtHandler, public GridData +{ +public: + GridDataTree(Grid& grid) : + widthNodeIcon_(screenToWxsize(IconBuffer::getPixSize(IconBuffer::IconSize::small))), + widthLevelStep_(widthNodeIcon_), + widthNodeStatus_(screenToWxsize(loadImage("node_expanded").GetWidth())), + rootIcon_(loadImage("root_folder", wxsizeToScreen(widthNodeIcon_))), + grid_(grid) + { + grid.Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onKeyDown(event); }); + grid.Bind(EVENT_GRID_MOUSE_LEFT_DOWN, [this](GridClickEvent& event) { onMouseLeft (event); }); + grid.Bind(EVENT_GRID_MOUSE_LEFT_DOUBLE, [this](GridClickEvent& event) { onMouseLeftDouble(event); }); + grid.Bind(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, [this](GridLabelClickEvent& event) { onGridLabelContext (event); }); + grid.Bind(EVENT_GRID_COL_LABEL_MOUSE_LEFT, [this](GridLabelClickEvent& event) { onGridLabelLeftClick(event); }); + } + + void setData(FolderComparison& folderCmp) + { + const TreeView::SortInfo sortCfg = treeDataView_.ref().getSortConfig(); //preserve! + + treeDataView_ = makeSharedRef(); //clear old data view first! avoid memory peaks! + treeDataView_ = makeSharedRef(folderCmp, sortCfg); + } + + const TreeView& getDataView() const { return treeDataView_.ref(); } + /**/ TreeView& getDataView() { return treeDataView_.ref(); } + + void setShowPercentage(bool value) { showPercentBar_ = value; grid_.Refresh(); } + bool getShowPercentage() const { return showPercentBar_; } + +private: + size_t getRowCount() const override { return getDataView().rowsTotal(); } + + std::wstring getToolTip(size_t row, ColumnType colType, HoverArea rowHover) override + { + switch (static_cast(colType)) + { + case ColumnTypeOverview::folder: + if (std::unique_ptr node = getDataView().getLine(row)) + if (const TreeView::RootNode* root = dynamic_cast(node.get())) + { + const std::wstring& dirLeft = AFS::getDisplayPath(root->baseFolder.getAbstractPath()); + const std::wstring& dirRight = AFS::getDisplayPath(root->baseFolder.getAbstractPath()); + if (dirLeft.empty()) + return dirRight; + else if (dirRight.empty()) + return dirLeft; + return dirLeft + /*L' ' + EM_DASH + */ L'\n' + dirRight; + } + break; + + case ColumnTypeOverview::itemCount: + case ColumnTypeOverview::bytes: + break; + } + return std::wstring(); + } + + std::wstring getValue(size_t row, ColumnType colType) const override + { + if (std::unique_ptr node = getDataView().getLine(row)) + switch (static_cast(colType)) + { + case ColumnTypeOverview::folder: + if (const TreeView::RootNode* root = dynamic_cast(node.get())) + return root->displayName; + else if (const TreeView::DirNode* dir = dynamic_cast(node.get())) + return utfTo(getFolderPairName(dir->folder)); + else if (dynamic_cast(node.get())) + return _("Files"); + break; + + case ColumnTypeOverview::itemCount: + return formatNumber(node->itemCount_); + + case ColumnTypeOverview::bytes: + return formatFilesizeShort(node->bytes_); + } + + return std::wstring(); + } + + void renderColumnLabel(wxDC& dc, const wxRect& rect, ColumnType colType, bool enabled, bool highlighted) override + { + const auto colTypeTree = static_cast(colType); + + const wxRect rectInner = drawColumnLabelBackground(dc, rect, highlighted); + wxRect rectRemain = rectInner; + + rectRemain.x += getColumnGapLeft(); + rectRemain.width -= getColumnGapLeft(); + drawColumnLabelText(dc, rectRemain, getColumnLabel(colType), enabled); + + if (const auto [sortCol, ascending] = getDataView().getSortConfig(); + colTypeTree == sortCol) + { + const wxImage sortMarker = loadImage(ascending ? "sort_ascending" : "sort_descending"); + drawBitmapRtlNoMirror(dc, enabled ? sortMarker : sortMarker.ConvertToDisabled(), rectInner, wxALIGN_CENTER_HORIZONTAL); + } + } + + + void renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected, HoverArea rowHover) override + { + if (enabled && selected) + GridData::renderRowBackgound(dc, rect, row, true /*enabled*/, true /*selected*/, rowHover); + else + ; //clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); -> already the default + } + + + enum class HoverAreaTree + { + node, + item, + }; + + void renderCell(wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover) override + { + wxDCTextColourChanger textColor(dc); + if (enabled && selected) //accessibility: always set *both* foreground AND background colors! + textColor.Set(*wxBLACK); + + //wxRect rectTmp= drawCellBorder(dc, rect); + wxRect rectTmp = rect; + + // Partitioning: + // ________________________________________________________________________________ + // | space | gap | percentage bar | 2 x gap | node status | gap |icon | gap | rest | + // -------------------------------------------------------------------------------- + // -> synchronize renderCell() <-> getBestSize() <-> getMouseHover() + + if (static_cast(colType) == ColumnTypeOverview::folder) + { + if (std::unique_ptr node = getDataView().getLine(row)) + { + auto drawIcon = [&](wxImage icon, const wxRect& rectIcon, bool drawActive) + { + if (!drawActive) + icon = icon.ConvertToGreyscale(1.0 / 3, 1.0 / 3, 1.0 / 3); //treat all channels equally! + + if (!enabled) + icon = icon.ConvertToDisabled(); + + drawBitmapRtlNoMirror(dc, icon, rectIcon, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); + }; + + //consume space + rectTmp.x += static_cast(node->level_) * widthLevelStep_; + rectTmp.width -= static_cast(node->level_) * widthLevelStep_; + + rectTmp.x += gapSize_; + rectTmp.width -= gapSize_; + + if (rectTmp.width > 0) + { + //percentage bar + if (showPercentBar_) + { + wxRect areaPerc(rectTmp.x, rectTmp.y + dipToWxsize(2), percentageBarWidth_, rectTmp.height - dipToWxsize(4)); + //clear background + drawFilledRectangle(dc, areaPerc, getColorPercentBackground(), getColorPercentBorder(), dipToWxsize(1)); + areaPerc.Deflate(dipToWxsize(1)); + + //inner area + wxRect areaPercTmp = areaPerc; + areaPercTmp.width = numeric::intDivRound(areaPercTmp.width * node->percent_, 100); + clearArea(dc, areaPercTmp, getColorForLevel(node->level_)); + + wxDCTextColourChanger textColorPercent(dc, *wxBLACK); //accessibility: always set both foreground AND background colors! + drawCellText(dc, areaPerc, numberTo(node->percent_) + L"%", wxALIGN_CENTER); + + rectTmp.x += percentageBarWidth_ + 2 * gapSize_; + rectTmp.width -= percentageBarWidth_ + 2 * gapSize_; + } + if (rectTmp.width > 0) + { + //node status + const bool drawMouseHover = static_cast(rowHover) == HoverAreaTree::node; + switch (node->status_) + { + case TreeView::NodeStatus::expanded: + drawIcon(loadImage(drawMouseHover ? "node_expanded_hover" : "node_expanded"), rectTmp, true /*drawActive*/); + break; + case TreeView::NodeStatus::reduced: + drawIcon(loadImage(drawMouseHover ? "node_reduced_hover" : "node_reduced"), rectTmp, true /*drawActive*/); + break; + case TreeView::NodeStatus::empty: + break; + } + + rectTmp.x += widthNodeStatus_ + gapSize_; + rectTmp.width -= widthNodeStatus_ + gapSize_; + if (rectTmp.width > 0) + { + wxImage nodeIcon; + bool isActive = true; + //icon + if (dynamic_cast(node.get())) + nodeIcon = rootIcon_; + else if (auto dir = dynamic_cast(node.get())) + { + nodeIcon = dirIcon_; + isActive = dir->folder.isActive(); + } + else if (dynamic_cast(node.get())) + nodeIcon = fileIcon_; + + drawIcon(nodeIcon, rectTmp, isActive); + + if (static_cast(rowHover) == HoverAreaTree::item) + drawRectangleBorder(dc, rectTmp, mouseHighlightColor_, dipToWxsize(1)); + + rectTmp.x += widthNodeIcon_ + gapSize_; + rectTmp.width -= widthNodeIcon_ + gapSize_; + + if (rectTmp.width > 0) + { + if (!isActive && + (!enabled || !selected)) + textColor.Set(wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT)); + + drawCellText(dc, rectTmp, getValue(row, colType), wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); + } + } + } + } + } + } + else + { + int alignment = wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL; + + //have file size and item count right-justified (but don't change for RTL languages) + if ((static_cast(colType) == ColumnTypeOverview::bytes || + static_cast(colType) == ColumnTypeOverview::itemCount) && grid_.GetLayoutDirection() != wxLayout_RightToLeft) + { + rectTmp.width -= 2 * gapSize_; + alignment = wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL; + } + else //left-justified + { + rectTmp.x += 2 * gapSize_; + rectTmp.width -= 2 * gapSize_; + } + + drawCellText(dc, rectTmp, getValue(row, colType), alignment); + } + } + + int getBestSize(const wxReadOnlyDC& dc, size_t row, ColumnType colType) override + { + // -> synchronize renderCell() <-> getBestSize() <-> getMouseHover() + + if (static_cast(colType) == ColumnTypeOverview::folder) + { + if (std::unique_ptr node = getDataView().getLine(row)) + return node->level_ * widthLevelStep_ + gapSize_ + (showPercentBar_ ? percentageBarWidth_ + 2 * gapSize_ : 0) + widthNodeStatus_ + gapSize_ + + widthNodeIcon_ + gapSize_ + dc.GetTextExtent(getValue(row, colType)).GetWidth() + + gapSize_; //additional gap from right + else + return 0; + } + else + return 2 * gapSize_ + dc.GetTextExtent(getValue(row, colType)).GetWidth() + + 2 * gapSize_; //include gap from right! + } + + HoverArea getMouseHover(const wxReadOnlyDC& dc, size_t row, ColumnType colType, int cellRelativePosX, int cellWidth) override + { + if (static_cast(colType) == ColumnTypeOverview::folder) + if (std::unique_ptr node = getDataView().getLine(row)) + { + const int nodeStatusXFirst = static_cast(node->level_) * widthLevelStep_ + gapSize_ + (showPercentBar_ ? percentageBarWidth_ + 2 * gapSize_ : 0); + const int nodeStatusXLast = nodeStatusXFirst + widthNodeStatus_; + // -> synchronize renderCell() <-> getBestSize() <-> getMouseHover() + + const int tolerance = dipToWxsize(5); + if (nodeStatusXFirst - tolerance <= cellRelativePosX && cellRelativePosX < nodeStatusXLast + tolerance) + return static_cast(HoverAreaTree::node); + } + return static_cast(HoverAreaTree::item); + } + + std::wstring getColumnLabel(ColumnType colType) const override + { + switch (static_cast(colType)) + { + case ColumnTypeOverview::folder: + return _("Folder"); + case ColumnTypeOverview::itemCount: + return _("Items"); + case ColumnTypeOverview::bytes: + return _("Size"); + } + return std::wstring(); + } + + void onMouseLeft(GridClickEvent& event) + { + switch (static_cast(event.hoverArea_)) + { + case HoverAreaTree::node: + switch (getDataView().getStatus(event.row_)) + { + case TreeView::NodeStatus::expanded: + return reduceNode(event.row_); + case TreeView::NodeStatus::reduced: + return expandNode(event.row_); + case TreeView::NodeStatus::empty: + break; + } + break; + case HoverAreaTree::item: + break; + } + event.Skip(); + } + + void onMouseLeftDouble(GridClickEvent& event) + { + switch (getDataView().getStatus(event.row_)) + { + case TreeView::NodeStatus::expanded: + return reduceNode(event.row_); + case TreeView::NodeStatus::reduced: + return expandNode(event.row_); + case TreeView::NodeStatus::empty: + break; + } + event.Skip(); + } + + void onKeyDown(wxKeyEvent& event) + { + int keyCode = event.GetKeyCode(); + if (grid_.GetLayoutDirection() == wxLayout_RightToLeft) + { + if (keyCode == WXK_LEFT || keyCode == WXK_NUMPAD_LEFT) + keyCode = WXK_RIGHT; + else if (keyCode == WXK_RIGHT || keyCode == WXK_NUMPAD_RIGHT) + keyCode = WXK_LEFT; + } + + const size_t rowCount = grid_.getRowCount(); + if (rowCount == 0) return; + + const size_t row = grid_.getGridCursor(); + if (event.ShiftDown()) + ; + else if (event.ControlDown()) + ; + else + switch (keyCode) + { + case WXK_LEFT: + case WXK_NUMPAD_LEFT: + case WXK_NUMPAD_SUBTRACT: //https://docs.microsoft.com/en-us/previous-versions/windows/desktop/dnacc/guidelines-for-keyboard-user-interface-design#windows-shortcut-keys + switch (getDataView().getStatus(row)) + { + case TreeView::NodeStatus::expanded: + return reduceNode(row); + case TreeView::NodeStatus::reduced: + case TreeView::NodeStatus::empty: + + const int parentRow = getDataView().getParent(row); + if (parentRow >= 0) + grid_.setGridCursor(parentRow, GridEventPolicy::allow); + break; + } + return; //swallow event + + case WXK_RIGHT: + case WXK_NUMPAD_RIGHT: + case WXK_NUMPAD_ADD: + switch (getDataView().getStatus(row)) + { + case TreeView::NodeStatus::expanded: + grid_.setGridCursor(std::min(rowCount - 1, row + 1), GridEventPolicy::allow); + break; + case TreeView::NodeStatus::reduced: + return expandNode(row); + case TreeView::NodeStatus::empty: + break; + } + return; //swallow event + } + + event.Skip(); + } + + void onGridLabelContext(GridLabelClickEvent& event) + { + ContextMenu menu; + //-------------------------------------------------------------------------------------------------------- + menu.addCheckBox(_("Percentage"), [this] { setShowPercentage(!getShowPercentage()); }, getShowPercentage()); + //-------------------------------------------------------------------------------------------------------- + auto toggleColumn = [&](ColumnType ct) + { + auto colAttr = grid_.getColumnConfig(); + + Grid::ColAttributes* caFolderName = nullptr; + Grid::ColAttributes* caToggle = nullptr; + + for (Grid::ColAttributes& ca : colAttr) + if (ca.type == static_cast(ColumnTypeOverview::folder)) + caFolderName = &ca; + else if (ca.type == ct) + caToggle = &ca; + + assert(caFolderName && caFolderName->stretch > 0 && caFolderName->visible); + assert(caToggle && caToggle->stretch == 0); + + if (caFolderName && caToggle) + { + caToggle->visible = !caToggle->visible; + + //take width of newly visible column from stretched folder name column + caFolderName->offset -= caToggle->visible ? caToggle->offset : -caToggle->offset; + + grid_.setColumnConfig(colAttr); + } + }; + + for (const Grid::ColAttributes& ca : grid_.getColumnConfig()) + { + menu.addCheckBox(getColumnLabel(ca.type), [ct = ca.type, toggleColumn] { toggleColumn(ct); }, + ca.visible, ca.type != static_cast(ColumnTypeOverview::folder)); //do not allow user to hide file name column! + } + //-------------------------------------------------------------------------------------------------------- + menu.addSeparator(); + + auto setDefaultColumns = [&] + { + setShowPercentage(overviewPanelShowPercentageDefault); + grid_.setColumnConfig(convertColAttributes(getOverviewDefaultColAttribs(), getOverviewDefaultColAttribs())); + }; + menu.addItem(_("&Default"), setDefaultColumns, loadImage("reset_sicon")); //'&' -> reuse text from "default" buttons elsewhere + //-------------------------------------------------------------------------------------------------------- + + menu.popup(grid_, {event.mousePos_.x, grid_.getColumnLabelHeight()}); + //event.Skip(); + } + + void onGridLabelLeftClick(GridLabelClickEvent& event) + { + const auto colTypeTree = static_cast(event.colType_); + + bool sortAscending = getDefaultSortDirection(colTypeTree); + const auto [sortCol, ascending] = getDataView().getSortConfig(); + if (sortCol == colTypeTree) + sortAscending = !ascending; + + getDataView().setSortDirection(colTypeTree, sortAscending); + grid_.Refresh(); //just in case, but setSortDirection() should not change grid size + grid_.clearSelection(GridEventPolicy::allow); + } + + void expandNode(size_t row) + { + getDataView().expandNode(row); + grid_.Refresh(); //implicitly clears selection (changed row count after expand) + grid_.setGridCursor(row, GridEventPolicy::allow); + //grid_.autoSizeColumns(); -> doesn't look as good as expected + } + + void reduceNode(size_t row) + { + getDataView().reduceNode(row); + grid_.Refresh(); + grid_.setGridCursor(row, GridEventPolicy::allow); + } + + SharedRef treeDataView_ = makeSharedRef(); + + const int gapSize_ = dipToWxsize(TREE_GRID_GAP_SIZE_DIP); + const int percentageBarWidth_ = dipToWxsize(PERCENTAGE_BAR_WIDTH_DIP); + + const wxImage fileIcon_ = IconBuffer::genericFileIcon(IconBuffer::IconSize::small); + const wxImage dirIcon_ = IconBuffer::genericDirIcon (IconBuffer::IconSize::small); + + const int widthNodeIcon_; + const int widthLevelStep_; + const int widthNodeStatus_; + + const wxImage rootIcon_; + const wxColor mouseHighlightColor_ = enhanceContrast(*wxBLUE, //primarily needed for dark mode! + wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW), 5 /*contrastRatioMin*/); //W3C recommends >= 4.5 + + Grid& grid_; + bool showPercentBar_ = true; +}; +} + + +void treegrid::init(Grid& grid) +{ + grid.setDataProvider(std::make_shared(grid)); + grid.showRowLabel(false); + + const int rowHeight = std::max(screenToWxsize(IconBuffer::getPixSize(IconBuffer::IconSize::small)) + dipToWxsize(2), //1 extra pixel on top/bottom; dearly needed on OS X! + grid.getMainWin().GetCharHeight()); //seems to already include 3 margin pixels on top/bottom (consider percentage area) + grid.setRowHeight(rowHeight); +} + + +void treegrid::setData(zen::Grid& grid, FolderComparison& folderCmp) +{ + if (auto* prov = dynamic_cast(grid.getDataProvider())) + return prov->setData(folderCmp); + throw std::runtime_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] treegrid was not initialized."); +} + + +TreeView& treegrid::getDataView(Grid& grid) +{ + if (auto* prov = dynamic_cast(grid.getDataProvider())) + return prov->getDataView(); + throw std::runtime_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] treegrid was not initialized."); +} + + +void treegrid::setShowPercentage(Grid& grid, bool value) +{ + if (auto* prov = dynamic_cast(grid.getDataProvider())) + prov->setShowPercentage(value); + else + assert(false); +} + + +bool treegrid::getShowPercentage(const Grid& grid) +{ + if (auto* prov = dynamic_cast(grid.getDataProvider())) + return prov->getShowPercentage(); + assert(false); + return true; +} diff --git a/FreeFileSync/Source/ui/tree_grid.h b/FreeFileSync/Source/ui/tree_grid.h new file mode 100644 index 0000000..9fb3310 --- /dev/null +++ b/FreeFileSync/Source/ui/tree_grid.h @@ -0,0 +1,179 @@ +// ***************************************************************************** +// * 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 TREE_VIEW_H_841703190201835280256673425 +#define TREE_VIEW_H_841703190201835280256673425 + +#include +#include +#include "tree_grid_attr.h" +#include "../base/file_hierarchy.h" + + +namespace fff +{ +//tree view of FolderComparison +class TreeView +{ +public: + struct SortInfo + { + ColumnTypeOverview sortCol = overviewPanelLastSortColumnDefault; + bool ascending = getDefaultSortDirection(overviewPanelLastSortColumnDefault); + }; + + TreeView() {} + TreeView(FolderComparison& folderCmp, const SortInfo& si); + + //apply view filter: comparison results + void applyDifferenceFilter(bool showExcluded, + bool leftOnlyFilesActive, + bool rightOnlyFilesActive, + bool leftNewerFilesActive, + bool rightNewerFilesActive, + bool differentFilesActive, + bool equalFilesActive, + bool conflictFilesActive); + + //apply view filter: synchronization preview + void applyActionFilter(bool showExcluded, + bool syncCreateLeftActive, + bool syncCreateRightActive, + bool syncDeleteLeftActive, + bool syncDeleteRightActive, + bool syncDirOverwLeftActive, + bool syncDirOverwRightActive, + bool syncDirNoneActive, + bool syncEqualActive, + bool conflictFilesActive); + + enum class NodeStatus + { + expanded, + reduced, + empty + }; + + //--------------------------------------------------------------------- + struct Node + { + Node(int percent, uint64_t bytes, int itemCount, unsigned int level, NodeStatus status) : + percent_(percent), bytes_(bytes), itemCount_(itemCount), level_(level), status_(status) {} + virtual ~Node() {} + + const int percent_; //[0, 100] + const uint64_t bytes_; + const int itemCount_; + const unsigned int level_; + const NodeStatus status_; + }; + + struct FilesNode : public Node + { + FilesNode(int percent, uint64_t bytes, int itemCount, unsigned int level, std::vector&& fsos) : + Node(percent, bytes, itemCount, level, NodeStatus::empty), filesAndLinks(std::move(fsos)) {} + + std::vector filesAndLinks; //files and symlinks matching view filter; pointers are bound! + }; + + struct DirNode : public Node + { + DirNode(int percent, uint64_t bytes, int itemCount, unsigned int level, NodeStatus status, FolderPair& fp) : Node(percent, bytes, itemCount, level, status), folder(fp) {} + FolderPair& folder; + }; + + struct RootNode : public Node + { + RootNode(int percent, uint64_t bytes, int itemCount, NodeStatus status, BaseFolderPair& bFolder, const std::wstring& dispName) : + Node(percent, bytes, itemCount, 0, status), baseFolder(bFolder), displayName(dispName) {} + + BaseFolderPair& baseFolder; + const std::wstring displayName; + }; + + std::unique_ptr getLine(size_t row) const; //return nullptr on error + size_t rowsTotal() const { return flatTree_.size(); } + + void expandNode(size_t row); + void reduceNode(size_t row); + NodeStatus getStatus(size_t row) const; + ptrdiff_t getParent(size_t row) const; //return < 0 if none + + void setSortDirection(ColumnTypeOverview colType, bool ascending); //apply permanently! + SortInfo getSortConfig() { return currentSort_; } + +private: + TreeView (const TreeView&) = delete; + TreeView& operator=(const TreeView&) = delete; + + struct Container + { + uint64_t bytesGross = 0; + uint64_t bytesNet = 0; //bytes for files on view in this directory only + int itemCountGross = 0; + int itemCountNet = 0; //number of files on view in this directory only + + std::vector subDirs; + bool showFilesNode = false; //"compress" algorithm may hide file nodes for directories with a single included file, i.e. itemCountGross == itemCountNet == 1 + std::weak_ptr containerRef; //-> BaseFolderPair if NodeType::root, + //FolderPair if NodeType::folder, and parent ContainerObject if NodeType::files + }; + + struct RootNodeImpl : public Container + { + std::wstring displayName; + }; + + enum class NodeType + { + root, //-> RootNodeImpl + folder, //-> Container + files //-> Container + }; + + struct TreeLine + { + unsigned int level = 0; + int percent = 0; //[0, 100] + const Container* node = nullptr; // + NodeType type = NodeType::root; //increase size of "flatTree" using C-style types rather than have a polymorphic "folderCmpView" + }; + + static void compressNode(Container& cont); + template + static void extractVisibleSubtree(ContainerObject& conObj, Container& cont, Function includeObject); + void getChildren(const Container& cont, unsigned int level, std::vector& output); + template void updateView(Predicate pred); + void applySubView(std::vector&& newView); + + template static void sortSingleLevel(std::vector& items, ColumnTypeOverview columnType); + template struct LessShortName; + + std::vector flatTree_; //collapsable/expandable sub-tree of folderCmpView -> always sorted! + /* /|\ + | (update...) */ + std::vector folderCmpView_; //partial view on folderCmp -> unsorted (cannot be, because files are not a separate entity) + std::function lastViewFilterPred_; //buffer view filter predicate for lazy evaluation of files/symlinks corresponding to a TYPE_FILES node + /* /|\ + | (update...) */ + std::vector> folderCmp_; //full raw data + + SortInfo currentSort_; +}; + + +namespace treegrid +{ +void init(zen::Grid& grid); +TreeView& getDataView(zen::Grid& grid); +void setData(zen::Grid& grid, FolderComparison& folderCmp); + +void setShowPercentage(zen::Grid& grid, bool value); +bool getShowPercentage(const zen::Grid& grid); +} +} + +#endif //TREE_VIEW_H_841703190201835280256673425 diff --git a/FreeFileSync/Source/ui/tree_grid_attr.h b/FreeFileSync/Source/ui/tree_grid_attr.h new file mode 100644 index 0000000..901f77b --- /dev/null +++ b/FreeFileSync/Source/ui/tree_grid_attr.h @@ -0,0 +1,65 @@ +// ***************************************************************************** +// * 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 TREE_GRID_ATTR_H_83470918473021745 +#define TREE_GRID_ATTR_H_83470918473021745 + +#include +#include +#include + + +namespace fff +{ +enum class ColumnTypeOverview +{ + folder, + itemCount, + bytes, +}; + +struct ColumnAttribOverview +{ + ColumnTypeOverview type = ColumnTypeOverview::folder; + int offset = 0; + int stretch = 0; + bool visible = false; +}; + + +inline +std::vector getOverviewDefaultColAttribs() +{ + using namespace zen; + return //harmonize with tree_view.cpp::onGridLabelContext() => expects stretched folder and non-stretched other columns! + { + {ColumnTypeOverview::folder, - 2 * dipToWxsize(70), 1, true}, + {ColumnTypeOverview::itemCount, dipToWxsize(70), 0, true}, + {ColumnTypeOverview::bytes, dipToWxsize(70), 0, true}, + }; +} + +const bool overviewPanelShowPercentageDefault = true; +const ColumnTypeOverview overviewPanelLastSortColumnDefault = ColumnTypeOverview::bytes; + +inline +bool getDefaultSortDirection(ColumnTypeOverview colType) +{ + switch (colType) + { + case ColumnTypeOverview::folder: + return true; + case ColumnTypeOverview::itemCount: + return false; + case ColumnTypeOverview::bytes: + return false; + } + assert(false); + return true; +} +} + +#endif //TREE_GRID_ATTR_H_83470918473021745 diff --git a/FreeFileSync/Source/ui/triple_splitter.cpp b/FreeFileSync/Source/ui/triple_splitter.cpp new file mode 100644 index 0000000..09a627f --- /dev/null +++ b/FreeFileSync/Source/ui/triple_splitter.cpp @@ -0,0 +1,237 @@ +// ***************************************************************************** +// * 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 "triple_splitter.h" +#include +#include + +using namespace zen; +using namespace fff; + + +namespace +{ +//------------ Grid Constants ------------------------------- +const int SASH_HIT_TOLERANCE_DIP = 5; //currently only a placebo! +const int SASH_SIZE_DIP = 10; + +const double SASH_GRAVITY = 0.5; //value within [0, 1]; 1 := resize left only, 0 := resize right only +const int CHILD_WINDOW_MIN_SIZE_DIP = 50; //min. size of managed windows +} + + +class TripleSplitter::SashMove +{ +public: + SashMove(wxWindow& wnd, int mousePosX, int centerOffset) : wnd_(wnd), mousePosX_(mousePosX), centerOffset_(centerOffset) + { + wnd_.SetCursor(wxCURSOR_SIZEWE); + wnd_.CaptureMouse(); + } + ~SashMove() + { + wnd_.SetCursor(*wxSTANDARD_CURSOR); + if (wnd_.HasCapture()) + wnd_.ReleaseMouse(); + } + int getMousePosXStart () const { return mousePosX_; } + int getCenterOffsetStart() const { return centerOffset_; } + +private: + wxWindow& wnd_; + const int mousePosX_; + const int centerOffset_; +}; + + +TripleSplitter::TripleSplitter(wxWindow* parent, + wxWindowID id, + const wxPoint& pos, + const wxSize& size, + long style) : wxWindow(parent, id, pos, size, style | wxTAB_TRAVERSAL), //tab between windows + sashSize_ (dipToWxsize(SASH_SIZE_DIP)), + childWindowMinSize_(dipToWxsize(CHILD_WINDOW_MIN_SIZE_DIP)) +{ + //https://wiki.wxwidgets.org/Flicker-Free_Drawing + SetBackgroundStyle(wxBG_STYLE_PAINT); //get's rid of needless wxEVT_ERASE_BACKGROUND + Bind(wxEVT_PAINT, [this](wxPaintEvent& event) { onPaintEvent(event); }); + Bind(wxEVT_SIZE, [this](wxSizeEvent& event) { updateWindowSizes(); event.Skip(); }); + + Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent& event) { onMouseLeftDown (event); }); + Bind(wxEVT_LEFT_UP, [this](wxMouseEvent& event) { onMouseLeftUp (event); }); + Bind(wxEVT_MOTION, [this](wxMouseEvent& event) { onMouseMovement (event); }); + Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent& event) { onLeaveWindow (event); }); + Bind(wxEVT_LEFT_DCLICK, [this](wxMouseEvent& event) { onMouseLeftDouble(event); }); + Bind(wxEVT_MOUSE_CAPTURE_LOST, [this](wxMouseCaptureLostEvent& event) { onMouseCaptureLost(event); }); +} + + +TripleSplitter::~TripleSplitter() {} //make sure correct destructor gets created for std::unique_ptr + + +void TripleSplitter::updateWindowSizes() +{ + if (windowL_ && windowC_ && windowR_) + { + const int centerPosX = getCenterPosX(); + const int centerWidth = getCenterWidth(); + + const wxRect clientRect = GetClientRect(); + + const int widthL = centerPosX; + const int windowRposX = widthL + centerWidth; + const int widthR = clientRect.width - windowRposX; + + windowL_->SetSize(0, 0, widthL, clientRect.height); + windowC_->SetSize(widthL + sashSize_, 0, windowC_->GetSize().GetWidth(), clientRect.height); + windowR_->SetSize(windowRposX, 0, widthR, clientRect.height); + Refresh(); //repaint sash + } +} + + +inline +int TripleSplitter::getCenterWidth() const +{ + return 2 * sashSize_ + (windowC_ ? windowC_->GetSize().GetWidth() : 0); +} + + +int TripleSplitter::getCenterPosXOptimal() const +{ + const wxRect clientRect = GetClientRect(); + const int centerWidth = getCenterWidth(); + return (clientRect.width - centerWidth) * SASH_GRAVITY; //allowed to be negative for extreme client widths! +} + + +int TripleSplitter::getCenterPosX() const +{ + const wxRect clientRect = GetClientRect(); + const int centerWidth = getCenterWidth(); + const int centerPosXOptimal = getCenterPosXOptimal(); + + //normalize "centerPosXOptimal + centerOffset" + if (clientRect.width < 2 * childWindowMinSize_ + centerWidth) + //use fixed "centeroffset" when "clientRect.width == 2 * childWindowMinSize_ + centerWidth" + return centerPosXOptimal + childWindowMinSize_ - static_cast(2 * childWindowMinSize_ * SASH_GRAVITY); //avoid rounding error + //make sure transition between conditional branches is continuous! + return std::max(childWindowMinSize_, //make sure centerPosXOptimal + offset is within bounds + std::min(centerPosXOptimal + centerOffset_, clientRect.width - childWindowMinSize_ - centerWidth)); +} + + +void TripleSplitter::onPaintEvent(wxPaintEvent& event) +{ + DynBufPaintDC dc(*this, doubleBuffer_); + //GetUpdateRegion()? nah, just redraw everything => we mostly land here due to wxEVT_SIZE anyway (see Refresh() in updateWindowSizes()) + + assert(GetSize() == GetClientSize()); + + const int centerPosX = getCenterPosX(); + const int centerWidth = getCenterWidth(); + + auto draw = [&](wxRect rect) + { + //clear everything in border color: + clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNSHADOW)); + + //clear inner area except for left/right borders + clearArea(dc, wxRect(rect.x + dipToWxsize(1), rect.y, rect.width - 2 * dipToWxsize(1), rect.height), wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE)); + }; + + const wxRect rectSashL(centerPosX, 0, sashSize_, GetClientRect().height); + const wxRect rectSashR(centerPosX + centerWidth - sashSize_, 0, sashSize_, GetClientRect().height); + + draw(rectSashL); + draw(rectSashR); +} + + +bool TripleSplitter::hitOnSashLine(int posX) const +{ + const int centerPosX = getCenterPosX(); + const int centerWidth = getCenterWidth(); + + //we don't get events outside of sash, so SASH_HIT_TOLERANCE_DIP is currently *useless* + auto hitSash = [&](int sashX) { return sashX - dipToWxsize(SASH_HIT_TOLERANCE_DIP) <= posX && posX < sashX + sashSize_ + dipToWxsize(SASH_HIT_TOLERANCE_DIP); }; + + return hitSash(centerPosX) || hitSash(centerPosX + centerWidth - sashSize_); //hit one of the two sash lines +} + + +void TripleSplitter::onMouseLeftDown(wxMouseEvent& event) +{ + activeMove_.reset(); + + const int posX = event.GetPosition().x; + if (hitOnSashLine(posX)) + activeMove_ = std::make_unique(*this, posX, centerOffset_); + event.Skip(); +} + + +void TripleSplitter::onMouseLeftUp(wxMouseEvent& event) +{ + activeMove_.reset(); //nothing else to do, actual work done by onMouseMovement() + event.Skip(); +} + + +void TripleSplitter::onMouseMovement(wxMouseEvent& event) +{ + if (activeMove_) + { + centerOffset_ = activeMove_->getCenterOffsetStart() + event.GetPosition().x - activeMove_->getMousePosXStart(); + + //CAVEAT: function getCenterPosX() normalizes centerPosX *not* centerOffset! + //This can lead to the strange effect of window not immediately resizing when centerOffset is extremely off limits + //=> normalize centerOffset right here + centerOffset_ = getCenterPosX() - getCenterPosXOptimal(); + + updateWindowSizes(); + Update(); //no time to wait until idle event! + } + else + { + //we receive those only while above the sash, not the managed windows (except when the managed windows are disabled!) + const int posX = event.GetPosition().x; + if (hitOnSashLine(posX)) + SetCursor(wxCURSOR_SIZEWE); //set window-local only! + else + SetCursor(*wxSTANDARD_CURSOR); + } + event.Skip(); +} + + +void TripleSplitter::onLeaveWindow(wxMouseEvent& event) +{ + //even called when moving from sash over to managed windows! + if (!activeMove_) + SetCursor(*wxSTANDARD_CURSOR); + event.Skip(); +} + + +void TripleSplitter::onMouseCaptureLost(wxMouseCaptureLostEvent& event) +{ + activeMove_.reset(); + updateWindowSizes(); + //event.Skip(); -> we DID handle it! +} + + +void TripleSplitter::onMouseLeftDouble(wxMouseEvent& event) +{ + const int posX = event.GetPosition().x; + if (hitOnSashLine(posX)) + { + centerOffset_ = 0; //reset sash according to gravity + updateWindowSizes(); + } + event.Skip(); +} diff --git a/FreeFileSync/Source/ui/triple_splitter.h b/FreeFileSync/Source/ui/triple_splitter.h new file mode 100644 index 0000000..b075c63 --- /dev/null +++ b/FreeFileSync/Source/ui/triple_splitter.h @@ -0,0 +1,82 @@ +// ***************************************************************************** +// * 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 TRIPLE_SPLITTER_H_8257804292846842573942534254 +#define TRIPLE_SPLITTER_H_8257804292846842573942534254 + +#include +#include +#include +#include +#include + + +/* manage three contained windows: + 1. left and right window are stretched + 2. middle window is fixed size + 3. middle window position can be changed via mouse with two sash lines + ----------------- + | | | | + | | | | + | | | | + ----------------- */ +namespace fff +{ +class TripleSplitter : public wxWindow +{ +public: + TripleSplitter(wxWindow* parent, + wxWindowID id = wxID_ANY, + const wxPoint& pos = wxDefaultPosition, + const wxSize& size = wxDefaultSize, + long style = 0); + + ~TripleSplitter(); + + void setupWindows(wxWindow* winL, wxWindow* winC, wxWindow* winR) + { + assert(winL->GetParent() == this && winC->GetParent() == this && winR->GetParent() == this && !GetSizer()); + windowL_ = winL; + windowC_ = winC; + windowR_ = winR; + updateWindowSizes(); + } + + int getSashOffset() const { return centerOffset_; } + void setSashOffset(int off) { centerOffset_ = off; updateWindowSizes(); } + +private: + void updateWindowSizes(); + int getCenterWidth() const; + int getCenterPosX() const; //return normalized posX + int getCenterPosXOptimal() const; + + void onPaintEvent(wxPaintEvent& event); + bool hitOnSashLine(int posX) const; + + void onMouseLeftDown(wxMouseEvent& event); + void onMouseLeftUp(wxMouseEvent& event); + void onMouseMovement(wxMouseEvent& event); + void onLeaveWindow(wxMouseEvent& event); + void onMouseCaptureLost(wxMouseCaptureLostEvent& event); + void onMouseLeftDouble(wxMouseEvent& event); + + class SashMove; + std::unique_ptr activeMove_; + + int centerOffset_ = 0; //offset to add after "gravity" stretching + const int sashSize_; + const int childWindowMinSize_; + + wxWindow* windowL_ = nullptr; + wxWindow* windowC_ = nullptr; + wxWindow* windowR_ = nullptr; + + std::optional doubleBuffer_; +}; +} + +#endif //TRIPLE_SPLITTER_H_8257804292846842573942534254 diff --git a/FreeFileSync/Source/ui/version_check.cpp b/FreeFileSync/Source/ui/version_check.cpp new file mode 100644 index 0000000..250ca82 --- /dev/null +++ b/FreeFileSync/Source/ui/version_check.cpp @@ -0,0 +1,358 @@ +// ***************************************************************************** +// * 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 "version_check.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include "../ffs_paths.h" +#include "../version/version.h" +#include "small_dlgs.h" + + #include + #include + #include + #include + + +using namespace zen; +using namespace fff; + + +namespace +{ +const Zchar ffsUpdateCheckUserAgent[] = Zstr("FFS-Update-Check"); + + +time_t getVersionCheckCurrentTime() +{ + time_t now = std::time(nullptr); + return now; +} + + +void openBrowserForDownload(wxWindow* parent) +{ + wxLaunchDefaultBrowser(L"https://freefilesync.org/get_latest.php"); +} +} + + +bool fff::automaticUpdateCheckDue(time_t lastUpdateCheck) +{ + const time_t now = std::time(nullptr); + return numeric::dist(now, lastUpdateCheck) >= 7 * 24 * 3600; //check weekly +} + + +namespace +{ +std::wstring getIso639Language() +{ + assert(runningOnMainThread()); //this function is not thread-safe: consider wxWidgets usage + + std::wstring localeName(copyStringTo(wxUILocale::GetLanguageCanonicalName(wxUILocale::GetSystemLanguage()))); + localeName = beforeFirst(localeName, L'@', IfNotFoundReturn::all); //the locale may contain an @, e.g. "sr_RS@latin"; see wxUILocale::InitLanguagesDB() + + if (!localeName.empty()) + { + const std::wstring langCode = beforeFirst(localeName, L'_', IfNotFoundReturn::all); + assert(langCode.size() == 2 || langCode.size() == 3); //ISO 639: 3-letter possible! + return langCode; + } + assert(false); + return L"zz"; +} + + +std::wstring getIso3166Country() +{ + assert(runningOnMainThread()); //this function is not thread-safe, consider wxWidgets usage + + std::wstring localeName(copyStringTo(wxUILocale::GetLanguageCanonicalName(wxUILocale::GetSystemLanguage()))); + localeName = beforeFirst(localeName, L'@', IfNotFoundReturn::all); //the locale may contain an @, e.g. "sr_RS@latin"; see wxUILocale::InitLanguagesDB() + + if (contains(localeName, L'_')) + { + const std::wstring cc = afterFirst(localeName, L'_', IfNotFoundReturn::none); + assert(cc.size() == 2 || cc.size() == 3); //ISO 3166: 3-letter possible! + return cc; + } + assert(false); + return L"ZZ"; +} + + +//coordinate with get_latest_version_number.php +std::vector> geHttpPostParameters() //throw SysError +{ + assert(runningOnMainThread()); //this function is not thread-safe, e.g. consider wxWidgets usage in getIso639Language() + std::vector> params; + + params.emplace_back("ffs_version", ffsVersion); + + + params.emplace_back("os_name", "Linux"); + + const OsVersion osv = getOsVersion(); + params.emplace_back("os_version", numberTo(osv.major) + "." + numberTo(osv.minor)); + + const char* osArch = cpuArchName; + params.emplace_back("os_arch", osArch); + +#if GTK_MAJOR_VERSION == 2 + //GetContentScaleFactor() requires GTK3 or later +#elif GTK_MAJOR_VERSION == 3 + params.emplace_back("dip_scale", numberTo(wxScreenDC().GetContentScaleFactor())); +#else +#error unknown GTK version! +#endif + + const std::string ffsLang = [] + { + const wxLanguage lang = getLanguage(); + + for (const TranslationInfo& ti : getAvailableTranslations()) + if (ti.languageID == lang) + return ti.locale; + return std::string("zz"); + }(); + params.emplace_back("ffs_lang", ffsLang); + + params.emplace_back("language", utfTo(getIso639Language())); + params.emplace_back("country", utfTo(getIso3166Country())); + + return params; +} + + + + +void showUpdateAvailableDialog(wxWindow* parent, const std::string& onlineVersion) +{ + std::wstring updateDetailsMsg; + try + { + updateDetailsMsg = utfTo(sendHttpGet(utfTo("https://api.freefilesync.org/latest_changes?" + xWwwFormUrlEncode({{"since", ffsVersion}})), + ffsUpdateCheckUserAgent, Zstring() /*caCertFilePath*/).readAll(nullptr /*notifyUnbufferedIO*/)); //throw SysError + } + catch (const SysError& e) { updateDetailsMsg = _("Failed to retrieve update information.") + + L"\n\n" + e.toString(); } + + + switch (showConfirmationDialog(parent, DialogInfoType::info, PopupDialogCfg(). + setIcon(loadImage("FreeFileSync", dipToScreen(48))). + setTitle(_("Check for Software Updates")). + setMainInstructions(replaceCpy(_("FreeFileSync %x is available!"), L"%x", utfTo(onlineVersion)) + L"\n\n" + _("Download now?")). + setDetailInstructions(updateDetailsMsg), _("&Download"))) + { + case ConfirmationButton::accept: //download + openBrowserForDownload(parent); + break; + case ConfirmationButton::cancel: + break; + } +} + + +std::string getOnlineVersion(const std::vector>& postParams) //throw SysError +{ + const std::string response = sendHttpPost(Zstr("https://api.freefilesync.org/latest_version"), postParams, nullptr /*notifyUnbufferedIO*/, + ffsUpdateCheckUserAgent, Zstring() /*caCertFilePath*/).readAll(nullptr /*notifyUnbufferedIO*/); //throw SysError + + if (response.empty() || + !std::all_of(response.begin(), response.end(), [](const char c) { return isDigit(c) || c == FFS_VERSION_SEPARATOR; }) || + startsWith(response, FFS_VERSION_SEPARATOR) || + endsWith(response, FFS_VERSION_SEPARATOR) || + contains(response, std::string() + FFS_VERSION_SEPARATOR + FFS_VERSION_SEPARATOR)) + throw SysError(L"Unexpected server response: \"" + utfTo(response) + L'"'); + //response may be "This website has been moved...", or a Javascript challenge: https://freefilesync.org/forum/viewtopic.php?t=8400 + + return response; +} + + +std::string getUnknownVersionTag() +{ + return '<' + utfTo(_("unknown version")) + '>'; +} +} + + +bool fff::haveNewerVersionOnline(const std::string& onlineVersion) +{ + auto parseVersion = [](const std::string_view& version) + { + std::vector output; + split(version, FFS_VERSION_SEPARATOR, + [&](const std::string_view digit) { output.push_back(stringTo(digit)); }); + assert(!output.empty()); + return output; + }; + const std::vector current = parseVersion(ffsVersion); + const std::vector online = parseVersion(onlineVersion); + + if (online.empty() || online[0] == 0) //online version string may be "Unknown", see automaticUpdateCheckEval() below! + return true; + + return online > current; //std::vector compares lexicographically +} + + +void fff::checkForUpdateNow(wxWindow& parent, std::string& lastOnlineVersion) +{ + try + { + const std::string onlineVersion = getOnlineVersion(geHttpPostParameters()); //throw SysError + lastOnlineVersion = onlineVersion; + + if (haveNewerVersionOnline(onlineVersion)) + showUpdateAvailableDialog(&parent, onlineVersion); + else + { + std::wstring ffsVersionName = L"FreeFileSync " + utfTo(ffsVersion); + showNotificationDialog(&parent, DialogInfoType::info, PopupDialogCfg(). + setIcon(loadImage("update_check")). + setTitle(_("Check for Software Updates")). + setMainInstructions(replaceCpy(_("FreeFileSync is up-to-date."), L"FreeFileSync", ffsVersionName))); + } + } + catch (const SysError& e) + { + if (internetIsAlive()) + { + lastOnlineVersion = getUnknownVersionTag(); + + switch (showConfirmationDialog(&parent, DialogInfoType::error, PopupDialogCfg(). + setTitle(_("Check for Software Updates")). + setMainInstructions(_("Cannot find current FreeFileSync version number online. A newer version is likely available. Check manually now?")). + setDetailInstructions(e.toString()), _("&Check"), _("&Retry"))) + { + case ConfirmationButton2::accept: + openBrowserForDownload(&parent); + break; + case ConfirmationButton2::accept2: //retry + checkForUpdateNow(parent, lastOnlineVersion); //note: retry via recursion!!! + break; + case ConfirmationButton2::cancel: + break; + } + } + else + switch (showConfirmationDialog(&parent, DialogInfoType::error, PopupDialogCfg(). + setTitle(_("Check for Software Updates")). + setMainInstructions(replaceCpy(_("Unable to connect to %x."), L"%x", L"freefilesync.org")). + setDetailInstructions(e.toString()), _("&Retry"))) + { + case ConfirmationButton::accept: //retry + checkForUpdateNow(parent, lastOnlineVersion); //note: retry via recursion!!! + break; + case ConfirmationButton::cancel: + break; + } + } +} + + +struct fff::UpdateCheckResultPrep +{ + std::vector> postParameters; + std::optional error; +}; +SharedRef fff::automaticUpdateCheckPrepare(wxWindow& parent) +{ + assert(runningOnMainThread()); + auto prep = makeSharedRef(); + try + { + prep.ref().postParameters = geHttpPostParameters(); //throw SysError + } + catch (const SysError& e) + { + prep.ref().error = e; + } + return prep; +} + + +struct fff::UpdateCheckResult +{ + std::string onlineVersion; + std::optional error; + bool internetIsAlive = false; +}; +SharedRef fff::automaticUpdateCheckRunAsync(const UpdateCheckResultPrep& resultPrep) +{ + //assert(!runningOnMainThread()); -> allow synchronous call, too + auto result = makeSharedRef(); + try + { + if (resultPrep.error) + throw* resultPrep.error; //throw SysError + + result.ref().onlineVersion = getOnlineVersion(resultPrep.postParameters); //throw SysError + result.ref().internetIsAlive = true; + } + catch (const SysError& e) + { + result.ref().error = e; + result.ref().internetIsAlive = internetIsAlive(); + } + return result; +} + + +void fff::automaticUpdateCheckEval(wxWindow& parent, time_t& lastUpdateCheck, std::string& lastOnlineVersion, const UpdateCheckResult& result) +{ + assert(runningOnMainThread()); + + if (!result.error) + { + lastUpdateCheck = getVersionCheckCurrentTime(); + + if (lastOnlineVersion != result.onlineVersion) //show new version popup only *once* per new release + { + lastOnlineVersion = result.onlineVersion; + + if (haveNewerVersionOnline(result.onlineVersion)) //beta or development version is newer than online + showUpdateAvailableDialog(&parent, result.onlineVersion); + } + } + else + { + if (result.internetIsAlive) + { + if (lastOnlineVersion != getUnknownVersionTag()) + switch (showConfirmationDialog(&parent, DialogInfoType::error, PopupDialogCfg(). + setTitle(_("Check for Software Updates")). + setMainInstructions(_("Cannot find current FreeFileSync version number online. A newer version is likely available. Check manually now?")). + setDetailInstructions(result.error->toString()), + _("&Check"), _("&Retry"))) + { + case ConfirmationButton2::accept: + lastOnlineVersion = getUnknownVersionTag(); + openBrowserForDownload(&parent); + break; + case ConfirmationButton2::accept2: //retry + automaticUpdateCheckEval(parent, lastUpdateCheck, lastOnlineVersion, + automaticUpdateCheckRunAsync(automaticUpdateCheckPrepare(parent).ref()).ref()); //retry via recursion!!! + break; + case ConfirmationButton2::cancel: + lastOnlineVersion = getUnknownVersionTag(); + break; + } + } + else //no internet connection + { + if (lastOnlineVersion.empty()) + lastOnlineVersion = ffsVersion; + } + } +} diff --git a/FreeFileSync/Source/ui/version_check.h b/FreeFileSync/Source/ui/version_check.h new file mode 100644 index 0000000..e7b75f1 --- /dev/null +++ b/FreeFileSync/Source/ui/version_check.h @@ -0,0 +1,35 @@ +// ***************************************************************************** +// * 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 VERSION_CHECK_H_324872374893274983275 +#define VERSION_CHECK_H_324872374893274983275 + +#include +#include + + +namespace fff +{ +bool haveNewerVersionOnline(const std::string& onlineVersion); +//---------------------------------------------------------------------------- +bool automaticUpdateCheckDue(time_t lastUpdateCheck); + +struct UpdateCheckResultPrep; +struct UpdateCheckResult; + +//run on main thread: +zen::SharedRef automaticUpdateCheckPrepare(wxWindow& parent); +//run on worker thread: (long-running part of the check) +zen::SharedRef automaticUpdateCheckRunAsync(const UpdateCheckResultPrep& resultPrep); +//run on main thread: +void automaticUpdateCheckEval(wxWindow& parent, time_t& lastUpdateCheck, std::string& lastOnlineVersion, const UpdateCheckResult& result); +//---------------------------------------------------------------------------- +//call from main thread: +void checkForUpdateNow(wxWindow& parent, std::string& lastOnlineVersion); +//---------------------------------------------------------------------------- +} + +#endif //VERSION_CHECK_H_324872374893274983275 diff --git a/FreeFileSync/Source/version/version.h b/FreeFileSync/Source/version/version.h new file mode 100644 index 0000000..4c18247 --- /dev/null +++ b/FreeFileSync/Source/version/version.h @@ -0,0 +1,10 @@ +#ifndef VERSION_HEADER_434343489702544325 +#define VERSION_HEADER_434343489702544325 + +namespace fff +{ +const char ffsVersion[] = "14.6"; //internal linkage! +const char FFS_VERSION_SEPARATOR = '.'; +} + +#endif diff --git a/License.txt b/License.txt new file mode 100644 index 0000000..2e3e12e --- /dev/null +++ b/License.txt @@ -0,0 +1,980 @@ + FreeFileSync: Terms of use + +The FreeFileSync standard and Donation Edition +are for **private use** only: +https://freefilesync.org/faq.php#donation-edition + +**Commercial use**, government use, and other non-private uses require +the purchase of the FreeFileSync Business Edition: +https://freefilesync.org/faq.php#business + +================================================================== + +A. GNU General Public License +B. wxWidgets License +C. OpenSSL License +D. curl License +E. libssh2 License +F. PuTTY License + +================================================================== +A. + GNU General Public License + Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + + Preamble + +The GNU General Public License is a free, copyleft license for +software and other kinds of works. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + +For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + +Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + +Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + +The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + +0. Definitions. + +"This License" refers to version 3 of the GNU General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based +on the Program. + +To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +1. Source Code. + +The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + +A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + +The Corresponding Source for a work in source code form is that +same work. + +2. Basic Permissions. + +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + +4. Conveying Verbatim Copies. + +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + +"Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +7. Additional Terms. + +"Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + +All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + +8. Termination. + +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +11. Patents. + +A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + +13. Use with the GNU Affero General Public License. + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + +14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + +================================================================== +B. + wxWidgets License + +wxWindows Library Licence, Version 3.1 + +Copyright (c) 1998-2005 Julian Smart, Robert Roebling et al + +Everyone is permitted to copy and distribute verbatim copies +of this licence document, but changing it is not allowed. + + WXWINDOWS LIBRARY LICENCE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +This library is free software; you can redistribute it and/or modify it +under the terms of the GNU Library General Public Licence as published by +the Free Software Foundation; either version 2 of the Licence, or (at your +option) any later version. + +This library is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public +Licence for more details. + +You should have received a copy of the GNU Library General Public Licence +along with this software, usually in a file named COPYING.LIB. If not, +write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth +Floor, Boston, MA 02110-1301 USA. + +EXCEPTION NOTICE + +1. As a special exception, the copyright holders of this library give + permission for additional uses of the text contained in this release of the + library as licenced under the wxWindows Library Licence, applying either + version 3.1 of the Licence, or (at your option) any later version of the + Licence as published by the copyright holders of version 3.1 of the Licence + document. + +2. The exception is that you may use, copy, link, modify and distribute + under your own terms, binary object code versions of works based on the + Library. + +3. If you copy code from files distributed under the terms of the GNU + General Public Licence or the GNU Library General Public Licence into a + copy of this library, as this licence permits, the exception does not apply + to the code that you add in this way. To avoid misleading anyone as to the + status of such modified files, you must delete this exception notice from + such code and/or adjust the licensing conditions notice accordingly. + +4. If you write modifications of your own for this library, it is your + choice whether to permit this exception to apply to your modifications. If + you do not wish that, you must delete the exception notice from such code + and/or adjust the licensing conditions notice accordingly. + +================================================================== +C. + OpenSSL License + + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +================================================================== +D. + curl License + +COPYRIGHT AND PERMISSION NOTICE + +Copyright (c) 1996 - 2021, Daniel Stenberg, daniel@haxx.se, and many +contributors, see the THANKS file. + +All rights reserved. + +Permission to use, copy, modify, and distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright +notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. IN +NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE +OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall not +be used in advertising or otherwise to promote the sale, use or other dealings +in this Software without prior written authorization of the copyright holder. + +================================================================== +E. + libssh2 License + +Copyright (c) 2004-2007 Sara Golemon +Copyright (c) 2005,2006 Mikhail Gusarov +Copyright (c) 2006-2007 The Written Word, Inc. +Copyright (c) 2007 Eli Fant +Copyright (c) 2009-2021 Daniel Stenberg +Copyright (C) 2008, 2009 Simon Josefsson +Copyright (c) 2000 Markus Friedl +Copyright (c) 2015 Microsoft Corp. +All rights reserved. + +Redistribution and use in source and binary forms, +with or without modification, are permitted provided +that the following conditions are met: + + Redistributions of source code must retain the above + copyright notice, this list of conditions and the + following disclaimer. + + Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials + provided with the distribution. + + Neither the name of the copyright holder nor the names + of any other contributors may be used to endorse or + promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY +OF SUCH DAMAGE. + +================================================================== +F. + PuTTY License + +PuTTY is copyright 1997-2022 Simon Tatham. + +Portions copyright Robert de Bath, Joris van Rantwijk, Delian +Delchev, Andreas Schultz, Jeroen Massar, Wez Furlong, Nicolas Barry, +Justin Bradford, Ben Harris, Malcolm Smith, Ahmad Khalifa, Markus +Kuhn, Colin Watson, Christopher Staite, Lorenz Diener, Christian +Brabandt, Jeff Smith, Pavel Kryukov, Maxim Kuznetsov, Svyatoslav +Kuzmich, Nico Williams, Viktor Dukhovni, Josh Dersch, Lars Brinkhoff, +and CORE SDI S.A. + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation files +(the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE +FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/libcurl/curl_wrap.cpp b/libcurl/curl_wrap.cpp new file mode 100644 index 0000000..b2a7560 --- /dev/null +++ b/libcurl/curl_wrap.cpp @@ -0,0 +1,411 @@ +// ***************************************************************************** +// * 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 "curl_wrap.h" +#include +#include +#include + #include + +using namespace zen; + + +namespace +{ +int curlInitLevel = 0; //support interleaving initialization calls! +//zero-initialized POD => not subject to static initialization order fiasco +} + +void zen::libcurlInit() +{ + assert(runningOnMainThread()); //all OpenSSL/libssh2/libcurl require init on main thread! + assert(curlInitLevel >= 0); + if (++curlInitLevel != 1) //non-atomic => require call from main thread + return; + + openSslInit(); + + try + { + ASSERT_SYSERROR(::curl_global_init(CURL_GLOBAL_NOTHING /*CURL_GLOBAL_DEFAULT = CURL_GLOBAL_SSL|CURL_GLOBAL_WIN32*/) == CURLE_OK); + } + catch (const SysError& e) { logExtraError(_("Error during process initialization.") + L"\n\n" + e.toString()); } +} + + +void zen::libcurlTearDown() +{ + assert(runningOnMainThread()); //+ avoid race condition on "curlInitLevel" + assert(curlInitLevel >= 1); + if (--curlInitLevel != 0) + return; + + ::curl_global_cleanup(); + openSslTearDown(); +} + + +HttpSession::HttpSession(const Zstring& server, bool useTls, const Zstring& caCertFilePath) : //throw SysError + serverPrefix_((useTls ? "https://" : "http://") + utfTo(server)), + caCertFilePath_(utfTo(caCertFilePath)) {} + + +HttpSession::~HttpSession() +{ + if (easyHandle_) + ::curl_easy_cleanup(easyHandle_); +} + + +HttpSession::Result HttpSession::perform(const std::string& serverRelPath, + const std::vector& extraHeaders, const std::vector& extraOptions, + const std::function buf)>& writeResponse /*throw X*/, //optional + const std::function buf)>& readRequest /*throw X*/, //optional; return "bytesToRead" bytes unless end of stream! + const std::function& receiveHeader /*throw X*/, + int timeoutSec) //throw SysError, X +{ + if (!easyHandle_) + { + easyHandle_ = ::curl_easy_init(); + if (!easyHandle_) + throw SysError(formatSystemError("curl_easy_init", formatCurlStatusCode(CURLE_OUT_OF_MEMORY), L"")); + } + else + ::curl_easy_reset(easyHandle_); + + auto setCurlOption = [easyHandle = easyHandle_](const CurlOption& curlOpt) //throw SysError + { + if (const CURLcode rc = ::curl_easy_setopt(easyHandle, curlOpt.option, curlOpt.value); + rc != CURLE_OK) + throw SysError(formatSystemError("curl_easy_setopt(" + numberTo(static_cast(curlOpt.option)) + ")", + formatCurlStatusCode(rc), utfTo(::curl_easy_strerror(rc)))); + }; + + char curlErrorBuf[CURL_ERROR_SIZE] = {}; + setCurlOption({CURLOPT_ERRORBUFFER, curlErrorBuf}); //throw SysError + + setCurlOption({CURLOPT_USERAGENT, "FreeFileSync"}); //throw SysError + //default value; may be overwritten by caller + + setCurlOption({CURLOPT_URL, (serverPrefix_ + serverRelPath).c_str()}); //throw SysError + + setCurlOption({CURLOPT_ACCEPT_ENCODING, ""}); //throw SysError + //libcurl: generate Accept-Encoding header containing all built-in supported encodings + //=> usually generates "Accept-Encoding: deflate, gzip" - note: "gzip" used by Google Drive + + setCurlOption({CURLOPT_NOSIGNAL, 1}); //throw SysError + //thread-safety: https://curl.haxx.se/libcurl/c/threadsafe.html + + setCurlOption({CURLOPT_CONNECTTIMEOUT, timeoutSec}); //throw SysError + + //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." + setCurlOption({CURLOPT_LOW_SPEED_TIME, timeoutSec}); //throw SysError + setCurlOption({CURLOPT_LOW_SPEED_LIMIT, 1 /*[bytes]*/}); //throw SysError + //can't use "0" which means "inactive", so use some low number + + //CURLOPT_SERVER_RESPONSE_TIMEOUT: does not apply to HTTP + + + std::exception_ptr userCallbackException; + + //libcurl does *not* set FD_CLOEXEC for us! https://github.com/curl/curl/issues/2252 + auto onSocketCreate = [&](curl_socket_t curlfd, curlsocktype purpose) + { + assert(::fcntl(curlfd, F_GETFD) == 0); + if (::fcntl(curlfd, F_SETFD, FD_CLOEXEC) == -1) //=> RACE-condition if other thread calls fork/execv before this thread sets FD_CLOEXEC! + { + userCallbackException = std::make_exception_ptr(SysError(formatSystemError("fcntl(FD_CLOEXEC)", errno))); + return CURL_SOCKOPT_ERROR; + } + return CURL_SOCKOPT_OK; + }; + + using SocketCbType = decltype(onSocketCreate); + using SocketCbWrapperType = int (*)(SocketCbType* clientp, curl_socket_t curlfd, curlsocktype purpose); //needed for cdecl function pointer cast + SocketCbWrapperType onSocketCreateWrapper = [](SocketCbType* clientp, curl_socket_t curlfd, curlsocktype purpose) + { + return (*clientp)(curlfd, purpose); //free this poor little C-API from its shackles and redirect to a proper lambda + }; + + setCurlOption({CURLOPT_SOCKOPTFUNCTION, onSocketCreateWrapper}); //throw SysError + setCurlOption({CURLOPT_SOCKOPTDATA, &onSocketCreate}); //throw SysError + + //libcurl forwards this char-string to OpenSSL as is, which - thank god - accepts UTF8 + if (caCertFilePath_.empty()) + { + setCurlOption({CURLOPT_CAINFO, 0}); //throw SysError + setCurlOption({CURLOPT_SSL_VERIFYPEER, 0}); //throw SysError + setCurlOption({CURLOPT_SSL_VERIFYHOST, 0}); //throw SysError + //see remarks in ftp.cpp + } + else + setCurlOption({CURLOPT_CAINFO, caCertFilePath_.c_str()}); //throw SysError + //hopefully latest version from https://curl.haxx.se/docs/caextract.html + //CURLOPT_SSL_VERIFYPEER => already active by default + //CURLOPT_SSL_VERIFYHOST => + + //--------------------------------------------------- + auto onHeaderReceived = [&](const char* buffer, size_t len) + { + try + { + receiveHeader({buffer, len}); //throw X + return len; + } + catch (...) + { + userCallbackException = std::current_exception(); + return len + 1; //signal error condition => CURLE_WRITE_ERROR + } + }; + curl_write_callback onHeaderReceivedWrapper = [](/*const*/ char* buffer, size_t size, size_t nitems, void* callbackData) + { + return (*static_cast(callbackData))(buffer, size * nitems); //free this poor little C-API from its shackles and redirect to a proper lambda + }; + //--------------------------------------------------- + auto onBytesReceived = [&](const char* buffer, size_t bytesToWrite) + { + try + { + writeResponse({buffer, bytesToWrite}); //throw X + //[!] let's NOT use "incomplete write Posix semantics" for libcurl! + //who knows if libcurl buffers properly, or if it sends incomplete packages!? + return bytesToWrite; + } + catch (...) + { + userCallbackException = std::current_exception(); + return bytesToWrite + 1; //signal error condition => CURLE_WRITE_ERROR + } + }; + curl_write_callback onBytesReceivedWrapper = [](char* buffer, size_t size, size_t nitems, void* callbackData) + { + return (*static_cast(callbackData))(buffer, size * nitems); //free this poor little C-API from its shackles and redirect to a proper lambda + }; + //--------------------------------------------------- + auto getBytesToSend = [&](char* buffer, size_t bytesToRead) -> 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 + + [!] let's NOT use "incomplete read Posix semantics" for libcurl! + who knows if libcurl buffers properly, or if it requests incomplete packages!? */ + const size_t bytesRead = readRequest({buffer, bytesToRead}); //throw X; return "bytesToRead" bytes unless end of stream + assert(bytesRead == bytesToRead || bytesRead == 0 || readRequest({buffer, bytesToRead}) == 0); + return bytesRead; + } + catch (...) + { + userCallbackException = std::current_exception(); + return CURL_READFUNC_ABORT; //signal error condition => CURLE_ABORTED_BY_CALLBACK + } + }; + curl_read_callback getBytesToSendWrapper = [](char* buffer, size_t size, size_t nitems, void* callbackData) + { + return (*static_cast(callbackData))(buffer, size * nitems); //free this poor little C-API from its shackles and redirect to a proper lambda + }; + //--------------------------------------------------- + if (receiveHeader) + { + setCurlOption({CURLOPT_HEADERDATA, &onHeaderReceived}); //throw SysError + setCurlOption({CURLOPT_HEADERFUNCTION, onHeaderReceivedWrapper}); //throw SysError + } + if (writeResponse) + { + setCurlOption({CURLOPT_WRITEDATA, &onBytesReceived}); //throw SysError + setCurlOption({CURLOPT_WRITEFUNCTION, onBytesReceivedWrapper}); //throw SysError + //{CURLOPT_BUFFERSIZE, 256 * 1024} -> defaults is 16 kB which seems to correspond to SSL packet size + //=> setting larget buffers size does nothing (recv still returns only 16 kB) + } + if (readRequest) + { + if (std::all_of(extraOptions.begin(), extraOptions.end(), [](const CurlOption& o) { return o.option != CURLOPT_POST; })) + /**/setCurlOption({CURLOPT_UPLOAD, 1}); //throw SysError + //issues HTTP PUT + + setCurlOption({CURLOPT_READDATA, &getBytesToSend}); //throw SysError + setCurlOption({CURLOPT_READFUNCTION, getBytesToSendWrapper}); //throw SysError + //{CURLOPT_UPLOAD_BUFFERSIZE, 256 * 1024} -> default is 64 kB. apparently no performance improvement for larger buffers like 256 kB + + //Contradicting options: CURLOPT_READFUNCTION, CURLOPT_POSTFIELDS: + if (std::any_of(extraOptions.begin(), extraOptions.end(), [](const CurlOption& o) { return o.option == CURLOPT_POSTFIELDS; })) + /**/ throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); + } + + if (std::any_of(extraOptions.begin(), extraOptions.end(), [](const CurlOption& o) { return o.option == CURLOPT_WRITEFUNCTION || o.option == CURLOPT_READFUNCTION; })) + /**/ throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); //Option already used here! + + //--------------------------------------------------- + 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() + //CURLOPT_EXPECT_100_TIMEOUT_MS: should not be needed + + //CURLOPT_TCP_NODELAY => already set by default https://brooker.co.za/blog/2024/05/09/nagle.html + + if (headers) + setCurlOption({CURLOPT_HTTPHEADER, headers}); //throw SysError + //--------------------------------------------------- + + for (const CurlOption& option : extraOptions) + setCurlOption(option); //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 + //=> BUT Google also screws up in their REST API design and returns HTTP 4XX status for domain-level errors! https://blog.slimjim.xyz/posts/stop-using-http-codes/ + //=> let caller handle HTTP status to work around this mess! + + if (userCallbackException) + std::rethrow_exception(userCallbackException); //throw X + //======================================================================================================= + + long httpStatus = 0; //optional + /*const CURLcode rc = */ ::curl_easy_getinfo(easyHandle_, CURLINFO_RESPONSE_CODE, &httpStatus); + + if (rcPerf != CURLE_OK) + { + std::wstring errorMsg = trimCpy(utfTo(curlErrorBuf)); //optional + + if (httpStatus != 0) //optional + errorMsg += (errorMsg.empty() ? L"" : L"\n") + formatHttpError(httpStatus); +#if 0 + //utfTo(::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(nativeErrorCode); +#endif + throw SysError(formatSystemError("curl_easy_perform", formatCurlStatusCode(rcPerf), errorMsg)); + } + + lastSuccessfulUseTime_ = std::chrono::steady_clock::now(); + return {static_cast(httpStatus) /*, contentType ? contentType : ""*/}; +} + + +std::wstring zen::formatCurlStatusCode(CURLcode sc) +{ + switch (sc) + { + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_OK); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_UNSUPPORTED_PROTOCOL); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_FAILED_INIT); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_URL_MALFORMAT); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_NOT_BUILT_IN); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_COULDNT_RESOLVE_PROXY); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_COULDNT_RESOLVE_HOST); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_COULDNT_CONNECT); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_WEIRD_SERVER_REPLY); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_REMOTE_ACCESS_DENIED); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_FTP_ACCEPT_FAILED); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_FTP_WEIRD_PASS_REPLY); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_FTP_ACCEPT_TIMEOUT); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_FTP_WEIRD_PASV_REPLY); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_FTP_WEIRD_227_FORMAT); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_FTP_CANT_GET_HOST); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_HTTP2); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_FTP_COULDNT_SET_TYPE); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_PARTIAL_FILE); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_FTP_COULDNT_RETR_FILE); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_OBSOLETE20); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_QUOTE_ERROR); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_HTTP_RETURNED_ERROR); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_WRITE_ERROR); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_OBSOLETE24); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_UPLOAD_FAILED); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_READ_ERROR); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_OUT_OF_MEMORY); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_OPERATION_TIMEDOUT); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_OBSOLETE29); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_FTP_PORT_FAILED); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_FTP_COULDNT_USE_REST); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_OBSOLETE32); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_RANGE_ERROR); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_OBSOLETE34); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_SSL_CONNECT_ERROR); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_BAD_DOWNLOAD_RESUME); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_FILE_COULDNT_READ_FILE); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_LDAP_CANNOT_BIND); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_LDAP_SEARCH_FAILED); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_OBSOLETE40); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_OBSOLETE41); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_ABORTED_BY_CALLBACK); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_BAD_FUNCTION_ARGUMENT); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_OBSOLETE44); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_INTERFACE_FAILED); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_OBSOLETE46); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_TOO_MANY_REDIRECTS); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_UNKNOWN_OPTION); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_SETOPT_OPTION_SYNTAX); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_OBSOLETE50); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_OBSOLETE51); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_GOT_NOTHING); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_SSL_ENGINE_NOTFOUND); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_SSL_ENGINE_SETFAILED); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_SEND_ERROR); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_RECV_ERROR); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_OBSOLETE57); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_SSL_CERTPROBLEM); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_SSL_CIPHER); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_PEER_FAILED_VERIFICATION); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_BAD_CONTENT_ENCODING); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_OBSOLETE62); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_FILESIZE_EXCEEDED); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_USE_SSL_FAILED); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_SEND_FAIL_REWIND); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_SSL_ENGINE_INITFAILED); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_LOGIN_DENIED); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_TFTP_NOTFOUND); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_TFTP_PERM); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_REMOTE_DISK_FULL); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_TFTP_ILLEGAL); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_TFTP_UNKNOWNID); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_REMOTE_FILE_EXISTS); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_TFTP_NOSUCHUSER); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_OBSOLETE75); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_OBSOLETE76); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_SSL_CACERT_BADFILE); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_REMOTE_FILE_NOT_FOUND); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_SSH); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_SSL_SHUTDOWN_FAILED); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_AGAIN); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_SSL_CRL_BADFILE); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_SSL_ISSUER_ERROR); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_FTP_PRET_FAILED); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_RTSP_CSEQ_ERROR); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_RTSP_SESSION_ERROR); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_FTP_BAD_FILE_LIST); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_CHUNK_FAILED); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_NO_CONNECTION_AVAILABLE); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_SSL_PINNEDPUBKEYNOTMATCH); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_SSL_INVALIDCERTSTATUS); + 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(CURLE_QUIC_CONNECT_ERROR); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_PROXY); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_SSL_CLIENTCERT); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_UNRECOVERABLE_POLL); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_TOO_LARGE); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_ECH_REQUIRED); + ZEN_CHECK_CASE_FOR_CONSTANT(CURL_LAST); + } + static_assert(CURL_LAST == CURLE_ECH_REQUIRED + 1); + + return replaceCpy(L"Curl status %x", L"%x", numberTo(static_cast(sc))); +} diff --git a/libcurl/curl_wrap.h b/libcurl/curl_wrap.h new file mode 100644 index 0000000..dc53442 --- /dev/null +++ b/libcurl/curl_wrap.h @@ -0,0 +1,79 @@ +// ***************************************************************************** +// * 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 CURL_WRAP_H_2879058325032785032789645 +#define CURL_WRAP_H_2879058325032785032789645 + +#include +#include +#include +#include + + +//------------------------------------------------- +#include +//------------------------------------------------- + +#ifndef CURLINC_CURL_H + #error curl.h header guard changed +#endif + +namespace zen +{ +void libcurlInit(); +void libcurlTearDown(); + + +struct CurlOption +{ + template + CurlOption(CURLoption o, T val) : option(o), value(static_cast(val)) { static_assert(sizeof(val) <= sizeof(value)); } + + template + CurlOption(CURLoption o, T* val) : option(o), value(reinterpret_cast(val)) { static_assert(sizeof(val) <= sizeof(value)); } + + CURLoption option = CURLOPT_LASTENTRY; + uint64_t value = 0; +}; + + +class HttpSession +{ +public: + HttpSession(const Zstring& server, bool useTls, const Zstring& caCertFilePath /*optional*/); //throw SysError + ~HttpSession(); + + struct Result + { + int statusCode = 0; + //std::string contentType; + }; + Result perform(const std::string& serverRelPath, + const std::vector& extraHeaders, const std::vector& extraOptions, + const std::function buf)>& writeResponse /*throw X*/, //optional + const std::function buf)>& readRequest /*throw X*/, //optional; return "bytesToRead" bytes unless end of stream! + const std::function& receiveHeader /*throw X*/, + int timeoutSec); //throw SysError, X + + std::chrono::steady_clock::time_point getLastUseTime() const { return lastSuccessfulUseTime_; } + +private: + HttpSession (const HttpSession&) = delete; + HttpSession& operator=(const HttpSession&) = delete; + + const std::string serverPrefix_; + const std::string caCertFilePath_; //optional + CURL* easyHandle_ = nullptr; + std::chrono::steady_clock::time_point lastSuccessfulUseTime_ = std::chrono::steady_clock::now(); +}; + + +std::wstring formatCurlStatusCode(CURLcode sc); +} + +#else +#error Why is this header already defined? Do not include in other headers: encapsulate the gory details! +#endif //CURL_WRAP_H_2879058325032785032789645 diff --git a/libssh2/libssh2_wrap.h b/libssh2/libssh2_wrap.h new file mode 100644 index 0000000..ab1af0c --- /dev/null +++ b/libssh2/libssh2_wrap.h @@ -0,0 +1,247 @@ +// ***************************************************************************** +// * 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 LIBSSH2_WRAP_H_087280957180967346572465 +#define LIBSSH2_WRAP_H_087280957180967346572465 + +#include +#include + + + +//------------------------------------------------- +#include +//------------------------------------------------- + +#ifndef LIBSSH2_SFTP_H + #error libssh2_sftp.h header guard changed +#endif + +//fix libssh2 64-bit warning mess: https://github.com/libssh2/libssh2/pull/96 +#undef libssh2_userauth_password +inline int libssh2_userauth_password(LIBSSH2_SESSION* session, const std::string& username, const std::string& password) +{ + return libssh2_userauth_password_ex(session, + username.c_str(), static_cast(username.size()), + password.c_str(), static_cast(password.size()), nullptr); +} + +#undef libssh2_userauth_keyboard_interactive +inline int libssh2_userauth_keyboard_interactive(LIBSSH2_SESSION* session, const std::string& username, LIBSSH2_USERAUTH_KBDINT_RESPONSE_FUNC((*response_callback))) +{ + return libssh2_userauth_keyboard_interactive_ex(session, username.c_str(), static_cast(username.size()), response_callback); +} + +inline char* libssh2_userauth_list(LIBSSH2_SESSION* session, const std::string& username) +{ + return libssh2_userauth_list(session, username.c_str(), static_cast(username.size())); +} + + +inline int libssh2_userauth_publickey_frommemory(LIBSSH2_SESSION* session, const std::string& username, const std::string& privateKeyStream, const std::string& passphrase) +{ + return libssh2_userauth_publickey_frommemory(session, username.c_str(), username.size(), nullptr, 0, + privateKeyStream.c_str(), privateKeyStream.size(), passphrase.c_str()); +} + +#undef libssh2_sftp_opendir +inline LIBSSH2_SFTP_HANDLE* libssh2_sftp_opendir(LIBSSH2_SFTP* sftp, const std::string& path) +{ + return libssh2_sftp_open_ex(sftp, path.c_str(), static_cast(path.size()), 0, 0, LIBSSH2_SFTP_OPENDIR); +} + +#undef libssh2_sftp_stat +inline int libssh2_sftp_stat(LIBSSH2_SFTP* sftp, const std::string& path, LIBSSH2_SFTP_ATTRIBUTES* attrs) +{ + return libssh2_sftp_stat_ex(sftp, path.c_str(), static_cast(path.size()), LIBSSH2_SFTP_STAT, attrs); +} + +#undef libssh2_sftp_open +inline LIBSSH2_SFTP_HANDLE* libssh2_sftp_open(LIBSSH2_SFTP* sftp, const std::string& path, unsigned long flags, long mode) +{ + return libssh2_sftp_open_ex(sftp, path.c_str(), static_cast(path.size()), flags, mode, LIBSSH2_SFTP_OPENFILE); +} + +#undef libssh2_sftp_setstat +inline int libssh2_sftp_setstat(LIBSSH2_SFTP* sftp, const std::string& path, LIBSSH2_SFTP_ATTRIBUTES* attrs) +{ + return libssh2_sftp_stat_ex(sftp, path.c_str(), static_cast(path.size()), LIBSSH2_SFTP_SETSTAT, attrs); +} + +#undef libssh2_sftp_lstat +inline int libssh2_sftp_lstat(LIBSSH2_SFTP* sftp, const std::string& path, LIBSSH2_SFTP_ATTRIBUTES* attrs) +{ + return libssh2_sftp_stat_ex(sftp, path.c_str(), static_cast(path.size()), LIBSSH2_SFTP_LSTAT, attrs); +} + +#undef libssh2_sftp_mkdir +inline int libssh2_sftp_mkdir(LIBSSH2_SFTP* sftp, const std::string& path, long mode) +{ + return libssh2_sftp_mkdir_ex(sftp, path.c_str(), static_cast(path.size()), mode); +} + +#undef libssh2_sftp_unlink +inline int libssh2_sftp_unlink(LIBSSH2_SFTP* sftp, const std::string& path) +{ + return libssh2_sftp_unlink_ex(sftp, path.c_str(), static_cast(path.size())); +} + +#undef libssh2_sftp_rmdir +inline int libssh2_sftp_rmdir(LIBSSH2_SFTP* sftp, const std::string& path) +{ + return libssh2_sftp_rmdir_ex(sftp, path.c_str(), static_cast(path.size())); +} + +#undef libssh2_sftp_realpath +inline int libssh2_sftp_realpath(LIBSSH2_SFTP* sftp, const std::string& path, char* buf, size_t bufSize) +{ + return libssh2_sftp_symlink_ex(sftp, path.c_str(), static_cast(path.size()), buf, static_cast(bufSize), LIBSSH2_SFTP_REALPATH); +} + +#undef libssh2_sftp_readlink +inline int libssh2_sftp_readlink(LIBSSH2_SFTP* sftp, const std::string& path, char* buf, size_t bufSize) +{ + return libssh2_sftp_symlink_ex(sftp, path.c_str(), static_cast(path.size()), buf, static_cast(bufSize), LIBSSH2_SFTP_READLINK); +} + +#undef libssh2_sftp_symlink +inline int libssh2_sftp_symlink(LIBSSH2_SFTP* sftp, const std::string& path, const std::string_view buf) +{ + return libssh2_sftp_symlink_ex(sftp, + /* CAVEAT: https://www.sftp.net/spec/openssh-sftp-extensions.txt + "When OpenSSH's sftp-server was implemented, the order of the arguments + to the SSH_FXP_SYMLINK method was inadvertently reversed." + + => of course libssh2 didn't get the memo: fix this shit: */ + /**/ buf .data (), static_cast(buf .size()), + const_cast(path.c_str()), static_cast(path.size()), + LIBSSH2_SFTP_SYMLINK); +} + +#undef libssh2_sftp_rename +inline int libssh2_sftp_rename(LIBSSH2_SFTP* sftp, const std::string& pathFrom, const std::string& pathTo, long flags) +{ + return libssh2_sftp_rename_ex(sftp, + pathFrom.c_str(), static_cast(pathFrom.size()), + pathTo .c_str(), static_cast(pathTo.size()), flags); +} + + +namespace zen +{ +namespace +{ +std::wstring formatSshStatusCode(int sc) +{ + switch (sc) + { + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_NONE); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_SOCKET_NONE); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_BANNER_RECV); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_BANNER_SEND); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_INVALID_MAC); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_KEX_FAILURE); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_ALLOC); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_SOCKET_SEND); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_KEY_EXCHANGE_FAILURE); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_TIMEOUT); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_HOSTKEY_INIT); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_HOSTKEY_SIGN); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_DECRYPT); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_SOCKET_DISCONNECT); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_PROTO); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_PASSWORD_EXPIRED); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_FILE); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_METHOD_NONE); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_AUTHENTICATION_FAILED); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_PUBLICKEY_UNVERIFIED); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_CHANNEL_OUTOFORDER); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_CHANNEL_FAILURE); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_CHANNEL_REQUEST_DENIED); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_CHANNEL_UNKNOWN); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_CHANNEL_WINDOW_EXCEEDED); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_CHANNEL_PACKET_EXCEEDED); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_CHANNEL_CLOSED); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_CHANNEL_EOF_SENT); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_SCP_PROTOCOL); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_ZLIB); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_SOCKET_TIMEOUT); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_SFTP_PROTOCOL); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_REQUEST_DENIED); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_METHOD_NOT_SUPPORTED); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_INVAL); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_INVALID_POLL_TYPE); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_PUBLICKEY_PROTOCOL); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_EAGAIN); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_BUFFER_TOO_SMALL); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_BAD_USE); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_COMPRESS); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_OUT_OF_BOUNDARY); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_AGENT_PROTOCOL); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_SOCKET_RECV); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_ENCRYPT); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_BAD_SOCKET); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_KNOWN_HOSTS); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_CHANNEL_WINDOW_FULL); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_KEYFILE_AUTH_FAILED); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_RANDGEN); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_MISSING_USERAUTH_BANNER); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_ALGO_UNSUPPORTED); + + default: + return replaceCpy(L"SSH status %x", L"%x", numberTo(sc)); + } +} + + +std::wstring formatSftpStatusCode(unsigned long sc) +{ + //libssh2 only defines LIBSSH2_FX_OK(0) to LIBSSH2_FX_LINK_LOOP(21) + //=> all SFTP codes: https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.1 + switch (sc) + { + case 0: return L"SSH_FX_OK"; + case 1: return L"SSH_FX_EOF"; + case 2: return L"SSH_FX_NO_SUCH_FILE"; + case 3: return L"SSH_FX_PERMISSION_DENIED"; + case 4: return L"SSH_FX_FAILURE"; + case 5: return L"SSH_FX_BAD_MESSAGE"; + case 6: return L"SSH_FX_NO_CONNECTION"; + case 7: return L"SSH_FX_CONNECTION_LOST"; + case 8: return L"SSH_FX_OP_UNSUPPORTED"; + case 9: return L"SSH_FX_INVALID_HANDLE"; + case 10: return L"SSH_FX_NO_SUCH_PATH"; + case 11: return L"SSH_FX_FILE_ALREADY_EXISTS"; + case 12: return L"SSH_FX_WRITE_PROTECT"; + case 13: return L"SSH_FX_NO_MEDIA"; + case 14: return L"SSH_FX_NO_SPACE_ON_FILESYSTEM"; + case 15: return L"SSH_FX_QUOTA_EXCEEDED"; + case 16: return L"SSH_FX_UNKNOWN_PRINCIPAL"; + case 17: return L"SSH_FX_LOCK_CONFLICT"; + case 18: return L"SSH_FX_DIR_NOT_EMPTY"; + case 19: return L"SSH_FX_NOT_A_DIRECTORY"; + case 20: return L"SSH_FX_INVALID_FILENAME"; + case 21: return L"SSH_FX_LINK_LOOP"; + case 22: return L"SSH_FX_CANNOT_DELETE"; + case 23: return L"SSH_FX_INVALID_PARAMETER"; + case 24: return L"SSH_FX_FILE_IS_A_DIRECTORY"; + case 25: return L"SSH_FX_BYTE_RANGE_LOCK_CONFLICT"; + case 26: return L"SSH_FX_BYTE_RANGE_LOCK_REFUSED"; + case 27: return L"SSH_FX_DELETE_PENDING"; + case 28: return L"SSH_FX_FILE_CORRUPT"; + case 29: return L"SSH_FX_OWNER_INVALID"; + case 30: return L"SSH_FX_GROUP_INVALID"; + case 31: return L"SSH_FX_NO_MATCHING_BYTE_RANGE_LOCK"; + + default: return replaceCpy(L"SFTP status %x", L"%x", numberTo(sc)); + } +} +} +} + +#else +#error Why is this header already defined? Do not include in other headers: encapsulate the gory details! +#endif //LIBSSH2_WRAP_H_087280957180967346572465 diff --git a/wx+/app_main.h b/wx+/app_main.h new file mode 100644 index 0000000..38438d0 --- /dev/null +++ b/wx+/app_main.h @@ -0,0 +1,17 @@ +// ***************************************************************************** +// * 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 APP_MAIN_H_08215601837818347575856 +#define APP_MAIN_H_08215601837818347575856 + +#include + + +namespace zen +{ +} + +#endif //APP_MAIN_H_08215601837818347575856 diff --git a/wx+/async_task.h b/wx+/async_task.h new file mode 100644 index 0000000..87a179f --- /dev/null +++ b/wx+/async_task.h @@ -0,0 +1,162 @@ +// ***************************************************************************** +// * 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 ASYNC_TASK_H_839147839170432143214321 +#define ASYNC_TASK_H_839147839170432143214321 + +#include +#include +#include +#include + + +namespace zen +{ +/* Run a task in an async thread, but process result in GUI event loop + ------------------------------------------------------------------- + 1. put AsyncGuiQueue instance inside a dialog: + AsyncGuiQueue guiQueue; + + 2. schedule async task and synchronous continuation: + guiQueue.processAsync(evalAsync, evalOnGui); + + Alternative: wxWidgets' inter-thread communication (wxEvtHandler::QueueEvent) https://wiki.wxwidgets.org/Inter-Thread_and_Inter-Process_communication + => don't bother, probably too many MT race conditions lurking around */ + +namespace impl +{ +struct Task +{ + virtual ~Task() {} + virtual bool resultReady () const = 0; + virtual void evaluateResult() = 0; +}; + + +template +class ConcreteTask : public Task +{ +public: + template + ConcreteTask(std::future&& asyncResult, Fun2&& evalOnGui) : + asyncResult_(std::move(asyncResult)), evalOnGui_(std::forward(evalOnGui)) {} + + bool resultReady() const override { return isReady(asyncResult_); } + + void evaluateResult() override + { + if constexpr (std::is_same_v) + { + asyncResult_.get(); + evalOnGui_(); + } + else + evalOnGui_(asyncResult_.get()); + } + +private: + std::future asyncResult_; + Fun evalOnGui_; //keep "evalOnGui" strictly separated from async thread: in particular do not copy in thread! +}; + + +class AsyncTasks +{ +public: + AsyncTasks() {} + + template + void add(Fun&& evalAsync, Fun2&& evalOnGui) + { + using ResultType = decltype(evalAsync()); + + std::promise prom; + tasks_.push_back(std::make_unique>>(prom.get_future(), std::forward(evalOnGui))); + + //don't use zen::runAsync() and std::packaged_task => let exceptions crash the app directly at throw location! + std::thread([prom = std::move(prom), + fun = std::forward(evalAsync)]() mutable + { + if constexpr (std::is_same_v) + { + fun(); //throw? => let it crash! + prom.set_value(); + } + else + prom.set_value(fun()); //throw? => let it crash! + }).detach(); + } + //equivalent to "evalOnGui(evalAsync())" + // -> evalAsync: the usual thread-safety requirements apply! + // -> evalOnGui: no thread-safety concerns, but must only reference variables with greater-equal lifetime than the AsyncTask instance! + + void evalResults() //call from GUI thread repreatedly + { + if (!inRecursion_) //prevent implicit recursion, e.g. if we're called from an idle event and spawn another one within the callback below + { + inRecursion_ = true; + ZEN_ON_SCOPE_EXIT(inRecursion_ = false); + + std::vector> readyTasks; //Reentrancy; access to AsyncTasks::add is not protected! => evaluate outside of eraseIf() + + eraseIf(tasks_, [&](std::unique_ptr& task) + { + if (task->resultReady()) + { + readyTasks.push_back(std::move(task)); + return true; + } + return false; + }); + + for (std::unique_ptr& task : readyTasks) + task->evaluateResult(); + } + } + + bool empty() const { return tasks_.empty(); } + +private: + AsyncTasks (const AsyncTasks&) = delete; + AsyncTasks& operator=(const AsyncTasks&) = delete; + + bool inRecursion_ = false; + std::vector> tasks_; +}; +} + + +class AsyncGuiQueue : private wxEvtHandler +{ +public: + explicit AsyncGuiQueue(int pollingMs = 50) : + pollingMs_(pollingMs) { timer_.Bind(wxEVT_TIMER, [this](wxTimerEvent& event) { onTimerEvent(event); }); } + + template + void processAsync(Fun&& evalAsync, Fun2&& evalOnGui) + { + asyncTasks_.add(std::forward(evalAsync), + std::forward(evalOnGui)); + if (!timer_.IsRunning()) + timer_.Start(pollingMs_ /*unit: [ms]*/); + } + +private: + void onTimerEvent(wxEvent& event) //schedule and run long-running tasks asynchronously + { + asyncTasks_.evalResults(); //process results on GUI queue + if (asyncTasks_.empty()) + timer_.Stop(); + } + + const int pollingMs_; + impl::AsyncTasks asyncTasks_; + wxTimer timer_; //don't use wxWidgets' idle handling => repeated idle requests/consumption hogs 100% cpu! +}; + +} + +#endif //ASYNC_TASK_H_839147839170432143214321 diff --git a/wx+/bitmap_button.h b/wx+/bitmap_button.h new file mode 100644 index 0000000..89ea891 --- /dev/null +++ b/wx+/bitmap_button.h @@ -0,0 +1,150 @@ +// ***************************************************************************** +// * 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 BITMAP_BUTTON_H_83415718945878341563415 +#define BITMAP_BUTTON_H_83415718945878341563415 + +#include +#include +#include +#include "image_tools.h" +#include "std_button_layout.h" +#include "dc.h" + + +namespace zen +{ +//zen::BitmapTextButton is identical to wxBitmapButton, but preserves the label via SetLabel(), which wxFormbuilder would ditch! +class BitmapTextButton : public wxBitmapButton +{ +public: + BitmapTextButton(wxWindow* parent, + wxWindowID id, + const wxString& label, + const wxPoint& pos = wxDefaultPosition, + const wxSize& size = wxDefaultSize, + long style = 0, + const wxValidator& validator = wxDefaultValidator, + const wxString& name = wxASCII_STR(wxButtonNameStr)) : + wxBitmapButton(parent, id, + //(FreeFileSync_x86_64:77379): Gtk-CRITICAL **: 11:04:31.752: IA__gtk_widget_modify_style: assertion 'GTK_IS_WIDGET (widget)' failed + rectangleImage({1, 1}, *wxRED), + pos, size, style, validator, name) + { + SetLabel(label); + } +}; + +//wxButton::SetBitmap() also supports "image + text", but screws up proper gap and border handling +void setBitmapTextLabel(wxBitmapButton& btn, const wxImage& img, const wxString& text, int gap = dipToWxsize(5), int border = dipToWxsize(5)); + +//set bitmap label flicker free: +void setImage(wxAnyButton& button, const wxImage& bmp); +void setImage(wxStaticBitmap& staticBmp, const wxImage& img); + +wxImage renderPressedButton(const wxSize& sz); + +inline wxColor getColorToggleButtonBorder() { return {0x79, 0xbc, 0xed}; } //medium blue +inline wxColor getColorToggleButtonFill () { return {0xcc, 0xe4, 0xf8}; } //light blue + + + + + + + + +//################################### implementation ################################### +inline +void setBitmapTextLabel(wxBitmapButton& btn, const wxImage& img, const wxString& text, int gap, int border) +{ + assert(gap >= 0 && border >= 0); + gap = std::max(0, gap); + border = std::max(0, border); + + wxImage imgTxt = createImageFromText(text, btn.GetFont(), btn.GetForegroundColour()); + if (img.IsOk()) + imgTxt = btn.GetLayoutDirection() != wxLayout_RightToLeft ? + stackImages(img, imgTxt, ImageStackLayout::horizontal, ImageStackAlignment::center, wxsizeToScreen(gap)) : + stackImages(imgTxt, img, ImageStackLayout::horizontal, ImageStackAlignment::center, wxsizeToScreen(gap)); + + //SetMinSize() instead of SetSize() is needed here for wxWidgets layout determination to work correctly + btn.SetMinSize({screenToWxsize(imgTxt.GetWidth()) + 2 * border, + std::max(screenToWxsize(imgTxt.GetHeight()) + 2 * border, getDefaultButtonHeight())}); + + setImage(btn, imgTxt); +} + + +inline +void setImage(wxAnyButton& button, const wxImage& img) +{ + if (!img.IsOk()) + { + button.SetBitmapLabel (wxNullBitmap); + button.SetBitmapDisabled(wxNullBitmap); + return; + } + + + button.SetBitmapLabel(toScaledBitmap(img)); + + //wxWidgets excels at screwing up consistently once again: + //the first call to SetBitmapLabel() *implicitly* sets the disabled bitmap, too, subsequent calls, DON'T! + button.SetBitmapDisabled(toScaledBitmap(img.ConvertToDisabled())); //inefficiency: wxBitmap::ConvertToDisabled() implicitly converts to wxImage! +} + + +inline +void setImage(wxStaticBitmap& staticBmp, const wxImage& img) +{ + staticBmp.SetBitmap(toScaledBitmap(img)); +} + + + +inline +wxImage generatePressedButtonBack(const wxSize& sz) +{ +#if 1 + return rectangleImage(sz, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW), {0x11, 0x79, 0xfe} /*light blue*/, dipToScreen(2)); + +#else //rectangle border with gradient as background + wxBitmap bmp(wxsizeToScreen(sz.x), + wxsizeToScreen(sz.y)); //seems we don't need to pass 24-bit depth here even for high-contrast color schemes + bmp.SetScaleFactor(getScreenDpiScale()); + { + //draw rectangle border with gradient + const wxColor colFrom = wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE); + const wxColor colTo(0x11, 0x79, 0xfe); //light blue + + wxMemoryDC dc(bmp); + dc.SetPen(*wxTRANSPARENT_PEN); //wxTRANSPARENT_PEN is about 2x faster than redundantly drawing with col! + + wxRect rect(sz); + + const int borderSize = dipToWxsize(3); + for (int i = 1 ; i <= borderSize; ++i) + { + const wxColor colGradient((colFrom.Red () * (borderSize - i) + colTo.Red () * i) / borderSize, + (colFrom.Green() * (borderSize - i) + colTo.Green() * i) / borderSize, + (colFrom.Blue () * (borderSize - i) + colTo.Blue () * i) / borderSize); + dc.SetBrush(colGradient); + dc.DrawRectangle(rect); + rect.Deflate(dipToWxsize(1)); + } + + dc.SetBrush(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); + dc.DrawRectangle(rect); + } + wxImage img = bmp.ConvertToImage(); + convertToVanillaImage(img); + return img; +#endif +} +} + +#endif //BITMAP_BUTTON_H_83415718945878341563415 diff --git a/wx+/choice_enum.h b/wx+/choice_enum.h new file mode 100644 index 0000000..6b28b4f --- /dev/null +++ b/wx+/choice_enum.h @@ -0,0 +1,117 @@ +// ***************************************************************************** +// * 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 CHOICE_ENUM_H_132413545345687 +#define CHOICE_ENUM_H_132413545345687 + +//#include +#include + + +namespace zen +{ +//handle mapping of enum values to wxChoice controls +template +class EnumDescrList +{ +public: + using DescrItem = std::tuple; + + EnumDescrList(wxChoice& ctrl, std::vector list); + ~EnumDescrList(); + + void set(Enum value); + Enum get() const ; + void updateTooltip(); //after user changed selection + + const std::vector& getConfig() const { return descrList_; } + +private: + wxChoice& ctrl_; + const std::vector descrList_; + std::vector labels_; +}; + + + + + + + + + + + + + + +//--------------- impelementation ------------------------------------------- +template +EnumDescrList::EnumDescrList(wxChoice& ctrl, std::vector list) : ctrl_(ctrl), descrList_(std::move(list)) +{ + for (const auto& [val, label, tooltip] : descrList_) + labels_.push_back(label); + + ctrl_.Set(labels_); //expensive as fuck! => only call when needed! +} + + +template inline +EnumDescrList::~EnumDescrList() +{ +} + + +template +void EnumDescrList::set(Enum value) +{ + const auto it = std::find_if(descrList_.begin(), descrList_.end(), [&](const auto& mapItem) { return std::get(mapItem) == value; }); + if (it != descrList_.end()) + { + const auto& [val, label, tooltip] = *it; + if (!tooltip.empty()) + ctrl_.SetToolTip(tooltip); + else + ctrl_.UnsetToolTip(); + + const int selectedPos = it - descrList_.begin(); + ctrl_.SetSelection(selectedPos); + } + else assert(false); +} + + +template +Enum EnumDescrList::get() const +{ + const int selectedPos = ctrl_.GetSelection(); + + if (0 <= selectedPos && selectedPos < std::ssize(descrList_)) + return std::get(descrList_[selectedPos]); + + assert(false); + return Enum(0); +} + + +template +void EnumDescrList::updateTooltip() +{ + const int selectedPos = ctrl_.GetSelection(); + + if (0 <= selectedPos && selectedPos < std::ssize(descrList_)) + { + const auto& [val, label, tooltip] = descrList_[selectedPos]; + if (!tooltip.empty()) + ctrl_.SetToolTip(tooltip); + else + ctrl_.UnsetToolTip(); + } + else assert(false); +} +} + +#endif //CHOICE_ENUM_H_132413545345687 diff --git a/wx+/color_tools.h b/wx+/color_tools.h new file mode 100644 index 0000000..45f2220 --- /dev/null +++ b/wx+/color_tools.h @@ -0,0 +1,219 @@ +// ***************************************************************************** +// * 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 COLOR_TOOLS_H_18301239864123785613 +#define COLOR_TOOLS_H_18301239864123785613 + +#include +#include + + +namespace zen +{ +inline +double srgbDecode(unsigned char c) //https://en.wikipedia.org/wiki/SRGB +{ + const double c_ = c / 255.0; + return c_ <= 0.04045 ? c_ / 12.92 : std::pow((c_ + 0.055) / 1.055, 2.4); +} + + +inline +unsigned char srgbEncode(double c) +{ + const double c_ = c <= 0.0031308 ? c * 12.92 : std::pow(c, 1 / 2.4) * 1.055 - 0.055; + return std::clamp(std::round(c_ * 255), 0, 255); +} + + +inline //https://www.w3.org/WAI/GL/wiki/Relative_luminance +double relLuminance(double r, double g, double b) //input: gamma-decoded sRGB +{ + return 0.2126 * r + 0.7152 * g + 0.0722 * b; //= the Y part of CIEXYZ +} + + +inline +double relativeLuminance(const wxColor& col) //[0, 1] +{ + assert(col.Alpha() == wxALPHA_OPAQUE); + return relLuminance(srgbDecode(col.Red()), srgbDecode(col.Green()), srgbDecode(col.Blue())); +} + + +inline +double relativeContrast(const wxColor& c1, const wxColor& c2) +{ + //https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef + //https://snook.ca/technical/colour_contrast/colour.html + double lum1 = relativeLuminance(c1); + double lum2 = relativeLuminance(c2); + if (lum1 < lum2) + std::swap(lum1, lum2); + return (lum1 + 0.05) / (lum2 + 0.05); +} + + +namespace +{ +//get first color between [col1, white/black] (assuming direct line in decoded sRGB) where minimum contrast is satisfied against col2 +wxColor enhanceContrast(wxColor col1, const wxColor& col2, double contrastRatioMin) +{ + const wxColor colMax = relativeLuminance(col2) < 0.17912878474779204 /* = sqrt(0.05 * 1.05) - 0.05 */ ? 0xffffff : 0; + //equivalent to: relativeContrast(col2, *wxWHITE) > relativeContrast(col2, *wxBLACK) ? *wxWHITE : *wxBLACK + + assert(col2.Alpha() == wxALPHA_OPAQUE); + if (col2.Alpha() != wxALPHA_OPAQUE) + return *wxRED; //make some noise + + /* CAVEAT: macOS uses partially-transparent colors! e.g. in #RGBA: + wxSYS_COLOUR_GRAYTEXT #FFFFFF3F + wxSYS_COLOUR_WINDOWTEXT #FFFFFFD8 + wxSYS_COLOUR_WINDOW #171717FF */ + if (col1.Alpha() != wxALPHA_OPAQUE) + { + auto calcChannel = [a = col1.Alpha()](unsigned char f, unsigned char b) + { + return static_cast(numeric::intDivRound(f * a + b * (255 - a), 255)); + }; + + col1 = wxColor(calcChannel(col1.Red (), col2.Red ()), + calcChannel(col1.Green(), col2.Green()), + calcChannel(col1.Blue (), col2.Blue ())); + } + + //--------------------------------------------------------------- + assert(contrastRatioMin >= 3); //lower values (especially near 1) probably aren't sensible mathematically, also: W3C recommends >= 4.5 for base AA compliance + auto contrast = [](double lum1, double lum2) //input: relative luminance + { + if (lum1 < lum2) + std::swap(lum1, lum2); + return (lum1 + 0.05) / (lum2 + 0.05); + }; + const double r_1 = srgbDecode(col1.Red()); + const double g_1 = srgbDecode(col1.Green()); + const double b_1 = srgbDecode(col1.Blue()); + const double r_m = srgbDecode(colMax.Red()); + const double g_m = srgbDecode(colMax.Green()); + const double b_m = srgbDecode(colMax.Blue()); + + const double lum_1 = relLuminance(r_1, g_1, b_1); + const double lum_m = relLuminance(r_m, g_m, b_m); + const double lum_2 = relativeLuminance(col2); + + if (contrast(lum_1, lum_2) >= contrastRatioMin) + return col1; //nothing to do! + + if (contrast(lum_m, lum_2) <= contrastRatioMin) + { + assert(false); //problem! + return colMax; + } + + if (lum_m < lum_2) + contrastRatioMin = 1 / contrastRatioMin; + + const double lum_t = contrastRatioMin * (lum_2 + 0.05) - 0.05; //target luminance + const double t = (lum_t - lum_1) / (lum_m - lum_1); + + return wxColor(srgbEncode(t * (r_m - r_1) + r_1), + srgbEncode(t * (g_m - g_1) + g_1), + srgbEncode(t * (b_m - b_1) + b_1)); +} +} + +#if 0 +//toy sample code: gamma-encoded sRGB -> CIEXYZ -> CIELAB and back: input === output RGB color (verified) +wxColor colorConversion(const wxColor& col) +{ + assert(col.GetAlpha() == wxALPHA_OPAQUE); + const double r = srgbDecode(col.Red()); + const double g = srgbDecode(col.Green()); + const double b = srgbDecode(col.Blue()); + + //https://en.wikipedia.org/wiki/SRGB#Correspondence_to_CIE_XYZ_stimulus + const double x = 0.4124 * r + 0.3576 * g + 0.1805 * b; + const double y = 0.2126 * r + 0.7152 * g + 0.0722 * b; + const double z = 0.0193 * r + 0.1192 * g + 0.9505 * b; + //----------------------------------------------- + //https://en.wikipedia.org/wiki/CIELAB_color_space#Converting_between_CIELAB_and_CIEXYZ_coordinates + using numeric::power; + auto f = [](double t) + { + constexpr double delta = 6.0 / 29; + return t > power<3>(delta) ? + std::pow(t, 1.0 / 3) : + t / (3 * power<2>(delta)) + 4.0 / 29; + }; + const double L_ = 116 * f(y) - 16; //[ 0, 100] + const double a_ = 500 * (f(x / 0.950489) - f(y)); //[-128, 127] + const double b_ = 200 * (f(y) - f(z / 1.088840)); //[-128, 127] + //----------------------------------------------- + auto f_1 = [](double t) + { + constexpr double delta = 6.0 / 29; + return t > delta ? + power<3>(t) : + 3 * power<2>(delta) * (t - 4.0 / 29); + }; + const double x2 = 0.950489 * f_1((L_ + 16) / 116 + a_ / 500); + const double y2 = f_1((L_ + 16) / 116); + const double z2 = 1.088840 * f_1((L_ + 16) / 116 - b_ / 200); + //----------------------------------------------- + const double r2 = 3.2406255 * x2 + -1.5372080 * y2 + -0.4986286 * z2; + const double g2 = -0.9689307 * x2 + 1.8757561 * y2 + 0.0415175 * z2; + const double b2 = 0.0557101 * x2 + -0.2040211 * y2 + 1.0569959 * z2; + + return wxColor(srgbEncode(r2), srgbEncode(g2), srgbEncode(b2)); +} + + +//https://en.wikipedia.org/wiki/HSL_and_HSV +wxColor hsvColor(double h, double s, double v) //h within [0, 360), s, v within [0, 1] +{ + //make input values fit into bounds + if (h > 360) + h -= static_cast(h / 360) * 360; + else if (h < 0) + h -= static_cast(h / 360) * 360 - 360; + s = std::clamp(s, 0.0, 1.0); + v = std::clamp(v, 0.0, 1.0); + //------------------------------------ + const int h_i = h / 60; + const float f = h / 60 - h_i; + + auto to8Bit = [](double val) -> unsigned char + { + return std::clamp(std::round(val * 255), 0, 255); + }; + + const unsigned char p = to8Bit(v * (1 - s)); + const unsigned char q = to8Bit(v * (1 - s * f)); + const unsigned char t = to8Bit(v * (1 - s * (1 - f))); + const unsigned char vi = to8Bit(v); + + switch (h_i) + { + case 0: + return wxColor(vi, t, p); + case 1: + return wxColor(q, vi, p); + case 2: + return wxColor(p, vi, t); + case 3: + return wxColor(p, q, vi); + case 4: + return wxColor(t, p, vi); + case 5: + return wxColor(vi, p, q); + } + assert(false); + return *wxBLACK; +} +#endif +} + +#endif //COLOR_TOOLS_H_18301239864123785613 diff --git a/wx+/context_menu.h b/wx+/context_menu.h new file mode 100644 index 0000000..06e8470 --- /dev/null +++ b/wx+/context_menu.h @@ -0,0 +1,163 @@ +// ***************************************************************************** +// * 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 CONTEXT_MENU_H_18047302153418174632141234 +#define CONTEXT_MENU_H_18047302153418174632141234 + +#include +#include +#include +#include +#include +#include "dc.h" + + +/* A context menu supporting lambda callbacks! + + Usage: + ContextMenu menu; + menu.addItem(L"Some Label", [&]{ ...do something... }); -> capture by reference is fine, as long as captured variables have at least scope of ContextMenu::popup()! + ... + menu.popup(wnd); */ + +namespace zen +{ +inline +void setImage(wxMenuItem& menuItem, const wxImage& img) +{ + menuItem.SetBitmap(toScaledBitmap(img)); +} + + +class ContextMenu : private wxEvtHandler +{ +public: + ContextMenu() {} + + void addItem(const wxString& label, const std::function& command, const wxImage& img = wxNullImage, bool enabled = true) + { + wxMenuItem* newItem = new wxMenuItem(menu_.get(), wxID_ANY, label); //menu owns item! + if (img.IsOk()) + setImage(*newItem, img); //do not set AFTER appending item! wxWidgets screws up for yet another crappy reason + menu_->Append(newItem); + if (!enabled) + newItem->Enable(false); //do not enable BEFORE appending item! wxWidgets screws up for yet another crappy reason + commandList_[newItem->GetId()] = command; //defer event connection, this may be a submenu only! + } + + void addCheckBox(const wxString& label, const std::function& command, bool checked, bool enabled = true) + { + wxMenuItem* newItem = menu_->AppendCheckItem(wxID_ANY, label); + newItem->Check(checked); + if (!enabled) + newItem->Enable(false); + commandList_[newItem->GetId()] = command; + } + + void addRadio(const wxString& label, const std::function& command, bool selected, bool enabled = true) + { + wxMenuItem* newItem = menu_->AppendRadioItem(wxID_ANY, label); + newItem->Check(selected); + if (!enabled) + newItem->Enable(false); + commandList_[newItem->GetId()] = command; + } + + void addSeparator() { menu_->AppendSeparator(); } + + void addSubmenu(const wxString& label, ContextMenu& submenu, const wxImage& img = wxNullImage, bool enabled = true) //invalidates submenu! + { + //transfer submenu commands: + commandList_.insert(submenu.commandList_.begin(), submenu.commandList_.end()); + submenu.commandList_.clear(); + + submenu.menu_->SetNextHandler(menu_.get()); //on wxGTK submenu events are not propagated to their parent menu by default! + + wxMenuItem* newItem = new wxMenuItem(menu_.get(), wxID_ANY, label, L"", wxITEM_NORMAL, submenu.menu_.release()); //menu owns item, item owns submenu! + if (img.IsOk()) + setImage(*newItem, img); //do not set AFTER appending item! wxWidgets screws up for yet another crappy reason + menu_->Append(newItem); + if (!enabled) + newItem->Enable(false); + } + + void popup(wxWindow& wnd, const wxPoint& pos = wxDefaultPosition) //show popup menu + process lambdas + { + //eventually all events from submenu items will be received by the parent menu + for (const auto& [itemId, command] : commandList_) + menu_->Bind(wxEVT_COMMAND_MENU_SELECTED, [command /*clang bug*/= command](wxCommandEvent& event) { command(); }, itemId); + + wnd.PopupMenu(menu_.get(), pos); + wxTheApp->ProcessPendingEvents(); //make sure lambdas are evaluated before going out of scope; + //although all events seem to be processed within wxWindows::PopupMenu, we shouldn't trust wxWidgets in this regard + } + +private: + ContextMenu (const ContextMenu&) = delete; + ContextMenu& operator=(const ContextMenu&) = delete; + + std::unique_ptr menu_ = std::make_unique(); + std::unordered_map /*command*/> commandList_; +}; + + +//GTK: image must be set *before* adding wxMenuItem to menu or it won't show => workaround: +inline //also needed on Windows + macOS since wxWidgets 3.1.6 (thanks?) +void fixMenuIcons(wxMenu& menu) +{ + std::vector> itemsWithBmp; + { + size_t pos = 0; + for (wxMenuItem* item : menu.GetMenuItems()) + { + if (item->GetBitmap().IsOk()) + itemsWithBmp.emplace_back(item, pos); + ++pos; + } + } + + for (const auto& [item, pos] : itemsWithBmp) + if (!menu.Insert(pos, menu.Remove(item))) //detach + reinsert + assert(false); +} + + +//better call wxClipboard::Get()->Flush() *once* during app exit instead of after each setClipboardText()? +// => OleFlushClipboard: "Carries out the clipboard shutdown sequence" +// => maybe this helps with clipboard randomly "forgetting" content after app exit? +inline +void setClipboardText(const wxString& txt) +{ + wxClipboard& clip = *wxClipboard::Get(); + if (clip.Open()) + { + ZEN_ON_SCOPE_EXIT(clip.Close()); + [[maybe_unused]] const bool rv = clip.SetData(new wxTextDataObject(txt)); //ownership passed + assert(rv); + } + else assert(false); +} + + +inline +std::optional getClipboardText() +{ + wxClipboard& clip = *wxClipboard::Get(); + if (clip.Open()) + { + ZEN_ON_SCOPE_EXIT(clip.Close()); + + //if (clip.IsSupported(wxDF_TEXT or wxDF_UNICODETEXT !???)) - superfluous? already handled by wxClipboard::GetData()!? + wxTextDataObject data; + if (clip.GetData(data)) + return data.GetText(); + } + else assert(false); + return std::nullopt; +} +} + +#endif //CONTEXT_MENU_H_18047302153418174632141234 diff --git a/wx+/darkmode.cpp b/wx+/darkmode.cpp new file mode 100644 index 0000000..626ba35 --- /dev/null +++ b/wx+/darkmode.cpp @@ -0,0 +1,102 @@ +// ***************************************************************************** +// * 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 "darkmode.h" +#include +#include +#include "color_tools.h" + #include + +using namespace zen; + + +bool zen::darkModeAvailable() +{ + +#if GTK_MAJOR_VERSION == 2 + return false; +#elif GTK_MAJOR_VERSION >= 3 + return true; +#else +#error unknown GTK version! +#endif + +} + + +namespace +{ +class SysColorsHook : public wxColorHook +{ +public: + + wxColor getColor(wxSystemColour index) const override + { + //fix contrast e.g. Ubuntu's Adwaita-Dark theme and macOS dark mode: + if (index == wxSYS_COLOUR_GRAYTEXT) + return colGreyTextEnhContrast_; +#if 0 + auto colToString = [](wxColor c) { return utfTo(c.GetAsString(wxC2S_HTML_SYNTAX)); /* #RRGGBB(AA) */ }; + std::cerr << "wxSYS_COLOUR_GRAYTEXT " << colToString(wxSystemSettingsNative::GetColour(wxSYS_COLOUR_GRAYTEXT)) << "\n"; +#endif + return wxSystemSettingsNative::GetColour(index); //fallback + } + +private: + const wxColor colGreyTextEnhContrast_ = + enhanceContrast(wxSystemSettingsNative::GetColour(wxSYS_COLOUR_GRAYTEXT), + wxSystemSettingsNative::GetColour(wxSYS_COLOUR_WINDOW), 4.5 /*contrastRatioMin*/); //W3C recommends >= 4.5 +}; + + +std::optional globalDefaultThemeIsDark; +} + + +void zen::colorThemeInit(wxApp& app, ColorTheme colTheme) //throw FileError +{ + assert(!refGlobalColorHook()); + + globalDefaultThemeIsDark = wxSystemSettings::GetAppearance().AreAppsDark(); + ZEN_ON_SCOPE_EXIT(if (!refGlobalColorHook()) refGlobalColorHook() = std::make_unique()); //*after* SetAppearance() and despite errors + + //caveat: on macOS there are more themes than light/dark: https://developer.apple.com/documentation/appkit/nsappearance/name-swift.struct + if (colTheme != ColorTheme::System && //"System" is already the default for macOS/Linux(GTK3) + darkModeAvailable()) + changeColorTheme(colTheme); //throw FileError +} + + +void zen::colorThemeCleanup() +{ + assert(refGlobalColorHook()); + refGlobalColorHook().reset(); +} + + +bool zen::equalAppearance(ColorTheme colTheme1, ColorTheme colTheme2) +{ + if (colTheme1 == ColorTheme::System) colTheme1 = *globalDefaultThemeIsDark ? ColorTheme::Dark : ColorTheme::Light; + if (colTheme2 == ColorTheme::System) colTheme2 = *globalDefaultThemeIsDark ? ColorTheme::Dark : ColorTheme::Light; + return colTheme1 == colTheme2; +} + + +void zen::changeColorTheme(ColorTheme colTheme) //throw FileError +{ + if (colTheme == ColorTheme::System) //SetAppearance(System) isn't working reliably! surprise!? + colTheme = *globalDefaultThemeIsDark ? ColorTheme::Dark : ColorTheme::Light; + + try + { + ZEN_ON_SCOPE_SUCCESS(refGlobalColorHook() = std::make_unique()); //*after* SetAppearance() + if (wxApp::AppearanceResult rv = wxTheApp->SetAppearance(colTheme); + rv != wxApp::AppearanceResult::Ok) + throw SysError(formatSystemError("wxApp::SetAppearance", + rv == wxApp::AppearanceResult::CannotChange ? L"CannotChange" : L"Failure", L"" /*errorMsg*/)); + } + catch (const SysError& e) { throw FileError(_("Failed to update the color theme."), e.toString()); } +} diff --git a/wx+/darkmode.h b/wx+/darkmode.h new file mode 100644 index 0000000..91d2a78 --- /dev/null +++ b/wx+/darkmode.h @@ -0,0 +1,28 @@ +// ***************************************************************************** +// * 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 DARKMODE_H_754298057018 +#define DARKMODE_H_754298057018 + +#include +#include + + +namespace zen +{ +bool darkModeAvailable(); + +//support not only "dark mode" but dark themes in general +using ColorTheme = wxApp::Appearance; //why reinvent the wheel? + +void colorThemeInit(wxApp& app, ColorTheme colTheme); //throw FileError +void colorThemeCleanup(); + +bool equalAppearance(ColorTheme colTheme1, ColorTheme colTheme2); +void changeColorTheme(ColorTheme colTheme); //throw FileError +} + +#endif //DARKMODE_H_754298057018 diff --git a/wx+/dc.h b/wx+/dc.h new file mode 100644 index 0000000..862568c --- /dev/null +++ b/wx+/dc.h @@ -0,0 +1,316 @@ +// ***************************************************************************** +// * 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 DC_H_4987123956832143243214 +#define DC_H_4987123956832143243214 + +#include +#include +#include +//#include //macOS: std::get +#include //for macro: wxALWAYS_NATIVE_DOUBLE_BUFFER +#include + + +namespace zen +{ +inline +void clearArea(wxDC& dc, const wxRect& rect, const wxColor& col) +{ + assert(col.IsSolid()); + if (rect.width > 0 && //clearArea() is surprisingly expensive + rect.height > 0) + { + //wxDC::DrawRectangle() just widens inner area if wxTRANSPARENT_PEN is used! + //bonus: wxTRANSPARENT_PEN is about 2x faster than redundantly drawing with col! + dc.SetPen(*wxTRANSPARENT_PEN); + dc.SetBrush(col); + dc.DrawRectangle(rect); + } +} + + +//properly draw rectangle respecting high DPI (and avoiding wxPen position fuzzyness) +inline +void drawFilledRectangle(wxDC& dc, wxRect rect, const wxColor& innerCol, const wxColor& borderCol, int borderSize) +{ + assert(innerCol.IsSolid() && borderCol.IsSolid()); + if (rect.width > 0 && + rect.height > 0) + { + dc.SetPen(*wxTRANSPARENT_PEN); + dc.SetBrush(borderCol); + dc.DrawRectangle(rect); + + rect.Deflate(borderSize); //more wxWidgets design mistakes: behavior of wxRect::Deflate depends on object being const/non-const!!! + + if (rect.width > 0 && + rect.height > 0) + { + dc.SetBrush(innerCol); + dc.DrawRectangle(rect); + } + } +} + + +inline +void drawRectangleBorder(wxDC& dc, const wxRect& rect, const wxColor& col, int borderSize) +{ + assert(col.IsSolid()); + if (rect.width > 0 && + rect.height > 0) + { + if (2 * borderSize >= std::min(rect.width, rect.height)) + return clearArea(dc, rect, col); + + dc.SetPen(*wxTRANSPARENT_PEN); + dc.SetBrush(col); + dc.DrawRectangle(rect.x, rect.y, borderSize, rect.height); //left + dc.DrawRectangle(rect.x + rect.width - borderSize, rect.y, borderSize, rect.height); //right + dc.DrawRectangle(rect.x, rect.y, rect.width, borderSize); //top + dc.DrawRectangle(rect.x, rect.y + rect.height - borderSize, rect.width, borderSize); //bottom + } +} + + +/* figure out wxWidgets cross-platform high DPI mess: + + 1. "wxsize" := what wxWidgets is using: device-dependent on Windows, device-indepent on macOS (...mostly) + 2. screen unit := device-dependent size in pixels + 3. DIP := device-independent pixels + + corollary: + macOS: "wxsize = DIP" + Windows: "wxsize = screen unit" + cross-platform: images are in "screen unit" */ + +inline +double getScreenDpiScale() +{ + //GTK2 doesn't properly support high DPI: https://freefilesync.org/forum/viewtopic.php?t=6114 + //=> requires general fix at wxWidgets-level + + //https://github.com/wxWidgets/wxWidgets/blob/d9d05c2bb201078f5e762c42458ca2f74af5b322/include/wx/window.h#L2060 + const double scale = 1.0; //e.g. macOS, GTK3 + + return scale; +} + + +inline +double getWxsizeDpiScale() +{ +#ifndef wxHAS_DPI_INDEPENDENT_PIXELS +#error why is wxHAS_DPI_INDEPENDENT_PIXELS not defined? +#endif + return 1.0; //e.g. macOS, GTK3 +} + + +//similar to wxWindow::FromDIP (but tied to primary monitor and buffered) +inline int dipToWxsize (int d) { return std::round(d * getWxsizeDpiScale() - 0.1 /*round values like 1.5 down => 1 pixel on 150% scale*/); } +inline int dipToScreen (int d) { return std::round(d * getScreenDpiScale()); } +inline int wxsizeToScreen(int u) { return std::round(u / getWxsizeDpiScale() * getScreenDpiScale()); } +inline int screenToWxsize(int s) { return std::round(s / getScreenDpiScale() * getWxsizeDpiScale()); } + +int dipToWxsize (double d) = delete; +int dipToScreen (double d) = delete; +int wxsizeToScreen(double d) = delete; +int screenToWxsize(double d) = delete; + + +inline +int getDpiScalePercent() +{ + return std::round(100 * getScreenDpiScale()); +} + + +inline +wxBitmap toScaledBitmap(const wxImage& img /*expected to be DPI-scaled!*/) +{ + //wxBitmap(const wxImage& image, int depth = -1, double WXUNUSED(scale) = 1.0) => wxWidgets just ignores scale parameter! WTF! + wxBitmap bmpScaled(img); + bmpScaled.SetScaleFactor(getScreenDpiScale()); + return bmpScaled; //when testing use 175% scaling: wxWidgets' scaling logic doesn't kick in for 150% only +} + + +//all this shit just because wxDC::SetScaleFactor() is missing: +inline +void setScaleFactor(wxDC& dc, double scale) +{ + struct wxDcSurgeon : public wxDCImpl + { + void setScaleFactor(double scale) { m_contentScaleFactor = scale; } + }; + static_cast(dc.GetImpl())->setScaleFactor(scale); +} + + +//add some sanity to moronic const/non-const wxRect::Intersect() +inline +wxRect getIntersection(const wxRect& rect1, const wxRect& rect2) +{ + return rect1.Intersect(rect2); +} + + +//---------------------- implementation ------------------------ +class RecursiveDcClipper //wxDCClipper does *not* stack => fix for yet another poor wxWidgets implementation: +{ +public: + RecursiveDcClipper(wxDC& dc, const wxRect& r) : dc_(dc) + { + if (auto it = clippingAreas_.find(&dc); + it != clippingAreas_.end()) + { + oldRect_ = it->second; + + const wxRect tmp = getIntersection(r, *oldRect_); //better safe than sorry + assert(!tmp.IsEmpty()); //"setting an empty clipping region is equivalent to DestroyClippingRegion()" + + if (tmp != *oldRect_) + { + dc.SetClippingRegion(tmp); //new clipping region is intersection of given and previously set regions + it->second = tmp; + clippingDone = true; + } + } + else + { + const wxRect dcArea(dc.GetSize()); + + //since wxWidgets 3.3.0 the DC may be pre-clipped to wxDC::GetSize() or smaller (related to double-buffering) + //=> consider "no clipping" and "clipped to wxDC::GetSize()" equivalent! + wxRect rectClip; + if (dc.GetClippingBox(rectClip)) + { + rectClip = getIntersection(rectClip, dcArea); + if (rectClip != dcArea) + oldRect_ = rectClip; + } + + //caveat: actual clipping region is smaller when rect is partially outside the DC + //=> ensure consistency for validateClippingBuffer() + const wxRect tmp = getIntersection(r, oldRect_? *oldRect_ : dcArea); + assert(!tmp.IsEmpty()); + + if (tmp != (oldRect_? *oldRect_ : dcArea)) + { + dc.SetClippingRegion(tmp); + clippingAreas_.emplace(&dc, tmp); + clippingDone = true; + recursionBegin_ = true; + } + } + } + + ~RecursiveDcClipper() + { + if (clippingDone) + { + dc_.DestroyClippingRegion(); + if (oldRect_) + dc_.SetClippingRegion(*oldRect_); + + if (recursionBegin_) + clippingAreas_.erase(&dc_); + else + clippingAreas_[&dc_] = *oldRect_; + } + } + +private: + RecursiveDcClipper (const RecursiveDcClipper&) = delete; + RecursiveDcClipper& operator=(const RecursiveDcClipper&) = delete; + + + //associate "active" clipping area with each DC + inline static std::unordered_map clippingAreas_; + + bool recursionBegin_ = false; + bool clippingDone = false; + std::optional oldRect_; + wxDC& dc_; +}; + + +//fix wxBufferedPaintDC: happily fucks up for RTL layout by not drawing the first column (x = 0)! +class BufferedPaintDC : public wxMemoryDC +{ +public: + BufferedPaintDC(wxWindow& wnd, std::optional& buffer) : buffer_(buffer), paintDc_(&wnd) + { + assert(!wnd.IsDoubleBuffered()); + + const wxSize clientSize = wnd.GetClientSize(); + if (clientSize.GetWidth() > 0 && clientSize.GetHeight() > 0) //wxBitmap asserts this!! width can be 0; test case "Grid::CornerWin": compare both sides, then change config + { + if (!buffer_ || buffer->GetSize() != clientSize) + buffer.emplace(clientSize); + + if (buffer->GetScaleFactor() != wnd.GetDPIScaleFactor()) + buffer->SetScaleFactor(wnd.GetDPIScaleFactor()); + + SelectObject(*buffer); //copies scale factor from wxBitmap + + //note: wxPaintDC on wxGTK/wxMAC does not implement SetLayoutDirection()!!! => GetLayoutDirection() == wxLayout_Default + if (paintDc_.IsOk() && paintDc_.GetLayoutDirection() == wxLayout_RightToLeft) + SetLayoutDirection(wxLayout_RightToLeft); + } + else + buffer.reset(); + } + + ~BufferedPaintDC() + { + if (buffer_) + { + if (GetLayoutDirection() == wxLayout_RightToLeft) + { + paintDc_.SetLayoutDirection(wxLayout_LeftToRight); //work around bug in wxDC::Blit() + SetLayoutDirection(wxLayout_LeftToRight); // + } + + const wxPoint origin = GetDeviceOrigin(); + paintDc_.Blit(0, 0, buffer_->GetWidth(), buffer_->GetHeight(), this, -origin.x, -origin.y); + } + } + +private: + BufferedPaintDC (const BufferedPaintDC&) = delete; + BufferedPaintDC& operator=(const BufferedPaintDC&) = delete; + + std::optional& buffer_; + wxPaintDC paintDc_; +}; + + +//BufferedPaintDC if wxWindow::IsDoubleBuffered, wxPaintDC otherwise (= the proper C++ implementation wxAutoBufferedPaintDCFactory wished it had) +class DynBufPaintDC +{ +public: + DynBufPaintDC(wxWindow& wnd, std::optional& buffer) + { + assert(wnd.IsDoubleBuffered()); + dc_.emplace(&wnd); + } + + operator wxDC& () + { + if (wxPaintDC* dc = std::get_if(&dc_)) + return *dc; + return std::get(dc_); + } + +private: + std::variant dc_; +}; +} + +#endif //DC_H_4987123956832143243214 diff --git a/wx+/file_drop.cpp b/wx+/file_drop.cpp new file mode 100644 index 0000000..cd233c5 --- /dev/null +++ b/wx+/file_drop.cpp @@ -0,0 +1,75 @@ +// ***************************************************************************** +// * 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 "file_drop.h" +#include +#include +#include + + +using namespace zen; + + +namespace zen +{ +wxDEFINE_EVENT(EVENT_DROP_FILE, FileDropEvent); +} + + + + +namespace +{ +class WindowDropTarget : public wxFileDropTarget +{ +public: + explicit WindowDropTarget(const wxWindow& dropWindow) : dropWindow_(dropWindow) {} + +private: + wxDragResult OnDragOver(wxCoord x, wxCoord y, wxDragResult def) override + { + //why the FUCK I is drag & drop still working while showing another modal dialog!??? + //why the FUCK II is drag & drop working even when dropWindow is disabled!?? [Windows] => we can fix this + //why the FUCK III is dropWindow NOT disabled while showing another modal dialog!??? [macOS, Linux] => we CANNOT fix this: FUUUUUUUUUUUUUU... + if (!dropWindow_.IsEnabled()) + return wxDragNone; + + return wxFileDropTarget::OnDragOver(x, y, def); + } + + //"bool wxDropTarget::GetData() [...] This method may only be called from within OnData()." + //=> FUUUUUUUUUUUUUU........ a.k.a. no support for DragDropValidator during mouse hover! >:( + + bool OnDropFiles(wxCoord x, wxCoord y, const wxArrayString& fileArray) override + { + /*Linux, MTP: we get an empty file array + => switching to wxTextDropTarget won't help (much): we'd get the format + mtp://[usb:001,002]/Telefonspeicher/Folder/file.txt + instead of + /run/user/1000/gvfs/mtp:host=%5Busb%3A001%2C002%5D/Telefonspeicher/Folder/file.txt */ + + if (!dropWindow_.IsEnabled()) + return false; + + //wxPoint clientDropPos(x, y) + std::vector filePaths; + for (const wxString& file : fileArray) + filePaths.push_back(utfTo(file)); + + //create a custom event on drop window: execute event after file dropping is completed! (after mouse is released) + dropWindow_.GetEventHandler()->AddPendingEvent(FileDropEvent(filePaths)); + return true; + } + + const wxWindow& dropWindow_; +}; +} + + +void zen::setupFileDrop(wxWindow& dropWindow) +{ + dropWindow.SetDropTarget(new WindowDropTarget(dropWindow)); /*takes ownership*/ +} diff --git a/wx+/file_drop.h b/wx+/file_drop.h new file mode 100644 index 0000000..2474ae4 --- /dev/null +++ b/wx+/file_drop.h @@ -0,0 +1,45 @@ +// ***************************************************************************** +// * 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 FILE_DROP_H_09457802957842560325626 +#define FILE_DROP_H_09457802957842560325626 + +#include +#include +#include +#include + + +namespace zen +{ +/* register simple file drop event (without issue of freezing dialogs and without wxFileDropTarget overdesign) + CAVEAT: a drop target window must not be directly or indirectly contained within a wxStaticBoxSizer until the following wxGTK bug + is fixed. According to wxWidgets release cycles this is expected to be: never https://github.com/wxWidgets/wxWidgets/issues/2763 + + 1. setup a window to emit EVENT_DROP_FILE: + - simple file system paths: setupFileDrop + - any shell paths with validation: setupShellItemDrop + + 2. register events: + wnd.Bind(EVENT_DROP_FILE, [this](FileDropEvent& event) { onFilesDropped(event); }); */ +struct FileDropEvent; +wxDECLARE_EVENT(EVENT_DROP_FILE, FileDropEvent); + + +struct FileDropEvent : public wxEvent +{ + explicit FileDropEvent(const std::vector& droppedPaths) : wxEvent(0 /*winid*/, EVENT_DROP_FILE), itemPaths_(droppedPaths) {} + FileDropEvent* Clone() const override { return new FileDropEvent(*this); } + + const std::vector itemPaths_; +}; + + + +void setupFileDrop(wxWindow& dropWindow); +} + +#endif //FILE_DROP_H_09457802957842560325626 diff --git a/wx+/graph.cpp b/wx+/graph.cpp new file mode 100644 index 0000000..2f10541 --- /dev/null +++ b/wx+/graph.cpp @@ -0,0 +1,800 @@ +// ***************************************************************************** +// * 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 "graph.h" +#include +#include +#include +#include + +using namespace zen; + + +//TODO: support zoom via mouse wheel? + +namespace zen +{ +wxDEFINE_EVENT(EVENT_GRAPH_SELECTION, GraphSelectEvent); +} + + +double zen::nextNiceNumber(double blockSize) //round to next number which is a convenient to read block size +{ + if (blockSize <= 0) + return 0; + + const double k = std::floor(std::log10(blockSize)); + const double e = std::pow(10, k); + if (numeric::isNull(e)) + return 0; + const double a = blockSize / e; //blockSize = a * 10^k with a in [1, 10) + assert(1 <= a && a < 10); + + //have a look at leading two digits: "nice" numbers start with 1, 2, 2.5 and 5 + const double steps[] = {1, 2, 2.5, 5, 10}; + return e * numeric::roundToGrid(a, std::begin(steps), std::end(steps)); +} + + +namespace +{ +wxColor getDefaultColor(size_t pos) +{ + switch (pos % 10) + { + case 0: return { 0, 69, 134}; //blue + case 1: return {255, 66, 14}; //red + case 2: return {255, 211, 32}; //yellow + case 3: return { 87, 157, 28}; //green + case 4: return {126, 0, 33}; //royal + case 5: return {131, 202, 255}; //light blue + case 6: return { 49, 64, 4}; //dark green + case 7: return {174, 207, 0}; //light green + case 8: return { 75, 31, 111}; //purple + case 9: return {255, 149, 14}; //orange + } + assert(false); + return *wxBLACK; +} + + +class ConvertCoord //convert between screen and input data coordinates +{ +public: + ConvertCoord(double valMin, double valMax, size_t screenSize) : + min_(valMin), + scaleToReal_(screenSize == 0 ? 0 : (valMax - valMin) / screenSize), + scaleToScr_(numeric::isNull((valMax - valMin)) ? 0 : screenSize / (valMax - valMin)), + outOfBoundsLow_ (-1 * scaleToReal_ + valMin), + outOfBoundsHigh_((screenSize + 1) * scaleToReal_ + valMin) { if (outOfBoundsLow_ > outOfBoundsHigh_) std::swap(outOfBoundsLow_, outOfBoundsHigh_); } + + double screenToReal(double screenPos) const //map [0, screenSize] -> [valMin, valMax] + { + return screenPos * scaleToReal_ + min_; + } + double realToScreen(double realPos) const //return screen position in pixel (but with double precision!) + { + return (realPos - min_) * scaleToScr_; + } + int realToScreenRound(double realPos) const //returns -1 and screenSize + 1 if out of bounds! + { + //catch large double values: if double is larger than what int can represent => undefined behavior! + realPos = std::clamp(realPos, outOfBoundsLow_, outOfBoundsHigh_); + return std::round(realToScreen(realPos)); + } + +private: + const double min_; + const double scaleToReal_; + const double scaleToScr_; + + double outOfBoundsLow_; + double outOfBoundsHigh_; +}; + + +//enlarge value range to display to a multiple of a "useful" block size +//returns block cound +int widenRange(double& valMin, double& valMax, //in/out + int graphAreaSize, //in pixel + int optimalBlockSizePx, // + const LabelFormatter& labelFmt) +{ + if (graphAreaSize <= 0) return 0; + + const double minValRangePerBlock = (valMax - valMin) / graphAreaSize; + const double proposedValRangePerBlock = (valMax - valMin) * optimalBlockSizePx / graphAreaSize; + double valRangePerBlock = labelFmt.getOptimalBlockSize(proposedValRangePerBlock); + assert(numeric::isNull(proposedValRangePerBlock) || valRangePerBlock > minValRangePerBlock); + + if (numeric::isNull(valRangePerBlock)) //valMin == valMax or strange "optimal block size" + return 1; + + //don't allow sub-pixel blocks! => avoid erroneously high GDI render work load! + if (valRangePerBlock < minValRangePerBlock) + valRangePerBlock = std::ceil(minValRangePerBlock / valRangePerBlock) * valRangePerBlock; + + double blockMin = std::floor(valMin / valRangePerBlock); //store as double, not int: truncation possible, e.g. if valRangePerBlock == 1 + double blockMax = std::ceil (valMax / valRangePerBlock); // + int blockCount = std::round(blockMax - blockMin); + assert(blockCount >= 0); + + //handle valMin == valMax == integer + if (blockCount <= 0) + { + ++blockMax; + blockCount = 1; + } + + valMin = blockMin * valRangePerBlock; + valMax = blockMax * valRangePerBlock; + return blockCount; +} + + +void drawXLabel(wxDC& dc, double xMin, double xMax, int blockCount, const ConvertCoord& cvrtX, const wxRect& graphArea, + const wxRect& labelArea, const LabelFormatter& labelFmt, const wxColor& colGridLine) +{ + assert(graphArea.width == labelArea.width && graphArea.x == labelArea.x); + if (blockCount <= 0) + return; + + const double valRangePerBlock = (xMax - xMin) / blockCount; + + for (int i = 1; i < blockCount; ++i) + { + const double valX = xMin + i * valRangePerBlock; //step over raw data, not graph area pixels, to not lose precision + const int x = graphArea.x + cvrtX.realToScreenRound(valX); + + //draw grey vertical lines + clearArea(dc, {x - dipToWxsize(1) / 2, graphArea.y, dipToWxsize(1), graphArea.height}, colGridLine); + + //draw x axis labels + const wxString label = labelFmt.formatText(valX, valRangePerBlock); + const wxSize labelExtent = dc.GetMultiLineTextExtent(label); + dc.DrawText(label, wxPoint(x - labelExtent.GetWidth() / 2, labelArea.y + (labelArea.height - labelExtent.GetHeight()) / 2)); //center + } +} + + +void drawYLabel(wxDC& dc, double yMin, double yMax, int blockCount, const ConvertCoord& cvrtY, const wxRect& graphArea, + const wxRect& labelArea, const LabelFormatter& labelFmt, const wxColor& colGridLine) +{ + assert(graphArea.height == labelArea.height && graphArea.y == labelArea.y); + if (blockCount <= 0) + return; + + const double valRangePerBlock = (yMax - yMin) / blockCount; + + for (int i = 1; i < blockCount; ++i) + { + //draw grey horizontal lines + const double valY = yMin + i * valRangePerBlock; //step over raw data, not graph area pixels, to not lose precision + const int y = graphArea.y + cvrtY.realToScreenRound(valY); + + clearArea(dc, {graphArea.x, y - dipToWxsize(1) / 2, graphArea.width, dipToWxsize(1)}, colGridLine); + + //draw y axis labels + const wxString label = labelFmt.formatText(valY, valRangePerBlock); + const wxSize labelExtent = dc.GetMultiLineTextExtent(label); + dc.DrawText(label, wxPoint(labelArea.x + (labelArea.width - labelExtent.GetWidth()) / 2, y - labelExtent.GetHeight() / 2)); //center + } +} + + +void drawCornerText(wxDC& dc, const wxRect& graphArea, const wxString& txt, GraphCorner pos, const wxColor& colorText, const wxColor& colorBack) +{ + if (txt.empty()) return; + + const wxSize border(dipToWxsize(5), dipToWxsize(2)); + //it looks like wxDC::GetMultiLineTextExtent() precisely returns width, but too large a height: maybe they consider "text row height"? + + const wxSize boxExtent = dc.GetMultiLineTextExtent(txt) + 2 * border; + + wxPoint drawPos = graphArea.GetTopLeft(); + switch (pos) + { + case GraphCorner::topL: + break; + case GraphCorner::topR: + drawPos.x += graphArea.width - boxExtent.GetWidth(); + break; + case GraphCorner::bottomL: + drawPos.y += graphArea.height - boxExtent.GetHeight(); + break; + case GraphCorner::bottomR: + drawPos.x += graphArea.width - boxExtent.GetWidth(); + drawPos.y += graphArea.height - boxExtent.GetHeight(); + break; + } + + //add text shadow to improve readability: + wxDCTextColourChanger textColor(dc, colorBack); + dc.DrawText(txt, drawPos + border + wxSize(1, 1) /*better without dipToWxsize()?*/); + + textColor.Set(colorText); + dc.DrawText(txt, drawPos + border); +} + + +//calculate intersection of polygon with half-plane +template +void cutPoints(std::vector& curvePoints, std::vector& oobMarker, Function isInside, Function2 getIntersection, bool doPolygonCut) +{ + assert(curvePoints.size() == oobMarker.size()); + + if (curvePoints.size() != oobMarker.size() || curvePoints.empty()) return; + + auto isMarkedOob = [&](size_t index) { return oobMarker[index] != 0; }; //test if point is start of an OOB line + + std::vector curvePointsTmp; + std::vector oobMarkerTmp; + curvePointsTmp.reserve(curvePoints.size()); //allocating memory for these containers is one + oobMarkerTmp .reserve(oobMarker .size()); //of the more expensive operations of Graph2D! + + auto savePoint = [&](const CurvePoint& pt, bool markedOob) { curvePointsTmp.push_back(pt); oobMarkerTmp.push_back(markedOob); }; + + bool pointInside = isInside(curvePoints[0]); + if (pointInside) + savePoint(curvePoints[0], isMarkedOob(0)); + + for (size_t index = 1; index < curvePoints.size(); ++index) + { + if (isInside(curvePoints[index]) != pointInside) + { + pointInside = !pointInside; + const CurvePoint is = getIntersection(curvePoints[index - 1], curvePoints[index]); //getIntersection returns "to" when delta is zero + savePoint(is, !pointInside || isMarkedOob(index - 1)); + } + if (pointInside) + savePoint(curvePoints[index], isMarkedOob(index)); + } + + //make sure the output polygon area is correctly shaped if either begin or end points are cut + if (doPolygonCut) //note: impacts min/max height-calculations! + if (curvePoints.size() >= 3) + if (isInside(curvePoints.front()) != pointInside) + { + assert(!oobMarkerTmp.empty()); + oobMarkerTmp.back() = true; + + const CurvePoint is = getIntersection(curvePoints.back(), curvePoints.front()); + savePoint(is, true); + } + + curvePointsTmp.swap(curvePoints); + oobMarkerTmp .swap(oobMarker); +} + + +struct GetIntersectionX +{ + explicit GetIntersectionX(double x) : x_(x) {} + + CurvePoint operator()(const CurvePoint& from, const CurvePoint& to) const + { + const double deltaX = to.x - from.x; + const double deltaY = to.y - from.y; + return numeric::isNull(deltaX) ? to : CurvePoint{x_, from.y + (x_ - from.x) / deltaX * deltaY}; + } + +private: + const double x_; +}; + +struct GetIntersectionY +{ + explicit GetIntersectionY(double y) : y_(y) {} + + CurvePoint operator()(const CurvePoint& from, const CurvePoint& to) const + { + const double deltaX = to.x - from.x; + const double deltaY = to.y - from.y; + return numeric::isNull(deltaY) ? to : CurvePoint{from.x + (y_ - from.y) / deltaY * deltaX, y_}; + } + +private: + const double y_; +}; + +void cutPointsOutsideX(std::vector& curvePoints, std::vector& oobMarker, double minX, double maxX, bool doPolygonCut) +{ + cutPoints(curvePoints, oobMarker, [&](const CurvePoint& pt) { return pt.x >= minX; }, GetIntersectionX(minX), doPolygonCut); + cutPoints(curvePoints, oobMarker, [&](const CurvePoint& pt) { return pt.x <= maxX; }, GetIntersectionX(maxX), doPolygonCut); +} + +void cutPointsOutsideY(std::vector& curvePoints, std::vector& oobMarker, double minY, double maxY, bool doPolygonCut) +{ + cutPoints(curvePoints, oobMarker, [&](const CurvePoint& pt) { return pt.y >= minY; }, GetIntersectionY(minY), doPolygonCut); + cutPoints(curvePoints, oobMarker, [&](const CurvePoint& pt) { return pt.y <= maxY; }, GetIntersectionY(maxY), doPolygonCut); +} +} + + +std::vector ContinuousCurveData::getPoints(double minX, double maxX, const wxSize& areaSizePx) const +{ + std::vector points; + + const int pixelWidth = areaSizePx.GetWidth(); + if (pixelWidth <= 1) return points; + const ConvertCoord cvrtX(minX, maxX, pixelWidth - 1); //map [minX, maxX] to [0, pixelWidth - 1] + + const std::pair rangeX = getRangeX(); + + const double screenLow = cvrtX.realToScreen(std::max(rangeX.first, minX)); //=> xLow >= 0 + const double screenHigh = cvrtX.realToScreen(std::min(rangeX.second, maxX)); //=> xHigh <= pixelWidth - 1 + //if double is larger than what int can represent => undefined behavior! + //=> convert to int *after* checking value range! + if (screenLow <= screenHigh) + { + const int posFrom = std::ceil (screenLow ); //do not step outside [minX, maxX] in loop below! + const int posTo = std::floor(screenHigh); // + //conversion from std::floor/std::ceil double return value to int is loss-free for full value range of 32-bit int! tested successfully on MSVC + + for (int i = posFrom; i <= posTo; ++i) + { + const double x = cvrtX.screenToReal(i); + points.emplace_back(CurvePoint{x, getValue(x)}); + } + } + return points; +} + + +std::vector SparseCurveData::getPoints(double minX, double maxX, const wxSize& areaSizePx) const +{ + std::vector points; + + const int pixelWidth = areaSizePx.GetWidth(); + if (pixelWidth <= 1) return points; + const ConvertCoord cvrtX(minX, maxX, pixelWidth - 1); //map [minX, maxX] to [0, pixelWidth - 1] + const std::pair rangeX = getRangeX(); + + auto addPoint = [&](const CurvePoint& pt) + { + if (!points.empty()) + { + if (pt.x <= points.back().x) //allow ascending x-positions only! algorithm below may cause double-insertion after empty x-ranges! + return; + + if (addSteps_) + if (pt.y != points.back().y) + points.emplace_back(CurvePoint{pt.x, points.back().y}); //[!] aliasing parameter not yet supported via emplace_back: VS bug! => make copy + } + points.push_back(pt); + }; + + const int posFrom = cvrtX.realToScreenRound(std::max(rangeX.first, minX)); + const int posTo = cvrtX.realToScreenRound(std::min(rangeX.second, maxX)); + + for (int i = posFrom; i <= posTo; ++i) + { + const double x = cvrtX.screenToReal(i); + std::optional ptLe = getLessEq(x); + std::optional ptGe = getGreaterEq(x); + //both non-existent and invalid return values are mapped to out of expected range: => check on posLe/posGe NOT ptLe/ptGe in the following! + const int posLe = ptLe ? cvrtX.realToScreenRound(ptLe->x) : i + 1; + const int posGe = ptGe ? cvrtX.realToScreenRound(ptGe->x) : i - 1; + assert(!ptLe || posLe <= i); //check for invalid return values + assert(!ptGe || posGe >= i); // + + /* Breakdown of all combinations of posLe, posGe and expected action (n >= 1) + Note: For every empty x-range of at least one pixel, both next and previous points must be saved to keep the interpolating line stable!!! + + posLe | posGe | action + +-------+-------+-------- + | none | none | break + | i | none | save ptLe; break + | i - n | none | break; + +-------+-------+-------- + | none | i | save ptGe; continue + | i | i | save one of ptLe, ptGe; continue + | i - n | i | save ptGe; continue + +-------+-------+-------- + | none | i + n | save ptGe; jump to position posGe + 1 + | i | i + n | save ptLe; if n == 1: continue; else: save ptGe; jump to position posGe + 1 + | i - n | i + n | save ptLe, ptGe; jump to position posGe + 1 + +-------+-------+-------- */ + if (posGe < i) + { + if (posLe == i) + addPoint(*ptLe); + break; + } + else if (posGe == i) //test if point would be mapped to pixel x-position i + { + if (posLe == i) // + addPoint(x - ptLe->x < ptGe->x - x ? *ptLe : *ptGe); + else + addPoint(*ptGe); + } + else + { + if (posLe <= i) + addPoint(*ptLe); + + if (posLe != i || posGe > i + 1) + { + addPoint(*ptGe); + i = posGe; //skip sparse area: +1 will be added by for-loop! + } + } + } + return points; +} + + +Graph2D::Graph2D(wxWindow* parent, + wxWindowID winid, + const wxPoint& pos, + const wxSize& size, + long style, + const wxString& name) : wxPanel(parent, winid, pos, size, style, name) +{ + //https://wiki.wxwidgets.org/Flicker-Free_Drawing + SetBackgroundStyle(wxBG_STYLE_PAINT); //get's rid of needless wxEVT_ERASE_BACKGROUND + Bind(wxEVT_PAINT, [this](wxPaintEvent& event) { onPaintEvent(event); }); + Bind(wxEVT_SIZE, [this](wxSizeEvent& event) { Refresh(); event.Skip(); }); + + //perf: WS_EX_COMPOSITED vs BufferedPaintDC doesn't seem to matter. Even for 200 FPS graph, CPU consumption is barely noticeable! + //MSWDisableComposited(); -> see comment in grid.cpp + + Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent& event) { onMouseLeftDown(event); }); + Bind(wxEVT_MOTION, [this](wxMouseEvent& event) { onMouseMovement(event); }); + Bind(wxEVT_LEFT_UP, [this](wxMouseEvent& event) { onMouseLeftUp (event); }); + Bind(wxEVT_MOUSE_CAPTURE_LOST, [this](wxMouseCaptureLostEvent& event) { onMouseCaptureLost(event); }); +} + + +void Graph2D::onPaintEvent(wxPaintEvent& event) +{ + DynBufPaintDC dc(*this, doubleBuffer_); + render(dc); +} + + +void Graph2D::onMouseLeftDown(wxMouseEvent& event) +{ + activeSel_ = std::make_unique(*this, event.GetPosition()); + + if (!event.ControlDown()) + oldSel_.clear(); + Refresh(); +} + + +void Graph2D::onMouseMovement(wxMouseEvent& event) +{ + if (activeSel_.get()) + { + activeSel_->refCurrentPos() = event.GetPosition(); //corresponding activeSel->refSelection() is updated in Graph2D::render() + Refresh(); + } +} + + +void Graph2D::onMouseLeftUp(wxMouseEvent& event) +{ + if (activeSel_.get()) + { + if (activeSel_->getStartPos() != activeSel_->refCurrentPos()) //if it's just a single mouse click: discard selection + { + GetEventHandler()->AddPendingEvent(GraphSelectEvent(activeSel_->refSelection())); + + oldSel_.push_back(activeSel_->refSelection()); //commit selection + } + + activeSel_.reset(); + Refresh(); + } +} + + +void Graph2D::onMouseCaptureLost(wxMouseCaptureLostEvent& event) +{ + activeSel_.reset(); + Refresh(); +} + + +void Graph2D::addCurve(const SharedRef& data, const CurveAttributes& ca) +{ + CurveAttributes newAttr = ca; + if (newAttr.autoColor) + newAttr.setColor(getDefaultColor(curves_.size())); + curves_.emplace_back(data, newAttr); + Refresh(); +} + + +void Graph2D::render(wxDC& dc) const +{ + //set label font right at the start so that it is considered by wxDC::GetTextExtent() below! + dc.SetFont(GetFont()); + dc.SetTextForeground(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); + + const wxRect clientRect = GetClientRect(); //DON'T use wxDC::GetSize()! DC may be larger than visible area! + + clearArea(dc, clientRect, GetBackgroundColour() /*user-configurable!*/); + //wxPanel::GetClassDefaultAttributes().colBg : + //wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE); + + const int xLabelHeight = attr_.xLabelHeight ? *attr_.xLabelHeight : GetCharHeight() + dipToWxsize(2) /*margin*/; + const int yLabelWidth = attr_.yLabelWidth ? *attr_.yLabelWidth : dc.GetTextExtent(L"1.23457e+07").x; + + /* ----------------------- + | | x-label | + ----------------------- + |y-label | graph area | + |---------------------- */ + + wxRect graphArea = clientRect; + int xLabelPosY = clientRect.y; + int yLabelPosX = clientRect.x; + + switch (attr_.xLabelpos) + { + case XLabelPos::none: + break; + case XLabelPos::top: + graphArea.y += xLabelHeight; + graphArea.height -= xLabelHeight; + break; + case XLabelPos::bottom: + xLabelPosY += clientRect.height - xLabelHeight; + graphArea.height -= xLabelHeight; + break; + } + switch (attr_.yLabelpos) + { + case YLabelPos::none: + break; + case YLabelPos::left: + graphArea.x += yLabelWidth; + graphArea.width -= yLabelWidth; + break; + case YLabelPos::right: + yLabelPosX += clientRect.width - yLabelWidth; + graphArea.width -= yLabelWidth; + break; + } + + assert(attr_.xLabelpos == XLabelPos::none || attr_.labelFmtX); + assert(attr_.yLabelpos == YLabelPos::none || attr_.labelFmtY); + + //paint graph background (excluding label area) + drawFilledRectangle(dc, graphArea, attr_.colorBack, attr_.colorGridLine, dipToWxsize(1)); + graphArea.Deflate(dipToWxsize(1)); + + //set label areas respecting graph area border! + const wxRect xLabelArea(graphArea.x, xLabelPosY, graphArea.width, xLabelHeight); + const wxRect yLabelArea(yLabelPosX, graphArea.y, yLabelWidth, graphArea.height); + + //detect x value range + double minX = attr_.minX ? *attr_.minX : std::numeric_limits::infinity(); //automatic: ensure values are initialized by first curve + double maxX = attr_.maxX ? *attr_.maxX : -std::numeric_limits::infinity(); // + for (const auto& [curve, attrib] : curves_) + { + const std::pair rangeX = curve.ref().getRangeX(); + assert(rangeX.first <= rangeX.second + 1.0e-9); + //GCC fucks up badly when comparing two *binary identical* doubles and finds "begin > end" with diff of 1e-18 + + if (!attr_.minX) + minX = std::min(minX, rangeX.first); + if (!attr_.maxX) + maxX = std::max(maxX, rangeX.second); + } + + if (minX <= maxX && maxX - minX < std::numeric_limits::infinity()) //valid x-range + { + const wxSize minimalBlockSizePx = dc.GetTextExtent(L"00"); + + int blockCountX = 0; + //enlarge minX, maxX to a multiple of a "useful" block size + if (attr_.xLabelpos != XLabelPos::none && attr_.labelFmtX.get()) + blockCountX = widenRange(minX, maxX, //in/out + graphArea.width, + minimalBlockSizePx.GetWidth() * 7, + *attr_.labelFmtX); + + //get raw values + detect y value range + double minY = attr_.minY ? *attr_.minY : std::numeric_limits::infinity(); //automatic: ensure values are initialized by first curve + double maxY = attr_.maxY ? *attr_.maxY : -std::numeric_limits::infinity(); // + + std::vector> curvePoints(curves_.size()); + std::vector> oobMarker (curves_.size()); //effectively a std::vector marking points that start an out-of-bounds line + + for (size_t index = 0; index < curves_.size(); ++index) + { + const CurveData& curve = curves_ [index].first.ref(); + std::vector& points = curvePoints[index]; + auto& marker = oobMarker [index]; + + points = curve.getPoints(minX, maxX, graphArea.GetSize()); + marker.resize(points.size()); //default value: false + if (!points.empty()) + { + //cut points outside visible x-range now in order to calculate height of visible line fragments only! + const bool doPolygonCut = curves_[index].second.fillMode == CurveFillMode::polygon; //impacts auto minY/maxY!! + cutPointsOutsideX(points, marker, minX, maxX, doPolygonCut); + + if (!attr_.minY || !attr_.maxY) + { + const auto& [itMin, itMax] = std::minmax_element(points.begin(), points.end(), [](const CurvePoint& lhs, const CurvePoint& rhs) { return lhs.y < rhs.y; }); + if (!attr_.minY) + minY = std::min(minY, itMin->y); + if (!attr_.maxY) + maxY = std::max(maxY, itMax->y); + } + } + } + + if (minY <= maxY) //valid y-range + { + int blockCountY = 0; + //enlarge minY, maxY to a multiple of a "useful" block size + if (attr_.yLabelpos != YLabelPos::none && attr_.labelFmtY.get()) + blockCountY = widenRange(minY, maxY, //in/out + graphArea.height, + minimalBlockSizePx.GetHeight() * 3, + *attr_.labelFmtY); + + if (graphArea.width <= 1 || graphArea.height <= 1) + return; + + const ConvertCoord cvrtX(minX, maxX, graphArea.width - 1); //map [minX, maxX] to [0, pixelWidth - 1] + const ConvertCoord cvrtY(maxY, minY, graphArea.height - 1); //map [minY, maxY] to [pixelHeight - 1, 0] + + //calculate curve coordinates on graph area + std::vector> drawPoints(curves_.size()); + + for (size_t index = 0; index < curves_.size(); ++index) + { + auto& cp = curvePoints[index]; + + //add two artificial points to fill the curve area towards x-axis => do this before cutPointsOutsideY() to handle curve leaving upper bound + if (curves_[index].second.fillMode == CurveFillMode::curve) + if (!cp.empty()) + { + cp.emplace_back(CurvePoint{cp.back ().x, minY}); //add lower right and left corners + cp.emplace_back(CurvePoint{cp.front().x, minY}); //[!] aliasing parameter not yet supported via emplace_back: VS bug! => make copy + oobMarker[index].back() = true; + oobMarker[index].push_back(true); + oobMarker[index].push_back(true); + } + + //cut points outside visible y-range before calculating pixels: + //1. realToScreenRound() deforms out-of-range values! + //2. pixels that are grossly out of range can be a severe performance problem when drawing on the DC (Windows) + const bool doPolygonCut = curves_[index].second.fillMode != CurveFillMode::none; + cutPointsOutsideY(cp, oobMarker[index], minY, maxY, doPolygonCut); + + auto& dp = drawPoints[index]; + for (const CurvePoint& pt : cp) + dp.push_back(wxSize(cvrtX.realToScreenRound(pt.x), + cvrtY.realToScreenRound(pt.y)) + graphArea.GetTopLeft()); + } + + //update active mouse selection + if (activeSel_) + { + wxPoint screenFrom = activeSel_->getStartPos() - graphArea.GetTopLeft(); //make relative to graphArea + wxPoint screenTo = activeSel_->refCurrentPos() - graphArea.GetTopLeft(); + + //normalize positions: + screenFrom.x = std::clamp(screenFrom.x, 0, graphArea.width - 1); + screenFrom.y = std::clamp(screenFrom.y, 0, graphArea.height - 1); + screenTo .x = std::clamp(screenTo .x, 0, graphArea.width - 1); + screenTo .y = std::clamp(screenTo .y, 0, graphArea.height - 1); + + //save current selection as "double" coordinates + activeSel_->refSelection().from = CurvePoint{cvrtX.screenToReal(screenFrom.x), + cvrtY.screenToReal(screenFrom.y)}; + + activeSel_->refSelection().to = CurvePoint{cvrtX.screenToReal(screenTo.x), + cvrtY.screenToReal(screenTo.y)}; + } + + //#################### begin drawing #################### + //1. draw colored area under curves + for (auto it = curves_.begin(); it != curves_.end(); ++it) + if (it->second.fillMode != CurveFillMode::none) + if (const std::vector& points = drawPoints[it - curves_.begin()]; + points.size() >= 3) + { + //wxDC::DrawPolygon() draws *transparent* border if wxTRANSPARENT_PEN is used! + //unlike wxDC::DrawRectangle() which widens inner area instead! + dc.SetPen ({it->second.fillColor, 1 /*[!] width*/}); + dc.SetBrush(it->second.fillColor); + dc.DrawPolygon(static_cast(points.size()), points.data()); + } + + //2. draw all currently set mouse selections (including active selection) + std::vector allSelections = oldSel_; + if (activeSel_) + allSelections.push_back(activeSel_->refSelection()); + + if (!allSelections.empty()) + { + const wxColor innerCol(168, 202, 236); //light blue + const wxColor borderCol(51, 153, 255); //dark blue + + //alpha channel not supported on wxMSW, so draw selection before curves + for (const SelectionBlock& sel : allSelections) + { + //harmonize with active mouse selection above + int screenFromX = cvrtX.realToScreenRound(sel.from.x); + int screenFromY = cvrtY.realToScreenRound(sel.from.y); + int screenToX = cvrtX.realToScreenRound(sel.to.x); + int screenToY = cvrtY.realToScreenRound(sel.to.y); + + if (screenFromX > screenToX) std::swap(screenFromX, screenToX); + if (screenFromY > screenToY) std::swap(screenFromY, screenToY); + + const wxRect rectSel{graphArea.GetTopLeft() + wxSize(screenFromX, + screenFromY), + wxSize(screenToX - screenFromX + 1, //mouse selection is symmetric + screenToY - screenFromY + 1)}; //and *not* a half-open range! + switch (attr_.mouseSelMode) + { + case GraphSelMode::none: + break; + case GraphSelMode::rect: + drawFilledRectangle(dc, rectSel, innerCol, borderCol, dipToWxsize(1)); + break; + case GraphSelMode::x: + drawFilledRectangle(dc, {rectSel.x, graphArea.y, rectSel.width, graphArea.height}, innerCol, borderCol, dipToWxsize(1)); + break; + case GraphSelMode::y: + drawFilledRectangle(dc, {graphArea.x, rectSel.y, graphArea.width, rectSel.height}, innerCol, borderCol, dipToWxsize(1)); + break; + } + } + } + + //3. draw labels and background grid + if (attr_.labelFmtX) drawXLabel(dc, minX, maxX, blockCountX, cvrtX, graphArea, xLabelArea, *attr_.labelFmtX, attr_.colorGridLine); + if (attr_.labelFmtY) drawYLabel(dc, minY, maxY, blockCountY, cvrtY, graphArea, yLabelArea, *attr_.labelFmtY, attr_.colorGridLine); + + //4. finally draw curves + { + dc.SetClippingRegion(graphArea); //prevent thick curves from drawing slightly outside + ZEN_ON_SCOPE_EXIT(dc.DestroyClippingRegion()); + + for (auto it = curves_.begin(); it != curves_.end(); ++it) + { + dc.SetPen({it->second.color, it->second.lineWidth}); + + const size_t index = it - curves_.begin(); + const std::vector& points = drawPoints[index]; + const auto& marker = oobMarker [index]; + assert(points.size() == marker.size()); + + //draw all parts of the curve except for the out-of-bounds fragments + size_t drawIndexFirst = 0; + while (drawIndexFirst < points.size()) + { + size_t drawIndexLast = std::find(marker.begin() + drawIndexFirst, marker.end(), static_cast(true)) - marker.begin(); + if (drawIndexLast < points.size()) ++drawIndexLast; + + const int pointCount = static_cast(drawIndexLast - drawIndexFirst); + if (pointCount > 0) + { + if (pointCount >= 2) //on macOS wxWidgets has a nasty assert on this + dc.DrawLines(pointCount, &points[drawIndexFirst]); + dc.DrawPoint(points[drawIndexLast - 1]); //wxDC::DrawLines() doesn't draw last pixel + } + drawIndexFirst = std::find(marker.begin() + drawIndexLast, marker.end(), static_cast(false)) - marker.begin(); + } + } + } + + //5. draw corner texts + for (const auto& [cornerPos, text] : attr_.cornerTexts) + drawCornerText(dc, graphArea, text, cornerPos, attr_.colorText, attr_.colorBack); + } + } +} diff --git a/wx+/graph.h b/wx+/graph.h new file mode 100644 index 0000000..ba35a08 --- /dev/null +++ b/wx+/graph.h @@ -0,0 +1,346 @@ +// ***************************************************************************** +// * 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 GRAPH_H_234425245936567345799 +#define GRAPH_H_234425245936567345799 + +#include +#include +#include +#include +#include +#include +#include +#include "color_tools.h" +#include "dc.h" + + +//elegant 2D graph as wxPanel specialization +namespace zen +{ +/* //init graph (optional) + m_panelGraph->setAttributes(Graph2D::MainAttributes(). + setLabelX(Graph2D::LABEL_X_BOTTOM, 20, std::make_shared()). + setLabelY(Graph2D::LABEL_Y_RIGHT, 60, std::make_shared())); + //set graph data + SharedRef curveDataBytes_ = ... + m_panelGraph->setCurve(curveDataBytes_, Graph2D::CurveAttributes().setLineWidth(2).setColor(wxColor(0, 192, 0))); */ + +struct CurvePoint +{ + double x = 0; + double y = 0; +}; + + +struct CurveData +{ + virtual ~CurveData() {} + + virtual std::pair getRangeX() const = 0; + virtual std::vector getPoints(double minX, double maxX, const wxSize& areaSizePx) const = 0; //points outside the draw area are automatically trimmed! +}; + +//special curve types: +struct ContinuousCurveData : public CurveData +{ + virtual double getValue(double x) const = 0; + +private: + std::vector getPoints(double minX, double maxX, const wxSize& areaSizePx) const override; +}; + +struct SparseCurveData : public CurveData +{ + explicit SparseCurveData(bool addSteps = false) : addSteps_(addSteps) {} //addSteps: add points to get a staircase effect or connect points via a direct line + + virtual std::optional getLessEq (double x) const = 0; + virtual std::optional getGreaterEq(double x) const = 0; + +private: + std::vector getPoints(double minX, double maxX, const wxSize& areaSizePx) const override; + const bool addSteps_; +}; + + +struct ArrayCurveData : public SparseCurveData +{ + virtual double getValue(size_t pos) const = 0; + virtual size_t getSize () const = 0; + +private: + std::pair getRangeX() const override { const size_t sz = getSize(); return { 0.0, sz == 0 ? 0.0 : sz - 1.0}; } + + std::optional getLessEq(double x) const override + { + const size_t sz = getSize(); + const size_t pos = std::min(std::floor(x), sz - 1); //[!] expect unsigned underflow if empty! + if (pos < sz) + return CurvePoint{1.0 * pos, getValue(pos)}; + return {}; + } + + std::optional getGreaterEq(double x) const override + { + const size_t pos = std::max(std::ceil(x), 0); //[!] use std::max with signed type! + if (pos < getSize()) + return CurvePoint{1.0 * pos, getValue(pos)}; + return {}; + } +}; + + +struct VectorCurveData : public ArrayCurveData +{ + std::vector& refData() { return data_; } +private: + double getValue(size_t pos) const override { return pos < data_.size() ? data_[pos] : 0; } + size_t getSize() const override { return data_.size(); } + + std::vector data_; +}; + +//------------------------------------------------------------------------------------------------------------ + +struct LabelFormatter +{ + virtual ~LabelFormatter() {} + + //determine convenient graph label block size in unit of data: usually some small deviation on "sizeProposed" + virtual double getOptimalBlockSize(double sizeProposed) const = 0; + + //create human-readable text for x or y-axis position + virtual wxString formatText(double value, double optimalBlockSize) const = 0; +}; + + +double nextNiceNumber(double blockSize); //round to next number which is convenient to read, e.g. 2.13 -> 2; 2.7 -> 2.5 + +struct DecimalNumberFormatter : public LabelFormatter +{ + double getOptimalBlockSize(double sizeProposed ) const override { return nextNiceNumber(sizeProposed); } + wxString formatText (double value, double optimalBlockSize) const override { return numberTo(value); } +}; + +//------------------------------------------------------------------------------------------------------------ +//example: wnd.Bind(EVENT_GRAPH_SELECTION, [this](GraphSelectEvent& event) { onGraphSelect(event); }); + +struct GraphSelectEvent; +wxDECLARE_EVENT(EVENT_GRAPH_SELECTION, GraphSelectEvent); + + +struct SelectionBlock +{ + CurvePoint from; + CurvePoint to; +}; + +struct GraphSelectEvent : public wxEvent +{ + explicit GraphSelectEvent(const SelectionBlock& selBlock) : wxEvent(0 /*winid*/, EVENT_GRAPH_SELECTION), selectBlock_(selBlock) {} + GraphSelectEvent* Clone() const override { return new GraphSelectEvent(*this); } + + SelectionBlock selectBlock_; +}; + +//------------------------------------------------------------------------------------------------------------ +enum class XLabelPos +{ + none, + top, + bottom, +}; + +enum class YLabelPos +{ + none, + left, + right, +}; + +enum class CurveFillMode +{ + none, + curve, + polygon +}; + +enum class GraphCorner +{ + topL, + topR, + bottomL, + bottomR, +}; + +enum class GraphSelMode +{ + none, + rect, + x, + y, +}; +//------------------------------------------------------------------------------------------------------------ + +class Graph2D : public wxPanel +{ +public: + Graph2D(wxWindow* parent, + wxWindowID winid = wxID_ANY, + const wxPoint& pos = wxDefaultPosition, + const wxSize& size = wxDefaultSize, + long style = wxTAB_TRAVERSAL | wxNO_BORDER, + const wxString& name = wxASCII_STR(wxPanelNameStr)); + + class CurveAttributes + { + public: + CurveAttributes() {} //required by GCC + CurveAttributes& setColor (const wxColor& col) { color = col; autoColor = false; return *this; } + CurveAttributes& fillCurveArea (const wxColor& col) { fillColor = col; fillMode = CurveFillMode::curve; return *this; } + CurveAttributes& fillPolygonArea(const wxColor& col) { fillColor = col; fillMode = CurveFillMode::polygon; return *this; } + CurveAttributes& setLineWidth(size_t width) { lineWidth = static_cast(width); return *this; } + + private: + friend class Graph2D; + + bool autoColor = true; + wxColor color; + + CurveFillMode fillMode = CurveFillMode::none; + wxColor fillColor; + + int lineWidth = dipToWxsize(2); + }; + + void addCurve(const SharedRef& data, const CurveAttributes& ca = CurveAttributes()); + void clearCurves() { curves_.clear(); } + + class MainAttributes + { + public: + MainAttributes() + { + //accessibility: consider system text and background colors; + //small drawback: color of graphs is NOT related to the background! => responsibility of client to use correct colors + setBaseColors(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT), + wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); + } + MainAttributes& setMinX(double newMinX) { minX = newMinX; return *this; } + MainAttributes& setMaxX(double newMaxX) { maxX = newMaxX; return *this; } + + MainAttributes& setMinY(double newMinY) { minY = newMinY; return *this; } + MainAttributes& setMaxY(double newMaxY) { maxY = newMaxY; return *this; } + + MainAttributes& setAutoSize() { minX = maxX = minY = maxY = {}; return *this; } + + MainAttributes& setLabelX(XLabelPos posX, int height = -1, std::shared_ptr newLabelFmt = nullptr) + { + xLabelpos = posX; + if (height >= 0) xLabelHeight = height; + if (newLabelFmt) labelFmtX = newLabelFmt; + return *this; + } + MainAttributes& setLabelY(YLabelPos posY, int width = -1, std::shared_ptr newLabelFmt = nullptr) + { + yLabelpos = posY; + if (width >= 0) yLabelWidth = width; + if (newLabelFmt) labelFmtY = newLabelFmt; + return *this; + } + + MainAttributes& setCornerText(const wxString& txt, GraphCorner pos) { cornerTexts[pos] = txt; return *this; } + + MainAttributes& setBaseColors(const wxColor& text, const wxColor& back) //accessibility: always set both colors + { + colorText = text; + colorBack = back; + colorGridLine = enhanceContrast(colorBack, //start with back color and deviate only as little as required + colorBack, 4 /*contrastRatioMin*/); //W3C recommends >= 4.5 for text + return *this; + } + + wxColor getGridLineColor() const { return colorGridLine; } + + MainAttributes& setSelectionMode(GraphSelMode mode) { mouseSelMode = mode; return *this; } + + private: + friend class Graph2D; + + std::optional minX; //x-range to visualize + std::optional maxX; // + + std::optional minY; //y-range to visualize + std::optional maxY; // + + XLabelPos xLabelpos = XLabelPos::bottom; + std::optional xLabelHeight; + std::shared_ptr labelFmtX = std::make_shared(); + + YLabelPos yLabelpos = YLabelPos::left; + std::optional yLabelWidth; + std::shared_ptr labelFmtY = std::make_shared(); + + std::map cornerTexts; + + wxColor colorText; + wxColor colorBack; + wxColor colorGridLine; + + GraphSelMode mouseSelMode = GraphSelMode::rect; + }; + + void setAttributes(const MainAttributes& newAttr) { attr_ = newAttr; Refresh(); } + MainAttributes getAttributes() const { return attr_; } + + std::vector getSelections() const { return oldSel_; } + void setSelections(const std::vector& sel) + { + oldSel_ = sel; + activeSel_.reset(); + Refresh(); + } + void clearSelection() { oldSel_.clear(); Refresh(); } + +private: + void onMouseLeftDown(wxMouseEvent& event); + void onMouseMovement(wxMouseEvent& event); + void onMouseLeftUp (wxMouseEvent& event); + void onMouseCaptureLost(wxMouseCaptureLostEvent& event); + + void onPaintEvent(wxPaintEvent& event); + + void render(wxDC& dc) const; + + class MouseSelection + { + public: + MouseSelection(wxWindow& wnd, const wxPoint& posDragStart) : wnd_(wnd), posDragStart_(posDragStart), posDragCurrent(posDragStart) { wnd_.CaptureMouse(); } + ~MouseSelection() { if (wnd_.HasCapture()) wnd_.ReleaseMouse(); } + + wxPoint getStartPos() const { return posDragStart_; } + wxPoint& refCurrentPos() { return posDragCurrent; } + + SelectionBlock& refSelection() { return selBlock; } //updated in Graph2d::render(): this is fine, since only what's shown is selected! + + private: + wxWindow& wnd_; + const wxPoint posDragStart_; + wxPoint posDragCurrent; + SelectionBlock selBlock; + }; + std::vector oldSel_; //applied selections + std::unique_ptr activeSel_; //set during mouse selection + + MainAttributes attr_; //global attributes + + std::vector, CurveAttributes>> curves_; + + std::optional doubleBuffer_; +}; +} + +#endif //GRAPH_H_234425245936567345799 diff --git a/wx+/grid.cpp b/wx+/grid.cpp new file mode 100644 index 0000000..489272d --- /dev/null +++ b/wx+/grid.cpp @@ -0,0 +1,2440 @@ +// ***************************************************************************** +// * 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 "grid.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "color_tools.h" +#include "dc.h" + + #include + +using namespace zen; + +/* wxWidgets 3.3 defaults to system-powered double-buffering (WS_EX_COMPOSITED) on Windows: + => ~60% higher CPU time (test case: scrolling large file list via keyboard) see comment in file_grid.cpp :(( + + "wxMSW now uses double buffering by default, meaning that updating the + windows using wxClientDC doesn't work any longer, which is consistent with + the behaviour of wxGTK with Wayland backend and of wxOSX, but not with the + traditional historic behaviour of wxMSW (or wxGTK/X11). + You may call MSWDisableComposited() to restore the previous behaviour [...]" + + WS_EX_COMPOSITED "Paints all *descendants* of a window in bottom-to-top painting order using double-buffering." + + => can only be set for *top* window below the wxFrame/wxDialog otherwise SetWindowLongPtr(WS_EX_COMPOSITED) fails!!! + + => use MSWDisableComposited() to remove WS_EX_COMPOSITED from top window under wxFrame/wxDialog if perf issue. + SetDoubleBuffered(false) OTOH is useless as it doesn't affect parent windows and silently fails! + IsDoubleBuffered() however correctly checks parents: CONSISTENCY, people, for fucks sake! + + CAVEAT: MSWDisableComposited() leads to severe flickering for other child windows (e.g. wxStaticBitmap, wxBitmapButton) + that lack custom double-buffering. It's even worse since wxWidgets in its wisdom sets WS_EX_COMPOSITED + together with CS_HREDRAW/CS_VREDRAW, https://github.com/vadz/wxWidgets/blob/8de0694a5e9c9d7c24e0af2ccf71454df5e6b9d0/src/msw/window.cpp#L507 + and MSWDisableComposited() only removes former attribute. */ + + +//let's NOT create wxWidgets objects statically: +wxColor GridData::getColorSelectionGradientFrom() { return {137, 172, 255}; } //blue: HSL: 158, 255, 196 HSV: 222, 0.46, 1 +wxColor GridData::getColorSelectionGradientTo () { return {225, 234, 255}; } // HSL: 158, 255, 240 HSV: 222, 0.12, 1 + +int GridData::getColumnGapLeft() { return dipToWxsize(4); } + + +namespace +{ +//------------------------------ Grid Parameters -------------------------------- +wxColor getColorLabelText(bool enabled) { return wxSystemSettings::GetColour(enabled ? wxSYS_COLOUR_BTNTEXT : wxSYS_COLOUR_GRAYTEXT); } +wxColor getColorGridLine() { return wxSystemSettings::GetColour(wxSYS_COLOUR_BTNSHADOW); } + +wxColor getColorLabelGradientFrom() +{ + if (wxSystemSettings::GetAppearance().IsDark()) //upper gradient part must always be lighter than lower part! + { + const wxColor backCol = wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE); + + auto liftChannel = [](unsigned char c) { return static_cast(std::clamp(c + 25, 0, 255)); }; + + return wxColor(liftChannel(backCol.Red ()), + liftChannel(backCol.Green()), + liftChannel(backCol.Blue ())); + } + else + return wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); +} +wxColor getColorLabelGradientTo() { return wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE); } + +wxColor getColorLabelGradientFocusFrom() { return wxSystemSettings::GetAppearance().IsDark() ? wxSystemSettings::GetColour(wxSYS_COLOUR_BTNHIGHLIGHT) : getColorLabelGradientFrom(); } +wxColor getColorLabelGradientFocusTo () { return wxSystemSettings::GetAppearance().IsDark() ? getColorLabelGradientTo() : GridData::getColorSelectionGradientFrom(); } + +const double MOUSE_DRAG_ACCELERATION_DIP = 1.5; //unit: [rows / (DIP * sec)] -> same value like Explorer! +const int DEFAULT_COL_LABEL_BORDER_DIP = 6; //top + bottom border in addition to label height +const int COLUMN_MOVE_DELAY_DIP = 5; //unit: [pixel] (from Explorer) +const int COLUMN_MIN_WIDTH_DIP = 40; //only honored when resizing manually! +const int ROW_LABEL_BORDER_DIP = 3; +const int COLUMN_RESIZE_TOLERANCE_DIP = 6; //unit [pixel] +const int COLUMN_FILL_GAP_TOLERANCE_DIP = 10; //enlarge column to fill full width when resizing +const int COLUMN_MOVE_MARKER_WIDTH_DIP = 3; + +const bool fillGapAfterColumns = true; //draw rows/column label to fill full window width; may become an instance variable some time? + +/* IsEnabled() vs IsThisEnabled() since wxWidgets 2.9.5: + + void wxWindowBase::NotifyWindowOnEnableChange(), called from bool wxWindowBase::Enable(), fails to refresh + child elements when disabling a IsTopLevel() dialog, e.g. when showing a modal dialog. + The unfortunate effect on XP for using IsEnabled() when rendering the grid is that the user can move the modal dialog + and *draw* with it on the background while the grid refreshes as disabled incrementally! + + => Don't use IsEnabled() since it considers the top level window, but a disabled top-level should NOT + lead to child elements being rendered disabled! + + => IsThisEnabled() OTOH is too shallow and does not consider parent windows which are not top level. + + The perfect solution would be a bool renderAsEnabled() { return "IsEnabled() but ignore effects of showing a modal dialog"; } + + However "IsThisEnabled()" is good enough (same as old IsEnabled() on wxWidgets 2.8.12) and it avoids this pathetic behavior on XP. + (Similar problem on Win 7: e.g. directly click sync button without comparing first) + + => 2018-07-30: roll our own: */ +bool renderAsEnabled(wxWindow& win) +{ + if (win.IsTopLevel()) + return true; + + if (wxWindow* parent = win.GetParent()) + return win.IsThisEnabled() && renderAsEnabled(*parent); + else + return win.IsThisEnabled(); +} +} + +//---------------------------------------------------------------------------------------------------------------- +namespace zen +{ +wxDEFINE_EVENT(EVENT_GRID_MOUSE_LEFT_DOUBLE, GridClickEvent); +wxDEFINE_EVENT(EVENT_GRID_MOUSE_LEFT_DOWN, GridClickEvent); +wxDEFINE_EVENT(EVENT_GRID_MOUSE_RIGHT_DOWN, GridClickEvent); +wxDEFINE_EVENT(EVENT_GRID_SELECT_RANGE, GridSelectEvent); +wxDEFINE_EVENT(EVENT_GRID_COL_LABEL_MOUSE_LEFT, GridLabelClickEvent); +wxDEFINE_EVENT(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, GridLabelClickEvent); +wxDEFINE_EVENT(EVENT_GRID_COL_RESIZE, GridColumnResizeEvent); +wxDEFINE_EVENT(EVENT_GRID_CONTEXT_MENU, GridContextMenuEvent); +} +//---------------------------------------------------------------------------------------------------------------- + +void GridData::renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected, HoverArea rowHover) +{ + if (enabled) + { + if (selected) + dc.GradientFillLinear(rect, getColorSelectionGradientFrom(), getColorSelectionGradientTo(), wxEAST); + //else: clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); -> already the default + } + else + clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE)); +} + + +void GridData::renderCell(wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover) +{ + wxDCTextColourChanger textColor(dc); + if (enabled && selected) //accessibility: always set *both* foreground AND background colors! + textColor.Set(*wxBLACK); + + wxRect rectTmp = drawCellBorder(dc, rect); + + rectTmp.x += getColumnGapLeft(); + rectTmp.width -= getColumnGapLeft(); + drawCellText(dc, rectTmp, getValue(row, colType)); +} + + +int GridData::getBestSize(const wxReadOnlyDC& dc, size_t row, ColumnType colType) +{ + return dc.GetTextExtent(getValue(row, colType)).GetWidth() + 2 * getColumnGapLeft() + dipToWxsize(1); //gap on left and right side + border +} + + +wxRect GridData::drawCellBorder(wxDC& dc, const wxRect& rect) //returns remaining rectangle +{ + clearArea(dc, {rect.x + rect.width - dipToWxsize(1), rect.y, dipToWxsize(1), rect.height}, getColorGridLine()); //right border + clearArea(dc, {rect.x, rect.y + rect.height - dipToWxsize(1), rect.width, dipToWxsize(1)}, getColorGridLine()); //bottom border + + return {rect.x, rect.y, rect.width - dipToWxsize(1), rect.height - dipToWxsize(1)}; +} + + +void GridData::drawCellText(wxDC& dc, const wxRect& rect, const std::wstring_view text, int alignment, const wxSize* textExtentHint) +{ + /* Performance Notes (Windows): + - wxDC::GetTextExtent() is by far the most expensive call (20x more expensive than wxDC::DrawText()) + - wxDC::DrawLabel() is inefficiently implemented; internally calls: wxDC::GetMultiLineTextExtent(), wxDC::GetTextExtent(), wxDC::DrawText() + - wxDC::GetMultiLineTextExtent() calls wxDC::GetTextExtent() + - wxDC::DrawText also calls wxDC::GetTextExtent()!! + => wxDC::DrawLabel() boils down to 3(!) calls to wxDC::GetTextExtent()!!! + - wxDC::DrawLabel results in GetTextExtent() call even for empty strings!!! + => NEVER EVER call wxDC::DrawLabel() cruft and directly call wxDC::DrawText()! */ + assert(!contains(text, L'\n')); + if (rect.width <= 0 || rect.height <= 0 || text.empty()) + return; + + //truncate large texts and add ellipsis + wxString textTrunc(&text[0], text.size()); + wxSize extentTrunc = textExtentHint ? *textExtentHint : dc.GetTextExtent(textTrunc); + assert(!textExtentHint || *textExtentHint == dc.GetTextExtent(textTrunc)); //"trust, but verify" :> + + if (extentTrunc.GetWidth() > rect.width) + { + //unlike File Explorer, we truncate UTF-16 correctly: e.g. CJK-Ideograph encodes to TWO wchar_t: utfTo("\xf0\xa4\xbd\x9c"); + size_t low = 0; //number of Unicode chars! + size_t high = unicodeLength(text); // + if (high > 1) + for (;;) + { + if (high - low <= 1) + { + if (low == 0) + { + textTrunc = ELLIPSIS; + extentTrunc = dc.GetTextExtent(ELLIPSIS); + } + break; + } + const size_t middle = (low + high) / 2; //=> never 0 when "high - low > 1" + + /*const*/ wxString candidate = getUnicodeSubstring(text, 0, middle) + ELLIPSIS; + const wxSize extentCand = dc.GetTextExtent(candidate); //perf: most expensive call of this routine! + + if (extentCand.GetWidth() <= rect.width) + { + low = middle; + textTrunc = std::move(candidate); + extentTrunc = extentCand; + } + else + high = middle; + } + } + + wxPoint pt = rect.GetTopLeft(); + if (alignment & wxALIGN_RIGHT) //note: wxALIGN_LEFT == 0! + pt.x += rect.width - extentTrunc.GetWidth(); + else if (alignment & wxALIGN_CENTER_HORIZONTAL) + pt.x += numeric::intDivFloor(rect.width - extentTrunc.GetWidth(), 2); //round down negative values, too! + + if (alignment & wxALIGN_BOTTOM) //note: wxALIGN_TOP == 0! + pt.y += rect.height - extentTrunc.GetHeight(); + else if (alignment & wxALIGN_CENTER_VERTICAL) + pt.y += numeric::intDivFloor(rect.height - extentTrunc.GetHeight(), 2); //round down negative values, too! + + //std::optional clip; -> redundant!? RecursiveDcClipper already used during grid cell rendering + //if (extentTrunc.GetWidth() > rect.width) + // clip.emplace(dc, rect); + + dc.DrawText(textTrunc, pt); +} + + +void GridData::renderColumnLabel(wxDC& dc, const wxRect& rect, ColumnType colType, bool enabled, bool highlighted) +{ + wxRect rectRemain = drawColumnLabelBackground(dc, rect, highlighted); + + rectRemain.x += getColumnGapLeft(); + rectRemain.width -= getColumnGapLeft(); + drawColumnLabelText(dc, rectRemain, getColumnLabel(colType), enabled); +} + + +wxRect GridData::drawColumnLabelBackground(wxDC& dc, const wxRect& rect, bool highlighted) +{ + if (highlighted) + dc.GradientFillLinear(rect, getColorLabelGradientFocusFrom(), getColorLabelGradientFocusTo(), wxSOUTH); + else //regular background gradient + dc.GradientFillLinear(rect, getColorLabelGradientFrom(), getColorLabelGradientTo(), wxSOUTH); + + //left border + clearArea(dc, wxRect(rect.GetTopLeft(), wxSize(dipToWxsize(1), rect.height)), wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); + + //right border + dc.GradientFillLinear(wxRect(rect.x + rect.width - dipToWxsize(1), rect.y, dipToWxsize(1), rect.height), + getColorLabelGradientFrom(), wxSystemSettings::GetColour(wxSYS_COLOUR_BTNSHADOW), wxSOUTH); + + //bottom border + clearArea(dc, wxRect(rect.x, rect.y + rect.height - dipToWxsize(1), rect.width, dipToWxsize(1)), wxSystemSettings::GetColour(wxSYS_COLOUR_BTNSHADOW)); + + return rect.Deflate(dipToWxsize(1), dipToWxsize(1)); +} + + +void GridData::drawColumnLabelText(wxDC& dc, const wxRect& rect, const std::wstring& text, bool enabled) +{ + wxDCTextColourChanger textColor(dc, getColorLabelText(enabled)); //accessibility: always set both foreground AND background colors! + drawCellText(dc, rect, text, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); +} + +//---------------------------------------------------------------------------------------------------------------- +/* SubWindow + /|\ + __________________|__________________ + | | | | + CornerWin RowLabelWin ColLabelWin MainWin */ + +class Grid::SubWindow : public wxWindow +{ +public: + SubWindow(Grid& parent) : + wxWindow(&parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxWANTS_CHARS | wxBORDER_NONE, wxASCII_STR(wxPanelNameStr)), + parent_(parent) + { + //https://wiki.wxwidgets.org/Flicker-Free_Drawing + SetBackgroundStyle(wxBG_STYLE_PAINT); //get's rid of needless wxEVT_ERASE_BACKGROUND + Bind(wxEVT_PAINT, [this](wxPaintEvent& event) { onPaintEvent(event); }); + Bind(wxEVT_SIZE, [this](wxSizeEvent& event) { Refresh(); event.Skip(); }); + + Bind(wxEVT_CHILD_FOCUS, [](wxChildFocusEvent& event) {}); //wxGTK::wxScrolledWindow automatically scrolls to child window when child gets focus -> prevent! + + Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent& event) { onMouseLeftDown (event); }); + Bind(wxEVT_LEFT_UP, [this](wxMouseEvent& event) { onMouseLeftUp (event); }); + Bind(wxEVT_LEFT_DCLICK, [this](wxMouseEvent& event) { onMouseLeftDouble(event); }); + Bind(wxEVT_RIGHT_DOWN, [this](wxMouseEvent& event) { onMouseRightDown (event); }); + Bind(wxEVT_RIGHT_UP, [this](wxMouseEvent& event) { onMouseRightUp (event); }); + Bind(wxEVT_MOTION, [this](wxMouseEvent& event) { onMouseMovement (event); }); + Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent& event) { onLeaveWindow (event); }); + Bind(wxEVT_MOUSEWHEEL, [this](wxMouseEvent& event) { onMouseWheel (event); }); + Bind(wxEVT_MOUSE_CAPTURE_LOST, [this](wxMouseCaptureLostEvent& event) { onMouseCaptureLost(event); }); + + Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) + { + if (!sendEventToParent(event)) //let parent collect all key events + event.Skip(); + }); + //Bind(wxEVT_KEY_UP, [this](wxKeyEvent& event) { onKeyUp (event); }); -> superfluous? + + assert(GetClientAreaOrigin() == wxPoint()); //generally assumed when dealing with coordinates below + } + Grid& refParent() { return parent_; } + const Grid& refParent() const { return parent_; } + + template + bool sendEventToParent(T&& event) //take both "rvalue + lvalues", return "true" if a suitable event handler function was found and executed, and the function did not call wxEvent::Skip. + { + return parent_.GetEventHandler()->ProcessEvent(event); + } + +protected: + void setToolTip(const std::wstring& text) //proper fix for wxWindow + { + if (text != GetToolTipText()) + { + if (text.empty()) + UnsetToolTip(); //wxGTK doesn't allow wxToolTip with empty text! + else + { + wxToolTip* tt = GetToolTip(); + if (!tt) + { + //wxWidgets bug: tooltip multiline property is defined by first tooltip text containing newlines or not (same is true for maximum width) + tt = new wxToolTip(L"a b\n\ + a b"); //ugly, but working (on Windows) + SetToolTip(tt); //pass ownership + } + tt->SetTip(text); + } + } + } + +private: + virtual void render(wxDC& dc, const wxRect& rect) = 0; + + virtual void onMouseLeftDown (wxMouseEvent& event) { event.Skip(); } + virtual void onMouseLeftUp (wxMouseEvent& event) { event.Skip(); } + virtual void onMouseLeftDouble(wxMouseEvent& event) { event.Skip(); } + virtual void onMouseRightDown (wxMouseEvent& event) { event.Skip(); } + virtual void onMouseRightUp (wxMouseEvent& event) { event.Skip(); } + virtual void onMouseMovement (wxMouseEvent& event) { event.Skip(); } + virtual void onLeaveWindow (wxMouseEvent& event) { event.Skip(); } + virtual void onMouseCaptureLost(wxMouseCaptureLostEvent& event) { event.Skip(); } + + void onMouseWheel(wxMouseEvent& event) + { + /* MSDN, WM_MOUSEWHEEL: "Sent to the focus window when the mouse wheel is rotated. + The DefWindowProc function propagates the message to the window's parent. + There should be no internal forwarding of the message, since DefWindowProc propagates + it up the parent chain until it finds a window that processes it." + + On macOS there is no such propagation! => we need a redirection (the same wxGrid implements) + + new wxWidgets 3.0 screw-up for GTK2: wxScrollHelperEvtHandler::ProcessEvent() ignores wxEVT_MOUSEWHEEL events + thereby breaking the scenario of redirection to parent we need here (but also breaking their very own wxGrid sample) + => call wxScrolledWindow mouse wheel handler directly */ + + //wxWidgets never ceases to amaze: multi-line scrolling is implemented maximally inefficient by repeating wxEVT_SCROLLWIN_LINEUP!! => WTF! + if (event.GetWheelAxis() == wxMOUSE_WHEEL_VERTICAL && //=> reimplement wxScrollHelperBase::HandleOnMouseWheel() in a non-retarded way + !event.IsPageScroll()) + { + mouseRotateRemainder_ += -event.GetWheelRotation(); + int rotations = mouseRotateRemainder_ / event.GetWheelDelta(); + mouseRotateRemainder_ -= rotations * event.GetWheelDelta(); + + if (rotations == 0) //macOS generates tiny GetWheelRotation()! => don't allow! Always scroll a single row at least! + { + rotations = -numeric::sign(event.GetWheelRotation()); + mouseRotateRemainder_ = 0; + } + + const int rowsDelta = rotations * event.GetLinesPerAction(); + parent_.scrollDelta(0, rowsDelta); + } + else + parent_.HandleOnMouseWheel(event); + + onMouseMovement(event); + event.Skip(false); + + //if (!sendEventToParent(event)) + // event.Skip(); + } + + void onPaintEvent(wxPaintEvent& event) + { + + DynBufPaintDC dc(*this, doubleBuffer_); + assert(GetSize() == GetClientSize()); + + const wxRegion& updateReg = GetUpdateRegion(); + for (wxRegionIterator it = updateReg; it; ++it) + render(dc, it.GetRect()); + } + + Grid& parent_; + std::optional doubleBuffer_; + int mouseRotateRemainder_ = 0; +}; + +//---------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------- + +class Grid::CornerWin : public SubWindow +{ +public: + explicit CornerWin(Grid& parent) : SubWindow(parent) {} + +private: + bool AcceptsFocus() const override { return false; } + + void render(wxDC& dc, const wxRect& /*rect*/) override + { + const wxRect& rect = GetClientRect(); //would be overkill to support GetUpdateRegion()! + + clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); + //caveat: wxSYS_COLOUR_BTNSHADOW is partially transparent on macOS! + + dc.GradientFillLinear(rect, getColorLabelGradientFrom(), getColorLabelGradientTo(), wxSOUTH); + + //left border + dc.GradientFillLinear(wxRect(rect.GetTopLeft(), wxSize(dipToWxsize(1), rect.height)), + getColorLabelGradientFrom(), wxSystemSettings::GetColour(wxSYS_COLOUR_BTNSHADOW), wxSOUTH); + + //left border2 + clearArea(dc, wxRect(rect.x + dipToWxsize(1), rect.y, dipToWxsize(1), rect.height), + wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); + + //right border + dc.GradientFillLinear(wxRect(rect.x + rect.width - dipToWxsize(1), rect.y, dipToWxsize(1), rect.height), + getColorLabelGradientFrom(), wxSystemSettings::GetColour(wxSYS_COLOUR_BTNSHADOW), wxSOUTH); + + //bottom border + clearArea(dc, wxRect(rect.x, rect.y + rect.height - dipToWxsize(1), rect.width, dipToWxsize(1)), + wxSystemSettings::GetColour(wxSYS_COLOUR_BTNSHADOW)); + } +}; + +//---------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------- + +class Grid::RowLabelWin : public SubWindow +{ +public: + explicit RowLabelWin(Grid& parent) : + SubWindow(parent), + rowHeight_(parent.GetCharHeight() + dipToWxsize(2) + dipToWxsize(1)) {} //default height; don't call any functions on "parent" other than those from wxWindow during construction! + //2 for some more space, 1 for bottom border (gives 15 + 2 + 1 on Windows, 17 + 2 + 1 on Ubuntu) + + int getBestWidth(ptrdiff_t rowFrom, ptrdiff_t rowTo) + { + wxInfoDC dc(this); + dc.SetFont(GetFont()); //harmonize with RowLabelWin::render()! + + int bestWidth = 0; + for (ptrdiff_t i = rowFrom; i <= rowTo; ++i) + bestWidth = std::max(bestWidth, dc.GetTextExtent(formatRowNum(i)).GetWidth() + dipToWxsize(2 * ROW_LABEL_BORDER_DIP)); + return bestWidth; + } + + size_t getLogicalHeight() const { return refParent().getRowCount() * rowHeight_; } + + ptrdiff_t getRowAtPos(ptrdiff_t posY) const //returns < 0 on invalid input, else row number within: [0, rowCount]; rowCount if out of range + { + if (posY < 0) + return -1; + + const size_t row = posY / rowHeight_; + return std::min(row, refParent().getRowCount()); + } + + int getRowHeight() const { return rowHeight_; } //guarantees to return size >= 1 ! + void setRowHeight(int height) { assert(height > 0); rowHeight_ = std::max(1, height); } + + wxRect getRowLabelArea(size_t row) const //returns empty rect if row not found + { + assert(GetClientAreaOrigin() == wxPoint()); + if (row < refParent().getRowCount()) + return wxRect(wxPoint(0, rowHeight_ * row), + wxSize(GetClientSize().GetWidth(), rowHeight_)); + return wxRect(); + } + +private: + static std::wstring formatRowNum(size_t row) { return formatNumber(row + 1); } //convert number to std::wstring including thousands separator + + bool AcceptsFocus() const override { return false; } + + void render(wxDC& dc, const wxRect& rect) override + { + clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); + + const bool enabled = renderAsEnabled(*this); + + dc.SetFont(GetFont()); //harmonize with RowLabelWin::getBestWidth()! + + const auto& [rowFirst, rowLast] = refParent().getVisibleRows(rect); + for (auto row = rowFirst; row < rowLast; ++row) + { + wxRect rectRowLabel = getRowLabelArea(row); //returns empty rect if row not found + if (rectRowLabel.height > 0) + { + rectRowLabel.y = refParent().CalcScrolledPosition(rectRowLabel.GetTopLeft()).y; + drawRowLabel(dc, rectRowLabel, row, enabled); + } + } + } + + void drawRowLabel(wxDC& dc, const wxRect& rect, size_t row, bool enabled) + { + //clearArea(dc, rect, getColorRowLabel()); + dc.GradientFillLinear(rect, getColorLabelGradientFrom(), getColorLabelGradientTo(), wxEAST); //clear overlapping cells + + //top border + clearArea(dc, wxRect(rect.x, rect.y, rect.width, dipToWxsize(1)), wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); + + //left border + clearArea(dc, wxRect(rect.x, rect.y, dipToWxsize(1), rect.height), wxSystemSettings::GetColour(wxSYS_COLOUR_BTNSHADOW)); + + //right border + clearArea(dc, wxRect(rect.x + rect.width - dipToWxsize(1), rect.y, dipToWxsize(1), rect.height), wxSystemSettings::GetColour(wxSYS_COLOUR_BTNSHADOW)); + + //bottom border + clearArea(dc, wxRect(rect.x, rect.y + rect.height - dipToWxsize(1), rect.width, dipToWxsize(1)), wxSystemSettings::GetColour(wxSYS_COLOUR_BTNSHADOW)); + + //label text + wxRect textRect = rect; + textRect.Deflate(dipToWxsize(1)); + + wxDCTextColourChanger textColor(dc, getColorLabelText(enabled)); //accessibility: always set both foreground AND background colors! + GridData::drawCellText(dc, textRect, formatRowNum(row), wxALIGN_CENTRE); + } + + void onMouseLeftDown(wxMouseEvent& event) override { redirectMouseEvent(event); } + void onMouseLeftUp (wxMouseEvent& event) override { redirectMouseEvent(event); } + void onMouseMovement(wxMouseEvent& event) override { redirectMouseEvent(event); } + void onLeaveWindow (wxMouseEvent& event) override { redirectMouseEvent(event); } + void onMouseCaptureLost(wxMouseCaptureLostEvent& event) override { refParent().getMainWin().GetEventHandler()->ProcessEvent(event); } + + void redirectMouseEvent(wxMouseEvent& event) + { + event.m_x = 0; //simulate click on left side of mainWin_! + + wxWindow& mainWin = refParent().getMainWin(); + mainWin.GetEventHandler()->ProcessEvent(event); + + if (event.ButtonDown() && wxWindow::FindFocus() != &mainWin) + mainWin.SetFocus(); + } + + int rowHeight_; +}; + + +namespace +{ +class ColumnResizing +{ +public: + ColumnResizing(wxWindow& wnd, size_t col, int startWidth, int clientPosX) : + wnd_(wnd), col_(col), startWidth_(startWidth), clientPosX_(clientPosX) + { + wnd_.CaptureMouse(); + } + ~ColumnResizing() + { + if (wnd_.HasCapture()) + wnd_.ReleaseMouse(); + } + + size_t getColumn () const { return col_; } + int getStartWidth () const { return startWidth_; } + int getStartPosX () const { return clientPosX_; } + +private: + wxWindow& wnd_; + const size_t col_; + const int startWidth_; + const int clientPosX_; +}; + + +class ColumnMove +{ +public: + ColumnMove(wxWindow& wnd, size_t colFrom, int clientPosX) : + wnd_(wnd), + colFrom_(colFrom), + colTo_(colFrom), + clientPosX_(clientPosX) { wnd_.CaptureMouse(); } + ~ColumnMove() { if (wnd_.HasCapture()) wnd_.ReleaseMouse(); } + + size_t getColumnFrom() const { return colFrom_; } + size_t& refColumnTo() { return colTo_; } + int getStartPosX () const { return clientPosX_; } + + bool isRealMove() const { return !singleClick_; } + void setRealMove() { singleClick_ = false; } + +private: + wxWindow& wnd_; + const size_t colFrom_; + size_t colTo_; + const int clientPosX_; + bool singleClick_ = true; +}; +} + +//---------------------------------------------------------------------------------------------------------------- + +class Grid::ColLabelWin : public SubWindow +{ +public: + explicit ColLabelWin(Grid& parent) : SubWindow(parent), + labelFont_(GetFont().Bold()) + { + //coordinate with ColLabelWin::render(): + colLabelHeight_ = dipToWxsize(2 * DEFAULT_COL_LABEL_BORDER_DIP) + labelFont_.GetPixelSize().GetHeight(); + } + + int getColumnLabelHeight() const { return colLabelHeight_; } + void setColumnLabelHeight(int height) { colLabelHeight_ = std::max(0, height); } + +private: + bool AcceptsFocus() const override { return false; } + + void render(wxDC& dc, const wxRect& rect) override + { + clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); + //caveat: system colors can be partially transparent on macOS + + dc.SetFont(labelFont_); //coordinate with "colLabelHeight" in Grid constructor + dc.SetTextForeground(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); + + const bool enabled = renderAsEnabled(*this); + + wxPoint labelAreaTL(refParent().CalcScrolledPosition(wxPoint(0, 0)).x, 0); //client coordinates + + const std::vector& absWidths = refParent().getColWidths(); //resolve stretched widths + for (size_t col = 0; col < absWidths.size(); ++col) + { + const int width = absWidths[col].width; //don't use unsigned for calculations! + + if (labelAreaTL.x > rect.GetRight()) + return; //done, rect is fully covered + if (labelAreaTL.x + width > rect.x) + drawColumnLabel(dc, wxRect(labelAreaTL, wxSize(width, colLabelHeight_)), col, absWidths[col].type, enabled); + labelAreaTL.x += width; + } + if (labelAreaTL.x > rect.GetRight()) + return; //done, rect is fully covered + + //fill gap after columns and cover full width + if (fillGapAfterColumns) + { + int totalWidth = 0; + for (const ColumnWidth& cw : absWidths) + totalWidth += cw.width; + const int clientWidth = GetClientSize().GetWidth(); //need reliable, stable width in contrast to rect.width + + if (totalWidth < clientWidth) + drawColumnLabel(dc, wxRect(labelAreaTL, wxSize(clientWidth - totalWidth, colLabelHeight_)), absWidths.size(), ColumnType::none, enabled); + } + } + + void drawColumnLabel(wxDC& dc, const wxRect& rect, size_t col, ColumnType colType, bool enabled) + { + if (auto prov = refParent().getDataProvider()) + { + const bool isHighlighted = activeResizing_ ? col == activeResizing_ ->getColumn () : //highlight_ column on mouse-over + activeClickOrMove_ ? col == activeClickOrMove_->getColumnFrom() : + highlightCol_ ? col == *highlightCol_ : + false; + + RecursiveDcClipper clip(dc, rect); + prov->renderColumnLabel(dc, rect, colType, enabled, isHighlighted); + + //draw move target location + if (refParent().allowColumnMove_) + if (activeClickOrMove_ && activeClickOrMove_->isRealMove()) + { + const int markerWidth = dipToWxsize(COLUMN_MOVE_MARKER_WIDTH_DIP); + + if (col + 1 == activeClickOrMove_->refColumnTo()) //handle pos 1, 2, .. up to "at end" position + dc.GradientFillLinear(wxRect(rect.x + rect.width - markerWidth, rect.y, markerWidth, rect.height), getColorLabelGradientFrom(), colDropMarkerColor_, wxSOUTH); + else if (col == activeClickOrMove_->refColumnTo() && col == 0) //pos 0 + dc.GradientFillLinear(wxRect(rect.GetTopLeft(), wxSize(markerWidth, rect.height)), getColorLabelGradientFrom(), colDropMarkerColor_, wxSOUTH); + } + } + } + + std::optional clientPosToColumnAction(const wxPoint& pos) const + { + if (0 <= pos.y && pos.y < colLabelHeight_) + if (const int absPosX = refParent().CalcUnscrolledPosition(pos).x; + absPosX >= 0) + { + const int resizeTolerance = refParent().allowColumnResize_ ? dipToWxsize(COLUMN_RESIZE_TOLERANCE_DIP) : 0; + const std::vector& absWidths = refParent().getColWidths(); //resolve stretched widths + + int accuWidth = 0; + for (size_t col = 0; col < absWidths.size(); ++col) + { + accuWidth += absWidths[col].width; + if (std::abs(absPosX - accuWidth) < resizeTolerance) + { + ColAction out; + out.wantResize = true; + out.col = col; + return out; + } + else if (absPosX < accuWidth) + { + ColAction out; + out.wantResize = false; + out.col = col; + return out; + } + } + } + return {}; + } + + size_t clientPosToMoveTargetColumn(const wxPoint& pos) const + { + const int absPosX = refParent().CalcUnscrolledPosition(pos).x; + const std::vector& absWidths = refParent().getColWidths(); //resolve negative/stretched widths + + int accWidth = 0; + for (size_t col = 0; col < absWidths.size(); ++col) + { + const int width = absWidths[col].width; //beware dreaded unsigned conversions! + accWidth += width; + + if (absPosX < accWidth - width / 2) + return col; + } + return absWidths.size(); + } + + void onMouseLeftDown(wxMouseEvent& event) override + { + //if (FindFocus() != &refParent().getMainWin()) -> clicking column label shouldn't change input focus, right!? e.g. resizing column, sorting...(other grid) + // refParent().getMainWin().SetFocus(); + + activeResizing_ .reset(); + activeClickOrMove_.reset(); + + if (std::optional action = clientPosToColumnAction(event.GetPosition())) + { + if (action->wantResize) + { + if (!event.LeftDClick()) //double-clicks never seem to arrive here; why is this checked at all??? + if (std::optional colWidth = refParent().getColWidth(action->col)) + activeResizing_.emplace(*this, action->col, *colWidth, event.GetPosition().x); + } + else //a move or single click + activeClickOrMove_.emplace(*this, action->col, event.GetPosition().x); + } + event.Skip(); + } + + void onMouseLeftUp(wxMouseEvent& event) override + { + activeResizing_.reset(); //nothing else to do, actual work done by onMouseMovement() + + if (activeClickOrMove_) + { + if (activeClickOrMove_->isRealMove()) + { + if (refParent().allowColumnMove_) + { + const size_t colFrom = activeClickOrMove_->getColumnFrom(); + size_t colTo = activeClickOrMove_->refColumnTo(); + + if (colTo > colFrom) //simulate "colFrom" deletion + --colTo; + + refParent().moveColumn(colFrom, colTo); + } + } + else //notify single label click + { + const wxPoint mousePos = GetPosition() + event.GetPosition(); + if (const std::optional colType = refParent().colToType(activeClickOrMove_->getColumnFrom())) + sendEventToParent(GridLabelClickEvent(EVENT_GRID_COL_LABEL_MOUSE_LEFT, *colType, mousePos)); + } + activeClickOrMove_.reset(); + } + + refParent().updateWindowSizes(); //looks strange if done during onMouseMovement() + refParent().Refresh(); + event.Skip(); + } + + void onMouseLeftDouble(wxMouseEvent& event) override + { + if (std::optional action = clientPosToColumnAction(event.GetPosition())) + if (action->wantResize) + { + //auto-size visible range on double-click + const int bestWidth = refParent().getBestColumnSize(action->col); //return -1 on error + if (bestWidth >= 0) + { + refParent().setColumnWidth(bestWidth, action->col, GridEventPolicy::allow); + refParent().Refresh(); //refresh main grid as well! + } + } + event.Skip(); + } + + void onMouseRightDown(wxMouseEvent& event) override + { + evalMouseMovement(event.GetPosition()); //update highlight in obscure cases (e.g. right-click while other context menu is open) + + const wxPoint mousePos = GetPosition() + event.GetPosition(); + + if (const std::optional action = clientPosToColumnAction(event.GetPosition())) + { + if (const std::optional colType = refParent().colToType(action->col)) + sendEventToParent(GridLabelClickEvent(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, *colType, mousePos)); //notify right click + else assert(false); + } + else + //notify right click (on free space after last column) + if (fillGapAfterColumns) + sendEventToParent(GridLabelClickEvent(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, ColumnType::none, mousePos)); + + //update mouse highlight (e.g. mouse position changed after showing context menu) => needed on Linux/macOS + evalMouseMovement(ScreenToClient(wxGetMousePosition())); + + event.Skip(); + } + + void onMouseMovement(wxMouseEvent& event) override + { + evalMouseMovement(event.GetPosition()); + event.Skip(); + } + + void evalMouseMovement(wxPoint clientPos) + { + if (activeResizing_) + { + const auto col = activeResizing_->getColumn(); + const int newWidth = activeResizing_->getStartWidth() + clientPos.x - activeResizing_->getStartPosX(); + + //set width tentatively + refParent().setColumnWidth(newWidth, col, GridEventPolicy::allow); + + //check if there's a small gap after last column, if yes, fill it + const int gapWidth = GetClientSize().GetWidth() - refParent().getColWidthsSum(GetClientSize().GetWidth()); + if (std::abs(gapWidth) < dipToWxsize(COLUMN_FILL_GAP_TOLERANCE_DIP)) + refParent().setColumnWidth(newWidth + gapWidth, col, GridEventPolicy::allow); + + Refresh(); + refParent().Refresh(); //refresh columns on main grid as well! + } + else if (activeClickOrMove_) + { + const int clientPosX = clientPos.x; + if (std::abs(clientPosX - activeClickOrMove_->getStartPosX()) > dipToWxsize(COLUMN_MOVE_DELAY_DIP)) //real move (not a single click) + { + activeClickOrMove_->setRealMove(); + activeClickOrMove_->refColumnTo() = clientPosToMoveTargetColumn(clientPos); + Refresh(); + } + } + else + { + if (const std::optional action = clientPosToColumnAction(clientPos)) + { + setMouseHighlight(action->col); + + if (action->wantResize) + SetCursor(wxCURSOR_SIZEWE); //window-local only! :) + else + SetCursor(*wxSTANDARD_CURSOR); //NOOP when setting same cursor + } + else + { + setMouseHighlight(std::nullopt); + SetCursor(*wxSTANDARD_CURSOR); + } + } + + const std::wstring toolTip = [&] + { + if (const ColumnType colType = refParent().getColumnAtWinPos(clientPos.x).colType; //returns ColumnType::none if no column at x position! + colType != ColumnType::none) + if (auto prov = refParent().getDataProvider()) + return prov->getToolTip(colType); + return std::wstring(); + }(); + setToolTip(toolTip); + } + + void onMouseCaptureLost(wxMouseCaptureLostEvent& event) override + { + if (activeResizing_ || activeClickOrMove_) + { + activeResizing_ .reset(); + activeClickOrMove_.reset(); + Refresh(); + } + setMouseHighlight(std::nullopt); + //event.Skip(); -> we DID handle it! + } + + void onLeaveWindow(wxMouseEvent& event) override + { + if (!activeResizing_ && !activeClickOrMove_) + //wxEVT_LEAVE_WINDOW does not respect mouse capture! -> however highlight is drawn unconditionally during move/resize! + setMouseHighlight(std::nullopt); + + event.Skip(); + } + + void setMouseHighlight(const std::optional& hl) + { + if (highlightCol_ != hl) + { + highlightCol_ = hl; + Refresh(); + } + } + + std::optional activeResizing_; + std::optional activeClickOrMove_; + std::optional highlightCol_; + + int colLabelHeight_ = 0; + const wxFont labelFont_; + + const wxColor colDropMarkerColor_ = enhanceContrast(*wxBLUE, //primarily needed for dark mode! + getColorLabelGradientTo(), 5 /*contrastRatioMin*/); //W3C recommends >= 4.5 for text +}; + +//---------------------------------------------------------------------------------------------------------------- + +class Grid::MainWin : public SubWindow +{ +public: + MainWin(Grid& parent, + RowLabelWin& rowLabelWin, + ColLabelWin& colLabelWin) : SubWindow(parent), + rowLabelWin_(rowLabelWin), + colLabelWin_(colLabelWin) + { + Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) + { + if (event.GetKeyCode() == WXK_ESCAPE && activeSelection_) //allow Escape key to cancel active selection! + { + wxMouseCaptureLostEvent evt; + GetEventHandler()->ProcessEvent(evt); //better integrate into event handling rather than calling onMouseCaptureLost() directly!? + return; + } + + /* using keyboard: => clear distracting mouse highlights + + wxEVT_KEY_DOWN evaluation order: + 1. this callback + 2. Grid::SubWindow ... sendEventToParent() + 3. clients binding to Grid wxEVT_KEY_DOWN + 4. Grid::onKeyDown() */ + setMouseHighlight(std::nullopt); + + event.Skip(); + }); + } + + ~MainWin() { assert(!gridUpdatePending_); } + + size_t getCursor() const { return cursorRow_; } + size_t getAnchor() const { return selectionAnchor_; } + + void setCursor(size_t newCursorRow, size_t newAnchorRow) + { + cursorRow_ = newCursorRow; + selectionAnchor_ = newAnchorRow; + activeSelection_.reset(); //e.g. user might search with F3 while holding down left mouse button + } + +private: + void render(wxDC& dc, const wxRect& rect) override + { + clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); //CONTRACT! expected by GridData::renderRowBackgound()! + + const bool enabled = renderAsEnabled(*this); + + if (auto prov = refParent().getDataProvider()) + { + dc.SetFont(GetFont()); //harmonize with Grid::getBestColumnSize() + dc.SetTextForeground(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); + + const std::vector& absWidths = refParent().getColWidths(); //resolve stretched widths + + int totalRowWidth = 0; + for (const ColumnWidth& cw : absWidths) + totalRowWidth += cw.width; + + //fill gap after columns and cover full width + if (fillGapAfterColumns) + totalRowWidth = std::max(totalRowWidth, GetClientSize().GetWidth()); + + RecursiveDcClipper dummy(dc, rect); //do NOT draw background on cells outside of invalidated rect invalidating foreground text! + + const wxPoint gridAreaTL(refParent().CalcScrolledPosition(wxPoint(0, 0))); //client coordinates + const int rowHeight = rowLabelWin_.getRowHeight(); + const auto& [rowFirst, rowLast] = refParent().getVisibleRows(rect); + + for (auto row = rowFirst; row < rowLast; ++row) + { + //draw background lines + const wxRect rowRect(gridAreaTL + wxPoint(0, row * rowHeight), wxSize(totalRowWidth, rowHeight)); + const bool drawSelected = drawAsSelected(row); + const HoverArea rowHover = getRowHoverToDraw(row); + + RecursiveDcClipper dummy2(dc, rowRect); + prov->renderRowBackgound(dc, rowRect, row, enabled, drawSelected, rowHover); + + //draw cells column by column + wxRect cellRect = rowRect; + for (const ColumnWidth& cw : absWidths) + { + cellRect.width = cw.width; + + if (cellRect.x > rect.GetRight()) + break; //done + + if (cellRect.x + cw.width > rect.x) + { + RecursiveDcClipper dummy3(dc, cellRect); + prov->renderCell(dc, cellRect, row, cw.type, enabled, drawSelected, rowHover); + } + cellRect.x += cw.width; + } + } + } + } + + HoverArea getRowHoverToDraw(ptrdiff_t row) const + { + if (activeSelection_) + { + if (activeSelection_->getFirstClick().row_ == row) + return activeSelection_->getFirstClick().hoverArea_; + } + else if (highlight_) + { + if (makeSigned(highlight_->row) == row) + return highlight_->rowHover; + } + return HoverArea::none; + } + + bool drawAsSelected(size_t row) const + { + if (activeSelection_) //check if user is currently selecting with mouse + { + const size_t rowFrom = std::min(activeSelection_->getStartRow(), activeSelection_->getCurrentRow()); + const size_t rowTo = std::max(activeSelection_->getStartRow(), activeSelection_->getCurrentRow()); + + if (rowFrom <= row && row <= rowTo) + return activeSelection_->isPositiveSelect(); //overwrite default + } + return refParent().isSelected(row); + } + + void onMouseLeftDown (wxMouseEvent& event) override { onMouseDown(event); } + void onMouseLeftUp (wxMouseEvent& event) override { onMouseUp (event); } + void onMouseRightDown(wxMouseEvent& event) override { onMouseDown(event); } + void onMouseRightUp (wxMouseEvent& event) override { onMouseUp (event); } + + void onMouseLeftDouble(wxMouseEvent& event) override + { + if (auto prov = refParent().getDataProvider()) + { + const wxPoint mousePos = GetPosition() + event.GetPosition(); + const ptrdiff_t rowCount = refParent().getRowCount(); + const ptrdiff_t row = refParent().getRowAtWinPos (event.GetPosition().y); //return -1 for invalid position; >= rowCount if out of range + const ColumnPosInfo cpi = refParent().getColumnAtWinPos(event.GetPosition().x); //returns ColumnType::none if no column at x position! + const HoverArea rowHover = [&] + { + if (0 <= row && row < rowCount && cpi.colType != ColumnType::none) + { + wxInfoDC dc(this); + dc.SetFont(GetFont()); + return prov->getMouseHover(dc, row, cpi.colType, cpi.cellRelativePosX, cpi.colWidth); + } + return HoverArea::none; + }(); + + //client is interested in all double-clicks, even those outside of the grid! + sendEventToParent(GridClickEvent(EVENT_GRID_MOUSE_LEFT_DOUBLE, row, rowHover, mousePos)); + } + event.Skip(); + } + + void onMouseDown(wxMouseEvent& event) //handle left and right mouse button clicks (almost) the same + { + if (activeSelection_) //allow other mouse button to cancel active selection! + { + wxMouseCaptureLostEvent evt; + GetEventHandler()->ProcessEvent(evt); + return; + } + + if (auto prov = refParent().getDataProvider()) + { + evalMouseMovement(event.GetPosition()); //update highlight in obscure cases (e.g. right-click while other context menu is open) + + const wxPoint mousePos = GetPosition() + event.GetPosition(); + const ptrdiff_t rowCount = refParent().getRowCount(); + const ptrdiff_t row = refParent().getRowAtWinPos (event.GetPosition().y); //return -1 for invalid position; >= rowCount if out of range + const ColumnPosInfo cpi = refParent().getColumnAtWinPos(event.GetPosition().x); //returns ColumnType::none if no column at x position! + const HoverArea rowHover = [&] + { + if (0 <= row && row < rowCount && cpi.colType != ColumnType::none) + { + wxInfoDC dc(this); + dc.SetFont(GetFont()); + return prov->getMouseHover(dc, row, cpi.colType, cpi.cellRelativePosX, cpi.colWidth); + } + return HoverArea::none; + }(); + + assert(row >= 0); + //row < 0 was possible in older wxWidgets: https://github.com/wxWidgets/wxWidgets/commit/2c69d27c0d225d3a331c773da466686153185320#diff-9f11c8f2cb1f734f7c0c1071aba491a5 + //=> pressing "Menu Key" simulated mouse-right-button down + up at position 0xffff/0xffff! + + GridClickEvent mouseEvent(event.RightDown() ? EVENT_GRID_MOUSE_RIGHT_DOWN : EVENT_GRID_MOUSE_LEFT_DOWN, row, rowHover, mousePos); + + if (const bool processed = sendEventToParent(mouseEvent); //allow client to swallow event! + !processed) + { + if (wxWindow::FindFocus() != this) //doesn't seem to happen automatically for right mouse button + SetFocus(); + + if (event.RightDown() && (row < 0 || refParent().isSelected(row))) //=> open context menu *immediately* and do *not* start a new selection + sendEventToParent(GridContextMenuEvent(mousePos)); + else if (row >= 0) + { + if (event.ControlDown()) + activeSelection_.emplace(*this, row, !refParent().isSelected(row) /*positive*/, false /*gridWasCleared*/, mouseEvent); + else if (event.ShiftDown()) + { + refParent().clearSelection(GridEventPolicy::deny); + activeSelection_.emplace(*this, selectionAnchor_, true /*positive*/, true /*gridWasCleared*/, mouseEvent); + } + else + { + refParent().clearSelection(GridEventPolicy::deny); + activeSelection_.emplace(*this, row, true /*positive*/, true /*gridWasCleared*/, mouseEvent); + //DO NOT emit range event for clearing selection! would be inconsistent with keyboard handling (moving cursor neither emits range event) + //and is also harmful when range event is considered a final action + //e.g. cfg grid would prematurely show a modal dialog after changed config + } + } + } + + //update mouse highlight (e.g. mouse position changed after showing context menu) => needed on Linux/macOS + evalMouseMovement(ScreenToClient(wxGetMousePosition())); + } + event.Skip(); //allow changing focus + } + + void onMouseUp(wxMouseEvent& event) + { + if (activeSelection_) + { + const size_t rowCount = refParent().getRowCount(); + if (rowCount > 0) + { + if (activeSelection_->getCurrentRow() < rowCount) + { + cursorRow_ = activeSelection_->getCurrentRow(); + selectionAnchor_ = activeSelection_->getStartRow(); //allowed to be "out of range" + } + else if (activeSelection_->getStartRow() < rowCount) //don't change cursor if "to" and "from" are out of range + { + cursorRow_ = rowCount - 1; + selectionAnchor_ = activeSelection_->getStartRow(); //allowed to be "out of range" + } + else //total selection "out of range" + selectionAnchor_ = cursorRow_; + } + //slight deviation from Explorer: change cursor while dragging mouse! -> unify behavior with shift + direction keys + const wxPoint mousePos = GetPosition() + event.GetPosition(); + const size_t rowFrom = activeSelection_->getStartRow(); + const size_t rowTo = activeSelection_->getCurrentRow(); + const bool positive = activeSelection_->isPositiveSelect(); + const GridClickEvent mouseClick = activeSelection_->getFirstClick(); + assert((mouseClick.GetEventType() == EVENT_GRID_MOUSE_RIGHT_DOWN) == event.RightUp()); + + activeSelection_.reset(); //release mouse capture *before* sending the event (which might show a modal popup dialog requiring the mouse!!!) + + const size_t rowFirst = std::min(rowFrom, rowTo); //sort + convert to half-open range + const size_t rowLast = std::max(rowFrom, rowTo) + 1; // + refParent().selectRange2(rowFirst, rowLast, positive, &mouseClick, GridEventPolicy::allow); + + if (mouseClick.GetEventType() == EVENT_GRID_MOUSE_RIGHT_DOWN) + sendEventToParent(GridContextMenuEvent(mousePos)); //... *not* mouseClick.mousePos_ + } +#if 0 + if (!event.RightUp()) + if (auto prov = refParent().getDataProvider()) + { + //this one may point to row which is not in visible area! + const wxPoint mousePos = GetPosition() + event.GetPosition(); + const ptrdiff_t rowCount = refParent().getRowCount(); + const ptrdiff_t row = refParent().getRowAtWinPos (event.GetPosition().y); //return -1 for invalid position; >= rowCount if out of range + const ColumnPosInfo cpi = refParent().getColumnAtWinPos(event.GetPosition().x); //returns ColumnType::none if no column at x position! + const HoverArea rowHover = [&] + { + if (0 <= row && row < rowCount && cpi.colType != ColumnType::none) + { + wxInfoDC dc(this); + dc.SetFont(GetFont()); + return prov->getMouseHover(dc, row, cpi.colType, cpi.cellRelativePosX, cpi.colWidth); + } + return HoverArea::none; + }(); + //notify click event after the range selection! e.g. this makes sure the selection is applied before showing a context menu + sendEventToParent(GridClickEvent(EVENT_GRID_MOUSE_LEFT_UP, row, rowHover, mousePos)); + } +#endif + //update mouse highlight (e.g. mouse position changed after showing context menu) + //=> macOS no mouse movement event is generated after a mouse button click (unlike on Windows) + evalMouseMovement(ScreenToClient(wxGetMousePosition())); + + event.Skip(); //allow changing focus + } + + void onMouseMovement(wxMouseEvent& event) override + { + evalMouseMovement(event.GetPosition()); + event.Skip(); + } + + void evalMouseMovement(wxPoint clientPos) + { + if (auto prov = refParent().getDataProvider()) + { + const ptrdiff_t rowCount = refParent().getRowCount(); + const ptrdiff_t row = refParent().getRowAtWinPos (clientPos.y); //return -1 for invalid position; >= rowCount if out of range + const ColumnPosInfo cpi = refParent().getColumnAtWinPos(clientPos.x); //returns ColumnType::none if no column at x position! + const HoverArea rowHover = [&] + { + if (0 <= row && row < rowCount && cpi.colType != ColumnType::none) + { + wxInfoDC dc(this); + dc.SetFont(GetFont()); + return prov->getMouseHover(dc, row, cpi.colType, cpi.cellRelativePosX, cpi.colWidth); + } + return HoverArea::none; + }(); + + const std::wstring toolTip = [&] + { + if (0 <= row && row < rowCount && cpi.colType != ColumnType::none) + return prov->getToolTip(row, cpi.colType, rowHover); + return std::wstring(); + }(); + setToolTip(toolTip); //change even during mouse selection! + + if (activeSelection_) + activeSelection_->evalMousePos(); //call on both mouse movement + timer event! + else + setMouseHighlight(rowHover != HoverArea::none ? std::make_optional({static_cast(row), rowHover}) : std::nullopt); + } + } + + void onMouseCaptureLost(wxMouseCaptureLostEvent& event) override + { + if (activeSelection_) + { + if (activeSelection_->gridWasCleared()) + refParent().clearSelection(GridEventPolicy::allow); //see onMouseDown(); selection is "completed" => emit GridSelectEvent + + activeSelection_.reset(); + Refresh(); + } + setMouseHighlight(std::nullopt); + //event.Skip(); -> we DID handle it! + } + + void onLeaveWindow(wxMouseEvent& event) override + { + if (!activeSelection_) //wxEVT_LEAVE_WINDOW does not respect mouse capture! + setMouseHighlight(std::nullopt); + + //CAVEAT: we can get wxEVT_MOTION *after* wxEVT_LEAVE_WINDOW: see RowLabelWin::redirectMouseEvent() + // => therefore we also redirect wxEVT_LEAVE_WINDOW, but user will see a little flicker when moving between RowLabelWin and MainWin + event.Skip(); + } + + class MouseSelection : private wxEvtHandler + { + public: + MouseSelection(MainWin& wnd, size_t rowStart, bool positive, bool gridWasCleared, const GridClickEvent& firstClick) : + wnd_(wnd), rowStart_(rowStart), rowCurrent_(rowStart), positiveSelect_(positive), gridWasCleared_(gridWasCleared), firstClick_(firstClick) + { + wnd_.CaptureMouse(); + timer_.Bind(wxEVT_TIMER, [this](wxTimerEvent& event) { evalMousePos(); }); + timer_.Start(100); //timer interval in ms + evalMousePos(); + wnd_.Refresh(); + } + ~MouseSelection() { if (wnd_.HasCapture()) wnd_.ReleaseMouse(); } + + size_t getStartRow () const { return rowStart_; } + size_t getCurrentRow () const { return rowCurrent_; } + bool isPositiveSelect() const { return positiveSelect_; } //are we selecting or unselecting? + bool gridWasCleared () const { return gridWasCleared_; } + + const GridClickEvent& getFirstClick() const { return firstClick_; } + + void evalMousePos() + { + const auto now = std::chrono::steady_clock::now(); + const double deltaSecs = std::chrono::duration(now - lastEvalTime_).count(); //unit: [sec] + lastEvalTime_ = now; + + const wxPoint clientPos = wnd_.ScreenToClient(wxGetMousePosition()); + const wxSize clientSize = wnd_.GetClientSize(); + assert(wnd_.GetClientAreaOrigin() == wxPoint()); + + //scroll while dragging mouse + const int overlapPixY = clientPos.y < 0 ? clientPos.y : + clientPos.y >= clientSize.GetHeight() ? clientPos.y - (clientSize.GetHeight() - 1) : 0; + const int overlapPixX = clientPos.x < 0 ? clientPos.x : + clientPos.x >= clientSize.GetWidth() ? clientPos.x - (clientSize.GetWidth() - 1) : 0; + + int pixelsPerUnitY = 0; + wnd_.refParent().GetScrollPixelsPerUnit(nullptr, &pixelsPerUnitY); + assert(pixelsPerUnitY > 0); + if (pixelsPerUnitY <= 0) + return; + + const double mouseDragSpeedIncScrollU = MOUSE_DRAG_ACCELERATION_DIP * wnd_.rowLabelWin_.getRowHeight() / pixelsPerUnitY; //unit: [scroll units / (DIP * sec)] + //design alternative: "Dynamic autoscroll based on escape velocity": https://devblogs.microsoft.com/oldnewthing/20210128-00/?p=104768 + + auto autoScroll = [&](int overlapPix, double& toScroll) + { + if (overlapPix != 0) + { + const double scrollSpeed = wnd_.ToDIP(overlapPix) * mouseDragSpeedIncScrollU; //unit: [scroll units / sec] + toScroll += scrollSpeed * deltaSecs; + } + else + toScroll = 0; + }; + + autoScroll(overlapPixX, toScrollX_); + autoScroll(overlapPixY, toScrollY_); + + if (static_cast(toScrollX_) != 0 || static_cast(toScrollY_) != 0) + { + wnd_.refParent().scrollDelta(static_cast(toScrollX_), static_cast(toScrollY_)); // + toScrollX_ -= static_cast(toScrollX_); //rounds down for positive numbers, up for negative, + toScrollY_ -= static_cast(toScrollY_); //exactly what we want + } + + //select current row *after* scrolling + wxPoint clientPosTrimmed = clientPos; + clientPosTrimmed.y = std::clamp(clientPosTrimmed.y, 0, clientSize.GetHeight() - 1); //do not select row outside client window! + + const ptrdiff_t newRow = wnd_.refParent().getRowAtWinPos(clientPosTrimmed.y); //return -1 for invalid position; >= rowCount if out of range + assert(newRow >= 0); + if (newRow >= 0) + if (rowCurrent_ != newRow) + { + rowCurrent_ = newRow; + wnd_.Refresh(); + } + } + + private: + MainWin& wnd_; + const size_t rowStart_; + ptrdiff_t rowCurrent_; + const bool positiveSelect_; + const bool gridWasCleared_; + const GridClickEvent firstClick_; + wxTimer timer_; + double toScrollX_ = 0; //count outstanding scroll unit fractions while dragging mouse + double toScrollY_ = 0; // + std::chrono::steady_clock::time_point lastEvalTime_ = std::chrono::steady_clock::now(); + }; + + void ScrollWindow(int dx, int dy, const wxRect* rect) override + { + wxWindow::ScrollWindow(dx, dy, rect); + rowLabelWin_.ScrollWindow(0, dy, rect); + colLabelWin_.ScrollWindow(dx, 0, rect); + + //attention, wxGTK call sequence: wxScrolledWindow::Scroll() -> wxScrolledHelperNative::Scroll() -> wxScrolledHelperNative::DoScroll() + //which *first* calls us, MainWin::ScrollWindow(), and *then* internally updates m_yScrollPosition + //=> we cannot use CalcUnscrolledPosition() here which gives the wrong/outdated value!!! + //=> we need to update asynchronously: + //=> don't send async event repeatedly => severe performance issues on wxGTK! + //=> can't use idle event neither: too few idle events on Windows, e.g. NO idle events while mouse drag-scrolling! + //=> solution: send single async event at most! + if (!gridUpdatePending_) //without guarding, the number of outstanding async events can become very high during scrolling!! test case: Ubuntu: 170; Windows: 20 + { + gridUpdatePending_ = true; + + GetEventHandler()->CallAfter([this] + { + refParent().updateWindowSizes(false); //row label width has changed -> do *not* update scrollbars: recursion on wxGTK! -> still a problem, now that this function is called async?? + rowLabelWin_.Update(); //update while dragging scroll thumb + + assert(gridUpdatePending_); + gridUpdatePending_ = false; + }); + } + } + + void refreshRow(size_t row) + { + const wxRect& rowArea = rowLabelWin_.getRowLabelArea(row); //returns empty rect if row not found + const wxPoint topLeft = refParent().CalcScrolledPosition(wxPoint(0, rowArea.y)); //logical -> window coordinates + wxRect cellArea(topLeft, wxSize(refParent().getColWidthsSum(GetClientSize().GetWidth()), rowArea.height)); + RefreshRect(cellArea); + } + + struct MouseHighlight + { + size_t row = 0; + HoverArea rowHover = HoverArea::none; + + bool operator==(const MouseHighlight&) const = default; + }; + + void setMouseHighlight(const std::optional& hl) + { + assert(!hl || (hl->row < refParent().getRowCount() && hl->rowHover != HoverArea::none)); + if (highlight_ != hl) + { + if (highlight_) + refreshRow(highlight_->row); + + highlight_ = hl; + + if (highlight_) + refreshRow(highlight_->row); + } + } + + + RowLabelWin& rowLabelWin_; + ColLabelWin& colLabelWin_; + + std::optional activeSelection_; //bound while user is selecting with mouse + std::optional highlight_; + + size_t cursorRow_ = 0; + size_t selectionAnchor_ = 0; + bool gridUpdatePending_ = false; +}; + +//---------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------- + +Grid::Grid(wxWindow* parent, + wxWindowID id, + const wxPoint& pos, + const wxSize& size, + long style, + const wxString& name) : wxScrolledWindow(parent, id, pos, size, style | wxWANTS_CHARS, name) +{ + cornerWin_ = new CornerWin (*this); // + rowLabelWin_ = new RowLabelWin(*this); //owership handled by "this" + colLabelWin_ = new ColLabelWin(*this); // + mainWin_ = new MainWin (*this, *rowLabelWin_, *colLabelWin_); // + + SetTargetWindow(mainWin_); + + SetInitialSize(size); //"Most controls will use this to set their initial size" -> why not + + assert(GetClientSize() == GetSize() && GetWindowBorderSize() == wxSize()); //borders are NOT allowed for Grid + //reason: updateWindowSizes() wants to use "GetSize()" as a "GetClientSize()" including scrollbars + + //https://wiki.wxwidgets.org/Flicker-Free_Drawing + SetBackgroundStyle(wxBG_STYLE_PAINT); //get's rid of needless wxEVT_ERASE_BACKGROUND + //wxEVT_PAINT: "If you have an EVT_PAINT() handler, you must create a wxPaintDC object within it even if you don't actually use it." + //=> and if not, wxScrollHelperEvtHandler::ProcessEvent() helps out and creates wxPaintDC (without rendering anything) + Bind(wxEVT_SIZE, [this](wxSizeEvent& event) { updateWindowSizes(); event.Skip(); }); + Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onKeyDown(event); }); +} + + +void Grid::updateWindowSizes(bool updateScrollbar) +{ + /* We have to deal with TWO nasty circular dependencies: + 1. + rowLabelWidth + /|\ + mainWin::client width + /|\ + SetScrollbars -> show/hide horizontal scrollbar depending on client width + /|\ + mainWin::client height -> possibly trimmed by horizontal scrollbars + /|\ + rowLabelWidth + + 2. + mainWin_->GetClientSize() + /|\ + SetScrollbars -> show/hide scrollbars depending on whether client size is big enough + /|\ + GetClientSize(); -> possibly trimmed by scrollbars + /|\ + mainWin_->GetClientSize() -> also trimmed, since it's a sub-window! + */ + + //break this vicious circle: + + //harmonize with Grid::GetSizeAvailableForScrollTarget()! + + //1. calculate row label width independent from scrollbars + const int mainWinHeightGross = std::max(0, GetSize().GetHeight() - getColumnLabelHeight()); //independent from client sizes and scrollbars! + const ptrdiff_t logicalHeight = rowLabelWin_->getLogicalHeight(); // + + const int rowLabelWidth = [&] + { + if (drawRowLabel_ && logicalHeight > 0) + { + ptrdiff_t yFrom = CalcUnscrolledPosition(wxPoint(0, 0)).y; + ptrdiff_t yTo = CalcUnscrolledPosition(wxPoint(0, mainWinHeightGross - 1)).y ; + yFrom = std::clamp(yFrom, 0, logicalHeight - 1); + yTo = std::clamp(yTo, 0, logicalHeight - 1); + + const ptrdiff_t rowFrom = rowLabelWin_->getRowAtPos(yFrom); + const ptrdiff_t rowTo = rowLabelWin_->getRowAtPos(yTo); + if (rowFrom >= 0 && rowTo >= 0) + return rowLabelWin_->getBestWidth(rowFrom, rowTo); + } + return 0; + }(); + + //2. update managed windows' sizes: just assume scrollbars are already set correctly, even if they may not be (yet)! + //this ensures mainWin_->SetVirtualSize() and AdjustScrollbars() are working with the correct main window size, unless sb change later, which triggers a recalculation anyway! + const wxSize mainWinSize(std::max(0, GetClientSize().GetWidth () - rowLabelWidth), + std::max(0, GetClientSize().GetHeight() - getColumnLabelHeight())); + + cornerWin_ ->SetSize(0, 0, rowLabelWidth, getColumnLabelHeight()); + rowLabelWin_->SetSize(0, getColumnLabelHeight(), rowLabelWidth, mainWinSize.GetHeight()); + colLabelWin_->SetSize(rowLabelWidth, 0, mainWinSize.GetWidth(), getColumnLabelHeight()); + mainWin_ ->SetSize(rowLabelWidth, getColumnLabelHeight(), mainWinSize.GetWidth(), mainWinSize.GetHeight()); + + //avoid flicker in wxWindowMSW::HandleSize() when calling ::EndDeferWindowPos() where the sub-windows are moved only although they need to be redrawn! + colLabelWin_->Refresh(); + mainWin_ ->Refresh(); + + //3. update scrollbars: "guide wxScrolledHelper to not screw up too much" + if (updateScrollbar) + { + auto setScrollbars2 = [&](int logWidth, int logHeight) //replace SetScrollbars, which loses precision of pixelsPerUnitX for some brain-dead reason + { + mainWin_->SetVirtualSize(logWidth, logHeight); //set before calling SetScrollRate(): + //else SetScrollRate() would fail to preserve scroll position when "new virtual pixel-pos > old virtual height" + + int ppsuX = 0; //pixel per scroll unit + int ppsuY = 0; + GetScrollPixelsPerUnit(&ppsuX, &ppsuY); + + const int ppsuNew = rowLabelWin_->getRowHeight(); + if (ppsuX != ppsuNew || ppsuY != ppsuNew) //support polling! + SetScrollRate(ppsuNew, ppsuNew); //internally calls AdjustScrollbars() and GetVirtualSize()! + + AdjustScrollbars(); //lousy wxWidgets design decision: internally calls mainWin_->GetClientSize() without considering impact of scrollbars! + //Attention: setting scrollbars triggers *synchronous* resize event if scrollbars are shown or hidden! => updateWindowSizes() recursion! (Windows) + }; + + const int mainWinWidthGross = std::max(0, GetSize().GetWidth() - rowLabelWidth); + + if (logicalHeight <= mainWinHeightGross && + getColWidthsSum(mainWinWidthGross) <= mainWinWidthGross && + //this special case needs to be considered *only* when both scrollbars are flexible: + showScrollbarH_ == SB_SHOW_AUTOMATIC && + showScrollbarV_ == SB_SHOW_AUTOMATIC) + setScrollbars2(0, 0); //no scrollbars required at all! -> wxScrolledWindow requires active help to detect this special case! + else + { + const int logicalWidthTmp = getColWidthsSum(mainWinSize.GetWidth()); //assuming vertical scrollbar stays as it is... + setScrollbars2(logicalWidthTmp, logicalHeight); //if scrollbars are shown or hidden a new resize event recurses into updateWindowSizes() + /* + is there a risk of endless recursion? No, 2-level recursion at most, consider the following 6 cases: + + <----------gw----------> + <----------nw------> + ------------------------ /|\ /|\ + | | | | | + | main window | | nh | + | | | | gh + ------------------------ \|/ | + | | | | + ------------------------ \|/ + gw := gross width + nw := net width := gross width - sb size + gh := gross height + nh := net height := gross height - sb size + + There are 6 cases that can occur: + --------------------------------- + lw := logical width + lh := logical height + + 1. lw <= gw && lh <= gh => no scrollbars needed + + 2. lw > gw && lh > gh => need both scrollbars + + lh > gh + 3. lw <= nw => need vertical scrollbar only + 4. nw < lw <= gw => need both scrollbars + + lw > gw + 5. lh <= nh => need horizontal scrollbar only + 6. nh < lh <= gh => need both scrollbars + */ + } + } +} + + +wxSize Grid::GetSizeAvailableForScrollTarget(const wxSize& size) +{ + //1. "size == GetSize() == (0, 0)" happens temporarily during initialization + //2. often it's even (0, 20) + //3. fuck knows why, but we *temporarily* get "size == GetSize() == (1, 1)" when wxAUI panel containing Grid is dropped + if (size.x <= 1 || size.y <= 1) + return {}; //probably best considering calling code in generic/scrlwing.cpp: wxScrollHelper::AdjustScrollbars() + + //1. calculate row label width independent from scrollbars + const int mainWinHeightGross = std::max(0, size.GetHeight() - getColumnLabelHeight()); //independent from client sizes and scrollbars! + const ptrdiff_t logicalHeight = rowLabelWin_->getLogicalHeight(); // + + const int rowLabelWidth = [&] + { + if (drawRowLabel_ && logicalHeight > 0) + { + ptrdiff_t yFrom = CalcUnscrolledPosition(wxPoint(0, 0)).y; + ptrdiff_t yTo = CalcUnscrolledPosition(wxPoint(0, mainWinHeightGross - 1)).y ; + yFrom = std::clamp(yFrom, 0, logicalHeight - 1); + yTo = std::clamp(yTo, 0, logicalHeight - 1); + + const ptrdiff_t rowFrom = rowLabelWin_->getRowAtPos(yFrom); + const ptrdiff_t rowTo = rowLabelWin_->getRowAtPos(yTo); + if (rowFrom >= 0 && rowTo >= 0) + return rowLabelWin_->getBestWidth(rowFrom, rowTo); + } + return 0; + }(); + + //2. try(!) to determine scrollbar sizes: +#if GTK_MAJOR_VERSION == 2 + /* Ubuntu 19.10: "scrollbar-spacing" has a default value of 3: https://developer.gnome.org/gtk2/stable/GtkScrolledWindow.html#GtkScrolledWindow--s-scrollbar-spacing + => the default Ubuntu theme (but also our Gtk2Styles.rc) set it to 0, but still the first call to gtk_widget_style_get() returns 3: why? + => maybe styles are applied asynchronously? GetClientSize() is affected by this, so can't use! + => always ignore spacing to get consistent scrollbar dimensions! */ + GtkScrolledWindow* scrollWin = GTK_SCROLLED_WINDOW(wxWindow::m_widget); + assert(scrollWin); + GtkWidget* rangeH = ::gtk_scrolled_window_get_hscrollbar(scrollWin); + GtkWidget* rangeV = ::gtk_scrolled_window_get_vscrollbar(scrollWin); + + GtkRequisition reqH = {}; + GtkRequisition reqV = {}; + if (rangeH) ::gtk_widget_size_request(rangeH, &reqH); + if (rangeV) ::gtk_widget_size_request(rangeV, &reqV); + assert(reqH.width > 0 && reqH.height > 0); + assert(reqV.width > 0 && reqV.height > 0); + + const wxSize scrollBarSizeTmp(reqV.width, reqH.height); + assert(scrollBarHeightH_ == 0 || scrollBarHeightH_ == scrollBarSizeTmp.y); + assert(scrollBarWidthV_ == 0 || scrollBarWidthV_ == scrollBarSizeTmp.x); + +#elif GTK_MAJOR_VERSION == 3 + //scrollbar size increases dynamically on mouse-hover! + //see "overlay scrolling": https://developer.gnome.org/gtk3/stable/GtkScrolledWindow.html#gtk-scrolled-window-set-overlay-scrolling + //luckily "scrollbar-spacing" is stable on GTK3 + const wxSize scrollBarSizeTmp = GetSize() - GetClientSize(); + + //lame hard-coded numbers (from Ubuntu 19.10) and openSuse + //=> let's have a *close* eye on scrollbar fluctuation! + assert(scrollBarSizeTmp.x == 0 || + scrollBarSizeTmp.x == 6 || scrollBarSizeTmp.x == 13 || //Ubuntu 19.10 + scrollBarSizeTmp.x == 16); //openSuse + assert(scrollBarSizeTmp.y == 0 || + scrollBarSizeTmp.y == 6 || scrollBarSizeTmp.y == 13 || //Ubuntu 19.10 + scrollBarSizeTmp.y == 16); //openSuse +#else +#error unknown GTK version! +#endif + scrollBarHeightH_ = std::max(scrollBarHeightH_, scrollBarSizeTmp.y); + scrollBarWidthV_ = std::max(scrollBarWidthV_, scrollBarSizeTmp.x); + //this function is called again by wxScrollHelper::AdjustScrollbars() if SB_SHOW_ALWAYS-scrollbars are not yet shown => scrollbar size > 0 eventually! + + //----------------------------------------------------------------------------- + //harmonize with Grid::updateWindowSizes()! + wxSize sizeAvail = size - wxSize(rowLabelWidth, getColumnLabelHeight()); + + //EXCEPTION: space consumed by SB_SHOW_ALWAYS-scrollbars is *never* available for "scroll target"; see wxScrollHelper::AdjustScrollbars() + if (showScrollbarH_ == SB_SHOW_ALWAYS) + sizeAvail.y -= (scrollBarHeightH_ > 0 ? scrollBarHeightH_ : /*fallback:*/ scrollBarWidthV_); + if (showScrollbarV_ == SB_SHOW_ALWAYS) + sizeAvail.x -= (scrollBarWidthV_ > 0 ? scrollBarWidthV_ : /*fallback:*/ scrollBarHeightH_); + + return wxSize(std::max(0, sizeAvail.x), + std::max(0, sizeAvail.y)); +} + + +void Grid::onKeyDown(wxKeyEvent& event) +{ + int keyCode = event.GetKeyCode(); + if (GetLayoutDirection() == wxLayout_RightToLeft) + { + if (keyCode == WXK_LEFT || keyCode == WXK_NUMPAD_LEFT) + keyCode = WXK_RIGHT; + else if (keyCode == WXK_RIGHT || keyCode == WXK_NUMPAD_RIGHT) + keyCode = WXK_LEFT; + } + if (event.ShiftDown() && keyCode == WXK_F10) //== alias for menu key + keyCode = WXK_WINDOWS_MENU; + + const ptrdiff_t rowCount = getRowCount(); + const ptrdiff_t cursorRow = mainWin_->getCursor(); + + auto moveCursorTo = [&](ptrdiff_t row) + { + if (rowCount > 0) + setGridCursor(std::clamp(row, 0, rowCount - 1), GridEventPolicy::allow); + }; + + auto selectWithCursorTo = [&](ptrdiff_t row) + { + if (rowCount > 0) + { + row = std::clamp(row, 0, rowCount - 1); + const ptrdiff_t anchorRow = mainWin_->getAnchor(); + + mainWin_->setCursor(row, anchorRow); + makeRowVisible(row); + + selection_.clear(); //clear selection, do NOT fire event + + const ptrdiff_t rowFirst = std::min(anchorRow, row); //sort + convert to half-open range + const ptrdiff_t rowLast = std::max(anchorRow, row) + 1; // + selectRange(rowFirst, rowLast, true /*positive*/, GridEventPolicy::allow); //set new selection + fire event + } + }; + + switch (keyCode) + { + case WXK_MENU: //simulate right mouse click at cursor row position (on lower edge) + case WXK_WINDOWS_MENU: //(but truncate to window if cursor is out of view) + { + const size_t row = std::min(mainWin_->getCursor(), getRowCount()); + + const int clientPosMainWinY = std::clamp(CalcScrolledPosition(wxPoint(0, rowLabelWin_->getRowHeight() * (row + 1))).y - 1, //logical -> window coordinates + 0, mainWin_->GetClientSize().GetHeight() - 1); + + const wxPoint mousePos = mainWin_->GetPosition() + wxPoint(0, clientPosMainWinY); //mainWin_-relative to Grid-relative + + GridContextMenuEvent contextEvent(mousePos); + GetEventHandler()->ProcessEvent(contextEvent); + } + return; + + //case WXK_TAB: + // if (Navigate(event.ShiftDown() ? wxNavigationKeyEvent::IsBackward : wxNavigationKeyEvent::IsForward)) + // return; + // break; + + case WXK_UP: + case WXK_NUMPAD_UP: + if (event.ShiftDown()) + selectWithCursorTo(cursorRow - 1); + else if (event.ControlDown()) + scrollDelta(0, -1); + else + moveCursorTo(cursorRow - 1); + return; //swallow event: wxScrolledWindow, wxWidgets 2.9.3 on Kubuntu x64 processes arrow keys: prevent this! + + case WXK_DOWN: + case WXK_NUMPAD_DOWN: + if (event.ShiftDown()) + selectWithCursorTo(cursorRow + 1); + else if (event.ControlDown()) + scrollDelta(0, 1); + else + moveCursorTo(cursorRow + 1); + return; //swallow event + + case WXK_LEFT: + case WXK_NUMPAD_LEFT: + if (event.ControlDown()) + scrollDelta(-1, 0); + else if (event.ShiftDown()) + ; + else + moveCursorTo(cursorRow); + return; + + case WXK_RIGHT: + case WXK_NUMPAD_RIGHT: + if (event.ControlDown()) + scrollDelta(1, 0); + else if (event.ShiftDown()) + ; + else + moveCursorTo(cursorRow); + return; + + case WXK_HOME: + case WXK_NUMPAD_HOME: + if (event.ShiftDown()) + selectWithCursorTo(0); + //else if (event.ControlDown()) + // ; + else + moveCursorTo(0); + return; + + case WXK_END: + case WXK_NUMPAD_END: + if (event.ShiftDown()) + selectWithCursorTo(rowCount - 1); + //else if (event.ControlDown()) + // ; + else + moveCursorTo(rowCount - 1); + return; + + case WXK_PAGEUP: + case WXK_NUMPAD_PAGEUP: + if (event.ShiftDown()) + selectWithCursorTo(cursorRow - rowLabelWin_->GetClientSize().GetHeight() / rowLabelWin_->getRowHeight()); + //else if (event.ControlDown()) + // ; + else + moveCursorTo(cursorRow - rowLabelWin_->GetClientSize().GetHeight() / rowLabelWin_->getRowHeight()); + return; + + case WXK_PAGEDOWN: + case WXK_NUMPAD_PAGEDOWN: + if (event.ShiftDown()) + selectWithCursorTo(cursorRow + rowLabelWin_->GetClientSize().GetHeight() / rowLabelWin_->getRowHeight()); + //else if (event.ControlDown()) + // ; + else + moveCursorTo(cursorRow + rowLabelWin_->GetClientSize().GetHeight() / rowLabelWin_->getRowHeight()); + return; + + case 'A': //Ctrl + A - select all + if (event.ControlDown()) + selectRange(0, rowCount, true /*positive*/, GridEventPolicy::allow); + break; + + case WXK_NUMPAD_ADD: //CTRL + '+' - auto-size all + if (event.ControlDown()) + autoSizeColumns(GridEventPolicy::allow); + return; + } + + event.Skip(); +} + + +void Grid::setColumnLabelHeight(int height) +{ + colLabelWin_->setColumnLabelHeight(height); + updateWindowSizes(); +} + + +int Grid::getColumnLabelHeight() const { return colLabelWin_->getColumnLabelHeight(); } + + +void Grid::showRowLabel(bool show) +{ + drawRowLabel_ = show; + updateWindowSizes(); +} + + +void Grid::selectRange(size_t rowFirst, size_t rowLast, bool positive, GridEventPolicy rangeEventPolicy) +{ + selectRange2(rowFirst, rowLast, positive, nullptr /*mouseClick*/, rangeEventPolicy); +} + + +void Grid::selectRange2(size_t rowFirst, size_t rowLast, bool positive, const GridClickEvent* mouseClick, GridEventPolicy rangeEventPolicy) +{ + assert(rowFirst <= rowLast); + assert(getRowCount() == selection_.gridSize()); + rowFirst = std::clamp(rowFirst, 0, selection_.gridSize()); + rowLast = std::clamp(rowLast, 0, selection_.gridSize()); + + if (rowFirst < rowLast && !selection_.matchesRange(rowFirst, rowLast, positive)) + { + selection_.selectRange(rowFirst, rowLast, positive); + mainWin_->Refresh(); + } + + //issue event even for unchanged selection! e.g. MainWin::onMouseDown() temporarily clears range with GridEventPolicy::deny! + if (rangeEventPolicy == GridEventPolicy::allow) + { + GridSelectEvent selEvent(rowFirst, rowLast, positive, mouseClick); + [[maybe_unused]] const bool processed = GetEventHandler()->ProcessEvent(selEvent); + } +} + +void Grid::selectRow(size_t row, GridEventPolicy rangeEventPolicy) { selectRange(row, row + 1, true /*positive*/, rangeEventPolicy); } +void Grid::selectAllRows (GridEventPolicy rangeEventPolicy) { selectRange(0, selection_.gridSize(), true /*positive*/, rangeEventPolicy); } +void Grid::clearSelection (GridEventPolicy rangeEventPolicy) { selectRange(0, selection_.gridSize(), false /*positive*/, rangeEventPolicy); } + + +void Grid::scrollDelta(int deltaX, int deltaY) +{ + const wxPoint scrollPosOld = GetViewStart(); + + wxPoint scrollPosNew = scrollPosOld; + scrollPosNew.x += deltaX; + scrollPosNew.y += deltaY; + + scrollPosNew.x = std::max(0, scrollPosNew.x); //wxScrollHelper::Scroll() will exit prematurely if input happens to be "-1"! + scrollPosNew.y = std::max(0, scrollPosNew.y); // + + if (scrollPosNew != scrollPosOld) + { + Scroll(scrollPosNew); //internally calls wxWindows::Update()! + updateWindowSizes(); //may show horizontal scroll bar if row column gets wider + } +} + + +size_t Grid::getRowCount() const +{ + return dataView_ ? dataView_->getRowCount() : 0; +} + + +void Grid::Refresh(bool eraseBackground, const wxRect* rect) +{ + const size_t rowCountNew = getRowCount(); + if (rowCountOld_ != rowCountNew) + { + rowCountOld_ = rowCountNew; + updateWindowSizes(); + } + + if (selection_.gridSize() != rowCountNew) + { + const bool priorSelection = !selection_.matchesRange(0, selection_.gridSize(), false /*positive*/); + + selection_.resize(rowCountNew); + + if (priorSelection) //clear selection only when needed + { + //clearSelection(GridEventPolicy::allow); -> no, we need async event to make filegrid::refresh(*m_gridMainL, *m_gridMainC, *m_gridMainR) work + selection_.clear(); + GetEventHandler()->AddPendingEvent(GridSelectEvent(0, rowCountNew, false /*positive*/, nullptr /*mouseClick*/)); + } + } + + wxScrolledWindow::Refresh(eraseBackground, rect); +} + + +void Grid::setRowHeight(int height) +{ + rowLabelWin_->setRowHeight(height); + updateWindowSizes(); + Refresh(); +} + + +int Grid::getRowHeight() const { return rowLabelWin_->getRowHeight(); } + + +void Grid::setColumnConfig(const std::vector& attr) +{ + //hold ownership of non-visible columns + oldColAttributes_ = attr; + + std::vector visCols; + for (const ColAttributes& ca : attr) + { + assert(ca.stretch >= 0); + assert(ca.type != ColumnType::none); + + if (ca.visible) + visCols.push_back({ca.type, ca.offset, std::max(ca.stretch, 0)}); + } + + //"ownership" of visible columns is now within Grid + visibleCols_ = std::move(visCols); + + updateWindowSizes(); + Refresh(); +} + + +std::vector Grid::getColumnConfig() const +{ + //get non-visible columns (+ outdated visible ones) + std::vector output = oldColAttributes_; + + auto itVcols = visibleCols_.begin(); + auto itVcolsend = visibleCols_.end(); + + //update visible columns but keep order of non-visible ones! + for (ColAttributes& ca : output) + if (ca.visible) + { + if (itVcols != itVcolsend) + { + ca.type = itVcols->type; + ca.stretch = itVcols->stretch; + ca.offset = itVcols->offset; + ++itVcols; + } + else + assert(false); + } + assert(itVcols == itVcolsend); + + return output; +} + + +void Grid::showScrollBars(Grid::ScrollBarStatus horizontal, Grid::ScrollBarStatus vertical) +{ + if (showScrollbarH_ == horizontal && + showScrollbarV_ == vertical) return; //support polling! + + showScrollbarH_ = horizontal; + showScrollbarV_ = vertical; + + //the following wxGTK approach is pretty much identical to wxWidgets 2.9 ShowScrollbars() code! + + auto mapStatus = [](ScrollBarStatus sbStatus) -> GtkPolicyType + { + switch (sbStatus) + { + case SB_SHOW_AUTOMATIC: + return GTK_POLICY_AUTOMATIC; + case SB_SHOW_ALWAYS: + return GTK_POLICY_ALWAYS; + case SB_SHOW_NEVER: + return GTK_POLICY_NEVER; + } + assert(false); + return GTK_POLICY_AUTOMATIC; + }; + + GtkScrolledWindow* scrollWin = GTK_SCROLLED_WINDOW(wxWindow::m_widget); + assert(scrollWin); + ::gtk_scrolled_window_set_policy(scrollWin, + mapStatus(horizontal), + mapStatus(vertical)); + + updateWindowSizes(); +} + + + +wxWindow& Grid::getCornerWin () { return *cornerWin_; } +wxWindow& Grid::getRowLabelWin() { return *rowLabelWin_; } +wxWindow& Grid::getColLabelWin() { return *colLabelWin_; } +wxWindow& Grid::getMainWin () { return *mainWin_; } +const wxWindow& Grid::getMainWin() const { return *mainWin_; } + + +void Grid::moveColumn(size_t colFrom, size_t colTo) +{ + if (colFrom < visibleCols_.size() && + colTo < visibleCols_.size() && + colTo != colFrom) + { + const VisibleColumn colAtt = visibleCols_[colFrom]; + visibleCols_.erase (visibleCols_.begin() + colFrom); + visibleCols_.insert(visibleCols_.begin() + colTo, colAtt); + } +} + + +ColumnType Grid::colToType(size_t col) const +{ + if (col < visibleCols_.size()) + return visibleCols_[col].type; + return ColumnType::none; +} + + +Grid::ColumnPosInfo Grid::getColumnAtWinPos(int posX) const +{ + if (const int absX = CalcUnscrolledPosition(wxPoint(posX, 0)).x; + absX >= 0) + { + int accWidth = 0; + for (const ColumnWidth& cw : getColWidths()) + { + accWidth += cw.width; + if (absX < accWidth) + return {cw.type, absX + cw.width - accWidth, cw.width}; + } + } + return {ColumnType::none, 0, 0}; +} + + +ptrdiff_t Grid::getRowAtWinPos(int posY) const +{ + const int absY = CalcUnscrolledPosition(wxPoint(0, posY)).y; + return rowLabelWin_->getRowAtPos(absY); //return -1 for invalid position, rowCount if past the end +} + + +std::pair Grid::getVisibleRows(const wxRect& clientRect) const //returns range [begin, end) +{ + if (clientRect.height > 0) + { + const int rowFrom = getRowAtWinPos(clientRect.y); + const int rowTo = getRowAtWinPos(clientRect.GetBottom()); + + return {std::max(rowFrom, 0), + std::min((rowTo) + 1, getRowCount())}; + } + return {}; +} + + +wxRect Grid::getColumnLabelArea(ColumnType colType) const +{ + const std::vector& absWidths = getColWidths(); //resolve negative/stretched widths + + //colType is not unique in general, but *this* function expects it! + assert(std::count_if(absWidths.begin(), absWidths.end(), [&](const ColumnWidth& cw) { return cw.type == colType; }) <= 1); + + auto itCol = std::find_if(absWidths.begin(), absWidths.end(), [&](const ColumnWidth& cw) { return cw.type == colType; }); + if (itCol != absWidths.end()) + { + ptrdiff_t posX = 0; + std::for_each(absWidths.begin(), itCol, + [&](const ColumnWidth& cw) { posX += cw.width; }); + + return wxRect(wxPoint(posX, 0), wxSize(itCol->width, getColumnLabelHeight())); + } + return wxRect(); +} + + +void Grid::refreshCell(size_t row, ColumnType colType) +{ + const wxRect& colArea = getColumnLabelArea(colType); //returns empty rect if column not found + const wxRect& rowArea = rowLabelWin_->getRowLabelArea(row); //returns empty rect if row not found + if (colArea.width > 0 && rowArea.height > 0) + { + const wxPoint topLeft = CalcScrolledPosition(wxPoint(colArea.x, rowArea.y)); //logical -> window coordinates + const wxRect cellArea(topLeft, wxSize(colArea.width, rowArea.height)); + + getMainWin().RefreshRect(cellArea); + } +} + + +void Grid::setGridCursor(size_t row, GridEventPolicy rangeEventPolicy) +{ + mainWin_->setCursor(row, row); + makeRowVisible(row); + + selection_.clear(); //clear selection, do NOT fire event + selectRow(row, rangeEventPolicy); //set new selection + fire event +} + + +void Grid::makeRowVisible(size_t row) +{ + const wxRect labelRect = rowLabelWin_->getRowLabelArea(row); //returns empty rect if row not found + if (labelRect.height > 0) + { + int pixelsPerUnitY = 0; + GetScrollPixelsPerUnit(nullptr, &pixelsPerUnitY); + if (pixelsPerUnitY > 0) + { + const wxPoint scrollPosOld = GetViewStart(); + + const int clientPosY = CalcScrolledPosition(labelRect.GetTopLeft()).y; + if (clientPosY < 0) + { + const int scrollPosNewY = labelRect.y / pixelsPerUnitY; + Scroll(scrollPosOld.x, scrollPosNewY); //internally calls wxWindows::Update()! + updateWindowSizes(); //may show horizontal scroll bar if row column gets wider + Refresh(); + } + else if (clientPosY + labelRect.height > rowLabelWin_->GetClientSize().GetHeight()) + { + auto execScroll = [&](int clientHeight) + { + const int scrollPosNewY = numeric::intDivCeil(labelRect.y + labelRect.height - clientHeight, pixelsPerUnitY); + Scroll(scrollPosOld.x, scrollPosNewY); + updateWindowSizes(); //may show horizontal scroll bar if row column gets wider + Refresh(); + }; + + const int clientHeightBefore = rowLabelWin_->GetClientSize().GetHeight(); + execScroll(clientHeightBefore); + + //client height may decrease after scroll due to a new horizontal scrollbar, resulting in a partially visible last row + const int clientHeightAfter = rowLabelWin_->GetClientSize().GetHeight(); + if (clientHeightAfter < clientHeightBefore) + execScroll(clientHeightAfter); + } + } + } +} + + +void Grid::scrollTo(size_t row) +{ + const wxRect labelRect = rowLabelWin_->getRowLabelArea(row); //returns empty rect if row not found + if (labelRect.height > 0) + { + int pixelsPerUnitY = 0; + GetScrollPixelsPerUnit(nullptr, &pixelsPerUnitY); + if (pixelsPerUnitY > 0) + { + const int scrollPosNewY = labelRect.y / pixelsPerUnitY; + const wxPoint scrollPosOld = GetViewStart(); + + if (scrollPosOld.y != scrollPosNewY) //support polling + { + Scroll(scrollPosOld.x, scrollPosNewY); //internally calls wxWindows::Update()! + updateWindowSizes(); //may show horizontal scroll bar if row column gets wider + Refresh(); + } + } + } +} + + +bool Grid::Enable(bool enable) +{ + Refresh(); + return wxScrolledWindow::Enable(enable); +} + + +size_t Grid::getGridCursor() const +{ + return mainWin_->getCursor(); +} + + +int Grid::getBestColumnSize(size_t col) const +{ + if (dataView_ && col < visibleCols_.size()) + { + const ColumnType type = visibleCols_[col].type; + + wxInfoDC dc(mainWin_); + dc.SetFont(mainWin_->GetFont()); //harmonize with MainWin::render() + + const auto& [rowFirst, rowLast] = getVisibleRows(mainWin_->GetClientRect()); + + int maxSize = 0; + for (auto row = rowFirst; row < rowLast; ++row) + maxSize = std::max(maxSize, dataView_->getBestSize(dc, row, type)); + + return maxSize; + } + return -1; +} + + +void Grid::setColumnWidth(int width, size_t col, GridEventPolicy columnResizeEventPolicy, bool notifyAsync) +{ + if (col < visibleCols_.size()) + { + VisibleColumn& vcRs = visibleCols_[col]; + + const std::vector stretchedWidths = getColStretchedWidths(mainWin_->GetClientSize().GetWidth()); + if (stretchedWidths.size() != visibleCols_.size()) + { + assert(false); + return; + } + //CAVEATS: + //I. fixed-size columns: normalize offset so that resulting width is at least COLUMN_MIN_WIDTH_DIP: this is NOT enforced by getColWidths()! + //II. stretched columns: do not allow user to set offsets so small that they result in negative (non-normalized) widths: this gives an + //unusual delay when enlarging the column again later + width = std::max(width, dipToWxsize(COLUMN_MIN_WIDTH_DIP)); + + vcRs.offset = width - stretchedWidths[col]; //width := stretchedWidth + offset + + //III. resizing any column should normalize *all* other stretched columns' offsets considering current mainWinWidth! + // test case: + //1. have columns, both fixed-size and stretched, fit whole window width + //2. shrink main window width so that horizontal scrollbars are shown despite the streched column + //3. shrink a fixed-size column so that the scrollbars vanish and columns cover full width again + //4. now verify that the stretched column is resizing immediately if main window is enlarged again + for (size_t col2 = 0; col2 < visibleCols_.size(); ++col2) + if (visibleCols_[col2].stretch > 0) //normalize stretched columns only + visibleCols_[col2].offset = std::max(visibleCols_[col2].offset, dipToWxsize(COLUMN_MIN_WIDTH_DIP) - stretchedWidths[col2]); + + if (columnResizeEventPolicy == GridEventPolicy::allow) + { + GridColumnResizeEvent sizeEvent(vcRs.offset, vcRs.type); + if (notifyAsync) + GetEventHandler()->AddPendingEvent(sizeEvent); + else + GetEventHandler()->ProcessEvent(sizeEvent); + } + } + else + assert(false); +} + + +void Grid::autoSizeColumns(GridEventPolicy columnResizeEventPolicy) +{ + if (allowColumnResize_) + { + for (size_t col = 0; col < visibleCols_.size(); ++col) + { + const int bestWidth = getBestColumnSize(col); //return -1 on error + if (bestWidth >= 0) + setColumnWidth(bestWidth, col, columnResizeEventPolicy, true /*notifyAsync*/); + } + updateWindowSizes(); + Refresh(); + } +} + + +std::vector Grid::getColStretchedWidths(int clientWidth) const //final width = (normalized) (stretchedWidth + offset) +{ + assert(clientWidth >= 0); + clientWidth = std::max(clientWidth, 0); + int stretchTotal = 0; + for (const VisibleColumn& vc : visibleCols_) + { + assert(vc.stretch >= 0); + stretchTotal += vc.stretch; + } + + int remainingWidth = clientWidth; + + std::vector output; + + if (stretchTotal <= 0) + output.resize(visibleCols_.size()); //fill with zeros + else + { + for (const VisibleColumn& vc : visibleCols_) + { + const int width = clientWidth * vc.stretch / stretchTotal; //rounds down! + output.push_back(width); + remainingWidth -= width; + } + + //distribute *all* of clientWidth: should suffice to enlarge the first few stretched columns; no need to minimize total absolute error of distribution + if (remainingWidth > 0) + for (size_t col2 = 0; col2 < visibleCols_.size(); ++col2) + if (visibleCols_[col2].stretch > 0) + { + ++output[col2]; + if (--remainingWidth == 0) + break; + } + assert(remainingWidth == 0); + } + return output; +} + + +std::vector Grid::getColWidths() const +{ + return getColWidths(mainWin_->GetClientSize().GetWidth()); +} + + +std::vector Grid::getColWidths(int mainWinWidth) const //evaluate stretched columns +{ + const std::vector stretchedWidths = getColStretchedWidths(mainWinWidth); + assert(stretchedWidths.size() == visibleCols_.size()); + + std::vector output; + for (size_t col2 = 0; col2 < visibleCols_.size(); ++col2) + { + const auto& vc = visibleCols_[col2]; + int width = stretchedWidths[col2] + vc.offset; + + if (vc.stretch > 0) + width = std::max(width, dipToWxsize(COLUMN_MIN_WIDTH_DIP)); //normalization really needed here: e.g. smaller main window would result in negative width + else + width = std::max(width, 0); //support smaller width than COLUMN_MIN_WIDTH_DIP if set via configuration + + output.push_back({vc.type, width}); + } + return output; +} + + +int Grid::getColWidthsSum(int mainWinWidth) const +{ + int sum = 0; + for (const ColumnWidth& cw : getColWidths(mainWinWidth)) + sum += cw.width; + return sum; +} diff --git a/wx+/grid.h b/wx+/grid.h new file mode 100644 index 0000000..9ee811b --- /dev/null +++ b/wx+/grid.h @@ -0,0 +1,404 @@ +// ***************************************************************************** +// * 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 GRID_H_834702134831734869987 +#define GRID_H_834702134831734869987 + +#include +#include +#include +#include +#include + + +//a user-friendly, extensible and high-performance grid control +namespace zen +{ +enum class ColumnType { none = -1 }; //user-defiend column type +enum class HoverArea { none = -1 }; //user-defined area for mouse selections for a given row (may span multiple columns or split a single column into multiple areas) + +//------------------------ events ------------------------------------------------ +//example: wnd.Bind(EVENT_GRID_COL_LABEL_LEFT_CLICK, [this](GridClickEvent& event) { onGridLeftClick(event); }); + +struct GridClickEvent; +struct GridSelectEvent; +struct GridLabelClickEvent; +struct GridColumnResizeEvent; +struct GridContextMenuEvent; + +wxDECLARE_EVENT(EVENT_GRID_MOUSE_LEFT_DOUBLE, GridClickEvent); +wxDECLARE_EVENT(EVENT_GRID_MOUSE_LEFT_DOWN, GridClickEvent); +wxDECLARE_EVENT(EVENT_GRID_MOUSE_RIGHT_DOWN, GridClickEvent); + +wxDECLARE_EVENT(EVENT_GRID_SELECT_RANGE, GridSelectEvent); +//NOTE: neither first nor second row need to match EVENT_GRID_MOUSE_LEFT_DOWN/EVENT_GRID_MOUSE_LEFT_UP: user holding SHIFT; moving out of window... + +wxDECLARE_EVENT(EVENT_GRID_COL_LABEL_MOUSE_LEFT, GridLabelClickEvent); +wxDECLARE_EVENT(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, GridLabelClickEvent); +wxDECLARE_EVENT(EVENT_GRID_COL_RESIZE, GridColumnResizeEvent); + +//wxContextMenuEvent? => generated by wxWidgets when right mouse down/up is not handled; even OS-dependent in which case event is generated +//=> inappropriate! we know better when to show context! +wxDECLARE_EVENT(EVENT_GRID_CONTEXT_MENU, GridContextMenuEvent); + + +struct GridClickEvent : public wxEvent +{ + GridClickEvent(wxEventType et, ptrdiff_t row, HoverArea hoverArea, const wxPoint& mousePos) : + wxEvent(0 /*winid*/, et), row_(row), hoverArea_(hoverArea), mousePos_(mousePos) {} + GridClickEvent* Clone() const override { return new GridClickEvent(*this); } + + const ptrdiff_t row_; //-1 for invalid position, >= rowCount if out of range + const HoverArea hoverArea_; //may be HoverArea::none + const wxPoint mousePos_; //Grid-relative coordinates +}; + +struct GridSelectEvent : public wxEvent +{ + GridSelectEvent(size_t rowFirst, size_t rowLast, bool positive, const GridClickEvent* mouseClick) : + wxEvent(0 /*winid*/, EVENT_GRID_SELECT_RANGE), rowFirst_(rowFirst), rowLast_(rowLast), positive_(positive), + mouseClick_(mouseClick ? *mouseClick : std::optional()) { assert(rowFirst <= rowLast); } + GridSelectEvent* Clone() const override { return new GridSelectEvent(*this); } + + const size_t rowFirst_; //selected range: [rowFirst_, rowLast_) + const size_t rowLast_; // + const bool positive_; //"false" when clearing selection! + const std::optional mouseClick_; //filled unless selection was performed via keyboard shortcuts +}; + +struct GridLabelClickEvent : public wxEvent +{ + GridLabelClickEvent(wxEventType et, ColumnType colType, const wxPoint& mousePos) : wxEvent(0 /*winid*/, et), colType_(colType), mousePos_(mousePos) {} + GridLabelClickEvent* Clone() const override { return new GridLabelClickEvent(*this); } + + const ColumnType colType_; //may be ColumnType::none + const wxPoint mousePos_; //Grid-relative coordinates +}; + +struct GridColumnResizeEvent : public wxEvent +{ + GridColumnResizeEvent(int offset, ColumnType colType) : wxEvent(0 /*winid*/, EVENT_GRID_COL_RESIZE), colType_(colType), offset_(offset) {} + GridColumnResizeEvent* Clone() const override { return new GridColumnResizeEvent(*this); } + + const ColumnType colType_; + const int offset_; +}; + +struct GridContextMenuEvent : public wxEvent +{ + GridContextMenuEvent(const wxPoint& mousePos) : wxEvent(0 /*winid*/, EVENT_GRID_CONTEXT_MENU), mousePos_(mousePos) {} + GridContextMenuEvent* Clone() const override { return new GridContextMenuEvent(*this); } + + const wxPoint mousePos_; //Grid-relative coordinates +}; +//------------------------------------------------------------------------------------------------------------ + +class GridData +{ +public: + virtual ~GridData() {} + + virtual size_t getRowCount() const = 0; + + //cell area: + virtual std::wstring getValue(size_t row, ColumnType colType) const = 0; + virtual void renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected, HoverArea rowHover); //default implementation + virtual void renderCell (wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover); + virtual int getBestSize (const wxReadOnlyDC& dc, size_t row, ColumnType colType); //must correspond to renderCell()! + virtual HoverArea getMouseHover(const wxReadOnlyDC& dc, size_t row, ColumnType colType, int cellRelativePosX, int cellWidth) { return HoverArea::none; } + virtual std::wstring getToolTip (size_t row, ColumnType colType, HoverArea rowHover) { return std::wstring(); } + + //label area: + virtual std::wstring getColumnLabel(ColumnType colType) const = 0; + virtual void renderColumnLabel(wxDC& dc, const wxRect& rect, ColumnType colType, bool enabled, bool highlighted); //default implementation + virtual std::wstring getToolTip(ColumnType colType) const { return std::wstring(); } + + //optional helper routines: + static int getColumnGapLeft(); //for left-aligned text + static wxColor getColorSelectionGradientFrom(); + static wxColor getColorSelectionGradientTo(); + + static void drawCellText(wxDC& dc, const wxRect& rect, const std::wstring_view text, + int alignment = wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, const wxSize* textExtentHint = nullptr); + static wxRect drawCellBorder(wxDC& dc, const wxRect& rect); //returns inner rectangle + + static wxRect drawColumnLabelBackground(wxDC& dc, const wxRect& rect, bool highlighted); //returns inner rectangle + static void drawColumnLabelText (wxDC& dc, const wxRect& rect, const std::wstring& text, bool enabled); +}; + + +enum class GridEventPolicy +{ + allow, + deny +}; + + +class Grid : public wxScrolledWindow +{ +public: + Grid(wxWindow* parent, + wxWindowID id = wxID_ANY, + const wxPoint& pos = wxDefaultPosition, + const wxSize& size = wxDefaultSize, + long style = wxTAB_TRAVERSAL | wxNO_BORDER, + const wxString& name = wxASCII_STR(wxPanelNameStr)); + + size_t getRowCount() const; + + void setRowHeight(int height); + int getRowHeight() const; + + struct ColAttributes + { + ColumnType type = ColumnType::none; + //first, client width is partitioned according to all available stretch factors, then "offset_" is added + //universal model: a non-stretched column has stretch factor 0 with the "offset" becoming identical to final width! + int offset = 0; + int stretch = 0; //>= 0 + bool visible = false; + }; + + void setColumnConfig(const std::vector& attr); //set column count + widths + std::vector getColumnConfig() const; + + void setDataProvider(const std::shared_ptr& dataView) { dataView_ = dataView; } + /**/ GridData* getDataProvider() { return dataView_.get(); } + const GridData* getDataProvider() const { return dataView_.get(); } + //----------------------------------------------------------------------------- + + void setColumnLabelHeight(int height); + int getColumnLabelHeight() const; + void showRowLabel(bool visible); + + enum ScrollBarStatus + { + SB_SHOW_AUTOMATIC, + SB_SHOW_ALWAYS, + SB_SHOW_NEVER, + }; + //alternative until wxScrollHelper::ShowScrollbars() becomes available in wxWidgets 2.9 + void showScrollBars(ScrollBarStatus horizontal, ScrollBarStatus vertical); + + std::vector getSelectedRows() const { return selection_.get(); } + + void selectRow(size_t row, GridEventPolicy rangeEventPolicy); + void selectAllRows (GridEventPolicy rangeEventPolicy); //turn off range selection event when calling this function in an event handler to avoid recursion! + void clearSelection(GridEventPolicy rangeEventPolicy); // + void selectRange(size_t rowFirst, size_t rowLast, bool positive, GridEventPolicy rangeEventPolicy); //select [rowFirst, rowLast) + + void scrollDelta(int deltaX, int deltaY); //in scroll units + + wxWindow& getCornerWin (); + wxWindow& getRowLabelWin(); + wxWindow& getColLabelWin(); + wxWindow& getMainWin (); + const wxWindow& getMainWin() const; + + struct ColumnPosInfo + { + ColumnType colType = ColumnType::none; //ColumnType::none => no column at posX! + int cellRelativePosX = 0; + int colWidth = 0; + }; + ColumnPosInfo getColumnAtWinPos(int posX) const; + ptrdiff_t getRowAtWinPos(int posY) const; //return -1 for invalid position, >= rowCount if out of range + + std::pair getVisibleRows(const wxRect& clientRect) const; //returns range [begin, end) + + void refreshCell(size_t row, ColumnType colType); + + void enableColumnMove (bool value) { allowColumnMove_ = value; } + void enableColumnResize(bool value) { allowColumnResize_ = value; } + + void setGridCursor(size_t row, GridEventPolicy rangeEventPolicy); //set + show + select cursor + size_t getGridCursor() const; //returns row + + void scrollTo(size_t row); + + void makeRowVisible(size_t row); + + void Refresh(bool eraseBackground = true, const wxRect* rect = nullptr) override; + bool Enable(bool enable = true) override; + + //############################################################################################################ + +private: + void onKeyDown(wxKeyEvent& event); + + void updateWindowSizes(bool updateScrollbar = true); + + void selectWithCursor(ptrdiff_t row); //emits GridSelectEvent + + wxSize GetSizeAvailableForScrollTarget(const wxSize& size) override; //required since wxWidgets 2.9 if SetTargetWindow() is used + + + int getBestColumnSize(size_t col) const; //return -1 on error + + void autoSizeColumns(GridEventPolicy columnResizeEventPolicy); + + friend class GridData; + class SubWindow; + class CornerWin; + class RowLabelWin; + class ColLabelWin; + class MainWin; + + class Selection + { + public: + void resize(size_t rowCount) { selected_.resize(rowCount, false); } + + size_t gridSize() const { return selected_.size(); } + + std::vector get() const + { + std::vector result; + for (size_t row = 0; row < selected_.size(); ++row) + if (selected_[row] != 0) + result.push_back(row); + return result; + } + + bool isSelected(size_t row) const { return row < selected_.size() ? selected_[row] != 0 : false; } + + bool matchesRange(size_t rowFirst, size_t rowLast, bool positive) + { + if (rowFirst <= rowLast && rowLast <= selected_.size()) + { + const auto rangeEnd = selected_.begin() + rowLast; + return std::find(selected_.begin() + rowFirst, rangeEnd, static_cast(!positive)) == rangeEnd; + } + else + { + assert(false); + return false; + } + } + + void clear() { selectRange(0, selected_.size(), false); } + + void selectRange(size_t rowFirst, size_t rowLast, bool positive = true) //select [rowFirst, rowLast), trims if required! + { + assert(rowFirst <= rowLast && rowLast <= selected_.size()); + if (rowFirst < rowLast) + std::fill(selected_.begin() + std::min(rowFirst, selected_.size()), + selected_.begin() + std::min(rowLast, selected_.size()), positive); + } + + private: + std::vector selected_; //effectively a vector of size "number of rows" + }; + + struct VisibleColumn + { + ColumnType type = ColumnType::none; + int offset = 0; + int stretch = 0; //>= 0 + }; + + struct ColumnWidth + { + ColumnType type = ColumnType::none; + int width = 0; + }; + std::vector getColWidths() const; // + std::vector getColWidths(int mainWinWidth) const; //evaluate stretched columns + int getColWidthsSum(int mainWinWidth) const; + std::vector getColStretchedWidths(int clientWidth) const; //final width = (normalized) (stretchedWidth + offset) + + std::optional getColWidth(size_t col) const + { + const auto& widths = getColWidths(); + if (col < widths.size()) + return widths[col].width; + return {}; + } + + void setColumnWidth(int width, size_t col, GridEventPolicy columnResizeEventPolicy, bool notifyAsync = false); + + wxRect getColumnLabelArea(ColumnType colType) const; //returns empty rect if column not found + + //select inclusive range [rowFrom, rowTo] + void selectRange2(size_t rowFirst, size_t rowLast, bool positive, const GridClickEvent* mouseClick, GridEventPolicy rangeEventPolicy); + + bool isSelected(size_t row) const { return selection_.isSelected(row); } + + struct ColAction + { + bool wantResize = false; //"!wantResize" means "move" or "single click" + size_t col = 0; + }; + void moveColumn(size_t colFrom, size_t colTo); + + ColumnType colToType(size_t col) const; //returns ColumnType::none on error + + /* Grid window layout: + _______________________________ + | CornerWin | ColLabelWin | + |_____________|_______________| + | RowLabelWin | MainWin | + | | | + |_____________|_______________| */ + CornerWin* cornerWin_; + RowLabelWin* rowLabelWin_; + ColLabelWin* colLabelWin_; + MainWin* mainWin_; + + ScrollBarStatus showScrollbarH_ = SB_SHOW_AUTOMATIC; + ScrollBarStatus showScrollbarV_ = SB_SHOW_AUTOMATIC; + + bool drawRowLabel_ = true; + + std::shared_ptr dataView_; + Selection selection_; + bool allowColumnMove_ = true; + bool allowColumnResize_ = true; + + std::vector visibleCols_; //individual widths, type and total column count + std::vector oldColAttributes_; //visible + nonvisible columns; use for conversion in setColumnConfig()/getColumnConfig() *only*! + + size_t rowCountOld_ = 0; //at the time of last Grid::Refresh() + + int scrollBarHeightH_ = 0; //optional: may not be known (yet) + int scrollBarWidthV_ = 0; // +}; + +//------------------------------------------------------------------------------------------------------------ + +template +std::vector makeConsistent(const std::vector& attribs, const std::vector& defaults) +{ + std::vector output = attribs; + append(output, defaults); //make sure each type is existing! + removeDuplicatesStable(output, [](const ColAttrReal& lhs, const ColAttrReal& rhs) { return lhs.type < rhs.type; }); + return output; +} + + +template +std::vector convertColAttributes(const std::vector& attribs, const std::vector& defaults) +{ + std::vector output; + for (const ColAttrReal& ca : makeConsistent(attribs, defaults)) + output.push_back({static_cast(ca.type), ca.offset, ca.stretch, ca.visible}); + return output; +} + + +template +std::vector convertColAttributes(const std::vector& attribs) +{ + using ColTypeReal = decltype(ColAttrReal().type); + + std::vector output; + for (const Grid::ColAttributes& ca : attribs) + output.push_back({static_cast(ca.type), ca.offset, ca.stretch, ca.visible}); + return output; +} +} + +#endif //GRID_H_834702134831734869987 diff --git a/wx+/image_holder.h b/wx+/image_holder.h new file mode 100644 index 0000000..8902104 --- /dev/null +++ b/wx+/image_holder.h @@ -0,0 +1,72 @@ +// ***************************************************************************** +// * 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 IMAGE_HOLDER_H_284578426342567457 +#define IMAGE_HOLDER_H_284578426342567457 + +#include + #include +//used by fs/abstract.h => check carefully before adding dependencies! +//DO NOT add any wx/wx+ includes! + +namespace zen +{ +struct ImageHolder //prepare conversion to wxImage as much as possible while staying thread-safe (in contrast to wxIcon/wxBitmap) +{ + ImageHolder() {} + + ImageHolder(int w, int h, bool withAlpha) : //init with memory allocated + width_(w), height_(h), + rgb_( static_cast(::malloc(w * h * 3))), + alpha_(withAlpha ? static_cast(::malloc(w * h)) : nullptr) {} + + ImageHolder (ImageHolder&&) noexcept = default; // + ImageHolder& operator=(ImageHolder&&) noexcept = default; //move semantics only! + ImageHolder (const ImageHolder&) = delete; // + ImageHolder& operator=(const ImageHolder&) = delete; // + + explicit operator bool() const { return rgb_.get() != nullptr; } + + int getWidth () const { return width_; } + int getHeight() const { return height_; } + + unsigned char* getRgb () { return rgb_ .get(); } + unsigned char* getAlpha() { return alpha_.get(); } + + unsigned char* releaseRgb () { return rgb_ .release(); } + unsigned char* releaseAlpha() { return alpha_.release(); } + +private: + struct CLibFree { void operator()(unsigned char* p) const { ::free(p); } }; //use malloc/free to allow direct move into wxImage! + + int width_ = 0; + int height_ = 0; + std::unique_ptr rgb_; //optional + std::unique_ptr alpha_; // +}; + + +struct FileIconHolder +{ + //- GTK is NOT thread-safe! The most we can do from worker threads is retrieve a GIcon and later *try*(!) to convert it on the MAIN THREAD! >:( what a waste + //- at least g_file_query_info() *always* returns G_IS_THEMED_ICON(gicon) for native file systems => main thread won't block https://gitlab.gnome.org/GNOME/glib/blob/master/gio/glocalfileinfo.c#L1733 + //- what about G_IS_FILE_ICON(gicon), G_IS_LOADABLE_ICON(gicon)? => may block! => do NOT convert on main thread! (no big deal: doesn't seem to occur in practice) + FileIconHolder() {}; + + FileIconHolder(GIcon* icon, int maxSz) : //takes ownership! + gicon(icon), + maxSize(maxSz) {} + + struct GiconFree { void operator()(GIcon* icon) const { ::g_object_unref(icon); } }; + + std::unique_ptr gicon; + int maxSize = 0; + + explicit operator bool() const { return static_cast(gicon); } +}; +} + +#endif //IMAGE_HOLDER_H_284578426342567457 diff --git a/wx+/image_resources.cpp b/wx+/image_resources.cpp new file mode 100644 index 0000000..5611df8 --- /dev/null +++ b/wx+/image_resources.cpp @@ -0,0 +1,340 @@ +// ***************************************************************************** +// * 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 "image_resources.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include "image_tools.h" +#include "image_holder.h" +#include "dc.h" + +using namespace zen; + + +namespace +{ +ImageHolder xbrzScale(int width, int height, const unsigned char* imageRgb, const unsigned char* imageAlpha, int hqScale) +{ + assert(imageRgb && imageAlpha && width > 0 && height > 0); //see convertToVanillaImage() + if (width <= 0 || height <= 0) + return ImageHolder(0, 0, true /*withAlpha*/); + + const int hqWidth = width * hqScale; + const int hqHeight = height * hqScale; + + //get rid of allocation and buffer std::vector<> at thread-level? => no discernable perf improvement + std::vector buf(hqWidth * hqHeight + width * height); + uint32_t* const argbSrc = buf.data() + hqWidth * hqHeight; + uint32_t* const xbrTrg = buf.data(); + + //convert RGB (RGB byte order) to ARGB (BGRA byte order) + { + const unsigned char* rgb = imageRgb; + const unsigned char* rgbEnd = rgb + 3 * width * height; + const unsigned char* alpha = imageAlpha; + uint32_t* out = argbSrc; + + for (; rgb < rgbEnd; rgb += 3) + *out++ = xbrz::makePixel(*alpha++, rgb[0], rgb[1], rgb[2]); + } + //----------------------------------------------------- + xbrz::scale(hqScale, //size_t factor - valid range: 2 - SCALE_FACTOR_MAX + argbSrc, //const uint32_t* src + xbrTrg, //uint32_t* trg + width, height, //int srcWidth, int srcHeight + xbrz::ColorFormat::argbUnbuffered); //ColorFormat colFmt + //test: total xBRZ scaling time with ARGB: 300ms, ARGB unbuffered: 50ms + //----------------------------------------------------- + //convert BGRA to RGB + alpha + ImageHolder trgImg(hqWidth, hqHeight, true /*withAlpha*/); + + std::for_each(xbrTrg, xbrTrg + hqWidth * hqHeight, [rgb = trgImg.getRgb(), alpha = trgImg.getAlpha()](uint32_t col) mutable + { + *alpha++ = xbrz::getAlpha(col); + *rgb++ = xbrz::getRed (col); + *rgb++ = xbrz::getGreen(col); + *rgb++ = xbrz::getBlue (col); + }); + return trgImg; +} + + +auto createScalerTask(const std::string& imageName, const wxImage& img, int hqScale, Protected>>& protResult) +{ + assert(runningOnMainThread()); + return [imageName, + width = img.GetWidth(), // + height = img.GetHeight(), //don't call these wxWidgets functions from worker thread + rgb = img.GetData(), // + alpha = img.GetAlpha(), // + hqScale, &protResult] + { + ImageHolder ih = xbrzScale(width, height, rgb, alpha, hqScale); + protResult.access([&](std::vector>& result) { result.emplace_back(imageName, std::move(ih)); }); + }; +} + + +class HqParallelScaler +{ +public: + explicit HqParallelScaler(int hqScale) : hqScale_(hqScale) { assert(hqScale > 1); } + + ~HqParallelScaler() { threadGroup_ = {}; } //imgKeeper_ must out-live threadGroup!!! + + void add(const std::string& imageName, const wxImage& img) + { + assert(runningOnMainThread()); + imgKeeper_.push_back(img); //retain (ref-counted) wxImage so that the rgb/alpha pointers remain valid after passed to threads + threadGroup_->run(createScalerTask(imageName, img, hqScale_, protResult_)); + } + + std::unordered_map waitAndGetResult() + { + assert(runningOnMainThread()); + threadGroup_->wait(); + + std::unordered_map output; + + protResult_.access([&](std::vector>& result) + { + for (auto& [imageName, ih] : result) + { + wxImage img(ih.getWidth(), ih.getHeight(), ih.releaseRgb(), false /*static_data*/); //pass ownership + img.SetAlpha(ih.releaseAlpha(), false /*static_data*/); + + output.emplace(imageName, std::move(img)); + } + }); + return output; + } + +private: + const int hqScale_; + std::vector imgKeeper_; + Protected>> protResult_; + + using TaskType = FunctionReturnTypeT; + std::optional> threadGroup_{ThreadGroup(std::max(std::thread::hardware_concurrency(), 1), Zstr("xBRZ Scaler"))}; + //hardware_concurrency() == 0 if "not computable or well defined" +}; + +//================================================================================================ +//================================================================================================ + +class ImageBuffer +{ +public: + explicit ImageBuffer(const Zstring& filePath); //throw FileError + + const wxImage& getImage(const std::string& name, int maxWidth /*optional*/, int maxHeight /*optional*/); + +private: + ImageBuffer (const ImageBuffer&) = delete; + ImageBuffer& operator=(const ImageBuffer&) = delete; + + const wxImage& getRawImage (const std::string& name); + const wxImage& getHqScaledImage(const std::string& name); + + std::unordered_map imagesRaw_; + std::unordered_map imagesScaled_; + + std::optional hqScaler_; + + using OutImageKey = std::tuple; + + struct OutImageKeyHash + { + size_t operator()(const OutImageKey& imKey) const + { + const auto& [name, height] = imKey; + + FNV1aHash hash; + for (const char c : name) + hash.add(c); + + hash.add(height); + + return hash.get(); + } + }; + std::unordered_map imagesOut_; +}; + + +ImageBuffer::ImageBuffer(const Zstring& zipPath) //throw FileError +{ + std::vector> streams; + + try //to load from ZIP first: + { + //wxFFileInputStream/wxZipInputStream loads in junks of 512 bytes => WTF!!! => implement sane file loading: + const std::string rawStream = getFileContent(zipPath, nullptr /*notifyUnbufferedIO*/); //throw FileError + wxMemoryInputStream memStream(rawStream.c_str(), rawStream.size()); //does not take ownership + wxZipInputStream zipStream(memStream, wxConvUTF8); + //do NOT rely on wxConvLocal! On failure shows unhelpful popup "Cannot convert from the charset 'Unknown encoding (-1)'!" + + while (const auto& entry = std::unique_ptr(zipStream.GetNextEntry())) //take ownership! + if (std::string stream(entry->GetSize(), '\0'); + zipStream.ReadAll(stream.data(), stream.size())) + streams.emplace_back(utfTo(entry->GetName()), std::move(stream)); + else + assert(false); + } + catch (FileError&) //fall back to folder: dev build (only!?) + { + const Zstring fallbackFolder = beforeLast(zipPath, Zstr(".zip"), IfNotFoundReturn::none); + if (!itemExists(fallbackFolder)) //throw FileError + throw; + + traverseFolder(fallbackFolder, [&](const FileInfo& fi) + { + if (endsWith(fi.fullPath, Zstr(".png"))) + { + std::string stream = getFileContent(fi.fullPath, nullptr /*notifyUnbufferedIO*/); //throw FileError + streams.emplace_back(fi.itemName, std::move(stream)); + } + }, nullptr, nullptr); //throw FileError + } + //-------------------------------------------------------------------- + + wxImage::AddHandler(new wxPNGHandler/*ownership passed*/); //activate support for .png files + + //do we need xBRZ scaling for high quality DPI images? + const int hqScale = std::clamp(static_cast(std::ceil(getScreenDpiScale())), 1, xbrz::SCALE_FACTOR_MAX); + //even for 125% DPI scaling, "2xBRZ + bilinear downscale" gives a better result than mere "125% bilinear upscale"! + if (hqScale > 1) + hqScaler_.emplace(hqScale); + + for (const auto& [fileName, stream] : streams) + if (endsWith(fileName, Zstr(".png"))) + { + wxMemoryInputStream wxstream(stream.c_str(), stream.size()); //stream does not take ownership of data + + wxImage img(wxstream, wxBITMAP_TYPE_PNG); + assert(img.IsOk()); + + //end this alpha/no-alpha/mask/wxDC::DrawBitmap/RTL/high-contrast-scheme interoperability nightmare here and now!!!! + //=> there's only one type of wxImage: with alpha channel, no mask!!! + convertToVanillaImage(img); + + const std::string imageName = utfTo(beforeLast(fileName, Zstr("."), IfNotFoundReturn::none)); + + imagesRaw_.emplace(imageName, img); + if (hqScaler_) + hqScaler_->add(imageName, img); //scale in parallel! + else + imagesScaled_.emplace(imageName, img); + + //wxBitmap::NewFromPNGData(stream.c_str(), stream.size())? + // => Windows: just a (slow!) wrapper for wxBitmap(wxImage())! + } + else + assert(false); +} + + +const wxImage& ImageBuffer::getRawImage(const std::string& name) +{ + if (auto it = imagesRaw_.find(name); + it != imagesRaw_.end()) + return it->second; + + assert(false); + return wxNullImage; +} + + +const wxImage& ImageBuffer::getHqScaledImage(const std::string& name) +{ + //test: this function is first called about 220ms after ImageBuffer::ImageBuffer() has ended + // => should be enough time to finish xBRZ scaling in parallel (which takes 50ms) + //debug perf: extra 800-1000ms during startup + if (hqScaler_) + { + imagesScaled_ = hqScaler_->waitAndGetResult(); + hqScaler_.reset(); + } + + if (auto it = imagesScaled_.find(name); + it != imagesScaled_.end()) + return it->second; + + assert(false); + return wxNullImage; +} + + +const wxImage& ImageBuffer::getImage(const std::string& name, int maxWidth /*optional*/, int maxHeight /*optional*/) +{ + const wxImage& rawImg = getRawImage(name); + + const wxSize dpiSize(dipToScreen(rawImg.GetWidth ()), + dipToScreen(rawImg.GetHeight())); + + int outHeight = dpiSize.y; + if (maxWidth >= 0 && maxWidth < dpiSize.x) + outHeight = numeric::intDivRound(maxWidth * rawImg.GetHeight(), rawImg.GetWidth()); + + if (maxHeight >= 0 && maxHeight < outHeight) + outHeight = maxHeight; + + const OutImageKey imgKey{name, outHeight}; + + auto it = imagesOut_.find(imgKey); + if (it == imagesOut_.end()) + { + if (rawImg.GetHeight() >= outHeight) //=> skip needless xBRZ upscaling + it = imagesOut_.emplace(imgKey, shrinkImage(rawImg, -1 /*maxWidth*/, outHeight)).first; + else if (rawImg.GetHeight() >= 0.9 * outHeight) //almost there: also no need for xBRZ-scale + it = imagesOut_.emplace(imgKey, bilinearScale(rawImg, numeric::intDivRound(outHeight * rawImg.GetWidth(), rawImg.GetHeight()), outHeight)).first; + else //however: for 125% DPI scaling, "2xBRZ + bilinear downscale" gives a better result than mere "125% bilinear upscale" + it = imagesOut_.emplace(imgKey, shrinkImage(getHqScaledImage(name), -1 /*maxWidth*/, outHeight)).first; + } + return it->second; +} + + +std::optional globalImageBuffer; +} + + +void zen::imageResourcesInit(const Zstring& zipPath) //throw FileError +{ + assert(runningOnMainThread()); //wxWidgets is not thread-safe! + assert(!globalImageBuffer); + globalImageBuffer.emplace(zipPath); //throw FileError +} + + +void zen::imageResourcesCleanup() +{ + assert(runningOnMainThread()); //wxWidgets is not thread-safe! + assert(globalImageBuffer); + globalImageBuffer.reset(); +} + + +const wxImage& zen::loadImage(const std::string& name, int maxWidth /*optional*/, int maxHeight /*optional*/) +{ + assert(runningOnMainThread()); //wxWidgets is not thread-safe! + assert(globalImageBuffer); + if (globalImageBuffer) + return globalImageBuffer->getImage(name, maxWidth, maxHeight); + return wxNullImage; +} + + +const wxImage& zen::loadImage(const std::string& name, int maxSize) +{ + return loadImage(name, maxSize, maxSize); +} diff --git a/wx+/image_resources.h b/wx+/image_resources.h new file mode 100644 index 0000000..0aa4dce --- /dev/null +++ b/wx+/image_resources.h @@ -0,0 +1,24 @@ +// ***************************************************************************** +// * 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 IMAGE_RESOURCES_H_8740257825342532457 +#define IMAGE_RESOURCES_H_8740257825342532457 + +#include +#include + + +namespace zen +{ +//pass resources .zip file at application startup +void imageResourcesInit(const Zstring& zipPath); //throw FileError +void imageResourcesCleanup(); + +const wxImage& loadImage(const std::string& name, int maxWidth /*optional*/, int maxHeight /*optional*/); +const wxImage& loadImage(const std::string& name, int maxSize = -1); +} + +#endif //IMAGE_RESOURCES_H_8740257825342532457 diff --git a/wx+/image_tools.cpp b/wx+/image_tools.cpp new file mode 100644 index 0000000..13de0a7 --- /dev/null +++ b/wx+/image_tools.cpp @@ -0,0 +1,506 @@ +// ***************************************************************************** +// * 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 "image_tools.h" +#include +#include +#include +#include +//#include +#include +#include +#include + +using namespace zen; + + +namespace +{ +template +void copyImageBlock(const unsigned char* src, int srcWidth, + /**/ unsigned char* trg, int trgWidth, int blockWidth, int blockHeight) +{ + assert(srcWidth >= blockWidth && trgWidth >= blockWidth); + const int srcPitch = srcWidth * PixBytes; + const int trgPitch = trgWidth * PixBytes; + const int blockPitch = blockWidth * PixBytes; + for (int y = 0; y < blockHeight; ++y) + std::memcpy(trg + y * trgPitch, src + y * srcPitch, blockPitch); +} + + +//...what wxImage::Resize() wants to be when it grows up +void copySubImage(const wxImage& src, wxPoint srcPos, + /**/ wxImage& trg, wxPoint trgPos, wxSize blockSize) +{ + auto pointClamp = [](const wxPoint& pos, const wxImage& img) -> wxPoint + { + return { + std::clamp(pos.x, 0, img.GetWidth ()), + std::clamp(pos.y, 0, img.GetHeight())}; + }; + auto subtract = [](const wxPoint& lhs, const wxPoint& rhs) { return wxSize{lhs.x - rhs.x, lhs.y - rhs.y}; }; + //work around yet another wxWidgets screw up: WTF does "operator-(wxPoint, wxPoint)" return wxPoint instead of wxSize!?? + + const wxPoint trgPos2 = pointClamp(trgPos, trg); + const wxPoint trgPos2End = pointClamp(trgPos + blockSize, trg); + + blockSize = subtract(trgPos2End, trgPos2); + srcPos += subtract(trgPos2, trgPos); + trgPos = trgPos2; + if (blockSize.x <= 0 || blockSize.y <= 0) + return; + + const wxPoint srcPos2 = pointClamp(srcPos, src); + const wxPoint srcPos2End = pointClamp(srcPos + blockSize, src); + + blockSize = subtract(srcPos2End, srcPos2); + trgPos += subtract(srcPos2, srcPos); + srcPos = srcPos2; + if (blockSize.x <= 0 || blockSize.y <= 0) + return; + //what if target block size is bigger than source block size? should we clear the area that is not copied from source? + + copyImageBlock<3>(src.GetData() + 3 * (srcPos.x + srcPos.y * src.GetWidth()), src.GetWidth(), + trg.GetData() + 3 * (trgPos.x + trgPos.y * trg.GetWidth()), trg.GetWidth(), + blockSize.x, blockSize.y); + + copyImageBlock<1>(src.GetAlpha() + srcPos.x + srcPos.y * src.GetWidth(), src.GetWidth(), + trg.GetAlpha() + trgPos.x + trgPos.y * trg.GetWidth(), trg.GetWidth(), + blockSize.x, blockSize.y); +} + + +void copyImageLayover(const wxImage& src, + /**/ wxImage& trg, wxPoint trgPos) +{ + const int srcWidth = src.GetWidth (); + const int srcHeight = src.GetHeight(); + const int trgWidth = trg.GetWidth(); + + assert(0 <= trgPos.x && trgPos.x + srcWidth <= trgWidth ); //draw area must be a + assert(0 <= trgPos.y && trgPos.y + srcHeight <= trg.GetHeight()); //subset of target image! + + const unsigned char* srcRgb = src.GetData(); + const unsigned char* srcAlpha = src.GetAlpha(); + + for (int y = 0; y < srcHeight; ++y) + { + unsigned char* trgRgb = trg.GetData () + 3 * (trgPos.x + (trgPos.y + y) * trgWidth); + unsigned char* trgAlpha = trg.GetAlpha() + trgPos.x + (trgPos.y + y) * trgWidth; + + for (int x = 0; x < srcWidth; ++x) + { + const unsigned char w1 = *srcAlpha; //alpha-composition interpreted as weighted average + const unsigned char w2 = numeric::intDivRound(*trgAlpha * (255 - w1), 255); + const unsigned char wSum = w1 + w2; + + auto calcColor = [w1, w2, wSum](unsigned char colsrc, unsigned char colTrg) + { + if (w1 == 0) return colTrg; + if (w2 == 0) return colsrc; + + //https://en.wikipedia.org/wiki/Alpha_compositing + //Limitation: alpha should be applied in gamma-decoded linear RGB space: https://ssp.impulsetrain.com/gamma-premult.html + // => srgbEncode((srgbDecode(colsrc) * w1 + srgbDecode(colTrg) * w2) / wSum) + return static_cast(numeric::intDivRound(colsrc * w1 + colTrg * w2, int(wSum))); + }; + trgRgb[0] = calcColor(srcRgb[0], trgRgb[0]); + trgRgb[1] = calcColor(srcRgb[1], trgRgb[1]); + trgRgb[2] = calcColor(srcRgb[2], trgRgb[2]); + + *trgAlpha = wSum; + + srcRgb += 3; + trgRgb += 3; + ++srcAlpha; + ++trgAlpha; + } + } +} +} + + +wxImage zen::stackImages(const wxImage& img1, const wxImage& img2, ImageStackLayout dir, ImageStackAlignment align, int gap) +{ + assert(gap >= 0); + gap = std::max(0, gap); + + const int img1Width = img1.GetWidth (); + const int img1Height = img1.GetHeight(); + const int img2Width = img2.GetWidth (); + const int img2Height = img2.GetHeight(); + + const wxSize newSize = dir == ImageStackLayout::horizontal ? + wxSize(img1Width + gap + img2Width, std::max(img1Height, img2Height)) : + wxSize(std::max(img1Width, img2Width), img1Height + gap + img2Height); + + wxImage output(newSize); + output.SetAlpha(); + std::memset(output.GetAlpha(), wxIMAGE_ALPHA_TRANSPARENT, newSize.x * newSize.y); + + auto calcPos = [&](int imageExtent, int totalExtent) + { + switch (align) + { + case ImageStackAlignment::center: + return (totalExtent - imageExtent) / 2; + case ImageStackAlignment::left: //or top + return 0; + case ImageStackAlignment::right: //or bottom + return totalExtent - imageExtent; + } + assert(false); + return 0; + }; + + switch (dir) + { + case ImageStackLayout::horizontal: + copySubImage(img1, wxPoint(), output, wxPoint(0, calcPos(img1Height, newSize.y)), img1.GetSize()); + copySubImage(img2, wxPoint(), output, wxPoint(img1Width + gap, calcPos(img2Height, newSize.y)), img2.GetSize()); + break; + + case ImageStackLayout::vertical: + copySubImage(img1, wxPoint(), output, wxPoint(calcPos(img1Width, newSize.x), 0), img1.GetSize()); + copySubImage(img2, wxPoint(), output, wxPoint(calcPos(img2Width, newSize.x), img1Height + gap), img2.GetSize()); + break; + } + return output; +} + + +wxImage zen::createImageFromText(const wxString& text, const wxFont& font, const wxColor& col, ImageStackAlignment textAlign) +{ + wxMemoryDC dc; //the context used for bitmaps + setScaleFactor(dc, getScreenDpiScale()); + dc.SetFont(font); //the font parameter of GetTextExtent() is not evaluated on OS X, wxWidgets 2.9.5, so apply it to the DC directly! + + std::vector> lineInfo; //text + extent + for (const wxString& line : splitCpy(text, L'\n', SplitOnEmpty::allow)) + lineInfo.emplace_back(line, dc.GetTextExtent(line)); //GetTextExtent() returns (0, 0) for empty string! + //------------------------------------------------------------------------------------------------ + + int maxWidth = 0; + int lineHeight = 0; + for (const auto& [lineText, lineSize] : lineInfo) + { + maxWidth = std::max(maxWidth, lineSize.GetWidth()); + lineHeight = std::max(lineHeight, lineSize.GetHeight()); + } + if (maxWidth == 0 || lineHeight == 0) + return wxNullImage; + + const bool darkMode = relativeContrast(col, *wxBLACK) > //wxSystemSettings::GetAppearance().IsDark() ? + relativeContrast(col, *wxWHITE); //=> no, make it text color-dependent + //small but noticeable difference; due to "ClearType"? + + wxBitmap newBitmap(wxsizeToScreen(maxWidth), + wxsizeToScreen(static_cast(lineHeight * lineInfo.size()))); //seems we don't need to pass 24-bit depth here even for high-contrast color schemes + newBitmap.SetScaleFactor(getScreenDpiScale()); + { + dc.SelectObject(newBitmap); //copies scale factor from wxBitmap + ZEN_ON_SCOPE_EXIT(dc.SelectObject(wxNullBitmap)); + + if (wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft) + dc.SetLayoutDirection(wxLayout_RightToLeft); //handle e.g. "weak" bidi characters: -> arrows in hebrew/arabic + + dc.SetBackground(darkMode ? *wxBLACK_BRUSH : *wxWHITE_BRUSH); + dc.Clear(); + + dc.SetTextBackground(darkMode ? *wxBLACK : *wxWHITE); //for proper alpha-channel calculation + dc.SetTextForeground(darkMode ? *wxWHITE : *wxBLACK); // + + int posY = 0; + for (const auto& [lineText, lineSize] : lineInfo) + { + if (!lineText.empty()) + switch (textAlign) + { + case ImageStackAlignment::left: + dc.DrawText(lineText, wxPoint(0, posY)); + break; + case ImageStackAlignment::right: + dc.DrawText(lineText, wxPoint(maxWidth - lineSize.GetWidth(), posY)); + break; + case ImageStackAlignment::center: + dc.DrawText(lineText, wxPoint((maxWidth - lineSize.GetWidth()) / 2, posY)); + break; + } + posY += lineHeight; + } + } + + wxImage output(newBitmap.ConvertToImage()); + output.SetAlpha(); + //wxDC::DrawLabel() doesn't respect alpha channel => calculate alpha values manually: + + unsigned char* rgb = output.GetData(); + unsigned char* alpha = output.GetAlpha(); + const int pixelCount = output.GetWidth() * output.GetHeight(); + + const unsigned char r = col.Red (); // + const unsigned char g = col.Green(); //getting RGB involves virtual function calls! + const unsigned char b = col.Blue (); // + + //Limitation: alpha should be applied in gamma-decoded linear RGB space: https://ssp.impulsetrain.com/gamma-premult.html + //=> however wxDC::DrawText most likely applied alpha in gamma-encoded sRGB => following simple calculations should be fine: + + if (darkMode) //black(0,0,0) becomes wxIMAGE_ALPHA_TRANSPARENT(0), white(255,255,255) becomes wxIMAGE_ALPHA_OPAQUE(255) + for (int i = 0; i < pixelCount; ++i) + { + *alpha++ = static_cast(numeric::intDivRound(rgb[0] + rgb[1] + rgb[2], 3)); //mixed-mode arithmetics! + *rgb++ = r; // + *rgb++ = g; //apply actual text color + *rgb++ = b; // + } + else //black(0,0,0) becomes wxIMAGE_ALPHA_OPAQUE(255), white(255,255,255) becomes wxIMAGE_ALPHA_TRANSPARENT(0) + for (int i = 0; i < pixelCount; ++i) + { + *alpha++ = static_cast(numeric::intDivRound(3 * 255 - rgb[0] - rgb[1] - rgb[2], 3)); //mixed-mode arithmetics! + *rgb++ = r; // + *rgb++ = g; //apply actual text color + *rgb++ = b; // + } + + return output; +} + + +wxImage zen::layOver(const wxImage& back, const wxImage& front, int alignment) +{ + if (!front.IsOk()) return back; + assert(front.HasAlpha() && back.HasAlpha()); + + const wxSize newSize(std::max(back.GetWidth(), front.GetWidth()), + std::max(back.GetHeight(), front.GetHeight())); + + auto calcNewPos = [&](const wxImage& img) + { + wxPoint newPos; + if (alignment & wxALIGN_RIGHT) //note: wxALIGN_LEFT == 0! + newPos.x = newSize.GetWidth() - img.GetWidth(); + else if (alignment & wxALIGN_CENTER_HORIZONTAL) + newPos.x = (newSize.GetWidth() - img.GetWidth()) / 2; + + if (alignment & wxALIGN_BOTTOM) //note: wxALIGN_TOP == 0! + newPos.y = newSize.GetHeight() - img.GetHeight(); + else if (alignment & wxALIGN_CENTER_VERTICAL) + newPos.y = (newSize.GetHeight() - img.GetHeight()) / 2; + + return newPos; + }; + + wxImage output(newSize); + output.SetAlpha(); + std::memset(output.GetAlpha(), wxIMAGE_ALPHA_TRANSPARENT, newSize.x * newSize.y); + + copySubImage(back, wxPoint(), output, calcNewPos(back), back.GetSize()); + //use resizeCanvas()? might return ref-counted copy! + + //can't use wxMemoryDC and wxDC::DrawBitmap(): no alpha channel support on wxGTK! + copyImageLayover(front, output, calcNewPos(front)); + + return output; +} + + +wxImage zen::resizeCanvas(const wxImage& img, wxSize newSize, int alignment) +{ + if (newSize == img.GetSize()) + return img; //caveat: wxImage is ref-counted *without* copy on write + + wxPoint newPos; + if (alignment & wxALIGN_RIGHT) //note: wxALIGN_LEFT == 0! + newPos.x = newSize.GetWidth() - img.GetWidth(); + else if (alignment & wxALIGN_CENTER_HORIZONTAL) + newPos.x = numeric::intDivFloor(newSize.GetWidth() - img.GetWidth(), 2); //consistency: round down negative values, too! + + if (alignment & wxALIGN_BOTTOM) //note: wxALIGN_TOP == 0! + newPos.y = newSize.GetHeight() - img.GetHeight(); + else if (alignment & wxALIGN_CENTER_VERTICAL) + newPos.y = numeric::intDivFloor(newSize.GetHeight() - img.GetHeight(), 2); //consistency: round down negative values, too! + + wxImage output(newSize); + output.SetAlpha(); + std::memset(output.GetAlpha(), wxIMAGE_ALPHA_TRANSPARENT, newSize.x * newSize.y); + + copySubImage(img, wxPoint(), output, newPos, img.GetSize()); + //about 50x faster than e.g. wxImage::Resize!!! surprise :> + return output; +} + + +wxImage zen::bilinearScale(const wxImage& img, int width, int height) +{ + assert(img.HasAlpha()); + + const auto pixRead = [rgb = img.GetData(), alpha = img.GetAlpha(), srcWidth = img.GetSize().x](int x, int y) + { + const int idx = y * srcWidth + x; + + return [a = int(alpha[idx]), pix = rgb + idx * 3](int channel) + { + if (channel == 3) + return a; + //Limitation: alpha should be applied in gamma-decoded linear RGB space: https://ssp.impulsetrain.com/gamma-premult.html + return pix[channel] * a; + }; + }; + + wxImage imgOut(width, height); + imgOut.SetAlpha(); + + const auto pixWrite = [rgb = imgOut.GetData(), alpha = imgOut.GetAlpha()](const auto& interpolate) mutable + { + const double a = interpolate(3); + if (a <= 0.0) + { + *alpha++ = 0; + rgb += 3; //don't care about color + } + else + { + *alpha++ = xbrz::byteRound(a); + *rgb++ = xbrz::byteRound(interpolate(0) / a); //r + *rgb++ = xbrz::byteRound(interpolate(1) / a); //g + *rgb++ = xbrz::byteRound(interpolate(2) / a); //b + } + }; + + xbrz::bilinearScale(pixRead, //PixReader pixRead + img.GetSize().x, //int srcWidth + img.GetSize().y, //int srcHeight + pixWrite, //PixWriter pixWrite + width, //int trgWidth + height, //int trgHeight + 0, //int yFirst + height); //int yLast + return imgOut; + //return img.Scale(width, height, wxIMAGE_QUALITY_BILINEAR); +} + + +wxImage zen::shrinkImage(const wxImage& img, int maxWidth /*optional*/, int maxHeight /*optional*/) +{ + wxSize newSize = img.GetSize(); + + if (0 <= maxWidth && maxWidth < newSize.x) + { + newSize.x = maxWidth; + newSize.y = numeric::intDivRound(maxWidth * img.GetHeight(), img.GetWidth()); + } + if (0 <= maxHeight && maxHeight < newSize.y) + { + newSize.x = numeric::intDivRound(maxHeight * img.GetWidth(), img.GetHeight()); //avoid loss of precision + newSize.y = maxHeight; + } + + if (newSize == img.GetSize()) + return img; + + return bilinearScale(img, newSize.x, newSize.y); //looks sharper than wxIMAGE_QUALITY_HIGH! +} + + +void zen::convertToVanillaImage(wxImage& img) +{ + if (!img.HasAlpha()) + { + const int width = img.GetWidth (); + const int height = img.GetHeight(); + if (width <= 0 || height <= 0) return; + + unsigned char maskR = 0; + unsigned char maskG = 0; + unsigned char maskB = 0; + const bool haveMask = img.HasMask() && img.GetOrFindMaskColour(&maskR, &maskG, &maskB); + //check for mask before calling wxImage::GetOrFindMaskColour() to skip needlessly searching for new mask color + + img.SetAlpha(); + ::memset(img.GetAlpha(), wxIMAGE_ALPHA_OPAQUE, width * height); + + //wxWidgets, as always, tries to be more clever than it really is and fucks up wxStaticBitmap if wxBitmap is fully opaque: + img.GetAlpha()[width * height - 1] = 254; + + if (haveMask) + { + img.SetMask(false); + unsigned char* alpha = img.GetAlpha(); + const unsigned char* rgb = img.GetData(); + + const int pixelCount = width * height; + for (int i = 0; i < pixelCount; ++i) + { + const unsigned char r = *rgb++; + const unsigned char g = *rgb++; + const unsigned char b = *rgb++; + + if (r == maskR && + g == maskG && + b == maskB) + alpha[i] = wxIMAGE_ALPHA_TRANSPARENT; + } + } + } + else + { + assert(!img.HasMask()); + } +} + + +wxImage zen::rectangleImage(wxSize size, const wxColor& col) +{ + assert(col.IsSolid()); + wxImage img(size); + + const unsigned char r = col.Red (); // + const unsigned char g = col.Green(); //getting RGB involves virtual function calls! + const unsigned char b = col.Blue (); // + + unsigned char* rgb = img.GetData(); + const int pixelCount = size.GetWidth() * size.GetHeight(); + for (int i = 0; i < pixelCount; ++i) + { + *rgb++ = r; + *rgb++ = g; + *rgb++ = b; + } + convertToVanillaImage(img); + return img; +} + + +wxImage zen::rectangleImage(wxSize size, const wxColor& innerCol, const wxColor& borderCol, int borderWidth) +{ + assert(innerCol.IsSolid() && borderCol.IsSolid()); + assert(borderWidth > 0); + wxImage img = rectangleImage(size, borderCol); + + const int heightInner = size.GetHeight() - 2 * borderWidth; + const int widthInner = size.GetWidth () - 2 * borderWidth; + + const unsigned char r = innerCol.Red (); // + const unsigned char g = innerCol.Green(); //getting RGB involves virtual function calls! + const unsigned char b = innerCol.Blue (); // + + if (widthInner > 0 && heightInner > 0 && innerCol != borderCol) + //copyImageLayover(rectangleImage({widthInner, heightInner}, innerCol), img, {borderWidth, borderWidth}); => inline: + for (int y = 0; y < heightInner; ++y) + { + unsigned char* rgb = img.GetData () + 3 * (borderWidth + (borderWidth + y) * size.GetWidth()); + + for (int x = 0; x < widthInner; ++x) + { + *rgb++ = r; + *rgb++ = g; + *rgb++ = b; + } + } + + return img; +} diff --git a/wx+/image_tools.h b/wx+/image_tools.h new file mode 100644 index 0000000..9cb5b71 --- /dev/null +++ b/wx+/image_tools.h @@ -0,0 +1,144 @@ +// ***************************************************************************** +// * 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 IMAGE_TOOLS_H_45782456427634254 +#define IMAGE_TOOLS_H_45782456427634254 + +#include +#include +#include +#include + + +namespace zen +{ +enum class ImageStackLayout +{ + horizontal, + vertical +}; + +enum class ImageStackAlignment //one-dimensional unlike wxAlignment +{ + center, + left, + right, + top = left, + bottom = right, +}; +wxImage stackImages(const wxImage& img1, const wxImage& img2, ImageStackLayout dir, ImageStackAlignment align, int gap = 0); + +wxImage createImageFromText(const wxString& text, const wxFont& font, const wxColor& col, ImageStackAlignment textAlign = ImageStackAlignment::left); //center/left/right + +wxImage layOver(const wxImage& back, const wxImage& front, int alignment = wxALIGN_CENTER); + +wxImage greyScale(const wxImage& img); //greyscale + brightness adaption +wxImage greyScaleIfDisabled(const wxImage& img, bool enabled); + +void adjustBrightness(wxImage& img, int targetLevel); +double getAvgBrightness(const wxImage& img); //in [0, 255] +void brighten(wxImage& img, int level); //level: delta per channel in points + +void convertToVanillaImage(wxImage& img); //add alpha channel if missing + remove mask if existing + +//wxColor gradient(const wxColor& from, const wxColor& to, double fraction); //maps fraction within [0, 1] to an intermediate color + +//wxColor hsvColor(double h, double s, double v); //h within [0, 360), s, v within [0, 1] + +//does *not* fuck up alpha channel like naive bilinear implementations, e.g. wxImage::Scale() +wxImage bilinearScale(const wxImage& img, int width, int height); + +wxImage shrinkImage(const wxImage& img, int maxWidth /*optional*/, int maxHeight /*optional*/); +inline wxImage shrinkImage(const wxImage& img, int maxSize) { return shrinkImage(img, maxSize, maxSize); } + +wxImage resizeCanvas(const wxImage& img, wxSize newSize, int alignment); + +wxImage rectangleImage(wxSize size, const wxColor& col); +wxImage rectangleImage(wxSize size, const wxColor& innerCol, const wxColor& borderCol, int borderWidth); + + + + + + + + + + +//################################### implementation ################################### + +inline +wxImage greyScale(const wxImage& img) //TODO support gamma-decoding and perceptual colors!? +{ + wxImage output = img.ConvertToGreyscale(1.0 / 3, 1.0 / 3, 1.0 / 3); //treat all channels equally + adjustBrightness(output, 160); + return output; +} + + +inline +wxImage greyScaleIfDisabled(const wxImage& img, bool enabled) +{ + if (enabled) //avoid ternary WTF + return img; + else + return greyScale(img); +} + + +inline +double getAvgBrightness(const wxImage& img) //TODO: consider gamma-encoded sRGB!? +{ + const int pixelCount = img.GetWidth() * img.GetHeight(); + auto pixBegin = img.GetData(); + + if (pixelCount > 0 && pixBegin) + { + auto pixEnd = pixBegin + 3 * pixelCount; //RGB + + if (img.HasAlpha()) + { + const unsigned char* alphaFirst = img.GetAlpha(); + + //calculate average weighted by alpha channel + double dividend = 0; + for (auto it = pixBegin; it != pixEnd; ++it) + dividend += *it * static_cast(alphaFirst[(it - pixBegin) / 3]); + + const double divisor = 3.0 * std::accumulate(alphaFirst, alphaFirst + pixelCount, 0.0); + + return numeric::isNull(divisor) ? 0 : dividend / divisor; + } + else + return std::accumulate(pixBegin, pixEnd, 0.0) / (3.0 * pixelCount); + } + return 0; +} + + +inline +void brighten(wxImage& img, int level) +{ + if (auto pixBegin = img.GetData()) + { + const int pixelCount = img.GetWidth() * img.GetHeight(); + auto pixEnd = pixBegin + 3 * pixelCount; //RGB + if (level > 0) + std::for_each(pixBegin, pixEnd, [level](unsigned char& c) { c = static_cast(std::min(c + level, 255)); }); + else + std::for_each(pixBegin, pixEnd, [level](unsigned char& c) { c = static_cast(std::max(c + level, 0)); }); + } +} + + +inline +void adjustBrightness(wxImage& img, int targetLevel) +{ + brighten(img, targetLevel - getAvgBrightness(img)); +} +} + +#endif //IMAGE_TOOLS_H_45782456427634254 diff --git a/wx+/no_flicker.h b/wx+/no_flicker.h new file mode 100644 index 0000000..6cc78b6 --- /dev/null +++ b/wx+/no_flicker.h @@ -0,0 +1,158 @@ +// ***************************************************************************** +// * 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 NO_FLICKER_H_893421590321532 +#define NO_FLICKER_H_893421590321532 + +#include +#include +#include +#include +#include +#include +#include +#include "color_tools.h" + + +namespace zen +{ +namespace +{ +void setText(wxTextCtrl& control, const wxString& newText, bool* additionalLayoutChange = nullptr) +{ + const wxString& label = control.GetValue(); //perf: don't call twice! + if (additionalLayoutChange && !*additionalLayoutChange && control.IsShown()) //never revert from true to false! + *additionalLayoutChange = label.length() != newText.length(); //avoid screen flicker: update layout only when necessary + + if (label != newText) + control.ChangeValue(newText); +} + + +void setText(wxStaticText& control, const wxString& newText, bool* additionalLayoutChange = nullptr) +{ + //wxControl::EscapeMnemonics() (& -> &&) => wxControl::GetLabelText/SetLabelText + //e.g. "filenames in the sync progress dialog": https://sourceforge.net/p/freefilesync/bugs/279/ + + const wxString& label = control.GetLabelText(); //perf: don't call twice! + if (additionalLayoutChange && !*additionalLayoutChange && control.IsShown()) //"better" or overkill(?): IsShownOnScreen() + *additionalLayoutChange = label.length() != newText.length(); //avoid screen flicker: update layout only when necessary + + if (label != newText) + control.SetLabelText(newText); +} + + +void setTextWithUrls(wxRichTextCtrl& richCtrl, const wxString& newText) +{ + enum class BlockType + { + text, + url, + }; + std::vector> blocks; + + for (auto it = newText.begin();;) + { + constexpr std::wstring_view urlPrefix = L"https://"; + const auto itUrl = std::search(it, newText.end(), urlPrefix.begin(), urlPrefix.end()); + if (it != itUrl) + blocks.emplace_back(BlockType::text, wxString(it, itUrl)); + + if (itUrl == newText.end()) + break; + + auto itUrlEnd = std::find_if(itUrl, newText.end(), [](wchar_t c) { return isWhiteSpace(c); }); + blocks.emplace_back(BlockType::url, wxString(itUrl, itUrlEnd)); + it = itUrlEnd; + } + richCtrl.BeginSuppressUndo(); + ZEN_ON_SCOPE_EXIT(richCtrl.EndSuppressUndo()); + + //fix mouse scroll speed: why the FUCK is this even necessary! + richCtrl.SetLineHeight(richCtrl.GetCharHeight()); + + //get rid of margins and space between text blocks/"paragraphs" + richCtrl.SetMargins({0, 0}); + richCtrl.BeginParagraphSpacing(0, 0); + ZEN_ON_SCOPE_EXIT(richCtrl.EndParagraphSpacing()); + + richCtrl.Clear(); + + wxRichTextAttr urlStyle; + urlStyle.SetTextColour(enhanceContrast(*wxBLUE, //primarily needed for dark mode! + wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW), 5 /*contrastRatioMin*/)); //W3C recommends >= 4.5 + urlStyle.SetFontUnderlined(true); + + for (auto& [type, text] : blocks) + switch (type) + { + case BlockType::text: + if (endsWith(text, L"\n\n")) //bug: multiple newlines before a URL are condensed to only one; + //Why? fuck knows why! no such issue with double newlines *after* URL => hack this shit + text.RemoveLast().Append(ZERO_WIDTH_SPACE).Append(L'\n'); + + richCtrl.WriteText(text); + break; + + case BlockType::url: + { + richCtrl.BeginStyle(urlStyle); + ZEN_ON_SCOPE_EXIT(richCtrl.EndStyle()); + richCtrl.BeginURL(text); + ZEN_ON_SCOPE_EXIT(richCtrl.EndURL()); + richCtrl.WriteText(text); + } + break; + } + + //register only once! => use a global function pointer, so that Unbind() works correctly: + using LaunchUrlFun = void(*)(wxTextUrlEvent& event); + static const LaunchUrlFun launchUrl = [](wxTextUrlEvent& event) { wxLaunchDefaultBrowser(event.GetString()); }; + + [[maybe_unused]] const bool unbindOk1 = richCtrl.Unbind(wxEVT_TEXT_URL, launchUrl); + if (std::any_of(blocks.begin(), blocks.end(), [](const auto& item) { return item.first == BlockType::url; })) + /**/richCtrl.Bind(wxEVT_TEXT_URL, launchUrl); + + struct UserData : public wxObject + { + explicit UserData(wxRichTextCtrl& rtc) : richCtrl(rtc) {} + wxRichTextCtrl& richCtrl; + }; + using KeyEventsFun = void(*)(wxKeyEvent& event); + static const KeyEventsFun onKeyEvents = [](wxKeyEvent& event) + { + wxRichTextCtrl& richCtrl2 = dynamic_cast(event.GetEventUserData())->richCtrl; //unclear if we can rely on event.GetEventObject() == richCtrl + + //CTRL/SHIFT + INS is broken for wxRichTextCtrl on Windows/Linux (apparently never was a thing on macOS) + if (event.ControlDown()) + switch (event.GetKeyCode()) + { + case WXK_INSERT: + case WXK_NUMPAD_INSERT: + assert(richCtrl2.CanCopy()); //except when no selection + richCtrl2.Copy(); + return; + } + + if (event.ShiftDown()) + switch (event.GetKeyCode()) + { + case WXK_INSERT: + case WXK_NUMPAD_INSERT: + assert(richCtrl2.CanPaste()); //except wxTE_READONLY + richCtrl2.Paste(); + return; + } + event.Skip(); + }; + [[maybe_unused]] const bool unbindOk2 = richCtrl.Unbind(wxEVT_KEY_DOWN, onKeyEvents); + /**/ richCtrl. Bind(wxEVT_KEY_DOWN, onKeyEvents, wxID_ANY, wxID_ANY, new UserData(richCtrl) /*pass ownership*/); +} +} +} + +#endif //NO_FLICKER_H_893421590321532 diff --git a/wx+/popup_dlg.cpp b/wx+/popup_dlg.cpp new file mode 100644 index 0000000..e288513 --- /dev/null +++ b/wx+/popup_dlg.cpp @@ -0,0 +1,398 @@ +// ***************************************************************************** +// * 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 "popup_dlg.h" +#include +#include +#include +#include +#include +#include "bitmap_button.h" +#include "no_flicker.h" +#include "window_layout.h" +#include "image_resources.h" +#include "popup_dlg_generated.h" +#include "taskbar.h" +#include "window_tools.h" + + +using namespace zen; + + +namespace +{ +void setBestInitialSize(wxRichTextCtrl& ctrl, const wxString& text, wxSize maxSize) +{ + const int scrollbarWidth = dipToWxsize(25); /*not only scrollbar, but also left/right padding (on macOS)! + better use slightly larger than exact value (Windows: 17, Linux(CentOS): 14, macOS: 25) + => worst case: minor increase in rowCount (no big deal) + slightly larger bestSize.x (good!) */ + + if (maxSize.x <= scrollbarWidth) //implicitly checks for non-zero, too! + return; + + const int rowGap = 0; + int maxLineWidth = 0; + int rowHeight = 0; //alternative: just call ctrl.GetCharHeight()!? + int rowCount = 0; + bool haveLineWrap = false; + + auto evalLineExtent = [&](const wxSize& sz) -> bool //return true when done + { + assert(rowHeight == 0 || rowHeight == sz.y + rowGap); //all rows *should* have same height + rowHeight = std::max(rowHeight, sz.y + rowGap); + maxLineWidth = std::max(maxLineWidth, sz.x); + + const int wrappedRows = numeric::intDivCeil(sz.x, maxSize.x - scrollbarWidth); //round up: consider line-wraps! + rowCount += wrappedRows; + if (wrappedRows > 1) + haveLineWrap = true; + + return rowCount * rowHeight >= maxSize.y; + }; + + for (auto it = text.begin();;) + { + auto itEnd = std::find(it, text.end(), L'\n'); + wxString line(it, itEnd); + if (line.empty()) + line = L' '; //GetTextExtent() returns (0, 0) for empty strings! + + wxSize sz = ctrl.GetTextExtent(line); //exactly gives row height, but does *not* consider newlines + if (evalLineExtent(sz)) + break; + + if (itEnd == text.end()) + break; + it = itEnd + 1; + } + + int extraWidth = 0; + if (haveLineWrap) //compensate for trivial intDivCeil() not... + extraWidth += ctrl.GetTextExtent(L"FreeFileSync").x / 2; //...understanding line wrap algorithm + + const wxSize bestSize(std::min(maxLineWidth + scrollbarWidth /*1*/+ extraWidth, maxSize.x), + std::min(rowHeight * (rowCount + 1 /*2*/), maxSize.y)); + //1: wxWidgets' layout algorithm sucks: e.g. shows scrollbar *nedlessly* => extra line wrap increases height => scrollbar suddenly *needed*: catch 22! + //2: add some vertical space just for looks (*instead* of using border gap)! Extra space needed anyway to avoid scrollbars on Windows (2 px) and macOS (11 px) + + ctrl.SetMinSize(bestSize); //alas, SetMinClientSize() is just not working! +#if 0 + std::cerr << "rowCount " << rowCount << "\n" << + "maxLineWidth " << maxLineWidth << "\n" << + "rowHeight " << rowHeight << "\n" << + "haveLineWrap " << haveLineWrap << "\n" << + "scrollbarWidth " << scrollbarWidth << "\n\n"; +#endif +} +} + + +int zen::getTextCtrlHeight(wxTextCtrl& ctrl, double rowCount) +{ + const int rowHeight = + ctrl.GetTextExtent(L"X").GetHeight(); + + return std::round( + 2 + + rowHeight * rowCount); +} + + +class zen::StandardPopupDialog : public PopupDialogGenerated +{ +public: + StandardPopupDialog(wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg, + const wxString& labelAccept, // + const wxString& labelAccept2, //optional, except: if "decline" or "accept2" is passed, so must be "accept" + const wxString& labelDecline) : // + PopupDialogGenerated(parent), + checkBoxValue_(cfg.checkBoxValue), + buttonToDisableWhenChecked_(cfg.buttonToDisableWhenChecked) + { + + //ensure wxWidgets' and our high-DPI handling are still matching + assert(GetDPIScaleFactor() == getScreenDpiScale()); + + if (type != DialogInfoType::info) + try + { + taskbar_.emplace(parent); //throw TaskbarNotAvailable + switch (type) + { + case DialogInfoType::info: + break; + case DialogInfoType::warning: + taskbar_->setStatus(Taskbar::Status::warning); + break; + case DialogInfoType::error: + taskbar_->setStatus(Taskbar::Status::error); + break; + } + } + catch (TaskbarNotAvailable&) {} + + + wxImage iconTmp; + wxString titleTmp; + switch (type) + { + case DialogInfoType::info: + //"Information" is meaningless as caption text! + //confirmation doesn't use info icon + //iconTmp = loadImage("msg_info"); + break; + case DialogInfoType::warning: + iconTmp = loadImage("msg_warning"); + titleTmp = _("Warning"); + break; + case DialogInfoType::error: + iconTmp = loadImage("msg_error"); + titleTmp = _("Error"); + break; + } + if (cfg.icon.IsOk()) + iconTmp = cfg.icon; + + if (!cfg.title.empty()) + titleTmp = cfg.title; + //----------------------------------------------- + if (iconTmp.IsOk()) + setImage(*m_bitmapMsgType, iconTmp); + + if (!parent || !parent->IsShownOnScreen()) + titleTmp = wxTheApp->GetAppDisplayName() + (!titleTmp.empty() ? SPACED_DASH + titleTmp : wxString()); + SetTitle(titleTmp); + + int maxWidth = dipToWxsize(500); + int maxHeight = dipToWxsize(400); //try to determine better value based on actual display resolution: + if (parent) + if (const int disPos = wxDisplay::GetFromWindow(parent); //window must be visible + disPos != wxNOT_FOUND) + maxHeight = wxDisplay(disPos).GetClientArea().GetHeight() * 2 / 3; + + assert(!cfg.textMain.empty() || !cfg.textDetail.empty()); + if (!cfg.textMain.empty()) + { + setMainInstructionFont(*m_staticTextMain); + m_staticTextMain->SetLabelText(cfg.textMain); + m_staticTextMain->Wrap(maxWidth); //call *after* SetLabel() + } + else + m_staticTextMain->Hide(); + + if (!cfg.textDetail.empty()) + { + const wxString& text = trimCpy(cfg.textDetail); + setBestInitialSize(*m_richTextDetail, text, wxSize(maxWidth, maxHeight)); + setTextWithUrls(*m_richTextDetail, text); + } + else + m_richTextDetail->Hide(); + + if (checkBoxValue_) + { + assert(contains(cfg.checkBoxLabel, L'&')); + m_checkBoxCustom->SetLabel(cfg.checkBoxLabel); + m_checkBoxCustom->SetValue(*checkBoxValue_); + } + else + m_checkBoxCustom->Hide(); + + Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onLocalKeyEvent(event); }); //dialog-specific local key events + + //play sound reminder when waiting for user confirmation + if (!cfg.soundFileAlertPending.empty()) + { + timer_.Bind(wxEVT_TIMER, [this, parent, alertSoundPath = cfg.soundFileAlertPending](wxTimerEvent& event) + { + //wxWidgets shows modal error dialog by default => "no, wxWidgets, NO!" + wxLog* oldLogTarget = wxLog::SetActiveTarget(new wxLogStderr); //transfer and receive ownership! + ZEN_ON_SCOPE_EXIT(delete wxLog::SetActiveTarget(oldLogTarget)); + + wxSound::Play(utfTo(alertSoundPath), wxSOUND_ASYNC); + + RequestUserAttention(wxUSER_ATTENTION_INFO); + /* wxUSER_ATTENTION_INFO: flashes window 3 times, unconditionally + wxUSER_ATTENTION_ERROR: flashes without limit, but *only* if not in foreground (FLASHW_TIMERNOFG) :( */ + if (parent) + if (auto tlw = dynamic_cast(&getRootWindow(*parent))) + tlw->RequestUserAttention(wxUSER_ATTENTION_INFO); //top-level window needed for the taskbar flash! + }); + timer_.Start(60'000 /*unit: [ms]*/); + } + + //------------------------------------------------------------------------------ + + auto setButtonImage = [&](wxButton& button, ConfirmationButton3 btnType) + { + auto it = cfg.buttonImages.find(btnType); + if (it != cfg.buttonImages.end()) + setImage(button, it->second); //caveat: image + text at the same time not working on GTK < 2.6 + }; + setButtonImage(*m_buttonAccept, ConfirmationButton3::accept); + setButtonImage(*m_buttonAccept2, ConfirmationButton3::accept2); + setButtonImage(*m_buttonDecline, ConfirmationButton3::decline); + setButtonImage(*m_buttonCancel, ConfirmationButton3::cancel); + + + if (cfg.disabledButtons.contains(ConfirmationButton3::accept )) m_buttonAccept ->Disable(); + if (cfg.disabledButtons.contains(ConfirmationButton3::accept2)) m_buttonAccept2->Disable(); + if (cfg.disabledButtons.contains(ConfirmationButton3::decline)) m_buttonDecline->Disable(); + assert(!cfg.disabledButtons.contains(ConfirmationButton3::cancel)); + assert(!cfg.disabledButtons.contains(cfg.buttonToDisableWhenChecked)); + + + StdButtons stdBtns; + stdBtns.setAffirmative(m_buttonAccept); + if (labelAccept.empty()) //notification dialog + { + assert(labelAccept2.empty() && labelDecline.empty()); + m_buttonAccept->SetLabel(_("Close")); //UX Guide: use "Close" for errors, warnings and windows in which users can't make changes (no ampersand!) + m_buttonAccept2->Hide(); + m_buttonDecline->Hide(); + m_buttonCancel ->Hide(); + } + else + { + assert(contains(labelAccept, L"&")); + m_buttonAccept->SetLabel(labelAccept); + stdBtns.setCancel(m_buttonCancel); + + if (labelDecline.empty()) //confirmation dialog(YES/CANCEL) + m_buttonDecline->Hide(); + else //confirmation dialog(YES/NO/CANCEL) + { + assert(contains(labelDecline, L"&")); + m_buttonDecline->SetLabel(labelDecline); + stdBtns.setNegative(m_buttonDecline); + + //m_buttonConfirm->SetId(wxID_IGNORE); -> setting id after button creation breaks "mouse snap to" functionality + //m_buttonDecline->SetId(wxID_RETRY); -> also wxWidgets docs seem to hide some info: "Normally, the identifier should be provided on creation and should not be modified subsequently." + } + + if (labelAccept2.empty()) + m_buttonAccept2->Hide(); + else + { + assert(contains(labelAccept2, L"&")); + m_buttonAccept2->SetLabel(labelAccept2); + stdBtns.setAffirmativeAll(m_buttonAccept2); + } + } + //set std order after button visibility was set + setStandardButtonLayout(*bSizerStdButtons, stdBtns); + + updateGui(); + + + GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() +#ifdef __WXGTK3__ + Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 + //Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404 +#endif + Center(); //apply *after* dialog size change! + + + Raise(); //[!] popup may be triggered by ffs_batch job running in the background! + + if (m_buttonAccept->IsEnabled()) + m_buttonAccept->SetFocus(); + else if (m_buttonAccept2->IsEnabled()) + m_buttonAccept2->SetFocus(); + else + m_buttonCancel->SetFocus(); + } + +private: + void onClose (wxCloseEvent& event) override { EndModal(static_cast(ConfirmationButton3::cancel)); } + void onCancel(wxCommandEvent& event) override { EndModal(static_cast(ConfirmationButton3::cancel)); } + + void onButtonAccept(wxCommandEvent& event) override + { + if (checkBoxValue_) + *checkBoxValue_ = m_checkBoxCustom->GetValue(); + EndModal(static_cast(ConfirmationButton3::accept)); + } + + void onButtonAccept2(wxCommandEvent& event) override + { + if (checkBoxValue_) + *checkBoxValue_ = m_checkBoxCustom->GetValue(); + EndModal(static_cast(ConfirmationButton3::accept2)); + } + + void onButtonDecline(wxCommandEvent& event) override + { + if (checkBoxValue_) + *checkBoxValue_ = m_checkBoxCustom->GetValue(); + EndModal(static_cast(ConfirmationButton3::decline)); + } + + void onLocalKeyEvent(wxKeyEvent& event) + { + switch (event.GetKeyCode()) + { + case WXK_ESCAPE: //handle case where cancel button is hidden! + EndModal(static_cast(ConfirmationButton3::cancel)); + return; + } + event.Skip(); + } + + void onCheckBoxClick(wxCommandEvent& event) override { updateGui(); event.Skip(); } + + void updateGui() + { + switch (buttonToDisableWhenChecked_) + { + case ConfirmationButton3::accept: m_buttonAccept ->Enable(!m_checkBoxCustom->GetValue()); break; + case ConfirmationButton3::accept2: m_buttonAccept2->Enable(!m_checkBoxCustom->GetValue()); break; + case ConfirmationButton3::decline: m_buttonDecline->Enable(!m_checkBoxCustom->GetValue()); break; + case ConfirmationButton3::cancel: break; + } + } + + bool* checkBoxValue_; + const ConfirmationButton3 buttonToDisableWhenChecked_; + std::optional taskbar_; + wxTimer timer_; +}; + +//######################################################################################## + +void zen::showNotificationDialog(wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg) +{ + StandardPopupDialog dlg(parent, type, cfg, wxString() /*labelAccept*/, wxString() /*labelAccept2*/, wxString() /*labelDecline*/); + dlg.ShowModal(); +} + + +ConfirmationButton zen::showConfirmationDialog(wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg, const wxString& labelAccept) +{ + StandardPopupDialog dlg(parent, type, cfg, labelAccept, wxString() /*labelAccept2*/, wxString() /*labelDecline*/); + return static_cast(dlg.ShowModal()); +} + + +ConfirmationButton2 zen::showConfirmationDialog(wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg, const wxString& labelAccept, const wxString& labelAccept2) +{ + StandardPopupDialog dlg(parent, type, cfg, labelAccept, labelAccept2, wxString() /*labelDecline*/); + return static_cast(dlg.ShowModal()); +} + + +ConfirmationButton3 zen::showConfirmationDialog(wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg, const wxString& labelAccept, const wxString& labelAccept2, const wxString& labelDecline) +{ + StandardPopupDialog dlg(parent, type, cfg, labelAccept, labelAccept2, labelDecline); + return static_cast(dlg.ShowModal()); +} + + +QuestionButton2 zen::showQuestionDialog(wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg, const wxString& labelYes, const wxString& labelNo) +{ + StandardPopupDialog dlg(parent, type, cfg, labelYes, wxString() /*labelAccept2*/, labelNo); + return static_cast(dlg.ShowModal()); +} diff --git a/wx+/popup_dlg.h b/wx+/popup_dlg.h new file mode 100644 index 0000000..92a60b4 --- /dev/null +++ b/wx+/popup_dlg.h @@ -0,0 +1,103 @@ +// ***************************************************************************** +// * 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 POPUP_DLG_H_820780154723456 +#define POPUP_DLG_H_820780154723456 + +#include +#include +#include +#include +#include +#include +#include + + +namespace zen +{ +//parent window, optional: support correct dialog placement above parent on multiple monitor systems +//this module requires error, warning and info image files in Icons.zip, see + +enum class DialogInfoType +{ + info, + warning, + error, +}; + +enum class ConfirmationButton3 +{ + cancel, + accept, + accept2, + decline, +}; +enum class ConfirmationButton +{ + cancel = static_cast(ConfirmationButton3::cancel), //[!] Clang requires "static_cast" + accept = static_cast(ConfirmationButton3::accept), // +}; +enum class ConfirmationButton2 +{ + cancel = static_cast(ConfirmationButton3::cancel), + accept = static_cast(ConfirmationButton3::accept), + accept2 = static_cast(ConfirmationButton3::accept2), +}; +enum class QuestionButton2 +{ + cancel = static_cast(ConfirmationButton3::cancel), + yes = static_cast(ConfirmationButton3::accept), + no = static_cast(ConfirmationButton3::decline), +}; + +struct PopupDialogCfg; + +void showNotificationDialog(wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg); +ConfirmationButton showConfirmationDialog(wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg, const wxString& labelAccept); +ConfirmationButton2 showConfirmationDialog(wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg, const wxString& labelAccept, const wxString& labelAccept2); +ConfirmationButton3 showConfirmationDialog(wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg, const wxString& labelAccept, const wxString& labelAccept2, const wxString& labelDecline); +QuestionButton2 showQuestionDialog (wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg, const wxString& labelYes, const wxString& labelNo); + +//---------------------------------------------------------------------------------------------------------------- +class StandardPopupDialog; + +struct PopupDialogCfg +{ + PopupDialogCfg& setIcon (const wxImage& bmp ) { icon = bmp; return *this; } + PopupDialogCfg& setTitle (const wxString& label) { title = label; return *this; } + PopupDialogCfg& setMainInstructions (const wxString& label) { textMain = label; return *this; } //set at least one of these! + PopupDialogCfg& setDetailInstructions(const wxString& label) { textDetail = label; return *this; } // + PopupDialogCfg& disableButton(ConfirmationButton3 button) { disabledButtons.insert(button); return *this; } + PopupDialogCfg& setButtonImage(ConfirmationButton3 button, const wxImage& img) { buttonImages.emplace(button, img); return *this; } + PopupDialogCfg& alertWhenPending(const Zstring& soundFilePath) { soundFileAlertPending = soundFilePath; return *this; } + PopupDialogCfg& setCheckBox(bool& value, const wxString& label, ConfirmationButton3 disableWhenChecked = ConfirmationButton3::cancel) + { + checkBoxValue = &value; + checkBoxLabel = label; + buttonToDisableWhenChecked = disableWhenChecked; + return *this; + } + +private: + friend class StandardPopupDialog; + + wxImage icon; + wxString title; + wxString textMain; + wxString textDetail; + std::unordered_set disabledButtons; + std::unordered_map buttonImages; + Zstring soundFileAlertPending; + bool* checkBoxValue = nullptr; //in/out + wxString checkBoxLabel; + ConfirmationButton3 buttonToDisableWhenChecked = ConfirmationButton3::cancel; +}; + + +int getTextCtrlHeight(wxTextCtrl& ctrl, double rowCount); +} + +#endif //POPUP_DLG_H_820780154723456 diff --git a/wx+/popup_dlg_generated.cpp b/wx+/popup_dlg_generated.cpp new file mode 100644 index 0000000..1741abc --- /dev/null +++ b/wx+/popup_dlg_generated.cpp @@ -0,0 +1,124 @@ +/////////////////////////////////////////////////////////////////////////// +// C++ code generated with wxFormBuilder (version 3.10.1-0-g8feb16b3) +// http://www.wxformbuilder.org/ +// +// PLEASE DO *NOT* EDIT THIS FILE! +/////////////////////////////////////////////////////////////////////////// + +#include "popup_dlg_generated.h" + +/////////////////////////////////////////////////////////////////////////// + +PopupDialogGenerated::PopupDialogGenerated( wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style ) : wxDialog( parent, id, title, pos, size, style ) +{ + this->SetSizeHints( wxSize( -1, -1 ), wxDefaultSize ); + this->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) ); + + wxBoxSizer* bSizer24; + bSizer24 = new wxBoxSizer( wxVERTICAL ); + + wxPanel* m_panel33; + m_panel33 = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panel33->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer165; + bSizer165 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapMsgType = new wxStaticBitmap( m_panel33, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizer165->Add( m_bitmapMsgType, 0, wxALL, 10 ); + + wxBoxSizer* bSizer16; + bSizer16 = new wxBoxSizer( wxVERTICAL ); + + + bSizer16->Add( 0, 10, 0, 0, 5 ); + + m_staticTextMain = new wxStaticText( m_panel33, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextMain->Wrap( -1 ); + bSizer16->Add( m_staticTextMain, 0, wxBOTTOM|wxRIGHT, 10 ); + + m_richTextDetail = new wxRichTextCtrl( m_panel33, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_READONLY|wxBORDER_NONE|wxVSCROLL|wxWANTS_CHARS ); + bSizer16->Add( m_richTextDetail, 1, wxEXPAND, 5 ); + + + bSizer165->Add( bSizer16, 1, wxEXPAND, 5 ); + + + m_panel33->SetSizer( bSizer165 ); + m_panel33->Layout(); + bSizer165->Fit( m_panel33 ); + bSizer24->Add( m_panel33, 1, wxEXPAND, 5 ); + + wxStaticLine* m_staticline6; + m_staticline6 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer24->Add( m_staticline6, 0, wxEXPAND, 5 ); + + wxBoxSizer* bSizer25; + bSizer25 = new wxBoxSizer( wxVERTICAL ); + + m_checkBoxCustom = new wxCheckBox( this, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + bSizer25->Add( m_checkBoxCustom, 0, wxALIGN_CENTER_HORIZONTAL|wxALL, 5 ); + + bSizerStdButtons = new wxBoxSizer( wxHORIZONTAL ); + + m_buttonAccept = new wxButton( this, wxID_YES, _("dummy"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + + m_buttonAccept->SetDefault(); + bSizerStdButtons->Add( m_buttonAccept, 0, wxALIGN_CENTER_VERTICAL|wxBOTTOM|wxRIGHT|wxLEFT, 5 ); + + m_buttonAccept2 = new wxButton( this, wxID_YESTOALL, _("dummy"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizerStdButtons->Add( m_buttonAccept2, 0, wxALIGN_CENTER_VERTICAL|wxBOTTOM|wxRIGHT, 5 ); + + m_buttonDecline = new wxButton( this, wxID_NO, _("dummy"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizerStdButtons->Add( m_buttonDecline, 0, wxALIGN_CENTER_VERTICAL|wxBOTTOM|wxRIGHT, 5 ); + + m_buttonCancel = new wxButton( this, wxID_CANCEL, _("Cancel"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizerStdButtons->Add( m_buttonCancel, 0, wxALIGN_CENTER_VERTICAL|wxBOTTOM|wxRIGHT, 5 ); + + + bSizer25->Add( bSizerStdButtons, 0, wxALIGN_RIGHT, 5 ); + + + bSizer24->Add( bSizer25, 0, wxEXPAND, 5 ); + + + this->SetSizer( bSizer24 ); + this->Layout(); + bSizer24->Fit( this ); + + // Connect Events + this->Connect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( PopupDialogGenerated::onClose ) ); + m_checkBoxCustom->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( PopupDialogGenerated::onCheckBoxClick ), NULL, this ); + m_buttonAccept->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( PopupDialogGenerated::onButtonAccept ), NULL, this ); + m_buttonAccept2->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( PopupDialogGenerated::onButtonAccept2 ), NULL, this ); + m_buttonDecline->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( PopupDialogGenerated::onButtonDecline ), NULL, this ); + m_buttonCancel->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( PopupDialogGenerated::onCancel ), NULL, this ); +} + +PopupDialogGenerated::~PopupDialogGenerated() +{ +} + +TooltipDlgGenerated::TooltipDlgGenerated( wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style ) : wxDialog( parent, id, title, pos, size, style ) +{ + this->SetSizeHints( wxDefaultSize, wxDefaultSize ); + + wxBoxSizer* bSizer158; + bSizer158 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapLeft = new wxStaticBitmap( this, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer158->Add( m_bitmapLeft, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + + m_staticTextMain = new wxStaticText( this, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextMain->Wrap( 600 ); + bSizer158->Add( m_staticTextMain, 0, wxALL|wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + + + this->SetSizer( bSizer158 ); + this->Layout(); + bSizer158->Fit( this ); +} + +TooltipDlgGenerated::~TooltipDlgGenerated() +{ +} diff --git a/wx+/popup_dlg_generated.h b/wx+/popup_dlg_generated.h new file mode 100644 index 0000000..f931bab --- /dev/null +++ b/wx+/popup_dlg_generated.h @@ -0,0 +1,89 @@ +/////////////////////////////////////////////////////////////////////////// +// C++ code generated with wxFormBuilder (version 3.10.1-0-g8feb16b3) +// http://www.wxformbuilder.org/ +// +// PLEASE DO *NOT* EDIT THIS FILE! +/////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "zen/i18n.h" + +/////////////////////////////////////////////////////////////////////////// + + +/////////////////////////////////////////////////////////////////////////////// +/// Class PopupDialogGenerated +/////////////////////////////////////////////////////////////////////////////// +class PopupDialogGenerated : public wxDialog +{ +private: + +protected: + wxStaticBitmap* m_bitmapMsgType; + wxStaticText* m_staticTextMain; + wxRichTextCtrl* m_richTextDetail; + wxCheckBox* m_checkBoxCustom; + wxBoxSizer* bSizerStdButtons; + wxButton* m_buttonAccept; + wxButton* m_buttonAccept2; + wxButton* m_buttonDecline; + wxButton* m_buttonCancel; + + // Virtual event handlers, override them in your derived class + virtual void onClose( wxCloseEvent& event ) { event.Skip(); } + virtual void onCheckBoxClick( wxCommandEvent& event ) { event.Skip(); } + virtual void onButtonAccept( wxCommandEvent& event ) { event.Skip(); } + virtual void onButtonAccept2( wxCommandEvent& event ) { event.Skip(); } + virtual void onButtonDecline( wxCommandEvent& event ) { event.Skip(); } + virtual void onCancel( wxCommandEvent& event ) { event.Skip(); } + + +public: + + PopupDialogGenerated( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = _("dummy"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER ); + + ~PopupDialogGenerated(); + +}; + +/////////////////////////////////////////////////////////////////////////////// +/// Class TooltipDlgGenerated +/////////////////////////////////////////////////////////////////////////////// +class TooltipDlgGenerated : public wxDialog +{ +private: + +protected: + +public: + wxStaticBitmap* m_bitmapLeft; + wxStaticText* m_staticTextMain; + + TooltipDlgGenerated( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = wxEmptyString, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = wxDEFAULT_DIALOG_STYLE ); + + ~TooltipDlgGenerated(); + +}; + diff --git a/wx+/rtl.h b/wx+/rtl.h new file mode 100644 index 0000000..c7c3f38 --- /dev/null +++ b/wx+/rtl.h @@ -0,0 +1,112 @@ +// ***************************************************************************** +// * 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 RTL_H_0183487180058718273432148 +#define RTL_H_0183487180058718273432148 + +#include +#include +#include +#include "dc.h" + + +namespace zen +{ +//functions supporting right-to-left GUI layout +void drawBitmapRtlMirror (wxDC& dc, const wxImage& img, const wxRect& rect, int alignment, std::optional& buffer); +void drawBitmapRtlNoMirror(wxDC& dc, const wxImage& img, const wxRect& rect, int alignment); +//wxDC::DrawIcon DOES mirror by default -> implement RTL support when needed + +wxImage mirrorIfRtl(const wxImage& img); + +//manual text flow correction: https://www.w3.org/International/articles/inline-bidi-markup/ + + + + + + + + +//---------------------- implementation ------------------------ +namespace impl +{ +//don't use wxDC::DrawLabel: +// - expensive GetTextExtent() call even when passing an empty string!!! +// - 1-off alignment bugs! +inline +void drawBitmapAligned(wxDC& dc, const wxImage& img, const wxRect& rect, int alignment) +{ + wxPoint pt = rect.GetTopLeft(); + if (alignment & wxALIGN_RIGHT) //note: wxALIGN_LEFT == 0! + pt.x += rect.width - screenToWxsize(img.GetWidth()); + else if (alignment & wxALIGN_CENTER_HORIZONTAL) + pt.x += (rect.width - screenToWxsize(img.GetWidth())) / 2; + + if (alignment & wxALIGN_BOTTOM) //note: wxALIGN_TOP == 0! + pt.y += rect.height - screenToWxsize(img.GetHeight()); + else if (alignment & wxALIGN_CENTER_VERTICAL) + pt.y += (rect.height - screenToWxsize(img.GetHeight())) / 2; + + dc.DrawBitmap(toScaledBitmap(img), pt); +} +} + + +inline +void drawBitmapRtlMirror(wxDC& dc, const wxImage& img, const wxRect& rect, int alignment, std::optional& buffer) +{ + switch (dc.GetLayoutDirection()) + { + case wxLayout_LeftToRight: + return impl::drawBitmapAligned(dc, img, rect, alignment); + + case wxLayout_RightToLeft: + if (rect.GetWidth() > 0 && rect.GetHeight() > 0) + { + if (!buffer || buffer->GetSize() != rect.GetSize()) //[!] since we do a mirror, width needs to match exactly! + buffer.emplace(rect.GetSize()); + + if (buffer->GetScaleFactor() != dc.GetContentScaleFactor()) //needed here? + buffer->SetScaleFactor(dc.GetContentScaleFactor()); // + + wxMemoryDC memDc(*buffer); //copies scale factor from wxBitmap + memDc.Blit(wxPoint(0, 0), rect.GetSize(), &dc, rect.GetTopLeft()); //blit in: background is mirrored due to memDc/dc having different layout direction! + + impl::drawBitmapAligned(memDc, img, wxRect(0, 0, rect.width, rect.height), alignment); + //note: we cannot simply use memDc.SetLayoutDirection(wxLayout_RightToLeft) due to some strange 1 pixel bug! 2022-04-04: maybe fixed in wxWidgets 3.1.6? + + dc.Blit(rect.GetTopLeft(), rect.GetSize(), &memDc, wxPoint(0, 0)); //blit out: mirror once again + } + break; + + case wxLayout_Default: //CAVEAT: wxPaintDC/wxMemoryDC on wxGTK/wxMAC does not implement SetLayoutDirection()!!! => GetLayoutDirection() == wxLayout_Default + if (wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft) + return impl::drawBitmapAligned(dc, img.Mirror(), rect, alignment); + else + return impl::drawBitmapAligned(dc, img, rect, alignment); + } +} + + +inline +void drawBitmapRtlNoMirror(wxDC& dc, const wxImage& img, const wxRect& rect, int alignment) +{ + return impl::drawBitmapAligned(dc, img, rect, alignment); //wxDC::DrawBitmap does NOT mirror by default +} + + +inline +wxImage mirrorIfRtl(const wxImage& img) +{ + if (wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft) + return img.Mirror(); + else + return img; +} +} + +#endif //RTL_H_0183487180058718273432148 diff --git a/wx+/std_button_layout.h b/wx+/std_button_layout.h new file mode 100644 index 0000000..2a2fd7c --- /dev/null +++ b/wx+/std_button_layout.h @@ -0,0 +1,142 @@ +// ***************************************************************************** +// * 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 STD_BUTTON_LAYOUT_H_183470321478317214 +#define STD_BUTTON_LAYOUT_H_183470321478317214 + +#include +#include +#include "dc.h" + + +namespace zen +{ +struct StdButtons +{ + StdButtons& setAffirmative (wxButton* btn) { btnYes = btn; return *this; } + StdButtons& setAffirmativeAll(wxButton* btn) { btnYes2 = btn; return *this; } + StdButtons& setNegative (wxButton* btn) { btnNo = btn; return *this; } + StdButtons& setCancel (wxButton* btn) { btnCancel = btn; return *this; } + + wxButton* btnYes = nullptr; + wxButton* btnYes2 = nullptr; + wxButton* btnNo = nullptr; + wxButton* btnCancel = nullptr; +}; + +void setStandardButtonLayout(wxBoxSizer& sizer, const StdButtons& buttons = StdButtons()); +//sizer width will change! => call wxWindow::Fit and wxWindow::Dimensions + + +inline +constexpr int getMenuIconDipSize() +{ + return 20; +} + + +inline +int getDefaultButtonHeight() +{ + const int defaultHeight = wxButton::GetDefaultSize().GetHeight(); //buffered by wxWidgets + return std::max(defaultHeight, dipToWxsize(31)); //default button height is much too small => increase! +} + + + + + + + + + + +//--------------- impelementation ------------------------------------------- +inline +void setStandardButtonLayout(wxBoxSizer& sizer, const StdButtons& buttons) +{ + assert(sizer.GetOrientation() == wxHORIZONTAL); + + //GNOME Human Interface Guidelines: https://developer.gnome.org/hig-book/3.2/hig-book.html#alert-spacing + const int spaceH = dipToWxsize( 6); //OK + const int spaceRimH = dipToWxsize(12); //OK + const int spaceRimV = dipToWxsize(12); //OK + + StdButtons buttonsTmp = buttons; + + auto detach = [&](wxButton*& btn) + { + if (btn) + { + assert(btn->GetContainingSizer() == &sizer); + if (btn->IsShown() && sizer.Detach(btn)) + return; + + assert(false); //why is it hidden!? + btn = nullptr; + } + }; + + detach(buttonsTmp.btnYes); + detach(buttonsTmp.btnYes2); + detach(buttonsTmp.btnNo); + detach(buttonsTmp.btnCancel); + + + //"All your fixed-size spacers are belong to us!" => have a clean slate: consider repeated setStandardButtonLayout() calls + for (size_t pos = sizer.GetItemCount(); pos-- > 0;) + if (wxSizerItem& item = *sizer.GetItem(pos); + item.IsSpacer() && item.GetProportion() == 0 && item.GetSize().y == 0) + { + [[maybe_unused]] const bool rv = sizer.Detach(pos); + assert(rv); + } + + //set border on left considering existing items + if (!sizer.IsEmpty()) //for yet another retarded reason wxWidgets will have wxSizer::GetItem(0) cause an assert rather than just return nullptr as documented + if (wxSizerItem& item = *sizer.GetItem(static_cast(0)); + item.IsShown()) + { + assert(item.GetBorder() <= spaceRimV); //pragmatic check: other controls in the sizer should not have a larger border + + if (const int flag = item.GetFlag(); + flag & wxLEFT) + item.SetFlag(flag & ~wxLEFT); + + sizer.Prepend(spaceRimH, 0); + } + + + bool settingFirstButton = true; + auto attach = [&](wxButton* btn) + { + if (btn) + { + assert(btn->GetMinSize().GetHeight() == -1); //let OS or this routine do the sizing! note: OS X does not allow changing the (visible!) button height! + btn->SetMinSize({-1, getDefaultButtonHeight()}); + + if (settingFirstButton) + settingFirstButton = false; + else + sizer.Add(spaceH, 0); + sizer.Add(btn, 0, wxTOP | wxBOTTOM | wxALIGN_CENTER_VERTICAL, spaceRimV); + } + }; + + sizer.Add(spaceRimH, 0); + attach(buttonsTmp.btnNo); + attach(buttonsTmp.btnCancel); + attach(buttonsTmp.btnYes2); + attach(buttonsTmp.btnYes); + + sizer.Add(spaceRimH, 0); + + //OS X: there should be at least one button following the gap after the "dangerous" no-button + assert(buttonsTmp.btnYes || buttonsTmp.btnCancel); +} +} + +#endif //STD_BUTTON_LAYOUT_H_183470321478317214 diff --git a/wx+/taskbar.cpp b/wx+/taskbar.cpp new file mode 100644 index 0000000..2cc3150 --- /dev/null +++ b/wx+/taskbar.cpp @@ -0,0 +1,19 @@ +// ***************************************************************************** +// * 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 "taskbar.h" + + +using namespace zen; + + +class Taskbar::Impl {}; + +Taskbar::Taskbar(wxWindow* window) { throw TaskbarNotAvailable(); } +Taskbar::~Taskbar() {} + +void Taskbar::setStatus(Status status) {} +void Taskbar::setProgress(double fraction) {} diff --git a/wx+/taskbar.h b/wx+/taskbar.h new file mode 100644 index 0000000..f660013 --- /dev/null +++ b/wx+/taskbar.h @@ -0,0 +1,42 @@ +// ***************************************************************************** +// * 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 TASKBAR_H_98170845709124456 +#define TASKBAR_H_98170845709124456 + +#include +#include + + +namespace zen +{ +class TaskbarNotAvailable {}; + +class Taskbar +{ +public: + Taskbar(wxWindow* window); //throw TaskbarNotAvailable + ~Taskbar(); + + enum class Status + { + normal, + indeterminate, + warning, + error, + paused, + }; + + void setStatus(Status status); //noexcept + void setProgress(double fraction); //between [0, 1]; noexcept + +private: + class Impl; + const std::unique_ptr pimpl_; +}; +} + +#endif //TASKBAR_H_98170845709124456 diff --git a/wx+/toggle_button.h b/wx+/toggle_button.h new file mode 100644 index 0000000..29aea60 --- /dev/null +++ b/wx+/toggle_button.h @@ -0,0 +1,89 @@ +// ***************************************************************************** +// * 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 TOGGLE_BUTTON_H_8173024810574556 +#define TOGGLE_BUTTON_H_8173024810574556 + +#include +#include + + +namespace zen +{ +class ToggleButton : public wxBitmapButton +{ +public: + //wxBitmapButton constructor + ToggleButton(wxWindow* parent, + wxWindowID id, + const wxBitmap& bitmap, + const wxPoint& pos = wxDefaultPosition, + const wxSize& size = wxDefaultSize, + long style = 0, + const wxValidator& validator = wxDefaultValidator, + const wxString& name = wxASCII_STR(wxButtonNameStr)) : + wxBitmapButton(parent, id, bitmap, pos, size, style, validator, name) {} + + //wxButton constructor + ToggleButton(wxWindow* parent, + wxWindowID id, + const wxString& label, + const wxPoint& pos = wxDefaultPosition, + const wxSize& size = wxDefaultSize, + long style = 0, + const wxValidator& validator = wxDefaultValidator, + const wxString& name = wxASCII_STR(wxButtonNameStr)) : + wxBitmapButton(parent, id, + //(FreeFileSync_x86_64:77379): Gtk-CRITICAL **: 11:04:31.752: IA__gtk_widget_modify_style: assertion 'GTK_IS_WIDGET (widget)' failed + rectangleImage({1, 1}, *wxRED), + pos, size, style, validator, name) + { + SetLabel(label); + } + + void init(const wxImage& imgActive, + const wxImage& imgInactive); + + void setActive(bool value); + bool isActive() const { return active_; } + void toggle() { setActive(!active_); } + +private: + bool active_ = false; + wxImage imgActive_; + wxImage imgInactive_; +}; + + + + + + + +//######################## implementation ######################## +inline +void ToggleButton::init(const wxImage& imgActive, + const wxImage& imgInactive) +{ + imgActive_ = imgActive; + imgInactive_ = imgInactive; + + setImage(*this, active_ ? imgActive_ : imgInactive_); +} + + +inline +void ToggleButton::setActive(bool value) +{ + if (active_ != value) + { + active_ = value; + setImage(*this, active_ ? imgActive_ : imgInactive_); + } +} +} + +#endif //TOGGLE_BUTTON_H_8173024810574556 diff --git a/wx+/tooltip.cpp b/wx+/tooltip.cpp new file mode 100644 index 0000000..007abbf --- /dev/null +++ b/wx+/tooltip.cpp @@ -0,0 +1,120 @@ +// ***************************************************************************** +// * 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 "tooltip.h" +#include +#include +#include +#include +#include +#include +#include "bitmap_button.h" +#include "dc.h" + #include + +using namespace zen; + + +namespace +{ +const int TIP_WINDOW_OFFSET_DIP = 20; +} + + +class Tooltip::TooltipDlgGenerated : public wxDialog +{ +public: + TooltipDlgGenerated(wxWindow* parent) : //Suse Linux/X11: needs parent window, else there are z-order issues + wxDialog(parent, wxID_ANY, L"" /*title*/, wxDefaultPosition, wxDefaultSize, wxSIMPLE_BORDER /*style*/) + //wxSIMPLE_BORDER side effect: removes title bar on KDE + { + SetSizeHints(wxDefaultSize, wxDefaultSize); + SetExtraStyle(this->GetExtraStyle() | wxWS_EX_TRANSIENT); + SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_INFOBK)); //both required: on Ubuntu background is black, foreground white! + SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_INFOTEXT)); // + + wxBoxSizer* bSizer158 = new wxBoxSizer(wxHORIZONTAL); + bitmapLeft_ = new wxStaticBitmap(this, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0); + bSizer158->Add(bitmapLeft_, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); + + staticTextMain_ = new wxStaticText(this, wxID_ANY, wxString(), wxDefaultPosition, wxDefaultSize, 0); + bSizer158->Add(staticTextMain_, 0, wxALL | wxALIGN_CENTER_HORIZONTAL | wxALIGN_CENTER_VERTICAL, 5); + + SetSizer(bSizer158); + + } + + bool AcceptsFocus() const override { return false; } //any benefit? + + wxStaticText* staticTextMain_ = nullptr; + wxStaticBitmap* bitmapLeft_ = nullptr; +}; + + +void Tooltip::show(const wxString& text, wxPoint mousePos, const wxImage* img) +{ + if (!tipWindow_) + tipWindow_ = new TooltipDlgGenerated(&parent_); //ownership passed to parent + + const wxImage& newImg = img ? *img : wxNullImage; + + const bool imgChanged = !newImg.IsSameAs(lastUsedImg_); + const bool txtChanged = text != lastUsedText_; + + if (imgChanged) + { + lastUsedImg_ = newImg; + setImage(*tipWindow_->bitmapLeft_, newImg); + // tipWindow_->Refresh(); //needed if bitmap size changed! ->??? + } + + if (txtChanged) + { + lastUsedText_ = text; + tipWindow_->staticTextMain_->SetLabelText(text); + + tipWindow_->staticTextMain_->Wrap(dipToWxsize(600)); + } + + if (imgChanged || txtChanged) + //tipWindow_->Dimensions(); -> apparently not needed!? + tipWindow_->GetSizer()->SetSizeHints(tipWindow_); //~=Fit() + SetMinSize() +#ifdef __WXGTK3__ + //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 + //=> call wxWindow::Show() to "execute" +#endif + + const wxPoint newPos = mousePos + wxPoint(wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft ? + - dipToWxsize(TIP_WINDOW_OFFSET_DIP) - tipWindow_->GetSize().GetWidth() : + dipToWxsize(TIP_WINDOW_OFFSET_DIP), + dipToWxsize(TIP_WINDOW_OFFSET_DIP)); + + if (newPos != tipWindow_->GetScreenPosition()) + tipWindow_->Move(newPos); + //caveat: possible endless loop! mouse pointer must NOT be within tipWindow! + //else it will trigger a wxEVT_LEAVE_WINDOW on middle grid which will hide the window, causing the window to be shown again via this method, etc. + + if (!tipWindow_->IsShown()) + tipWindow_->Show(); +} + + +void Tooltip::hide() +{ + if (tipWindow_) + { +#if GTK_MAJOR_VERSION == 2 //the tooltip sometimes turns blank or is not shown again after it was hidden: e.g. drag-selection on middle grid + //=> no such issues on GTK3! + tipWindow_->Destroy(); //apply brute force: + tipWindow_ = nullptr; // + lastUsedImg_ = wxNullImage; + +#elif GTK_MAJOR_VERSION == 3 + tipWindow_->Hide(); +#else +#error unknown GTK version! +#endif + } +} diff --git a/wx+/tooltip.h b/wx+/tooltip.h new file mode 100644 index 0000000..e20cb3d --- /dev/null +++ b/wx+/tooltip.h @@ -0,0 +1,35 @@ +// ***************************************************************************** +// * 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 TOOLTIP_H_8912740832170515 +#define TOOLTIP_H_8912740832170515 + +#include +#include + + +namespace zen +{ +class Tooltip +{ +public: + Tooltip(wxWindow& parent) : parent_(parent) {} //parent needs to live at least as long as this instance! + + void show(const wxString& text, + wxPoint mousePos, //absolute screen coordinates + const wxImage* img = nullptr); + void hide(); + +private: + class TooltipDlgGenerated; + TooltipDlgGenerated* tipWindow_ = nullptr; + wxWindow& parent_; + wxImage lastUsedImg_; + wxString lastUsedText_; //needed, considering "SetLabelText(textFixed)" +}; +} + +#endif //TOOLTIP_H_8912740832170515 diff --git a/wx+/window_layout.h b/wx+/window_layout.h new file mode 100644 index 0000000..dee29fb --- /dev/null +++ b/wx+/window_layout.h @@ -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 * +// ***************************************************************************** + +#ifndef WINDOW_LAYOUT_H_23849632846734343234532 +#define WINDOW_LAYOUT_H_23849632846734343234532 + +#include +#include + #include +#include "color_tools.h" +#include "dc.h" + + +namespace zen +{ +//set portable font size in multiples of the operating system's default font size +void setRelativeFontSize(wxWindow& control, double factor); +void setMainInstructionFont(wxWindow& control); //following Windows/Gnome/OS X guidelines + +void setDefaultWidth(wxSpinCtrl& m_spinCtrl); + + + + + + + + +//###################### implementation ##################### +inline +void setRelativeFontSize(wxWindow& control, double factor) +{ + wxFont font = control.GetFont(); + font.SetPointSize(std::round(wxNORMAL_FONT->GetPointSize() * factor)); + control.SetFont(font); +} + + +inline +void setMainInstructionFont(wxWindow& control) +{ + wxFont font = control.GetFont(); + font.SetPointSize(std::round(wxNORMAL_FONT->GetPointSize() * 12.0 / 11)); + font.SetWeight(wxFONTWEIGHT_BOLD); + + control.SetFont(font); +} + + +inline +void setDefaultWidth(wxSpinCtrl& m_spinCtrl) +{ +#ifdef __WXGTK3__ + //there's no way to set width using GTK's CSS! => + m_spinCtrl.InvalidateBestSize(); + ::gtk_entry_set_width_chars(GTK_ENTRY(m_spinCtrl.m_widget), 3); + +#if 0 //apparently not needed!? + if (::gtk_check_version(3, 12, 0) == NULL) + ::gtk_entry_set_max_width_chars(GTK_ENTRY(m_spinCtrl.m_widget), 3); +#endif + + //get rid of excessive default width on old GTK3 3.14 (Debian); + //gtk_entry_set_width_chars() not working => mitigate + m_spinCtrl.SetMinSize({dipToWxsize(100), -1}); //must be wider than gtk_entry_set_width_chars(), or it breaks newer GTK e.g. 3.22! + +#if 0 //generic property syntax: + GValue bval = G_VALUE_INIT; + ::g_value_init(&bval, G_TYPE_BOOLEAN); + ::g_value_set_boolean(&bval, false); + ZEN_ON_SCOPE_EXIT(::g_value_unset(&bval)); + ::g_object_set_property(G_OBJECT(m_spinCtrl.m_widget), "visibility", &bval); +#endif +#else + m_spinCtrl.SetMinSize({dipToWxsize(70), -1}); +#endif + +} +} + +#endif //WINDOW_LAYOUT_H_23849632846734343234532 diff --git a/wx+/window_tools.h b/wx+/window_tools.h new file mode 100644 index 0000000..5be28fc --- /dev/null +++ b/wx+/window_tools.h @@ -0,0 +1,273 @@ +// ***************************************************************************** +// * 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 FOCUS_1084731021985757843 +#define FOCUS_1084731021985757843 + +#include +#include + + +namespace zen +{ +//pretty much the same like "bool wxWindowBase::IsDescendant(wxWindowBase* child) const" but without the obvious misnomer +inline +bool isComponentOf(const wxWindow* child, const wxWindow* top) +{ + for (const wxWindow* wnd = child; wnd != nullptr; wnd = wnd->GetParent()) + if (wnd == top) + return true; + return false; +} + + +inline +wxWindow& getRootWindow(wxWindow& child) +{ + wxWindow* root = &child; + for (;;) + if (wxWindow* parent = root->GetParent()) + root = parent; + else + return *root; +} + + +inline +wxTopLevelWindow* getTopLevelWindow(wxWindow* child) +{ + for (wxWindow* wnd = child; wnd != nullptr; wnd = wnd->GetParent()) + if (auto tlw = dynamic_cast(wnd)) //why does wxWidgets use wxWindows::IsTopLevel() ?? + return tlw; + return nullptr; +} + + +/* Preserving input focus has to be more clever than: + wxWindow* oldFocus = wxWindow::FindFocus(); + ZEN_ON_SCOPE_EXIT(if (oldFocus) oldFocus->SetFocus()); + +=> wxWindow::SetFocus() internally calls Win32 ::SetFocus, which calls ::SetActiveWindow, which - lord knows why - changes the foreground window to the focus window + even if the user is currently busy using a different app! More curiosity: this foreground focus stealing happens only during the *first* SetFocus() after app start! + It also can be avoided by changing focus back and forth with some other app after start => wxWidgets bug or Win32 feature??? */ + +inline +void setFocusIfActive(wxWindow& win) //don't steal keyboard focus when currently using a different foreground application +{ + if (wxTopLevelWindow* topWin = getTopLevelWindow(&win)) + if (topWin->IsActive()) //Linux/macOS: already behaves just like ::GetForegroundWindow() on Windows! + win.SetFocus(); +} + + +struct FocusPreserver +{ + FocusPreserver() + { + if (wxWindow* win = wxWindow::FindFocus()) + setFocus(win); + } + + ~FocusPreserver() + { + //wxTopLevelWindow::IsActive() does NOT call Win32 ::GetActiveWindow()! + //Instead it checks if ::GetFocus() is set somewhere inside the top level + //Note: Both Win32 active and focus windows are *thread-local* values, while foreground window is global! https://devblogs.microsoft.com/oldnewthing/20131016-00/?p=2913 + + if (oldFocusId_ != wxID_ANY) + if (wxWindow* oldFocusWin = wxWindow::FindWindowById(oldFocusId_)) + { + assert(oldFocusWin->IsEnabled()); //only enabled windows can have focus, so why wouldn't it be anymore? + setFocusIfActive(*oldFocusWin); + } + } + + wxWindowID getFocusId() const { return oldFocusId_; } + + void setFocus(wxWindow* win) + { + oldFocusId_ = win->GetId(); + assert(oldFocusId_ != wxID_ANY); + } + + void dismiss() { oldFocusId_ = wxID_ANY; } + +private: + wxWindowID oldFocusId_ = wxID_ANY; + //don't store wxWindow* which may be dangling during ~FocusPreserver()! + //test: click on delete folder pair and immediately press F5 => focus window (= FP del button) is defer-deleted during sync +}; + + +class WindowLayout +{ +public: + struct Dimensions + { + std::optional size; + std::optional pos; + bool isMaximized = false; + }; + static void setInitial(wxTopLevelWindow& topWin, const Dimensions& dim, wxSize defaultSize) + { + initialDims_[&topWin] = dim; + + wxSize newSize = defaultSize; + std::optional newPos; + //set dialog size and position: + // - width/height are invalid if the window is minimized (eg x,y = -32000; width = 160, height = 28) + // - multi-monitor setup: dialog may be placed on second monitor which is currently turned off + if (dim.size && + dim.size->GetWidth () > 0 && + dim.size->GetHeight() > 0) + { + if (dim.pos) + { + //calculate how much of the dialog will be visible on screen + const int dlgArea = dim.size->GetWidth() * dim.size->GetHeight(); + int dlgAreaMaxVisible = 0; + + const int monitorCount = wxDisplay::GetCount(); + for (int i = 0; i < monitorCount; ++i) + { + wxRect overlap = wxDisplay(i).GetClientArea().Intersect(wxRect(*dim.pos, *dim.size)); + dlgAreaMaxVisible = std::max(dlgAreaMaxVisible, overlap.GetWidth() * overlap.GetHeight()); + } + + if (dlgAreaMaxVisible > 0.1 * dlgArea //at least 10% of the dialog should be visible! + ) + { + newSize = *dim.size; + newPos = dim.pos; + } + } + else + newSize = *dim.size; + } + + //old comment: "wxGTK's wxWindow::SetSize seems unreliable and behaves like a wxWindow::SetClientSize + // => use wxWindow::SetClientSize instead (for the record: no such issue on Windows/macOS) + //2018-10-15: Weird new problem on CentOS/Ubuntu: SetClientSize() + SetPosition() fail to set correct dialog *position*, but SetSize() + SetPosition() do! + // => old issues with SetSize() seem to be gone... => revert to SetSize() + if (newPos) + topWin.SetSize(wxRect(*newPos, newSize)); + else + { + topWin.SetSize(newSize); + topWin.Center(); + } + + if (dim.isMaximized) //no real need to support both maximize and full screen functions + { + topWin.Maximize(true); + } + + +#if 0 //wxWidgets alternative: apparently no benefits (not even on Wayland! but strange decisions: why restore the minimized state!???) + class GeoSerializer : public wxTopLevelWindow::GeometrySerializer + { + public: + GeoSerializer(const std::string& l) + { + split(l, ' ', [&](const std::string_view phrase) + { + assert(phrase.empty() || contains(phrase, '=')); + if (contains(phrase, '=')) + valuesByName_[utfTo(beforeFirst(phrase, '=', IfNotFoundReturn::none))] = + /**/ stringTo(afterFirst(phrase, '=', IfNotFoundReturn::none)); + }); + } + + bool SaveField(const wxString& name, int value) const /*NO, this must not be const!*/ override { return false; } + + bool RestoreField(const wxString& name, int* value) /*const: yes, this MAY(!) be const*/ override + { + auto it = valuesByName_.find(name); + if (it == valuesByName_.end()) + return false; + * value = it->second; + return true; + } + private: + std::unordered_map valuesByName_; + } serializer(layout); + + if (!topWin.RestoreToGeometry(serializer)) //apparently no-fail as long as GeometrySerializer::RestoreField is! + assert(false); +#endif + } + + //destructive! changes window size! + static Dimensions getBeforeClose(wxTopLevelWindow& topWin) + { + //we need to portably retrieve non-iconized, non-maximized size and position + // non-portable: Win32 GetWindowPlacement(); wxWidgets take: wxTopLevelWindow::SaveGeometry/RestoreToGeometry() + if (topWin.IsIconized()) + topWin.Iconize(false); + + bool isMaximized = false; + if (topWin.IsMaximized()) //evaluate AFTER uniconizing! + { + topWin.Maximize(false); + isMaximized = true; + } + + std::optional size = topWin.GetSize(); + std::optional pos = topWin.GetPosition(); + + if (isMaximized) + if (!topWin.IsShown() //=> Win: can't trust size GetSize()/GetPosition(): still at full screen size! + //wxGTK: returns full screen size and strange position (65/-4) + //OS X 10.9 (but NO issue on 10.11!) returns full screen size and strange position (0/-22) + || pos->y < 0 + ) + { + size = std::nullopt; + pos = std::nullopt; + } + + //reuse previous values if current ones are not available: + if (const auto it = initialDims_.find(&topWin); + it != initialDims_.end()) + { + if (!size) + size = it->second.size; + + if (!pos) + pos = it->second.pos; + } + + return {size, pos, isMaximized}; + +#if 0 //wxWidgets alternative: apparently no benefits (not even on Wayland! but strange decisions: why restore the minimized state!???) + struct : wxTopLevelWindow::GeometrySerializer + { + bool SaveField(const wxString& name, int value) const /*NO, this must not be const!*/ override + { + layout_ += utfTo(name) + '=' + numberTo(value) + ' '; + return true; + } + + bool RestoreField(const wxString& name, int* value) /*const: yes, this MAY(!) be const*/ override { return false; } + + mutable //wxWidgets people: 1. learn when and when not to use const for input/output functions! see SaveField/RestoreField() + // 2. learn flexible software design: why are input/output tied up in a single GeometrySerializer implementation? + std::string layout_; + } serializer; + + if (topWin.SaveGeometry(serializer)) //apparently no-fail as long as GeometrySerializer::SaveField is! + return serializer.layout_; + else + assert(false); +#endif + } + +private: + inline static std::unordered_map initialDims_; +}; +} + +#endif //FOCUS_1084731021985757843 diff --git a/xBRZ/src/xbrz.cpp b/xBRZ/src/xbrz.cpp new file mode 100644 index 0000000..654266c --- /dev/null +++ b/xBRZ/src/xbrz.cpp @@ -0,0 +1,1363 @@ +// **************************************************************************** +// * This file is part of the xBRZ project. It is distributed under * +// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * +// * Copyright (C) Zenju (zenju AT gmx DOT de) - All Rights Reserved * +// * * +// * Additionally and as a special exception, the author gives permission * +// * to link the code of this program with the following libraries * +// * (or with modified versions that use the same licenses), and distribute * +// * linked combinations including the two: MAME, FreeFileSync, Snes9x, ePSXe * +// * * +// * You must obey the GNU General Public License in all respects for all of * +// * the code used other than MAME, FreeFileSync, Snes9x, ePSXe. * +// * If you modify this file, you may extend this exception to your version * +// * of the file, but you are not obligated to do so. If you do not wish to * +// * do so, delete this exception statement from your version. * +// **************************************************************************** + +#include "xbrz.h" +#include +#include +#include //std::sqrt +#include +#include "xbrz_tools.h" + +using namespace xbrz; + + +namespace +{ +//blend front color with opacity M / N over opaque background: https://en.wikipedia.org/wiki/Alpha_compositing +//Limitation: alpha should be applied in gamma-decoded linear RGB space: https://ssp.impulsetrain.com/gamma-premult.html +template inline +uint32_t gradientRGB(uint32_t pixFront, uint32_t pixBack) +{ + static_assert(0 < M && M < N && N <= 1000); + + auto calcColor = [](unsigned char colFront, unsigned char colBack) + { + return static_cast(uintDivRound(colFront * M + colBack * (N - M), N)); + }; + + return makePixel(calcColor(getRed (pixFront), getRed (pixBack)), + calcColor(getGreen(pixFront), getGreen(pixBack)), + calcColor(getBlue (pixFront), getBlue (pixBack))); +} + + +//find intermediate color between two colors with alpha channels (=> NO alpha blending!!!) +//Limitation: alpha should be applied in gamma-decoded linear RGB space: https://ssp.impulsetrain.com/gamma-premult.html +template inline +uint32_t gradientARGB(uint32_t pixFront, uint32_t pixBack) +{ + static_assert(0 < M && M < N && N <= 1000); + + const unsigned int weightFront = getAlpha(pixFront) * M; + const unsigned int weightBack = getAlpha(pixBack) * (N - M); + const unsigned int weightSum = weightFront + weightBack; + if (weightSum == 0) + return 0; + + auto calcColor = [=](unsigned char colFront, unsigned char colBack) + { + return static_cast(uintDivRound(colFront * weightFront + colBack * weightBack, weightSum)); + }; + + return makePixel(static_cast(uintDivRound(weightSum, N)), + calcColor(getRed (pixFront), getRed (pixBack)), + calcColor(getGreen(pixFront), getGreen(pixBack)), + calcColor(getBlue (pixFront), getBlue (pixBack))); +} + + +//inline +//double fastSqrt(double n) +//{ +// __asm //speeds up xBRZ by about 9% compared to std::sqrt which internally uses the same assembler instructions but adds some "fluff" +// { +// fld n +// fsqrt +// } +//} +// + + +#if defined __GNUC__ + #define FORCE_INLINE __attribute__((always_inline)) inline +#else + #define FORCE_INLINE inline +#endif + + +enum RotationDegree //clock-wise +{ + ROT_0, + ROT_90, + ROT_180, + ROT_270 +}; + +//calculate input matrix coordinates after rotation at compile time +template +struct MatrixRotation; + +template +struct MatrixRotation +{ + static const size_t I_old = I; + static const size_t J_old = J; +}; + +template //(i, j) = (row, col) indices, N = size of (square) matrix +struct MatrixRotation +{ + static const size_t I_old = N - 1 - MatrixRotation(rotDeg - 1), I, J, N>::J_old; //old coordinates before rotation! + static const size_t J_old = MatrixRotation(rotDeg - 1), I, J, N>::I_old; // +}; + + +template +class OutputMatrix +{ +public: + OutputMatrix(uint32_t* out, int outWidth) : //access matrix area, top-left at position "out" for image with given width + out_(out), + outWidth_(outWidth) {} + + template + uint32_t& ref() const + { + static const size_t I_old = MatrixRotation::I_old; + static const size_t J_old = MatrixRotation::J_old; + return *(out_ + J_old + I_old * outWidth_); + } + +private: + uint32_t* out_; + const int outWidth_; +}; + + +template inline +T square(T value) { return value * value; } + + +#if 0 +inline +double distRGB(uint32_t pix1, uint32_t pix2) +{ + const double r_diff = static_cast(getRed (pix1)) - getRed (pix2); + const double g_diff = static_cast(getGreen(pix1)) - getGreen(pix2); + const double b_diff = static_cast(getBlue (pix1)) - getBlue (pix2); + + //euklidean RGB distance + return std::sqrt(square(r_diff) + square(g_diff) + square(b_diff)); +} +#endif + + +inline +double distYCbCr(uint32_t pix1, uint32_t pix2, double /*testAttribute*/) +{ + //https://en.wikipedia.org/wiki/YCbCr#ITU-R_BT.601_conversion + //Y'CbCr conversion is a matrix multiplication => take advantage of linearity by subtracting first! + //NOTE: input is gamma-encoded RGB! => what does this mean for the output distance!?? + const int r_diff = static_cast(getRed (pix1)) - getRed (pix2); //defer division by 255 to after matrix multiplication + const int g_diff = static_cast(getGreen(pix1)) - getGreen(pix2); // + const int b_diff = static_cast(getBlue (pix1)) - getBlue (pix2); //substraction for int is noticeable faster than for double! + + //const double k_b = 0.0722; //ITU-R BT.709 conversion + //const double k_r = 0.2126; // + const double k_b = 0.0593; //ITU-R BT.2020 conversion + const double k_r = 0.2627; // + const double k_g = 1 - k_b - k_r; + + const double scale_b = 0.5 / (1 - k_b); + const double scale_r = 0.5 / (1 - k_r); + + const double y = k_r * r_diff + k_g * g_diff + k_b * b_diff; //[!], analog YCbCr! + const double c_b = scale_b * (b_diff - y); + const double c_r = scale_r * (r_diff - y); + + //we skip division by 255 to have similar range like other distance functions + return std::sqrt(square(y) + square(c_b) + square(c_r)); +} + + +inline +double distYCbCrBuffered(uint32_t pix1, uint32_t pix2, double /*testAttribute*/) +{ + //30% perf boost compared to plain distYCbCr()! + //consumes 64 MB memory; using double is only 2% faster, but takes 128 MB + static const std::vector diffToDist = [] + { + std::vector tmp; + + for (uint32_t i = 0; i < 256 * 256 * 256; ++i) //startup time: 114 ms on Intel Core i5 (four cores) + { + const int r_diff = static_cast(getByte<2>(i)) * 2; + const int g_diff = static_cast(getByte<1>(i)) * 2; + const int b_diff = static_cast(getByte<0>(i)) * 2; + + const double k_b = 0.0593; //ITU-R BT.2020 conversion + const double k_r = 0.2627; // + const double k_g = 1 - k_b - k_r; + + const double scale_b = 0.5 / (1 - k_b); + const double scale_r = 0.5 / (1 - k_r); + + const double y = k_r * r_diff + k_g * g_diff + k_b * b_diff; //[!], analog YCbCr! + const double c_b = scale_b * (b_diff - y); + const double c_r = scale_r * (r_diff - y); + + tmp.push_back(static_cast(std::sqrt(square(y) + square(c_b) + square(c_r)))); + } + return tmp; + }(); + + //if (pix1 == pix2) -> 8% perf degradation! + // return 0; + //if (pix1 < pix2) + // std::swap(pix1, pix2); -> 30% perf degradation!!! + + const int r_diff = static_cast(getRed (pix1)) - getRed (pix2); + const int g_diff = static_cast(getGreen(pix1)) - getGreen(pix2); + const int b_diff = static_cast(getBlue (pix1)) - getBlue (pix2); + + const size_t index = (static_cast(r_diff / 2) << 16) | //slightly reduce precision (division by 2) to squeeze value into single byte + (static_cast(g_diff / 2) << 8) | + (static_cast(b_diff / 2)); + +#if 0 //attention: the following calculation creates an asymmetric color distance!!! (e.g. r_diff=46 will be unpacked as 45, but r_diff=-46 unpacks to -47 + const size_t index = (((r_diff + 0xFF) / 2) << 16) | //slightly reduce precision (division by 2) to squeeze value into single byte + (((g_diff + 0xFF) / 2) << 8) | + (( b_diff + 0xFF) / 2); +#endif + return diffToDist[index]; +} + + + + +enum BlendType +{ + BLEND_NONE = 0, + BLEND_NORMAL, //a normal indication to blend + BLEND_DOMINANT, //a strong indication to blend + //attention: BlendType must fit into the value range of 2 bit!!! +}; + +struct BlendResult +{ + BlendType + blend_e, blend_f, + blend_h, blend_i; +}; + + +struct Kernel_3x3 +{ + uint32_t + a, b, c, + d, e, f, + g, h, i; +}; + +struct Kernel_4x4 : Kernel_3x3 +{ + uint32_t j, k, l, m, n, o, p; +}; +/* input kernel for preprocessing step: + + ----------------- + | A | B | C | P | + |---|---|---|---| + | D | E | F | O | evaluate the four corners between E, F, H, I + |---|---|---|---| input pixel is at position E + | G | H | I | N | + |---|---|---|---| + | J | K | L | M | + ----------------- */ + +template +FORCE_INLINE //detect blend direction +BlendResult preProcessCorners(const Kernel_4x4& ker, const xbrz::ScalerCfg& cfg) //result: E, F, H, I corners of "GradientType" +{ + if ((ker.e == ker.f && + ker.h == ker.i) || + (ker.e == ker.h && + ker.f == ker.i)) + return {}; + + auto dist = [&](uint32_t pix1, uint32_t pix2) { return ColorDistance::dist(pix1, pix2, cfg.testAttribute); }; + + const double hf = dist(ker.g, ker.e) + dist(ker.e, ker.c) + dist(ker.k, ker.i) + dist(ker.i, ker.o) + cfg.centerDirectionBias * dist(ker.h, ker.f); + const double ei = dist(ker.d, ker.h) + dist(ker.h, ker.l) + dist(ker.b, ker.f) + dist(ker.f, ker.n) + cfg.centerDirectionBias * dist(ker.e, ker.i); + + BlendResult result = {}; + if (hf < ei) //test sample: 70% of values max(hf, ei) / min(hf, ei) are between 1.1 and 3.7 with median being 1.8 + { + const bool dominantGradient = cfg.dominantDirectionThreshold * hf < ei; + if (ker.e != ker.f && ker.e != ker.h) + result.blend_e = dominantGradient ? BLEND_DOMINANT : BLEND_NORMAL; + + if (ker.i != ker.h && ker.i != ker.f) + result.blend_i = dominantGradient ? BLEND_DOMINANT : BLEND_NORMAL; + } + else if (ei < hf) + { + const bool dominantGradient = cfg.dominantDirectionThreshold * ei < hf; + if (ker.h != ker.e && ker.h != ker.i) + result.blend_h = dominantGradient ? BLEND_DOMINANT : BLEND_NORMAL; + + if (ker.f != ker.e && ker.f != ker.i) + result.blend_f = dominantGradient ? BLEND_DOMINANT : BLEND_NORMAL; + } + return result; +} + +#define DEF_GETTER(x) template uint32_t inline get_##x(const Kernel_3x3& ker) { return ker.x; } +//we cannot and NEED NOT write "ker.##x" since ## concatenates preprocessor tokens but "." is not a token +DEF_GETTER(a) DEF_GETTER(b) DEF_GETTER(c) +DEF_GETTER(d) DEF_GETTER(e) DEF_GETTER(f) +DEF_GETTER(g) DEF_GETTER(h) DEF_GETTER(i) +#undef DEF_GETTER + +#define DEF_GETTER(x, y) template <> inline uint32_t get_##x(const Kernel_3x3& ker) { return ker.y; } +DEF_GETTER(a, g) DEF_GETTER(b, d) DEF_GETTER(c, a) +DEF_GETTER(d, h) DEF_GETTER(e, e) DEF_GETTER(f, b) +DEF_GETTER(g, i) DEF_GETTER(h, f) DEF_GETTER(i, c) +#undef DEF_GETTER + +#define DEF_GETTER(x, y) template <> inline uint32_t get_##x(const Kernel_3x3& ker) { return ker.y; } +DEF_GETTER(a, i) DEF_GETTER(b, h) DEF_GETTER(c, g) +DEF_GETTER(d, f) DEF_GETTER(e, e) DEF_GETTER(f, d) +DEF_GETTER(g, c) DEF_GETTER(h, b) DEF_GETTER(i, a) +#undef DEF_GETTER + +#define DEF_GETTER(x, y) template <> inline uint32_t get_##x(const Kernel_3x3& ker) { return ker.y; } +DEF_GETTER(a, c) DEF_GETTER(b, f) DEF_GETTER(c, i) +DEF_GETTER(d, b) DEF_GETTER(e, e) DEF_GETTER(f, h) +DEF_GETTER(g, a) DEF_GETTER(h, d) DEF_GETTER(i, g) +#undef DEF_GETTER + + +//compress four blend types into a single byte +//inline BlendType getTopL (unsigned char b) { return static_cast(0x3 & b); } +inline BlendType getTopR (unsigned char b) { return static_cast(0x3 & (b >> 2)); } +inline BlendType getBottomR(unsigned char b) { return static_cast(0x3 & (b >> 4)); } +inline BlendType getBottomL(unsigned char b) { return static_cast(0x3 & (b >> 6)); } + +inline void clearAddTopL(unsigned char& b, BlendType bt) { b = static_cast(bt); } +inline void addTopR (unsigned char& b, BlendType bt) { b |= (bt << 2); } //buffer is assumed to be initialized before preprocessing! +inline void addBottomR (unsigned char& b, BlendType bt) { b |= (bt << 4); } //e.g. via clearAddTopL() +inline void addBottomL (unsigned char& b, BlendType bt) { b |= (bt << 6); } // + +inline bool blendingNeeded(unsigned char b) +{ + static_assert(BLEND_NONE == 0); + return b != 0; +} + +template inline +unsigned char rotateBlendInfo(unsigned char b) { return b; } +template <> inline unsigned char rotateBlendInfo(unsigned char b) { return ((b << 2) | (b >> 6)) & 0xff; } +template <> inline unsigned char rotateBlendInfo(unsigned char b) { return ((b << 4) | (b >> 4)) & 0xff; } +template <> inline unsigned char rotateBlendInfo(unsigned char b) { return ((b << 6) | (b >> 2)) & 0xff; } + + +/* input kernel area naming convention: +------------- +| A | B | C | +|---|---|---| +| D | E | F | input pixel is at position E +|---|---|---| +| G | H | I | +------------- */ + +template +FORCE_INLINE //perf: quite worth it! +void blendPixel(const Kernel_3x3& ker, + uint32_t* target, int trgWidth, + unsigned char blendInfo, //result of preprocessing all four corners of pixel "E" + const xbrz::ScalerCfg& cfg) +{ + //#define a get_a(ker) +#define b get_b(ker) +#define c get_c(ker) +#define d get_d(ker) +#define e get_e(ker) +#define f get_f(ker) +#define g get_g(ker) +#define h get_h(ker) +#define i get_i(ker) + + + const unsigned char blend = rotateBlendInfo(blendInfo); + + if (getBottomR(blend) >= BLEND_NORMAL) + { + auto eq = [&](uint32_t pix1, uint32_t pix2) { return ColorDistance::dist(pix1, pix2, cfg.testAttribute) < cfg.equalColorTolerance; }; + auto dist = [&](uint32_t pix1, uint32_t pix2) { return ColorDistance::dist(pix1, pix2, cfg.testAttribute); }; + + const bool doLineBlend = [&]() -> bool + { + if (getBottomR(blend) >= BLEND_DOMINANT) + return true; + + //make sure there is no second blending in an adjacent rotation for this pixel: handles insular pixels, mario eyes + if (getTopR(blend) != BLEND_NONE && !eq(e, g)) //but support double-blending for 90° corners + return false; + if (getBottomL(blend) != BLEND_NONE && !eq(e, c)) + return false; + + //no full blending for L-shapes; blend corner only (handles "mario mushroom eyes") + if (!eq(e, i) && eq(g, h) && eq(h, i) && eq(i, f) && eq(f, c)) + return false; + + return true; + }(); + + const uint32_t px = dist(e, f) <= dist(e, h) ? f : h; //choose most similar color + + OutputMatrix out(target, trgWidth); + + if (doLineBlend) + { + const double fg = dist(f, g); //test sample: 70% of values max(fg, hc) / min(fg, hc) are between 1.1 and 3.7 with median being 1.9 + const double hc = dist(h, c); // + + const bool haveShallowLine = cfg.steepDirectionThreshold * fg <= hc && e != g && d != g; + const bool haveSteepLine = cfg.steepDirectionThreshold * hc <= fg && e != c && b != c; + + if (haveShallowLine) + { + if (haveSteepLine) + Scaler::blendLineSteepAndShallow(px, out); + else + Scaler::blendLineShallow(px, out); + } + else + { + if (haveSteepLine) + Scaler::blendLineSteep(px, out); + else + Scaler::blendLineDiagonal(px, out); + } + } + else + Scaler::blendCorner(px, out); + } + + //#undef a +#undef b +#undef c +#undef d +#undef e +#undef f +#undef g +#undef h +#undef i +} + + +class OobReaderTransparent +{ +public: + OobReaderTransparent(const uint32_t* src, int srcWidth, int srcHeight, int y) : + s_m1(0 <= y - 1 && y - 1 < srcHeight ? src + srcWidth * (y - 1) : nullptr), + s_0 (0 <= y && y < srcHeight ? src + srcWidth * y : nullptr), + s_p1(0 <= y + 1 && y + 1 < srcHeight ? src + srcWidth * (y + 1) : nullptr), + s_p2(0 <= y + 2 && y + 2 < srcHeight ? src + srcWidth * (y + 2) : nullptr), + srcWidth_(srcWidth) {} + + void readPonm(Kernel_4x4& ker, int x) const //(x, y) is at kernel position E + { + [[likely]] if (const int x_p2 = x + 2; 0 <= x_p2 && x_p2 < srcWidth_) + { + ker.p = s_m1 ? s_m1[x_p2] : 0; + ker.o = s_0 ? s_0 [x_p2] : 0; + ker.n = s_p1 ? s_p1[x_p2] : 0; + ker.m = s_p2 ? s_p2[x_p2] : 0; + } + else + { + ker.p = 0; + ker.o = 0; + ker.n = 0; + ker.m = 0; + } + } + +private: + const uint32_t* const s_m1; + const uint32_t* const s_0; + const uint32_t* const s_p1; + const uint32_t* const s_p2; + const int srcWidth_; +}; + + +class OobReaderDuplicate +{ +public: + OobReaderDuplicate(const uint32_t* src, int srcWidth, int srcHeight, int y) : + s_m1(src + srcWidth * std::clamp(y - 1, 0, srcHeight - 1)), + s_0 (src + srcWidth * std::clamp(y, 0, srcHeight - 1)), + s_p1(src + srcWidth * std::clamp(y + 1, 0, srcHeight - 1)), + s_p2(src + srcWidth * std::clamp(y + 2, 0, srcHeight - 1)), + srcWidth_(srcWidth) {} + + void readPonm(Kernel_4x4& ker, int x) const //(x, y) is at kernel position E + { + const int x_p2 = std::clamp(x + 2, 0, srcWidth_ - 1); + ker.p = s_m1[x_p2]; + ker.o = s_0 [x_p2]; + ker.n = s_p1[x_p2]; + ker.m = s_p2[x_p2]; + } + +private: + const uint32_t* const s_m1; + const uint32_t* const s_0; + const uint32_t* const s_p1; + const uint32_t* const s_p2; + const int srcWidth_; +}; + + +inline +void fillBlock(uint32_t* trg, int trgWidth, uint32_t col, int blockSize) +{ + for (int y = 0; y < blockSize; ++y, trg += trgWidth) + // std::fill(trg, trg + blockSize, col); + for (int x = 0; x < blockSize; ++x) + trg[x] = col; +} + + +template //scaler policy: see "Scaler2x" reference implementation +void scaleImage(const uint32_t* src, uint32_t* trg, int srcWidth, int srcHeight, const xbrz::ScalerCfg& cfg, int yFirst, int yLast) +{ + yFirst = std::max(yFirst, 0); + yLast = std::min(yLast, srcHeight); + if (yFirst >= yLast || srcWidth <= 0) + return; + + const int trgWidth = srcWidth * Scaler::scale; + + //(ab)use space of "sizeof(uint32_t) * srcWidth * Scaler::scale" at the end of the image as temporary + //buffer for "on the fly preprocessing" without risk of accidental overwriting before accessing + unsigned char* const preProcBuf = reinterpret_cast(trg + yLast * Scaler::scale * trgWidth) - srcWidth; + + //initialize preprocessing buffer for first row of current stripe: detect upper left and right corner blending + //this cannot be optimized for adjacent processing stripes; we must not allow for a memory race condition! + { + const OobReader oobReader(src, srcWidth, srcHeight, yFirst - 1); + + //initialize at position x = -1 + Kernel_4x4 ker4 = {}; + oobReader.readPonm(ker4, -4); //hack: read a, d, g, j at x = -1 + ker4.a = ker4.p; + ker4.d = ker4.o; + ker4.g = ker4.n; + ker4.j = ker4.m; + + oobReader.readPonm(ker4, -3); + ker4.b = ker4.p; + ker4.e = ker4.o; + ker4.h = ker4.n; + ker4.k = ker4.m; + + oobReader.readPonm(ker4, -2); + ker4.c = ker4.p; + ker4.f = ker4.o; + ker4.i = ker4.n; + ker4.l = ker4.m; + + oobReader.readPonm(ker4, -1); + + { + const BlendResult res = preProcessCorners(ker4, cfg); + clearAddTopL(preProcBuf[0], res.blend_i); //set 1st known corner for (0, yFirst) + } + + for (int x = 0; x < srcWidth; ++x) + { + ker4.a = ker4.b; //shift previous kernel to the left + ker4.d = ker4.e; // ----------------- + ker4.g = ker4.h; // | A | B | C | P | + ker4.j = ker4.k; // |---|---|---|---| + /**/ // | D | E | F | O | (x, yFirst - 1) is at position E + ker4.b = ker4.c; // |---|---|---|---| + ker4.e = ker4.f; // | G | H | I | N | + ker4.h = ker4.i; // |---|---|---|---| + ker4.k = ker4.l; // | J | K | L | M | + /**/ // ----------------- + ker4.c = ker4.p; + ker4.f = ker4.o; + ker4.i = ker4.n; + ker4.l = ker4.m; + + oobReader.readPonm(ker4, x); + + /* preprocessing blend result: + --------- + | E | F | evaluate corner between E, F, H, I + |---+---| current input pixel is at position E + | H | I | + --------- */ + const BlendResult res = preProcessCorners(ker4, cfg); + addTopR(preProcBuf[x], res.blend_h); //set 2nd known corner for (x, yFirst) + + if (x + 1 < srcWidth) + clearAddTopL(preProcBuf[x + 1], res.blend_i); //set 1st known corner for (x + 1, yFirst) + } + } + //------------------------------------------------------------------------------------ + + for (int y = yFirst; y < yLast; ++y) + { + uint32_t* out = trg + Scaler::scale * y * trgWidth; //consider MT "striped" access + + const OobReader oobReader(src, srcWidth, srcHeight, y); + + //initialize at position x = -1 + Kernel_4x4 ker4 = {}; + oobReader.readPonm(ker4, -4); //hack: read a, d, g, j at x = -1 + ker4.a = ker4.p; + ker4.d = ker4.o; + ker4.g = ker4.n; + ker4.j = ker4.m; + + oobReader.readPonm(ker4, -3); + ker4.b = ker4.p; + ker4.e = ker4.o; + ker4.h = ker4.n; + ker4.k = ker4.m; + + oobReader.readPonm(ker4, -2); + ker4.c = ker4.p; + ker4.f = ker4.o; + ker4.i = ker4.n; + ker4.l = ker4.m; + + oobReader.readPonm(ker4, -1); + + unsigned char blend_xy1 = 0; //corner blending for current (x, y + 1) position + { + const BlendResult res = preProcessCorners(ker4, cfg); + clearAddTopL(blend_xy1, res.blend_i); //set 1st known corner for (0, y + 1) and buffer for use on next column + + addBottomL(preProcBuf[0], res.blend_f); //set 3rd known corner for (0, y) + } + + for (int x = 0; x < srcWidth; ++x, out += Scaler::scale) + { + ker4.a = ker4.b; //shift previous kernel to the left + ker4.d = ker4.e; // ----------------- + ker4.g = ker4.h; // | A | B | C | P | + ker4.j = ker4.k; // |---|---|---|---| + /**/ // | D | E | F | O | (x, y) is at position E + ker4.b = ker4.c; // |---|---|---|---| + ker4.e = ker4.f; // | G | H | I | N | + ker4.h = ker4.i; // |---|---|---|---| + ker4.k = ker4.l; // | J | K | L | M | + /**/ // ----------------- + ker4.c = ker4.p; + ker4.f = ker4.o; + ker4.i = ker4.n; + ker4.l = ker4.m; + + oobReader.readPonm(ker4, x); + + //evaluate the four corners on bottom-right of current pixel + unsigned char blend_xy = preProcBuf[x]; //for current (x, y) position + { + /* preprocessing blend result: + --------- + | E | F | evaluate corner between E, F, H, I + |---+---| current input pixel is at position E + | H | I | + --------- */ + const BlendResult res = preProcessCorners(ker4, cfg); + addBottomR(blend_xy, res.blend_e); //all four corners of (x, y) have been determined at this point due to processing sequence! + + addTopR(blend_xy1, res.blend_h); //set 2nd known corner for (x, y + 1) + preProcBuf[x] = blend_xy1; //store on current buffer position for use on next row + + [[likely]] if (x + 1 < srcWidth) + { + //blend_xy1 -> blend_x1y1 + clearAddTopL(blend_xy1, res.blend_i); //set 1st known corner for (x + 1, y + 1) and buffer for use on next column + + addBottomL(preProcBuf[x + 1], res.blend_f); //set 3rd known corner for (x + 1, y) + } + } + + //fill block of size scale * scale with the given color + fillBlock(out, trgWidth, ker4.e, Scaler::scale); + + //place *after* preprocessing step, to not overwrite the results while processing the last pixel! + + //blend all four corners of current pixel + if (blendingNeeded(blend_xy)) + { + const Kernel_3x3& ker3 = ker4; //"The Things We Do for [Perf]" + blendPixel(ker3, out, trgWidth, blend_xy, cfg); + blendPixel(ker3, out, trgWidth, blend_xy, cfg); + blendPixel(ker3, out, trgWidth, blend_xy, cfg); + blendPixel(ker3, out, trgWidth, blend_xy, cfg); + } + } + } +} + +//------------------------------------------------------------------------------------ + +template +struct Scaler2x : public ColorGradient +{ + static const int scale = 2; + + template //bring template function into scope for GCC + static void alphaGrad(uint32_t& pixBack, uint32_t pixFront) { ColorGradient::template alphaGrad(pixBack, pixFront); } + + + template + static void blendLineShallow(uint32_t col, OutputMatrix& out) + { + alphaGrad<1, 4>(out.template ref(), col); + alphaGrad<3, 4>(out.template ref(), col); + } + + template + static void blendLineSteep(uint32_t col, OutputMatrix& out) + { + alphaGrad<1, 4>(out.template ref<0, scale - 1>(), col); + alphaGrad<3, 4>(out.template ref<1, scale - 1>(), col); + } + + template + static void blendLineSteepAndShallow(uint32_t col, OutputMatrix& out) + { + alphaGrad<1, 4>(out.template ref<1, 0>(), col); + alphaGrad<1, 4>(out.template ref<0, 1>(), col); + alphaGrad<5, 6>(out.template ref<1, 1>(), col); //[!] fixes 7/8 used in xBR + } + + template + static void blendLineDiagonal(uint32_t col, OutputMatrix& out) + { + alphaGrad<1, 2>(out.template ref<1, 1>(), col); + } + + template + static void blendCorner(uint32_t col, OutputMatrix& out) + { + //model a round corner + alphaGrad<21, 100>(out.template ref<1, 1>(), col); //exact: 1 - pi/4 = 0.2146018366 + } +}; + + +template +struct Scaler3x : public ColorGradient +{ + static const int scale = 3; + + template //bring template function into scope for GCC + static void alphaGrad(uint32_t& pixBack, uint32_t pixFront) { ColorGradient::template alphaGrad(pixBack, pixFront); } + + + template + static void blendLineShallow(uint32_t col, OutputMatrix& out) + { + alphaGrad<1, 4>(out.template ref(), col); + alphaGrad<1, 4>(out.template ref(), col); + + alphaGrad<3, 4>(out.template ref(), col); + out.template ref() = col; + } + + template + static void blendLineSteep(uint32_t col, OutputMatrix& out) + { + alphaGrad<1, 4>(out.template ref<0, scale - 1>(), col); + alphaGrad<1, 4>(out.template ref<2, scale - 2>(), col); + + alphaGrad<3, 4>(out.template ref<1, scale - 1>(), col); + out.template ref<2, scale - 1>() = col; + } + + template + static void blendLineSteepAndShallow(uint32_t col, OutputMatrix& out) + { + alphaGrad<1, 4>(out.template ref<2, 0>(), col); + alphaGrad<1, 4>(out.template ref<0, 2>(), col); + alphaGrad<3, 4>(out.template ref<2, 1>(), col); + alphaGrad<3, 4>(out.template ref<1, 2>(), col); + out.template ref<2, 2>() = col; + } + + template + static void blendLineDiagonal(uint32_t col, OutputMatrix& out) + { + alphaGrad<1, 8>(out.template ref<1, 2>(), col); //conflict with other rotations for this odd scale + alphaGrad<1, 8>(out.template ref<2, 1>(), col); + alphaGrad<7, 8>(out.template ref<2, 2>(), col); // + } + + template + static void blendCorner(uint32_t col, OutputMatrix& out) + { + //model a round corner + alphaGrad<45, 100>(out.template ref<2, 2>(), col); //exact: 0.4545939598 + //alphaGrad<3, 100>(out.template ref<2, 1>(), col); //0.02826017254 -> negligible + avoid overlap with other rotations at this scale + //alphaGrad<3, 100>(out.template ref<1, 2>(), col); //0.02826017254 + } +}; + + +template +struct Scaler4x : public ColorGradient +{ + static const int scale = 4; + + template //bring template function into scope for GCC + static void alphaGrad(uint32_t& pixBack, uint32_t pixFront) { ColorGradient::template alphaGrad(pixBack, pixFront); } + + + template + static void blendLineShallow(uint32_t col, OutputMatrix& out) + { + alphaGrad<1, 4>(out.template ref(), col); + alphaGrad<1, 4>(out.template ref(), col); + + alphaGrad<3, 4>(out.template ref(), col); + alphaGrad<3, 4>(out.template ref(), col); + + out.template ref() = col; + out.template ref() = col; + } + + template + static void blendLineSteep(uint32_t col, OutputMatrix& out) + { + alphaGrad<1, 4>(out.template ref<0, scale - 1>(), col); + alphaGrad<1, 4>(out.template ref<2, scale - 2>(), col); + + alphaGrad<3, 4>(out.template ref<1, scale - 1>(), col); + alphaGrad<3, 4>(out.template ref<3, scale - 2>(), col); + + out.template ref<2, scale - 1>() = col; + out.template ref<3, scale - 1>() = col; + } + + template + static void blendLineSteepAndShallow(uint32_t col, OutputMatrix& out) + { + alphaGrad<3, 4>(out.template ref<3, 1>(), col); + alphaGrad<3, 4>(out.template ref<1, 3>(), col); + alphaGrad<1, 4>(out.template ref<3, 0>(), col); + alphaGrad<1, 4>(out.template ref<0, 3>(), col); + + alphaGrad<1, 3>(out.template ref<2, 2>(), col); //[!] fixes 1/4 used in xBR + + out.template ref<3, 3>() = col; + out.template ref<3, 2>() = col; + out.template ref<2, 3>() = col; + } + + template + static void blendLineDiagonal(uint32_t col, OutputMatrix& out) + { + alphaGrad<1, 2>(out.template ref(), col); + alphaGrad<1, 2>(out.template ref(), col); + out.template ref() = col; + } + + template + static void blendCorner(uint32_t col, OutputMatrix& out) + { + //model a round corner + alphaGrad<68, 100>(out.template ref<3, 3>(), col); //exact: 0.6848532563 + alphaGrad< 9, 100>(out.template ref<3, 2>(), col); //0.08677704501 + alphaGrad< 9, 100>(out.template ref<2, 3>(), col); //0.08677704501 + } +}; + + +template +struct Scaler5x : public ColorGradient +{ + static const int scale = 5; + + template //bring template function into scope for GCC + static void alphaGrad(uint32_t& pixBack, uint32_t pixFront) { ColorGradient::template alphaGrad(pixBack, pixFront); } + + + template + static void blendLineShallow(uint32_t col, OutputMatrix& out) + { + alphaGrad<1, 4>(out.template ref(), col); + alphaGrad<1, 4>(out.template ref(), col); + alphaGrad<1, 4>(out.template ref(), col); + + alphaGrad<3, 4>(out.template ref(), col); + alphaGrad<3, 4>(out.template ref(), col); + + out.template ref() = col; + out.template ref() = col; + out.template ref() = col; + out.template ref() = col; + } + + template + static void blendLineSteep(uint32_t col, OutputMatrix& out) + { + alphaGrad<1, 4>(out.template ref<0, scale - 1>(), col); + alphaGrad<1, 4>(out.template ref<2, scale - 2>(), col); + alphaGrad<1, 4>(out.template ref<4, scale - 3>(), col); + + alphaGrad<3, 4>(out.template ref<1, scale - 1>(), col); + alphaGrad<3, 4>(out.template ref<3, scale - 2>(), col); + + out.template ref<2, scale - 1>() = col; + out.template ref<3, scale - 1>() = col; + out.template ref<4, scale - 1>() = col; + out.template ref<4, scale - 2>() = col; + } + + template + static void blendLineSteepAndShallow(uint32_t col, OutputMatrix& out) + { + alphaGrad<1, 4>(out.template ref<0, scale - 1>(), col); + alphaGrad<1, 4>(out.template ref<2, scale - 2>(), col); + alphaGrad<3, 4>(out.template ref<1, scale - 1>(), col); + + alphaGrad<1, 4>(out.template ref(), col); + alphaGrad<1, 4>(out.template ref(), col); + alphaGrad<3, 4>(out.template ref(), col); + + alphaGrad<2, 3>(out.template ref<3, 3>(), col); + + out.template ref<2, scale - 1>() = col; + out.template ref<3, scale - 1>() = col; + out.template ref<4, scale - 1>() = col; + + out.template ref() = col; + out.template ref() = col; + } + + template + static void blendLineDiagonal(uint32_t col, OutputMatrix& out) + { + alphaGrad<1, 8>(out.template ref(), col); //conflict with other rotations for this odd scale + alphaGrad<1, 8>(out.template ref(), col); + alphaGrad<1, 8>(out.template ref(), col); // + + alphaGrad<7, 8>(out.template ref<4, 3>(), col); + alphaGrad<7, 8>(out.template ref<3, 4>(), col); + + out.template ref<4, 4>() = col; + } + + template + static void blendCorner(uint32_t col, OutputMatrix& out) + { + //model a round corner + alphaGrad<86, 100>(out.template ref<4, 4>(), col); //exact: 0.8631434088 + alphaGrad<23, 100>(out.template ref<4, 3>(), col); //0.2306749731 + alphaGrad<23, 100>(out.template ref<3, 4>(), col); //0.2306749731 + //alphaGrad<2, 100>(out.template ref<4, 2>(), col); //0.01676812367 -> negligible + avoid overlap with other rotations at this scale + //alphaGrad<2, 100>(out.template ref<2, 4>(), col); //0.01676812367 + } +}; + + +template +struct Scaler6x : public ColorGradient +{ + static const int scale = 6; + + template //bring template function into scope for GCC + static void alphaGrad(uint32_t& pixBack, uint32_t pixFront) { ColorGradient::template alphaGrad(pixBack, pixFront); } + + + template + static void blendLineShallow(uint32_t col, OutputMatrix& out) + { + alphaGrad<1, 4>(out.template ref(), col); + alphaGrad<1, 4>(out.template ref(), col); + alphaGrad<1, 4>(out.template ref(), col); + + alphaGrad<3, 4>(out.template ref(), col); + alphaGrad<3, 4>(out.template ref(), col); + alphaGrad<3, 4>(out.template ref(), col); + + out.template ref() = col; + out.template ref() = col; + out.template ref() = col; + out.template ref() = col; + + out.template ref() = col; + out.template ref() = col; + } + + template + static void blendLineSteep(uint32_t col, OutputMatrix& out) + { + alphaGrad<1, 4>(out.template ref<0, scale - 1>(), col); + alphaGrad<1, 4>(out.template ref<2, scale - 2>(), col); + alphaGrad<1, 4>(out.template ref<4, scale - 3>(), col); + + alphaGrad<3, 4>(out.template ref<1, scale - 1>(), col); + alphaGrad<3, 4>(out.template ref<3, scale - 2>(), col); + alphaGrad<3, 4>(out.template ref<5, scale - 3>(), col); + + out.template ref<2, scale - 1>() = col; + out.template ref<3, scale - 1>() = col; + out.template ref<4, scale - 1>() = col; + out.template ref<5, scale - 1>() = col; + + out.template ref<4, scale - 2>() = col; + out.template ref<5, scale - 2>() = col; + } + + template + static void blendLineSteepAndShallow(uint32_t col, OutputMatrix& out) + { + alphaGrad<1, 4>(out.template ref<0, scale - 1>(), col); + alphaGrad<1, 4>(out.template ref<2, scale - 2>(), col); + alphaGrad<3, 4>(out.template ref<1, scale - 1>(), col); + alphaGrad<3, 4>(out.template ref<3, scale - 2>(), col); + + alphaGrad<1, 4>(out.template ref(), col); + alphaGrad<1, 4>(out.template ref(), col); + alphaGrad<3, 4>(out.template ref(), col); + alphaGrad<3, 4>(out.template ref(), col); + + out.template ref<2, scale - 1>() = col; + out.template ref<3, scale - 1>() = col; + out.template ref<4, scale - 1>() = col; + out.template ref<5, scale - 1>() = col; + + out.template ref<4, scale - 2>() = col; + out.template ref<5, scale - 2>() = col; + + out.template ref() = col; + out.template ref() = col; + } + + template + static void blendLineDiagonal(uint32_t col, OutputMatrix& out) + { + alphaGrad<1, 2>(out.template ref(), col); + alphaGrad<1, 2>(out.template ref(), col); + alphaGrad<1, 2>(out.template ref(), col); + + out.template ref() = col; + out.template ref() = col; + out.template ref() = col; + } + + template + static void blendCorner(uint32_t col, OutputMatrix& out) + { + //model a round corner + alphaGrad<97, 100>(out.template ref<5, 5>(), col); //exact: 0.9711013910 + alphaGrad<42, 100>(out.template ref<4, 5>(), col); //0.4236372243 + alphaGrad<42, 100>(out.template ref<5, 4>(), col); //0.4236372243 + alphaGrad< 6, 100>(out.template ref<5, 3>(), col); //0.05652034508 + alphaGrad< 6, 100>(out.template ref<3, 5>(), col); //0.05652034508 + } +}; + +//------------------------------------------------------------------------------------ + +struct ColorDistanceRGB +{ + static double dist(uint32_t pix1, uint32_t pix2, double testAttribute) + { + return distYCbCrBuffered(pix1, pix2, testAttribute); + + //if (pix1 == pix2) //about 4% perf boost + // return 0; + //return distYCbCr(pix1, pix2, luminanceWeight); + } +}; + +struct ColorDistanceARGB +{ + static double dist(uint32_t pix1, uint32_t pix2, double testAttribute) + { + const double a1 = getAlpha(pix1) / 255.0 ; + const double a2 = getAlpha(pix2) / 255.0 ; + + /* Requirements for a color distance handling alpha channel: with a1, a2 in [0, 1] + + 1. if a1 = a2, distance should be: a1 * distYCbCr() + 2. if a1 = 0, distance should be: a2 * distYCbCr(black, white) = a2 * 255 + 3. if a1 = 1, ??? maybe: 255 * (1 - a2) + a2 * distYCbCr() + + std::min(a1, a2) * distYCbCrBuffered(pix1, pix2) + 255 * abs(a1 - a2); + + alternative? std::sqrt(a1 * a2 * square(distYCbCrBuffered(pix1, pix2)) + square(255 * (a1 - a2))); */ + + //=> following code is 15% faster: + const double d = distYCbCrBuffered(pix1, pix2, testAttribute); + if (a1 < a2) + return a1 * d + 255 * (a2 - a1); + else + return a2 * d + 255 * (a1 - a2); + } +}; + + +struct ColorDistanceUnbufferedARGB +{ + static double dist(uint32_t pix1, uint32_t pix2, double testAttribute) + { + const double a1 = getAlpha(pix1) / 255.0 ; + const double a2 = getAlpha(pix2) / 255.0 ; + + const double d = distYCbCr(pix1, pix2, testAttribute); + if (a1 < a2) + return a1 * d + 255 * (a2 - a1); + else + return a2 * d + 255 * (a1 - a2); + } +}; + + +struct ColorGradientRGB +{ + template + static void alphaGrad(uint32_t& pixBack, uint32_t pixFront) + { + pixBack = gradientRGB(pixFront, pixBack); + } +}; + +struct ColorGradientARGB +{ + template + static void alphaGrad(uint32_t& pixBack, uint32_t pixFront) + { + pixBack = gradientARGB(pixFront, pixBack); + } +}; +} + + +void xbrz::scale(size_t factor, const uint32_t* src, uint32_t* trg, int srcWidth, int srcHeight, ColorFormat colFmt, const xbrz::ScalerCfg& cfg, int yFirst, int yLast) +{ + if (factor == 1) + { + std::copy(src + yFirst * srcWidth, src + yLast * srcWidth, trg); + return; + } + + static_assert(SCALE_FACTOR_MAX == 6); + switch (colFmt) + { + case ColorFormat::rgb: + switch (factor) + { + case 2: return scaleImage, ColorDistanceRGB, OobReaderDuplicate>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + case 3: return scaleImage, ColorDistanceRGB, OobReaderDuplicate>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + case 4: return scaleImage, ColorDistanceRGB, OobReaderDuplicate>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + case 5: return scaleImage, ColorDistanceRGB, OobReaderDuplicate>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + case 6: return scaleImage, ColorDistanceRGB, OobReaderDuplicate>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + } + break; + + case ColorFormat::argb: + switch (factor) + { + case 2: return scaleImage, ColorDistanceARGB, OobReaderTransparent>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + case 3: return scaleImage, ColorDistanceARGB, OobReaderTransparent>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + case 4: return scaleImage, ColorDistanceARGB, OobReaderTransparent>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + case 5: return scaleImage, ColorDistanceARGB, OobReaderTransparent>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + case 6: return scaleImage, ColorDistanceARGB, OobReaderTransparent>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + } + break; + + case ColorFormat::argbUnbuffered: + switch (factor) + { + case 2: return scaleImage, ColorDistanceUnbufferedARGB, OobReaderTransparent>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + case 3: return scaleImage, ColorDistanceUnbufferedARGB, OobReaderTransparent>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + case 4: return scaleImage, ColorDistanceUnbufferedARGB, OobReaderTransparent>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + case 5: return scaleImage, ColorDistanceUnbufferedARGB, OobReaderTransparent>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + case 6: return scaleImage, ColorDistanceUnbufferedARGB, OobReaderTransparent>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + } + break; + } + assert(false); +} + + +bool xbrz::equalColorTest2(uint32_t col1, uint32_t col2, ColorFormat colFmt, double equalColorTolerance, double testAttribute) +{ + switch (colFmt) + { + case ColorFormat::rgb: + return ColorDistanceRGB::dist(col1, col2, testAttribute) < equalColorTolerance; + case ColorFormat::argb: + return ColorDistanceARGB::dist(col1, col2, testAttribute) < equalColorTolerance; + case ColorFormat::argbUnbuffered: + return ColorDistanceUnbufferedARGB::dist(col1, col2, testAttribute) < equalColorTolerance; + } + assert(false); + return false; +} + + +void xbrz::bilinearScale(const uint32_t* src, int srcWidth, int srcHeight, + /**/ uint32_t* trg, int trgWidth, int trgHeight) +{ + const auto pixRead = [src, srcWidth](int x, int y) + { + const uint32_t pixSrc = src[y * srcWidth + x]; + + return [pixSrc, a = int(getAlpha(pixSrc))](int channel) + { + if (channel == 3) + return a; + + //Limitation: alpha should be applied in gamma-decoded linear RGB space: https://ssp.impulsetrain.com/gamma-premult.html + return getByte(pixSrc, channel) * a; + }; + }; + + const auto pixWrite = [trg](const auto& interpolate) mutable + { + const double a = interpolate(3); + if (a <= 0.0) + * trg++ = 0; + else + * trg++ = makePixel(byteRound(a), + byteRound(interpolate(2) / a), //r + byteRound(interpolate(1) / a), //g + byteRound(interpolate(0) / a)); //b + }; + + bilinearScale(pixRead, srcWidth, srcHeight, + pixWrite, trgWidth, trgHeight, 0, trgHeight); +} + + +void xbrz::nearestNeighborScale(const uint32_t* src, int srcWidth, int srcHeight, + /**/ uint32_t* trg, int trgWidth, int trgHeight) +{ + const auto pixRead = [src, srcWidth](int x, int y) { return src[y * srcWidth + x]; }; + + const auto pixWrite = [trg](uint32_t pix) mutable { *trg++ = pix; }; + + nearestNeighborScale(pixRead, srcWidth, srcHeight, + pixWrite, trgWidth, trgHeight, 0, trgHeight); +} + + +#if 0 +//#include +void bilinearScaleCpu(const uint32_t* src, int srcWidth, int srcHeight, + /**/ uint32_t* trg, int trgWidth, int trgHeight) +{ + const int TASK_GRANULARITY = 16; + + concurrency::task_group tg; + + for (int i = 0; i < trgHeight; i += TASK_GRANULARITY) + tg.run([=] + { + const int iLast = std::min(i + TASK_GRANULARITY, trgHeight); + bilinearScale(src, srcWidth, srcHeight, srcWidth * sizeof(uint32_t), + trg, trgWidth, trgHeight, trgWidth * sizeof(uint32_t), + i, iLast, [](uint32_t pix) { return pix; }); + }); + tg.wait(); +} + + +//Perf: AMP vs CPU: merely ~10% shorter runtime (scaling 1280x800 -> 1920x1080) +//#include +void bilinearScaleAmp(const uint32_t* src, int srcWidth, int srcHeight, //throw concurrency::runtime_exception + /**/ uint32_t* trg, int trgWidth, int trgHeight) +{ + //C++ AMP reference: https://docs.microsoft.com/en-us/cpp/parallel/amp/reference/reference-cpp-amp + //introduction to C++ AMP: https://docs.microsoft.com/en-us/archive/msdn-magazine/2012/april/c-a-code-based-introduction-to-c-amp + using namespace concurrency; + //TODO: pitch + + if (srcHeight <= 0 || srcWidth <= 0) return; + + const float scaleX = static_cast(trgWidth ) / srcWidth; + const float scaleY = static_cast(trgHeight) / srcHeight; + + array_view srcView(srcHeight, srcWidth, src); + array_view< uint32_t, 2> trgView(trgHeight, trgWidth, trg); + trgView.discard_data(); + + parallel_for_each(trgView.extent, [=](index<2> idx) restrict(amp) //throw ? + { + const int y = idx[0]; + const int x = idx[1]; + //Perf notes: + // -> float-based calculation is (almost) 2x as fas as double! + // -> no noticeable improvement via tiling: https://docs.microsoft.com/en-us/archive/msdn-magazine/2012/april/c-amp-introduction-to-tiling-in-c-amp + // -> no noticeable improvement with restrict(amp,cpu) + // -> iterating over y-axis only is significantly slower! + // -> pre-calculating x,y-dependent variables in a buffer + array_view<> is ~ 20 % slower! + const int y1 = srcHeight * y / trgHeight; + int y2 = y1 + 1; + if (y2 == srcHeight) --y2; + + const float yy1 = y / scaleY - y1; + const float y2y = 1 - yy1; + //------------------------------------- + const int x1 = srcWidth * x / trgWidth; + int x2 = x1 + 1; + if (x2 == srcWidth) --x2; + + const float xx1 = x / scaleX - x1; + const float x2x = 1 - xx1; + //------------------------------------- + const float x2xy2y = x2x * y2y; + const float xx1y2y = xx1 * y2y; + const float x2xyy1 = x2x * yy1; + const float xx1yy1 = xx1 * yy1; + + auto interpolate = [=](int offset) + { + /* + https://en.wikipedia.org/wiki/Bilinear_interpolation + (c11(x2 - x) + c21(x - x1)) * (y2 - y ) + + (c12(x2 - x) + c22(x - x1)) * (y - y1) + */ + const auto c11 = (srcView(y1, x1) >> (8 * offset)) & 0xff; + const auto c21 = (srcView(y1, x2) >> (8 * offset)) & 0xff; + const auto c12 = (srcView(y2, x1) >> (8 * offset)) & 0xff; + const auto c22 = (srcView(y2, x2) >> (8 * offset)) & 0xff; + + return c11 * x2xy2y + c21 * xx1y2y + + c12 * x2xyy1 + c22 * xx1yy1; + }; + + const float bi = interpolate(0); + const float gi = interpolate(1); + const float ri = interpolate(2); + const float ai = interpolate(3); + + const auto b = static_cast(bi + 0.5f); + const auto g = static_cast(gi + 0.5f); + const auto r = static_cast(ri + 0.5f); + const auto a = static_cast(ai + 0.5f); + + trgView(y, x) = (a << 24) | (r << 16) | (g << 8) | b; + }); + trgView.synchronize(); //throw ? +} +#endif diff --git a/xBRZ/src/xbrz.h b/xBRZ/src/xbrz.h new file mode 100644 index 0000000..f92b65c --- /dev/null +++ b/xBRZ/src/xbrz.h @@ -0,0 +1,78 @@ +// **************************************************************************** +// * This file is part of the xBRZ project. It is distributed under * +// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * +// * Copyright (C) Zenju (zenju AT gmx DOT de) - All Rights Reserved * +// * * +// * Additionally and as a special exception, the author gives permission * +// * to link the code of this program with the following libraries * +// * (or with modified versions that use the same licenses), and distribute * +// * linked combinations including the two: MAME, FreeFileSync, Snes9x, ePSXe * +// * * +// * You must obey the GNU General Public License in all respects for all of * +// * the code used other than MAME, FreeFileSync, Snes9x, ePSXe. * +// * If you modify this file, you may extend this exception to your version * +// * of the file, but you are not obligated to do so. If you do not wish to * +// * do so, delete this exception statement from your version. * +// **************************************************************************** + +#ifndef XBRZ_HEADER_3847894708239054 +#define XBRZ_HEADER_3847894708239054 + +#include //size_t +#include //uint32_t +#include +#include "xbrz_config.h" + + +namespace xbrz +{ +/* ------------------------------------------------------------------------- + | xBRZ: "Scale by rules" - high quality image upscaling filter by Zenju | + ------------------------------------------------------------------------- + using a modified approach of xBR: https://forums.libretro.com/t/xbr-algorithm-tutorial/123 + - new rule set preserving small image features + - highly optimized for performance + - support alpha channel + - support multithreading + - support 64-bit architectures + - support processing image slices + - support scaling up to 6xBRZ */ + +enum class ColorFormat //from high bits -> low bits, 8 bit per channel +{ + rgb, //8 bit for each red, green, blue, upper 8 bits unused + argb, //including alpha channel, BGRA byte order on little-endian machines + argbUnbuffered, //like ARGB, but without the one-time buffer creation overhead (ca. 100 - 300 ms) at the expense of a slightly slower scaling time +}; + +const int SCALE_FACTOR_MAX = 6; + +/* +-> map source (srcWidth * srcHeight) to target (scale * width x scale * height) image, optionally processing a half-open slice of rows [yFirst, yLast) only +-> if your emulator changes only a few image slices during each cycle (e.g. DOSBox) then there's no need to run xBRZ on the complete image: + Just make sure you enlarge the source image slice by 2 rows on top and 2 on bottom (this is the additional range the xBRZ algorithm is using during analysis) + CAVEAT: If there are multiple changed slices, make sure they do not overlap after adding these additional rows in order to avoid a memory race condition + in the target image data if you are using multiple threads for processing each enlarged slice! + +THREAD-SAFETY: - parts of the same image may be scaled by multiple threads as long as the [yFirst, yLast) ranges do not overlap! + - there is a minor inefficiency for the first row of a slice, so avoid processing single rows only; suggestion: process at least 8-16 rows +*/ +void scale(size_t factor, //valid range: 2 - SCALE_FACTOR_MAX + const uint32_t* src, uint32_t* trg, int srcWidth, int srcHeight, + ColorFormat colFmt, + const ScalerCfg& cfg = ScalerCfg(), + int yFirst = 0, int yLast = std::numeric_limits::max()); //slice of source image + +//BGRA byte order +void bilinearScale(const uint32_t* src, int srcWidth, int srcHeight, + /**/ uint32_t* trg, int trgWidth, int trgHeight); + +void nearestNeighborScale(const uint32_t* src, int srcWidth, int srcHeight, + /**/ uint32_t* trg, int trgWidth, int trgHeight); + + +//parameter tuning +bool equalColorTest2(uint32_t col1, uint32_t col2, ColorFormat colFmt, double equalColorTolerance, double testAttribute); +} + +#endif diff --git a/xBRZ/src/xbrz_config.h b/xBRZ/src/xbrz_config.h new file mode 100644 index 0000000..bd7deff --- /dev/null +++ b/xBRZ/src/xbrz_config.h @@ -0,0 +1,35 @@ +// **************************************************************************** +// * This file is part of the xBRZ project. It is distributed under * +// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * +// * Copyright (C) Zenju (zenju AT gmx DOT de) - All Rights Reserved * +// * * +// * Additionally and as a special exception, the author gives permission * +// * to link the code of this program with the following libraries * +// * (or with modified versions that use the same licenses), and distribute * +// * linked combinations including the two: MAME, FreeFileSync, Snes9x, ePSXe * +// * * +// * You must obey the GNU General Public License in all respects for all of * +// * the code used other than MAME, FreeFileSync, Snes9x, ePSXe. * +// * If you modify this file, you may extend this exception to your version * +// * of the file, but you are not obligated to do so. If you do not wish to * +// * do so, delete this exception statement from your version. * +// **************************************************************************** + +#ifndef XBRZ_CONFIG_HEADER_284578425345 +#define XBRZ_CONFIG_HEADER_284578425345 + +//do NOT include any headers here! used by xBRZ_dll!!! + +namespace xbrz +{ +struct ScalerCfg +{ + double equalColorTolerance = 30; + double centerDirectionBias = 4; + double dominantDirectionThreshold = 3.6; + double steepDirectionThreshold = 2.2; + double testAttribute = 0; //unused; test new parameters +}; +} + +#endif diff --git a/xBRZ/src/xbrz_tools.h b/xBRZ/src/xbrz_tools.h new file mode 100644 index 0000000..0ce9187 --- /dev/null +++ b/xBRZ/src/xbrz_tools.h @@ -0,0 +1,248 @@ +// **************************************************************************** +// * This file is part of the xBRZ project. It is distributed under * +// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * +// * Copyright (C) Zenju (zenju AT gmx DOT de) - All Rights Reserved * +// * * +// * Additionally and as a special exception, the author gives permission * +// * to link the code of this program with the following libraries * +// * (or with modified versions that use the same licenses), and distribute * +// * linked combinations including the two: MAME, FreeFileSync, Snes9x, ePSXe * +// * * +// * You must obey the GNU General Public License in all respects for all of * +// * the code used other than MAME, FreeFileSync, Snes9x, ePSXe. * +// * If you modify this file, you may extend this exception to your version * +// * of the file, but you are not obligated to do so. If you do not wish to * +// * do so, delete this exception statement from your version. * +// **************************************************************************** + +#ifndef XBRZ_TOOLS_H_825480175091875 +#define XBRZ_TOOLS_H_825480175091875 + +#include +#include +#include +#include + + +namespace xbrz +{ +template +inline unsigned char getByte(uint32_t val) { return static_cast((val >> (8 * N)) & 0xff); } +inline unsigned char getByte(uint32_t val, int n) { return static_cast((val >> (8 * n)) & 0xff); } + +inline unsigned char getAlpha(uint32_t pix) { return getByte<3>(pix); } +inline unsigned char getRed (uint32_t pix) { return getByte<2>(pix); } +inline unsigned char getGreen(uint32_t pix) { return getByte<1>(pix); } +inline unsigned char getBlue (uint32_t pix) { return getByte<0>(pix); } + +inline uint32_t makePixel(uint32_t a, uint32_t r, uint32_t g, uint32_t b) { return (a << 24) | (r << 16) | (g << 8) | b; } +inline uint32_t makePixel( uint32_t r, uint32_t g, uint32_t b) { return (r << 16) | (g << 8) | b; } + +inline uint32_t rgb555to888(uint16_t pix) { return ((pix & 0x7C00) << 9) | ((pix & 0x03E0) << 6) | ((pix & 0x001F) << 3); } +inline uint32_t rgb565to888(uint16_t pix) { return ((pix & 0xF800) << 8) | ((pix & 0x07E0) << 5) | ((pix & 0x001F) << 3); } + +inline uint16_t rgb888to555(uint32_t pix) { return static_cast(((pix & 0xF80000) >> 9) | ((pix & 0x00F800) >> 6) | ((pix & 0x0000F8) >> 3)); } +inline uint16_t rgb888to565(uint32_t pix) { return static_cast(((pix & 0xF80000) >> 8) | ((pix & 0x00FC00) >> 5) | ((pix & 0x0000F8) >> 3)); } + + +template inline +void unscaledCopy(PixReader pixRead /* (int x, int y) -> uint32_t */, + PixWriter pixWrite /* (uint32_t pix) */, int width, int height) +{ + for (int y = 0; y < height; ++y) + for (int x = 0; x < width; ++x) + pixWrite(pixRead(x, y)); +} + + +//nearest-neighbor (going over target image - slow for upscaling, since source is read multiple times missing out on cache! Fast for similar image sizes!) +template +void nearestNeighborScale(PixReader pixRead /* (int x, int y) -> uint32_t */, int srcWidth, int srcHeight, + PixWriter pixWrite /* (uint32_t pix) */, int trgWidth, int trgHeight, + int yFirst, int yLast) +{ + yFirst = std::max(yFirst, 0); + yLast = std::min(yLast, trgHeight); + if (yFirst >= yLast || srcHeight <= 0 || srcWidth <= 0) return; + + for (int y = yFirst; y < yLast; ++y) + { + const int ySrc = srcHeight * y / trgHeight; + + for (int x = 0; x < trgWidth; ++x) + { + const int xSrc = srcWidth * x / trgWidth; + pixWrite(pixRead(xSrc, ySrc)); + } + } +} + + +inline +unsigned char byteRound(double v) //std::round is prohibitively expensive! +{ + //assert(v >= 0); + return static_cast(std::min(v + 0.5, 255.0)); +} + + +#if 0 +inline +unsigned char byteCeil(double v) +{ + //assert(v >= 0); + if (v >= 255.0) return 255; + unsigned char i = static_cast(v); + return v == i ? i : i + 1; +} +#endif + + +inline +unsigned int uintDivRound(unsigned int num, unsigned int den) +{ + assert(den != 0); + return (num + den / 2) / den; +} + + +//caveat: treats alpha channel like regular color! => caller needs to pre/de-multiply alpha! +template +void bilinearScale(PixReader pixRead /* (int x, int y) -> Function */, int srcWidth, int srcHeight, + PixWriter pixWrite /* ( const Function& interpolate ) */, int trgWidth, int trgHeight, + int yFirst, int yLast) +{ + yFirst = std::max(yFirst, 0); + yLast = std::min(yLast, trgHeight); + if (yFirst >= yLast || srcHeight <= 0 || srcWidth <= 0) + return; + + const double scaleX = static_cast(trgWidth ) / srcWidth; + const double scaleY = static_cast(trgHeight) / srcHeight; + + //perf notes: + // -> double-based calculation is (slightly) faster than float + // -> pre-calculation gives significant boost; std::vector<> memory allocation is negligible! + struct CoeffsX + { + int x1 = 0; + int x2 = 0; + double xx1 = 0; + double x2x = 0; + }; + std::vector buf(trgWidth); + for (int x = 0; x < trgWidth; ++x) + { + const int x1 = srcWidth * x / trgWidth; + int x2 = x1 + 1; + if (x2 == srcWidth) + --x2; + + const double xx1 = x / scaleX - x1; + const double x2x = 1 - xx1; + + buf[x] = {x1, x2, xx1, x2x}; + } + + for (int y = yFirst; y < yLast; ++y) + { + const int y1 = srcHeight * y / trgHeight; + int y2 = y1 + 1; + if (y2 == srcHeight) + --y2; + + const double yy1 = y / scaleY - y1; + const double y2y = 1 - yy1; + + for (int x = 0; x < trgWidth; ++x) + { + //perf: do NOT "simplify" the variable layout without measurement! + const CoeffsX& bufX = buf[x]; + const int x1 = bufX.x1; + const int x2 = bufX.x2; + const double xx1 = bufX.xx1; + const double x2x = bufX.x2x; + + const double x2xy2y = x2x * y2y; + const double xx1y2y = xx1 * y2y; + const double x2xyy1 = x2x * yy1; + const double xx1yy1 = xx1 * yy1; + + auto pix11 = pixRead(x1, y1); + auto pix21 = pixRead(x2, y1); + auto pix12 = pixRead(x1, y2); + auto pix22 = pixRead(x2, y2); + + auto interpolate = [&](int channel) + { + /* https://en.wikipedia.org/wiki/Bilinear_interpolation + (c11(x2 - x) + c21(x - x1)) * (y2 - y ) + + (c12(x2 - x) + c22(x - x1)) * (y - y1) */ + return pix11(channel) * x2xy2y + pix21(channel) * xx1y2y + + pix12(channel) * x2xyy1 + pix22(channel) * xx1yy1; + }; + pixWrite(std::move(interpolate)); + } + } +} + + +#if 0 +//nearest-neighbor (going over source image - fast for upscaling, since source is read only once +template +void nearestNeighborScaleOverSource(const PixSrc* src, int srcWidth, int srcHeight, int srcPitch /*[bytes]*/, + /**/ PixTrg* trg, int trgWidth, int trgHeight, int trgPitch /*[bytes]*/, + int yFirst, int yLast, PixConverter pixCvrt /*convert PixSrc to PixTrg*/) +{ + static_assert(std::is_integral_v, "PixSrc* is expected to be cast-able to char*"); + static_assert(std::is_integral_v, "PixTrg* is expected to be cast-able to char*"); + + static_assert(std::is_same_v, "PixConverter returning wrong pixel format"); + + if (srcPitch < srcWidth * static_cast(sizeof(PixSrc)) || + trgPitch < trgWidth * static_cast(sizeof(PixTrg))) + { + assert(false); + return; + } + + yFirst = std::max(yFirst, 0); + yLast = std::min(yLast, srcHeight); + if (yFirst >= yLast || trgWidth <= 0 || trgHeight <= 0) return; + + for (int y = yFirst; y < yLast; ++y) + { + //mathematically: ySrc = floor(srcHeight * yTrg / trgHeight) + // => search for integers in: [ySrc, ySrc + 1) * trgHeight / srcHeight + + //keep within for loop to support MT input slices! + const int yTrgFirst = ( y * trgHeight + srcHeight - 1) / srcHeight; //=ceil(y * trgHeight / srcHeight) + const int yTrgLast = ((y + 1) * trgHeight + srcHeight - 1) / srcHeight; //=ceil(((y + 1) * trgHeight) / srcHeight) + const int blockHeight = yTrgLast - yTrgFirst; + + if (blockHeight > 0) + { + const PixSrc* srcLine = byteAdvance(src, y * srcPitch); + /**/ PixTrg* trgLine = byteAdvance(trg, yTrgFirst * trgPitch); + int xTrgFirst = 0; + + for (int x = 0; x < srcWidth; ++x) + { + const int xTrgLast = ((x + 1) * trgWidth + srcWidth - 1) / srcWidth; + const int blockWidth = xTrgLast - xTrgFirst; + if (blockWidth > 0) + { + xTrgFirst = xTrgLast; + + const auto trgPix = pixCvrt(srcLine[x]); + fillBlock(trgLine, trgPitch, trgPix, blockWidth, blockHeight); + trgLine += blockWidth; + } + } + } + } +} +#endif +} + +#endif //XBRZ_TOOLS_H_825480175091875 diff --git a/zen/argon2.cpp b/zen/argon2.cpp new file mode 100644 index 0000000..333128c --- /dev/null +++ b/zen/argon2.cpp @@ -0,0 +1,977 @@ +// ***************************************************************************** +// * 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 * +// ***************************************************************************** + +/* The code in this file, except for zen::zargon2(), is from PuTTY: + + PuTTY is copyright 1997-2022 Simon Tatham. + + Portions copyright Robert de Bath, Joris van Rantwijk, Delian + Delchev, Andreas Schultz, Jeroen Massar, Wez Furlong, Nicolas Barry, + Justin Bradford, Ben Harris, Malcolm Smith, Ahmad Khalifa, Markus + Kuhn, Colin Watson, Christopher Staite, Lorenz Diener, Christian + Brabandt, Jeff Smith, Pavel Kryukov, Maxim Kuznetsov, Svyatoslav + Kuzmich, Nico Williams, Viktor Dukhovni, Josh Dersch, Lars Brinkhoff, + and CORE SDI S.A. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE + FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + +#include "argon2.h" +#include +#include + +#if defined __GNUC__ //including clang + #pragma GCC diagnostic ignored "-Wimplicit-fallthrough" //"this statement may fall through" + #pragma GCC diagnostic ignored "-Wcast-align" //"cast from 'char *' to 'blake2b *' increases required alignment from 1 to 8" +#endif + +/* + * Implementation of the Argon2 password hash function. + * + * My sources for the algorithm description and test vectors (the latter in + * test/cryptsuite.py) were the reference implementation on Github, and also + * the Internet-Draft description: + * + * https://github.com/P-H-C/phc-winner-argon2 + * https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-argon2-13 + */ + + +/* ---------------------------------------------------------------------- + * + * A sort of 'abstract base class' or 'interface' or 'trait' which is + * the common feature of all types that want to accept data formatted + * using the SSH binary conventions of uint32, string, mpint etc. + */ +typedef struct BinarySink BinarySink; + +struct BinarySink +{ + void (*write)(BinarySink* sink, const void* data, size_t len); + void (*writefmtv)(BinarySink* sink, const char* fmt, va_list ap); + BinarySink* binarysink_; +}; + +#define BinarySink_INIT(obj, writefn) \ + ((obj)->binarysink_->write = (writefn), \ + (obj)->binarysink_->writefmtv = NULL, \ + (obj)->binarysink_->binarysink_ = (obj)->binarysink_) + +#define BinarySink_DELEGATE_IMPLEMENTATION BinarySink *binarysink_ + +#define BinarySink_DELEGATE_INIT(obj, othersink) ((obj)->binarysink_ = BinarySink_UPCAST(othersink)) + +#define BinarySink_DOWNCAST(object, type) \ + TYPECHECK((object) == ((type *)0)->binarysink_, \ + ((type *)(((char *)(object)) - offsetof(type, binarysink_)))) + +#define BinarySink_IMPLEMENTATION BinarySink binarysink_[1] + +/* Return a pointer to the object of structure type 'type' whose field + * with name 'field' is pointed at by 'object'. */ +#define container_of(object, type, field) \ + TYPECHECK(object == &((type *)0)->field, \ + ((type *)(((char *)(object)) - offsetof(type, field)))) + + +static void no_op(void* /*ptr*/, size_t /*size*/) {} + +static void (*const volatile maybe_read)(void* ptr, size_t size) = no_op; + +void smemclr(void* b, size_t n) +{ + if (b && n > 0) + { + /* + * Zero out the memory. + */ + memset(b, 0, n); + + /* + * Call the above function pointer, which (for all the + * compiler knows) might check that we've really zeroed the + * memory. + */ + maybe_read(b, n); + } +} + +void* safemalloc(size_t factor1, size_t factor2, size_t addend) +{ + if (factor1 > SIZE_MAX / factor2) + return nullptr; + size_t product = factor1 * factor2; + + if (addend > SIZE_MAX) + return nullptr; + if (product > SIZE_MAX - addend) + return nullptr; + size_t size = product + addend; + + if (size == 0) + size = 1; + + return malloc(size); +} + +void safefree(void* ptr) +{ + if (ptr) + free(ptr); +} + + +#define snmalloc safemalloc +#define smalloc(z) safemalloc(z,1,0) + +#define snewn(n, type) ((type *)snmalloc((n), sizeof(type), 0)) +#define snew(type) ((type *) smalloc (sizeof (type)) ) + +#define sfree safefree + + +/* + * A small structure wrapping up a (pointer, length) pair so that it + * can be conveniently passed to or from a function. + */ +typedef struct ptrlen +{ + const void* ptr; + size_t len; +} ptrlen; + + +struct ssh_hash +{ + //const ssh_hashalg* vt; + BinarySink_DELEGATE_IMPLEMENTATION; +}; + + +static inline void PUT_32BIT_LSB_FIRST(void* vp, uint32_t value) +{ + uint8_t* p = (uint8_t*)vp; + p[0] = (uint8_t)((value ) & 0xff); + p[1] = (uint8_t)((value >> 8) & 0xff); + p[2] = (uint8_t)((value >> 16) & 0xff); + p[3] = (uint8_t)((value >> 24) & 0xff); +} + + +static inline uint64_t GET_64BIT_LSB_FIRST(const void* vp) +{ + const uint8_t* p = (const uint8_t*)vp; + return (((uint64_t)p[0] ) | ((uint64_t)p[1] << 8) | + ((uint64_t)p[2] << 16) | ((uint64_t)p[3] << 24) | + ((uint64_t)p[4] << 32) | ((uint64_t)p[5] << 40) | + ((uint64_t)p[6] << 48) | ((uint64_t)p[7] << 56)); +} + + +static inline void PUT_64BIT_LSB_FIRST(void* vp, uint64_t value) +{ + uint8_t* p = (uint8_t*)vp; + p[0] = (uint8_t)((value ) & 0xff); + p[1] = (uint8_t)((value >> 8) & 0xff); + p[2] = (uint8_t)((value >> 16) & 0xff); + p[3] = (uint8_t)((value >> 24) & 0xff); + p[4] = (uint8_t)((value >> 32) & 0xff); + p[5] = (uint8_t)((value >> 40) & 0xff); + p[6] = (uint8_t)((value >> 48) & 0xff); + p[7] = (uint8_t)((value >> 56) & 0xff); +} + + +static void BinarySink_put_uint32_le(BinarySink* bs, unsigned long val) +{ + unsigned char data[4]; + PUT_32BIT_LSB_FIRST(data, val); + bs->write(bs, data, sizeof(data)); +} + +static void BinarySink_put_stringpl_le(BinarySink* bs, ptrlen pl) +{ + /* Check that the string length fits in a uint32, without doing a + * potentially implementation-defined shift of more than 31 bits */ + assert((pl.len >> 31) < 2); + + BinarySink_put_uint32_le(bs, pl.len); + bs->write(bs, pl.ptr, pl.len); +} + + +#define TYPECHECK(to_check, to_return) \ + (sizeof(to_check) ? (to_return) : (to_return)) + + +#define BinarySink_UPCAST(object) \ + TYPECHECK((object)->binarysink_ == (BinarySink *)0, \ + (object)->binarysink_) + +#define put_uint32_le(bs, val) \ + BinarySink_put_uint32_le(BinarySink_UPCAST(bs), val) +#define put_stringpl_le(bs, val) \ + BinarySink_put_stringpl_le(BinarySink_UPCAST(bs), val) + + +static inline uint32_t GET_32BIT_LSB_FIRST(const void* vp) +{ + const uint8_t* p = (const uint8_t*)vp; + return (((uint32_t)p[0] ) | ((uint32_t)p[1] << 8) | + ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24)); +} + + +void memxor(uint8_t* out, const uint8_t* in1, const uint8_t* in2, size_t size) +{ + switch (size & 15) + { + case 0: + while (size >= 16) + { + size -= 16; + *out++ = *in1++ ^ *in2++; + case 15: + *out++ = *in1++ ^ *in2++; + case 14: + *out++ = *in1++ ^ *in2++; + case 13: + *out++ = *in1++ ^ *in2++; + case 12: + *out++ = *in1++ ^ *in2++; + case 11: + *out++ = *in1++ ^ *in2++; + case 10: + *out++ = *in1++ ^ *in2++; + case 9: + *out++ = *in1++ ^ *in2++; + case 8: + *out++ = *in1++ ^ *in2++; + case 7: + *out++ = *in1++ ^ *in2++; + case 6: + *out++ = *in1++ ^ *in2++; + case 5: + *out++ = *in1++ ^ *in2++; + case 4: + *out++ = *in1++ ^ *in2++; + case 3: + *out++ = *in1++ ^ *in2++; + case 2: + *out++ = *in1++ ^ *in2++; + case 1: + *out++ = *in1++ ^ *in2++; + } + } +} + + +/* RFC 7963 section 2.1 */ +enum { R1 = 32, R2 = 24, R3 = 16, R4 = 63 }; + +/* RFC 7693 section 2.6 */ +static const uint64_t iv[] = +{ + 0x6a09e667f3bcc908, /* floor(2^64 * frac(sqrt(2))) */ + 0xbb67ae8584caa73b, /* floor(2^64 * frac(sqrt(3))) */ + 0x3c6ef372fe94f82b, /* floor(2^64 * frac(sqrt(5))) */ + 0xa54ff53a5f1d36f1, /* floor(2^64 * frac(sqrt(7))) */ + 0x510e527fade682d1, /* floor(2^64 * frac(sqrt(11))) */ + 0x9b05688c2b3e6c1f, /* floor(2^64 * frac(sqrt(13))) */ + 0x1f83d9abfb41bd6b, /* floor(2^64 * frac(sqrt(17))) */ + 0x5be0cd19137e2179, /* floor(2^64 * frac(sqrt(19))) */ +}; + +/* RFC 7693 section 2.7 */ +static const unsigned char sigma[][16] = +{ + { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, + {14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3}, + {11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4}, + { 7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8}, + { 9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13}, + { 2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9}, + {12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11}, + {13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10}, + { 6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5}, + {10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0}, + /* This array recycles if you have more than 10 rounds. BLAKE2b + * has 12, so we repeat the first two rows again. */ + { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, + {14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3}, +}; + +static inline uint64_t ror(uint64_t x, unsigned rotation) +{ + unsigned lshift = 63 & -rotation, rshift = 63 & rotation; + return (x << lshift) | (x >> rshift); +} + +static inline void g_half(uint64_t v[16], unsigned a, unsigned b, unsigned c, + unsigned d, uint64_t x, unsigned r1, unsigned r2) +{ + v[a] += v[b] + x; + v[d] ^= v[a]; + v[d] = ror(v[d], r1); + v[c] += v[d]; + v[b] ^= v[c]; + v[b] = ror(v[b], r2); +} + +static inline void g(uint64_t v[16], unsigned a, unsigned b, unsigned c, + unsigned d, uint64_t x, uint64_t y) +{ + g_half(v, a, b, c, d, x, R1, R2); + g_half(v, a, b, c, d, y, R3, R4); +} + +static inline void f(uint64_t h[8], uint64_t m[16], uint64_t offset_hi, + uint64_t offset_lo, unsigned final) +{ + uint64_t v[16]; + memcpy(v, h, 8 * sizeof(*v)); + memcpy(v + 8, iv, 8 * sizeof(*v)); + v[12] ^= offset_lo; + v[13] ^= offset_hi; + v[14] ^= -(uint64_t)final; + for (unsigned round = 0; round < 12; round++) + { + const unsigned char* s = sigma[round]; + g(v, 0, 4, 8, 12, m[s[ 0]], m[s[ 1]]); + g(v, 1, 5, 9, 13, m[s[ 2]], m[s[ 3]]); + g(v, 2, 6, 10, 14, m[s[ 4]], m[s[ 5]]); + g(v, 3, 7, 11, 15, m[s[ 6]], m[s[ 7]]); + g(v, 0, 5, 10, 15, m[s[ 8]], m[s[ 9]]); + g(v, 1, 6, 11, 12, m[s[10]], m[s[11]]); + g(v, 2, 7, 8, 13, m[s[12]], m[s[13]]); + g(v, 3, 4, 9, 14, m[s[14]], m[s[15]]); + } + for (unsigned i = 0; i < 8; i++) + h[i] ^= v[i] ^ v[i+8]; + smemclr(v, sizeof(v)); +} + + +static inline void f_outer(uint64_t h[8], uint8_t blk[128], uint64_t offset_hi, + uint64_t offset_lo, unsigned final) +{ + uint64_t m[16]; + for (unsigned i = 0; i < 16; i++) + m[i] = GET_64BIT_LSB_FIRST(blk + 8*i); + f(h, m, offset_hi, offset_lo, final); + smemclr(m, sizeof(m)); +} + + +typedef struct blake2b +{ + uint64_t h[8]; + unsigned hashlen; + + uint8_t block[128]; + size_t used; + uint64_t lenhi, lenlo; + + BinarySink_IMPLEMENTATION; + ssh_hash hash; +} blake2b; + + +static void blake2b_reset(ssh_hash* hash) +{ + blake2b* s = container_of(hash, blake2b, hash); + + /* Initialise the hash to the standard IV */ + memcpy(s->h, iv, sizeof(s->h)); + + /* XOR in the parameters: secret key length (here always 0) in + * byte 1, and hash length in byte 0. */ + s->h[0] ^= 0x01010000 ^ s->hashlen; + + s->used = 0; + s->lenhi = s->lenlo = 0; +} + + +static void blake2b_digest(ssh_hash* hash, uint8_t* digest) +{ + blake2b* s = container_of(hash, blake2b, hash); + + memset(s->block + s->used, 0, sizeof(s->block) - s->used); + f_outer(s->h, s->block, s->lenhi, s->lenlo, 1); + + uint8_t hash_pre[128]; + for (unsigned i = 0; i < 8; i++) + PUT_64BIT_LSB_FIRST(hash_pre + 8*i, s->h[i]); + memcpy(digest, hash_pre, s->hashlen); + smemclr(hash_pre, sizeof(hash_pre)); +} + + +static void blake2b_free(ssh_hash* hash) +{ + blake2b* s = container_of(hash, blake2b, hash); + + smemclr(s, sizeof(*s)); + sfree(s); +} + + +static void blake2b_write(BinarySink* bs, const void* vp, size_t len) +{ + blake2b* s = BinarySink_DOWNCAST(bs, blake2b); + const uint8_t* p = (const uint8_t*)vp; + + while (len > 0) + { + if (s->used == sizeof(s->block)) + { + f_outer(s->h, s->block, s->lenhi, s->lenlo, 0); + s->used = 0; + } + + size_t chunk = sizeof(s->block) - s->used; + if (chunk > len) + chunk = len; + + memcpy(s->block + s->used, p, chunk); + s->used += chunk; + p += chunk; + len -= chunk; + + s->lenlo += chunk; + s->lenhi += (s->lenlo < chunk); + } +} + + +static inline ssh_hash* ssh_hash_reset(ssh_hash* h) +{ + blake2b_reset(h); + return h; +} + + +static ssh_hash* blake2b_new_inner(unsigned hashlen) +{ + assert(hashlen <= 64); + + blake2b* s = snew(struct blake2b); + //s->hash.vt = &ssh_blake2b; + s->hashlen = hashlen; + BinarySink_INIT(s, blake2b_write); + BinarySink_DELEGATE_INIT(&s->hash, s); + return &s->hash; +} + + +ssh_hash* blake2b_new_general(unsigned hashlen) +{ + ssh_hash* h = blake2b_new_inner(hashlen); + ssh_hash_reset(h); + return h; +} + +/* ---------------------------------------------------------------------- + * Argon2 defines a hash-function family that's an extension of BLAKE2b to + * generate longer output digests, by repeatedly outputting half of a BLAKE2 + * hash output and then re-hashing the whole thing until there are 64 or fewer + * bytes left to output. The spec calls this H' (a variant of the original + * hash it calls H, which is the unmodified BLAKE2b). + */ + +static ssh_hash* hprime_new(unsigned length) +{ + ssh_hash* h = blake2b_new_general(length > 64 ? 64 : length); + put_uint32_le(h, length); + return h; +} + +void BinarySink_put_data(BinarySink* bs, const void* data, size_t len) +{ + bs->write(bs, data, len); +} + +#define put_data(bs, val, len) BinarySink_put_data(BinarySink_UPCAST(bs), val, len) + +static inline void ssh_hash_final(ssh_hash* h, unsigned char* out) +{ + blake2b_digest(h, out); + blake2b_free(h); +} + +static void hprime_final(ssh_hash* h, unsigned length, void* vout) +{ + uint8_t* out = (uint8_t*)vout; + + while (length > 64) + { + uint8_t hashbuf[64]; + ssh_hash_final(h, hashbuf); + + memcpy(out, hashbuf, 32); + out += 32; + length -= 32; + + h = blake2b_new_general(length > 64 ? 64 : length); + put_data(h, hashbuf, 64); + + smemclr(hashbuf, sizeof(hashbuf)); + } + + ssh_hash_final(h, out); +} + +/* ---------------------------------------------------------------------- + * Argon2's own mixing function G, which operates on 1Kb blocks of data. + * + * The definition of G in the spec takes two 1Kb blocks as input and produces + * a 1Kb output block. The first thing that happens to the input blocks is + * that they get XORed together, and then only the XOR output is used, so you + * could perfectly well regard G as a 1Kb->1Kb function. + */ + +static inline uint64_t trunc32(uint64_t x) +{ + return x & 0xFFFFFFFF; +} + +/* Internal function similar to the BLAKE2b round, which mixes up four 64-bit + * words */ +static inline void GB(uint64_t* a, uint64_t* b, uint64_t* c, uint64_t* d) +{ + *a += *b + 2 * trunc32(*a) * trunc32(*b); + *d = ror(*d ^ *a, 32); + *c += *d + 2 * trunc32(*c) * trunc32(*d); + *b = ror(*b ^ *c, 24); + *a += *b + 2 * trunc32(*a) * trunc32(*b); + *d = ror(*d ^ *a, 16); + *c += *d + 2 * trunc32(*c) * trunc32(*d); + *b = ror(*b ^ *c, 63); +} + +/* Higher-level internal function which mixes up sixteen 64-bit words. This is + * applied to different subsets of the 128 words in a kilobyte block, and the + * API here is designed to make it easy to apply in the circumstances the spec + * requires. In every call, the sixteen words form eight pairs adjacent in + * memory, whose addresses are in arithmetic progression. So the 16 input + * words are in[0], in[1], in[instep], in[instep+1], ..., in[7*instep], + * in[7*instep+1], and the 16 output words similarly. */ +static inline void P(uint64_t* out, unsigned outstep, + uint64_t* in, unsigned instep) +{ + for (unsigned i = 0; i < 8; i++) + { + out[i*outstep] = in[i*instep]; + out[i*outstep+1] = in[i*instep+1]; + } + + GB(out+0*outstep+0, out+2*outstep+0, out+4*outstep+0, out+6*outstep+0); + GB(out+0*outstep+1, out+2*outstep+1, out+4*outstep+1, out+6*outstep+1); + GB(out+1*outstep+0, out+3*outstep+0, out+5*outstep+0, out+7*outstep+0); + GB(out+1*outstep+1, out+3*outstep+1, out+5*outstep+1, out+7*outstep+1); + + GB(out+0*outstep+0, out+2*outstep+1, out+5*outstep+0, out+7*outstep+1); + GB(out+0*outstep+1, out+3*outstep+0, out+5*outstep+1, out+6*outstep+0); + GB(out+1*outstep+0, out+3*outstep+1, out+4*outstep+0, out+6*outstep+1); + GB(out+1*outstep+1, out+2*outstep+0, out+4*outstep+1, out+7*outstep+0); +} + +/* The full G function, taking input blocks X and Y. The result of G is most + * often XORed into an existing output block, so this API is designed with + * that in mind: the mixing function's output is always XORed into whatever + * 1Kb of data is already at 'out'. */ +static void G_xor(uint8_t* out, const uint8_t* X, const uint8_t* Y) +{ + uint64_t R[128], Q[128], Z[128]; + + for (unsigned i = 0; i < 128; i++) + R[i] = GET_64BIT_LSB_FIRST(X + 8*i) ^ GET_64BIT_LSB_FIRST(Y + 8*i); + + for (unsigned i = 0; i < 8; i++) + P(Q+16*i, 2, R+16*i, 2); + + for (unsigned i = 0; i < 8; i++) + P(Z+2*i, 16, Q+2*i, 16); + + for (unsigned i = 0; i < 128; i++) + PUT_64BIT_LSB_FIRST(out + 8*i, + GET_64BIT_LSB_FIRST(out + 8*i) ^ R[i] ^ Z[i]); + + smemclr(R, sizeof(R)); + smemclr(Q, sizeof(Q)); + smemclr(Z, sizeof(Z)); +} + +/* ---------------------------------------------------------------------- + * The main Argon2 function. + */ + +static void argon2_internal(uint32_t p, uint32_t T, uint32_t m, uint32_t t, + uint32_t y, ptrlen P, ptrlen S, ptrlen K, ptrlen X, + uint8_t* out) +{ + /* + * Start by hashing all the input data together: the four string arguments + * (password P, salt S, optional secret key K, optional associated data + * X), plus all the parameters for the function's memory and time usage. + * + * The output of this hash is the sole input to the subsequent mixing + * step: Argon2 does not preserve any more entropy from the inputs, it + * just makes it extra painful to get the final answer. + */ + uint8_t h0[64]; + { + ssh_hash* h = blake2b_new_general(64); + put_uint32_le(h, p); + put_uint32_le(h, T); + put_uint32_le(h, m); + put_uint32_le(h, t); + put_uint32_le(h, 0x13); /* hash function version number */ + put_uint32_le(h, y); + put_stringpl_le(h, P); + put_stringpl_le(h, S); + put_stringpl_le(h, K); + put_stringpl_le(h, X); + ssh_hash_final(h, h0); + } + + struct blk { uint8_t data[1024]; }; + + /* + * Array of 1Kb blocks. The total size is (approximately) m, the + * caller-specified parameter for how much memory to use; the blocks are + * regarded as a rectangular array of p rows ('lanes') by q columns, where + * p is the 'parallelism' input parameter (the lanes can be processed + * concurrently up to a point) and q is whatever makes the product pq come + * to m. + * + * Additionally, each row is divided into four equal 'segments', which are + * important to the way the algorithm decides which blocks to use as input + * to each step of the function. + * + * The term 'slice' refers to a whole set of vertically aligned segments, + * i.e. slice 0 is the whole left quarter of the array, and slice 3 the + * whole right quarter. + */ + size_t SL = m / (4*p); /* segment length: # of 1Kb blocks in a segment */ + size_t q = 4 * SL; /* width of the array: 4 segments times SL */ + size_t mprime = q * p; /* total size of the array, approximately m */ + + /* Allocate the memory. */ + struct blk* B = snewn(mprime, struct blk); + memset(B, 0, mprime * sizeof(struct blk)); + + /* + * Initial setup: fill the first two full columns of the array with data + * expanded from the starting hash h0. Each block is the result of using + * the long-output hash function H' to hash h0 itself plus the block's + * coordinates in the array. + */ + for (size_t i = 0; i < p; i++) + { + ssh_hash* h = hprime_new(1024); + put_data(h, h0, 64); + put_uint32_le(h, 0); + put_uint32_le(h, i); + hprime_final(h, 1024, B[i].data); + } + for (size_t i = 0; i < p; i++) + { + ssh_hash* h = hprime_new(1024); + put_data(h, h0, 64); + put_uint32_le(h, 1); + put_uint32_le(h, i); + hprime_final(h, 1024, B[i+p].data); + } + + /* + * Declarations for the main loop. + * + * The basic structure of the main loop is going to involve processing the + * array one whole slice (vertically divided quarter) at a time. Usually + * we'll write a new value into every single block in the slice, except + * that in the initial slice on the first pass, we've already written + * values into the first two columns during the initial setup above. So + * 'jstart' indicates the starting index in each segment we process; it + * starts off as 2 so that we don't overwrite the initial setup, and then + * after the first slice is done, we set it to 0, and it stays there. + * + * d_mode indicates whether we're being data-dependent (true) or + * data-independent (false). In the hybrid Argon2id mode, we start off + * independent, and then once we've mixed things up enough, switch over to + * dependent mode to force long serial chains of computation. + */ + size_t jstart = 2; + bool d_mode = (y == 0); + struct blk out2i, tmp2i, in2i; + + /* Outermost loop: t whole passes from left to right over the array */ + for (size_t pass = 0; pass < t; pass++) + { + + /* Within that, we process the array in its four main slices */ + for (unsigned slice = 0; slice < 4; slice++) + { + + /* In Argon2id mode, if we're half way through the first pass, + * this is the moment to switch d_mode from false to true */ + if (pass == 0 && slice == 2 && y == 2) + d_mode = true; + + /* Loop over every segment in the slice (i.e. every row). So i is + * the y-coordinate of each block we process. */ + for (size_t i = 0; i < p; i++) + { + + /* And within that segment, process the blocks from left to + * right, starting at 'jstart' (usually 0, but 2 in the first + * slice). */ + for (size_t jpre = jstart; jpre < SL; jpre++) + { + + /* j is the x-coordinate of each block we process, made up + * of the slice number and the index 'jpre' within the + * segment. */ + size_t j = slice * SL + jpre; + + /* jm1 is j-1 (mod q) */ + uint32_t jm1 = (j == 0 ? q-1 : j-1); + + /* + * Construct two 32-bit pseudorandom integers J1 and J2. + * This is the part of the algorithm that varies between + * the data-dependent and independent modes. + */ + uint32_t J1, J2; + if (d_mode) + { + /* + * Data-dependent: grab the first 64 bits of the block + * to the left of this one. + */ + J1 = GET_32BIT_LSB_FIRST(B[i + p * jm1].data); + J2 = GET_32BIT_LSB_FIRST(B[i + p * jm1].data + 4); + } + else + { + /* + * Data-independent: generate pseudorandom data by + * hashing a sequence of preimage blocks that include + * all our input parameters, plus the coordinates of + * this point in the algorithm (array position and + * pass number) to make all the hash outputs distinct. + * + * The hash we use is G itself, applied twice. So we + * generate 1Kb of data at a time, which is enough for + * 128 (J1,J2) pairs. Hence we only need to do the + * hashing if our index within the segment is a + * multiple of 128, or if we're at the very start of + * the algorithm (in which case we started at 2 rather + * than 0). After that we can just keep picking data + * out of our most recent hash output. + */ + if (jpre == jstart || jpre % 128 == 0) + { + /* + * Hash preimage is mostly zeroes, with a + * collection of assorted integer values we had + * anyway. + */ + memset(in2i.data, 0, sizeof(in2i.data)); + PUT_64BIT_LSB_FIRST(in2i.data + 0, pass); + PUT_64BIT_LSB_FIRST(in2i.data + 8, i); + PUT_64BIT_LSB_FIRST(in2i.data + 16, slice); + PUT_64BIT_LSB_FIRST(in2i.data + 24, mprime); + PUT_64BIT_LSB_FIRST(in2i.data + 32, t); + PUT_64BIT_LSB_FIRST(in2i.data + 40, y); + PUT_64BIT_LSB_FIRST(in2i.data + 48, jpre / 128 + 1); + + /* + * Now apply G twice to generate the hash output + * in out2i. + */ + memset(tmp2i.data, 0, sizeof(tmp2i.data)); + G_xor(tmp2i.data, tmp2i.data, in2i.data); + memset(out2i.data, 0, sizeof(out2i.data)); + G_xor(out2i.data, out2i.data, tmp2i.data); + } + + /* + * Extract J1 and J2 from the most recent hash output + * (whether we've just computed it or not). + */ + J1 = GET_32BIT_LSB_FIRST( + out2i.data + 8 * (jpre % 128)); + J2 = GET_32BIT_LSB_FIRST( + out2i.data + 8 * (jpre % 128) + 4); + } + + /* + * Now convert J1 and J2 into the index of an existing + * block of the array to use as input to this step. This + * is fairly fiddly. + * + * The easy part: the y-coordinate of the input block is + * obtained by reducing J2 mod p, except that at the very + * start of the algorithm (processing the first slice on + * the first pass) we simply use the same y-coordinate as + * our output block. + * + * Note that it's safe to use the ordinary % operator + * here, without any concern for timing side channels: in + * data-independent mode J2 is not correlated to any + * secrets, and in data-dependent mode we're going to be + * giving away side-channel data _anyway_ when we use it + * as an array index (and by assumption we don't care, + * because it's already massively randomised from the real + * inputs). + */ + uint32_t index_l = (pass == 0 && slice == 0) ? i : J2 % p; + + /* + * The hard part: which block in this array row do we use? + * + * First, we decide what the possible candidates are. This + * requires some case analysis, and depends on whether the + * array row is the same one we're writing into or not. + * + * If it's not the same row: we can't use any block from + * the current slice (because the segments within a slice + * have to be processable in parallel, so in a concurrent + * implementation those blocks are potentially in the + * process of being overwritten by other threads). But the + * other three slices are fair game, except that in the + * first pass, slices to the right of us won't have had + * any values written into them yet at all. + * + * If it is the same row, we _are_ allowed to use blocks + * from the current slice, but only the ones before our + * current position. + * + * In both cases, we also exclude the individual _column_ + * just to the left of the current one. (The block + * immediately to our left is going to be the _other_ + * input to G, but the spec also says that we avoid that + * column even in a different row.) + * + * All of this means that we end up choosing from a + * cyclically contiguous interval of blocks within this + * lane, but the start and end points require some thought + * to get them right. + */ + + /* Start position is the beginning of the _next_ slice + * (containing data from the previous pass), unless we're + * on pass 0, where the start position has to be 0. */ + uint32_t Wstart = (pass == 0 ? 0 : (slice + 1) % 4 * SL); + + /* End position splits up by cases. */ + uint32_t Wend; + if (index_l == i) + { + /* Same lane as output: we can use anything up to (but + * not including) the block immediately left of us. */ + Wend = jm1; + } + else + { + /* Different lane from output: we can use anything up + * to the previous slice boundary, or one less than + * that if we're at the very left edge of our slice + * right now. */ + Wend = SL * slice; + if (jpre == 0) + Wend = (Wend + q-1) % q; + } + + /* Total number of blocks available to choose from */ + uint32_t Wsize = (Wend + q - Wstart) % q; + + /* Fiddly computation from the spec that chooses from the + * available blocks, in a deliberately non-uniform + * fashion, using J1 as pseudorandom input data. Output is + * zz which is the index within our contiguous interval. */ + uint32_t x = ((uint64_t)J1 * J1) >> 32; + uint32_t y2 = ((uint64_t)Wsize * x) >> 32; + uint32_t zz = Wsize - 1 - y2; + + /* And index_z is the actual x coordinate of the block we + * want. */ + uint32_t index_z = (Wstart + zz) % q; + + /* Phew! Combine that block with the one immediately to + * our left, and XOR over the top of whatever is already + * in our current output block. */ + G_xor(B[i + p * j].data, B[i + p * jm1].data, + B[index_l + p * index_z].data); + } + } + + /* We've finished processing a slice. Reset jstart to 0. It will + * onily _not_ have been 0 if this was pass 0 slice 0, in which + * case it still had its initial value of 2 to avoid the starting + * data. */ + jstart = 0; + } + } + + /* + * The main output is all done. Final output works by taking the XOR of + * all the blocks in the rightmost column of the array, and then using + * that as input to our long hash H'. The output of _that_ is what we + * deliver to the caller. + */ + + struct blk C = B[p * (q-1)]; + for (size_t i = 1; i < p; i++) + memxor(C.data, C.data, B[i + p * (q-1)].data, 1024); + + { + ssh_hash* h = hprime_new(T); + put_data(h, C.data, 1024); + hprime_final(h, T, out); + } + + /* + * Clean up. + */ + smemclr(out2i.data, sizeof(out2i.data)); + smemclr(tmp2i.data, sizeof(tmp2i.data)); + smemclr(in2i.data, sizeof(in2i.data)); + smemclr(C.data, sizeof(C.data)); + smemclr(B, mprime * sizeof(struct blk)); + sfree(B); +} + + +std::string zen::zargon2(zen::Argon2Flavor flavour, uint32_t mem, uint32_t passes, uint32_t parallel, uint32_t taglen, + const std::string_view password, const std::string_view salt) +{ + std::string output(taglen, '\0'); + argon2_internal(parallel, taglen, mem, passes, static_cast(flavour), + {.ptr = password.data(), .len = password.size()}, + {.ptr = salt .data(), .len = salt .size()}, + {.ptr = "", .len = 0}, + {.ptr = "", .len = 0}, reinterpret_cast(output.data())); + return output; +} diff --git a/zen/argon2.h b/zen/argon2.h new file mode 100644 index 0000000..5d2bb52 --- /dev/null +++ b/zen/argon2.h @@ -0,0 +1,20 @@ +// ***************************************************************************** +// * 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 ARGON2_H_0175896874598102356081374 +#define ARGON2_H_0175896874598102356081374 + +#include + +namespace zen +{ +enum class Argon2Flavor { d, i, id }; + +std::string zargon2(zen::Argon2Flavor flavour, uint32_t mem, uint32_t passes, uint32_t parallel, uint32_t taglen, + const std::string_view password, const std::string_view salt); +} + +#endif //ARGON2_H_0175896874598102356081374 diff --git a/zen/base64.h b/zen/base64.h new file mode 100644 index 0000000..3874f03 --- /dev/null +++ b/zen/base64.h @@ -0,0 +1,176 @@ +// ***************************************************************************** +// * 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 BASE64_H_08473021856321840873021487213453214 +#define BASE64_H_08473021856321840873021487213453214 + +#include +#include +#include "type_traits.h" + + +namespace zen +{ +/* https://en.wikipedia.org/wiki/Base64 + + Usage: + const std::string input = "Sample text"; + std::string output; + zen::encodeBase64(input.begin(), input.end(), std::back_inserter(output)); + //output contains "U2FtcGxlIHRleHQ=" */ + +template +OutputIterator encodeBase64(InputIterator first, InputIterator last, OutputIterator result); //nothrow! + +template +OutputIterator decodeBase64(InputIterator first, InputIterator last, OutputIterator result); //nothrow! + +std::string stringEncodeBase64(const std::string_view& str); +std::string stringDecodeBase64(const std::string_view& str); + + + + + + + + + + +//------------------------- implementation ------------------------------- +namespace impl +{ +//64 chars for base64 encoding + padding char +constexpr char ENCODING_MIME[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; +constexpr signed char DECODING_MIME[] = +{ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, 64, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1 +}; +const unsigned char INDEX_PAD = 64; //index of "=" +} + + +template inline +OutputIterator encodeBase64(InputIterator first, InputIterator last, OutputIterator result) +{ + using namespace impl; + static_assert(sizeof(typename std::iterator_traits::value_type) == 1); + static_assert(std::size(ENCODING_MIME) == 64 + 1 + 1); + static_assert(arrayHash(ENCODING_MIME) == 1616767125); + + while (first != last) + { + const unsigned char a = static_cast(*first++); + *result++ = ENCODING_MIME[a >> 2]; + + if (first == last) + { + *result++ = ENCODING_MIME[((a & 0x3) << 4)]; + *result++ = ENCODING_MIME[INDEX_PAD]; + *result++ = ENCODING_MIME[INDEX_PAD]; + break; + } + const unsigned char b = static_cast(*first++); + *result++ = ENCODING_MIME[((a & 0x3) << 4) | (b >> 4)]; + + if (first == last) + { + *result++ = ENCODING_MIME[((b & 0xf) << 2)]; + *result++ = ENCODING_MIME[INDEX_PAD]; + break; + } + const unsigned char c = static_cast(*first++); + *result++ = ENCODING_MIME[((b & 0xf) << 2) | (c >> 6)]; + *result++ = ENCODING_MIME[c & 0x3f]; + } + return result; +} + + +template inline +OutputIterator decodeBase64(InputIterator first, InputIterator last, OutputIterator result) +{ + using namespace impl; + static_assert(sizeof(typename std::iterator_traits::value_type) == 1); + static_assert(std::size(DECODING_MIME) == 128); + static_assert(arrayHash(DECODING_MIME)== 1169145114); + + const unsigned char INDEX_END = INDEX_PAD + 1; + + auto readIndex = [&]() -> unsigned char //return index within [0, 64] or INDEX_END if end of input + { + for (;;) + { + if (first == last) + return INDEX_END; + + const unsigned char ch = static_cast(*first++); + if (ch < std::size(DECODING_MIME)) //we're in lower ASCII table half + { + const int index = DECODING_MIME[ch]; + if (0 <= index && index <= static_cast(INDEX_PAD)) //skip all unknown characters (including carriage return, line-break, tab) + return static_cast(index); + } + } + }; + + for (;;) + { + const unsigned char index1 = readIndex(); + const unsigned char index2 = readIndex(); + if (index1 >= INDEX_PAD || index2 >= INDEX_PAD) + { + assert(index1 == INDEX_END && index2 == INDEX_END); + break; + } + *result++ = static_cast((index1 << 2) | (index2 >> 4)); + + const unsigned char index3 = readIndex(); + if (index3 >= INDEX_PAD) //padding + { + assert(index3 == INDEX_PAD); + break; + } + *result++ = static_cast(((index2 & 0xf) << 4) | (index3 >> 2)); + + const unsigned char index4 = readIndex(); + if (index4 >= INDEX_PAD) //padding + { + assert(index4 == INDEX_PAD); + break; + } + *result++ = static_cast(((index3 & 0x3) << 6) | index4); + } + return result; +} + + +inline +std::string stringEncodeBase64(const std::string_view& str) +{ + std::string out; + encodeBase64(str.begin(), str.end(), std::back_inserter(out)); + return out; +} + + +inline +std::string stringDecodeBase64(const std::string_view& str) +{ + std::string out; + decodeBase64(str.begin(), str.end(), std::back_inserter(out)); + return out; +} +} + +#endif //BASE64_H_08473021856321840873021487213453214 diff --git a/zen/basic_math.h b/zen/basic_math.h new file mode 100644 index 0000000..9080d07 --- /dev/null +++ b/zen/basic_math.h @@ -0,0 +1,342 @@ +// ***************************************************************************** +// * 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 BASIC_MATH_H_3472639843265675 +#define BASIC_MATH_H_3472639843265675 + +#include +#include +#include +#include "type_traits.h" + + +namespace numeric +{ +template auto dist(T a, T b); +template int sign(T value); //returns one of {-1, 0, 1} +template bool isNull(T value); //...definitively fishy... + +template //precondition: range must be sorted! +auto roundToGrid(T val, InputIterator first, InputIterator last); + +template auto intDivRound(N numerator, D denominator); +template auto intDivCeil (N numerator, D denominator); +template auto intDivFloor(N numerator, D denominator); + +template +constexpr T power(T value); + +double radToDeg(double rad); //convert unit [rad] into [°] +double degToRad(double degree); //convert unit [°] into [rad] + +template +double arithmeticMean(InputIterator first, InputIterator last); + +template +double median(RandomAccessIterator first, RandomAccessIterator last); //note: invalidates input range! + +template +double stdDeviation(InputIterator first, InputIterator last, double* mean = nullptr); //estimate standard deviation (and thereby arithmetic mean) + +//median absolute deviation: "mad / 0.6745" is a robust measure for standard deviation of a normal distribution +template +double mad(RandomAccessIterator first, RandomAccessIterator last); //note: invalidates input range! + +template +double norm2(InputIterator first, InputIterator last); + +//---------------------------------------------------------------------------------- + + + + + + + + + + + + + +//################# inline implementation ######################### +template inline +auto dist(T a, T b) //return type might be different than T, e.g. std::chrono::duration instead of std::chrono::time_point +{ + return a > b ? a - b : b - a; +} + + +template inline +int sign(T value) //returns one of {-1, 0, 1} +{ + static_assert(std::is_signed_v); + return value < 0 ? -1 : (value > 0 ? 1 : 0); +} + +/* +part of C++11 now! +template inline +std::pair minMaxElement(InputIterator first, InputIterator last, Compare compLess) +{ + //by factor 1.5 to 3 faster than boost::minmax_element (=two-step algorithm) for built-in types! + + InputIterator itMin = first; + InputIterator itMax = first; + + if (first != last) + { + auto minVal = *itMin; //nice speedup on 64 bit! + auto maxVal = *itMax; // + for (;;) + { + ++first; + if (first == last) + break; + const auto val = *first; + + if (compLess(maxVal, val)) + { + itMax = first; + maxVal = val; + } + else if (compLess(val, minVal)) + { + itMin = first; + minVal = val; + } + } + } + return {itMin, itMax}; +} + + +template inline +std::pair minMaxElement(InputIterator first, InputIterator last) +{ + return minMaxElement(first, last, std::less()); +} +*/ + +template inline +auto roundToGrid(T val, InputIterator first, InputIterator last) +{ + assert(std::is_sorted(first, last)); + if (first == last) + return static_cast(val); + + InputIterator it = std::lower_bound(first, last, val); + if (it == last) + return *--last; + if (it == first) + return *first; + + const auto nextVal = *it; + const auto prevVal = *--it; + return val - prevVal < nextVal - val ? prevVal : nextVal; +} + + +template inline +bool isNull(T value) +{ + return abs(value) <= std::numeric_limits::epsilon(); //epsilon is 0 für integral types => less-equal +} + + +template inline +auto intDivRound(N num, D den) +{ + using namespace zen; + static_assert(isInteger&& isInteger); + static_assert(isSignedInt == isSignedInt); //until further + assert(den != 0); + if constexpr (isSignedInt) + { + if ((num < 0) != (den < 0)) + return (num - den / 2) / den; + } + return (num + den / 2) / den; +} + + +template inline +auto intDivCeil(N num, D den) +{ + using namespace zen; + static_assert(isInteger&& isInteger); + static_assert(isSignedInt == isSignedInt); //until further + assert(den != 0); + if constexpr (isSignedInt) + { + if ((num < 0) != (den < 0)) + return num / den; + + if (num < 0 && den < 0) + num += 2; //return (num + den + 1) / den + } + return (num + den - 1) / den; +} + + +template inline +auto intDivFloor(N num, D den) +{ + using namespace zen; + static_assert(isInteger&& isInteger); + static_assert(isSignedInt == isSignedInt); //until further + assert(den != 0); + if constexpr (isSignedInt) + { + if ((num < 0) != (den < 0)) + { + if (num < 0) + num += 2; //return (num - den + 1) / den + + return (num - den - 1) / den; + } + } + return num / den; +} + + +namespace +{ +template struct PowerImpl; +//let's use non-recursive specializations to help the compiler +template struct PowerImpl<2, T> { static constexpr T result(T value) { return value * value; } }; +template struct PowerImpl<3, T> { static constexpr T result(T value) { return value * value * value; } }; +} + +template inline +constexpr T power(T value) +{ + return PowerImpl::result(value); +} + + +inline +double radToDeg(double rad) +{ + return rad * (180.0 / std::numbers::pi); +} + + +inline +double degToRad(double degree) +{ + return degree / (180.0 / std::numbers::pi); +} + + +template inline +double arithmeticMean(InputIterator first, InputIterator last) +{ + size_t n = 0; //avoid random-access requirement for iterator! + double sum_xi = 0; + + for (; first != last; ++first, ++n) + sum_xi += *first; + + return n == 0 ? 0 : sum_xi / n; +} + + +template inline +double median(RandomAccessIterator first, RandomAccessIterator last) //note: invalidates input range! +{ + const size_t n = last - first; + if (n == 0) + return 0; + + std::nth_element(first, first + n / 2, last); //complexity: O(n) + const double midVal = *(first + n / 2); + + if (n % 2 != 0) + return midVal; + else //n is even and >= 2 in this context: return mean of two middle values + return 0.5 * (*std::max_element(first, first + n / 2) + midVal); //this operation is the reason why median() CANNOT support a comparison predicate!!! +} + + +template inline +double mad(RandomAccessIterator first, RandomAccessIterator last) //note: invalidates input range! +{ + //https://en.wikipedia.org/wiki/Median_absolute_deviation + const size_t n = last - first; + if (n == 0) + return 0; + + const double m = median(first, last); + + //the second median needs to operate on absolute residuals => avoid transforming input range which may have less than double precision! + auto lessMedAbs = [m](double lhs, double rhs) { return abs(lhs - m) < abs(rhs - m); }; + + std::nth_element(first, first + n / 2, last, lessMedAbs); //complexity: O(n) + const double midVal = abs(*(first + n / 2) - m); + + if (n % 2 != 0) + return midVal; + else //n is even and >= 2 in this context: return mean of two middle values + return 0.5 * (abs(*std::max_element(first, first + n / 2, lessMedAbs) - m) + midVal); +} + + +template inline +double stdDeviation(InputIterator first, InputIterator last, double* arithMean) +{ + //implementation minimizing rounding errors, see: https://en.wikipedia.org/wiki/Standard_deviation + //combined with technique avoiding overflow, see: https://www.netlib.org/blas/dnrm2.f -> only 10% performance degradation + + size_t n = 0; + double mean = 0; + double q = 0; + double scale = 1; + + for (; first != last; ++first) + { + ++n; + const double val = *first - mean; + + if (abs(val) > scale) + { + q = (n - 1.0) / n + q * power<2>(scale / val); + scale = abs(val); + } + else + q += (n - 1.0) * power<2>(val / scale) / n; + + mean += val / n; + } + + if (arithMean) + *arithMean = mean; + + return n <= 1 ? 0 : std::sqrt(q / (n - 1)) * scale; +} + + +template inline +double norm2(InputIterator first, InputIterator last) +{ + double result = 0; + double scale = 1; + for (; first != last; ++first) + { + const double tmp = abs(*first); + if (tmp > scale) + { + result = 1 + result * power<2>(scale / tmp); + scale = tmp; + } + else + result += power<2>(tmp / scale); + } + return std::sqrt(result) * scale; +} +} + +#endif //BASIC_MATH_H_3472639843265675 diff --git a/zen/build_info.h b/zen/build_info.h new file mode 100644 index 0000000..86ff303 --- /dev/null +++ b/zen/build_info.h @@ -0,0 +1,34 @@ +// ***************************************************************************** +// * 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 BUILD_INFO_H_5928539285603428657 +#define BUILD_INFO_H_5928539285603428657 + + + +namespace zen +{ +enum class BuildArch +{ + bit32, + bit64, + +#ifdef __LP64__ + program = bit64 +#else + program = bit32 +#endif +}; + +static_assert((BuildArch::program == BuildArch::bit32 ? 32 : 64) == sizeof(void*) * 8); + + +//harmonize with os_arch enum in update_checks table: +constexpr const char* cpuArchName = BuildArch::program == BuildArch::bit32 ? "i686": "x86-64"; + +} + +#endif //BUILD_INFO_H_5928539285603428657 diff --git a/zen/crc.h b/zen/crc.h new file mode 100644 index 0000000..009ff9d --- /dev/null +++ b/zen/crc.h @@ -0,0 +1,110 @@ +// ***************************************************************************** +// * 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 CRC_H_23489275827847235 +#define CRC_H_23489275827847235 + +#include "type_traits.h" + + +namespace zen +{ +uint16_t getCrc16(const std::string_view& str); +uint32_t getCrc32(const std::string_view& str); +template uint16_t getCrc16(ByteIterator first, ByteIterator last); +template uint32_t getCrc32(ByteIterator first, ByteIterator last); + + + + +//------------------------- implementation ------------------------------- +inline uint16_t getCrc16(const std::string_view& str) { return getCrc16(str.begin(), str.end()); } +inline uint32_t getCrc32(const std::string_view& str) { return getCrc32(str.begin(), str.end()); } + + +template inline +uint16_t getCrc16(ByteIterator first, ByteIterator last) //http://www.sunshine2k.de/articles/coding/crc/understanding_crc.html +{ + static_assert(sizeof(typename std::iterator_traits::value_type) == 1); + + uint16_t crc = 0; + std::for_each(first, last, [&](unsigned char b) + { + constexpr uint16_t crcTable[] = + { + 0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241, 0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440, + 0xcc01, 0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40, 0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841, + 0xd801, 0x18c0, 0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40, 0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41, + 0x1400, 0xd4c1, 0xd581, 0x1540, 0xd701, 0x17c0, 0x1680, 0xd641, 0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081, 0x1040, + 0xf001, 0x30c0, 0x3180, 0xf141, 0x3300, 0xf3c1, 0xf281, 0x3240, 0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441, + 0x3c00, 0xfcc1, 0xfd81, 0x3d40, 0xff01, 0x3fc0, 0x3e80, 0xfe41, 0xfa01, 0x3ac0, 0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840, + 0x2800, 0xe8c1, 0xe981, 0x2940, 0xeb01, 0x2bc0, 0x2a80, 0xea41, 0xee01, 0x2ec0, 0x2f80, 0xef41, 0x2d00, 0xedc1, 0xec81, 0x2c40, + 0xe401, 0x24c0, 0x2580, 0xe541, 0x2700, 0xe7c1, 0xe681, 0x2640, 0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0, 0x2080, 0xe041, + 0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281, 0x6240, 0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441, + 0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41, 0xaa01, 0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840, + 0x7800, 0xb8c1, 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41, 0xbe01, 0x7ec0, 0x7f80, 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40, + 0xb401, 0x74c0, 0x7580, 0xb541, 0x7700, 0xb7c1, 0xb681, 0x7640, 0x7200, 0xb2c1, 0xb381, 0x7340, 0xb101, 0x71c0, 0x7080, 0xb041, + 0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0, 0x5280, 0x9241, 0x9601, 0x56c0, 0x5780, 0x9741, 0x5500, 0x95c1, 0x9481, 0x5440, + 0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40, 0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901, 0x59c0, 0x5880, 0x9841, + 0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40, 0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0, 0x4c80, 0x8c41, + 0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641, 0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081, 0x4040 + }; + static_assert(std::size(crcTable) == 256); + static_assert(arrayHash(crcTable) == 728085957); + + crc = (crc >> 8) ^ crcTable[(crc ^ b) & 0xFF]; + }); + return crc; +} + + +template inline +uint32_t getCrc32(ByteIterator first, ByteIterator last) //https://en.wikipedia.org/wiki/Cyclic_redundancy_check +{ + static_assert(sizeof(typename std::iterator_traits::value_type) == 1); + + uint32_t crc = 0xFFFFFFFF; + std::for_each(first, last, [&](unsigned char b) + { + constexpr uint32_t crcTable[] = + { + 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, + 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, + 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, + 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, + 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, 0x26d930ac, 0x51de003a, + 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, + 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, + 0x9fbfe4a5, 0xe8b8d433, 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, + 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, + 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, + 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, + 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f, + 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, + 0x03b6e20c, 0x74b1d29a, 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, + 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, + 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, + 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, 0xd80d2bda, 0xaf0a1b4c, + 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, + 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, + 0x2cd99e8b, 0x5bdeae1d, 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, + 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, + 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, + 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, + 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, + 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, + 0x5d681b02, 0x2a6f2b94, 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d + }; + static_assert(std::size(crcTable) == 256); + static_assert(arrayHash(crcTable) == 2988069445); + + crc = (crc >> 8) ^ crcTable[(crc ^ b) & 0xFF]; + }); + return crc ^ 0xFFFFFFFF; +} +} + +#endif //CRC_H_23489275827847235 diff --git a/zen/dir_watcher.cpp b/zen/dir_watcher.cpp new file mode 100644 index 0000000..b553350 --- /dev/null +++ b/zen/dir_watcher.cpp @@ -0,0 +1,158 @@ +// ***************************************************************************** +// * 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 "dir_watcher.h" +#include "thread.h" +#include "scope_guard.h" +#include "file_access.h" + + #include + #include + #include //fcntl + #include //close + #include //NAME_MAX + #include "file_traverser.h" + + +using namespace zen; + + +struct DirWatcher::Impl +{ + int notifDescr = 0; + std::unordered_map watchedPaths; //watch descriptor and (sub-)directory paths -> owned by "notifDescr" +}; + + +DirWatcher::DirWatcher(const Zstring& dirPath) : //throw FileError + baseDirPath_(dirPath), + pimpl_(std::make_unique()) +{ + //get all subdirectories + std::vector fullFolderList {baseDirPath_}; + { + std::function traverse; + + traverse = [&traverse, &fullFolderList](const Zstring& path) //throw FileError + { + traverseFolder(path, nullptr, + [&](const FolderInfo& fi ) + { + fullFolderList.push_back(fi.fullPath); + traverse(fi.fullPath); //throw FileError + }, + nullptr /*don't traverse into symlinks (analog to Windows)*/); //throw FileError + }; + + traverse(baseDirPath_); //throw FileError + } + + //init + pimpl_->notifDescr = ::inotify_init(); + if (pimpl_->notifDescr == -1) + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot monitor directory %x."), L"%x", fmtPath(baseDirPath_)), "inotify_init"); + + ZEN_ON_SCOPE_FAIL( ::close(pimpl_->notifDescr); ); + + //set non-blocking mode + const int flags = ::fcntl(pimpl_->notifDescr, F_GETFL); + if (flags == -1) + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot monitor directory %x."), L"%x", fmtPath(baseDirPath_)), "fcntl(F_GETFL)"); + + if (::fcntl(pimpl_->notifDescr, F_SETFL, flags | O_NONBLOCK) != 0) + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot monitor directory %x."), L"%x", fmtPath(baseDirPath_)), "fcntl(F_SETFL, O_NONBLOCK)"); + + //add watches + for (const Zstring& subDirPath : fullFolderList) + { + int wd = ::inotify_add_watch(pimpl_->notifDescr, subDirPath.c_str(), + IN_ONLYDIR | //"Only watch pathname if it is a directory." + IN_DONT_FOLLOW | //don't follow symbolic links + IN_CREATE | + IN_MODIFY | + IN_CLOSE_WRITE | + IN_DELETE | + IN_DELETE_SELF | + IN_MOVED_FROM | + IN_MOVED_TO | + IN_MOVE_SELF); + if (wd == -1) + { + const ErrorCode ec = getLastError(); //copy before directly/indirectly making other system calls! + if (ec == ENOSPC) //fix misleading system message "No space left on device" + throw FileError(replaceCpy(_("Cannot monitor directory %x."), L"%x", fmtPath(subDirPath)), + formatSystemError("inotify_add_watch", L"ENOSPC", + L"The user limit on the total number of inotify watches was reached or the kernel failed to allocate a needed resource.")); + + throw FileError(replaceCpy(_("Cannot monitor directory %x."), L"%x", fmtPath(subDirPath)), formatSystemError("inotify_add_watch", ec)); + } + + pimpl_->watchedPaths.emplace(wd, subDirPath); + } +} + + +DirWatcher::~DirWatcher() +{ + ::close(pimpl_->notifDescr); //associated watches are removed automatically! +} + + +std::vector DirWatcher::fetchChanges(const std::function& requestUiUpdate, std::chrono::milliseconds cbInterval) //throw FileError +{ + std::vector buf(512 * (sizeof(inotify_event) + NAME_MAX + 1)); + + ssize_t bytesRead = 0; + do + { + //non-blocking call, see O_NONBLOCK + bytesRead = ::read(pimpl_->notifDescr, buf.data(), buf.size()); + } + while (bytesRead < 0 && errno == EINTR); //"Interrupted function call; When this happens, you should try the call again." + + if (bytesRead < 0) + { + if (errno == EAGAIN) //this error is ignored in all inotify wrappers I found + return std::vector(); + + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot monitor directory %x."), L"%x", fmtPath(baseDirPath_)), "read"); + } + + std::vector output; + + ssize_t bytePos = 0; + while (bytePos < bytesRead) + { + inotify_event& evt = reinterpret_cast(buf[bytePos]); + + if (evt.len != 0) //exclude case: deletion of "self", already reported by parent directory watch + { + auto it = pimpl_->watchedPaths.find(evt.wd); + if (it != pimpl_->watchedPaths.end()) + { + //Note: evt.len is NOT the size of the evt.name c-string, but the array size including all padding 0 characters! + //It may be even 0 in which case evt.name must not be used! + const Zstring itemPath = appendPath(it->second, evt.name); + + if ((evt.mask & IN_CREATE) || + (evt.mask & IN_MOVED_TO)) + output.push_back({ChangeType::create, itemPath}); + else if ((evt.mask & IN_MODIFY) || + (evt.mask & IN_CLOSE_WRITE)) + output.push_back({ChangeType::update, itemPath}); + else if ((evt.mask & IN_DELETE ) || + (evt.mask & IN_DELETE_SELF) || + (evt.mask & IN_MOVE_SELF ) || + (evt.mask & IN_MOVED_FROM)) + output.push_back({ChangeType::remove, itemPath}); + } + } + bytePos += sizeof(inotify_event) + evt.len; + } + + return output; +} + diff --git a/zen/dir_watcher.h b/zen/dir_watcher.h new file mode 100644 index 0000000..8c82707 --- /dev/null +++ b/zen/dir_watcher.h @@ -0,0 +1,72 @@ +// ***************************************************************************** +// * 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 DIR_WATCHER_348577025748023458 +#define DIR_WATCHER_348577025748023458 + +#include +#include +#include +#include "file_error.h" + + +namespace zen +{ +//Windows: ReadDirectoryChangesW https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw +//Linux: inotify https://linux.die.net/man/7/inotify +//macOS: kqueue https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man2/kqueue.2.html + +//watch directory including subdirectories +/* +!Note handling of directories!: + Windows: removal of top watched directory is NOT notified when watching the dir handle, e.g. brute force usb stick removal, + (watchting for GUID_DEVINTERFACE_WPD OTOH works fine!) + however manual unmount IS notified (e.g. USB stick removal, then re-insert), but watching is stopped! + Renaming of top watched directory handled incorrectly: Not notified(!) + additional changes in subfolders + now do report FILE_ACTION_MODIFIED for directory (check that should prevent this fails!) + + Linux: newly added subdirectories are reported but not automatically added for watching! -> reset Dirwatcher! + removal of base directory is NOT notified! + + macOS: everything works as expected; renaming of base directory is also detected + + Overcome all issues portably: check existence of top watched directory externally + reinstall watch after changes in directory structure (added directories) are detected +*/ +class DirWatcher +{ +public: + explicit DirWatcher(const Zstring& dirPath); //throw FileError + ~DirWatcher(); + + enum class ChangeType + { + create, // + update, //informal: use for debugging/logging only! + remove, // + baseFolderUnavailable, //1. not existing or 2. can't access + }; + + struct Change + { + ChangeType type = ChangeType::create; + Zstring itemPath; + }; + + //extract accumulated changes since last call + std::vector fetchChanges(const std::function& requestUiUpdate, std::chrono::milliseconds cbInterval); //throw FileError + +private: + DirWatcher (const DirWatcher&) = delete; + DirWatcher& operator=(const DirWatcher&) = delete; + + const Zstring baseDirPath_; + + struct Impl; + const std::unique_ptr pimpl_; +}; +} + +#endif diff --git a/zen/error_log.h b/zen/error_log.h new file mode 100644 index 0000000..2e5c4eb --- /dev/null +++ b/zen/error_log.h @@ -0,0 +1,127 @@ +// ***************************************************************************** +// * 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 ERROR_LOG_H_8917590832147915 +#define ERROR_LOG_H_8917590832147915 + +#include +#include +#include "time.h" +#include "i18n.h" +#include "zstring.h" + + +namespace zen +{ +enum MessageType +{ + MSG_TYPE_INFO = 0x1, + MSG_TYPE_WARNING = 0x2, + MSG_TYPE_ERROR = 0x4, +}; + +struct LogEntry +{ + time_t time = 0; + MessageType type = MSG_TYPE_ERROR; + Zstringc message; //conserve memory (=> avoid std::string SSO overhead!) +}; + +std::string formatMessage(const LogEntry& entry); + +using ErrorLog = std::vector; + +void logMsg(ErrorLog& log, const std::wstring& msg, MessageType type, time_t time = std::time(nullptr)); + +struct ErrorLogStats +{ + int infos = 0; + int warnings = 0; + int errors = 0; +}; +ErrorLogStats getStats(const ErrorLog& log); + + + + + + + +//######################## implementation ########################## +inline +void logMsg(ErrorLog& log, const std::wstring& msg, MessageType type, time_t time) +{ + log.push_back({time, type, utfTo(msg)}); +} + + +inline +ErrorLogStats getStats(const ErrorLog& log) +{ + ErrorLogStats count; + for (const LogEntry& entry : log) + switch (entry.type) + { + case MSG_TYPE_INFO: + ++count.infos; + break; + case MSG_TYPE_WARNING: + ++count.warnings; + break; + case MSG_TYPE_ERROR: + ++count.errors; + break; + } + assert(std::ssize(log) == count.infos + count.warnings + count.errors); + return count; +} + + +inline +std::wstring getMessageTypeLabel(MessageType type) +{ + switch (type) + { + case MSG_TYPE_INFO: + return _("Info"); + case MSG_TYPE_WARNING: + return _("Warning"); + case MSG_TYPE_ERROR: + return _("Error"); + } + assert(false); + return std::wstring(); +} + + +inline +std::string formatMessage(const LogEntry& entry) +{ + std::string msgFmt = '[' + utfTo(formatTime(formatTimeTag, getLocalTime(entry.time))) + "] " + utfTo(getMessageTypeLabel(entry.type)) + ": "; + const size_t prefixLen = unicodeLength(msgFmt); //consider Unicode! + + const Zstringc msg = trimCpy(entry.message); + static_assert(std::is_same_v, "no worries about copying as long as we're using a ref-counted string!"); + assert(msg == entry.message); //trimming shouldn't be needed usually!? + + for (auto it = msg.begin(); it != msg.end(); ) + if (*it == '\n') + { + msgFmt += *it++; + msgFmt.append(prefixLen, ' '); + //skip duplicate newlines + for (; it != msg.end() && *it == '\n'; ++it) + ; + } + else + msgFmt += *it++; + + msgFmt += '\n'; + return msgFmt; +} +} + +#endif //ERROR_LOG_H_8917590832147915 diff --git a/zen/extra_log.h b/zen/extra_log.h new file mode 100644 index 0000000..3928d0f --- /dev/null +++ b/zen/extra_log.h @@ -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 * +// ***************************************************************************** + +#ifndef EXTRA_LOG_H_601673246392441846218957402563 +#define EXTRA_LOG_H_601673246392441846218957402563 + +#include "error_log.h" +#include "thread.h" + +/* log errors in "exceptional situations" when no other means are available, e.g. + - while an exception is in flight + - cleanup errors + - nothrow GUI functions */ + +namespace zen +{ +namespace impl +{ +class ExtraLog +{ +public: + ~ExtraLog() + { + assert(reportOutstandingLog_); + if (!log_.empty() && reportOutstandingLog_) + reportOutstandingLog_(log_); + } + + void init(const std::function& reportOutstandingLog) + { + assert(!reportOutstandingLog_); + reportOutstandingLog_ = reportOutstandingLog; + } + + ErrorLog fetchLog() { return std::exchange(log_, ErrorLog()); } + + void logError(const std::wstring& msg) { logMsg(log_, msg, MessageType::MSG_TYPE_ERROR); } //nothrow! + +private: + ErrorLog log_; + std::function reportOutstandingLog_; +}; + +inline constinit Global> globalExtraLog; + +template +auto accessExtraLog(Function fun) +{ + globalExtraLog.setOnce([] { return std::make_unique>(); }); + + if (auto protExtraLog = impl::globalExtraLog.get()) + protExtraLog->access([&](ExtraLog& log) { fun(log); }); + else + assert(false); //access after global shutdown!? => SOL! +} +} + +inline +void initExtraLog(const std::function& reportOutstandingLog /*nothrow! runs during global shutdown!*/) +{ + impl::accessExtraLog([&](impl::ExtraLog& el) { el.init(reportOutstandingLog); }); +} + + +inline +ErrorLog fetchExtraLog() +{ + ErrorLog output; + impl::accessExtraLog([&](impl::ExtraLog& el) { output = el.fetchLog(); }); + return output; +} + + +inline +void logExtraError(const std::wstring& msg) //nothrow! +{ + impl::accessExtraLog([&](impl::ExtraLog& el) { el.logError(msg); }); +} +} + +#endif //EXTRA_LOG_H_601673246392441846218957402563 diff --git a/zen/file_access.cpp b/zen/file_access.cpp new file mode 100644 index 0000000..0e07ddf --- /dev/null +++ b/zen/file_access.cpp @@ -0,0 +1,768 @@ +// ***************************************************************************** +// * 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 "file_access.h" +#include +#include +#include "file_traverser.h" +#include "scope_guard.h" +#include "symlink_target.h" +#include "file_io.h" +#include "crc.h" +#include "guid.h" +#include "ring_buffer.h" + + #include //statfs + #ifdef HAVE_SELINUX + #include + #endif + + + #include //open, close, AT_SYMLINK_NOFOLLOW, UTIME_OMIT + #include + +using namespace zen; + + +namespace +{ + + +struct SysErrorCode : public zen::SysError +{ + SysErrorCode(const std::string& functionName, ErrorCode ec) : SysError(formatSystemError(functionName, ec)), errorCode(ec) {} + + const ErrorCode errorCode; +}; + + +ItemType getItemTypeImpl(const Zstring& itemPath) //throw SysErrorCode +{ + struct stat itemInfo = {}; + if (::lstat(itemPath.c_str(), &itemInfo) != 0) + throw SysErrorCode("lstat", errno); + + if (S_ISLNK(itemInfo.st_mode)) + return ItemType::symlink; + if (S_ISDIR(itemInfo.st_mode)) + return ItemType::folder; + return ItemType::file; //S_ISREG || S_ISCHR || S_ISBLK || S_ISFIFO || S_ISSOCK +} + + +std::variant getItemTypeIfExistsImpl(const Zstring& itemPath) //throw SysError +{ + try + { + //fast check: 1. perf 2. expected by getFolderStatusNonBlocking() + return getItemTypeImpl(itemPath); //throw SysErrorCode + } + catch (const SysErrorCode& e) //let's dig deeper, but *only* if error code sounds like "not existing" + { + const std::optional& parentPath = getParentFolderPath(itemPath); + if (!parentPath) //device root => quick access test + throw; + if (e.errorCode == ENOENT) + { + const std::variant parentTypeOrPath = getItemTypeIfExistsImpl(*parentPath); //throw SysError + + if (const ItemType* parentType = std::get_if(&parentTypeOrPath)) + { + if (*parentType == ItemType::file /*obscure, but possible*/) + throw SysError(replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(getItemName(*parentPath)))); + + const Zstring itemName = getItemName(itemPath); + assert(!itemName.empty()); + + try + { + traverseFolder(*parentPath, //throw FileError + [&](const FileInfo& fi) { if (fi.itemName == itemName) throw SysError(_("Temporary access error:") + L' ' + e.toString()); }, + [&](const FolderInfo& fi) { if (fi.itemName == itemName) throw SysError(_("Temporary access error:") + L' ' + e.toString()); }, + [&](const SymlinkInfo& si) { if (si.itemName == itemName) throw SysError(_("Temporary access error:") + L' ' + e.toString()); }); + //- case-sensitive comparison! itemPath must be normalized! + //- finding the item after getItemType() previously failed is exceptional + } + catch (const FileError& e2) { throw SysError(replaceCpy(e2.toString(), L"\n\n", L'\n')); } + + return *parentPath; + } + else + return parentTypeOrPath; + } + else + throw; + } +} +} + + +ItemType zen::getItemType(const Zstring& itemPath) //throw FileError +{ + try + { + return getItemTypeImpl(itemPath); //throw SysErrorCode + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(itemPath)), e.toString()); } +} + + +std::optional zen::getItemTypeIfExists(const Zstring& itemPath) //throw FileError +{ + try + { + const std::variant typeOrPath = getItemTypeIfExistsImpl(itemPath); //throw SysError + if (const ItemType* type = std::get_if(&typeOrPath)) + return *type; + else + return std::nullopt; + } + catch (const SysError& e) + { + throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(itemPath)), e.toString()); + } +} + + +namespace +{ +} + + +//- symlink handling: follow +//- returns < 0 if not available +//- folderPath does not need to exist (yet) +int64_t zen::getFreeDiskSpace(const Zstring& folderPath) //throw FileError +{ + try + { + const Zstring existingPath = [&] + { + const std::variant typeOrPath = getItemTypeIfExistsImpl(folderPath); //throw SysError + if (std::get_if(&typeOrPath)) + return folderPath; + else + return std::get(typeOrPath); + }(); + struct statfs info = {}; + if (::statfs(existingPath.c_str(), &info) != 0) //follows symlinks! + THROW_LAST_SYS_ERROR("statfs"); + //Linux: "Fields that are undefined for a particular file system are set to 0." + //macOS: "Fields that are undefined for a particular file system are set to -1." - mkay :> + if (makeSigned(info.f_bsize) <= 0 || + makeSigned(info.f_bavail) <= 0) + return -1; + + return static_cast(info.f_bsize) * info.f_bavail; + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot determine free disk space for %x."), L"%x", fmtPath(folderPath)), e.toString()); } +} + + +uint64_t zen::getFileSize(const Zstring& filePath) //throw FileError +{ + try + { + struct stat fileInfo = {}; + if (::stat(filePath.c_str(), &fileInfo) != 0) + THROW_LAST_SYS_ERROR("stat"); + + return fileInfo.st_size; + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(filePath)), e.toString()); } +} + + +Zstring zen::getTempFolderPath() //throw FileError +{ + if (const std::optional tempDirPath = getEnvironmentVar("TMPDIR")) + return *tempDirPath; + //TMPDIR not set on CentOS 7, WTF! + return P_tmpdir; //usually resolves to "/tmp" +} + + + +namespace +{ +} + + +void zen::removeFilePlain(const Zstring& filePath) //throw FileError +{ + try + { + if (::unlink(filePath.c_str()) != 0) + THROW_LAST_SYS_ERROR("unlink"); + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot delete file %x."), L"%x", fmtPath(filePath)), e.toString()); } +} + + + +void zen::removeDirectoryPlain(const Zstring& dirPath) //throw FileError +{ + try + { + if (::rmdir(dirPath.c_str()) != 0) + THROW_LAST_SYS_ERROR("rmdir"); + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot delete directory %x."), L"%x", fmtPath(dirPath)), e.toString()); } +} + + +void zen::removeSymlinkPlain(const Zstring& linkPath) //throw FileError +{ + try + { + if (::unlink(linkPath.c_str()) != 0) + THROW_LAST_SYS_ERROR("unlink"); + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot delete symbolic link %x."), L"%x", fmtPath(linkPath)), e.toString()); } +} + + +namespace +{ +void removeDirectoryImpl(const Zstring& folderPath) //throw FileError +{ + std::vector folderPaths; + { + std::vector filePaths; + std::vector symlinkPaths; + + //get all files and directories from current directory (WITHOUT subdirectories!) + traverseFolder(folderPath, + [&](const FileInfo& fi) { filePaths.push_back(fi.fullPath); }, + [&](const FolderInfo& fi) { folderPaths.push_back(fi.fullPath); }, + [&](const SymlinkInfo& si) { symlinkPaths.push_back(si.fullPath); }); //throw FileError + + for (const Zstring& filePath : filePaths) + removeFilePlain(filePath); //throw FileError + + for (const Zstring& symlinkPath : symlinkPaths) + removeSymlinkPlain(symlinkPath); //throw FileError + } //=> save stack space and allow deletion of extremely deep hierarchies! + + //delete directories recursively + for (const Zstring& subFolderPath : folderPaths) + removeDirectoryImpl(subFolderPath); //throw FileError; call recursively to correctly handle symbolic links + + removeDirectoryPlain(folderPath); //throw FileError +} +} + + +void zen::removeDirectoryPlainRecursion(const Zstring& dirPath) //throw FileError +{ + try + { + if (getItemTypeImpl(dirPath) == ItemType::symlink) //throw SysErrorCode + removeSymlinkPlain(dirPath); //throw FileError + else + removeDirectoryImpl(dirPath); //throw FileError + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot delete directory %x."), L"%x", fmtPath(dirPath)), e.toString()); } +} + + +namespace +{ +std::wstring generateMoveErrorMsg(const Zstring& pathFrom, const Zstring& pathTo) +{ + if (getParentFolderPath(pathFrom) == getParentFolderPath(pathTo)) //pure "rename" + return replaceCpy(replaceCpy(_("Cannot rename %x to %y."), + L"%x", fmtPath(pathFrom)), + L"%y", fmtPath(getItemName(pathTo))); + else //"move" or "move + rename" + return trimCpy(replaceCpy(replaceCpy(_("Cannot move %x to %y."), + L"%x", L'\n' + fmtPath(pathFrom)), + L"%y", L'\n' + fmtPath(pathTo))); +} + +/* Usage overview: (avoid circular pattern!) + + moveAndRenameItem() --> moveAndRenameFileSub() + | /|\ + \|/ | + Fix8Dot3NameClash() */ + +//wrapper for file system rename function: +void moveAndRenameFileSub(const Zstring& pathFrom, const Zstring& pathTo, bool replaceExisting) //throw FileError, ErrorMoveUnsupported, ErrorTargetExisting +{ + auto getErrorMsg = [&] { return generateMoveErrorMsg(pathFrom, pathTo); }; + + //rename() will never fail with EEXIST, but always (atomically) overwrite! + //=> equivalent to SetFileInformationByHandle() + FILE_RENAME_INFO::ReplaceIfExists or ::MoveFileEx() + MOVEFILE_REPLACE_EXISTING + //Linux: renameat2() with RENAME_NOREPLACE -> still new, probably buggy + //macOS: no solution https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man2/rename.2.html + if (!replaceExisting) + { + struct stat sourceInfo = {}; + if (::lstat(pathFrom.c_str(), &sourceInfo) != 0) + throw FileError(getErrorMsg(), formatSystemError("lstat(source)", errno)); + + struct stat targetInfo = {}; + if (::lstat(pathTo.c_str(), &targetInfo) != 0) + { + if (errno != ENOENT) + throw FileError(getErrorMsg(), formatSystemError("lstat(target)", errno)); + } + else + { + if (sourceInfo.st_dev != targetInfo.st_dev || + sourceInfo.st_ino != targetInfo.st_ino) + throw ErrorTargetExisting(getErrorMsg(), replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(getItemName(pathTo)))); + //else: continue with a rename in case + //caveat: if we have a hardlink referenced by two different paths, the source one will be unlinked => fine, but not exactly a "rename"... + } + } + + if (::rename(pathFrom.c_str(), pathTo.c_str()) != 0) + { + const int ec = errno; //copy before making other system calls! + std::wstring errorDescr = formatSystemError("rename", ec); + + if (ec == EXDEV) + throw ErrorMoveUnsupported(getErrorMsg(), errorDescr); + + if (ec == EINVAL) + for (const Zchar c : getItemName(pathTo)) + if (contains(fileNameForbiddenChars, c)) + { + errorDescr += L' ' + replaceCpy(_("Unsupported character %x"), L"%x", L"'" + utfTo(c) + L"'"); + break; + } + + throw FileError(getErrorMsg(), errorDescr); + } +} + + +} + + +//rename file: no copying!!! +void zen::moveAndRenameItem(const Zstring& pathFrom, const Zstring& pathTo, bool replaceExisting) //throw FileError, ErrorMoveUnsupported, ErrorTargetExisting +{ + try + { + moveAndRenameFileSub(pathFrom, pathTo, replaceExisting); //throw FileError, ErrorMoveUnsupported, ErrorTargetExisting + } + catch (ErrorTargetExisting&) + { + throw; + } +} + +namespace +{ +void setWriteTimeNative(const Zstring& itemPath, const timespec& modTime, ProcSymlink procSl) //throw FileError +{ + /* [2013-05-01] sigh, we can't use utimensat() on NTFS volumes on Ubuntu: silent failure!!! what morons are programming this shit??? + => fallback to "retarded-idiot version"! -- DarkByte + + [2015-03-09] + - cannot reproduce issues with NTFS and utimensat() on Ubuntu + - utimensat() is supposed to obsolete utime/utimes and is also used by "cp" and "touch" + => let's give utimensat another chance: + using open()/futimens() for regular files and utimensat(AT_SYMLINK_NOFOLLOW) for symlinks is consistent with "cp" and "touch"! + cp: https://github.com/coreutils/coreutils/blob/master/src/cp.c + => utimens: https://github.com/coreutils/gnulib/blob/master/lib/utimens.c + touch: https://github.com/coreutils/coreutils/blob/master/src/touch.c + => fdutimensat: https://github.com/coreutils/gnulib/blob/master/lib/fdutimensat.c */ + const timespec newTimes[2] + { + {.tv_sec = ::time(nullptr)}, //access time; don't use UTIME_NOW/UTIME_OMIT: more bugs! https://freefilesync.org/forum/viewtopic.php?t=1701 + modTime, + }; + //test: even modTime == 0 is correctly applied (no NOOP!) test2: same behavior for "utime()" + + //hell knows why files on gvfs-mounted Samba shares fail to open(O_WRONLY) returning EOPNOTSUPP: + //https://freefilesync.org/forum/viewtopic.php?t=2803 => utimensat() works (but not for gvfs SFTP) + if (::utimensat(AT_FDCWD /*'dirfd' ignored for absolute paths*/, itemPath.c_str(), newTimes, procSl == ProcSymlink::asLink ? AT_SYMLINK_NOFOLLOW : 0) == 0) + return; + const ErrorCode ecUtimensat = errno; + try + { + if (procSl == ProcSymlink::asLink) + { + if (getItemTypeImpl(itemPath) == ItemType::symlink) //throw SysErrorCode + throw SysError(formatSystemError("utimensat(AT_SYMLINK_NOFOLLOW)", ecUtimensat)); //use lutimes()? just a wrapper around utimensat()! + //else: fall back + } + + //in other cases utimensat() returns EINVAL for CIFS/NTFS drives, but open+futimens works: https://freefilesync.org/forum/viewtopic.php?t=387 + //2017-07-04: O_WRONLY | O_APPEND seems to avoid EOPNOTSUPP on gvfs SFTP! + const int fdFile = ::open(itemPath.c_str(), O_WRONLY | O_APPEND | O_CLOEXEC); + if (fdFile == -1) + THROW_LAST_SYS_ERROR("open"); + ZEN_ON_SCOPE_EXIT(::close(fdFile)); + + if (::futimens(fdFile, newTimes) != 0) + THROW_LAST_SYS_ERROR("futimens"); + + //need more fallbacks? e.g. futimes()? careful, bugs! futimes() rounds instead of truncates when falling back on utime()! + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot write modification time of %x."), L"%x", fmtPath(itemPath)), e.toString()); } +} + + +} + + +void zen::setFileTime(const Zstring& filePath, time_t modTime, ProcSymlink procSl) //throw FileError +{ + setWriteTimeNative(filePath, timetToNativeFileTime(modTime), + procSl); //throw FileError +} + + +bool zen::supportsPermissions(const Zstring& dirPath) //throw FileError +{ + return true; +} + + +namespace +{ +#ifdef HAVE_SELINUX +//copy SELinux security context +void copySecurityContext(const Zstring& source, const Zstring& target, ProcSymlink procSl) //throw FileError +{ + security_context_t contextSource = nullptr; + const int rv = procSl == ProcSymlink::follow ? + ::getfilecon (source.c_str(), &contextSource) : + ::lgetfilecon(source.c_str(), &contextSource); + if (rv < 0) + { + if (errno == ENODATA || //no security context (allegedly) is not an error condition on SELinux + errno == EOPNOTSUPP) //extended attributes are not supported by the filesystem + return; + + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read security context of %x."), L"%x", fmtPath(source)), "getfilecon"); + } + ZEN_ON_SCOPE_EXIT(::freecon(contextSource)); + + { + security_context_t contextTarget = nullptr; + const int rv2 = procSl == ProcSymlink::follow ? + ::getfilecon(target.c_str(), &contextTarget) : + ::lgetfilecon(target.c_str(), &contextTarget); + if (rv2 < 0) + { + if (errno == EOPNOTSUPP) + return; + //else: still try to set security context + } + else + { + ZEN_ON_SCOPE_EXIT(::freecon(contextTarget)); + + if (::strcmp(contextSource, contextTarget) == 0) //nothing to do + return; + } + } + + const int rv3 = procSl == ProcSymlink::follow ? + ::setfilecon(target.c_str(), contextSource) : + ::lsetfilecon(target.c_str(), contextSource); + if (rv3 < 0) + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot write security context of %x."), L"%x", fmtPath(target)), "setfilecon"); +} +#endif +} + + +//copy permissions for files, directories or symbolic links: requires admin rights +void zen::copyItemPermissions(const Zstring& sourcePath, const Zstring& targetPath, ProcSymlink procSl) //throw FileError +{ + +#ifdef HAVE_SELINUX //copy SELinux security context + copySecurityContext(sourcePath, targetPath, procSl); //throw FileError +#endif + + struct stat fileInfo = {}; + if (procSl == ProcSymlink::follow) + { + if (::stat(sourcePath.c_str(), &fileInfo) != 0) + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read permissions of %x."), L"%x", fmtPath(sourcePath)), "stat"); + + if (::chown(targetPath.c_str(), fileInfo.st_uid, fileInfo.st_gid) != 0) // may require admin rights! + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(targetPath)), "chown"); + + if (::chmod(targetPath.c_str(), fileInfo.st_mode) != 0) + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(targetPath)), "chmod"); + } + else + { + if (::lstat(sourcePath.c_str(), &fileInfo) != 0) + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read permissions of %x."), L"%x", fmtPath(sourcePath)), "lstat"); + + if (::lchown(targetPath.c_str(), fileInfo.st_uid, fileInfo.st_gid) != 0) // may require admin rights! + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(targetPath)), "lchown"); + + try + { + if (getItemTypeImpl(targetPath) != ItemType::symlink && //throw SysErrorCode + //setting access permissions doesn't make sense for symlinks on Linux: there is no lchmod() + ::chmod(targetPath.c_str(), fileInfo.st_mode) != 0) + THROW_LAST_SYS_ERROR("chmod"); + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(targetPath)), e.toString()); } + } + +} + + +void zen::createDirectory(const Zstring& dirPath) //throw FileError, ErrorTargetExisting +{ + try + { + //don't allow creating irregular folders! + const Zstring dirName = getItemName(dirPath); + + //e.g. "...." https://social.technet.microsoft.com/Forums/windows/en-US/ffee2322-bb6b-4fdf-86f9-8f93cf1fa6cb/ + if (std::all_of(dirName.begin(), dirName.end(), [](Zchar c) { return c == Zstr('.'); })) + /**/throw SysError(replaceCpy(L"Invalid folder name %x.", L"%x", fmtPath(dirName))); + +#if 0 //not appreciated: https://freefilesync.org/forum/viewtopic.php?t=7509 + if (startsWith(dirName, Zstr(' ')) || //Windows can access these just fine once created! + endsWith (dirName, Zstr(' '))) // + throw SysError(replaceCpy(L"Invalid folder name %x starts/ends with space character.", L"%x", fmtPath(dirName))); +#endif + + const mode_t mode = S_IRWXU | S_IRWXG | S_IRWXO; //0777 => consider umask! + + if (::mkdir(dirPath.c_str(), mode) != 0) + { + const int ec = errno; //copy before making other system calls! + std::wstring errorDescr = formatSystemError("mkdir", ec); + + if (ec == EEXIST) + throw ErrorTargetExisting(replaceCpy(_("Cannot create directory %x."), L"%x", fmtPath(dirPath)), errorDescr); + + if (ec == EINVAL) + for (const Zchar c : getItemName(dirPath)) + if (contains(fileNameForbiddenChars, c)) + { + errorDescr += L' ' + replaceCpy(_("Unsupported character %x"), L"%x", L"'" + utfTo(c) + L"'"); + break; + } + + throw SysError(errorDescr); + } + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot create directory %x."), L"%x", fmtPath(dirPath)), e.toString()); } +} + + +void zen::createDirectoryIfMissingRecursion(const Zstring& dirPath) //throw FileError +{ + auto getItemType2 = [&](const Zstring& itemPath) //throw FileError + { + try + { return getItemTypeImpl(itemPath); } //throw SysErrorCode + catch (const SysErrorCode& e) //need to add context! + { + throw FileError(replaceCpy(_("Cannot create directory %x."), L"%x", fmtPath(dirPath)), + replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(getParentFolderPath(itemPath) ? getItemName(itemPath) : itemPath)) + L'\n' + + e.toString()); + } + }; + + try + { + //- path most likely already exists (see: versioning, base folder, log file path) => check first + //- do NOT use getItemTypeIfExists()! race condition when multiple threads are calling createDirectoryIfMissingRecursion(): https://freefilesync.org/forum/viewtopic.php?t=10137#p38062 + //- find first existing + accessible parent folder (backwards iteration): + Zstring dirPathEx = dirPath; + RingBuffer dirNames; //caveat: 1. might have been created in the meantime 2. getItemType2() may have failed with access error + for (;;) + try + { + if (getItemType2(dirPathEx) == ItemType::file /*obscure, but possible*/) //throw FileError + throw SysError(replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(getItemName(dirPathEx)))); + break; + } + catch (FileError&) //not yet existing or access error + { + const std::optional& parentPath = getParentFolderPath(dirPathEx); + if (!parentPath)//device root => quick access test + throw; + dirNames.push_front(getItemName(dirPathEx)); + dirPathEx = *parentPath; + } + //----------------------------------------------------------- + + Zstring dirPathNew = dirPathEx; + for (const Zstring& dirName : dirNames) + try + { + dirPathNew = appendPath(dirPathNew, dirName); + + createDirectory(dirPathNew); //throw FileError + } + catch (FileError&) + { + try + { + if (getItemType2(dirPathNew) == ItemType::file /*obscure, but possible*/) //throw FileError + throw SysError(replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(getItemName(dirPathNew)))); + else + continue; //already existing => possible, if createDirectoryIfMissingRecursion() is run in parallel + } + catch (FileError&) {} //not yet existing or access error + + throw; + } + } + catch (const SysError& e) + { + throw FileError(replaceCpy(_("Cannot create directory %x."), L"%x", fmtPath(dirPath)), e.toString()); + } +} + + +void zen::copyDirectoryAttributes(const Zstring& sourcePath, const Zstring& targetPath) //throw FileError +{ + //do NOT copy attributes for volume root paths which return as: FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM | FILE_ATTRIBUTE_DIRECTORY + //https://freefilesync.org/forum/viewtopic.php?t=5550 + if (!getParentFolderPath(sourcePath)) //=> root path + return; + +} + + +void zen::copySymlink(const Zstring& sourcePath, const Zstring& targetPath) //throw FileError +{ + SymlinkRawContent linkContent{}; + try //harmonize with NativeFileSystem::equalSymlinkContentForSameAfsType() + { + linkContent = getSymlinkRawContent_impl(sourcePath); //throw SysError; accept broken symlinks + + if (::symlink(linkContent.targetPath.c_str(), targetPath.c_str()) != 0) + THROW_LAST_SYS_ERROR("symlink"); + } + catch (const SysError& e) + { + throw FileError(replaceCpy(replaceCpy(_("Cannot copy symbolic link %x to %y."), L"%x", L'\n' + fmtPath(sourcePath)), L"%y", L'\n' + fmtPath(targetPath)), e.toString()); + } + + //allow only consistent objects to be created -> don't place before ::symlink(); targetPath may already exist! + ZEN_ON_SCOPE_FAIL(try { removeSymlinkPlain(targetPath); } + catch (const FileError& e) { logExtraError(e.toString()); }); + + //file times: essential for syncing a symlink: enforce this! (don't just try!) + struct stat sourceInfo = {}; + if (::lstat(sourcePath.c_str(), &sourceInfo) != 0) + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(sourcePath)), "lstat"); + + setWriteTimeNative(targetPath, sourceInfo.st_mtim, ProcSymlink::asLink); //throw FileError +} + + +FileCopyResult zen::copyNewFile(const Zstring& sourceFile, const Zstring& targetFile, //throw FileError, ErrorTargetExisting, (ErrorFileLocked), X + const IoCallback& notifyUnbufferedIO /*throw X*/) +{ + int64_t totalBytesNotified = 0; + IOCallbackDivider notifyIoDiv(notifyUnbufferedIO, totalBytesNotified); + + FileInputPlain fileIn(sourceFile); //throw FileError, (ErrorFileLocked -> Windows-only) + + const struct stat& sourceInfo = fileIn.getStatBuffered(); //throw FileError + + //analog to "cp" which copies "mode" (considering umask) by default: + const mode_t mode = (sourceInfo.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO)) | + S_IWUSR;//macOS: S_IWUSR apparently needed to write extended attributes (see copyfile() function) + //Linux: not needed even for the setFileTime() below! (tested with source file having different user/group!) + + //=> need copyItemPermissions() only for "chown" and umask-agnostic permissions + const int fdTarget = ::open(targetFile.c_str(), O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC, mode); + if (fdTarget == -1) + { + const int ec = errno; //copy before making other system calls! + const std::wstring errorMsg = replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(targetFile)); + std::wstring errorDescr = formatSystemError("open", ec); + + if (ec == EEXIST) + throw ErrorTargetExisting(errorMsg, errorDescr); + + if (ec == EINVAL) + for (const Zchar c : getItemName(targetFile)) + if (contains(fileNameForbiddenChars, c)) + { + errorDescr += L' ' + replaceCpy(_("Unsupported character %x"), L"%x", L"'" + utfTo(c) + L"'"); + break; + } + + throw FileError(errorMsg, errorDescr); + } + FileOutputPlain fileOut(fdTarget, targetFile); //pass ownership + + //preallocate disk space + reduce fragmentation + fileOut.reserveSpace(sourceInfo.st_size); //throw FileError + + unbufferedStreamCopy([&](void* buffer, size_t bytesToRead) + { + const size_t bytesRead = fileIn.tryRead(buffer, bytesToRead); //throw FileError, (ErrorFileLocked) + notifyIoDiv(bytesRead); //throw X + return bytesRead; + }, + fileIn.getBlockSize() /*throw FileError*/, + + [&](const void* buffer, size_t bytesToWrite) + { + const size_t bytesWritten = fileOut.tryWrite(buffer, bytesToWrite); //throw FileError + notifyIoDiv(bytesWritten); //throw X + return bytesWritten; + }, + fileOut.getBlockSize() /*throw FileError*/); //throw FileError, X + + //possible improvement: copy_file_range() performs an in-kernel copy: https://github.com/coreutils/coreutils/blob/17479ef60c8edbd2fe8664e31a7f69704f0cd221/src/copy.c#L342 + +#if 0 + //clean file system cache: needed at all? no user complaints at all so far!!! + //posix_fadvise(POSIX_FADV_DONTNEED) does nothing, unless data was already read from/written to disk: https://insights.oetiker.ch/linux/fadvise/ + // => should be "most" of the data at this point => good enough? + if (::posix_fadvise(fileIn.getHandle(), 0 /*offset*/, 0 /*len*/, POSIX_FADV_DONTNEED) != 0) //"len == 0" means "end of the file" + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(sourceFile)), "posix_fadvise(POSIX_FADV_DONTNEED)"); + if (::posix_fadvise(fileOut.getHandle(), 0 /*offset*/, 0 /*len*/, POSIX_FADV_DONTNEED) != 0) //"len == 0" means "end of the file" + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(targetFile)), "posix_fadvise(POSIX_FADV_DONTNEED)"); +#endif + + + const auto targetFileIdx = fileOut.getStatBuffered().st_ino; //throw FileError + + //close output file handle before setting file time; also good place to catch errors when closing stream! + fileOut.close(); //throw FileError + //========================================================================================================== + //take over fileOut ownership => from this point on, WE are responsible for calling removeFilePlain() on error!! + // not needed *currently*! see below: ZEN_ON_SCOPE_FAIL(try { removeFilePlain(targetFile); } catch (FileError&) {}); + //=========================================================================================================== + std::optional errorModTime; + try + { + /* we cannot set the target file times (::futimes) while the file descriptor is still open after a write operation: + this triggers bugs on Samba shares where the modification time is set to current time instead. + Linux: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=340236 + http://comments.gmane.org/gmane.linux.file-systems.cifs/2854 + macOS: https://freefilesync.org/forum/viewtopic.php?t=356 */ + setWriteTimeNative(targetFile, sourceInfo.st_mtim, ProcSymlink::follow); //throw FileError + } + catch (const FileError& e) { errorModTime = e; /*might slice derived class?*/ } + + return + { + .fileSize = makeUnsigned(sourceInfo.st_size), + .sourceModTime = sourceInfo.st_mtim, + .sourceFileIdx = sourceInfo.st_ino, + .targetFileIdx = targetFileIdx, + .errorModTime = errorModTime, + }; +} + + diff --git a/zen/file_access.h b/zen/file_access.h new file mode 100644 index 0000000..42ee06c --- /dev/null +++ b/zen/file_access.h @@ -0,0 +1,99 @@ +// ***************************************************************************** +// * 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 FILE_ACCESS_H_8017341345614857 +#define FILE_ACCESS_H_8017341345614857 + +#include "file_path.h" //we'll need this later anyway! +#include "file_error.h" +#include "serialize.h" //IoCallback + #include + +namespace zen +{ +//note: certain functions require COM initialization! (vista_file_op.h) + +//FAT/FAT32: "Why does the timestamp of a file *increase* by up to 2 seconds when I copy it to a USB thumb drive?" +const int FAT_FILE_TIME_PRECISION_SEC = 2; //https://devblogs.microsoft.com/oldnewthing/?p=83 +//https://web.archive.org/web/20141127143832/http://support.microsoft.com/kb/127830 + +using FileIndex = ino_t; +using FileTimeNative = timespec; + +inline time_t nativeFileTimeToTimeT(const timespec& ft) { return ft.tv_sec; } //follow File Explorer: always round down! +inline timespec timetToNativeFileTime(time_t utcTime) { return {.tv_sec = utcTime}; } + +enum class ItemType +{ + file, + folder, + symlink, +}; +//(hopefully) fast: does not distinguish between error/not existing +ItemType getItemType(const Zstring& itemPath); //throw FileError +//execute potentially SLOW folder traversal but distinguish error/not existing: +// - all child item path parts must correspond to folder traversal +// => we can conclude whether an item is *not* existing anymore by doing a *case-sensitive* name search => potentially SLOW! +std::optional getItemTypeIfExists(const Zstring& itemPath); //throw FileError + +inline bool itemExists(const Zstring& itemPath) { return static_cast(getItemTypeIfExists(itemPath)); } //throw FileError + +enum class ProcSymlink +{ + asLink, + follow +}; +void setFileTime(const Zstring& filePath, time_t modTime, ProcSymlink procSl); //throw FileError + + +int64_t getFreeDiskSpace(const Zstring& folderPath); //throw FileError, returns < 0 if not available +//- symlink handling: follow +//- returns < 0 if not available +//- folderPath does not need to exist (yet) + +//symlink handling: follow +uint64_t getFileSize(const Zstring& filePath); //throw FileError + +//get per-user directory designated for temporary files: +Zstring getTempFolderPath(); //throw FileError + +void removeFilePlain (const Zstring& filePath); //throw FileError; ERROR if not existing +void removeSymlinkPlain (const Zstring& linkPath); //throw FileError; ERROR if not existing +void removeDirectoryPlain(const Zstring& dirPath ); //throw FileError; ERROR if not existing +void removeDirectoryPlainRecursion(const Zstring& dirPath); //throw FileError; ERROR if not existing + +void moveAndRenameItem(const Zstring& pathFrom, const Zstring& pathTo, bool replaceExisting); //throw FileError, ErrorMoveUnsupported, ErrorTargetExisting + +bool supportsPermissions(const Zstring& dirPath); //throw FileError, follows symlinks +//copy permissions for files, directories or symbolic links: requires admin rights +void copyItemPermissions(const Zstring& sourcePath, const Zstring& targetPath, ProcSymlink procSl); //throw FileError + +void createDirectory(const Zstring& dirPath); //throw FileError, ErrorTargetExisting + +//creates directories recursively if not existing +void createDirectoryIfMissingRecursion(const Zstring& dirPath); //throw FileError + +//symlink handling: follow +//expects existing source/target directories +void copyDirectoryAttributes(const Zstring& sourcePath, const Zstring& targetPath); //throw FileError + +void copySymlink(const Zstring& sourcePath, const Zstring& targetPath); //throw FileError + +struct FileCopyResult +{ + uint64_t fileSize = 0; + FileTimeNative sourceModTime = {}; + FileIndex sourceFileIdx = 0; + FileIndex targetFileIdx = 0; + std::optional errorModTime; //failure to set modification time +}; + +FileCopyResult copyNewFile(const Zstring& sourceFile, const Zstring& targetFile, //throw FileError, ErrorTargetExisting, ErrorFileLocked, X + //accummulated delta != file size! consider ADS, sparse, compressed files + const IoCallback& notifyUnbufferedIO /*throw X*/); +} + +#endif //FILE_ACCESS_H_8017341345614857 diff --git a/zen/file_error.h b/zen/file_error.h new file mode 100644 index 0000000..93c95f9 --- /dev/null +++ b/zen/file_error.h @@ -0,0 +1,50 @@ +// ***************************************************************************** +// * 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 FILE_ERROR_H_839567308565656789 +#define FILE_ERROR_H_839567308565656789 + +#include "sys_error.h" //we'll need this later anyway! + + +namespace zen +{ +class FileError //A high-level exception class giving detailed context information for end users +{ +public: + explicit FileError(const std::wstring& msg) : msg_(msg) {} + FileError(const std::wstring& msg, const std::wstring& details) : msg_(msg + L"\n\n" + details) {} + virtual ~FileError() {} + + const std::wstring& toString() const { return msg_; } + +private: + std::wstring msg_; +}; + +#define DEFINE_NEW_FILE_ERROR(X) struct X : public zen::FileError { X(const std::wstring& msg) : FileError(msg) {} X(const std::wstring& msg, const std::wstring& descr) : FileError(msg, descr) {} }; + +DEFINE_NEW_FILE_ERROR(ErrorTargetExisting) +DEFINE_NEW_FILE_ERROR(ErrorFileLocked) +DEFINE_NEW_FILE_ERROR(ErrorMoveUnsupported) +DEFINE_NEW_FILE_ERROR(RecycleBinUnavailable) + + +//CAVEAT: thread-local Win32 error code is easily overwritten => evaluate *before* making any (indirect) system calls: +//-> MinGW + Win XP: "throw" statement allocates memory to hold the exception object => error code is cleared +//-> VC 2015, Debug: std::wstring allocator internally calls ::FlsGetValue() => error code is cleared +// https://connect.microsoft.com/VisualStudio/feedback/details/1775690/calling-operator-new-may-set-lasterror-to-0 +#define THROW_LAST_FILE_ERROR(msg, functionName) \ + do { const ErrorCode ecInternal = getLastError(); throw FileError(msg, formatSystemError(functionName, ecInternal)); } while (false) + +//----------- facilitate usage of std::wstring for error messages -------------------- + +inline std::wstring fmtPath(const std::wstring& displayPath) { return L'"' + displayPath + L'"'; } +inline std::wstring fmtPath(const Zstring& displayPath) { return fmtPath(utfTo(displayPath)); } +inline std::wstring fmtPath(const wchar_t* displayPath) { return fmtPath(std::wstring(displayPath)); } //resolve overload ambiguity +} + +#endif //FILE_ERROR_H_839567308565656789 diff --git a/zen/file_io.cpp b/zen/file_io.cpp new file mode 100644 index 0000000..0a119b4 --- /dev/null +++ b/zen/file_io.cpp @@ -0,0 +1,345 @@ +// ***************************************************************************** +// * 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 "file_io.h" + #include + #include //open + #include //close, read, write + +using namespace zen; + + +size_t FileBase::getBlockSize() //throw FileError +{ + if (blockSizeBuf_ == 0) + { + /* - statfs::f_bsize - "optimal transfer block size" + - stat::st_blksize - "blocksize for file system I/O. Writing in smaller chunks may cause an inefficient read-modify-rewrite." + + e.g. local disk: f_bsize 4096 st_blksize 4096 + USB memory: f_bsize 32768 st_blksize 32768 */ + const auto st_blksize = getStatBuffered().st_blksize; //throw FileError + if (st_blksize > 0) //st_blksize is signed! + blockSizeBuf_ = st_blksize; // + + blockSizeBuf_ = std::max(blockSizeBuf_, defaultBlockSize); + //ha, convergent evolution! https://github.com/coreutils/coreutils/blob/master/src/ioblksize.h#L74 + } + return blockSizeBuf_; +} + + +const struct stat& FileBase::getStatBuffered() //throw FileError +{ + if (!statBuf_) + try + { + if (hFile_ == invalidFileHandle) + throw SysError(L"Contract error: getStatBuffered() called after close()."); + + struct stat fileInfo = {}; + if (::fstat(hFile_, &fileInfo) != 0) + THROW_LAST_SYS_ERROR("fstat"); + statBuf_ = std::move(fileInfo); + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(filePath_)), e.toString()); } + + return *statBuf_; +} + + +FileBase::~FileBase() +{ + if (hFile_ != invalidFileHandle) + try + { + close(); //throw FileError + } + catch (const FileError& e) { logExtraError(e.toString()); } +} + + +void FileBase::close() //throw FileError +{ + try + { + if (hFile_ == invalidFileHandle) + throw SysError(L"Contract error: close() called more than once."); + if (::close(hFile_) != 0) + THROW_LAST_SYS_ERROR("close"); + hFile_ = invalidFileHandle; //do NOT set on error! => ~FileOutputPlain() still wants to (try to) delete the file! + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(getFilePath())), e.toString()); } +} + +//---------------------------------------------------------------------------------------------------- + +namespace +{ + std::pair +openHandleForRead(const Zstring& filePath) //throw FileError, ErrorFileLocked +{ + try + { + //caveat: check for file types that block during open(): character device, block device, named pipe + struct stat fileInfo = {}; + if (::stat(filePath.c_str(), &fileInfo) != 0) //follows symlinks + THROW_LAST_SYS_ERROR("stat"); + + if (!S_ISREG(fileInfo.st_mode) && + !S_ISDIR(fileInfo.st_mode) && //open() will fail with "EISDIR: Is a directory" => nice + !S_ISLNK(fileInfo.st_mode)) //?? shouldn't be possible after successful stat() + { + const std::wstring typeName = [m = fileInfo.st_mode] + { + std::wstring name = + S_ISCHR (m) ? L"character device" : //e.g. /dev/null + S_ISBLK (m) ? L"block device" : //e.g. /dev/sda1 + S_ISFIFO(m) ? L"FIFO, named pipe" : + S_ISSOCK(m) ? L"socket" : L""; //doesn't block but open() error is unclear: "ENXIO: No such device or address" + if (!name.empty()) + name += L", "; + return name + printNumber(L"0%06o", m & S_IFMT); + }(); + throw SysError(_("Unsupported item type.") + L" [" + typeName + L']'); + } + + //don't use O_DIRECT: https://yarchive.net/comp/linux/o_direct.html + const int fdFile = ::open(filePath.c_str(), O_RDONLY | O_CLOEXEC); + if (fdFile == -1) //don't check "< 0" -> docu seems to allow "-2" to be a valid file handle + THROW_LAST_SYS_ERROR("open"); + return {fdFile /*pass ownership*/, fileInfo}; + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot open file %x."), L"%x", fmtPath(filePath)), e.toString()); } +} +} + + +FileInputPlain::FileInputPlain(const Zstring& filePath) : + FileInputPlain(openHandleForRead(filePath), filePath) {} //throw FileError, ErrorFileLocked + + +FileInputPlain::FileInputPlain(const std::pair& fileDetails, const Zstring& filePath) : + FileInputPlain(fileDetails.first, filePath) +{ + setStatBuffered(fileDetails.second); +} + + +FileInputPlain::FileInputPlain(FileHandle handle, const Zstring& filePath) : + FileBase(handle, filePath) +{ + //optimize read-ahead on input file: + if (::posix_fadvise(getHandle(), 0 /*offset*/, 0 /*len*/, POSIX_FADV_SEQUENTIAL) != 0) //"len == 0" means "end of the file" + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(filePath)), "posix_fadvise(POSIX_FADV_SEQUENTIAL)"); + + /* - POSIX_FADV_SEQUENTIAL is like POSIX_FADV_NORMAL, but with twice the read-ahead buffer size + - POSIX_FADV_NOREUSE "since kernel 2.6.18 this flag is a no-op" WTF!? + - POSIX_FADV_DONTNEED may be used to clear the OS file system cache (offset and len must be page-aligned!) + => does nothing, unless data was already written to disk: https://insights.oetiker.ch/linux/fadvise/ + - POSIX_FADV_WILLNEED: issue explicit read-ahead; almost the same as readahead(), but with weaker error checking + https://unix.stackexchange.com/questions/681188/difference-between-posix-fadvise-and-readahead + + clear file system cache manually: sync; echo 3 > /proc/sys/vm/drop_caches */ + +} + + +//may return short, only 0 means EOF! => CONTRACT: bytesToRead > 0! +size_t FileInputPlain::tryRead(void* buffer, size_t bytesToRead) //throw FileError, ErrorFileLocked +{ + if (bytesToRead == 0) //"read() with a count of 0 returns zero" => indistinguishable from end of file! => check! + throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); + assert(bytesToRead % getBlockSize() == 0); + try + { + ssize_t bytesRead = 0; + do + { + bytesRead = ::read(getHandle(), buffer, bytesToRead); + } + while (bytesRead < 0 && errno == EINTR); //Compare copy_reg() in copy.c: ftp://ftp.gnu.org/gnu/coreutils/coreutils-8.23.tar.xz + //EINTR is not checked on macOS' copyfile: https://opensource.apple.com/source/copyfile/copyfile-173.40.2/copyfile.c.auto.html + //read() on macOS: https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man2/read.2.html + //if ::read is interrupted (EINTR) right in the middle, it will return successfully with "bytesRead < bytesToRead" + + if (bytesRead < 0) + THROW_LAST_SYS_ERROR("read"); + + ASSERT_SYSERROR(makeUnsigned(bytesRead) <= bytesToRead); //better safe than sorry + return bytesRead; //"zero indicates end of file" + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(getFilePath())), e.toString()); } +} + +//---------------------------------------------------------------------------------------------------- + +namespace +{ +FileBase::FileHandle openHandleForWrite(const Zstring& filePath) //throw FileError, ErrorTargetExisting +{ + try + { + //check for named pipe, etc.? not needed, open() + O_WRONLY should fail fast + + const mode_t lockFileMode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH; //0666 => umask will be applied implicitly! + + //O_EXCL contains a race condition on NFS file systems: https://linux.die.net/man/2/open + const int fdFile = ::open(filePath.c_str(), //const char* pathname + O_CREAT | //int flags + /*access == FileOutput::ACC_OVERWRITE ? O_TRUNC : */ O_EXCL | O_WRONLY | O_CLOEXEC, + lockFileMode); //mode_t mode + if (fdFile == -1) + { + const int ec = errno; //copy before making other system calls! + std::wstring errorDescr = formatSystemError("open", ec); + + if (ec == EEXIST) + throw ErrorTargetExisting(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(filePath)), errorDescr); + + if (ec == EINVAL) + for (const Zchar c : getItemName(filePath)) + if (contains(fileNameForbiddenChars, c)) + { + errorDescr += L' ' + replaceCpy(_("Unsupported character %x"), L"%x", L"'" + utfTo(c) + L"'"); + break; + } + + throw SysError(errorDescr); + } + return fdFile; //pass ownership + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(filePath)), e.toString()); } +} +} + + +FileOutputPlain::FileOutputPlain(const Zstring& filePath) : + FileOutputPlain(openHandleForWrite(filePath), filePath) {} //throw FileError, ErrorTargetExisting + + +FileOutputPlain::FileOutputPlain(FileHandle handle, const Zstring& filePath) : + FileBase(handle, filePath) +{ +} + + +FileOutputPlain::~FileOutputPlain() +{ + + if (getHandle() != invalidFileHandle) //not finalized => clean up garbage + try + { + //"deleting while handle is open" == FILE_FLAG_DELETE_ON_CLOSE + if (::unlink(getFilePath().c_str()) != 0) + THROW_LAST_SYS_ERROR("unlink"); + } + catch (const SysError& e) + { + logExtraError(replaceCpy(_("Cannot delete file %x."), L"%x", fmtPath(getFilePath())) + L"\n\n" + e.toString()); + } +} + + +void FileOutputPlain::reserveSpace(uint64_t expectedSize) //throw FileError +{ + //NTFS: "If you set the file allocation info [...] the file contents will be forced into nonresident data, even if it would have fit inside the MFT." + if (expectedSize < 1024) //https://docs.microsoft.com/en-us/archive/blogs/askcore/the-four-stages-of-ntfs-file-growth + return; + + try + { +#if 0 /* fallocate(FALLOC_FL_KEEP_SIZE): + - perf: no real benefit (in a quick and dirty local test) + - breaks Btrfs compression: https://freefilesync.org/forum/viewtopic.php?t=10356 + - apparently not even used by cp: https://github.com/coreutils/coreutils/blob/17479ef60c8edbd2fe8664e31a7f69704f0cd221/src/copy.c#LL1234C5-L1234C5 */ + + //don't use ::posix_fallocate which uses horribly inefficient fallback if FS doesn't support it (EOPNOTSUPP) and changes files size! + //FALLOC_FL_KEEP_SIZE => allocate only, file size is NOT changed! + if (::fallocate(getHandle(), //int fd + FALLOC_FL_KEEP_SIZE, //int mode + 0, //off_t offset + expectedSize) != 0) //off_t len + if (errno != EOPNOTSUPP) //possible, unlike with posix_fallocate() + THROW_LAST_SYS_ERROR("fallocate"); +#endif + + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(getFilePath())), e.toString()); } +} + + +//may return short! CONTRACT: bytesToWrite > 0 +size_t FileOutputPlain::tryWrite(const void* buffer, size_t bytesToWrite) //throw FileError +{ + if (bytesToWrite == 0) + throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); + assert(bytesToWrite % getBlockSize() == 0 || bytesToWrite < getBlockSize()); + try + { + ssize_t bytesWritten = 0; + do + { + bytesWritten = ::write(getHandle(), buffer, bytesToWrite); + } + while (bytesWritten < 0 && errno == EINTR); + //write() on macOS: https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man2/write.2.html + //if ::write() is interrupted (EINTR) right in the middle, it will return successfully with "bytesWritten < bytesToWrite"! + + if (bytesWritten <= 0) + { + if (bytesWritten == 0) //comment in safe-read.c suggests to treat this as an error due to buggy drivers + errno = ENOSPC; + + THROW_LAST_SYS_ERROR("write"); + } + + ASSERT_SYSERROR(makeUnsigned(bytesWritten) <= bytesToWrite); //better safe than sorry + return bytesWritten; + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(getFilePath())), e.toString()); } +} + +//---------------------------------------------------------------------------------------------------- + +std::string zen::getFileContent(const Zstring& filePath, const IoCallback& notifyUnbufferedIO /*throw X*/) //throw FileError, X +{ + FileInputPlain fileIn(filePath); //throw FileError, ErrorFileLocked + + return unbufferedLoad([&](void* buffer, size_t bytesToRead) + { + const size_t bytesRead = fileIn.tryRead(buffer, bytesToRead); //throw FileError; may return short, only 0 means EOF! => CONTRACT: bytesToRead > 0! + if (notifyUnbufferedIO) notifyUnbufferedIO(bytesRead); //throw X! + return bytesRead; + }, + fileIn.getBlockSize()); //throw FileError, X +} + + +void zen::setFileContent(const Zstring& filePath, const std::string_view byteStream, const IoCallback& notifyUnbufferedIO /*throw X*/) //throw FileError, X +{ + const Zstring tmpFilePath = getPathWithTempName(filePath); + + FileOutputPlain tmpFile(tmpFilePath); //throw FileError, (ErrorTargetExisting) + + tmpFile.reserveSpace(byteStream.size()); //throw FileError + + unbufferedSave(byteStream, [&](const void* buffer, size_t bytesToWrite) + { + const size_t bytesWritten = tmpFile.tryWrite(buffer, bytesToWrite); //throw FileError; may return short! CONTRACT: bytesToWrite > 0 + if (notifyUnbufferedIO) notifyUnbufferedIO(bytesWritten); //throw X! + return bytesWritten; + }, + tmpFile.getBlockSize()); //throw FileError, X + + tmpFile.close(); //throw FileError + //take over ownership: + ZEN_ON_SCOPE_FAIL( try { removeFilePlain(tmpFilePath); } + catch (const FileError& e) { logExtraError(e.toString()); }); + + //operation finished: move temp file transactionally + moveAndRenameItem(tmpFilePath, filePath, true /*replaceExisting*/); //throw FileError, (ErrorMoveUnsupported), (ErrorTargetExisting) +} diff --git a/zen/file_io.h b/zen/file_io.h new file mode 100644 index 0000000..838b502 --- /dev/null +++ b/zen/file_io.h @@ -0,0 +1,181 @@ +// ***************************************************************************** +// * 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 FILE_IO_H_89578342758342572345 +#define FILE_IO_H_89578342758342572345 + +#include "file_access.h" +#include "serialize.h" +#include "crc.h" +#include "guid.h" + + +namespace zen +{ + const char LINE_BREAK[] = "\n"; //since OS X Apple uses newline, too + +/* OS-buffered file I/O: + - sequential read/write accesses + - better error reporting + - long path support + - follows symlinks */ +class FileBase +{ +public: + using FileHandle = int; + static const int invalidFileHandle = -1; + + FileHandle getHandle() { return hFile_; } + + const Zstring& getFilePath() const { return filePath_; } + + size_t getBlockSize(); //throw FileError + + static constexpr size_t defaultBlockSize = 256 * 1024; + + void close(); //throw FileError -> good place to catch errors when closing stream, otherwise called in ~FileBase()! + + const struct stat& getStatBuffered(); //throw FileError + +protected: + FileBase(FileHandle handle, const Zstring& filePath) : hFile_(handle), filePath_(filePath) {} + ~FileBase(); + + void setStatBuffered(const struct stat& fileInfo) { statBuf_ = fileInfo; } + +private: + FileBase (const FileBase&) = delete; + FileBase& operator=(const FileBase&) = delete; + + FileHandle hFile_ = invalidFileHandle; + const Zstring filePath_; + size_t blockSizeBuf_ = 0; + std::optional statBuf_; +}; + +//----------------------------------------------------------------------------------------------- + +class FileInputPlain : public FileBase +{ +public: + FileInputPlain( const Zstring& filePath); //throw FileError, ErrorFileLocked + FileInputPlain(FileHandle handle, const Zstring& filePath); //takes ownership! + + //may return short, only 0 means EOF! CONTRACT: bytesToRead > 0! + size_t tryRead(void* buffer, size_t bytesToRead); //throw FileError, ErrorFileLocked + +private: + FileInputPlain(const std::pair& fileDetails, const Zstring& filePath); +}; + + +class FileOutputPlain : public FileBase +{ +public: + FileOutputPlain( const Zstring& filePath); //throw FileError, ErrorTargetExisting + FileOutputPlain(FileHandle handle, const Zstring& filePath); //takes ownership! + ~FileOutputPlain(); + + //preallocate disk space & reduce fragmentation + void reserveSpace(uint64_t expectedSize); //throw FileError + + //may return short! CONTRACT: bytesToWrite > 0 + size_t tryWrite(const void* buffer, size_t bytesToWrite); //throw FileError + + //close() when done, or else file is considered incomplete and will be deleted! + +private: +}; + +//-------------------------------------------------------------------- + +namespace impl +{ +inline +auto makeTryRead(FileInputPlain& fip, const IoCallback& notifyUnbufferedIO /*throw X*/) +{ + return [&](void* buffer, size_t bytesToRead) + { + const size_t bytesRead = fip.tryRead(buffer, bytesToRead); //throw FileError, ErrorFileLocked; may return short, only 0 means EOF! => CONTRACT: bytesToRead > 0! + if (notifyUnbufferedIO) notifyUnbufferedIO(bytesRead); //throw X + return bytesRead; + }; +} + + +inline +auto makeTryWrite(FileOutputPlain& fop, const IoCallback& notifyUnbufferedIO /*throw X*/) +{ + return [&](const void* buffer, size_t bytesToWrite) + { + const size_t bytesWritten = fop.tryWrite(buffer, bytesToWrite); //throw FileError + if (notifyUnbufferedIO) notifyUnbufferedIO(bytesWritten); //throw X + return bytesWritten; + }; +} +} + +//-------------------------------------------------------------------- + +class FileInputBuffered +{ +public: + FileInputBuffered(const Zstring& filePath, const IoCallback& notifyUnbufferedIO /*throw X*/) : //throw FileError, ErrorFileLocked + fileIn_(filePath), //throw FileError, ErrorFileLocked + notifyUnbufferedIO_(notifyUnbufferedIO) {} + + //return "bytesToRead" bytes unless end of stream! + size_t read(void* buffer, size_t bytesToRead) { return streamIn_.read(buffer, bytesToRead); } //throw FileError, ErrorFileLocked, X + +private: + FileInputPlain fileIn_; + const IoCallback notifyUnbufferedIO_; //throw X + + BufferedInputStream> + streamIn_{impl::makeTryRead(fileIn_, notifyUnbufferedIO_), fileIn_.getBlockSize()}; //throw FileError +}; + + +class FileOutputBuffered +{ +public: + FileOutputBuffered(const Zstring& filePath, const IoCallback& notifyUnbufferedIO /*throw X*/) : //throw FileError, ErrorTargetExisting + fileOut_(filePath), //throw FileError, ErrorTargetExisting + notifyUnbufferedIO_(notifyUnbufferedIO) {} + + void write(const void* buffer, size_t bytesToWrite) { streamOut_.write(buffer, bytesToWrite); } //throw FileError, X + + void finalize() //throw FileError, X + { + streamOut_.flushBuffer(); //throw FileError, X + fileOut_.close(); //throw FileError + } + +private: + FileOutputPlain fileOut_; + const IoCallback notifyUnbufferedIO_; //throw X + + BufferedOutputStream> + streamOut_{impl::makeTryWrite(fileOut_, notifyUnbufferedIO_), fileOut_.getBlockSize()}; //throw FileError +}; +//----------------------------------------------------------------------------------------------- + +//stream I/O convenience functions: + +inline +Zstring getPathWithTempName(const Zstring& filePath) //generate (hopefully) unique file name +{ + const Zstring shortGuid_ = printNumber(Zstr("%04x"), static_cast(getCrc16(generateGUID()))); + return filePath + Zstr('.') + shortGuid_ + Zstr(".tmp"); +} + +[[nodiscard]] std::string getFileContent(const Zstring& filePath, const IoCallback& notifyUnbufferedIO /*throw X*/); //throw FileError, X + +//overwrites if existing + transactional! :) +void setFileContent(const Zstring& filePath, const std::string_view bytes, const IoCallback& notifyUnbufferedIO /*throw X*/); //throw FileError, X +} + +#endif //FILE_IO_H_89578342758342572345 diff --git a/zen/file_path.cpp b/zen/file_path.cpp new file mode 100644 index 0000000..bb77922 --- /dev/null +++ b/zen/file_path.cpp @@ -0,0 +1,200 @@ +// ***************************************************************************** +// * 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 "file_path.h" +#include "zstring.h" + +using namespace zen; + + +std::optional zen::parsePathComponents(const Zstring& itemPath) +{ + auto doParse = [&](int sepCountVolumeRoot, bool rootWithSep) -> std::optional + { + assert(sepCountVolumeRoot > 0); + const Zstring itemPathPf = appendSeparator(itemPath); //simplify analysis of root without separator, e.g. \\server-name\share + + for (auto it = itemPathPf.begin(); it != itemPathPf.end(); ++it) + if (*it == FILE_NAME_SEPARATOR) + if (--sepCountVolumeRoot == 0) + { + Zstring rootPath(itemPathPf.begin(), rootWithSep ? it + 1 : it); + + Zstring relPath(it + 1, itemPathPf.end()); + trim(relPath, TrimSide::both, [](Zchar c) { return c == FILE_NAME_SEPARATOR; }); + + return PathComponents{std::move(rootPath), std::move(relPath)}; + } + return {}; + }; + + std::optional pc; //"/media/zenju/" and "/Volumes/" should not fail to parse + + if (!pc && startsWith(itemPath, "/mnt/")) //e.g. /mnt/DEVICE_NAME + pc = doParse(3 /*sepCountVolumeRoot*/, false /*rootWithSep*/); + + if (!pc && startsWith(itemPath, "/media/")) //Ubuntu: e.g. /media/zenju/DEVICE_NAME + if (const std::optional username = getEnvironmentVar("USER")) + if (startsWith(itemPath, std::string("/media/") + *username + "/")) + pc = doParse(4 /*sepCountVolumeRoot*/, false /*rootWithSep*/); + + if (!pc && startsWith(itemPath, "/run/media/")) //CentOS, Suse: e.g. /run/media/zenju/DEVICE_NAME + if (const std::optional username = getEnvironmentVar("USER")) + if (startsWith(itemPath, std::string("/run/media/") + *username + "/")) + pc = doParse(5 /*sepCountVolumeRoot*/, false /*rootWithSep*/); + + if (!pc && startsWith(itemPath, "/run/user/")) //Ubuntu, e.g.: /run/user/1000/gvfs/smb-share:server=192.168.62.145,share=folder + { + Zstring tmp(itemPath.begin() + strLength("/run/user/"), itemPath.end()); + tmp = beforeFirst(tmp, "/gvfs/", IfNotFoundReturn::none); + if (!tmp.empty() && std::all_of(tmp.begin(), tmp.end(), [](const char c) { return isDigit(c); })) + /**/pc = doParse(6 /*sepCountVolumeRoot*/, false /*rootWithSep*/); + } + + + if (!pc && startsWith(itemPath, "/")) + pc = doParse(1 /*sepCountVolumeRoot*/, true /*rootWithSep*/); + + return pc; +} + + +std::optional zen::getParentFolderPath(const Zstring& itemPath) +{ + if (const std::optional pc = parsePathComponents(itemPath)) + { + if (pc->relPath.empty()) + return std::nullopt; + + return appendPath(pc->rootPath, beforeLast(pc->relPath, FILE_NAME_SEPARATOR, IfNotFoundReturn::none)); + } + assert(itemPath.empty()); + return std::nullopt; +} + + +Zstring zen::getFileExtension(const ZstringView filePath) +{ + const ZstringView fileName = afterLast(filePath, FILE_NAME_SEPARATOR, IfNotFoundReturn::all); + return Zstring(afterLast(fileName, Zstr('.'), IfNotFoundReturn::none)); +} + + +Zstring zen::appendSeparator(Zstring path) //support rvalue references! +{ + assert(!endsWith(path, FILE_NAME_SEPARATOR == Zstr('/') ? Zstr('\\' ) : Zstr('/' ))); + + if (!endsWith(path, FILE_NAME_SEPARATOR)) + path += FILE_NAME_SEPARATOR; + return path; //returning a by-value parameter => RVO if possible, r-value otherwise! +} + + +bool zen::isValidRelPath(const Zstring& relPath) +{ + //relPath is expected to use FILE_NAME_SEPARATOR! + if constexpr (FILE_NAME_SEPARATOR != Zstr('/' )) if (contains(relPath, Zstr('/' ))) return false; + if constexpr (FILE_NAME_SEPARATOR != Zstr('\\')) if (contains(relPath, Zstr('\\'))) return false; + + const Zchar doubleSep[] = {FILE_NAME_SEPARATOR, FILE_NAME_SEPARATOR, 0}; + return !startsWith(relPath, FILE_NAME_SEPARATOR) && !endsWith(relPath, FILE_NAME_SEPARATOR) && + !contains(relPath, doubleSep); +} + + +Zstring zen::appendPath(const Zstring& basePath, const Zstring& relPath) +{ + assert(isValidRelPath(relPath)); + if (relPath.empty()) + return basePath; //with or without path separator, e.g. C:\ or C:\folder + + //assert(!basePath.empty()); + if (basePath.empty()) //basePath might be a relative path, too! + return relPath; + + if (endsWith(basePath, FILE_NAME_SEPARATOR)) + return basePath + relPath; + + Zstring output = basePath; + output.reserve(basePath.size() + 1 + relPath.size()); //append all three strings using a single memory allocation + return std::move(output) + FILE_NAME_SEPARATOR + relPath; // +} + + +/* https://docs.microsoft.com/de-de/windows/desktop/Intl/handling-sorting-in-your-applications + + Perf test: compare strings 10 mio times; 64 bit build + ----------------------------------------------------- + string a = "Fjk84$%kgfj$%T\\\\Gffg\\gsdgf\\fgsx----------d-" + string b = "fjK84$%kgfj$%T\\\\gfFg\\gsdgf\\fgSy----------dfdf" + + Windows (UTF16 wchar_t) + 4 ns | wcscmp + 67 ns | CompareStringOrdinalFunc+ + bIgnoreCase + 314 ns | LCMapString + wmemcmp + + OS X (UTF8 char) + 6 ns | strcmp + 98 ns | strcasecmp + 120 ns | strncasecmp + std::min(sizeLhs, sizeRhs); + 856 ns | CFStringCreateWithCString + CFStringCompare(kCFCompareCaseInsensitive) + 1110 ns | CFStringCreateWithCStringNoCopy + CFStringCompare(kCFCompareCaseInsensitive) + ________________________ + time per call | function */ + +std::weak_ordering zen::compareNativePath(const Zstring& lhs, const Zstring& rhs) +{ + assert(!contains(lhs, Zchar('\0'))); //don't expect embedded nulls! + assert(!contains(rhs, Zchar('\0'))); // + + return lhs <=> rhs; + +} + + +namespace +{ + constinit Global> globalEnvVars; +} + + +std::optional zen::getEnvironmentVar(const ZstringView name) +{ + /* const char* buffer = ::getenv(name); => NO! *not* thread-safe: returns pointer to internal memory! + might change after setenv(), allegedly possible even after another getenv()! + + getenv_s() to the rescue!? not implemented on GCC, apparently *still* not threadsafe!!! + + => *eff* this: make a global copy during start up! */ + globalEnvVars.setOnce([] + { + assert(runningOnMainThread()); + + auto envVars = std::make_unique>(); + if (char** line = environ) + for (; *line; ++line) + { + const std::string_view l(*line); + envVars->emplace(beforeFirst(l, '=', IfNotFoundReturn::all), + afterFirst(l, '=', IfNotFoundReturn::none)); + } + + return envVars; + }); + + if (std::shared_ptr> envVars = globalEnvVars.get()) + { + if (const auto it = envVars->find(name); + it != envVars->end()) + return it->second; + } + else + assert(false); //access during global shutdown => SOL! + + return {}; +} + + diff --git a/zen/file_path.h b/zen/file_path.h new file mode 100644 index 0000000..9446ad7 --- /dev/null +++ b/zen/file_path.h @@ -0,0 +1,60 @@ +// ***************************************************************************** +// * 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 FILE_PATH_H_3984678473567247567 +#define FILE_PATH_H_3984678473567247567 + +#include "zstring.h" + + +namespace zen +{ + const Zchar FILE_NAME_SEPARATOR = '/'; + +/* forbidden characters in file names: + Windows: <>:"/\|?* https://docs.microsoft.com/de-de/windows/win32/fileio/naming-a-file#naming-conventions + Linux: / + macOS: : +*/ +const Zchar fileNameForbiddenChars[] = Zstr(R"(<>:"/\|?*)"); + + +struct PathComponents +{ + Zstring rootPath; //itemPath = rootPath + (FILE_NAME_SEPARATOR?) + relPath + Zstring relPath; // +}; +std::optional parsePathComponents(const Zstring& itemPath); //no value on error + +std::optional getParentFolderPath(const Zstring& itemPath); +inline Zstring getItemName(const Zstring& itemPath) { return afterLast(itemPath, FILE_NAME_SEPARATOR, IfNotFoundReturn::all); } + +Zstring getFileExtension(const ZstringView filePath); + +Zstring appendSeparator(Zstring path); //support rvalue references! + +bool isValidRelPath(const Zstring& relPath); + +Zstring appendPath(const Zstring& basePath, const Zstring& relPath); + +//------------------------------------------------------------------------------------------ +/* Compare *local* file paths: + Windows: igore case (but distinguish Unicode normalization forms!) + Linux: byte-wise comparison + macOS: ignore case + Unicode normalization forms */ +std::weak_ordering compareNativePath(const Zstring& lhs, const Zstring& rhs); + +inline bool equalNativePath(const Zstring& lhs, const Zstring& rhs) { return compareNativePath(lhs, rhs) == std::weak_ordering::equivalent; } + +struct LessNativePath { bool operator()(const Zstring& lhs, const Zstring& rhs) const { return compareNativePath(lhs, rhs) < 0; } }; +//------------------------------------------------------------------------------------------ + +std::optional getEnvironmentVar(const ZstringView name); + + +} + +#endif //FILE_PATH_H_3984678473567247567 diff --git a/zen/file_traverser.cpp b/zen/file_traverser.cpp new file mode 100644 index 0000000..75075b8 --- /dev/null +++ b/zen/file_traverser.cpp @@ -0,0 +1,79 @@ +// ***************************************************************************** +// * 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 "file_traverser.h" +#include "file_error.h" +#include "file_access.h" + + + #include + #include + +using namespace zen; + + +void zen::traverseFolder(const Zstring& dirPath, + const std::function& onFile, + const std::function& onFolder, + const std::function& onSymlink) //throw FileError +{ + DIR* folder = ::opendir(dirPath.c_str()); //directory must NOT end with path separator, except "/" + if (!folder) + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot open directory %x."), L"%x", fmtPath(dirPath)), "opendir"); + ZEN_ON_SCOPE_EXIT(::closedir(folder)); //never close nullptr handles! -> crash + + for (;;) + { + errno = 0; + const dirent* dirEntry = ::readdir(folder); //don't use readdir_r(), see comment in native.cpp + if (!dirEntry) + { + if (errno == 0) //errno left unchanged => no more items + return; + + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read directory %x."), L"%x", fmtPath(dirPath)), "readdir"); + //don't retry but restart dir traversal on error! https://devblogs.microsoft.com/oldnewthing/20140612-00/?p=753/ + } + + //don't return "." and ".." + const char* itemNameRaw = dirEntry->d_name; + + if (itemNameRaw[0] == '.' && + (itemNameRaw[1] == 0 || (itemNameRaw[1] == '.' && itemNameRaw[2] == 0))) + continue; + + const Zstring& itemName = itemNameRaw; + if (itemName.empty()) //checks result of normalizeUtfForPosix, too! + throw FileError(replaceCpy(_("Cannot read directory %x."), L"%x", fmtPath(dirPath)), formatSystemError("readdir", L"", L"Folder contains an item without name.")); + + const Zstring& itemPath = appendPath(dirPath, itemName); + + struct stat statData = {}; + if (::lstat(itemPath.c_str(), &statData) != 0) //lstat() does not resolve symlinks + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(itemPath)), "lstat"); + + if (S_ISLNK(statData.st_mode)) //on Linux there is no distinction between file and directory symlinks! + { + if (onSymlink) + onSymlink({ itemName, itemPath, statData.st_mtime}); + } + else if (S_ISDIR(statData.st_mode)) //a directory + { + if (onFolder) + onFolder({itemName, itemPath}); + } + else //a file or named pipe, etc. S_ISREG, S_ISCHR, S_ISBLK, S_ISFIFO, S_ISSOCK + { + if (onFile) + onFile({itemName, itemPath, makeUnsigned(statData.st_size), statData.st_mtime}); + /* It may be a good idea to not check "S_ISREG(statData.st_mode)" explicitly and to not issue an error message on other types to support these scenarios: + - RTS setup watch (essentially wants to read directories only) + - removeDirectory (wants to delete everything; pipes can be deleted just like files via "unlink") + + However an "open" on a pipe will block (https://sourceforge.net/p/freefilesync/bugs/221/), so the copy routines better be smart! */ + } + } +} diff --git a/zen/file_traverser.h b/zen/file_traverser.h new file mode 100644 index 0000000..8f7d836 --- /dev/null +++ b/zen/file_traverser.h @@ -0,0 +1,43 @@ +// ***************************************************************************** +// * 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 FILER_TRAVERSER_H_127463214871234 +#define FILER_TRAVERSER_H_127463214871234 + +#include +#include "file_error.h" + +namespace zen +{ +struct FileInfo +{ + Zstring itemName; + Zstring fullPath; + uint64_t fileSize = 0; //[bytes] + time_t modTime = 0; //number of seconds since Jan. 1st 1970 GMT +}; + +struct FolderInfo +{ + Zstring itemName; + Zstring fullPath; +}; + +struct SymlinkInfo +{ + Zstring itemName; + Zstring fullPath; + time_t modTime = 0; //number of seconds since Jan. 1st 1970 GMT +}; + +//- non-recursive +void traverseFolder(const Zstring& dirPath, + const std::function& onFile, /*optional*/ + const std::function& onFolder,/*optional*/ + const std::function& onSymlink/*optional*/); //throw FileError +} + +#endif //FILER_TRAVERSER_H_127463214871234 diff --git a/zen/format_unit.cpp b/zen/format_unit.cpp new file mode 100644 index 0000000..f684fac --- /dev/null +++ b/zen/format_unit.cpp @@ -0,0 +1,236 @@ +// ***************************************************************************** +// * 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 "format_unit.h" +#include "basic_math.h" +#include "sys_error.h" +#include "i18n.h" +#include "time.h" +#include "globals.h" +#include "utf.h" + + #include + #include + #include //thousands separator + #include "utf.h" // + +using namespace zen; + + +std::wstring zen::formatTwoDigitPrecision(double value) +{ + //print two digits: 0,1 | 1,1 | 11 + if (std::abs(value) < 9.95) //9.99 must not be formatted as "10.0" + return printNumber(L"%.1f", value); + + return formatNumber(std::llround(value)); +} + + +std::wstring zen::formatThreeDigitPrecision(double value) +{ + //print three digits: 0,01 | 0,11 | 1,11 | 11,1 | 111 + if (std::abs(value) < 9.995) //9.999 must not be formatted as "10.00" + return printNumber(L"%.2f", value); + if (std::abs(value) < 99.95) //99.99 must not be formatted as "100.0" + return printNumber(L"%.1f", value); + + return formatNumber(std::llround(value)); +} + + +std::wstring zen::formatFilesizeShort(int64_t size) +{ + //if (size < 0) return _("Error"); -> really? + + if (std::abs(size) <= 999) + return _P("1 byte", "%x bytes", static_cast(size)); + + double sizeInUnit = static_cast(size); + + auto formatUnit = [&](const std::wstring& unitTxt) { return replaceCpy(unitTxt, L"%x", formatThreeDigitPrecision(sizeInUnit)); }; + + sizeInUnit /= bytesPerKilo; + if (std::abs(sizeInUnit) < 999.5) + return formatUnit(_("%x KB")); + + sizeInUnit /= bytesPerKilo; + if (std::abs(sizeInUnit) < 999.5) + return formatUnit(_("%x MB")); + + sizeInUnit /= bytesPerKilo; + if (std::abs(sizeInUnit) < 999.5) + return formatUnit(_("%x GB")); + + sizeInUnit /= bytesPerKilo; + if (std::abs(sizeInUnit) < 999.5) + return formatUnit(_("%x TB")); + + sizeInUnit /= bytesPerKilo; + return formatUnit(_("%x PB")); +} + + +namespace +{ +enum class UnitRemTime +{ + sec, + min, + hour, + day +}; + + +std::wstring formatUnitTime(int val, UnitRemTime unit) +{ + switch (unit) + { + case UnitRemTime::sec: return _P("1 sec", "%x sec", val); + case UnitRemTime::min: return _P("1 min", "%x min", val); + case UnitRemTime::hour: return _P("1 hour", "%x hours", val); + case UnitRemTime::day: return _P("1 day", "%x days", val); + } + assert(false); + return _("Error"); +} + + +template +std::wstring roundToBlock(double timeInHigh, + UnitRemTime unitHigh, const int (&stepsHigh)[M], + int unitLowPerHigh, + UnitRemTime unitLow, const int (&stepsLow)[N]) +{ + assert(unitLowPerHigh > 0); + const double granularity = 0.1; + const double timeInLow = timeInHigh * unitLowPerHigh; + const int blockSizeLow = granularity * timeInHigh < 1 ? + numeric::roundToGrid(granularity * timeInLow, std::begin(stepsLow), std::end(stepsLow)): + numeric::roundToGrid(granularity * timeInHigh, std::begin(stepsHigh), std::end(stepsHigh)) * unitLowPerHigh; + const int roundedtimeInLow = std::lround(timeInLow / blockSizeLow) * blockSizeLow; + + std::wstring output = formatUnitTime(roundedtimeInLow / unitLowPerHigh, unitHigh); + if (unitLowPerHigh > blockSizeLow) + output += L' ' + formatUnitTime(roundedtimeInLow % unitLowPerHigh, unitLow); + return output; +} +} + + +std::wstring zen::formatRemainingTime(double timeInSec) +{ + const int steps10[] = {1, 2, 5, 10}; + const int steps24[] = {1, 2, 3, 4, 6, 8, 12, 24}; + const int steps60[] = {1, 2, 5, 10, 15, 20, 30, 60}; + + //determine preferred unit + double timeInUnit = timeInSec; + if (timeInUnit <= 60) + return roundToBlock(timeInUnit, UnitRemTime::sec, steps60, 1, UnitRemTime::sec, steps60); + + timeInUnit /= 60; + if (timeInUnit <= 60) + return roundToBlock(timeInUnit, UnitRemTime::min, steps60, 60, UnitRemTime::sec, steps60); + + timeInUnit /= 60; + if (timeInUnit <= 24) + return roundToBlock(timeInUnit, UnitRemTime::hour, steps24, 60, UnitRemTime::min, steps60); + + timeInUnit /= 24; + return roundToBlock(timeInUnit, UnitRemTime::day, steps10, 24, UnitRemTime::hour, steps24); + //note: for 10% granularity steps10 yields a valid blocksize only up to timeInUnit == 100! + //for larger time sizes this results in a finer granularity than expected: 10 days -> should not be a problem considering "usual" remaining time for synchronization +} + + +std::wstring zen::formatProgressPercent(double fraction, int decPlaces) +{ + if (decPlaces == 0) //special case for perf + return numberTo(static_cast(std::floor(fraction * 100))) + L'%'; + + //round down! don't show 100% when not actually done: https://freefilesync.org/forum/viewtopic.php?t=9781 + const double blocks = std::pow(10, decPlaces); + const double percent = std::floor(fraction * 100 * blocks) / blocks; + + assert(0 <= decPlaces && decPlaces <= 9); + wchar_t format[] = L"%.0f" L"%%" /*literal %: need to localize?*/; + format[2] += static_cast(std::clamp(decPlaces, 0, 9)); + + return printNumber(format, percent); +} + + + + +std::wstring zen::formatNumber(int64_t n) +{ + //::setlocale (LC_ALL, ""); -> see localization.cpp::wxWidgetsLocale + static_assert(sizeof(long long int) == sizeof(n)); + return printNumber(L"%'lld", n); //considers grouping (') +} + + +std::wstring zen::formatUtcToLocalTime(time_t utcTime) +{ + auto fmtFallback = [utcTime] //don't take "no" for an answer! + { + if (const TimeComp tc = getUtcTime(utcTime); + tc != TimeComp()) + { + wchar_t buf[128] = {}; //the only way to format abnormally large or invalid modTime: std::strftime() will fail! + if (const int rv = std::swprintf(buf, std::size(buf), L"%d-%02d-%02d %02d:%02d:%02d GMT", tc.year, tc.month, tc.day, tc.hour, tc.minute, tc.second); + 0 < rv && rv < std::ssize(buf)) + return std::wstring(buf, rv); + } + + return L"time_t = " + numberTo(utcTime); + }; + + const TimeComp& loc = getLocalTime(utcTime); //returns TimeComp() on error + + /*const*/ std::wstring dateTimeFmt = utfTo(formatTime(Zstr("%x %X"), loc)); + if (dateTimeFmt.empty()) + return fmtFallback(); + + return dateTimeFmt; +} + + + + +WeekDay impl::getFirstDayOfWeekImpl() //throw SysError +{ + /* testing: change locale via command line + --------------------------------------- + LC_TIME=en_DK.utf8 => Monday + LC_TIME=en_US.utf8 => Sunday */ + const char* firstDay = ::nl_langinfo(_NL_TIME_FIRST_WEEKDAY); //[1-Sunday, 7-Saturday] + ASSERT_SYSERROR(firstDay && 1 <= *firstDay && *firstDay <= 7); + + const int weekDayStartSunday = *firstDay; //[1-Sunday, 7-Saturday] + const int weekDayStartMonday = (weekDayStartSunday - 2 + 7) % 7; //[0-Monday, 6-Sunday] 7 == 0 in Z_7 + + return static_cast(weekDayStartMonday); +} + + +WeekDay zen::getFirstDayOfWeek() +{ + static const WeekDay weekDay = [] + { + try + { + return impl::getFirstDayOfWeekImpl(); //throw SysError + } + catch (const SysError& e) + { + throw std::runtime_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Failed to get first day of the week." + "\n\n" + + utfTo(e.toString())); + } + }(); + return weekDay; +} diff --git a/zen/format_unit.h b/zen/format_unit.h new file mode 100644 index 0000000..6a8f6b7 --- /dev/null +++ b/zen/format_unit.h @@ -0,0 +1,44 @@ +// ***************************************************************************** +// * 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 FMT_UNIT_8702184019487324 +#define FMT_UNIT_8702184019487324 + +#include +#include + + +namespace zen +{ + const int bytesPerKilo = 1000; +std::wstring formatFilesizeShort(int64_t filesize); +std::wstring formatRemainingTime(double timeInSec); +std::wstring formatProgressPercent(double fraction /*[0, 1]*/, int decPlaces = 0 /*[0, 9]*/); //rounded down! +std::wstring formatUtcToLocalTime(time_t utcTime); //like File Explorer would... + +std::wstring formatTwoDigitPrecision (double value); //format with fixed number of digits +std::wstring formatThreeDigitPrecision(double value); //(unless value is too large) + +std::wstring formatNumber(int64_t n); //format integer number including thousands separator + + + +enum class WeekDay +{ + monday, + tuesday, + wednesday, + thursday, + friday, + saturday, + sunday, +}; +WeekDay getFirstDayOfWeek(); + +namespace impl { WeekDay getFirstDayOfWeekImpl(); } //throw SysError +} + +#endif diff --git a/zen/globals.h b/zen/globals.h new file mode 100644 index 0000000..1a11037 --- /dev/null +++ b/zen/globals.h @@ -0,0 +1,308 @@ +// ***************************************************************************** +// * 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 GLOBALS_H_8013740213748021573485 +#define GLOBALS_H_8013740213748021573485 + +#include +#include +#include +#include "scope_guard.h" + + +namespace zen +{ +/* Solve static destruction order fiasco by providing shared ownership and serialized access to global variables + + => e.g. accesses to "Global::get()" during process shutdown: _("") used by message in debug_minidump.cpp or by some detached thread assembling an error message! + => use trivially-destructible POD only!!! + + ATTENTION: function-static globals have the compiler generate "magic statics" == compiler-genenerated locking code which will crash or leak memory when accessed after global is "dead" + => "solved" by FunStatGlobal, but we can't have "too many" of these... */ +class PodSpinMutex +{ +public: + bool tryLock(); + void lock(); + void unlock(); + bool isLocked(); + +private: + std::atomic_flag flag_{}; /* => avoid potential contention with worker thread during Global<> construction! + - "For an atomic_flag with static storage duration, this guarantees static initialization:" => just what the doctor ordered! + - "[default initialization] initializes std::atomic_flag to clear state" - since C++20 => + - "std::atomic_flag is [...] guaranteed to be lock-free" + - interestingly, is_trivially_constructible_v<> is false, thanks to constexpr! https://developercommunity.visualstudio.com/content/problem/416343/stdatomic-no-longer-is-trivially-constructible.html */ +}; + + +#define GLOBAL_RUN_ONCE(X) \ + struct ZEN_CONCAT(GlobalInitializer, __LINE__) \ + { \ + ZEN_CONCAT(GlobalInitializer, __LINE__)() { X; } \ + } ZEN_CONCAT(globalInitializer, __LINE__) + + +template +class Global //don't use for function-scope statics! +{ +public: + consteval Global() {}; //demand static zero-initialization! + + ~Global() + { + static_assert(std::is_trivially_destructible_v, "this memory needs to live forever"); + + pod_.spinLock.lock(); + std::shared_ptr* oldInst = std::exchange(pod_.inst, nullptr); + pod_.destroyed = true; + pod_.spinLock.unlock(); + + delete oldInst; + } + + std::shared_ptr get() //=> return std::shared_ptr to let instance life time be handled by caller (MT usage!) + { + pod_.spinLock.lock(); + ZEN_ON_SCOPE_EXIT(pod_.spinLock.unlock()); + + if (pod_.inst) + return *pod_.inst; + return nullptr; + } + + void set(std::unique_ptr&& newInst) + { + std::shared_ptr* tmpInst = nullptr; + if (newInst) + tmpInst = new std::shared_ptr(std::move(newInst)); + { + pod_.spinLock.lock(); + ZEN_ON_SCOPE_EXIT(pod_.spinLock.unlock()); + + if (!pod_.destroyed) + std::swap(pod_.inst, tmpInst); + else + assert(false); + + pod_.initialized = true; + } + delete tmpInst; + } + + //for initialization via a frequently-called function (which may be running on parallel threads) + template + void setOnce(Function getInitialValue /*-> std::unique_ptr*/) + { + pod_.spinLock.lock(); + ZEN_ON_SCOPE_EXIT(pod_.spinLock.unlock()); + + if (!pod_.initialized) + { + assert(!pod_.inst); + if (!pod_.destroyed) + { + if (std::unique_ptr newInst = getInitialValue()) //throw ? + pod_.inst = new std::shared_ptr(std::move(newInst)); + } + else + assert(false); + + pod_.initialized = true; + } + } + +private: + struct Pod + { + PodSpinMutex spinLock; //rely entirely on static zero-initialization! => avoid potential contention with worker thread during Global<> construction! + //serialize access: can't use std::mutex: has non-trival destructor + std::shared_ptr* inst = nullptr; + bool initialized = false; + bool destroyed = false; + } pod_; +}; + +//=================================================================================================================== +//=================================================================================================================== + +struct CleanUpEntry +{ + using CleanUpFunction = void (*)(void* callbackData); + CleanUpFunction cleanUpFun = nullptr; + void* callbackData = nullptr; + CleanUpEntry* prev = nullptr; +}; +void registerGlobalForDestruction(CleanUpEntry& entry); + + +template +class FunStatGlobal +{ +public: + consteval FunStatGlobal() {}; //demand static zero-initialization! + + //No ~FunStatGlobal(): required to avoid generation of magic statics code for a function-scope static! + + std::shared_ptr get() + { + static_assert(std::is_trivially_destructible_v, "this class must not generate code for magic statics!"); + + pod_.spinLock.lock(); + ZEN_ON_SCOPE_EXIT(pod_.spinLock.unlock()); + + if (pod_.inst) + return *pod_.inst; + return nullptr; + } + + void set(std::unique_ptr&& newInst) + { + std::shared_ptr* tmpInst = nullptr; + if (newInst) + tmpInst = new std::shared_ptr(std::move(newInst)); + { + pod_.spinLock.lock(); + ZEN_ON_SCOPE_EXIT(pod_.spinLock.unlock()); + + if (!pod_.destroyed) + std::swap(pod_.inst, tmpInst); + else + assert(false); + + registerDestruction(); + } + delete tmpInst; + } + + template + void setOnce(Function getInitialValue /*-> std::unique_ptr*/) + { + pod_.spinLock.lock(); + ZEN_ON_SCOPE_EXIT(pod_.spinLock.unlock()); + + if (!pod_.cleanUpEntry.cleanUpFun) + { + assert(!pod_.inst); + if (!pod_.destroyed) + { + if (std::unique_ptr newInst = getInitialValue()) //throw ? + pod_.inst = new std::shared_ptr(std::move(newInst)); + } + else + assert(false); + + registerDestruction(); + } + } + +private: + void destruct() + { + static_assert(std::is_trivially_destructible_v, "this memory needs to live forever"); + + pod_.spinLock.lock(); + std::shared_ptr* oldInst = std::exchange(pod_.inst, nullptr); + pod_.destroyed = true; + pod_.spinLock.unlock(); + + delete oldInst; + } + + //call while holding pod_.spinLock + void registerDestruction() + { + assert(pod_.spinLock.isLocked()); + + if (!pod_.cleanUpEntry.cleanUpFun) + { + pod_.cleanUpEntry.callbackData = this; + pod_.cleanUpEntry.cleanUpFun = [](void* callbackData) + { + static_cast(callbackData)->destruct(); + }; + + registerGlobalForDestruction(pod_.cleanUpEntry); + } + } + + struct Pod + { + PodSpinMutex spinLock; //rely entirely on static zero-initialization! => avoid potential contention with worker thread during Global<> construction! + //serialize access; can't use std::mutex: has non-trival destructor + std::shared_ptr* inst = nullptr; + CleanUpEntry cleanUpEntry; + bool destroyed = false; + } pod_; +}; + + +inline +void registerGlobalForDestruction(CleanUpEntry& entry) +{ + static struct + { + PodSpinMutex spinLock; + CleanUpEntry* head = nullptr; + } cleanUpList; + + static_assert(std::is_trivially_destructible_v, "we must not generate code for magic statics!"); + + cleanUpList.spinLock.lock(); + ZEN_ON_SCOPE_EXIT(cleanUpList.spinLock.unlock()); + + std::atexit([] + { + cleanUpList.spinLock.lock(); + ZEN_ON_SCOPE_EXIT(cleanUpList.spinLock.unlock()); + + (*cleanUpList.head->cleanUpFun)(cleanUpList.head->callbackData); + cleanUpList.head = cleanUpList.head->prev; //nicely clean up in reverse order of construction + }); + + entry.prev = cleanUpList.head; + cleanUpList.head = &entry; + +} + +//------------------------------------------------------------------------------------------ + +inline +bool PodSpinMutex::tryLock() +{ + return !flag_.test_and_set(std::memory_order_acquire); +} + + + + +inline +void PodSpinMutex::lock() +{ + while (!tryLock()) + flag_.wait(true, std::memory_order_relaxed); +} + + +inline +void PodSpinMutex::unlock() +{ + flag_.clear(std::memory_order_release); + flag_.notify_one(); +} + + +inline +bool PodSpinMutex::isLocked() +{ + if (!tryLock()) + return true; + unlock(); + return false; +} +} + +#endif //GLOBALS_H_8013740213748021573485 diff --git a/zen/guid.h b/zen/guid.h new file mode 100644 index 0000000..79d457f --- /dev/null +++ b/zen/guid.h @@ -0,0 +1,54 @@ +// ***************************************************************************** +// * 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 GUID_H_80425780237502345 +#define GUID_H_80425780237502345 + + #include //open + #include //close, getentropy + #include + //#include -> uuid_generate(), uuid_unparse(); avoid additional dependency for "sudo apt-get install uuid-dev" + + +namespace zen +{ +inline +std::string generateGUID() //creates a 16-byte GUID +{ + std::string guid(16, '\0'); + +#ifndef __GLIBC_PREREQ +#error Where is Glibc? +#endif + +#if __GLIBC_PREREQ(2, 25) //getentropy() requires Glibc 2.25 (ldd --version) PS: CentOS 7 is on 2.17 + if (::getentropy(guid.data(), guid.size()) != 0) //"The maximum permitted value for the length argument is 256" + throw std::runtime_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Failed to generate GUID." + "\n\n" + + utfTo(formatSystemError("getentropy", errno))); +#else + //keep fd open and thread_local? NO! susceptible to global destruction fiasco: e.g. used by setFileContent() + getPathWithTempName() by globalShutdownTasks + const int fd = ::open("/dev/urandom", O_RDONLY | O_CLOEXEC); + if (fd == -1) + throw std::runtime_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Failed to generate GUID." + "\n\n" + + utfTo(formatSystemError("open", errno))); + ZEN_ON_SCOPE_EXIT(::close(fd)); + + for (size_t offset = 0; offset < guid.size(); ) + { + const ssize_t bytesRead = ::read(fd, guid.data() + offset, guid.size() - offset); + if (bytesRead <= 0) //0 means EOF => error in this context (should check for buffer overflow, too?) + throw std::runtime_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Failed to generate GUID." + "\n\n" + + utfTo(formatSystemError("read", bytesRead < 0 ? errno : EIO))); + offset += bytesRead; + assert(offset <= guid.size()); + } +#endif + return guid; + +} +} + +#endif //GUID_H_80425780237502345 diff --git a/zen/http.cpp b/zen/http.cpp new file mode 100644 index 0000000..ddf0300 --- /dev/null +++ b/zen/http.cpp @@ -0,0 +1,555 @@ +// ***************************************************************************** +// * 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 "http.h" + + #include //DON'T include directly! + #include "stream_buffer.h" + +using namespace zen; + + +const int HTTP_ACCESS_TIMEOUT_SEC = 20; + +const size_t HTTP_BLOCK_SIZE_DOWNLOAD = 64 * 1024; +//- InternetReadFile() is buffered + prefetching +//- libcurl returns blocks of only 16 kB as returned by recv() even if we request larger blocks via CURLOPT_BUFFERSIZE + const size_t HTTP_STREAM_BUFFER_SIZE = 1024 * 1024; //unit: [byte] + //stream buffer should be big enough to facilitate prefetching during alternating read/write operations => e.g. see serialize.h::unbufferedStreamCopy() + + + + +class HttpInputStream::Impl +{ +public: + Impl(const Zstring& url, + const std::string* postBuf, //issue POST if bound, GET otherwise + const std::string& contentType, //required for POST + const IoCallback& onPostBytesSent /*throw X*/, + bool disableGetCache, //not relevant for POST (= never cached) + const Zstring& userAgent, + const Zstring& caCertFilePath /*optional: enable certificate validation*/) //throw SysError, X + { + ZEN_ON_SCOPE_FAIL(cleanup()); //destructor call would lead to member double clean-up!!! + + assert(postBuf || !onPostBytesSent); + + const Zstring urlFmt = afterFirst(url, Zstr("://"), IfNotFoundReturn::none); + const Zstring server = beforeFirst(urlFmt, Zstr('/'), IfNotFoundReturn::all); + const Zstring page = Zstr('/') + afterFirst(urlFmt, Zstr('/'), IfNotFoundReturn::none); + + const bool useTls = [&] + { + if (startsWithAsciiNoCase(url, "http://")) + return false; + if (startsWithAsciiNoCase(url, "https://")) + return true; + throw SysError(L"URL uses unexpected protocol."); + }(); + + std::unordered_map headers; + + assert(postBuf || contentType.empty()); + if (postBuf && !contentType.empty()) + headers["Content-Type"] = contentType; + + if (!postBuf /*=> HTTP GET*/ && disableGetCache) //libcurl doesn't cache internally, so it should be enough to set this header + headers["Cache-Control"] = "no-cache"; //= similar to WinInet's INTERNET_FLAG_RELOAD + //caveat: INTERNET_FLAG_RELOAD issues "Pragma: no-cache" instead if "request is going through a proxy" + + + auto promHeader = std::make_shared>(); + std::future futHeader = promHeader->get_future(); + + auto postBytesSent = std::make_shared>(0); + + worker_ = InterruptibleThread([asyncStreamOut = this->asyncStreamIn_, promHeader, headers = std::move(headers), postBytesSent, + server, useTls, caCertFilePath, userAgent = utfTo(userAgent), + postBuf = postBuf ? std::optional(*postBuf) : std::nullopt, //[!] life-time! + serverRelPath = utfTo(page)] + { + setCurrentThreadName(Zstr("Istream ") + server); + + bool headerReceived = false; + try + { + std::vector curlHeaders; + for (const auto& [name, value] : headers) + curlHeaders.push_back(name + ": " + value); + + std::vector extraOptions {{CURLOPT_USERAGENT, userAgent.c_str()}}; + //CURLOPT_FOLLOWLOCATION already off by default :) + + std::function buf)> readRequest; + if (postBuf) + { + readRequest = [&, postBufStream{MemoryStreamIn(*postBuf)}](std::span buf) mutable + { + const size_t bytesRead = postBufStream.read(buf.data(), buf.size()); + * postBytesSent += bytesRead; + return bytesRead; + }; + extraOptions.emplace_back(CURLOPT_POST, 1); + extraOptions.emplace_back(CURLOPT_POSTFIELDSIZE_LARGE, postBuf->size()); //avoid HTTP chunked transfer encoding? + } + + //careful with these callbacks! First receive HTTP header without blocking, + //and only then allow AsyncStreamBuffer::write() which can block! + + std::string headerBuf; + auto onHeaderData = [&](const std::string_view& headerLine) + { + if (headerReceived) + throw SysError(L"Unexpected header data after end of HTTP header."); + + //"The callback will be called once for each header and only complete header lines are passed on to the callback" (including \r\n at the end) + headerBuf += headerLine; + + if (headerLine == "\r\n") + { + headerReceived = true; + promHeader->set_value(std::move(headerBuf)); + } + }; + + HttpSession httpSession(server, useTls, caCertFilePath); //throw SysError + + auto writeResponse = [&](std::span buf) + { + if (!headerReceived) + throw SysError(L"Received HTTP body without header."); + + asyncStreamOut->write(buf.data(), buf.size()); //throw ThreadStopRequest + }; + + httpSession.perform(serverRelPath, + curlHeaders, extraOptions, + writeResponse /*throw ThreadStopRequest*/, + readRequest, + onHeaderData /*throw SysError*/, + HTTP_ACCESS_TIMEOUT_SEC); //throw SysError, ThreadStopRequest + + if (!headerReceived) + throw SysError(L"HTTP response is missing header."); + + asyncStreamOut->closeStream(); + } + catch (SysError&) //let ThreadStopRequest pass through! + { + if (!headerReceived) + promHeader->set_exception(std::current_exception()); + + asyncStreamOut->setWriteError(std::current_exception()); + } + }); + + //------------------------------------------------------------------------------------ + if (postBuf && onPostBytesSent) + { + int64_t bytesReported = 0; + while (futHeader.wait_for(std::chrono::milliseconds(25)) == std::future_status::timeout) + { + const int64_t bytesDelta = *postBytesSent /*atomic shared access!*/- bytesReported; + bytesReported += bytesDelta; + onPostBytesSent(bytesDelta); //throw X + } + } + //------------------------------------------------------------------------------------ + + const std::string headBuf = futHeader.get(); //throw SysError + //parse header: https://www.w3.org/Protocols/HTTP/1.0/spec.html#Request-Line + const std::string_view& statusBuf = beforeFirst(headBuf, "\r\n", IfNotFoundReturn::all); + const std::string_view& headersBuf = afterFirst (headBuf, "\r\n", IfNotFoundReturn::none); + + const std::vector statusItems = splitCpy(statusBuf, ' ', SplitOnEmpty::allow); //HTTP-Version SP Status-Code SP Reason-Phrase CRLF + if (statusItems.size() < 2 || !startsWith(statusItems[0], "HTTP/")) + throw SysError(L"Invalid HTTP response: \"" + utfTo(statusBuf) + L'"'); + + statusCode_ = stringTo(statusItems[1]); + + split(headersBuf, '\n', [&](const std::string_view line) + { + if (!line.empty()) //careful: actual line separator is "\r\n"! + responseHeaders_.emplace(trimCpy(beforeFirst(line, ':', IfNotFoundReturn::all)), + trimCpy(afterFirst (line, ':', IfNotFoundReturn::none))); + }); + /* let's NOT consider "Content-Length" header: + - may be unavailable ("Transfer-Encoding: chunked") + - may refer to compressed data size ("Content-Encoding: gzip") */ + } + + ~Impl() { cleanup(); } + + const int getStatusCode() const { return statusCode_; } + + const std::string* getHeader(const std::string& name) const + { + auto it = responseHeaders_.find(name); + return it != responseHeaders_.end() ? &it->second : nullptr; + } + + size_t getBlockSize() const { return HTTP_BLOCK_SIZE_DOWNLOAD; } + + size_t tryRead(void* buffer, size_t bytesToRead) //throw SysError; may return short; only 0 means EOF! CONTRACT: bytesToRead > 0! + { + return asyncStreamIn_->tryRead(buffer, bytesToRead); //throw SysError + //no need for asyncStreamIn_->checkWriteErrors(): once end of stream is reached, asyncStreamOut->closeStream() was called => no errors occured + } + +private: + Impl (const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + + void cleanup() + { + asyncStreamIn_->setReadError(std::make_exception_ptr(ThreadStopRequest())); + } + + std::shared_ptr asyncStreamIn_ = std::make_shared(HTTP_STREAM_BUFFER_SIZE); + InterruptibleThread worker_; + int statusCode_ = 0; + std::unordered_map responseHeaders_; +}; + + +HttpInputStream::HttpInputStream(std::unique_ptr&& pimpl) : pimpl_(std::move(pimpl)) {} + +HttpInputStream::~HttpInputStream() {} + +size_t HttpInputStream::tryRead(void* buffer, size_t bytesToRead) { return pimpl_->tryRead(buffer, bytesToRead); } + +size_t HttpInputStream::getBlockSize() const { return pimpl_->getBlockSize(); } + +std::string HttpInputStream::readAll(const IoCallback& notifyUnbufferedIO /*throw X*/) //throw SysError, X +{ + return unbufferedLoad([&](void* buffer, size_t bytesToRead) + { + const size_t bytesRead = pimpl_->tryRead(buffer, bytesToRead); //throw SysError; may return short, only 0 means EOF! => CONTRACT: bytesToRead > 0! + if (notifyUnbufferedIO) notifyUnbufferedIO(bytesRead); //throw X! + return bytesRead; + }, + pimpl_->getBlockSize()); //throw SysError, X +} + + +namespace +{ +std::unique_ptr sendHttpRequestImpl(const Zstring& url, + const std::string* postBuf /*issue POST if bound, GET otherwise*/, + const std::string& contentType, //required for POST + const IoCallback& onPostBytesSent /*throw X*/, + const Zstring& userAgent, + const Zstring& caCertFilePath /*optional: enable certificate validation*/) //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(urlRed, postBuf, contentType, onPostBytesSent, false /*disableGetCache*/, + userAgent, caCertFilePath); //throw SysError, X + + //https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_Redirection + const int httpStatus = response->getStatusCode(); + if (httpStatus / 100 == 3) //e.g. 301, 302, 303, 307... we're not too greedy since we check location, too! + { + const std::string* value = response->getHeader("Location"); + if (!value || value->empty()) + throw SysError(L"Unresolvable redirect. No target Location."); + + urlRed = utfTo(*value); + } + else + { + if (httpStatus != 200) //HTTP_STATUS_OK + { +#if 0 //beneficial to add error details? + std::wstring errorDetails; + try + { + errorDetails = utfTo(HttpInputStream(std::move(response)).readAll(nullptr /*notifyUnbufferedIO*/)); //throw SysError + } + catch (const SysError& e) { errorDetails = e.toString(); } +#endif + throw SysError(formatHttpError(httpStatus) /*+ L' ' + errorDetails*/); //e.g. "HTTP status 404: Not found." + } + + return response; + } + } + throw SysError(L"Too many redirects."); +} + + +//encode for "application/x-www-form-urlencoded" +std::string urlencode(const std::string_view& str) +{ + 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 == ' ') + output += '+'; + else if (('0' <= c && c <= '9') || + ('A' <= c && c <= 'Z') || + ('a' <= c && c <= 'z') || + c == '-' || c == '.' || c == '_') //note: "~" is encoded by PHP! + output += c; + else + { + const auto [high, low] = hexify(c); + output += '%'; + output += high; + output += low; + } + return output; +} + + +std::string urldecode(const std::string_view& str) +{ + std::string output; + for (size_t i = 0; i < str.size(); ++i) + { + const char c = str[i]; + if (c == '+') + output += ' '; + else if (c == '%' && str.size() - i >= 3 && + isHexDigit(str[i + 1]) && + isHexDigit(str[i + 2])) + { + output += unhexify(str[i + 1], str[i + 2]); + i += 2; + } + else + output += c; + } + return output; +} +} + + +std::string zen::xWwwFormUrlEncode(const std::vector>& paramPairs) +{ + std::string output; + for (const auto& [name, value] : paramPairs) + output += urlencode(name) + '=' + urlencode(value) + '&'; + //encode both key and value: https://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1 + if (!output.empty()) + output.pop_back(); + return output; +} + + +std::vector> zen::xWwwFormUrlDecode(const std::string_view str) +{ + std::vector> output; + + split(str, '&', [&](const std::string_view nvPair) + { + if (!nvPair.empty()) + output.emplace_back(urldecode(beforeFirst(nvPair, '=', IfNotFoundReturn::all)), + urldecode(afterFirst (nvPair, '=', IfNotFoundReturn::none))); + }); + return output; +} + + +HttpInputStream zen::sendHttpGet(const Zstring& url, const Zstring& userAgent, const Zstring& caCertFilePath) //throw SysError +{ + return sendHttpRequestImpl(url, nullptr /*postBuf*/, "" /*contentType*/, nullptr /*onPostBytesSent*/, userAgent, caCertFilePath); //throw SysError +} + + +HttpInputStream zen::sendHttpPost(const Zstring& url, const std::vector>& postParams, + const IoCallback& notifyUnbufferedIO /*throw X*/, + const Zstring& userAgent, + const Zstring& caCertFilePath) //throw SysError, X +{ + return sendHttpPost(url, xWwwFormUrlEncode(postParams), "application/x-www-form-urlencoded", notifyUnbufferedIO, userAgent, caCertFilePath); //throw SysError, X +} + + + +HttpInputStream zen::sendHttpPost(const Zstring& url, + const std::string& postBuf, + const std::string& contentType, + const IoCallback& notifyUnbufferedIO /*throw X*/, + const Zstring& userAgent, + const Zstring& caCertFilePath) //throw SysError, X +{ + return sendHttpRequestImpl(url, &postBuf, contentType, notifyUnbufferedIO, userAgent, caCertFilePath); //throw SysError, X +} + + +bool zen::internetIsAlive() //noexcept +{ + try + { + auto response = std::make_unique(Zstr("https://www.google.com/"), //https more appropriate than http for testing? (different ports!) + nullptr /*postParams*/, + "" /*contentType*/, + nullptr /*onPostBytesSent*/, + true /*disableGetCache*/, + Zstr("FreeFileSync"), + Zstring() /*caCertFilePath*/); //throw SysError + const int statusCode = response->getStatusCode(); + + //attention: google.com might redirect to https://consent.google.com => don't follow, just return "true"!!! + return statusCode / 100 == 2 || //e.g. 200 + statusCode / 100 == 3; //e.g. 301, 302, 303, 307... when in doubt, consider internet alive! + } + catch (SysError&) { return false; } +} + + +std::wstring zen::formatHttpError(int sc) +{ + const wchar_t* statusDescr = [&] //https://en.wikipedia.org/wiki/List_of_HTTP_status_codes + { + switch (sc) + { + case 300: return L"Multiple choices."; + case 301: return L"Moved permanently."; + case 302: return L"Moved temporarily."; + case 303: return L"See other"; + case 304: return L"Not modified."; + case 305: return L"Use proxy."; + case 306: return L"Switch proxy."; + case 307: return L"Temporary redirect."; + case 308: return L"Permanent redirect."; + + case 400: return L"Bad request."; + case 401: return L"Unauthorized."; + case 402: return L"Payment required."; + case 403: return L"Forbidden."; + case 404: return L"Not found."; + case 405: return L"Method not allowed."; + case 406: return L"Not acceptable."; + case 407: return L"Proxy authentication required."; + case 408: return L"Request timeout."; + case 409: return L"Conflict."; + case 410: return L"Gone."; + case 411: return L"Length required."; + case 412: return L"Precondition failed."; + case 413: return L"Payload too large."; + case 414: return L"URI too long."; + case 415: return L"Unsupported media type."; + case 416: return L"Range not satisfiable."; + case 417: return L"Expectation failed."; + case 418: return L"I'm a teapot."; + case 421: return L"Misdirected request."; + case 422: return L"Unprocessable entity."; + case 423: return L"Locked."; + case 424: return L"Failed dependency."; + case 425: return L"Too early."; + case 426: return L"Upgrade required."; + case 428: return L"Precondition required."; + case 429: return L"Too many requests."; + case 431: return L"Request header fields too large."; + case 451: return L"Unavailable for legal reasons."; + + case 500: return L"Internal server error."; + case 501: return L"Not implemented."; + case 502: return L"Bad gateway."; + case 503: return L"Service unavailable."; + case 504: return L"Gateway timeout."; + case 505: return L"HTTP version not supported."; + case 506: return L"Variant also negotiates."; + case 507: return L"Insufficient storage."; + case 508: return L"Loop detected."; + case 510: return L"Not extended."; + case 511: return L"Network authentication required."; + + //Cloudflare errors regarding origin server: + case 520: return L"Unknown error (Cloudflare)"; + case 521: return L"Web server is down (Cloudflare)"; + case 522: return L"Connection timed out (Cloudflare)"; + case 523: return L"Origin is unreachable (Cloudflare)"; + case 524: return L"A timeout occurred (Cloudflare)"; + case 525: return L"SSL handshake failed (Cloudflare)"; + case 526: return L"Invalid SSL certificate (Cloudflare)"; + case 527: return L"Railgun error (Cloudflare)"; + case 530: return L"Origin DNS error (Cloudflare)"; + + default: return L""; + } + }(); + + return formatSystemError("", L"HTTP status " + numberTo(sc), statusDescr); +} + + +bool zen::isValidEmail(const std::string_view& 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_view local = beforeLast(email, '@', IfNotFoundReturn::none); + std::string_view domain = afterLast(email, '@', IfNotFoundReturn::none); + //consider: "t@st"@email.com t\@st@email.com" + + auto stripComments = [](std::string_view& part) + { + if (startsWith(part, '(')) + part = afterFirst(part, ')', IfNotFoundReturn::none); + + if (endsWith(part, ')')) + part = beforeLast(part, '(', IfNotFoundReturn::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; + //--------------------------------------------------------------------- + + //we're not going to parse and validate this! + const bool quoted = (startsWith(local, '"') && endsWith(local, '"')) || + contains(local, '\\'); //e.g. "t\@st@email.com" + if (!quoted) + for (const std::string_view& comp : splitCpy(local, '.', SplitOnEmpty::allow)) + if (comp.empty() || !std::all_of(comp.begin(), comp.end(), [](const char c) + { + constexpr std::string_view printable("!#$%&'*+-/=?^_`{|}~"); + return isAsciiAlpha(c) || isDigit(c) || !isAsciiChar(c) || + contains(printable, c); + })) + 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_view& comp : splitCpy(domain, '.', SplitOnEmpty::allow)) + if (comp.empty() || comp.size() > 63 || + !std::all_of(comp.begin(), comp.end(), [](const char c) { return isAsciiAlpha(c) ||isDigit(c) || !isAsciiChar(c) || c == '-'; })) + return false; + } + + return true; +} + + +std::string zen::htmlSpecialChars(const std::string_view& 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) + { + case '&': output += "&" ; break; + case '"': output += """; break; + case '<': output += "<" ; break; + case '>': output += ">" ; break; + //case '\'': output += "'"; break; -> not encoded by default (needs ENT_QUOTES) + default: output += c; break; + } + return output; +} diff --git a/zen/http.h b/zen/http.h new file mode 100644 index 0000000..1943b12 --- /dev/null +++ b/zen/http.h @@ -0,0 +1,60 @@ +// ***************************************************************************** +// * 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 HTTP_H_879083425703425702 +#define HTTP_H_879083425703425702 + +#include "sys_error.h" +#include "serialize.h" + +namespace zen +{ +/* - Linux/macOS: init libcurl before use! + - safe to use on worker thread */ +class HttpInputStream +{ +public: + //zen/serialize.h unbuffered input stream concept: + size_t tryRead(void* buffer, size_t bytesToRead); //throw SysError; may return short; only 0 means EOF! CONTRACT: bytesToRead > 0! + + size_t getBlockSize() const; + + std::string readAll(const IoCallback& notifyUnbufferedIO /*throw X*/); //throw SysError, X + + class Impl; + HttpInputStream(std::unique_ptr&& pimpl); + HttpInputStream(HttpInputStream&&) noexcept = default; + ~HttpInputStream(); + +private: + std::unique_ptr pimpl_; +}; + + +HttpInputStream sendHttpGet(const Zstring& url, + const Zstring& userAgent, + const Zstring& caCertFilePath /*optional: enable certificate validation*/); //throw SysError + +HttpInputStream sendHttpPost(const Zstring& url, + const std::vector>& postParams, const IoCallback& notifyUnbufferedIO /*throw X*/, + const Zstring& userAgent, + const Zstring& caCertFilePath /*optional: enable certificate validation*/); //throw SysError, X + +HttpInputStream sendHttpPost(const Zstring& url, + const std::string& postBuf, const std::string& contentType, const IoCallback& notifyUnbufferedIO /*throw X*/, + const Zstring& userAgent, + const Zstring& caCertFilePath /*optional: enable certificate validation*/); //throw SysError, X + +bool internetIsAlive(); //noexcept +std::wstring formatHttpError(int httpStatus); +bool isValidEmail(const std::string_view& email); +std::string htmlSpecialChars(const std::string_view& str); + +std::string xWwwFormUrlEncode(const std::vector>& paramPairs); +std::vector> xWwwFormUrlDecode(const std::string_view str); +} + +#endif //HTTP_H_879083425703425702 diff --git a/zen/i18n.h b/zen/i18n.h new file mode 100644 index 0000000..28b8c08 --- /dev/null +++ b/zen/i18n.h @@ -0,0 +1,115 @@ +// ***************************************************************************** +// * 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 I18_N_H_3843489325044253425456 +#define I18_N_H_3843489325044253425456 + +#include "globals.h" +#include "string_tools.h" +#include "format_unit.h" + + +//minimal layer enabling text translation - without platform/library dependencies! + +#define ZEN_TRANS_CONCAT_SUB(X, Y) X ## Y +#define _(s) zen::translate(ZEN_TRANS_CONCAT_SUB(L, s)) +#define _P(s, p, n) zen::translate(ZEN_TRANS_CONCAT_SUB(L, s), ZEN_TRANS_CONCAT_SUB(L, p), n) +//source and translation are required to use %x as number placeholder +//for plural form, which will be substituted automatically!!! + + static_assert(WXINTL_NO_GETTEXT_MACRO, "...must be defined to deactivate wxWidgets underscore macro"); + +namespace zen +{ +//implement handler to enable program-wide localizations: +struct TranslationHandler +{ + //THREAD-SAFETY: "const" member must model thread-safe access! + TranslationHandler() {} + virtual ~TranslationHandler() {} + + //C++11: std::wstring should be thread-safe like an int + virtual std::wstring translate(const std::wstring& text) const = 0; //simple translation + virtual std::wstring translate(const std::wstring& singular, const std::wstring& plural, int64_t n) const = 0; + + virtual bool layoutIsRtl() const = 0; //right-to-left? e.g. Hebrew, Arabic + +private: + TranslationHandler (const TranslationHandler&) = delete; + TranslationHandler& operator=(const TranslationHandler&) = delete; +}; + +void setTranslator(std::unique_ptr&& newHandler); //take ownership +std::shared_ptr getTranslator(); + + + + + + + + +//######################## implementation ############################## +namespace impl +{ +//getTranslator() may be called even after static objects of this translation unit are destroyed! +inline constinit Global globalTranslationHandler; +} + +inline +std::shared_ptr getTranslator() +{ + return impl::globalTranslationHandler.get(); +} + + +inline +void setTranslator(std::unique_ptr&& newHandler) +{ + impl::globalTranslationHandler.set(std::move(newHandler)); +} + + +inline +std::wstring translate(const std::wstring& text) +{ + if (std::shared_ptr t = getTranslator()) //std::shared_ptr => temporarily take (shared) ownership while using the interface! + return t->translate(text); + return text; +} + + +//translate plural forms: "%x day" "%x days" +//returns "1 day" if n == 1; "123 days" if n == 123 for english language +template inline +std::wstring translate(const std::wstring& singular, const std::wstring& plural, T n) +{ + static_assert(sizeof(n) <= sizeof(int64_t)); + const auto n64 = static_cast(n); + + assert(contains(plural, L"%x")); + + if (std::shared_ptr t = getTranslator()) + { + std::wstring translation = t->translate(singular, plural, n64); + assert(!contains(translation, L"%x")); + return translation; + } + //fallback: + return replaceCpy(std::abs(n64) == 1 ? singular : plural, L"%x", formatNumber(n)); +} + + +inline +bool languageLayoutIsRtl() +{ + if (std::shared_ptr t = getTranslator()) + return t->layoutIsRtl(); + return false; +} +} + +#endif //I18_N_H_3843489325044253425456 diff --git a/zen/json.h b/zen/json.h new file mode 100644 index 0000000..856aa87 --- /dev/null +++ b/zen/json.h @@ -0,0 +1,616 @@ +// ***************************************************************************** +// * 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 JSON_H_0187348321748321758934215734 +#define JSON_H_0187348321748321758934215734 + +#include +#include + + +namespace zen +{ +//Spec: https://tools.ietf.org/html/rfc8259 +//Test: http://seriot.ch/parsing_json.php +struct JsonValue; + +class JsonObject +{ +public: + using Item = std::list>; //must NOT invalidate references used by "valuesByName_"! + //¹) careful: should be const, but fails to compile in debug => do not allow write access by the API: + + Range getItems() const { return {values_.begin(), values_.end()}; } + + bool empty() const { return values_.empty(); } + + const JsonValue* get(std::string_view name) const; + + template + void set(std::string&& name, T&& value); + + //------------------------------------------------------------- +#warning("review default operators") + JsonObject() = default; + + JsonObject(const JsonObject& other) : values_(other.values_) { initLookup(); } + + JsonObject& operator=(const JsonObject& other) { JsonObject(other).swap(*this); return *this; } + + JsonObject (JsonObject&& tmp) noexcept { swap(tmp); } + JsonObject& operator=(JsonObject&& tmp) noexcept { swap(tmp); return *this; } + +private: + void swap(JsonObject& other) noexcept { values_.swap(other.values_); valuesByName_.swap(other.valuesByName_); } + + void initLookup(); + + //"[...] most implementations of JSON libraries do not accept duplicate keys [...]" => fine! + Item values_; //in order of insertion + std::unordered_map valuesByName_; //alternate view for lookup +}; + + +struct JsonValue +{ + enum class Type + { + null, // + boolean, //primitive types + number, // + string, // + array, + object, + }; + + /**/ JsonValue() {} + 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(num)) {} + explicit JsonValue(int64_t num) : type(Type::number), primVal(numberTo(num)) {} + explicit JsonValue(double num) : type(Type::number), primVal(numberTo(num)) {} + explicit JsonValue(std::string str) : type(Type::string), primVal(std::move(str)) {} //unifying assignment + explicit JsonValue(const char* str) : type(Type::string), primVal(str) {} + explicit JsonValue(const void*) = delete; //catch usage errors e.g. const int* -> JsonValue(bool) + //explicit JsonValue(std::initializer_list initList) : type(Type::array), arrayVal(initList) {} => empty list is ambiguous + explicit JsonValue(std::vector initList) : type(Type::array), arrayVal(std::move(initList)) {} //unifying assignment + + + Type type = Type::null; + std::string primVal; //for primitive types + std::vector arrayVal; + JsonObject objectVal; +}; + + +std::string serializeJson(const JsonValue& jval, + const std::string& lineBreak = "\n", + const std::string& indent = " "); //noexcept + + +struct JsonParsingError +{ + JsonParsingError(size_t rowNo, size_t colNo) : row(rowNo), col(colNo) {} + const size_t row; //beginning with 0 + const size_t col; // +}; +JsonValue parseJson(const std::string& stream); //throw JsonParsingError + + + +//---------------------- implementation ---------------------- + +//helper functions for JsonValue access: +inline +const JsonValue* getChildFromJsonObject(const JsonValue& jvalue, const std::string& name) +{ + return jvalue.type != JsonValue::Type::object ? nullptr : jvalue.objectVal.get(name); +} + + +inline +std::optional getPrimitiveFromJsonObject(const JsonValue& jvalue, const std::string& name) +{ + if (const JsonValue* childValue = getChildFromJsonObject(jvalue, name)) + if (childValue->type != JsonValue::Type::object && + childValue->type != JsonValue::Type::array) + return childValue->primVal; + return std::nullopt; +} + + +inline +void JsonObject::initLookup() +{ + assert(valuesByName_.empty()); + for (auto it = values_.begin(); it != values_.end(); ++it) + valuesByName_.emplace(it->first, it); +} + + +inline +const JsonValue* JsonObject::get(std::string_view name) const +{ + auto it = valuesByName_.find(name); + return it == valuesByName_.end() ? nullptr : &(it->second->second); +} + + +template inline +void JsonObject::set(std::string&& name, T&& value) +{ + auto it = valuesByName_.find(name); + if (it != valuesByName_.end()) + it->second->second = JsonValue(std::forward(value)); + else + { + //values_.emplace_back(std::move(name), std::forward(value)); -> not yet on macOS/clang + values_.push_back({std::move(name), JsonValue(std::forward(value))}); + valuesByName_.emplace(values_.back().first, --values_.end()); + } +} + + +namespace json_impl +{ +namespace +{ +[[nodiscard]] std::string jsonEscape(const std::string& str) +{ + std::string output; + for (const char c : str) + switch (c) + { + case '\\': output += "\\\\"; break; // + case '"': output += "\\\""; break; //escaping mandatory + + case '\b': output += "\\b"; break; // + case '\f': output += "\\f"; break; // + case '\n': output += "\\n"; break; //prefer compact escaping + case '\r': output += "\\r"; break; // + case '\t': output += "\\t"; break; // + + default: + if (static_cast(c) < 32) + { + const auto [high, low] = hexify(c); + output += "\\u00"; + output += high; + output += low; + } + else + output += c; + break; + } + return output; +} + + +[[nodiscard]] std::string jsonUnescape(const std::string& str) +{ + std::string output; + std::basic_string utf16Buf; + + auto flushUtf16 = [&] + { + if (!utf16Buf.empty()) + { + UtfDecoder decoder(utf16Buf.c_str(), utf16Buf.size()); + while (std::optional cp = decoder.getNext()) + codePointToUtf(*cp, [&](const char c) { output += c; }); + utf16Buf.clear(); + } + }; + auto writeOut = [&](const char c) + { + flushUtf16(); + output += c; + }; + + for (auto it = str.begin(); it != str.end(); ++it) + { + const char c = *it; + if (c == '\\') + { + ++it; + if (it == str.end()) //unexpected end! + { + writeOut(c); + break; + } + + const char c2 = *it; + switch (c2) + { + case '\\': + case '"': + case '/': writeOut(c2); break; + case 'b': writeOut('\b'); break; + case 'f': writeOut('\f'); break; + case 'n': writeOut('\n'); break; + case 'r': writeOut('\r'); break; + case 't': writeOut('\t'); break; + default: + if (c2 == 'u' && + str.end() - it >= 5 && + isHexDigit(it[1]) && + isHexDigit(it[2]) && + isHexDigit(it[3]) && + isHexDigit(it[4])) + { + utf16Buf += static_cast(static_cast(unhexify(it[1], it[2])) * 256 + + static_cast(unhexify(it[3], it[4]))); + it += 4; + } + else //unknown escape sequence! + { + writeOut(c); + writeOut(c2); + } + break; + } + } + else + writeOut(c); + } + flushUtf16(); + return output; +} + + +void serialize(const JsonValue& jval, std::string& stream, + const std::string& lineBreak, + const std::string& indent, + size_t indentLevel) +{ + //unlike our XML serialization the caller is repsonsible for line breaks and indentation of *first* line + auto writeIndent = [&](size_t level) + { + for (size_t i = 0; i < level; ++i) + stream += indent; + }; + + switch (jval.type) + { + case JsonValue::Type::null: + stream += "null"; + break; + + case JsonValue::Type::boolean: + case JsonValue::Type::number: + stream += jval.primVal; + break; + + case JsonValue::Type::string: + stream += '"' + jsonEscape(jval.primVal) + '"'; + break; + + case JsonValue::Type::object: + stream += '{'; + if (!jval.objectVal.empty()) + { + bool first = true; + + for (const auto& [childName, childValue] : jval.objectVal.getItems()) + { + if (!std::exchange(first, false)) + stream += ','; + + stream += lineBreak; + writeIndent(indentLevel + 1); + + stream += '"' + jsonEscape(childName) + "\":"; + + if ((childValue.type == JsonValue::Type::object && !childValue.objectVal.empty()) || + (childValue.type == JsonValue::Type::array && !childValue.arrayVal .empty())) + { + stream += lineBreak; + writeIndent(indentLevel + 1); + } + else if (!indent.empty()) + stream += ' '; + + serialize(childValue, stream, lineBreak, indent, indentLevel + 1); + } + stream += lineBreak; + writeIndent(indentLevel); + } + stream += '}'; + break; + + case JsonValue::Type::array: + stream += '['; + if (!jval.arrayVal.empty()) + { + for (auto it = jval.arrayVal.begin(); it != jval.arrayVal.end(); ++it) + { + const auto& childValue = *it; + + if (it != jval.arrayVal.begin()) + stream += ','; + + stream += lineBreak; + writeIndent(indentLevel + 1); + + serialize(childValue, stream, lineBreak, indent, indentLevel + 1); + } + stream += lineBreak; + writeIndent(indentLevel); + } + stream += ']'; + break; + } +} +} +} + + +inline +std::string serializeJson(const JsonValue& jval, + const std::string& lineBreak, + const std::string& indent) //noexcept +{ + std::string output; + json_impl::serialize(jval, output, lineBreak, indent, 0); + output += lineBreak; + return output; +} + + +namespace json_impl +{ +enum class TokenType +{ + eof, + curlyOpen, + curlyClose, + squareOpen, + squareClose, + colon, + comma, + string, // + number, //primitive types + boolean, // + null, // +}; + +struct Token +{ + Token(TokenType t) : type(t) {} + + TokenType type; + std::string primVal; //for primitive types +}; + +class Scanner +{ +public: + explicit Scanner(const std::string& stream) : stream_(stream), pos_(stream_.begin()) + { + if (zen::startsWith(stream_, BYTE_ORDER_MARK_UTF8)) + pos_ += BYTE_ORDER_MARK_UTF8.size(); + } + + Token getNextToken() //throw JsonParsingError + { + //skip whitespace + pos_ = std::find_if_not(pos_, stream_.end(), isJsonWhiteSpace); + + if (pos_ == stream_.end()) + return TokenType::eof; + + if (*pos_ == '{') return ++pos_, TokenType::curlyOpen; + if (*pos_ == '}') return ++pos_, TokenType::curlyClose; + if (*pos_ == '[') return ++pos_, TokenType::squareOpen; + if (*pos_ == ']') return ++pos_, TokenType::squareClose; + if (*pos_ == ':') return ++pos_, TokenType::colon; + if (*pos_ == ',') return ++pos_, TokenType::comma; + if (startsWith("null")) return pos_ += 4, Token(TokenType::null); + + if (startsWith("true")) + { + pos_ += 4; + Token tk(TokenType::boolean); + tk.primVal = "true"; + return tk; + } + if (startsWith("false")) + { + pos_ += 5; + Token tk(TokenType::boolean); + tk.primVal = "false"; + return tk; + } + + if (*pos_ == '"') + { + for (auto it = ++pos_; it != stream_.end(); ++it) + if (*it == '"') + { + Token tk(TokenType::string); + tk.primVal = jsonUnescape({pos_, it}); + pos_ = ++it; + return tk; + } + else if (*it == '\\') //skip next char + if (++it == stream_.end()) + break; + + throw JsonParsingError(posRow(), posCol()); + } + + //expect a number: + const auto itNumEnd = std::find_if_not(pos_, stream_.end(), isJsonNumDigit); + if (itNumEnd == pos_) + throw JsonParsingError(posRow(), posCol()); + + Token tk(TokenType::number); + tk.primVal.assign(pos_, itNumEnd); + pos_ = itNumEnd; + return tk; + } + + size_t posRow() const //current row beginning with 0 + { + const size_t crSum = std::count(stream_.begin(), pos_, '\r'); //carriage returns + const size_t nlSum = std::count(stream_.begin(), pos_, '\n'); //new lines + assert(crSum == 0 || nlSum == 0 || crSum == nlSum); + return std::max(crSum, nlSum); //be compatible with Linux/Mac/Win + } + + size_t posCol() const //current col beginning with 0 + { + //seek beginning of line + for (auto it = pos_; it != stream_.begin(); ) + { + --it; + if (isLineBreak(*it)) + return pos_ - it - 1; + } + return pos_ - stream_.begin(); + } + +private: + Scanner (const Scanner&) = delete; + Scanner& operator=(const Scanner&) = delete; + + static bool isJsonWhiteSpace(const char c) { return c == ' ' || c == '\t' || c == '\r' || c == '\n'; } + static bool isJsonNumDigit (const char c) { return ('0' <= c && c <= '9') || c == '-' || c == '+' || c == '.' || c == 'e'|| c == 'E'; } + + bool startsWith(const std::string& prefix) const + { + return zen::startsWith(makeStringView(pos_, stream_.end()), prefix); + } + + const std::string stream_; + std::string::const_iterator pos_; +}; + + +class JsonParser +{ +public: + explicit JsonParser(const std::string& stream) : + scn_(stream), + tk_(scn_.getNextToken()) {} //throw JsonParsingError + + JsonValue parse() //throw JsonParsingError + { + JsonValue jval = parseValue(); //throw JsonParsingError + expectToken(TokenType::eof); // + return jval; + } + +private: + JsonParser (const JsonParser&) = delete; + JsonParser& operator=(const JsonParser&) = delete; + + JsonValue parseValue() //throw JsonParsingError + { + if (token().type == TokenType::curlyOpen) + { + nextToken(); //throw JsonParsingError + + JsonValue jval(JsonValue::Type::object); + + if (token().type != TokenType::curlyClose) + for (;;) + { + expectToken(TokenType::string); //throw JsonParsingError + std::string name = token().primVal; + nextToken(); //throw JsonParsingError + + consumeToken(TokenType::colon); //throw JsonParsingError + + JsonValue value = parseValue(); //throw JsonParsingError + jval.objectVal.set(std::move(name), std::move(value)); + + if (token().type != TokenType::comma) + break; + nextToken(); //throw JsonParsingError + } + + consumeToken(TokenType::curlyClose); //throw JsonParsingError + return jval; + } + else if (token().type == TokenType::squareOpen) + { + nextToken(); //throw JsonParsingError + + JsonValue jval(JsonValue::Type::array); + + if (token().type != TokenType::squareClose) + for (;;) + { + JsonValue value = parseValue(); //throw JsonParsingError + jval.arrayVal.emplace_back(std::move(value)); + + if (token().type != TokenType::comma) + break; + nextToken(); //throw JsonParsingError + } + + consumeToken(TokenType::squareClose); //throw JsonParsingError + return jval; + } + else if (token().type == TokenType::string) + { + JsonValue jval(token().primVal); + nextToken(); //throw JsonParsingError + return jval; + } + else if (token().type == TokenType::number) + { + JsonValue jval(JsonValue::Type::number); + jval.primVal = token().primVal; + nextToken(); //throw JsonParsingError + return jval; + } + else if (token().type == TokenType::boolean) + { + JsonValue jval(JsonValue::Type::boolean); + jval.primVal = token().primVal; + nextToken(); //throw JsonParsingError + return jval; + } + else if (token().type == TokenType::null) + { + nextToken(); //throw JsonParsingError + return JsonValue(); + } + else //unexpected token + throw JsonParsingError(scn_.posRow(), scn_.posCol()); + } + + const Token& token() const { return tk_; } + + void nextToken() { tk_ = scn_.getNextToken(); } //throw JsonParsingError + + void expectToken(TokenType t) //throw JsonParsingError + { + if (token().type != t) + throw JsonParsingError(scn_.posRow(), scn_.posCol()); + } + + void consumeToken(TokenType t) //throw JsonParsingError + { + expectToken(t); //throw JsonParsingError + nextToken(); // + } + + Scanner scn_; + Token tk_; +}; +} + +inline +JsonValue parseJson(const std::string& stream) //throw JsonParsingError +{ + return json_impl::JsonParser(stream).parse(); //throw JsonParsingError +} +} + +#endif //JSON_H_0187348321748321758934215734 diff --git a/zen/legacy_compiler.cpp b/zen/legacy_compiler.cpp new file mode 100644 index 0000000..6c5489d --- /dev/null +++ b/zen/legacy_compiler.cpp @@ -0,0 +1,28 @@ +// ***************************************************************************** +// * 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 "legacy_compiler.h" +#include +/* 1. including in header file blows up VC++: + - string_tools.h: "An internal error has occurred in the compiler. (compiler file 'd:\agent\_work\1\s\src\vctools\Compiler\Utc\src\p2\p2symtab.c', line 2618)" + - PCH: "fatal error C1076: compiler limit: internal heap limit reached" + => include in separate compilation unit + 2. Disable "C/C++ -> Code Generation -> Smaller Type Check" (and PCH usage!), at least for this compilation unit: https://github.com/microsoft/STL/pull/171 */ + +double zen::fromChars(const char* first, const char* last) +{ + double num = 0; + [[maybe_unused]] const std::from_chars_result rv = std::from_chars(first, last, num); + return num; +} + + +const char* zen::toChars(char* first, char* last, double num) +{ + const std::to_chars_result rv = std::to_chars(first, last, num); + return rv.ec == std::errc{} ? rv.ptr : first; +} + diff --git a/zen/legacy_compiler.h b/zen/legacy_compiler.h new file mode 100644 index 0000000..2e7ee8e --- /dev/null +++ b/zen/legacy_compiler.h @@ -0,0 +1,81 @@ +// ***************************************************************************** +// * 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 LEGACY_COMPILER_H_839567308565656789 +#define LEGACY_COMPILER_H_839567308565656789 + +#include //contains all __cpp_lib_ macros +#include + +/* C++ standard conformance: + https://en.cppreference.com/w/cpp/feature_test + https://en.cppreference.com/w/User:D41D8CD98F/feature_testing_macros + https://isocpp.org/std/standing-documents/sd-6-sg10-feature-test-recommendations + + MSVC https://docs.microsoft.com/en-us/cpp/overview/visual-cpp-language-conformance + + GCC https://gcc.gnu.org/projects/cxx-status.html + libstdc++ https://gcc.gnu.org/onlinedocs/libstdc++/manual/status.html + + Clang https://clang.llvm.org/cxx_status.html + Xcode https://developer.apple.com/xcode/cpp + libc++ https://libcxx.llvm.org/cxx2a_status.html */ + + +namespace std +{ + + + +//W(hy)TF is this not standard? https://stackoverflow.com/a/47735624 +template inline +basic_string operator+(basic_string&& lhs, const basic_string_view rhs) +{ return std::move(lhs.append(rhs.begin(), rhs.end())); } //the move *is* needed!!! + +//template inline +//basic_string operator+(const basic_string& lhs, const basic_string_view& rhs) { return basic_string(lhs) + rhs; } +//-> somewhat inefficient: single memory allocation should suffice!!! +} +//--------------------------------------------------------------------------------- + +//support for std::string::resize_and_overwrite() + #define ZEN_HAVE_RESIZE_AND_OVERWRITE 1 + +namespace zen +{ +//reference a sub-string for consumption by zen string_tools +//=> std::string_view seems decent, but of course fucks up in one regard: construction + +//std::string_view(first, last) is not available before C++20 (at least on clang) +template inline +auto makeStringView(Iterator first, Iterator last) +{ + using CharType = std::remove_cvref_t; + + return std::basic_string_view(first != last ? &*first : + reinterpret_cast(0x1000), /*Win32 APIs like CompareStringOrdinal() choke on nullptr!*/ + last - first); +} +//std::string_view(char*, int) fails to compile! expected size_t as second parameter +template inline +auto makeStringView(Iterator first, size_t len) { return makeStringView(first, first + len); } + + + +double fromChars(const char* first, const char* last); +const char* toChars(char* first, char* last, double num); +} + + +#if 0 //neat: supported on MSVC and GCC, but not yet on Clang +auto closure = [](this auto&& self) +{ + self(); //just call ourself until the stack overflows + //e.g. use for: deleteEmptyFolderTask, removeFolderRecursionImpl, scheduleMoreTasks, traverse +}; +#endif + +#endif //LEGACY_COMPILER_H_839567308565656789 diff --git a/zen/open_ssl.cpp b/zen/open_ssl.cpp new file mode 100644 index 0000000..536e7f5 --- /dev/null +++ b/zen/open_ssl.cpp @@ -0,0 +1,871 @@ +// ***************************************************************************** +// * 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 "open_ssl.h" +#include //std::endian (needed for macOS) +#include "base64.h" +#include "thread.h" +#include "argon2.h" +#include "serialize.h" +#include +#include +#include +#include +#include +#include +#include + + +using namespace zen; + + +namespace +{ +#ifndef OPENSSL_THREADS + #error FFS, we are royally screwed! +#endif + +/* Sign a file using SHA-256: + openssl dgst -sha256 -sign private.pem -out file.sig file.txt + + verify the signature: (caveat: public key expected to be in pkix format!) + openssl dgst -sha256 -verify public.pem -signature file.sig file.txt */ + + +std::wstring formatOpenSSLError(const char* functionName, unsigned long ec) +{ + char errorBuf[256] = {}; //== buffer size used by ERR_error_string(); err.c: it seems the message uses at most ~200 bytes + ::ERR_error_string_n(ec, errorBuf, sizeof(errorBuf)); //includes null-termination + + return formatSystemError(functionName, replaceCpy(_("Error code %x"), L"%x", numberTo(ec)), utfTo(errorBuf)); +} + + +std::wstring formatLastOpenSSLError(const char* functionName) +{ + const auto ec = ::ERR_peek_last_error(); //"returns latest error code from the thread's error queue without modifying it" - unlike ERR_get_error() + //ERR_get_error: "returns the earliest error code from the thread's error queue and removes the entry. + // This function can be called repeatedly until there are no more error codes to return." + ::ERR_clear_error(); //clean up for next OpenSSL operation on this thread + return formatOpenSSLError(functionName, ec); +} +} + + +void zen::openSslInit() +{ + //official Wiki: https://wiki.openssl.org/index.php/Library_Initialization + //see apps_shutdown(): https://github.com/openssl/openssl/blob/master/apps/openssl.c + //see Curl_ossl_cleanup(): https://github.com/curl/curl/blob/master/lib/vtls/openssl.c + + assert(runningOnMainThread()); + //explicitly init OpenSSL on main thread: seems to initialize atomically! But it still might help to avoid issues: + //https://www.openssl.org/docs/manmaster/man3/OPENSSL_init_ssl.html + if (::OPENSSL_init_ssl(OPENSSL_INIT_SSL_DEFAULT | OPENSSL_INIT_NO_LOAD_CONFIG, nullptr) != 1) + logExtraError(_("Error during process initialization.") + L"\n\n" + formatLastOpenSSLError("OPENSSL_init_ssl")); +} + + +void zen::openSslTearDown() {} +//OpenSSL 1.1.0+ deprecates all clean up functions +//=> so much the theory, in practice it leaks, of course: https://github.com/openssl/openssl/issues/6283 +namespace +{ +struct OpenSslThreadCleanUp +{ + ~OpenSslThreadCleanUp() + { + ::OPENSSL_thread_stop(); + } +}; +thread_local OpenSslThreadCleanUp tearDownOpenSslThreadData; + +//================================================================================ + +std::shared_ptr generateRsaKeyPair(int bits) //throw SysError +{ + EVP_PKEY_CTX* keyCtx = ::EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, //int id + nullptr); //ENGINE* e + if (!keyCtx) + throw SysError(formatLastOpenSSLError("EVP_PKEY_CTX_new_id")); + ZEN_ON_SCOPE_EXIT(::EVP_PKEY_CTX_free(keyCtx)); + + if (::EVP_PKEY_keygen_init(keyCtx) != 1) + throw SysError(formatLastOpenSSLError("EVP_PKEY_keygen_init")); + + //"RSA keys set the key length during key generation rather than parameter generation" + if (::EVP_PKEY_CTX_set_rsa_keygen_bits(keyCtx, bits) <= 0) //"[...] return a positive value for success" => effectively returns "1" + throw SysError(formatLastOpenSSLError("EVP_PKEY_CTX_set_rsa_keygen_bits")); + + EVP_PKEY* keyPair = nullptr; + if (::EVP_PKEY_keygen(keyCtx, &keyPair) != 1) + throw SysError(formatLastOpenSSLError("EVP_PKEY_keygen")); + + return std::shared_ptr(keyPair, ::EVP_PKEY_free); +} + +//================================================================================ + +std::shared_ptr streamToKey(const std::string_view keyStream, RsaStreamType streamType, bool publicKey) //throw SysError +{ + switch (streamType) + { + case RsaStreamType::pkix: + { + BIO* bio = ::BIO_new_mem_buf(keyStream.data(), static_cast(keyStream.size())); + if (!bio) + throw SysError(formatLastOpenSSLError("BIO_new_mem_buf")); + ZEN_ON_SCOPE_EXIT(::BIO_free_all(bio)); + + if (EVP_PKEY* evp = (publicKey ? + ::PEM_read_bio_PUBKEY : + ::PEM_read_bio_PrivateKey) + (bio, //BIO* bp + nullptr, //EVP_PKEY** x + nullptr, //pem_password_cb* cb + nullptr)) //void* u + return std::shared_ptr(evp, ::EVP_PKEY_free); + throw SysError(formatLastOpenSSLError(publicKey ? "PEM_read_bio_PUBKEY" : "PEM_read_bio_PrivateKey")); + } + + case RsaStreamType::pkcs1: + { + EVP_PKEY* evp = nullptr; + auto guardEvp = makeGuard([&] { if (evp) ::EVP_PKEY_free(evp); }); + + const int selection = publicKey ? OSSL_KEYMGMT_SELECT_PUBLIC_KEY : OSSL_KEYMGMT_SELECT_PRIVATE_KEY; + + OSSL_DECODER_CTX* decCtx = ::OSSL_DECODER_CTX_new_for_pkey(&evp, //EVP_PKEY** pkey + "PEM", //const char* input_type + nullptr, //const char* input_struct + "RSA", //const char* keytype + selection, //int selection + nullptr, //OSSL_LIB_CTX* libctx + nullptr); //const char* propquery + if (!decCtx) + throw SysError(formatLastOpenSSLError("OSSL_DECODER_CTX_new_for_pkey")); + ZEN_ON_SCOPE_EXIT(::OSSL_DECODER_CTX_free(decCtx)); + +#if 0 //key stream is password-protected? => OSSL_DECODER_CTX_set_passphrase() + if (!password.empty()) + if (::OSSL_DECODER_CTX_set_passphrase(decCtx, //OSSL_DECODER_CTX *ctx + reinterpret_cast(password.c_str()), //const unsigned char* kstr + password.size()) != 1) //size_t klen + throw SysError(formatLastOpenSSLError("OSSL_DECODER_CTX_set_passphrase")); +#endif + + const unsigned char* keyBuf = reinterpret_cast(keyStream.data()); + size_t keyLen = keyStream.size(); + if (::OSSL_DECODER_from_data(decCtx, &keyBuf, &keyLen) != 1) + throw SysError(formatLastOpenSSLError("OSSL_DECODER_from_data")); + + guardEvp.dismiss(); //pass ownership + return std::shared_ptr(evp, ::EVP_PKEY_free); // + } + + case RsaStreamType::raw: + break; + } + + auto tmp = reinterpret_cast(keyStream.data()); + EVP_PKEY* evp = (publicKey ? ::d2i_PublicKey : ::d2i_PrivateKey)(EVP_PKEY_RSA, //int type + nullptr, //EVP_PKEY** a + &tmp, /*changes tmp pointer itself!*/ //const unsigned char** pp + static_cast(keyStream.size())); //long length + if (!evp) + throw SysError(formatLastOpenSSLError(publicKey ? "d2i_PublicKey" : "d2i_PrivateKey")); + return std::shared_ptr(evp, ::EVP_PKEY_free); +} + +//================================================================================ + +std::string keyToStream(const EVP_PKEY* evp, RsaStreamType streamType, bool publicKey) //throw SysError +{ + //assert(::EVP_PKEY_get_base_id(evp) == EVP_PKEY_RSA); + + switch (streamType) + { + case RsaStreamType::pkix: + { + //fix OpenSSL API inconsistencies: + auto PEM_write_bio_PrivateKey2 = [](BIO* bio, const EVP_PKEY* key) + { + return ::PEM_write_bio_PrivateKey(bio, //BIO* bp + key, //const EVP_PKEY* x + nullptr, //const EVP_CIPHER* enc + nullptr, //const unsigned char* kstr + 0, //int klen + nullptr, //pem_password_cb* cb + nullptr); //void* u + }; + + BIO* bio = ::BIO_new(BIO_s_mem()); + if (!bio) + throw SysError(formatLastOpenSSLError("BIO_new")); + ZEN_ON_SCOPE_EXIT(::BIO_free_all(bio)); + + if ((publicKey ? + ::PEM_write_bio_PUBKEY : + PEM_write_bio_PrivateKey2)(bio, evp) != 1) + throw SysError(formatLastOpenSSLError(publicKey ? "PEM_write_bio_PUBKEY" : "PEM_write_bio_PrivateKey")); + //--------------------------------------------- + const int keyLen = BIO_pending(bio); + if (keyLen < 0) + throw SysError(formatLastOpenSSLError("BIO_pending")); + if (keyLen == 0) + throw SysError(formatSystemError("BIO_pending", L"", L"No more error details.")); //no more error details + + std::string keyStream(keyLen, '\0'); + + if (::BIO_read(bio, keyStream.data(), keyLen) != keyLen) + throw SysError(formatLastOpenSSLError("BIO_read")); + return keyStream; + } + + case RsaStreamType::pkcs1: + { + const int selection = publicKey ? OSSL_KEYMGMT_SELECT_PUBLIC_KEY : OSSL_KEYMGMT_SELECT_PRIVATE_KEY; + + OSSL_ENCODER_CTX* encCtx = ::OSSL_ENCODER_CTX_new_for_pkey(evp, //const EVP_PKEY* pkey + selection, //int selection + "PEM", //const char* output_type + nullptr, //const char* output_structure + nullptr); //const char* propquery + if (!encCtx) + throw SysError(formatLastOpenSSLError("OSSL_ENCODER_CTX_new_for_pkey")); + ZEN_ON_SCOPE_EXIT(::OSSL_ENCODER_CTX_free(encCtx)); + + //password-protect stream? => OSSL_ENCODER_CTX_set_passphrase() + + unsigned char* keyBuf = nullptr; + size_t keyLen = 0; + if (::OSSL_ENCODER_to_data(encCtx, &keyBuf, &keyLen) != 1) + throw SysError(formatLastOpenSSLError("OSSL_ENCODER_to_data")); + ZEN_ON_SCOPE_EXIT(::OPENSSL_free(keyBuf)); + + return {reinterpret_cast(keyBuf), keyLen}; + } + + case RsaStreamType::raw: + break; + } + + unsigned char* buf = nullptr; + const int bufSize = (publicKey ? ::i2d_PublicKey : ::i2d_PrivateKey)(evp, &buf); + if (bufSize <= 0) + throw SysError(formatLastOpenSSLError(publicKey ? "i2d_PublicKey" : "i2d_PrivateKey")); + ZEN_ON_SCOPE_EXIT(::OPENSSL_free(buf)); //memory is only allocated for bufSize > 0 + + return {reinterpret_cast(buf), static_cast(bufSize)}; +} + +//================================================================================ + +std::string createHash(const std::string_view str, const EVP_MD* type) //throw SysError +{ + std::string output(EVP_MAX_MD_SIZE, '\0'); + unsigned int bytesWritten = 0; +#if 1 + //https://www.openssl.org/docs/manmaster/man3/EVP_Digest.html + if (::EVP_Digest(str.data(), //const void* data + str.size(), //size_t count + reinterpret_cast(output.data()), //unsigned char* md + &bytesWritten, //unsigned int* size + type, //const EVP_MD* type + nullptr) != 1) //ENGINE* impl + throw SysError(formatLastOpenSSLError("EVP_Digest")); +#else //streaming version + EVP_MD_CTX* mdctx = ::EVP_MD_CTX_new(); + if (!mdctx) + throw SysError(formatSystemError("EVP_MD_CTX_new", L"", L"No more error details.")); //no more error details + ZEN_ON_SCOPE_EXIT(::EVP_MD_CTX_free(mdctx)); + + if (::EVP_DigestInit(mdctx, //EVP_MD_CTX* ctx + type) != 1) //const EVP_MD* type + throw SysError(formatLastOpenSSLError("EVP_DigestInit")); + + if (::EVP_DigestUpdate(mdctx, //EVP_MD_CTX* ctx + str.data(), //const void* + str.size()) != 1) //size_t cnt + throw SysError(formatLastOpenSSLError("EVP_DigestUpdate")); + + if (::EVP_DigestFinal_ex(mdctx, //EVP_MD_CTX* ctx + reinterpret_cast(output.data()), //unsigned char* md + &bytesWritten) != 1) //unsigned int* s + throw SysError(formatLastOpenSSLError("EVP_DigestFinal_ex")); +#endif + output.resize(bytesWritten); + return output; +} + + +std::string createSignature(const std::string_view message, EVP_PKEY* privateKey) //throw SysError +{ + //https://www.openssl.org/docs/manmaster/man3/EVP_DigestSign.html + EVP_MD_CTX* mdctx = ::EVP_MD_CTX_new(); + if (!mdctx) + throw SysError(formatSystemError("EVP_MD_CTX_new", L"", L"No more error details.")); //no more error details + ZEN_ON_SCOPE_EXIT(::EVP_MD_CTX_free(mdctx)); + + if (::EVP_DigestSignInit(mdctx, //EVP_MD_CTX* ctx + nullptr, //EVP_PKEY_CTX** pctx + EVP_sha256(), //const EVP_MD* type + nullptr, //ENGINE* e + privateKey) != 1) //EVP_PKEY* pkey + throw SysError(formatLastOpenSSLError("EVP_DigestSignInit")); + + if (::EVP_DigestSignUpdate(mdctx, //EVP_MD_CTX* ctx + message.data(), //const void* d + message.size()) != 1) //size_t cnt + throw SysError(formatLastOpenSSLError("EVP_DigestSignUpdate")); + + size_t sigLenMax = 0; //"first call to EVP_DigestSignFinal returns the maximum buffer size required" + if (::EVP_DigestSignFinal(mdctx, //EVP_MD_CTX* ctx + nullptr, //unsigned char* sigret + &sigLenMax) != 1) //size_t* siglen + throw SysError(formatLastOpenSSLError("EVP_DigestSignFinal")); + + std::string signature(sigLenMax, '\0'); + size_t sigLen = sigLenMax; + + if (::EVP_DigestSignFinal(mdctx, //EVP_MD_CTX* ctx + reinterpret_cast(signature.data()), //unsigned char* sigret + &sigLen) != 1) //size_t* siglen + throw SysError(formatLastOpenSSLError("EVP_DigestSignFinal")); + + signature.resize(sigLen); + return signature; +} + + +void verifySignature(const std::string_view message, const std::string_view signature, EVP_PKEY* publicKey) //throw SysError +{ + //https://www.openssl.org/docs/manmaster/man3/EVP_DigestVerify.html + EVP_MD_CTX* mdctx = ::EVP_MD_CTX_new(); + if (!mdctx) + throw SysError(formatSystemError("EVP_MD_CTX_new", L"", L"No more error details.")); //no more error details + ZEN_ON_SCOPE_EXIT(::EVP_MD_CTX_free(mdctx)); + + if (::EVP_DigestVerifyInit(mdctx, //EVP_MD_CTX* ctx + nullptr, //EVP_PKEY_CTX** pctx + EVP_sha256(), //const EVP_MD* type + nullptr, //ENGINE* e + publicKey) != 1) //EVP_PKEY* pkey + throw SysError(formatLastOpenSSLError("EVP_DigestVerifyInit")); + + if (::EVP_DigestVerifyUpdate(mdctx, //EVP_MD_CTX* ctx + message.data(), //const void* d + message.size()) != 1) //size_t cnt + throw SysError(formatLastOpenSSLError("EVP_DigestVerifyUpdate")); + + if (::EVP_DigestVerifyFinal(mdctx, //EVP_MD_CTX* ctx + reinterpret_cast(signature.data()), //const unsigned char* sig + signature.size()) != 1) //size_t siglen + throw SysError(formatLastOpenSSLError("EVP_DigestVerifyFinal")); +} +} + + +std::string zen::convertRsaKey(const std::string_view keyStream, RsaStreamType typeFrom, RsaStreamType typeTo, bool publicKey) //throw SysError +{ + assert(typeFrom != typeTo); + std::shared_ptr evp = streamToKey(keyStream, typeFrom, publicKey); //throw SysError + return keyToStream(evp.get(), typeTo, publicKey); //throw SysError +} + + +void zen::verifySignature(const std::string_view message, const std::string_view signature, const std::string_view publicKeyStream, RsaStreamType streamType) //throw SysError +{ + std::shared_ptr publicKey = streamToKey(publicKeyStream, streamType, true /*publicKey*/); //throw SysError + ::verifySignature(message, signature, publicKey.get()); //throw SysError +} + + +bool zen::isPuttyKeyStream(const std::string_view keyStream) +{ + return startsWith(trimCpy(keyStream, TrimSide::left), "PuTTY-User-Key-File-"); +} + + +std::string zen::convertPuttyKeyToPkix(const std::string_view keyStream, const std::string_view passphrase) //throw SysError +{ + std::vector lines; + + split2(keyStream, isLineBreak, [&lines](const std::string_view block) + { + if (!block.empty()) //consider Windows' + lines.push_back(block); + }); + + //----------- parse PuTTY ppk structure ---------------------------------- + auto itLine = lines.begin(); + + auto lineStartsWith = [&](const char* str) + { + return itLine != lines.end() && startsWith(*itLine, str); + }; + + const int ppkFormat = [&] + { + if (lineStartsWith("PuTTY-User-Key-File-2: ")) + return 2; + else if (lineStartsWith("PuTTY-User-Key-File-3: ")) + return 3; + else + throw SysError(L"Unknown key file format"); + }(); + + const std::string_view algorithm = afterFirst(*itLine++, ' ', IfNotFoundReturn::none); + + if (!lineStartsWith("Encryption: ")) + throw SysError(L"Missing key encryption"); + const std::string_view keyEncryption = afterFirst(*itLine++, ' ', IfNotFoundReturn::none); + + const bool keyEncrypted = keyEncryption == "aes256-cbc"; + if (!keyEncrypted && keyEncryption != "none") + throw SysError(L"Unknown key encryption"); + + if (!lineStartsWith("Comment: ")) + throw SysError(L"Missing comment"); + const std::string_view comment = afterFirst(*itLine++, ' ', IfNotFoundReturn::none); + + if (!lineStartsWith("Public-Lines: ")) + throw SysError(L"Missing public lines"); + size_t pubLineCount = stringTo(afterFirst(*itLine++, ' ', IfNotFoundReturn::none)); + + std::string publicBlob64; + while (pubLineCount-- != 0) + if (itLine != lines.end()) + publicBlob64 += *itLine++; + else + throw SysError(L"Invalid key: incomplete public lines"); + + Argon2Flavor argonFlavor = Argon2Flavor::d; + uint32_t argonMemory = 0; + uint32_t argonPasses = 0; + uint32_t argonParallelism = 0; + std::string argonSalt; + if (ppkFormat >= 3 && keyEncrypted) + { + if (!lineStartsWith("Key-Derivation: ")) + throw SysError(L"Missing Argon2 parameter: Key-Derivation"); + const std::string_view keyDerivation = afterFirst(*itLine++, ' ', IfNotFoundReturn::none); + + argonFlavor = [&] + { + if (keyDerivation == "Argon2d") + return Argon2Flavor::d; + else if (keyDerivation == "Argon2i") + return Argon2Flavor::i; + else if (keyDerivation == "Argon2id") + return Argon2Flavor::id; + else + throw SysError(L"Unexpected Argon2 parameter for Key-Derivation"); + }(); + + if (!lineStartsWith("Argon2-Memory: ")) + throw SysError(L"Missing Argon2 parameter: Argon2-Memory"); + argonMemory = stringTo(afterFirst(*itLine++, ' ', IfNotFoundReturn::none)); + + if (!lineStartsWith("Argon2-Passes: ")) + throw SysError(L"Missing Argon2 parameter: Argon2-Passes"); + argonPasses = stringTo(afterFirst(*itLine++, ' ', IfNotFoundReturn::none)); + + if (!lineStartsWith("Argon2-Parallelism: ")) + throw SysError(L"Missing Argon2 parameter: Argon2-Parallelism"); + argonParallelism = stringTo(afterFirst(*itLine++, ' ', IfNotFoundReturn::none)); + + if (!lineStartsWith("Argon2-Salt: ")) + throw SysError(L"Missing Argon2 parameter: Argon2-Salt"); + const std::string_view argonSaltHex = afterFirst(*itLine++, ' ', IfNotFoundReturn::none); + + if (argonSaltHex.size() % 2 != 0 || !std::all_of(argonSaltHex.begin(), argonSaltHex.end(), isHexDigit)) + throw SysError(L"Invalid Argon2 parameter: Argon2-Salt"); + + for (size_t i = 0; i < argonSaltHex.size(); i += 2) + argonSalt += unhexify(argonSaltHex[i], argonSaltHex[i + 1]); + } + + if (!lineStartsWith("Private-Lines: ")) + throw SysError(L"Missing private lines"); + size_t privLineCount = stringTo(afterFirst(*itLine++, ' ', IfNotFoundReturn::none)); + + std::string privateBlob64; + while (privLineCount-- != 0) + if (itLine != lines.end()) + privateBlob64 += *itLine++; + else + throw SysError(L"Invalid key: incomplete private lines"); + + if (!lineStartsWith("Private-MAC: ")) + throw SysError(L"MAC missing"); //apparently "Private-Hash" is/was possible here: maybe with ppk version 1!? + const std::string_view macHex = afterFirst(*itLine++, ' ', IfNotFoundReturn::none); + + //----------- unpack key file elements --------------------- + if (macHex.size() % 2 != 0 || !std::all_of(macHex.begin(), macHex.end(), isHexDigit)) + throw SysError(L"Invalid key: invalid MAC"); + + std::string mac; + for (size_t i = 0; i < macHex.size(); i += 2) + mac += unhexify(macHex[i], macHex[i + 1]); + + const std::string publicBlob = stringDecodeBase64(publicBlob64); + const std::string privateBlobEnc = stringDecodeBase64(privateBlob64); + + std::string privateBlob; + std::string macKeyFmt3; + + if (!keyEncrypted) + privateBlob = privateBlobEnc; + else + { + if (passphrase.empty()) + throw SysError(L"Passphrase required to access private key"); + + const EVP_CIPHER* const cipher = EVP_aes_256_cbc(); + std::string decryptKey; + std::string iv; + if (ppkFormat >= 3) + { + decryptKey.resize(::EVP_CIPHER_get_key_length(cipher)); + iv .resize(::EVP_CIPHER_get_iv_length (cipher)); + macKeyFmt3.resize(32); + + const std::string argonBlob = zargon2(argonFlavor, argonMemory, argonPasses, argonParallelism, + static_cast(decryptKey.size() + iv.size() + macKeyFmt3.size()), passphrase, argonSalt); + MemoryStreamIn streamIn(argonBlob); + readArray(streamIn, decryptKey.data(), decryptKey.size()); // + readArray(streamIn, iv .data(), iv .size()); //throw SysErrorUnexpectedEos + readArray(streamIn, macKeyFmt3.data(), macKeyFmt3.size()); // + } + else + { + decryptKey = createHash(std::string("\0\0\0\0", 4) + passphrase, EVP_sha1()) + //throw SysError + createHash(std::string("\0\0\0\1", 4) + passphrase, EVP_sha1()); // + decryptKey.resize(::EVP_CIPHER_get_key_length(cipher)); //PuTTYgen only uses first 32 bytes as key (== key length of EVP_aes_256_cbc) + + iv.assign(::EVP_CIPHER_get_iv_length(cipher), 0); //initialization vector is 16-byte-range of zeros (== default for EVP_aes_256_cbc) + } + + EVP_CIPHER_CTX* cipCtx = ::EVP_CIPHER_CTX_new(); + if (!cipCtx) + throw SysError(formatSystemError("EVP_CIPHER_CTX_new", L"", L"No more error details.")); //no more error details + ZEN_ON_SCOPE_EXIT(::EVP_CIPHER_CTX_free(cipCtx)); + + if (::EVP_DecryptInit(cipCtx, //EVP_CIPHER_CTX* ctx + cipher, //const EVP_CIPHER* type + reinterpret_cast(decryptKey.c_str()), //const unsigned char* key + reinterpret_cast(iv.c_str())) != 1) //const unsigned char* iv => nullptr = 16-byte zeros for EVP_aes_256_cbc + throw SysError(formatLastOpenSSLError("EVP_DecryptInit_ex")); + + if (::EVP_CIPHER_CTX_set_padding(cipCtx, 0 /*padding*/) != 1) + throw SysError(formatSystemError("EVP_CIPHER_CTX_set_padding", L"", L"No more error details.")); //no more error details + + privateBlob.resize(privateBlobEnc.size() + ::EVP_CIPHER_block_size(EVP_aes_256_cbc())); + //"EVP_DecryptUpdate() should have room for (inl + cipher_block_size) bytes" + + int decLen1 = 0; + if (::EVP_DecryptUpdate(cipCtx, //EVP_CIPHER_CTX* ctx + reinterpret_cast(privateBlob.data()), //unsigned char* out + &decLen1, //int* outl + reinterpret_cast(privateBlobEnc.c_str()), //const unsigned char* in + static_cast(privateBlobEnc.size())) != 1) //int inl + throw SysError(formatLastOpenSSLError("EVP_DecryptUpdate")); + + int decLen2 = 0; + if (::EVP_DecryptFinal(cipCtx, //EVP_CIPHER_CTX* ctx + reinterpret_cast(&privateBlob[decLen1]), //unsigned char* outm + &decLen2) != 1) //int* outl + throw SysError(formatLastOpenSSLError("EVP_DecryptFinal_ex")); + + privateBlob.resize(decLen1 + decLen2); + } + + //----------- verify key consistency --------------------- + std::string macKey; + if (ppkFormat >= 3) + macKey = macKeyFmt3; + else + macKey = createHash(std::string("putty-private-key-file-mac-key") + (keyEncrypted ? passphrase : ""), EVP_sha1()); //throw SysError + + auto numToBeString = [](size_t n) -> std::string + { + static_assert(std::endian::native == std::endian::little && sizeof(n) >= 4); + assert(n == static_cast(n)); + const char* numStr = reinterpret_cast(&n); + return {numStr[3], numStr[2], numStr[1], numStr[0]}; //big endian! + }; + + const std::string macData = numToBeString(algorithm .size()) + algorithm + + numToBeString(keyEncryption.size()) + keyEncryption + + numToBeString(comment .size()) + comment + + numToBeString(publicBlob .size()) + publicBlob + + numToBeString(privateBlob .size()) + privateBlob; + char md[EVP_MAX_MD_SIZE] = {}; + unsigned int mdLen = 0; + if (!::HMAC(ppkFormat <= 2 ? EVP_sha1() : EVP_sha256(), //const EVP_MD* evp_md + macKey.c_str(), //const void* key + static_cast(macKey.size()), //int key_len + reinterpret_cast(macData.c_str()), //const unsigned char* d + static_cast(macData.size()), //int n + reinterpret_cast(md), //unsigned char* md + &mdLen)) //unsigned int* md_len + throw SysError(formatSystemError("HMAC", L"", L"No more error details.")); //no more error details + + if (mac != std::string_view(md, mdLen)) + throw SysError(keyEncrypted ? L"Wrong passphrase (or corrupted key)" : L"Validation failed: corrupted key"); + //---------------------------------------------------------- + + auto extractString = [](auto& it, auto itEnd) + { + uint32_t byteCount = 0; + if (itEnd - it < makeSigned(sizeof(byteCount))) + throw SysError(L"String extraction failed: unexpected end of stream"); + + static_assert(std::endian::native == std::endian::little); + char* numStr = reinterpret_cast(&byteCount); + numStr[3] = *it++; // + numStr[2] = *it++; //Putty uses big endian! + numStr[1] = *it++; // + numStr[0] = *it++; // + + if (makeUnsigned(itEnd - it) < byteCount) + throw SysError(L"String extraction failed: unexpected end of stream(2)"); + + std::string str(it, it + byteCount); + it += byteCount; + return str; + }; + + struct BnFree { void operator()(BIGNUM* num) const { ::BN_free(num); } }; + auto createBigNum = [] + { + BIGNUM* bn = ::BN_new(); + if (!bn) + throw SysError(formatLastOpenSSLError("BN_new")); + return std::unique_ptr(bn); + }; + + auto extractBigNum = [&extractString](auto& it, auto itEnd) + { + const std::string bytes = extractString(it, itEnd); + + BIGNUM* bn = ::BN_bin2bn(reinterpret_cast(bytes.c_str()), static_cast(bytes.size()), nullptr); + if (!bn) + throw SysError(formatLastOpenSSLError("BN_bin2bn")); + return std::unique_ptr(bn); + }; + + auto itPub = publicBlob .begin(); + auto itPriv = privateBlob.begin(); + + auto extractStringPub = [&] { return extractString(itPub, publicBlob .end()); }; + auto extractStringPriv = [&] { return extractString(itPriv, privateBlob.end()); }; + + auto extractBigNumPub = [&] { return extractBigNum(itPub, publicBlob .end()); }; + auto extractBigNumPriv = [&] { return extractBigNum(itPriv, privateBlob.end()); }; + + //----------- parse public/private key blobs ---------------- + if (extractStringPub() != algorithm) + throw SysError(L"Invalid public key stream (header)"); + + if (algorithm == "ssh-rsa") + { + std::unique_ptr e = extractBigNumPub (); // + std::unique_ptr n = extractBigNumPub (); // + std::unique_ptr d = extractBigNumPriv(); //throw SysError + std::unique_ptr p = extractBigNumPriv(); // + std::unique_ptr q = extractBigNumPriv(); // + std::unique_ptr iqmp = extractBigNumPriv(); // + + //------ calculate missing numbers: dmp1, dmq1 ------------- + std::unique_ptr dmp1 = createBigNum(); // + std::unique_ptr dmq1 = createBigNum(); //throw SysError + std::unique_ptr tmp = createBigNum(); // + + BN_CTX* bnCtx = BN_CTX_new(); + if (!bnCtx) + throw SysError(formatLastOpenSSLError("BN_CTX_new")); + ZEN_ON_SCOPE_EXIT(::BN_CTX_free(bnCtx)); + + if (::BN_sub(tmp.get(), p.get(), BN_value_one()) != 1) + throw SysError(formatLastOpenSSLError("BN_sub")); + + if (::BN_mod(dmp1.get(), d.get(), tmp.get(), bnCtx) != 1) + throw SysError(formatLastOpenSSLError("BN_mod")); + + if (::BN_sub(tmp.get(), q.get(), BN_value_one()) != 1) + throw SysError(formatLastOpenSSLError("BN_sub")); + + if (::BN_mod(dmq1.get(), d.get(), tmp.get(), bnCtx) != 1) + throw SysError(formatLastOpenSSLError("BN_mod")); + //---------------------------------------------------------- + + OSSL_PARAM_BLD* paramBld = ::OSSL_PARAM_BLD_new(); + if (!paramBld) + throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_new")); + ZEN_ON_SCOPE_EXIT(::OSSL_PARAM_BLD_free(paramBld)); + + if (::OSSL_PARAM_BLD_push_BN(paramBld, OSSL_PKEY_PARAM_RSA_N, n.get()) != 1) throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_push_BN(n)")); + if (::OSSL_PARAM_BLD_push_BN(paramBld, OSSL_PKEY_PARAM_RSA_E, e.get()) != 1) throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_push_BN(e)")); + if (::OSSL_PARAM_BLD_push_BN(paramBld, OSSL_PKEY_PARAM_RSA_D, d.get()) != 1) throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_push_BN(d)")); + if (::OSSL_PARAM_BLD_push_BN(paramBld, OSSL_PKEY_PARAM_RSA_FACTOR1, p.get()) != 1) throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_push_BN(p)")); + if (::OSSL_PARAM_BLD_push_BN(paramBld, OSSL_PKEY_PARAM_RSA_FACTOR2, q.get()) != 1) throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_push_BN(q)")); + if (::OSSL_PARAM_BLD_push_BN(paramBld, OSSL_PKEY_PARAM_RSA_EXPONENT1, dmp1.get()) != 1) throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_push_BN(dmp1)")); + if (::OSSL_PARAM_BLD_push_BN(paramBld, OSSL_PKEY_PARAM_RSA_EXPONENT2, dmq1.get()) != 1) throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_push_BN(dmq1)")); + if (::OSSL_PARAM_BLD_push_BN(paramBld, OSSL_PKEY_PARAM_RSA_COEFFICIENT1, iqmp.get()) != 1) throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_push_BN(iqmp)")); + + OSSL_PARAM* sslParams = ::OSSL_PARAM_BLD_to_param(paramBld); + if (!sslParams) + throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_to_param")); + ZEN_ON_SCOPE_EXIT(::OSSL_PARAM_free(sslParams)); + + + EVP_PKEY_CTX* evpCtx = ::EVP_PKEY_CTX_new_from_name(nullptr, "RSA", nullptr); + if (!evpCtx) + throw SysError(formatLastOpenSSLError("EVP_PKEY_CTX_new_from_name(RSA)")); + ZEN_ON_SCOPE_EXIT(::EVP_PKEY_CTX_free(evpCtx)); + + if (::EVP_PKEY_fromdata_init(evpCtx) != 1) + throw SysError(formatLastOpenSSLError("EVP_PKEY_fromdata_init")); + + EVP_PKEY* evp = nullptr; + if (::EVP_PKEY_fromdata(evpCtx, &evp, EVP_PKEY_KEYPAIR, sslParams) != 1) + throw SysError(formatLastOpenSSLError("EVP_PKEY_fromdata")); + ZEN_ON_SCOPE_EXIT(::EVP_PKEY_free(evp)); + + return keyToStream(evp, RsaStreamType::pkix, false /*publicKey*/); //throw SysError + } + //---------------------------------------------------------- + else if (algorithm == "ssh-dss") + { + std::unique_ptr p = extractBigNumPub (); // + std::unique_ptr q = extractBigNumPub (); // + std::unique_ptr g = extractBigNumPub (); //throw SysError + std::unique_ptr pub = extractBigNumPub (); // + std::unique_ptr pri = extractBigNumPriv(); // + //---------------------------------------------------------- + OSSL_PARAM_BLD* paramBld = ::OSSL_PARAM_BLD_new(); + if (!paramBld) + throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_new")); + ZEN_ON_SCOPE_EXIT(::OSSL_PARAM_BLD_free(paramBld)); + + if (::OSSL_PARAM_BLD_push_BN(paramBld, OSSL_PKEY_PARAM_FFC_P, p.get()) != 1) throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_push_BN(p)")); + if (::OSSL_PARAM_BLD_push_BN(paramBld, OSSL_PKEY_PARAM_FFC_Q, q.get()) != 1) throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_push_BN(q)")); + if (::OSSL_PARAM_BLD_push_BN(paramBld, OSSL_PKEY_PARAM_FFC_G, g.get()) != 1) throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_push_BN(g)")); + if (::OSSL_PARAM_BLD_push_BN(paramBld, OSSL_PKEY_PARAM_PUB_KEY, pub.get()) != 1) throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_push_BN(pub)")); + if (::OSSL_PARAM_BLD_push_BN(paramBld, OSSL_PKEY_PARAM_PRIV_KEY, pri.get()) != 1) throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_push_BN(pri)")); + + OSSL_PARAM* sslParams = ::OSSL_PARAM_BLD_to_param(paramBld); + if (!sslParams) + throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_to_param")); + ZEN_ON_SCOPE_EXIT(::OSSL_PARAM_free(sslParams)); + + + EVP_PKEY_CTX* evpCtx = ::EVP_PKEY_CTX_new_from_name(nullptr, "DSA", nullptr); + if (!evpCtx) + throw SysError(formatLastOpenSSLError("EVP_PKEY_CTX_new_from_name(DSA)")); + ZEN_ON_SCOPE_EXIT(::EVP_PKEY_CTX_free(evpCtx)); + + if (::EVP_PKEY_fromdata_init(evpCtx) != 1) + throw SysError(formatLastOpenSSLError("EVP_PKEY_fromdata_init")); + + EVP_PKEY* evp = nullptr; + if (::EVP_PKEY_fromdata(evpCtx, &evp, EVP_PKEY_KEYPAIR, sslParams) != 1) + throw SysError(formatLastOpenSSLError("EVP_PKEY_fromdata")); + ZEN_ON_SCOPE_EXIT(::EVP_PKEY_free(evp)); + + return keyToStream(evp, RsaStreamType::pkix, false /*publicKey*/); //throw SysError + } + //---------------------------------------------------------- + else if (algorithm == "ecdsa-sha2-nistp256" || + algorithm == "ecdsa-sha2-nistp384" || + algorithm == "ecdsa-sha2-nistp521") + { + const std::string_view algoShort = afterLast(algorithm, '-', IfNotFoundReturn::none); + if (extractStringPub() != algoShort) + throw SysError(L"Invalid public key stream (header)"); + + const std::string pointStream = extractStringPub(); + std::unique_ptr pri = extractBigNumPriv(); //throw SysError + //---------------------------------------------------------- + const char* groupName = [&] + { + if (algoShort == "nistp256") + return SN_X9_62_prime256v1; //same as SECG secp256r1 + if (algoShort == "nistp384") + return SN_secp384r1; + if (algoShort == "nistp521") + return SN_secp521r1; + throw SysError(L"Unknown elliptic curve: " + utfTo(algorithm)); + }(); + + OSSL_PARAM_BLD* paramBld = ::OSSL_PARAM_BLD_new(); + if (!paramBld) + throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_new")); + ZEN_ON_SCOPE_EXIT(::OSSL_PARAM_BLD_free(paramBld)); + + if (::OSSL_PARAM_BLD_push_utf8_string(paramBld, OSSL_PKEY_PARAM_GROUP_NAME, groupName, 0) != 1) + throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_push_utf8_string(group)")); + + if (::OSSL_PARAM_BLD_push_octet_string(paramBld, OSSL_PKEY_PARAM_PUB_KEY, pointStream.data(), pointStream.size()) != 1) + throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_push_octet_string(pub)")); + + if (::OSSL_PARAM_BLD_push_BN(paramBld, OSSL_PKEY_PARAM_PRIV_KEY, pri.get()) != 1) + throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_push_BN(priv)")); + + OSSL_PARAM* sslParams = ::OSSL_PARAM_BLD_to_param(paramBld); + if (!sslParams) + throw SysError(formatLastOpenSSLError("OSSL_PARAM_BLD_to_param")); + ZEN_ON_SCOPE_EXIT(::OSSL_PARAM_free(sslParams)); + + + EVP_PKEY_CTX* evpCtx = ::EVP_PKEY_CTX_new_from_name(nullptr, "EC", nullptr); + if (!evpCtx) + throw SysError(formatLastOpenSSLError("EVP_PKEY_CTX_new_from_name(EC)")); + ZEN_ON_SCOPE_EXIT(::EVP_PKEY_CTX_free(evpCtx)); + + if (::EVP_PKEY_fromdata_init(evpCtx) != 1) + throw SysError(formatLastOpenSSLError("EVP_PKEY_fromdata_init")); + + EVP_PKEY* evp = nullptr; + if (::EVP_PKEY_fromdata(evpCtx, &evp, EVP_PKEY_KEYPAIR, sslParams) != 1) + throw SysError(formatLastOpenSSLError("EVP_PKEY_fromdata")); + ZEN_ON_SCOPE_EXIT(::EVP_PKEY_free(evp)); + + return keyToStream(evp, RsaStreamType::pkix, false /*publicKey*/); //throw SysError + } + //---------------------------------------------------------- + else if (algorithm == "ssh-ed25519") + { + //const std::string pubStream = extractStringPub(); -> we don't need the public key + const std::string priStream = extractStringPriv(); + + EVP_PKEY* evpPriv = ::EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, //int type + nullptr, //ENGINE* e + reinterpret_cast(priStream.c_str()), //const unsigned char* priv + priStream.size()); //size_t len + if (!evpPriv) + throw SysError(formatLastOpenSSLError("EVP_PKEY_new_raw_private_key")); + ZEN_ON_SCOPE_EXIT(::EVP_PKEY_free(evpPriv)); + + return keyToStream(evpPriv, RsaStreamType::pkix, false /*publicKey*/); //throw SysError + } + else + throw SysError(L"Unsupported key algorithm: " + utfTo(algorithm)); + /* PuTTYgen supports many more (which are not yet supported by libssh2): + - rsa-sha2-256 + - rsa-sha2-512 + - ssh-ed448 + - ssh-dss-cert-v01@openssh.com + - ssh-rsa-cert-v01@openssh.com + - rsa-sha2-256-cert-v01@openssh.com + - rsa-sha2-512-cert-v01@openssh.com + - ssh-ed25519-cert-v01@openssh.com + - ecdsa-sha2-nistp256-cert-v01@openssh.com + - ecdsa-sha2-nistp384-cert-v01@openssh.com + - ecdsa-sha2-nistp521-cert-v01@openssh.com */ +} diff --git a/zen/open_ssl.h b/zen/open_ssl.h new file mode 100644 index 0000000..96e25ff --- /dev/null +++ b/zen/open_ssl.h @@ -0,0 +1,40 @@ +// ***************************************************************************** +// * 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 OPEN_SSL_H_801974580936508934568792347506 +#define OPEN_SSL_H_801974580936508934568792347506 + +#include "sys_error.h" + + +namespace zen +{ +//init OpenSSL before use! +void openSslInit(); +void openSslTearDown(); + + +enum class RsaStreamType +{ + pkix, //base-64-encoded X.509 SubjectPublicKeyInfo structure ("BEGIN PUBLIC KEY") + pkcs1, //base-64-encoded PKCS#1 RSAPublicKey: RSA number and exponent ("BEGIN RSA PUBLIC KEY") + raw //raw bytes: DER-encoded PKCS#1 +}; + +//verify signatures produced with: "openssl dgst -sha256 -sign private.pem -out file.sig file.txt" +void verifySignature(const std::string_view message, + const std::string_view signature, + const std::string_view publicKeyStream, + RsaStreamType streamType); //throw SysError + +std::string convertRsaKey(const std::string_view keyStream, RsaStreamType typeFrom, RsaStreamType typeTo, bool publicKey); //throw SysError + + +bool isPuttyKeyStream(const std::string_view keyStream); +std::string convertPuttyKeyToPkix(const std::string_view keyStream, const std::string_view passphrase); //throw SysError +} + +#endif //OPEN_SSL_H_801974580936508934568792347506 diff --git a/zen/perf.h b/zen/perf.h new file mode 100644 index 0000000..61ec0c4 --- /dev/null +++ b/zen/perf.h @@ -0,0 +1,103 @@ +// ***************************************************************************** +// * 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 PERF_H_83947184145342652456 +#define PERF_H_83947184145342652456 + +#include +#include "string_tools.h" + + #include + + +//############# two macros for quick performance measurements ############### +#define PERF_START zen::PerfTimer perfTest; +#define PERF_STOP perfTest.showResult(); +//########################################################################### + +/* Example: Aggregated function call time: + + static zen::PerfTimer perfTest(true); //startPaused + perfTest.resume(); + ZEN_ON_SCOPE_EXIT(perfTest.pause()); */ + +namespace zen +{ +/* issue with wxStopWatch? https://freefilesync.org/forum/viewtopic.php?t=1426 + - wxStopWatch implementation uses QueryPerformanceCounter: https://github.com/wxWidgets/wxWidgets/blob/17d72a48ffd4d8ff42eed070ac48ee2de50ceabd/src/common/stopwatch.cpp + - MSDN: "How often does QPC roll over? Not less than 100 years from the most recent system boot" + https://docs.microsoft.com/en-us/windows/win32/sysinfo/acquiring-high-resolution-time-stamps#general-faq-about-qpc-and-tsc + - But QPC can glitch out in VMs: https://web.archive.org/web/20190420142348/https://blogs.msdn.microsoft.com/tvoellm/2008/06/05/negative-ping-times-in-windows-vms-whats-up/ + ... or for AMD Opteron CPUs: https://web.archive.org/web/20191101122320/https://support.microsoft.com/en-us/help/938448/a-windows-server-2003-based-server-may-experience-time-stamp-counter-d + + - system clock is obviously no alternative: https://freefilesync.org/forum/viewtopic.php?t=5280 + + std::chrono::system_clock wraps ::GetSystemTimePreciseAsFileTime() + std::chrono::steady_clock wraps ::QueryPerformanceCounter() */ +class StopWatch +{ +public: + explicit StopWatch(bool startPaused = false) + { + if (startPaused) + startTime_ = {}; + } + + bool isPaused() const { return startTime_ == std::chrono::steady_clock::time_point{}; } + + void pause() + { + if (!isPaused()) + elapsedUntilPause_ += std::chrono::steady_clock::now() - std::exchange(startTime_, {}); + } + + void resume() + { + if (isPaused()) + startTime_ = std::chrono::steady_clock::now(); + } + + std::chrono::nanoseconds elapsed() const + { + auto elapsedTotal = elapsedUntilPause_; + if (!isPaused()) + elapsedTotal += std::chrono::steady_clock::now() - startTime_; + return elapsedTotal; + } + +private: + std::chrono::steady_clock::time_point startTime_ = std::chrono::steady_clock::now(); + std::chrono::nanoseconds elapsedUntilPause_{}; //std::chrono::duration is uninitialized by default! WTF! When will this stupidity end! +}; + + +class PerfTimer +{ +public: + [[deprecated]] explicit PerfTimer(bool startPaused = false) : watch_(startPaused) {} + + ~PerfTimer() { if (!resultShown_) showResult(); } + + void pause () { watch_.pause(); } + void resume() { watch_.resume(); } + + void showResult() + { + const int64_t timeMs = std::chrono::duration_cast(watch_.elapsed()).count(); + const std::string msg = numberTo(timeMs) + " ms"; + std::clog << "Perf: duration: " << msg + '\n'; + resultShown_ = true; + + watch_ = StopWatch(watch_.isPaused()); + } + +private: + StopWatch watch_; + bool resultShown_ = false; +}; +} + +#endif //PERF_H_83947184145342652456 diff --git a/zen/process_exec.cpp b/zen/process_exec.cpp new file mode 100644 index 0000000..2026a98 --- /dev/null +++ b/zen/process_exec.cpp @@ -0,0 +1,267 @@ +// ***************************************************************************** +// * 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 "process_exec.h" +#include "guid.h" +#include "file_access.h" +#include "file_io.h" + + #include //fork, pipe + #include //waitpid + #include + +using namespace zen; + + +Zstring zen::escapeCommandArg(const Zstring& arg) +{ + Zstring output; + bool addQuotes = false; + + for (const char c : arg) + { + switch (c) + { + case '"': // + case '\\': //must be escaped - no matter if string is double-quoted or not + case '`': // + case '$': // + output += '\\'; + break; + + default: + if (contains(" '&*()|;<>#~", c)) //must *either* be escaped or protected by double-quotes, never both! + addQuotes = true; + break; + } + output += c; + } + + if (addQuotes) + return '"' + output + '"'; //caveat: single-quotes not working on macOS if string contains escaped chars! no such issue on Linux + return output; +} + + + + +namespace +{ +std::pair processExecuteImpl(const Zstring& filePath, const std::vector& arguments, + std::optional timeoutMs) //throw SysError, SysErrorTimeOut +{ + const Zstring tempFilePath = appendPath(getTempFolderPath(), //throw FileError + Zstr("FFS-") + utfTo(formatAsHexString(generateGUID()))); + /* can't use popen(): does NOT return the exit code on Linux (despite the documentation!), although it works correctly on macOS + => use pipes instead: https://linux.die.net/man/2/waitpid + bonus: no need for "2>&1" to redirect STDERR to STDOUT + + What about premature exit via SysErrorTimeOut? + Linux: child process' end of the pipe *still works* even after the parent process is gone: + There does not seem to be any output buffer size limit + no observable strain on system memory or disk space! :) + macOS: child process exits if parent end of pipe is closed: fuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu.......... + + => solution: buffer output in temporary file + + Unresolved problem: premature exit via SysErrorTimeOut (=> no waitpid()) creates zombie proceses: + "As long as a zombie is not removed from the system via a wait, + it will consume a slot in the kernel process table, and if this table fills, + it will not be possible to create further processes." */ + + const int EC_CHILD_LAUNCH_FAILED = 120; //avoid 127: used by the system, e.g. failure to execute due to missing .so file + + //use O_TMPFILE? sounds nice, but support is probably crap: https://github.com/libvips/libvips/issues/1151 + const int fdTempFile = ::open(tempFilePath.c_str(), O_CREAT | O_EXCL | O_RDWR | O_CLOEXEC, + S_IRUSR | S_IWUSR); //0600 + if (fdTempFile == -1) + THROW_LAST_SYS_ERROR("open(" + utfTo(tempFilePath) + ")"); + auto guardTmpFile = makeGuard([&] { ::close(fdTempFile); }); + + //"deleting while handle is open" == FILE_FLAG_DELETE_ON_CLOSE + if (::unlink(tempFilePath.c_str()) != 0) + THROW_LAST_SYS_ERROR("unlink"); + + //-------------------------------------------------------------- + //waitpid() is a useless pile of garbage without time out => check EOF from dummy pipe instead + int pipe[2] = {}; + if (::pipe2(pipe, O_CLOEXEC) != 0) + THROW_LAST_SYS_ERROR("pipe2"); + + const int fdLifeSignR = pipe[0]; //for parent process + const int fdLifeSignW = pipe[1]; //for child process + ZEN_ON_SCOPE_EXIT(::close(fdLifeSignR)); + auto guardFdLifeSignW = makeGuard([&] { ::close(fdLifeSignW ); }); + + //-------------------------------------------------------------- + + //follow implemenation of ::system(): https://github.com/lattera/glibc/blob/master/sysdeps/posix/system.c + const pid_t pid = ::fork(); + if (pid < 0) //pids are never negative, empiric proof: https://linux.die.net/man/2/wait + THROW_LAST_SYS_ERROR("fork"); + + if (pid == 0) //child process + try + { + //first task: set STDOUT redirection in case an error needs to be reported + if (::dup2(fdTempFile, STDOUT_FILENO) != STDOUT_FILENO) //O_CLOEXEC does NOT propagate with dup2() + THROW_LAST_SYS_ERROR("dup2(STDOUT)"); + + if (::dup2(fdTempFile, STDERR_FILENO) != STDERR_FILENO) //O_CLOEXEC does NOT propagate with dup2() + THROW_LAST_SYS_ERROR("dup2(STDERR)"); + + //avoid blocking scripts waiting for user input + // => appending " < /dev/null" is not good enough! e.g. hangs for: read -p "still hanging here"; echo fuuuuu... + const int fdDevNull = ::open("/dev/null", O_RDONLY | O_CLOEXEC); + if (fdDevNull == -1) //don't check "< 0" -> docu seems to allow "-2" to be a valid file handle + THROW_LAST_SYS_ERROR("open(/dev/null)"); + ZEN_ON_SCOPE_EXIT(::close(fdDevNull)); + + if (::dup2(fdDevNull, STDIN_FILENO) != STDIN_FILENO) //O_CLOEXEC does NOT propagate with dup2() + THROW_LAST_SYS_ERROR("dup2(STDIN)"); + + //*leak* the fd and have it closed automatically on child process exit after execv() + if (::dup(fdLifeSignW) == -1) //O_CLOEXEC does NOT propagate with dup() + THROW_LAST_SYS_ERROR("dup(fdLifeSignW)"); + + std::vector argv{filePath.c_str()}; + for (const Zstring& arg : arguments) + argv.push_back(arg.c_str()); + argv.push_back(nullptr); + + /*int rv =*/::execv(argv[0], const_cast(argv.data())); //only returns if an error occurred + //safe to cast away const: https://pubs.opengroup.org/onlinepubs/9699919799/functions/exec.html + // "The statement about argv[] and envp[] being constants is included to make explicit to future + // writers of language bindings that these objects are completely constant. Due to a limitation of + // the ISO C standard, it is not possible to state that idea in standard C." + THROW_LAST_SYS_ERROR("execv"); + } + catch (const SysError& e) + { + ::puts(utfTo(e.toString()).c_str()); + ::fflush(stdout); //note: stderr is unbuffered by default + ::_exit(EC_CHILD_LAUNCH_FAILED); //[!] avoid flushing I/O buffers or doing other clean up from child process like with "exit()"! + } + //else: parent process + + + if (timeoutMs) + { + guardFdLifeSignW.dismiss(); + ::close(fdLifeSignW); //[!] make sure we get EOF when fd is closed by child! + + const int flags = ::fcntl(fdLifeSignR, F_GETFL); + if (flags == -1) + THROW_LAST_SYS_ERROR("fcntl(F_GETFL)"); + + //fcntl() success: Linux: 0 + // macOS: "Value other than -1." + if (::fcntl(fdLifeSignR, F_SETFL, flags | O_NONBLOCK) == -1) + THROW_LAST_SYS_ERROR("fcntl(F_SETFL, O_NONBLOCK)"); + + + const auto stopTime = std::chrono::steady_clock::now() + std::chrono::milliseconds(*timeoutMs); + for (;;) //EINTR handling? => allow interruption!? + { + //read until EAGAIN + char buf[16]; + const ssize_t bytesRead = ::read(fdLifeSignR, buf, sizeof(buf)); + if (bytesRead < 0) + { + if (errno != EAGAIN) + THROW_LAST_SYS_ERROR("read"); + } + else if (bytesRead > 0) + throw SysError(formatSystemError("read", L"", L"Unexpected data.")); + else //bytesRead == 0: EOF + break; + + //wait for stream input + const auto now = std::chrono::steady_clock::now(); + if (now > stopTime) + throw SysErrorTimeOut(_P("Operation timed out after 1 second.", "Operation timed out after %x seconds.", *timeoutMs / 1000)); + + const auto waitTimeMs = std::chrono::duration_cast(stopTime - now).count(); + + timeval tv{.tv_sec = static_cast(waitTimeMs / 1000)}; + tv.tv_usec = static_cast(waitTimeMs - tv.tv_sec * 1000) * 1000; + + fd_set rfd{}; //includes FD_ZERO + FD_SET(fdLifeSignR, &rfd); + + if (const int rv = ::select(fdLifeSignR + 1, //int nfds = "highest-numbered file descriptor in any of the three sets, plus 1" + &rfd, //fd_set* readfds + nullptr, //fd_set* writefds + nullptr, //fd_set* exceptfds + &tv); //struct timeval* timeout + rv < 0) + THROW_LAST_SYS_ERROR("select"); + else if (rv == 0) + throw SysErrorTimeOut(_P("Operation timed out after 1 second.", "Operation timed out after %x seconds.", *timeoutMs / 1000)); + } + } + + //https://linux.die.net/man/2/waitpid + int statusCode = 0; + if (::waitpid(pid, //pid_t pid + &statusCode, //int* status + 0) != pid) //int options + THROW_LAST_SYS_ERROR("waitpid"); + + + if (::lseek(fdTempFile, 0, SEEK_SET) != 0) + THROW_LAST_SYS_ERROR("lseek"); + + guardTmpFile.dismiss(); + FileInputPlain streamIn(fdTempFile, tempFilePath); //pass ownership! + + std::string output = unbufferedLoad([&](void* buffer, size_t bytesToRead) + { + return streamIn.tryRead(buffer, bytesToRead); //throw FileError; may return short, only 0 means EOF! => CONTRACT: bytesToRead > 0! + }, + streamIn.getBlockSize()); //throw FileError + + if (!WIFEXITED(statusCode)) //signalled, crashed? + throw SysError(formatSystemError("waitpid", WIFSIGNALED(statusCode) ? + L"Killed by signal " + numberTo(WTERMSIG(statusCode)) : + L"Exit status " + numberTo(statusCode), + utfTo(trimCpy(output)))); + + const int exitCode = WEXITSTATUS(statusCode); //precondition: "WIFEXITED() == true" + if (exitCode == EC_CHILD_LAUNCH_FAILED || //child process should already have provided details to STDOUT + exitCode == 127) //details should have been streamed to STDERR: used by /bin/sh, e.g. failure to execute due to missing .so file + throw SysError(utfTo(trimCpy(output))); + + return {exitCode, std::move(output)}; +} +} + + +std::pair zen::consoleExecute(const Zstring& cmdLine, std::optional timeoutMs) //throw SysError, SysErrorTimeOut +{ + const auto& [exitCode, output] = processExecuteImpl("/bin/sh", {"-c", cmdLine.c_str()}, timeoutMs); //throw SysError, SysErrorTimeOut + return {exitCode, copyStringTo(output)}; +} + + +void zen::openWithDefaultApp(const Zstring& itemPath) //throw FileError +{ + try + { + std::optional timeoutMs; + const Zstring cmdTemplate = "xdg-open %x"; //*might* block! + timeoutMs = 0; //e.g. on Lubuntu if Firefox is started and not already running => no need for time out! https://freefilesync.org/forum/viewtopic.php?t=8260 + const Zstring cmdLine = replaceCpy(cmdTemplate, Zstr("%x"), escapeCommandArg(itemPath)); + + if (const auto& [exitCode, output] = consoleExecute(cmdLine, timeoutMs); //throw SysError, SysErrorTimeOut + exitCode != 0) + throw SysError(formatSystemError(utfTo(cmdTemplate), + replaceCpy(_("Exit code %x"), L"%x", numberTo(exitCode)), utfTo(output))); + } + catch (SysErrorTimeOut&) {} //child process not failed yet => probably fine :> + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot open file %x."), L"%x", fmtPath(itemPath)), e.toString()); } +} + + diff --git a/zen/process_exec.h b/zen/process_exec.h new file mode 100644 index 0000000..1c18c3f --- /dev/null +++ b/zen/process_exec.h @@ -0,0 +1,28 @@ +// ***************************************************************************** +// * 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 SHELL_EXECUTE_H_23482134578134134 +#define SHELL_EXECUTE_H_23482134578134134 + +#include "file_error.h" + + +namespace zen +{ +Zstring escapeCommandArg(const Zstring& arg); + + +DEFINE_NEW_SYS_ERROR(SysErrorTimeOut) +[[nodiscard]] std::pair consoleExecute(const Zstring& cmdLine, std::optional timeoutMs); //throw SysError, SysErrorTimeOut +/* Windows: - cmd.exe returns exit code 1 if file not found (instead of throwing SysError) => nodiscard! + - handles elevation when CreateProcess() would fail with ERROR_ELEVATION_REQUIRED! + - no support for UNC path and Unicode on Win7; apparently no issue on Win10! + Linux/macOS: SysErrorTimeOut leaves zombie process behind if timeoutMs is used */ + +void openWithDefaultApp(const Zstring& itemPath); //throw FileError +} + +#endif //SHELL_EXECUTE_H_23482134578134134 diff --git a/zen/process_priority.cpp b/zen/process_priority.cpp new file mode 100644 index 0000000..6bd2e3f --- /dev/null +++ b/zen/process_priority.cpp @@ -0,0 +1,124 @@ +// ***************************************************************************** +// * 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 "process_priority.h" + + #include //setpriority + +using namespace zen; + + +namespace +{ +#if 0 +//https://linux.die.net/man/2/getpriority +//CPU priority from highest to lowest range: [-NZERO, NZERO -1] usually: [-20, 19] +enum //with values from CentOS 7 +{ + CPU_PRIO_VERYHIGH = -NZERO, + CPU_PRIO_HIGH = -5, + CPU_PRIO_NORMAL = 0, + CPU_PRIO_LOW = 5, + CPU_PRIO_VERYLOW = NZERO - 1, +}; + +int getCpuPriority() //throw SysError +{ + errno = 0; + const int prio = getpriority(PRIO_PROCESS, 0 /* = the calling process */); + if (prio == -1 && errno != 0) //"can legitimately return the value -1" + THROW_LAST_SYS_ERROR("getpriority"); + return prio; +} + + +//lowering is allowed, but increasing CPU prio requires admin rights >:( +void setCpuPriority(int prio) //throw SysError +{ + if (setpriority(PRIO_PROCESS, 0 /* = the calling process */, prio) != 0) + THROW_LAST_SYS_ERROR("setpriority(" + numberTo(prio) + ')'); +} +#endif +//--------------------------------------------------------------------------------------------------- + +//- required functions ioprio_get/ioprio_set are not part of glibc: https://linux.die.net/man/2/ioprio_set +//- and probably never will: https://sourceware.org/bugzilla/show_bug.cgi?id=4464 +//https://github.com/torvalds/linux/blob/master/include/uapi/linux/ioprio.h +#define IOPRIO_CLASS_SHIFT 13 + +#define IOPRIO_PRIO_VALUE(prioclass, priolevel) \ + (((prioclass) << IOPRIO_CLASS_SHIFT) | (priolevel)) + +#define IOPRIO_NORM 4 + +enum +{ + IOPRIO_WHO_PROCESS = 1, + IOPRIO_WHO_PGRP, + IOPRIO_WHO_USER, +}; + +enum +{ + IOPRIO_CLASS_NONE = 0, + IOPRIO_CLASS_RT = 1, + IOPRIO_CLASS_BE = 2, + IOPRIO_CLASS_IDLE = 3, +}; + + +int getIoPriority() //throw SysError +{ + const int rv = ::syscall(SYS_ioprio_get, IOPRIO_WHO_PROCESS, ::getpid()); + if (rv == -1) + THROW_LAST_SYS_ERROR("ioprio_get"); + + //fix Linux kernel fuck up: bogus system default value + if (rv == IOPRIO_PRIO_VALUE(IOPRIO_CLASS_NONE, IOPRIO_NORM)) + return IOPRIO_PRIO_VALUE(IOPRIO_CLASS_BE, IOPRIO_NORM); + + return rv; +} + + +void setIoPriority(int ioPrio) //throw SysError +{ + if (::syscall(SYS_ioprio_set, IOPRIO_WHO_PROCESS, ::getpid(), ioPrio) != 0) + THROW_LAST_SYS_ERROR("ioprio_set(0x" + printNumber("%x", static_cast(ioPrio)) + ')'); +} +} + + +struct SetProcessPriority::Impl +{ + std::optional oldIoPrio; +}; + + +SetProcessPriority::SetProcessPriority(ProcessPriority prio) : //throw FileError + pimpl_(new Impl) +{ + if (prio == ProcessPriority::background) + try + { + pimpl_->oldIoPrio = getIoPriority(); //throw SysError + + setIoPriority(IOPRIO_PRIO_VALUE(IOPRIO_CLASS_BE, 6 /*0 (highest) to 7 (lowest)*/)); //throw SysError + //maybe even IOPRIO_PRIO_VALUE(IOPRIO_CLASS_IDLE, 0) ? nope: "only served when no one else is using the disk" + } + catch (const SysError& e) { throw FileError(_("Cannot change process I/O priorities."), e.toString()); } +} + + +SetProcessPriority::~SetProcessPriority() +{ + if (pimpl_->oldIoPrio) + try + { + setIoPriority(*pimpl_->oldIoPrio); //throw SysError + } + catch (const SysError& e) { logExtraError(_("Cannot change process I/O priorities.") + L"\n\n" + e.toString()); } +} diff --git a/zen/process_priority.h b/zen/process_priority.h new file mode 100644 index 0000000..216c115 --- /dev/null +++ b/zen/process_priority.h @@ -0,0 +1,36 @@ +// ***************************************************************************** +// * 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 PROCESS_PRIORITY_H_83421759082143245 +#define PROCESS_PRIORITY_H_83421759082143245 + +#include +#include "file_error.h" + + +namespace zen +{ +enum class ProcessPriority +{ + normal, + background, //lower CPU and file I/O priorities +}; + +//- prevent operating system going into sleep state +//- set process I/O priorities +class SetProcessPriority +{ +public: + explicit SetProcessPriority(ProcessPriority prio); //throw FileError + ~SetProcessPriority(); + +private: + struct Impl; + const std::unique_ptr pimpl_; +}; +} + +#endif //PROCESS_PRIORITY_H_83421759082143245 diff --git a/zen/recycler.cpp b/zen/recycler.cpp new file mode 100644 index 0000000..7e6205b --- /dev/null +++ b/zen/recycler.cpp @@ -0,0 +1,106 @@ +// ***************************************************************************** +// * 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 "recycler.h" + + #include + #include "scope_guard.h" + +using namespace zen; + + + + +void zen::moveToRecycleBin(const Zstring& itemPath) //throw FileError, RecycleBinUnavailable +{ + GFile* file = ::g_file_new_for_path(itemPath.c_str()); //never fails according to docu + ZEN_ON_SCOPE_EXIT(g_object_unref(file);) + + GError* error = nullptr; + ZEN_ON_SCOPE_EXIT(if (error) ::g_error_free(error)); + + if (!::g_file_trash(file, nullptr, &error)) + { + /* g_file_trash() can fail with different error codes/messages when trash is unavailable: + Debian 8 (GLib 2.42): G_IO_ERROR_NOT_SUPPORTED: Unable to find or create trash directory + CentOS 7 (GLib 2.56): G_IO_ERROR_FAILED: Unable to find or create trash directory for file.txt => localized! >:( + master (GLib 2.64): G_IO_ERROR_NOT_SUPPORTED: Trashing on system internal mounts is not supported + https://gitlab.gnome.org/GNOME/glib/blob/master/gio/glocalfile.c#L2042 */ + + //*INDENT-OFF* + const bool trashUnavailable = error && error->domain == G_IO_ERROR && + (error->code == G_IO_ERROR_NOT_SUPPORTED || + + //yes, the following is a cluster fuck, but what can you do? + (error->code == G_IO_ERROR_FAILED && [&] + { + for (const char* msgLoc : //translations from https://gitlab.gnome.org/GNOME/glib/-/tree/main/po + { + "Unable to find or create trash directory for", + "No s'ha pogut trobar o crear el directori de la paperera per", + "Nelze nalézt nebo vytvořit složku koše pro", + "Kan ikke finde eller oprette papirkurvskatalog for", + "Αδύνατη η εύρεση ή δημιουργία του καταλόγου απορριμμάτων", + "Unable to find or create wastebasket directory for", + "Ne eblas trovi aŭ krei rubujan dosierujon", + "No se pudo encontrar o crear la carpeta de la papelera para", + "Prügikasti kataloogi pole võimalik leida või luua", + "zakarrontziaren direktorioa aurkitu edo sortu", + "Roskakori kansiota ei löydy tai sitä ei voi luoda", + "Impossible de trouver ou créer le répertoire de la corbeille pour", + "Non é posíbel atopar ou crear o directorio do lixo para", + "Nisam mogao promijeniti putanju u mapu", + "Nem található vagy nem hozható létre a Kuka könyvtár ehhez:", + "Tidak bisa menemukan atau membuat direktori tong sampah bagi", + "Impossibile trovare o creare la directory cestino per", + "のゴミ箱ディレクトリが存在しないか作成できません", + "휴지통 디렉터리를 찾을 수 없거나 만들 수 없습니다", + "Nepavyko rasti ar sukurti šiukšlių aplanko", + "Nevar atrast vai izveidot miskastes mapi priekš", + "Tidak boleh mencari atau mencipta direktori tong sampah untuk", + "Kan ikke finne eller opprette mappe for papirkurv for", + "फाइल सिर्जना गर्न असफल:", + "Impossible de trobar o crear lo repertòri de l'escobilhièr per", + "ਲਈ ਰੱਦੀ ਡਾਇਰੈਕਟਰੀ ਲੱਭਣ ਜਾਂ ਬਣਾਉਣ ਲਈ ਅਸਮਰੱਥ", + "Nie można odnaleźć lub utworzyć katalogu kosza dla", + "Impossível encontrar ou criar a pasta de lixo para", + "Não é possível localizar ou criar o diretório da lixeira para", + "Nu se poate găsi sau crea directorul coșului de gunoi pentru", + "Не удалось найти или создать каталог корзины для", + "Nepodarilo sa nájsť ani vytvoriť adresár Kôš pre", + "Ni mogoče najti oziroma ustvariti mape smeti za", + "Не могу да нађем или направим директоријум смећа за", + "Ne mogu da nađem ili napravim direktorijum smeća za", + "Kunde inte hitta eller skapa papperskorgskatalog för", + "için çöp dizini bulunamıyor ya da oluşturulamıyor", + "Не вдалося знайти або створити каталог смітника для", + "หาหรือสร้างไดเรกทอรีถังขยะสำหรับ", + }) + if (contains(error->message, msgLoc)) + return true; + + for (const auto& [msgLoc1, msgLoc2] : + { + std::pair{"Papierkorb-Ordner konnte für", "nicht gefunden oder angelegt werden"}, + std::pair{"Kan prullenbakmap voor", "niet vinden of aanmaken"}, + std::pair{"无法为", "找到或创建回收站目录"}, + std::pair{"無法找到或建立", "的垃圾桶目錄"}, + }) + if (contains(error->message, msgLoc1) && contains(error->message, msgLoc2)) + return true; + + return false; + }())); + //*INDENT-ON* + + if (trashUnavailable) + throw RecycleBinUnavailable(replaceCpy(_("The recycle bin is not available for %x."), L"%x", fmtPath(itemPath)), + formatGlibError("g_file_trash", error)); + + throw FileError(replaceCpy(_("Unable to move %x to the recycle bin."), L"%x", fmtPath(itemPath)), + formatGlibError("g_file_trash", error)); + } +} diff --git a/zen/recycler.h b/zen/recycler.h new file mode 100644 index 0000000..7977132 --- /dev/null +++ b/zen/recycler.h @@ -0,0 +1,34 @@ +// ***************************************************************************** +// * 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 RECYCLER_H_18345067341545 +#define RECYCLER_H_18345067341545 + +#include +#include +#include "file_error.h" + + +namespace zen +{ +/* -------------------- + |Recycle Bin Access| + -------------------- + + Windows: -> Recycler API (IFileOperation) always available + -> COM needs to be initialized before calling any of these functions! CoInitializeEx/CoUninitialize + + Linux: Compiler flags: `pkg-config --cflags gio-2.0` + Linker flags: `pkg-config --libs gio-2.0` + + Already included in package "gtk+-2.0"! */ + + +//fails if item is not existing (anymore) +void moveToRecycleBin(const Zstring& itemPath); //throw FileError, RecycleBinUnavailable +} + +#endif //RECYCLER_H_18345067341545 diff --git a/zen/resolve_path.cpp b/zen/resolve_path.cpp new file mode 100644 index 0000000..7bf50b1 --- /dev/null +++ b/zen/resolve_path.cpp @@ -0,0 +1,231 @@ +// ***************************************************************************** +// * 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 "resolve_path.h" +#include "time.h" +#include "thread.h" +#include "file_access.h" + + #include + #include //getcwd() + +using namespace zen; + + +namespace +{ +Zstring resolveRelativePath(const Zstring& relativePath) +{ + if (relativePath.empty()) + return relativePath; + + Zstring pathTmp = relativePath; + //https://linux.die.net/man/2/path_resolution + if (!startsWith(pathTmp, FILE_NAME_SEPARATOR)) //absolute names are exactly those starting with a '/' + { + /* basic support for '~': strictly speaking this is a shell-layer feature, so "realpath()" won't handle it + https://www.gnu.org/software/bash/manual/html_node/Tilde-Expansion.html */ + if (startsWith(pathTmp, "~/") || pathTmp == "~") + { + try + { + const Zstring& homePath = getUserHome(); //throw FileError + + if (startsWith(pathTmp, "~/")) + pathTmp = appendPath(homePath, pathTmp.c_str() + 2); + else //pathTmp == "~" + pathTmp = homePath; + } + catch (FileError&) {} + //else: error! no further processing! + } + else + { + //we cannot use ::realpath() which only resolves *existing* relative paths! + if (char* dirPath = ::getcwd(nullptr, 0)) + { + ZEN_ON_SCOPE_EXIT(::free(dirPath)); + pathTmp = appendPath(dirPath, pathTmp); + } + } + } + //get rid of some cruft (just like GetFullPathName()) + replace(pathTmp, "/./", '/'); + if (endsWith(pathTmp, "/.")) + pathTmp.pop_back(); //keep the "/" => consider pathTmp == "/." + + //what about "/../"? might be relative to symlinks => preserve! + + return pathTmp; +} + + + + +//returns value if resolved +std::optional tryResolveMacro(const ZstringView macro) //macro without %-characters +{ + Zstring timeStr; + auto resolveTimePhrase = [&](const Zchar* phrase, const Zchar* format) -> bool + { + if (!equalAsciiNoCase(macro, phrase)) + return false; + + timeStr = formatTime(format); + return true; + }; + + //https://en.cppreference.com/w/cpp/chrono/c/strftime + //there exist environment variables named %TIME%, %DATE% so check for our internal macros first! + if (resolveTimePhrase(Zstr("Date"), Zstr("%Y-%m-%d"))) return timeStr; + if (resolveTimePhrase(Zstr("Time"), Zstr("%H%M%S"))) return timeStr; + if (resolveTimePhrase(Zstr("TimeStamp"), Zstr("%Y-%m-%d %H%M%S"))) return timeStr; //e.g. "2012-05-15 131513" + if (resolveTimePhrase(Zstr("Year"), Zstr("%Y"))) return timeStr; + if (resolveTimePhrase(Zstr("Month"), Zstr("%m"))) return timeStr; + if (resolveTimePhrase(Zstr("MonthName"), Zstr("%b"))) return timeStr; //e.g. "Jan" + if (resolveTimePhrase(Zstr("Day"), Zstr("%d"))) return timeStr; + if (resolveTimePhrase(Zstr("Hour"), Zstr("%H"))) return timeStr; + if (resolveTimePhrase(Zstr("Min"), Zstr("%M"))) return timeStr; + if (resolveTimePhrase(Zstr("Sec"), Zstr("%S"))) return timeStr; + if (resolveTimePhrase(Zstr("WeekDayName"), Zstr("%a"))) return timeStr; //e.g. "Mon" + if (resolveTimePhrase(Zstr("Week"), Zstr("%V"))) return timeStr; //ISO 8601 week of the year + + if (equalAsciiNoCase(macro, Zstr("WeekDay"))) + { + const int weekDayStartSunday = stringTo(formatTime(Zstr("%w"))); //[0 (Sunday), 6 (Saturday)] => not localized! + //alternative 1: use "%u": ISO 8601 weekday as number with Monday as 1 (1-7) => newer standard than %w + //alternative 2: ::mktime() + std::tm::tm_wday + + const int weekDayStartMonday = (weekDayStartSunday + 6) % 7; //+6 == -1 in Z_7 + // [0-Monday, 6-Sunday] + + const int weekDayStartLocal = ((weekDayStartMonday + 7 - static_cast(getFirstDayOfWeek())) % 7) + 1; + //[1 (local first day of week), 7 (local last day of week)] + + return numberTo(weekDayStartLocal); + } + + //try to resolve as environment variables + if (std::optional value = getEnvironmentVar(macro)) + return *value; + + return {}; +} + +const Zchar MACRO_SEP = Zstr('%'); +} + + +//returns expanded or original string +Zstring zen::expandMacros(const Zstring& text) +{ + if (contains(text, MACRO_SEP)) + { + Zstring prefix = beforeFirst(text, MACRO_SEP, IfNotFoundReturn::none); + Zstring rest = afterFirst (text, MACRO_SEP, IfNotFoundReturn::none); + if (contains(rest, MACRO_SEP)) + { + Zstring potentialMacro = beforeFirst(rest, MACRO_SEP, IfNotFoundReturn::none); + Zstring postfix = afterFirst (rest, MACRO_SEP, IfNotFoundReturn::none); //text == prefix + MACRO_SEP + potentialMacro + MACRO_SEP + postfix + + if (std::optional value = tryResolveMacro(potentialMacro)) + return prefix + *value + expandMacros(postfix); + else + return prefix + MACRO_SEP + potentialMacro + expandMacros(MACRO_SEP + postfix); + } + } + return text; +} + + +namespace +{ + + +//expand volume name if possible, return original input otherwise +Zstring tryExpandVolumeName(Zstring pathPhrase) // [volname]:\folder [volname]\folder [volname]folder -> C:\folder +{ + //we only expect the [.*] pattern at the beginning => do not touch dir names like "C:\somedir\[stuff]" + trim(pathPhrase, TrimSide::left); + + if (startsWith(pathPhrase, Zstr('['))) + { + return "/.../" + pathPhrase; + } + return pathPhrase; +} +} + + +std::vector zen::getPathPhraseAliases(const Zstring& itemPath) +{ + assert(!itemPath.empty()); + std::vector pathAliases{makePathPhrase(itemPath)}; + + { + + //environment variables: C:\Users\ -> %UserProfile% + auto substByMacro = [&](const ZstringView macroName, const Zstring& macroPath) + { + //should use a replaceCpy() that considers "local path" case-sensitivity (if only we had one...) + if (contains(itemPath, macroPath)) + pathAliases.push_back(makePathPhrase(replaceCpyAsciiNoCase(itemPath, macroPath, Zstring() + MACRO_SEP + macroName + MACRO_SEP))); + }; + + for (const ZstringView envName : + { + "HOME", //Linux: /home/ Mac: /Users/ + //"USER", -> any benefit? + }) + if (const std::optional envPath = getEnvironmentVar(envName)) + substByMacro(envName, *envPath); + + } + //removeDuplicates()? should not be needed... + + std::sort(pathAliases.begin(), pathAliases.end(), LessNaturalSort() /*even on Linux*/); + return pathAliases; +} + + +Zstring zen::makePathPhrase(const Zstring& itemPath) +{ + if (endsWith(itemPath, Zstr(' '))) //path phrase concept must survive trimming! + return itemPath + FILE_NAME_SEPARATOR; + return itemPath; +} + + +//coordinate changes with acceptsFolderPathPhraseNative()! +Zstring zen::getResolvedFilePath(const Zstring& pathPhrase) //noexcept +{ + Zstring path = pathPhrase; + + path = expandMacros(path); //expand before trimming! + + trim(path); //remove leading/trailing whitespace before allowing misinterpretation in applyLongPathPrefix() + + { + path = tryExpandVolumeName(path); //may block for slow USB sticks and idle HDDs! + + /* need to resolve relative paths: + WINDOWS: + - \\?\-prefix requires absolute names + - Volume Shadow Copy: volume name needs to be part of each file path + - file icon buffer (at least for extensions that are actually read from disk, like "exe") + WINDOWS/LINUX: + - detection of dependent directories, e.g. "\" and "C:\test" */ + path = resolveRelativePath(path); + } + + //remove trailing slash, unless volume root: + if (const std::optional pc = parsePathComponents(path)) + path = appendPath(pc->rootPath, pc->relPath); + + return path; +} + + diff --git a/zen/resolve_path.h b/zen/resolve_path.h new file mode 100644 index 0000000..bfef087 --- /dev/null +++ b/zen/resolve_path.h @@ -0,0 +1,31 @@ +// ***************************************************************************** +// * 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 RESOLVE_PATH_H_817402834713454 +#define RESOLVE_PATH_H_817402834713454 + +#include "file_error.h" + + +namespace zen +{ +/* - expand macros + - trim whitespace + - expand volume path by name + - convert relative paths into absolute + + => may block for slow USB sticks and idle HDDs */ +Zstring getResolvedFilePath(const Zstring& pathPhrase); //noexcept + +//macro substitution only +Zstring expandMacros(const Zstring& text); + +std::vector getPathPhraseAliases(const Zstring& itemPath); +Zstring makePathPhrase(const Zstring& itemPath); + +} + +#endif //RESOLVE_PATH_H_817402834713454 diff --git a/zen/ring_buffer.h b/zen/ring_buffer.h new file mode 100644 index 0000000..f6cc3e5 --- /dev/null +++ b/zen/ring_buffer.h @@ -0,0 +1,255 @@ +// ***************************************************************************** +// * 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 RING_BUFFER_H_01238467085684139453534 +#define RING_BUFFER_H_01238467085684139453534 + +#include +#include "scope_guard.h" + + +namespace zen +{ +//like std::deque<> but with a non-garbage implementation: circular buffer with std::vector<>-like exponential growth! +//https://stackoverflow.com/questions/39324192/why-is-an-stl-deque-not-implemented-as-just-a-circular-vector + +template +class RingBuffer +{ +public: + RingBuffer() {} + + RingBuffer(RingBuffer&& tmp) noexcept : rawMem_(std::move(tmp.rawMem_)), capacity_(tmp.capacity_), bufStart_(tmp.bufStart_), size_(tmp.size_) + { + tmp.capacity_ = tmp.bufStart_ = tmp.size_ = 0; + } + RingBuffer& operator=(RingBuffer&& tmp) noexcept { swap(tmp); return *this; } //noexcept *required* to support move for reallocations in std::vector and std::swap!!! + + ~RingBuffer() { clear(); } + + using value_type = T; + using reference = T&; + using const_reference = const T&; + + size_t size () const { return size_; } + size_t capacity() const { return capacity_; } + bool empty () const { return size_ == 0; } + + reference front() { checkInvariants(); assert(!empty()); return getBufPtr()[bufStart_]; } + const_reference front() const { checkInvariants(); assert(!empty()); return getBufPtr()[bufStart_]; } + + reference back() { checkInvariants(); assert(!empty()); return getBufPtr()[getBufPos(size_ - 1)]; } + const_reference back() const { checkInvariants(); assert(!empty()); return getBufPtr()[getBufPos(size_ - 1)]; } + + template + void push_front(U&& value) + { + reserve(size_ + 1); //throw ? + ::new (getBufPtr() + getBufPos(capacity_ - 1)) T(std::forward(value)); //throw ? + ++size_; + bufStart_ = getBufPos(capacity_ - 1); + } + + template + void push_back(U&& value) + { + reserve(size_ + 1); //throw ? + ::new (getBufPtr() + getBufPos(size_)) T(std::forward(value)); //throw ? + ++size_; + } + + void pop_front() + { + front().~T(); + --size_; + + if (size_ == 0) + bufStart_ = 0; + else + bufStart_ = getBufPos(1); + } + + void pop_back() + { + back().~T(); + --size_; + + if (size_ == 0) + bufStart_ = 0; + } + + void clear() + { + checkInvariants(); + + const size_t frontSize = std::min(size_, capacity_ - bufStart_); + + std::destroy(getBufPtr() + bufStart_, getBufPtr() + bufStart_ + frontSize); + std::destroy(getBufPtr(), getBufPtr() + size_ - frontSize); + bufStart_ = size_ = 0; + } + + template + void insert_back(Iterator first, Iterator last) //throw ? (strong exception-safety!) + { + const size_t len = last - first; + reserve(size_ + len); //throw ? + + const size_t endPos = getBufPos(size_); + const size_t tailSize = std::min(len, capacity_ - endPos); + + std::uninitialized_copy(first, first + tailSize, getBufPtr() + endPos); //throw ? + ZEN_ON_SCOPE_FAIL(std::destroy(first, first + tailSize)); + std::uninitialized_copy(first + tailSize, last, getBufPtr()); //throw ? + + size_ += len; + } + + //contract: last - first <= size() + template + void extract_front(Iterator first, Iterator last) //throw ? strongly exception-safe! (but only basic exception safety for [first, last) range) + { + checkInvariants(); + const size_t len = last - first; + assert(size_ >= len); + + const size_t frontSize = std::min(len, capacity_ - bufStart_); + + auto itTrg = std::copy(getBufPtr() + bufStart_, getBufPtr() + bufStart_ + frontSize, first); //throw ? + /**/ std::copy(getBufPtr(), getBufPtr() + len - frontSize, itTrg); // + + std::destroy(getBufPtr() + bufStart_, getBufPtr() + bufStart_ + frontSize); + std::destroy(getBufPtr(), getBufPtr() + len - frontSize); + + size_ -= len; + + if (size_ == 0) + bufStart_ = 0; + else + bufStart_ = getBufPos(len); + } + + void swap(RingBuffer& other) + { + std::swap(rawMem_, other.rawMem_); + std::swap(capacity_, other.capacity_); + std::swap(bufStart_, other.bufStart_); + std::swap(size_, other.size_); + } + + void reserve(size_t minCapacity) //throw ? (strong exception-safety!) + { + checkInvariants(); + + if (minCapacity > capacity_) + { + const size_t newCapacity = std::max(minCapacity + minCapacity / 2, minCapacity); //no lower limit for capacity: just like std::vector<> + + RingBuffer newBuf(newCapacity); //throw ? + + T* itTrg = reinterpret_cast(newBuf.rawMem_.get()); + + const size_t frontSize = std::min(size_, capacity_ - bufStart_); + + itTrg = uninitializedMoveIfNoexcept(getBufPtr() + bufStart_, getBufPtr() + bufStart_ + frontSize, itTrg); //throw ? + newBuf.size_ = frontSize; //pass ownership + /**/ uninitializedMoveIfNoexcept(getBufPtr(), getBufPtr() + size_ - frontSize, itTrg); //throw ? + newBuf.size_ = size_; // + + newBuf.swap(*this); + } + } + + const T& operator[](size_t offset) const + { + assert(offset < size()); //design by contract! no runtime check! + return getBufPtr()[getBufPos(offset)]; + } + + T& operator[](size_t offset) { return const_cast(static_cast(this)->operator[](offset)); } + + template + class Iterator + { + public: + using iterator_category = std::random_access_iterator_tag; + using value_type = Value; + using difference_type = ptrdiff_t; + using pointer = Value*; + using reference = Value&; + + Iterator(Container& container, size_t offset) : container_(&container), offset_(offset) {} + Iterator& operator++() { ++offset_; return *this; } + Iterator& operator--() { --offset_; return *this; } + Iterator& operator+=(ptrdiff_t offset) { offset_ += offset; return *this; } + Value& operator* () const { return (*container_)[offset_]; } + Value* operator->() const { return &(*container_)[offset_]; } + inline friend Iterator operator+(const Iterator& lhs, ptrdiff_t offset) { Iterator tmp(lhs); return tmp += offset; } + inline friend ptrdiff_t operator-(const Iterator& lhs, const Iterator& rhs) { return lhs.offset_ - rhs.offset_; } + inline friend bool operator==(const Iterator& lhs, const Iterator& rhs) { assert(lhs.container_ == rhs.container_); return lhs.offset_ == rhs.offset_; } + inline friend std::strong_ordering operator<=>(const Iterator& lhs, const Iterator& rhs) { assert(lhs.container_ == rhs.container_); return lhs.offset_ <=> rhs.offset_; } + //GCC debug needs "operator<=" + private: + Container* container_ = nullptr; //iterator must be assignable + ptrdiff_t offset_ = 0; + }; + + using iterator = Iterator< RingBuffer, T>; + using const_iterator = Iterator; + + iterator begin() { return {*this, 0 }; } + iterator end () { return {*this, size_}; } + + const_iterator begin() const { return {*this, 0 }; } + const_iterator end () const { return {*this, size_}; } + + const_iterator cbegin() const { return begin(); } + const_iterator cend () const { return end (); } + +private: + RingBuffer (const RingBuffer&) = delete; //wait until there is a reason to copy a RingBuffer + RingBuffer& operator=(const RingBuffer&) = delete; // + + explicit RingBuffer(size_t capacity) : + rawMem_(static_cast(::operator new (capacity * sizeof(T)))), //throw std::bad_alloc + capacity_(capacity) {} + + /**/ T* getBufPtr() { return reinterpret_cast(rawMem_.get()); } + const T* getBufPtr() const { return reinterpret_cast(rawMem_.get()); } + + //unlike pure std::uninitialized_move, this one allows for strong exception-safety! + static T* uninitializedMoveIfNoexcept(T* first, T* last, T* firstTrg) + { + return uninitializedMoveIfNoexcept(first, last, firstTrg, std::is_nothrow_move_constructible()); + } + static T* uninitializedMoveIfNoexcept(T* first, T* last, T* firstTrg, std::true_type ) { return std::uninitialized_move(first, last, firstTrg); } + static T* uninitializedMoveIfNoexcept(T* first, T* last, T* firstTrg, std::false_type) { return std::uninitialized_copy(first, last, firstTrg); } //throw ? + + size_t getBufPos(size_t offset) const + { + //assert(offset < capacity_); -> redundant in this context + size_t bufPos = bufStart_ + offset; + if (bufPos >= capacity_) + bufPos -= capacity_; + return bufPos; + } + + void checkInvariants() const + { + assert(bufStart_ == 0 || bufStart_ < capacity_); + assert(size_ <= capacity_); + } + + struct FreeStoreDelete { void operator()(std::byte* p) const { ::operator delete (p); } }; + + std::unique_ptr rawMem_; + size_t capacity_ = 0; //as number of T + size_t bufStart_ = 0; //< capacity_ + size_t size_ = 0; //<= capacity_ +}; +} + +#endif //RING_BUFFER_H_01238467085684139453534 diff --git a/zen/scope_guard.h b/zen/scope_guard.h new file mode 100644 index 0000000..8575a07 --- /dev/null +++ b/zen/scope_guard.h @@ -0,0 +1,113 @@ +// ***************************************************************************** +// * 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 SCOPE_GUARD_H_8971632487321434 +#define SCOPE_GUARD_H_8971632487321434 + +#include +#include "legacy_compiler.h" //std::uncaught_exceptions + +//best of Zen, Loki and C++17 + +namespace zen +{ +/* Scope Guard + + auto guardAio = zen::makeGuard([&] { ::CloseHandle(hDir); }); + ... + guardAio.dismiss(); + + Scope Exit: + ZEN_ON_SCOPE_EXIT (CleanUp()); + ZEN_ON_SCOPE_FAIL (UndoTemporaryWork()); + ZEN_ON_SCOPE_SUCCESS(NotifySuccess()); */ + +enum class ScopeGuardRunMode +{ + onExit, + onSuccess, + onFail +}; + + +//partially specialize scope guard destructor code and get rid of those pesky MSVC "4127 conditional expression is constant" +template inline +void runScopeGuardDestructor(F& fun, bool failed, std::integral_constant) +{ + if (!failed) + fun(); //throw X + else + try { fun(); } + catch (...) { assert(false); } +} + + +template inline +void runScopeGuardDestructor(F& fun, bool failed, std::integral_constant) +{ + if (!failed) + fun(); //throw X +} + + +template inline +void runScopeGuardDestructor(F& fun, bool failed, std::integral_constant) noexcept +{ + if (failed) + try { fun(); } + catch (...) { assert(false); } +} + + +template +class ScopeGuard +{ +public: + explicit ScopeGuard(const F& fun) : fun_(fun) {} + explicit ScopeGuard( F&& fun) : fun_(std::move(fun)) {} + + //ScopeGuard(ScopeGuard&& tmp) : + // fun_(std::move(tmp.fun_)), + // exeptionCount_(tmp.exeptionCount_), + // dismissed_(tmp.dismissed_) { tmp.dismissed_ = true; } + + ~ScopeGuard() noexcept(runMode == ScopeGuardRunMode::onFail) + { + if (!dismissed_) + { + const bool failed = std::uncaught_exceptions() > exeptionCount_; + runScopeGuardDestructor(fun_, failed, std::integral_constant()); + } + } + + void dismiss() { dismissed_ = true; } + +private: + ScopeGuard (const ScopeGuard&) = delete; + ScopeGuard& operator=(const ScopeGuard&) = delete; + + const F fun_; + const int exeptionCount_ = std::uncaught_exceptions(); + bool dismissed_ = false; +}; + + +template inline +auto makeGuard(F&& fun) { return ScopeGuard>(std::forward(fun)); } +} + +#define ZEN_CONCAT_SUB(X, Y) X ## Y +#define ZEN_CONCAT(X, Y) ZEN_CONCAT_SUB(X, Y) + +#define ZEN_CHECK_CASE_FOR_CONSTANT(X) case X: return ZEN_CHECK_CASE_FOR_CONSTANT_IMPL(#X) +#define ZEN_CHECK_CASE_FOR_CONSTANT_IMPL(X) L ## X + + +#define ZEN_ON_SCOPE_EXIT(X) [[maybe_unused]] auto ZEN_CONCAT(scopeGuard, __LINE__) = zen::makeGuard([&]{ X; }); +#define ZEN_ON_SCOPE_FAIL(X) [[maybe_unused]] auto ZEN_CONCAT(scopeGuard, __LINE__) = zen::makeGuard([&]{ X; }); +#define ZEN_ON_SCOPE_SUCCESS(X) [[maybe_unused]] auto ZEN_CONCAT(scopeGuard, __LINE__) = zen::makeGuard([&]{ X; }); + +#endif //SCOPE_GUARD_H_8971632487321434 diff --git a/zen/serialize.h b/zen/serialize.h new file mode 100644 index 0000000..844c27d --- /dev/null +++ b/zen/serialize.h @@ -0,0 +1,437 @@ +// ***************************************************************************** +// * 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 SERIALIZE_H_839405783574356 +#define SERIALIZE_H_839405783574356 + +#include +#include "sys_error.h" +//keep header clean from specific stream implementations! (e.g.file_io.h)! used by abstract.h! + + +namespace zen +{ +/* high-performance unformatted serialization (avoiding wxMemoryOutputStream/wxMemoryInputStream inefficiencies) + + ---------------------------- + | Binary Container Concept | + ---------------------------- + binary container for data storage: must support "basic" std::vector interface (e.g. std::vector, std::string, Zbase) + + --------------------------------- + | Unbuffered Input Stream Concept | + --------------------------------- + size_t getBlockSize(); //throw X + size_t tryRead(void* buffer, size_t bytesToRead); //throw X; may return short; only 0 means EOF! CONTRACT: bytesToRead > 0! + + ---------------------------------- + | Unbuffered Output Stream Concept | + ---------------------------------- + size_t getBlockSize(); //throw X + size_t tryWrite(const void* buffer, size_t bytesToWrite); //throw X; may return short! CONTRACT: bytesToWrite > 0 + + =============================================================================================== + + --------------------------------- + | Buffered Input Stream Concept | + --------------------------------- + size_t read(void* buffer, size_t bytesToRead); //throw X; return "bytesToRead" bytes unless end of stream! + + ---------------------------------- + | Buffered Output Stream Concept | + ---------------------------------- + void write(const void* buffer, size_t bytesToWrite); //throw X */ + +using IoCallback = std::function; //throw X + + +template +BinContainer unbufferedLoad(Function tryRead/*(void* buffer, size_t bytesToRead) throw X; may return short; only 0 means EOF*/, + size_t blockSize); //throw X + +template +void unbufferedSave(const BinContainer& cont, Function tryWrite /*(const void* buffer, size_t bytesToWrite) throw X; may return short*/, + size_t blockSize); //throw X + +template +uint64_t /*streamSize*/ unbufferedStreamCopy(Function1 tryRead /*(void* buffer, size_t bytesToRead) throw X; may return short; only 0 means EOF*/, size_t blockSizeIn, + Function2 tryWrite /*(const void* buffer, size_t bytesToWrite) throw X; may return short*/, size_t blockSizeOut); //throw X + + +template void writeNumber (BufferedOutputStream& stream, const N& num); // +template void writeContainer(BufferedOutputStream& stream, const C& str); //noexcept +template < class BufferedOutputStream> void writeArray (BufferedOutputStream& stream, const void* buffer, size_t len); // +//---------------------------------------------------------------------- +struct SysErrorUnexpectedEos : public SysError +{ + SysErrorUnexpectedEos() : SysError(_("File content is corrupted.") + L" (unexpected end of stream)") {} +}; + +template N readNumber (BufferedInputStream& stream); //throw SysErrorUnexpectedEos (corrupted data) +template C readContainer(BufferedInputStream& stream); // +template < class BufferedInputStream> void readArray (BufferedInputStream& stream, void* buffer, size_t len); // + + +struct IOCallbackDivider +{ + IOCallbackDivider(const IoCallback& notifyUnbufferedIO, int64_t& totalBytesNotified) : + totalBytesNotified_(totalBytesNotified), + notifyUnbufferedIO_(notifyUnbufferedIO) { assert(totalBytesNotified == 0); } + + void operator()(int64_t bytesDelta) //throw X! + { + if (notifyUnbufferedIO_) notifyUnbufferedIO_((totalBytesNotified_ + bytesDelta) / 2 - totalBytesNotified_ / 2); //throw X! + totalBytesNotified_ += bytesDelta; + } + +private: + int64_t& totalBytesNotified_; + const IoCallback& notifyUnbufferedIO_; +}; + +//------------------------------------------------------------------------------------- + +//buffered input/output stream reference implementations: +struct MemoryStreamIn +{ + explicit MemoryStreamIn(const std::string_view& stream) : memRef_(stream) {} + + MemoryStreamIn(std::string&&) = delete; //careful: do NOT store reference to a temporary! + + size_t read(void* buffer, size_t bytesToRead) //return "bytesToRead" bytes unless end of stream! + { + const size_t junkSize = std::min(bytesToRead, memRef_.size() - pos_); + std::memcpy(buffer, memRef_.data() + pos_, junkSize); + pos_ += junkSize; + return junkSize; + } + + size_t pos() const { return pos_; } + +private: + //MemoryStreamIn (const MemoryStreamIn&) = delete; -> why not allow copying? + MemoryStreamIn& operator=(const MemoryStreamIn&) = delete; + + const std::string_view memRef_; + size_t pos_ = 0; +}; + + +struct MemoryStreamOut +{ + MemoryStreamOut() = default; + + void write(const void* buffer, size_t bytesToWrite) + { + memBuf_.append(static_cast(buffer), bytesToWrite); + } + + const std::string& ref() const { return memBuf_; } + /**/ std::string& ref() { return memBuf_; } + +private: + MemoryStreamOut (const MemoryStreamOut&) = delete; + MemoryStreamOut& operator=(const MemoryStreamOut&) = delete; + + std::string memBuf_; +}; + +//------------------------------------------------------------------------------------- + +template +struct BufferedInputStream +{ + BufferedInputStream(Function tryRead /*(void* buffer, size_t bytesToRead) throw X; may return short; only 0 means EOF*/, + size_t blockSize) : + tryRead_(tryRead), blockSize_(blockSize) {} + + size_t read(void* buffer, size_t bytesToRead) //throw X; return "bytesToRead" bytes unless end of stream! + { + assert(memBuf_.size() >= blockSize_); + assert(bufPos_ <= bufPosEnd_ && bufPosEnd_ <= memBuf_.size()); + const auto bufStart = buffer; + for (;;) + { + const size_t junkSize = std::min(bytesToRead, bufPosEnd_ - bufPos_); + std::memcpy(buffer, memBuf_.data() + bufPos_ /*caveat: vector debug checks*/, junkSize); + bufPos_ += junkSize; + buffer = static_cast(buffer) + junkSize; + bytesToRead -= junkSize; + + if (bytesToRead == 0) + break; + //-------------------------------------------------------------------- + const size_t bytesRead = tryRead_(memBuf_.data(), blockSize_); //throw X; may return short, only 0 means EOF! => CONTRACT: bytesToRead > 0 + bufPos_ = 0; + bufPosEnd_ = bytesRead; + + if (bytesRead == 0) //end of file + break; + } + return static_cast(buffer) - + static_cast(bufStart); + } + +private: + BufferedInputStream (const BufferedInputStream&) = delete; + BufferedInputStream& operator=(const BufferedInputStream&) = delete; + + Function tryRead_; + const size_t blockSize_; + + size_t bufPos_ = 0; + size_t bufPosEnd_= 0; + std::vector memBuf_{blockSize_}; +}; + + +template +struct BufferedOutputStream +{ + BufferedOutputStream(Function tryWrite /*(const void* buffer, size_t bytesToWrite) throw X; may return short*/, + size_t blockSize) : + tryWrite_(tryWrite), blockSize_(blockSize) {} + + ~BufferedOutputStream() + { + } + + void write(const void* buffer, size_t bytesToWrite) //throw X + { + assert(memBuf_.size() >= blockSize_); + assert(bufPos_ <= bufPosEnd_ && bufPosEnd_ <= memBuf_.size()); + + for (;;) + { + const size_t junkSize = std::min(bytesToWrite, blockSize_ - (bufPosEnd_ - bufPos_)); + std::memcpy(memBuf_.data() + bufPosEnd_, buffer, junkSize); + bufPosEnd_ += junkSize; + buffer = static_cast(buffer) + junkSize; + bytesToWrite -= junkSize; + + if (bytesToWrite == 0) + return; + //-------------------------------------------------------------------- + bufPos_ += tryWrite_(memBuf_.data() + bufPos_, blockSize_); //throw X; may return short + + if (memBuf_.size() - bufPos_ < blockSize_ || //support memBuf_.size() > blockSize to avoid memmove()s + bufPos_ == bufPosEnd_) + { + std::memmove(memBuf_.data(), memBuf_.data() + bufPos_, bufPosEnd_ - bufPos_); + bufPosEnd_ -= bufPos_; + bufPos_ = 0; + } + } + } + + void flushBuffer() //throw X + { + assert(bufPosEnd_ - bufPos_ <= blockSize_); + assert(bufPos_ <= bufPosEnd_ && bufPosEnd_ <= memBuf_.size()); + while (bufPos_ != bufPosEnd_) + bufPos_ += tryWrite_(memBuf_.data() + bufPos_, bufPosEnd_ - bufPos_); //throw X + } + +private: + BufferedOutputStream (const BufferedOutputStream&) = delete; + BufferedOutputStream& operator=(const BufferedOutputStream&) = delete; + + Function tryWrite_; + const size_t blockSize_; + + size_t bufPos_ = 0; + size_t bufPosEnd_ = 0; + std::vector memBuf_{2 * /*=> mitigate memmove()*/ blockSize_}; //throw FileError +}; + +//------------------------------------------------------------------------------------- + +template inline +BinContainer unbufferedLoad(Function tryRead /*(void* buffer, size_t bytesToRead) throw X; may return short; only 0 means EOF*/, + size_t blockSize) //throw X +{ + static_assert(sizeof(typename BinContainer::value_type) == 1); //expect: bytes + if (blockSize == 0) + throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); + + BinContainer buf; + for (;;) + { +#ifndef ZEN_HAVE_RESIZE_AND_OVERWRITE +#error include legacy_compiler.h! +#endif +#if ZEN_HAVE_RESIZE_AND_OVERWRITE //permature(?) perf optimization; avoid needless zero-initialization: + size_t bytesRead = 0; + buf.resize_and_overwrite(buf.size() + blockSize, [&, bufSizeOld = buf.size()](char* rawBuf, size_t /*rawBufSize: caveat: may be larger than what's requested*/) + //permature(?) perf optimization; avoid needless zero-initialization: + { + bytesRead = tryRead(rawBuf + bufSizeOld, blockSize); //throw X; may return short; only 0 means EOF + return bufSizeOld + bytesRead; + }); +#else + buf.resize(buf.size() + blockSize); //needless zero-initialization! + const size_t bytesRead = tryRead(buf.data() + buf.size() - blockSize, blockSize); //throw X; may return short; only 0 means EOF + buf.resize(buf.size() - blockSize + bytesRead); //caveat: unsigned arithmetics +#endif + if (bytesRead == 0) //end of file + { + //caveat: memory consumption of returned string! + if (buf.capacity() > buf.size() * 3 / 2) //reference: in worst case, std::vector with growth factor 1.5 "wastes" 50% of its size as unused capacity + buf.shrink_to_fit(); //=> shrink if buffer is wasting more than that! + + return buf; + } + } +} + + +template inline +void unbufferedSave(const BinContainer& cont, + Function tryWrite /*(const void* buffer, size_t bytesToWrite) throw X; may return short*/, + size_t blockSize) //throw X +{ + static_assert(sizeof(typename BinContainer::value_type) == 1); //expect: bytes + if (blockSize == 0) + throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); + + const size_t bufPosEnd = cont.size(); + size_t bufPos = 0; + + while (bufPos < bufPosEnd) + bufPos += tryWrite(cont.data() + bufPos, std::min(bufPosEnd - bufPos, blockSize)); //throw X +} + + +template inline +uint64_t /*streamSize*/ unbufferedStreamCopy(Function1 tryRead /*(void* buffer, size_t bytesToRead) throw X; may return short; only 0 means EOF*/, + size_t blockSizeIn, + Function2 tryWrite /*(const void* buffer, size_t bytesToWrite) throw X; may return short*/, + size_t blockSizeOut) //throw X +{ + /* caveat: buffer block sizes might not be a power of 2: + - f_iosize for network share on macOS + - libssh2 uses weird packet sizes like MAX_SFTP_OUTGOING_SIZE (30000), and will send incomplete packages if block size is not an exact multiple :( + - MTP uses file size as blocksize if under 256 kB (=> can be as small as 1 byte! https://freefilesync.org/forum/viewtopic.php?t=9823) + => that's a problem because we want input/output sizes to be multiples of each other to help avoid the std::memmove() below */ +#if 0 + blockSizeIn = std::bit_ceil(blockSizeIn); + blockSizeOut = std::bit_ceil(blockSizeOut); +#endif + if (blockSizeIn == 0 || blockSizeOut == 0) + throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); + + const size_t bufCapacity = blockSizeOut - 1 + blockSizeIn; + const size_t alignment = ::sysconf(_SC_PAGESIZE); //-1 on error => posix_memalign() will fail + assert(alignment >= sizeof(void*) && std::has_single_bit(alignment)); //required by posix_memalign() + std::byte* buf = nullptr; + errno = ::posix_memalign(reinterpret_cast(&buf), alignment, bufCapacity); + ZEN_ON_SCOPE_EXIT(::free(buf)); + + uint64_t streamSize = 0; + size_t bufPosEnd = 0; + for (;;) + { + const size_t bytesRead = tryRead(buf + bufPosEnd, blockSizeIn); //throw X; may return short; only 0 means EOF + + if (bytesRead == 0) //end of file + { + size_t bufPos = 0; + while (bufPos < bufPosEnd) + bufPos += tryWrite(buf + bufPos, bufPosEnd - bufPos); //throw X; may return short + return streamSize; + } + else + { + streamSize += bytesRead; + bufPosEnd += bytesRead; + + size_t bufPos = 0; + while (bufPosEnd - bufPos >= blockSizeOut) + bufPos += tryWrite(buf + bufPos, blockSizeOut); //throw X; may return short + + if (bufPos > 0) + { + bufPosEnd -= bufPos; + std::memmove(buf, buf + bufPos, bufPosEnd); + } + } + } +} + +//------------------------------------------------------------------------------------- + +template inline +void writeArray(BufferedOutputStream& stream, const void* buffer, size_t len) +{ + stream.write(buffer, len); +} + + +template inline +void writeNumber(BufferedOutputStream& stream, const N& num) +{ + static_assert(isArithmetic || std::is_same_v || std::is_enum_v); + writeArray(stream, &num, sizeof(N)); +} + + +template inline +void writeContainer(BufferedOutputStream& stream, const C& cont) //don't even consider UTF8 conversions here, we're handling arbitrary binary data! +{ + const auto size = cont.size(); + + assert(size <= INT32_MAX); + writeNumber(stream, static_cast(size)); //use *signed* integer to help catch data corruption + + if (size > 0) + writeArray(stream, &cont[0], sizeof(typename C::value_type) * size); //don't use c_str(), but access uniformly via STL interface +} + + +template inline +void readArray(BufferedInputStream& stream, void* buffer, size_t len) //throw SysErrorUnexpectedEos +{ + const size_t bytesRead = stream.read(buffer, len); + assert(bytesRead <= len); //buffer overflow otherwise not always detected! + if (bytesRead < len) + throw SysErrorUnexpectedEos(); +} + + +template inline +N readNumber(BufferedInputStream& stream) //throw SysErrorUnexpectedEos +{ + static_assert(isArithmetic || std::is_same_v || std::is_enum_v); + N num; //uninitialized + readArray(stream, &num, sizeof(N)); //throw SysErrorUnexpectedEos + return num; +} + + +template inline +C readContainer(BufferedInputStream& stream) //throw SysErrorUnexpectedEos +{ + const auto size = readNumber(stream); //throw SysErrorUnexpectedEos + if (size < 0) //most likely due to data corruption! + throw SysErrorUnexpectedEos(); + + C cont; + if (size > 0) + { + try + { + cont.resize(size); //throw std::length_error, std::bad_alloc + } + catch (std::length_error&) { throw SysErrorUnexpectedEos(); } //most likely due to data corruption! + catch ( std::bad_alloc&) { throw SysErrorUnexpectedEos(); } // + + readArray(stream, &cont[0], sizeof(typename C::value_type) * size); //throw SysErrorUnexpectedEos + } + return cont; +} +} + +#endif //SERIALIZE_H_839405783574356 diff --git a/zen/shutdown.cpp b/zen/shutdown.cpp new file mode 100644 index 0000000..ee68b46 --- /dev/null +++ b/zen/shutdown.cpp @@ -0,0 +1,105 @@ +// ***************************************************************************** +// * 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 "shutdown.h" +#include "thread.h" + #include + + +using namespace zen; + + + + +void zen::shutdownSystem() //throw FileError +{ + assert(runningOnMainThread()); + if (runningOnMainThread()) + onSystemShutdownRunTasks(); + try + { + //https://linux.die.net/man/2/reboot => needs admin rights! + //"systemctl" should work without admin rights: + auto [exitCode, output] = consoleExecute("systemctl poweroff", std::nullopt /*timeoutMs*/); //throw SysError, (SysErrorTimeOut) + trim(output); + if (!output.empty()) //see comment in suspendSystem() + throw SysError(utfTo(output)); + + } + catch (const SysError& e) { throw FileError(_("Unable to shut down the system."), e.toString()); } +} + + +void zen::suspendSystem() //throw FileError +{ + try + { + //"systemctl" should work without admin rights: + auto [exitCode, output] = consoleExecute("systemctl suspend", std::nullopt /*timeoutMs*/); //throw SysError, (SysErrorTimeOut) + trim(output); + //why does "systemctl suspend" return exit code 1 despite apparent success!?? + if (!output.empty()) //at least we can assume "no output" on success + throw SysError(utfTo(output)); + + } + catch (const SysError& e) { throw FileError(_("Unable to shut down the system."), e.toString()); } +} + + +void zen::terminateProcess(int exitCode) +{ + std::quick_exit(exitCode); //[[noreturn]]; "Causes normal program termination to occur without completely cleaning the resources." => perfect + + + for (;;) //why still here?? => crash deliberately! + *reinterpret_cast(0) = 0; //crude but at least we'll get crash dumps *if* it ever happens +} + + +//Command line alternatives: + //Shut down: systemctl poweroff //alternative requiring admin: sudo shutdown -h 1 + //Sleep: systemctl suspend //alternative requiring admin: sudo pm-suspend + //Log off: gnome-session-quit --no-prompt + // alternative requiring admin: sudo killall Xorg + // alternative without admin: dbus-send --session --print-reply --dest=org.gnome.SessionManager /org/gnome/SessionManager org.gnome.SessionManager.Logout uint32:1 + + + +namespace +{ +using ShutdownTaskList = std::vector>>; +constinit Global globalShutdownTasks; +GLOBAL_RUN_ONCE(globalShutdownTasks.set(std::make_unique())); +} + + +void zen::onSystemShutdownRegister(const SharedRef>& task) +{ + assert(runningOnMainThread()); + + const auto& tasks = globalShutdownTasks.get(); + assert(tasks); + if (tasks) + tasks->push_back(task.ptr()); +} + + +void zen::onSystemShutdownRunTasks() +{ + assert(runningOnMainThread()); //no multithreading! else: after taskWeak.lock() task() references may go out of scope! (e.g. "this") + + const auto& tasks = globalShutdownTasks.get(); + assert(tasks); + if (tasks) + for (const std::weak_ptr>& taskWeak : *tasks) + if (const std::shared_ptr>& task = taskWeak.lock(); + task) + try + { (*task)(); } + catch (...) { assert(false); } + + globalShutdownTasks.set(nullptr); //trigger assert in onSystemShutdownRegister(), just in case... +} diff --git a/zen/shutdown.h b/zen/shutdown.h new file mode 100644 index 0000000..b4d51f6 --- /dev/null +++ b/zen/shutdown.h @@ -0,0 +1,26 @@ +// ***************************************************************************** +// * 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 SHUTDOWN_H_3423847870238407783265 +#define SHUTDOWN_H_3423847870238407783265 + +#include +#include "file_error.h" + + +namespace zen +{ +void shutdownSystem(); //throw FileError +void suspendSystem(); // +[[noreturn]] void terminateProcess(int exitCode); + +void onSystemShutdownRegister(const SharedRef>& task /*noexcept*/); //save important/user data! +void onSystemShutdownRegister( SharedRef>&& task) = delete; //no temporaries! shared_ptr should manage life time! +void onSystemShutdownRunTasks(); //call at appropriate time, e.g. when receiving wxEVT_QUERY_END_SESSION/wxEVT_END_SESSION +//+ also called by shutdownSystem() +} + +#endif //SHUTDOWN_H_3423847870238407783265 diff --git a/zen/socket.h b/zen/socket.h new file mode 100644 index 0000000..29b871c --- /dev/null +++ b/zen/socket.h @@ -0,0 +1,286 @@ +// ***************************************************************************** +// * 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 SOCKET_H_23498325972583947678456437 +#define SOCKET_H_23498325972583947678456437 + +#include "sys_error.h" + #include + #include //close + #include + #include //TCP_NODELAY + #include //getaddrinfo + + +namespace zen +{ +#define THROW_LAST_SYS_ERROR_WSA(functionName) \ + do { const ErrorCode ecInternal = getLastError(); throw SysError(formatSystemError(functionName, ecInternal)); } while (false) + + +#define THROW_LAST_SYS_ERROR_GAI(rcGai) \ + do { \ + if (rcGai == EAI_SYSTEM) /*"check errno for details"*/ \ + THROW_LAST_SYS_ERROR("getaddrinfo"); \ + \ + throw SysError(formatSystemError("getaddrinfo", formatGaiErrorCode(rcGai), utfTo(::gai_strerror(rcGai)))); \ + } while (false) + +inline +std::wstring formatGaiErrorCode(int ec) +{ + switch (ec) //codes used on both Linux and macOS + { + ZEN_CHECK_CASE_FOR_CONSTANT(EAI_ADDRFAMILY); + ZEN_CHECK_CASE_FOR_CONSTANT(EAI_AGAIN); + ZEN_CHECK_CASE_FOR_CONSTANT(EAI_BADFLAGS); + ZEN_CHECK_CASE_FOR_CONSTANT(EAI_FAIL); + ZEN_CHECK_CASE_FOR_CONSTANT(EAI_FAMILY); + ZEN_CHECK_CASE_FOR_CONSTANT(EAI_MEMORY); + ZEN_CHECK_CASE_FOR_CONSTANT(EAI_NODATA); + ZEN_CHECK_CASE_FOR_CONSTANT(EAI_NONAME); + ZEN_CHECK_CASE_FOR_CONSTANT(EAI_SERVICE); + ZEN_CHECK_CASE_FOR_CONSTANT(EAI_SOCKTYPE); + ZEN_CHECK_CASE_FOR_CONSTANT(EAI_SYSTEM); + ZEN_CHECK_CASE_FOR_CONSTANT(EAI_OVERFLOW); + ZEN_CHECK_CASE_FOR_CONSTANT(EAI_INPROGRESS); + ZEN_CHECK_CASE_FOR_CONSTANT(EAI_CANCELED); + ZEN_CHECK_CASE_FOR_CONSTANT(EAI_NOTCANCELED); + ZEN_CHECK_CASE_FOR_CONSTANT(EAI_ALLDONE); + ZEN_CHECK_CASE_FOR_CONSTANT(EAI_INTR); + ZEN_CHECK_CASE_FOR_CONSTANT(EAI_IDN_ENCODE); + default: + return replaceCpy(_("Error code %x"), L"%x", numberTo(ec)); + } +} + +//patch up socket portability: +using SocketType = int; +const SocketType invalidSocket = -1; +inline void closeSocket(SocketType s) { ::close(s); } + +void setNonBlocking(SocketType socket, bool value); //throw SysError + + +//Winsock needs to be initialized before calling any of these functions! (WSAStartup/WSACleanup) + + + +class Socket //throw SysError +{ +public: + Socket(const Zstring& server, const Zstring& serviceName, int timeoutSec) //throw SysError + { + //GetAddrInfo(): "If the pNodeName parameter contains an empty string, all registered addresses on the local computer are returned." + // "If the pNodeName parameter points to a string equal to "localhost", all loopback addresses on the local computer are returned." + if (trimCpy(server).empty()) + throw SysError(_("Server name must not be empty.")); + + Zstring nodeName = server; //macOS supports IDN out of the box, it seems :) - unlike Linux: + if (!isAsciiString(server)) + { + char* punyEncoded = nullptr; + int rc = idn2_lookup_ul(server.c_str(), &punyEncoded, IDN2_NFC_INPUT | IDN2_NONTRANSITIONAL); //follow libcurl/src/lib/idn.c + if (rc != IDN2_OK) + rc = idn2_lookup_ul(server.c_str(), &punyEncoded, IDN2_TRANSITIONAL); //fallback to TR46 Transitional mode for better IDNA2003 compatibility + if (rc != IDN2_OK) + throw SysError(formatSystemError("idn2_lookup_ul", replaceCpy(_("Error code %x"), L"%x", numberTo(rc)), L"")); + ZEN_ON_SCOPE_EXIT(idn2_free(punyEncoded)); + + nodeName = punyEncoded; + } + const addrinfo hints + { + .ai_flags = AI_ADDRCONFIG, //return IPv4 address iff system supports it; dito for IPv6 + .ai_family = AF_UNSPEC, //don't care if AF_INET or AF_INET6 + .ai_socktype = SOCK_STREAM, //we *do* care about this one! + }; + + addrinfo* servinfo = nullptr; + ZEN_ON_SCOPE_EXIT(if (servinfo) ::freeaddrinfo(servinfo)); + + const int rcGai = ::getaddrinfo(nodeName.c_str(), serviceName.c_str(), &hints, &servinfo); + if (rcGai != 0) + THROW_LAST_SYS_ERROR_GAI(rcGai); + if (!servinfo) + throw SysError(formatSystemError("getaddrinfo", L"", L"Empty server info.")); + + const auto getConnectedSocket = [timeoutSec](const auto& /*addrinfo*/ ai) + { + SocketType testSocket = ::socket(ai.ai_family, //int socket_family + SOCK_CLOEXEC | SOCK_NONBLOCK | + ai.ai_socktype, //int socket_type + ai.ai_protocol); //int protocol + if (testSocket == invalidSocket) + THROW_LAST_SYS_ERROR_WSA("socket"); + ZEN_ON_SCOPE_FAIL(closeSocket(testSocket)); + + if (::connect(testSocket, ai.ai_addr, static_cast(ai.ai_addrlen)) != 0) //0 or SOCKET_ERROR(-1) + { + if (errno != EINPROGRESS) + THROW_LAST_SYS_ERROR_WSA("connect"); + + fd_set writefds{}; + fd_set exceptfds{}; //mostly only relevant for connect() + FD_SET(testSocket, &writefds); + FD_SET(testSocket, &exceptfds); + + /*const*/ timeval tv{.tv_sec = timeoutSec}; + + const int rv = ::select( + testSocket + 1, //int nfds = "highest-numbered file descriptor in any of the three sets, plus 1" + nullptr, //fd_set* readfds + &writefds, //fd_set* writefds + &exceptfds, //fd_set* exceptfds + &tv); //const timeval* timeout + if (rv < 0) + THROW_LAST_SYS_ERROR_WSA("select"); + + if (rv == 0) //time-out! + throw SysError(formatSystemError("select, " + utfTo(_P("1 sec", "%x sec", timeoutSec)), ETIMEDOUT)); + int error = 0; + socklen_t optLen = sizeof(error); + if (::getsockopt(testSocket, //[in] SOCKET s + SOL_SOCKET, //[in] int level + SO_ERROR, //[in] int optname + reinterpret_cast(&error), //[out] char* optval + &optLen) //[in, out] socklen_t* optlen + != 0) + THROW_LAST_SYS_ERROR_WSA("getsockopt(SO_ERROR)"); + + if (error != 0) + throw SysError(formatSystemError("connect, SO_ERROR", static_cast(error))/*== system error code, apparently!?*/); + } + + setNonBlocking(testSocket, false); //throw SysError + + return testSocket; + }; + + /* getAddrInfo() often returns only one ai_family == AF_INET address, but more items are possible: + facebook.com: 1 x AF_INET6, 3 x AF_INET + microsoft.com: 5 x AF_INET => server not allowing connection: hanging for 5x timeoutSec :( */ + std::optional firstError; + for (const auto* /*::addrinfo*/ si = servinfo; si; si = si->ai_next) + try + { + socket_ = getConnectedSocket(*si); //throw SysError; pass ownership + firstError = std::nullopt; + break; + } + catch (const SysError& e) { if (!firstError) firstError = e; } + + if (firstError) + throw* firstError; + assert(socket_ != invalidSocket); //list was non-empty, so there's either an error, or a valid socket + ZEN_ON_SCOPE_FAIL(closeSocket(socket_)); + //----------------------------------------------------------- + //configure *after* selecting appropriate socket: cfg-failure should not discard otherwise fine connection! + + int noDelay = 1; //disable Nagle algorithm: https://brooker.co.za/blog/2024/05/09/nagle.html + //e.g. test case "website sync": 23% shorter comparison time! + if (::setsockopt(socket_, //_In_ SOCKET s + IPPROTO_TCP, //_In_ int level + TCP_NODELAY, //_In_ int optname + reinterpret_cast(&noDelay), //_In_ const char* optval + sizeof(noDelay)) != 0) //_In_ int optlen + THROW_LAST_SYS_ERROR_WSA("setsockopt(TCP_NODELAY)"); + } + + ~Socket() { closeSocket(socket_); } + + SocketType get() const { return socket_; } + +private: + Socket (const Socket&) = delete; + Socket& operator=(const Socket&) = delete; + + SocketType socket_ = invalidSocket; +}; + + +//more socket helper functions: +namespace +{ +size_t tryReadSocket(SocketType socket, void* buffer, size_t bytesToRead) //throw SysError; may return short, only 0 means EOF! +{ + if (bytesToRead == 0) //"read() with a count of 0 returns zero" => indistinguishable from end of file! => check! + throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); + + int bytesReceived = 0; + for (;;) + { + bytesReceived = ::recv(socket, //_In_ SOCKET s + static_cast(buffer), //_Out_ char* buf + static_cast(bytesToRead), //_In_ int len + 0); //_In_ int flags + if (bytesReceived >= 0 || errno != EINTR) + break; + } + if (bytesReceived < 0) + THROW_LAST_SYS_ERROR_WSA("recv"); + + ASSERT_SYSERROR(makeUnsigned(bytesReceived) <= bytesToRead); //better safe than sorry + + return bytesReceived; //"zero indicates end of file" +} + + +size_t tryWriteSocket(SocketType socket, const void* buffer, size_t bytesToWrite) //throw SysError; may return short! CONTRACT: bytesToWrite > 0 +{ + if (bytesToWrite == 0) + throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); + + int bytesWritten = 0; + for (;;) + { + bytesWritten = ::send(socket, //_In_ SOCKET s + static_cast(buffer), //_In_ const char* buf + static_cast(bytesToWrite), //_In_ int len + 0); //_In_ int flags + if (bytesWritten >= 0 || errno != EINTR) + break; + } + if (bytesWritten < 0) + THROW_LAST_SYS_ERROR_WSA("send"); + + if (bytesWritten == 0) + throw SysError(formatSystemError("send", L"", L"Zero bytes processed.")); + + ASSERT_SYSERROR(makeUnsigned(bytesWritten) <= bytesToWrite); //better safe than sorry + + return bytesWritten; +} +} + + +//initiate termination of connection by sending TCP FIN package +inline +void shutdownSocketSend(SocketType socket) //throw SysError +{ + if (::shutdown(socket, SHUT_WR) != 0) + THROW_LAST_SYS_ERROR_WSA("shutdown"); +} + + +inline +void setNonBlocking(SocketType socket, bool nonBlocking) //throw SysError +{ + int flags = ::fcntl(socket, F_GETFL); + if (flags == -1) + THROW_LAST_SYS_ERROR("fcntl(F_GETFL)"); + + if (nonBlocking) + flags |= O_NONBLOCK; + else + flags &= ~O_NONBLOCK; + + if (::fcntl(socket, F_SETFL, flags) != 0) + THROW_LAST_SYS_ERROR(nonBlocking ? "fcntl(F_SETFL, O_NONBLOCK)" : "fcntl(F_SETFL, ~O_NONBLOCK)"); +} +} + +#endif //SOCKET_H_23498325972583947678456437 diff --git a/zen/stl_tools.h b/zen/stl_tools.h new file mode 100644 index 0000000..8f1f5a2 --- /dev/null +++ b/zen/stl_tools.h @@ -0,0 +1,397 @@ +// ***************************************************************************** +// * 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 STL_TOOLS_H_84567184321434 +#define STL_TOOLS_H_84567184321434 + +#include +#include +#include +#include +#include +#include +#include +#include +#include "type_traits.h" + + +//enhancements for +namespace zen +{ +//unfortunately std::erase_if is useless garbage on GCC 12 (requires non-modifying predicate) +template +void eraseIf(std::vector& v, Predicate p); + +template +void eraseIf(std::set& s, Predicate p); + +template +void eraseIf(std::map& m, Predicate p); + +//append STL containers +template +void append(std::vector& v, const C& c); + +template +void append(std::set& s, const C& c); + +template +void append(std::map& m, const C& c); + +template +void removeDuplicates(std::vector& v); + +template +void removeDuplicates(std::vector& v, CompLess less); + +template +void removeDuplicatesStable(std::vector& v, CompLess less); + +template +void removeDuplicatesStable(std::vector& v); + +//searching STL containers +template +BidirectionalIterator findLast(BidirectionalIterator first, BidirectionalIterator last, const T& value); + +template inline +RandomAccessIterator1 searchFirst(const RandomAccessIterator1 first, const RandomAccessIterator1 last, + const RandomAccessIterator2 needleFirst, const RandomAccessIterator2 needleLast); + +template inline +RandomAccessIterator1 searchFirst(const RandomAccessIterator1 first, const RandomAccessIterator1 last, + const RandomAccessIterator2 needleFirst, const RandomAccessIterator2 needleLast, IsEq isEqual); + +//replacement for std::find_end taking advantage of bidirectional iterators (and giving the algorithm a reasonable name) +template +RandomAccessIterator1 searchLast(RandomAccessIterator1 first, RandomAccessIterator1 last, + RandomAccessIterator2 needleFirst, RandomAccessIterator2 needleLast); + +//binary search returning an iterator +template +RandomAccessIterator binarySearch(RandomAccessIterator first, RandomAccessIterator last, const T& value, CompLess less); + +//read-only variant of std::merge; input: two sorted ranges +template +void mergeTraversal(Iterator first1, Iterator last1, + Iterator first2, Iterator last2, + FunctionLeftOnly lo, FunctionBoth bo, FunctionRightOnly ro, Compare compare); + +//why, oh why is there no std::optional::get()??? +template inline T* get( std::optional& opt) { return opt ? &*opt : nullptr; } +template inline const T* get(const std::optional& opt) { return opt ? &*opt : nullptr; } + + +//=========================================================================== +template +class SharedRef //why is there no std::shared_ref??? +{ +public: + SharedRef() = delete; //no surprise memory allocations! + + explicit SharedRef(const std::shared_ptr& ptr) : ptr_ (ptr) { assert(ptr_); } + explicit SharedRef( std::shared_ptr&& ptr) : ptr_(std::move(ptr)) { assert(ptr_); } + + template SharedRef(const SharedRef& other) : ptr_ (other.ptr_) {} + template SharedRef( SharedRef&& other) : ptr_(std::move(other.ptr_)) {} + + /**/ T& ref() { return *ptr_; }; + const T& ref() const { return *ptr_; }; + + const std::shared_ptr< T>& ptr() { return ptr_; }; + /**/ std::shared_ptr ptr() const { return ptr_; }; //careful: return value has different type => creates temporary! + +private: + template friend class SharedRef; + + std::shared_ptr ptr_; //always bound +}; + +template inline +SharedRef makeSharedRef(Args&& ... args) { return SharedRef(std::make_shared(std::forward(args)...)); } + + + +//hide SharedRef as an implementation detail +template //target value type +class DerefIter +{ +public: + using iterator_category = std::bidirectional_iterator_tag; + using value_type = T; + using difference_type = ptrdiff_t; + using pointer = T*; + using reference = T&; + + DerefIter() {} + DerefIter(IterImpl it) : it_(std::move(it)) {} + //DerefIter(const DerefIter& other) : it_(other.it_) {} + DerefIter& operator++() { ++it_; return *this; } + DerefIter& operator--() { --it_; return *this; } + inline friend DerefIter operator++(DerefIter& it, int) { return it++; } + inline friend DerefIter operator--(DerefIter& it, int) { return it--; } + inline friend ptrdiff_t operator-(const DerefIter& lhs, const DerefIter& rhs) { return lhs.it_ - rhs.it_; } + bool operator==(const DerefIter&) const = default; + T& operator* () const { return it_->ref(); } + T* operator->() const { return &it_->ref(); } +private: + IterImpl it_{}; +}; + + +template +class Range +{ +public: + Range(Iterator first, Iterator last) : first_(std::move(first)), last_(std::move(last)) {} + Iterator begin() const { return first_; } + Iterator end () const { return last_; } + + bool empty() const { return first_ == last_; } + size_t size() const { return last_ - first_; } + +private: + Iterator first_; + Iterator last_; +}; + +//######################## implementation ######################## + +template inline +void eraseIf(std::vector& v, Predicate p) +{ + v.erase(std::remove_if(v.begin(), v.end(), p), v.end()); +} + + +namespace impl +{ +template inline +void setOrMapEraseIf(S& s, Predicate p) +{ + for (auto it = s.begin(); it != s.end();) + if (p(*it)) + s.erase(it++); + else + ++it; +} +} + + +template inline +void eraseIf(std::set& s, Predicate p) { impl::setOrMapEraseIf(s, p); } //don't make this any more generic! e.g. must not compile for std::vector!!! + + +template inline +void eraseIf(std::map& m, Predicate p) { impl::setOrMapEraseIf(m, p); } + + +template inline +void eraseIf(std::unordered_set& s, Predicate p) { impl::setOrMapEraseIf(s, p); } + + +template inline +void eraseIf(std::unordered_map& m, Predicate p) { impl::setOrMapEraseIf(m, p); } + + +template inline +void append(std::vector& v, const C& c) { v.insert(v.end(), c.begin(), c.end()); } + + +template inline +void append(std::set& s, const C& c) { s.insert(c.begin(), c.end()); } + + +template inline +void append(std::map& m, const C& c) { m.insert(c.begin(), c.end()); } + + +template inline +void removeDuplicates(std::vector& v, CompLess less, CompEqual eq) +{ + std::sort(v.begin(), v.end(), less); + v.erase(std::unique(v.begin(), v.end(), eq), v.end()); +} + + +template inline +void removeDuplicates(std::vector& v, CompLess less) +{ + removeDuplicates(v, less, [&](const auto& lhs, const auto& rhs) { return !less(lhs, rhs) && !less(rhs, lhs); }); +} + + +template inline +void removeDuplicates(std::vector& v) +{ + removeDuplicates(v, std::less{}, std::equal_to{}); +} + + +template inline +void removeDuplicatesStable(std::vector& v, CompLess less) +{ + std::set usedItems(less); + v.erase(std::remove_if(v.begin(), v.end(), + /**/[&usedItems](const T& e) { return !usedItems.insert(e).second; }), v.end()); +} + + +template inline +void removeDuplicatesStable(std::vector& v) +{ + removeDuplicatesStable(v, std::less{}); +} + + +template inline +RandomAccessIterator binarySearch(RandomAccessIterator first, RandomAccessIterator last, const T& value, CompLess less) +{ + static_assert(std::is_same_v::iterator_category, std::random_access_iterator_tag>); + + first = std::lower_bound(first, last, value, less); //alternative: std::partition_point + if (first != last && !less(value, *first)) + return first; + else + return last; +} + + +template inline +BidirectionalIterator findLast(const BidirectionalIterator first, const BidirectionalIterator last, const T& value) +{ + for (BidirectionalIterator it = last; it != first;) //reverse iteration: 1. check 2. decrement 3. evaluate + { + --it; // + + if (*it == value) + return it; + } + return last; +} + + +template inline +RandomAccessIterator1 searchFirst(const RandomAccessIterator1 first, const RandomAccessIterator1 last, + const RandomAccessIterator2 needleFirst, const RandomAccessIterator2 needleLast, IsEq isEqual) +{ + if (needleLast - needleFirst == 1) //don't use expensive std::search unless required! + return std::find_if(first, last, [needleFirst, isEqual](const auto c) { return isEqual(*needleFirst, c); }); + //"*needleFirst" could be improved with value rather than iterator access, at least for built-in types like "char" + + return std::search(first, last, + needleFirst, needleLast, isEqual); +} + + +template inline +RandomAccessIterator1 searchFirst(const RandomAccessIterator1 first, const RandomAccessIterator1 last, + const RandomAccessIterator2 needleFirst, const RandomAccessIterator2 needleLast) +{ + return searchFirst(first, last, needleFirst, needleLast, std::equal_to{}); +} + + + +template inline +RandomAccessIterator1 searchLast(const RandomAccessIterator1 first, RandomAccessIterator1 last, + const RandomAccessIterator2 needleFirst, const RandomAccessIterator2 needleLast) +{ + if (needleLast - needleFirst == 1) //fast-path + return findLast(first, last, *needleFirst); + + const RandomAccessIterator1 itNotFound = last; + + //reverse iteration: 1. check 2. decrement 3. evaluate + for (;;) + { + RandomAccessIterator1 it1 = last; + RandomAccessIterator2 it2 = needleLast; + + for (;;) + { + if (it2 == needleFirst) return it1; + if (it1 == first) return itNotFound; + + --it1; + --it2; + + if (*it1 != *it2) break; + } + --last; + } +} + +//--------------------------------------------------------------------------------------- + +//read-only variant of std::merge; input: two sorted ranges +template inline +void mergeTraversal(Iterator firstL, Iterator lastL, + Iterator firstR, Iterator lastR, + FunctionLeftOnly lo, FunctionBoth bo, FunctionRightOnly ro, Compare compare) +{ + auto itL = firstL; + auto itR = firstR; + + auto finishLeft = [&] { std::for_each(itL, lastL, lo); }; + auto finishRight = [&] { std::for_each(itR, lastR, ro); }; + + if (itL == lastL) return finishRight(); + if (itR == lastR) return finishLeft (); + + for (;;) + if (const std::weak_ordering cmp = compare(*itL, *itR); + cmp < 0) + { + lo(*itL); + if (++itL == lastL) + return finishRight(); + } + else if (cmp > 0) + { + ro(*itR); + if (++itR == lastR) + return finishLeft(); + } + else + { + bo(*itL, *itR); + ++itL; // + ++itR; //increment BOTH before checking for end of range! + if (itL == lastL) return finishRight(); + if (itR == lastR) return finishLeft (); + //simplify loop by placing both EOB checks at the beginning? => slightly slower + } +} + + +template +class FNV1aHash //FNV-1a: https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function +{ +public: + FNV1aHash() {} + explicit FNV1aHash(Num startVal) : hashVal_(startVal) { assert(startVal != 0); /*yes, might be a real hash, but most likely bad init value*/} + + void add(Num n) + { + hashVal_ ^= n; + hashVal_ *= prime_; + } + + Num get() const { return hashVal_; } + +private: + static_assert(isUnsignedInt); + static_assert(sizeof(Num) == 4 || sizeof(Num) == 8); + static constexpr Num base_ = sizeof(Num) == 4 ? 2166136261U : 14695981039346656037ULL; + static constexpr Num prime_ = sizeof(Num) == 4 ? 16777619U : 1099511628211ULL; + + Num hashVal_ = base_; +}; +} + +#endif //STL_TOOLS_H_84567184321434 diff --git a/zen/stream_buffer.h b/zen/stream_buffer.h new file mode 100644 index 0000000..752c9b7 --- /dev/null +++ b/zen/stream_buffer.h @@ -0,0 +1,207 @@ +// ***************************************************************************** +// * 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 STREAM_BUFFER_H_08492572089560298 +#define STREAM_BUFFER_H_08492572089560298 + +#include +#include "ring_buffer.h" +#include "string_tools.h" + + +namespace zen +{ +/* implement streaming API on top of libcurl's icky callback-based design + + curl uses READBUFFER_SIZE download buffer size, but returns via a retarded sendf.c::chop_write() writing in small junks of CURL_MAX_WRITE_SIZE (16 kB) + => support copying arbitrarily-large files: https://freefilesync.org/forum/viewtopic.php?t=4471 + => maximum performance through async processing (prefetching + output buffer!) + => cost per worker thread creation ~ 1/20 ms */ +class AsyncStreamBuffer +{ +public: + explicit AsyncStreamBuffer(size_t capacity) { ringBuf_.reserve(capacity); } + + //context of input thread, blocking + size_t read(void* buffer, size_t bytesToRead) //throw ; return "bytesToRead" bytes unless end of stream! + { + std::unique_lock dummy(lockStream_); + const auto bufStart = buffer; + + while (bytesToRead > 0) + { + const size_t bytesRead = tryReadImpl(dummy, buffer, bytesToRead); //throw + if (bytesRead == 0) //end of file + break; + conditionBytesRead_.notify_all(); + buffer = static_cast(buffer) + bytesRead; + bytesToRead -= bytesRead; + } + return static_cast(buffer) - + static_cast(bufStart); + } + + //context of input thread, blocking + size_t tryRead(void* buffer, size_t bytesToRead) //throw ; may return short; only 0 means EOF! CONTRACT: bytesToRead > 0! + { + size_t bytesRead = 0; + { + std::unique_lock dummy(lockStream_); + bytesRead = tryReadImpl(dummy, buffer, bytesToRead); + } + if (bytesRead > 0) + conditionBytesRead_.notify_all(); //...*outside* the lock + return bytesRead; + } + + //context of output thread, blocking + void write(const void* buffer, size_t bytesToWrite) //throw + { + std::unique_lock dummy(lockStream_); + while (bytesToWrite > 0) + { + const size_t bytesWritten = tryWriteWhileImpl(dummy, buffer, bytesToWrite); //throw + conditionBytesWritten_.notify_all(); + buffer = static_cast(buffer) + bytesWritten; + bytesToWrite -= bytesWritten; + } + } + + //context of output thread, blocking + size_t tryWrite(const void* buffer, size_t bytesToWrite) //throw ; may return short! CONTRACT: bytesToWrite > 0 + { + size_t bytesWritten = 0; + { + std::unique_lock dummy(lockStream_); + bytesWritten = tryWriteWhileImpl(dummy, buffer, bytesToWrite); + } + conditionBytesWritten_.notify_all(); //...*outside* the lock + return bytesWritten; + } + + //context of output thread + void closeStream() + { + { + std::lock_guard dummy(lockStream_); + assert(!eof_ && !errorWrite_); + eof_ = true; + } + conditionBytesWritten_.notify_all(); + } + + //context of input thread + void setReadError(const std::exception_ptr& error) + { + { + std::lock_guard dummy(lockStream_); + assert(error && !errorRead_); + if (!errorRead_) + errorRead_ = error; + } + conditionBytesRead_.notify_all(); + } + + //context of output thread + void setWriteError(const std::exception_ptr& error) + { + { + std::lock_guard dummy(lockStream_); + assert(error && !errorWrite_); + if (!errorWrite_) + errorWrite_ = error; + } + conditionBytesWritten_.notify_all(); + } + +#if 0 + //function not needed: after file upload completed successfully, no further error can occur! + // => caveat: writing is NOT done (yet) when closeStream() is called! + //context of *output* thread + void checkReadErrors() //throw + { + std::lock_guard dummy(lockStream_); + if (errorRead_) + std::rethrow_exception(errorRead_); //throw + } + + //function not needed: when EOF is reached (without errors), reading is done => no further error can occur! + //context of *input* thread + void checkWriteErrors() //throw + { + std::lock_guard dummy(lockStream_); + if (errorWrite_) + std::rethrow_exception(errorWrite_); //throw + } +#endif + + uint64_t getTotalBytesWritten() const { return totalBytesWritten_; } + uint64_t getTotalBytesRead () const { return totalBytesRead_; } + +private: + AsyncStreamBuffer (const AsyncStreamBuffer&) = delete; + AsyncStreamBuffer& operator=(const AsyncStreamBuffer&) = delete; + + //context of input thread, blocking + size_t tryReadImpl(std::unique_lock& ul, void* buffer, size_t bytesToRead) //throw ; may return short; only 0 means EOF! CONTRACT: bytesToRead > 0! + { + if (bytesToRead == 0) //"read() with a count of 0 returns zero" => indistinguishable from end of file! => check! + throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); + + assert(isLocked(lockStream_)); + assert(!errorRead_); + + conditionBytesWritten_.wait(ul, [this] { return errorWrite_ || !ringBuf_.empty() || eof_; }); + + if (errorWrite_) + std::rethrow_exception(errorWrite_); //throw + + const size_t junkSize = std::min(bytesToRead, ringBuf_.size()); + ringBuf_.extract_front(static_cast(buffer), + static_cast(buffer)+ junkSize); + totalBytesRead_ += junkSize; + return junkSize; + } + + //context of output thread, blocking + size_t tryWriteWhileImpl(std::unique_lock& ul, const void* buffer, size_t bytesToWrite) //throw ; may return short! CONTRACT: bytesToWrite > 0 + { + if (bytesToWrite == 0) + throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); + + assert(isLocked(lockStream_)); + assert(!eof_ && !errorWrite_); + /* => can't use InterruptibleThread's interruptibleWait() :( + -> AsyncStreamBuffer is used for input and output streaming + => both AsyncStreamBuffer::write()/read() would have to implement interruptibleWait() + => one of these usually called from main thread + => but interruptibleWait() cannot be called from main thread! */ + conditionBytesRead_.wait(ul, [this] { return errorRead_ || ringBuf_.size() < ringBuf_.capacity(); }); + + if (errorRead_) + std::rethrow_exception(errorRead_); //throw + + const size_t junkSize = std::min(bytesToWrite, ringBuf_.capacity() - ringBuf_.size()); + + ringBuf_.insert_back(static_cast(buffer), + static_cast(buffer) + junkSize); + totalBytesWritten_ += junkSize; + return junkSize; + } + + std::mutex lockStream_; + RingBuffer ringBuf_; //prefetch/output buffer + bool eof_ = false; + std::exception_ptr errorWrite_; + std::exception_ptr errorRead_; + std::condition_variable conditionBytesWritten_; + std::condition_variable conditionBytesRead_; + + std::atomic totalBytesWritten_{0}; //std:atomic is uninitialized by default! + std::atomic totalBytesRead_ {0}; // +}; +} + +#endif //STREAM_BUFFER_H_08492572089560298 diff --git a/zen/string_base.h b/zen/string_base.h new file mode 100644 index 0000000..b19b485 --- /dev/null +++ b/zen/string_base.h @@ -0,0 +1,682 @@ +// ***************************************************************************** +// * 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 STRING_BASE_H_083217454562342526 +#define STRING_BASE_H_083217454562342526 + +#include +#include //std::exchange +#include "string_tools.h" + + +//Zbase - a policy based string class optimizing performance and flexibility +namespace zen +{ +/* Allocator Policy: + ----------------- + void* allocate(size_t size) //throw std::bad_alloc + void deallocate(void* ptr) + size_t calcCapacity(size_t length) */ +class AllocatorOptimalSpeed //exponential growth + min size +{ +protected: + //::operator new/delete show same performance characterisics like malloc()/free()! + static void* allocate(size_t size) { return ::operator new (size); } //throw std::bad_alloc + static void deallocate(void* ptr) { ::operator delete (ptr); } + static size_t calcCapacity(size_t length) { return std::max(16, std::max(length + length / 2, length)); } + //- size_t might overflow! => better catch here than return a too small size covering up the real error: a way too large length! + //- any growth rate should not exceed golden ratio: 1.618033989 +}; + + +class AllocatorOptimalMemory //no wasted memory, but more reallocations required when manipulating string +{ +protected: + static void* allocate(size_t size) { return ::operator new (size); } //throw std::bad_alloc + static void deallocate(void* ptr) { ::operator delete (ptr); } + static size_t calcCapacity(size_t length) { return length; } +}; + +/* Storage Policy: + --------------- + template //Allocator Policy + + Char* create(size_t size) + Char* create(size_t size, size_t minCapacity) + Char* clone(Char* ptr) + void destroy(Char* ptr) //must handle "destroy(nullptr)"! + bool canWrite(const Char* ptr, size_t minCapacity) //needs to be checked before writing to "ptr" + size_t length(const Char* ptr) + void setLength(Char* ptr, size_t newLength) */ + +template //Allocator Policy +class StorageDeepCopy : public AP +{ +protected: + ~StorageDeepCopy() {} + + Char* create(size_t size) { return create(size, size); } + Char* create(size_t size, size_t minCapacity) + { + assert(size <= minCapacity); + const size_t newCapacity = AP::calcCapacity(minCapacity); + assert(newCapacity >= minCapacity); + + Descriptor* const newDescr = static_cast(this->allocate(sizeof(Descriptor) + (newCapacity + 1) * sizeof(Char))); //throw std::bad_alloc + new (newDescr) Descriptor(size, newCapacity); + + return reinterpret_cast(newDescr + 1); //alignment note: "newDescr + 1" is Descriptor-aligned, which is larger than alignment for Char-array! => no problem! + } + + Char* clone(Char* ptr) + { + const size_t len = length(ptr); + Char* newData = create(len); //throw std::bad_alloc + std::copy(ptr, ptr + len + 1, newData); + return newData; + } + + void destroy(Char* ptr) + { + if (!ptr) return; //support "destroy(nullptr)" + + Descriptor* const d = descr(ptr); + d->~Descriptor(); + this->deallocate(d); + } + + //this needs to be checked before writing to "ptr" + static bool canWrite(const Char* ptr, size_t minCapacity) { return minCapacity <= descr(ptr)->capacity; } + static size_t size(const Char* ptr) { return descr(ptr)->length; } + + static void setLength(Char* ptr, size_t newLength) + { + assert(canWrite(ptr, newLength)); + descr(ptr)->length = newLength; + } + +private: + struct Descriptor + { + Descriptor(size_t len, size_t cap) : + length (static_cast(len)), + capacity(static_cast(cap)) {} + + uint32_t length; + const uint32_t capacity; //allocated size without null-termination + }; + + static Descriptor* descr( Char* ptr) { return reinterpret_cast< Descriptor*>(ptr) - 1; } + static const Descriptor* descr(const Char* ptr) { return reinterpret_cast(ptr) - 1; } +}; + + +template //Allocator Policy +class StorageRefCountThreadSafe : public AP +{ +protected: + ~StorageRefCountThreadSafe() {} + + Char* create(size_t size) { return create(size, size); } + Char* create(size_t size, size_t minCapacity) + { + assert(size <= minCapacity); + + if (minCapacity == 0) //perf: avoid memory allocation for empty string + { + ++globalEmptyString.descr.refCount; + return &globalEmptyString.nullTerm; + } + + const size_t newCapacity = AP::calcCapacity(minCapacity); + assert(newCapacity >= minCapacity); + + Descriptor* const newDescr = static_cast(this->allocate(sizeof(Descriptor) + (newCapacity + 1) * sizeof(Char))); //throw std::bad_alloc + new (newDescr) Descriptor(size, newCapacity); + + return reinterpret_cast(newDescr + 1); + } + + static Char* clone(Char* ptr) + { + ++descr(ptr)->refCount; + return ptr; + } + + void destroy(Char* ptr) + { + assert(ptr != reinterpret_cast(0x1)); //detect double-deletion + + if (!ptr) //support "destroy(nullptr)" + { + return; + } + + Descriptor* const d = descr(ptr); + + if (--(d->refCount) == 0) //operator--() is overloaded to decrement and evaluate in a single atomic operation! + { + d->~Descriptor(); + this->deallocate(d); + } + } + + static bool canWrite(const Char* ptr, size_t minCapacity) //needs to be checked before writing to "ptr" + { + const Descriptor* const d = descr(ptr); + assert(d->refCount > 0); + return d->refCount == 1 && minCapacity <= d->capacity; + } + + static size_t size(const Char* ptr) { return descr(ptr)->length; } + + static void setLength(Char* ptr, size_t newLength) + { + assert(canWrite(ptr, newLength)); + descr(ptr)->length = static_cast(newLength); + } + +private: + struct Descriptor + { + constexpr Descriptor(size_t len, size_t cap) : + length (static_cast(len)), + capacity(static_cast(cap)) + { + static_assert(decltype(refCount)::is_always_lock_free); + } + + std::atomic refCount{1}; //std:atomic is uninitialized by default! + uint32_t length; + const uint32_t capacity; //allocated size without null-termination + }; + + static Descriptor* descr( Char* ptr) { return reinterpret_cast< Descriptor*>(ptr) - 1; } + static const Descriptor* descr(const Char* ptr) { return reinterpret_cast(ptr) - 1; } + + struct GlobalEmptyString + { + Descriptor descr{0 /*length*/, 0 /*capacity*/}; + Char nullTerm = 0; + }; + static_assert(offsetof(GlobalEmptyString, nullTerm) - offsetof(GlobalEmptyString, descr) == sizeof(Descriptor), "no gap!"); + static_assert(std::is_trivially_destructible_v, "this memory needs to live forever"); + + inline static constinit GlobalEmptyString globalEmptyString; //constinit: dodge static initialization order fiasco! +}; + + +template +using DefaultStoragePolicy = StorageRefCountThreadSafe; + + +//################################################################################################################################################################ + +//perf note: interestingly StorageDeepCopy and StorageRefCountThreadSafe show same performance in FFS comparison + +template class SP = DefaultStoragePolicy> //Storage Policy +class Zbase : public SP +{ +public: + Zbase(); + Zbase(const Char* str) : Zbase(str, str + strLength(str)) {} //implicit conversion from a C-string! + Zbase(const Char* str, size_t len) : Zbase(str, str + len) {} + explicit Zbase(const std::basic_string_view view) : Zbase(view.begin(), view.end()) {} + Zbase(size_t count, Char fillChar); + template + Zbase(RandomAccessIterator first, RandomAccessIterator last); + Zbase(const Zbase& str); + Zbase(Zbase&& tmp) noexcept; + //explicit Zbase(Char ch); //dangerous if implicit: Char buffer[]; return buffer[0]; ups... forgot &, but not a compiler error! //-> non-standard extension!!! + + ~Zbase(); + + //operator const Char* () const; //NO implicit conversion to a C-string!! Many problems... one of them: if we forget to provide operator overloads, it'll just work with a Char*... + + operator std::basic_string_view() const& noexcept { return {data(), size()}; } + //operator std::basic_string_view() const&& = delete; //=> probably a bug! + + //STL accessors + using iterator = Char*; + using const_iterator = const Char*; + using reference = Char&; + using const_reference = const Char&; + using value_type = Char; + + iterator begin(); + iterator end (); + + const_iterator begin () const { return rawStr_; } + const_iterator end () const { return rawStr_ + size(); } + + const_iterator cbegin() const { return begin(); } + const_iterator cend () const { return end (); } + + //std::string functions + size_t length() const { return size(); } + size_t size () const; + const Char* c_str() const { return rawStr_; } //C-string format with 0-termination + const Char* data() const { return &*begin(); } + /**/ Char* data() { return &*begin(); } + const Char& operator[](size_t pos) const; + /**/ Char& operator[](size_t pos); + bool empty() const { return size() == 0; } + void clear(); +#if 0 //avoid redundant std::string API bloat! + size_t find (const Zbase& str, size_t pos = 0) const; // + size_t find (const Char* str, size_t pos = 0) const; // + size_t find (Char ch, size_t pos = 0) const; //returns "npos" if not found + size_t rfind(Char ch, size_t pos = npos) const; // + size_t rfind(const Char* str, size_t pos = npos) const; // +#endif + //Zbase& replace(size_t pos1, size_t n1, const Zbase& str); + void reserve(size_t minCapacity); + Zbase& assign(const Char* str, size_t len) { return assign(str, str + len); } + Zbase& append(const Char* str, size_t len) { return append(str, str + len); } + + template Zbase& assign(RandomAccessIterator first, RandomAccessIterator last); + template Zbase& append(RandomAccessIterator first, RandomAccessIterator last); + + void resize(size_t newSize, Char fillChar = 0); + void swap(Zbase& str) { std::swap(rawStr_, str.rawStr_); } + void push_back(Char val) { operator+=(val); } //STL access + void pop_back(); + + Zbase& operator=(Zbase&& tmp) noexcept; + Zbase& operator=(const Zbase& str); + Zbase& operator=(const Char* str) { return assign(str, strLength(str)); } + Zbase& operator=(Char ch) { return assign(&ch, 1); } + Zbase& operator+=(const Zbase& str) { return append(str.c_str(), str.size()); } + Zbase& operator+=(const Char* str) { return append(str, strLength(str)); } + Zbase& operator+=(Char ch) { return append(&ch, 1); } + Zbase& operator+=(const std::basic_string_view str) { return append(str.begin(), str.end()); } + + static const size_t npos = static_cast(-1); + + inline friend Zbase operator+( const Char* lhs, const Zbase& rhs) { return Zbase(lhs, strLength(lhs), rhs.c_str(), rhs.size()); } + inline friend Zbase operator+( Char lhs, const Zbase& rhs) { return Zbase(&lhs, 1, rhs.c_str(), rhs.size()); } + inline friend Zbase operator+(const std::basic_string_view lhs, const Zbase& rhs) { return Zbase(lhs.data(), lhs.size(), rhs.c_str(), rhs.size()); } + +private: + Zbase (int) = delete; // + Zbase(size_t count, int) = delete; // + Zbase& operator= (int) = delete; //detect usage errors by creating an intentional ambiguity with "Char" + Zbase& operator+= (int) = delete; // + void push_back (int) = delete; // + + Zbase (std::nullptr_t) = delete; + Zbase(size_t count, std::nullptr_t) = delete; + Zbase& operator= (std::nullptr_t) = delete; + Zbase& operator+= (std::nullptr_t) = delete; + void push_back (std::nullptr_t) = delete; + + //not part of std::string API => private: + Zbase(const Char* str1, size_t len1, const Char* str2, size_t len2); + //alternative: Zbase() + reserve() + 2 x append() + + Char* rawStr_; +}; + + + +template class SP> bool operator==(const Zbase& lhs, const Zbase& rhs); +template class SP> bool operator==(const Zbase& lhs, const Char* rhs); +template class SP> inline bool operator==(const Char* lhs, const Zbase& rhs) { return operator==(rhs, lhs); } + +//follow convention + compare by unsigned char; alternative: std::lexicographical_compare_three_way + reinterpret_cast*>() +template class SP> std::strong_ordering operator<=>(const Zbase& lhs, const Zbase& rhs) { return compareString(lhs, rhs); } +template class SP> std::strong_ordering operator<=>(const Zbase& lhs, const Char* rhs) { return compareString(lhs, rhs); } +template class SP> std::strong_ordering operator<=>(const Char* lhs, const Zbase& rhs) { return compareString(lhs, rhs); } + +template class SP> inline Zbase operator+(const Zbase& lhs, const Zbase& rhs) { return Zbase(lhs) += rhs; } +template class SP> inline Zbase operator+(const Zbase& lhs, const Char* rhs) { return Zbase(lhs) += rhs; } +template class SP> inline Zbase operator+(const Zbase& lhs, Char rhs) { return Zbase(lhs) += rhs; } +template class SP> inline Zbase operator+(const Zbase& lhs, const std::basic_string_view rhs) { return Zbase(lhs) += rhs; } + +//don't use unified first argument but save one move-construction in the r-value case instead! +template class SP> inline Zbase operator+(Zbase&& lhs, const Zbase& rhs) { return std::move(lhs += rhs); } //the move *is* needed!!! +template class SP> inline Zbase operator+(Zbase&& lhs, const Char* rhs) { return std::move(lhs += rhs); } //lhs, is an l-value parameter... +template class SP> inline Zbase operator+(Zbase&& lhs, Char rhs) { return std::move(lhs += rhs); } //and not a local variable => no copy elision +template class SP> inline Zbase operator+(Zbase&& lhs, const std::basic_string_view rhs) { return std::move(lhs += rhs); } + +template class SP> inline Zbase operator+(const Zbase&, int) = delete; //detect usage errors +template class SP> inline Zbase operator+(int, const Zbase&) = delete; // + + + + + + + + + + +//################################# implementation ######################################## +template class SP> inline +Zbase::Zbase() +{ + rawStr_ = this->create(0); + rawStr_[0] = 0; +} + + +template class SP> +template inline +Zbase::Zbase(RandomAccessIterator first, RandomAccessIterator last) +{ + rawStr_ = this->create(last - first); + *std::copy(first, last, rawStr_) = 0; +} + + +template class SP> inline +Zbase::Zbase(size_t count, Char fillChar) +{ + rawStr_ = this->create(count); + std::fill(rawStr_, rawStr_ + count, fillChar); + rawStr_[count] = 0; +} + + +template class SP> inline +Zbase::Zbase(const Zbase& str) +{ + rawStr_ = this->clone(str.rawStr_); +} + + +template class SP> inline +Zbase::Zbase(Zbase&& tmp) noexcept +{ + rawStr_ = std::exchange(tmp.rawStr_, nullptr); + //usually nullptr would violate the class invarants, but it is good enough for the destructor! + //caveat: do not increment ref-count of an unshared string! We'd lose optimization opportunity of reusing its memory! +} + + +template class SP> inline +Zbase::Zbase(const Char* str1, size_t len1, const Char* str2, size_t len2) +{ + rawStr_ = this->create(len1 + len2); + std::copy (str1, str1 + len1, rawStr_); + *std::copy(str2, str2 + len2, rawStr_ + len1) = 0; +} + + +template class SP> inline +Zbase::~Zbase() +{ + static_assert(noexcept(this->~Zbase())); //has exception spec of compiler-generated destructor by default + + this->destroy(rawStr_); //rawStr_ may be nullptr; see move constructor! +} + + +#if 0 //avoid redundant std::string API bloat! +template class SP> inline +size_t Zbase::find(const Zbase& str, size_t pos) const //returns "npos" if not found +{ + assert(pos <= size()); + const size_t len = size(); + const Char* thisEnd = begin() + len; //respect embedded 0 + const Char* it = searchFirst(begin() + std::min(pos, len), thisEnd, + str.begin(), str.end()); + return it == thisEnd ? npos : it - begin(); +} + + +template class SP> inline +size_t Zbase::find(const Char* str, size_t pos) const //returns "npos" if not found +{ + assert(pos <= size()); + const size_t len = size(); + const Char* thisEnd = begin() + len; //respect embedded 0 + const Char* it = searchFirst(begin() + std::min(pos, len), thisEnd, + str, str + strLength(str)); + return it == thisEnd ? npos : it - begin(); +} + + +template class SP> inline +size_t Zbase::find(Char ch, size_t pos) const //returns "npos" if not found +{ + assert(pos <= size()); + const size_t len = size(); + const Char* thisEnd = begin() + len; //respect embedded 0 + const Char* it = std::find(begin() + std::min(pos, len), thisEnd, ch); + return it == thisEnd ? npos : it - begin(); +} + + +template class SP> inline +size_t Zbase::rfind(Char ch, size_t pos) const //returns "npos" if not found +{ + assert(pos == npos || pos <= size()); + const size_t len = size(); + const Char* currEnd = begin() + (pos == npos ? len : std::min(pos + 1, len)); + const Char* it = findLast(begin(), currEnd, ch); + return it == currEnd ? npos : it - begin(); +} + + +template class SP> inline +size_t Zbase::rfind(const Char* str, size_t pos) const //returns "npos" if not found +{ + assert(pos == npos || pos <= size()); + const size_t strLen = strLength(str); + const size_t len = size(); + const Char* currEnd = begin() + (pos == npos ? len : std::min(pos + strLen, len)); + const Char* it = searchLast(begin(), currEnd, + str, str + strLen); + return it == currEnd ? npos : it - begin(); +} +#endif + + +template class SP> inline +void Zbase::resize(size_t newSize, Char fillChar) +{ + const size_t oldSize = size(); + if (this->canWrite(rawStr_, newSize)) + { + if (oldSize < newSize) + std::fill(rawStr_ + oldSize, rawStr_ + newSize, fillChar); + rawStr_[newSize] = 0; + this->setLength(rawStr_, newSize); + } + else + { + Char* newStr = this->create(newSize); + if (oldSize < newSize) + { + std::copy(rawStr_, rawStr_ + oldSize, newStr); + std::fill(newStr + oldSize, newStr + newSize, fillChar); + } + else + std::copy(rawStr_, rawStr_ + newSize, newStr); + newStr[newSize] = 0; + + this->destroy(rawStr_); + rawStr_ = newStr; + } +} + + +template class SP> inline +bool operator==(const Zbase& lhs, const Zbase& rhs) +{ + return lhs.size() == rhs.size() && std::equal(lhs.begin(), lhs.end(), rhs.begin()); //respect embedded 0 +} + + +template class SP> inline +bool operator==(const Zbase& lhs, const Char* rhs) +{ + return lhs.size() == strLength(rhs) && std::equal(lhs.begin(), lhs.end(), rhs); //respect embedded 0 +} + + +template class SP> inline +size_t Zbase::size() const +{ + return SP::size(rawStr_); +} + + +template class SP> inline +const Char& Zbase::operator[](size_t pos) const +{ + assert(pos < size()); //design by contract! no runtime check! + return rawStr_[pos]; +} + + +template class SP> inline +Char& Zbase::operator[](size_t pos) +{ + reserve(size()); //make unshared! + assert(pos < size()); //design by contract! no runtime check! + return rawStr_[pos]; +} + + +template class SP> inline +auto Zbase::begin() -> iterator +{ + reserve(size()); //make unshared! + return rawStr_; +} + + +template class SP> inline +auto Zbase::end() -> iterator +{ + return begin() + size(); +} + + +template class SP> inline +void Zbase::clear() +{ + if (!empty()) + { + if (this->canWrite(rawStr_, 0)) + { + rawStr_[0] = 0; //keep allocated memory + this->setLength(rawStr_, 0); // + } + else + *this = Zbase(); + } +} + + +template class SP> inline +void Zbase::reserve(size_t minCapacity) //make unshared and check capacity +{ + if (!this->canWrite(rawStr_, minCapacity)) + { + //allocate a new string + const size_t len = size(); + Char* newStr = this->create(len, std::max(len, minCapacity)); //reserve() must NEVER shrink the string: logical const! + *std::copy(rawStr_, rawStr_ + len, newStr) = 0; + + this->destroy(rawStr_); + rawStr_ = newStr; + } +} + + +template class SP> +template inline +Zbase& Zbase::assign(RandomAccessIterator first, RandomAccessIterator last) +{ + const size_t len = last - first; + if (this->canWrite(rawStr_, len)) + { + *std::copy(first, last, rawStr_) = 0; + this->setLength(rawStr_, len); + } + else + *this = Zbase(first, last); + + return *this; +} + + +template class SP> +template inline +Zbase& Zbase::append(RandomAccessIterator first, RandomAccessIterator last) +{ + const size_t len = last - first; //std::distance(first, last); + if (len > 0) //avoid making this string unshared for no reason + { + const size_t thisLen = size(); + reserve(thisLen + len); //make unshared and check capacity + + *std::copy(first, last, rawStr_ + thisLen) = 0; + this->setLength(rawStr_, thisLen + len); + } + return *this; +} + + +//don't use unifying assignment but save one move-construction in the r-value case instead! +template class SP> inline +Zbase& Zbase::operator=(const Zbase& str) +{ + Zbase(str).swap(*this); + return *this; +} + + +template class SP> inline +Zbase& Zbase::operator=(Zbase&& tmp) noexcept +{ + //don't swap() but end rawStr_ life time immediately + this->destroy(rawStr_); + + rawStr_ = std::exchange(tmp.rawStr_, nullptr); + return *this; +} + + +template class SP> inline +void Zbase::pop_back() +{ + const size_t len = size(); + assert(len > 0); + if (len > 0) + resize(len - 1); +} +} + + +//std::hash specialization in global namespace +template class SP> +struct std::hash> +{ + using is_transparent = int; //allow heterogenous lookup! + + template + size_t operator()(const String& str) const { return zen::hashString(str); } +}; + + +template class SP> +struct std::equal_to> +{ + using is_transparent = int; //enable heterogenous lookup! + + template + bool operator()(const String1& lhs, const String2& rhs) const { return zen::equalString(lhs, rhs); } +}; + +#endif //STRING_BASE_H_083217454562342526 diff --git a/zen/string_tools.h b/zen/string_tools.h new file mode 100644 index 0000000..f937170 --- /dev/null +++ b/zen/string_tools.h @@ -0,0 +1,1019 @@ +// ***************************************************************************** +// * 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 STRING_TOOLS_H_213458973046 +#define STRING_TOOLS_H_213458973046 + +#include //sprintf +#include //swprintf +#include "stl_tools.h" +#include "string_traits.h" +#include "legacy_compiler.h" // but without the compiler crashes :> + + +//enhance *any* string class with useful non-member functions: +namespace zen +{ +template bool isWhiteSpace(Char c); +template bool isLineBreak (Char c); +template bool isDigit (Char c); //not exactly the same as "std::isdigit" -> we consider '0'-'9' only! +template bool isHexDigit (Char c); +template bool isAsciiChar (Char c); +template bool isAsciiAlpha(Char c); +template bool isAsciiString(const S& str); +template Char asciiToLower(Char c); +template Char asciiToUpper(Char c); + +//both S and T can be strings or char/wchar_t arrays or single char/wchar_t +template /*Astyle hates tripe >*/ >> bool contains(const S& str, const T& term); + +template bool startsWith (const S& str, const T& prefix); +template bool startsWithAsciiNoCase(const S& str, const T& prefix); + +template bool endsWith (const S& str, const T& postfix); +template bool endsWithAsciiNoCase(const S& str, const T& postfix); + +template bool equalString (const S& lhs, const T& rhs); +template bool equalAsciiNoCase(const S& lhs, const T& rhs); + +template std::strong_ordering compareString(const S& lhs, const T& rhs); +template std::weak_ordering compareAsciiNoCase(const S& lhs, const T& rhs); //basic case-insensitive comparison (considering A-Z only!) + +//STL container predicates for std::map, std::unordered_set/map +struct StringHash; +struct StringEqual; + +struct LessAsciiNoCase; +struct StringHashAsciiNoCase; +struct StringEqualAsciiNoCase; + +template Num hashString(const S& str); + +enum class IfNotFoundReturn +{ + all, + none +}; +template S afterLast (const S& str, const T& term, IfNotFoundReturn infr); +template S beforeLast (const S& str, const T& term, IfNotFoundReturn infr); +template S afterFirst (const S& str, const T& term, IfNotFoundReturn infr); +template S beforeFirst(const S& str, const T& term, IfNotFoundReturn infr); + +enum class SplitOnEmpty +{ + allow, + skip +}; +template void split(const S& str, Char delimiter, Function onStringPart); +template void split2(const S& str, Function1 isDelimiter, Function2 onStringPart); +template [[nodiscard]] std::vector splitCpy(const S& str, Char delimiter, SplitOnEmpty soe); + +enum class TrimSide +{ + both, + left, + right, +}; +template [[nodiscard]] S trimCpy(const S& str, TrimSide side = TrimSide::both); +template void trim(S& str, TrimSide side = TrimSide::both); +template void trim(S& str, TrimSide side, Function trimThisChar); + + +template [[nodiscard]] S replaceCpy(S str, const T& oldTerm, const U& newTerm); +template void replace (S& str, const T& oldTerm, const U& newTerm); + +template [[nodiscard]] S replaceCpyAsciiNoCase(S str, const T& oldTerm, const U& newTerm); +template void replaceAsciiNoCase (S& str, const T& oldTerm, const U& newTerm); + +//high-performance conversion between numbers and strings +template S numberTo(const Num& number); +template Num stringTo(const S& str); + +std::pair hexify (unsigned char c, bool upperCase = true); +char unhexify(char high, char low); +std::string formatAsHexString(const std::string_view& blob); //bytes -> (human-readable) hex string + +template S printNumber(const T& format, const Num& number); //format a single number using std::snprintf() + +//string to string conversion: converts string-like type into char-compatible target string class +template T copyStringTo(S&& str); + + + + + + + + + + + + + + + +//---------------------- implementation ---------------------- +template inline +bool isWhiteSpace(Char c) +{ + static_assert(std::is_same_v || std::is_same_v); + assert(c != 0); //std C++ does not consider 0 as white space + return c == static_cast(' ') || (static_cast('\t') <= c && c <= static_cast('\r')); + //following std::isspace() for default locale but without the interface insanity: + // - std::isspace() takes an int, but expects an unsigned char + // - some parts of UTF-8 chars are erroneously seen as whitespace, e.g. the a0 from "\xec\x8b\xa0" (MSVC) +} + +template inline +bool isLineBreak(Char c) +{ + static_assert(std::is_same_v || std::is_same_v); + return c == static_cast('\r') || c == static_cast('\n'); +} + + +template inline +bool isDigit(Char c) //similar to implementation of std::isdigit()! +{ + static_assert(std::is_same_v || std::is_same_v); + return static_cast('0') <= c && c <= static_cast('9'); +} + + +template inline +bool isHexDigit(Char c) +{ + static_assert(std::is_same_v || std::is_same_v); + return (static_cast('0') <= c && c <= static_cast('9')) || + (static_cast('A') <= c && c <= static_cast('F')) || + (static_cast('a') <= c && c <= static_cast('f')); +} + + +template inline +bool isAsciiChar(Char c) +{ + return makeUnsigned(c) < 128; +} + + +template inline +bool isAsciiAlpha(Char c) +{ + static_assert(std::is_same_v || std::is_same_v); + return (static_cast('A') <= c && c <= static_cast('Z')) || + (static_cast('a') <= c && c <= static_cast('z')); +} + + +template inline +bool isAsciiString(const S& str) +{ + const auto* const first = strBegin(str); + return std::all_of(first, first + strLength(str), [](auto c) { return isAsciiChar(c); }); +} + + +template inline +Char asciiToLower(Char c) +{ + if (static_cast('A') <= c && c <= static_cast('Z')) + return static_cast(c - static_cast('A') + static_cast('a')); + return c; +} + + +template inline +Char asciiToUpper(Char c) +{ + if (static_cast('a') <= c && c <= static_cast('z')) + return static_cast(c - static_cast('a') + static_cast('A')); + return c; +} + + +namespace impl +{ +template inline +bool equalSubstring(const Char* lhs, const Char* rhs, size_t len) +{ + //support embedded 0, unlike strncmp/wcsncmp: + return std::equal(lhs, lhs + len, rhs); +} + + +template inline +std::weak_ordering strcmpAsciiNoCase(const Char1* lhs, const Char2* rhs, size_t len) +{ + while (len-- > 0) + { + const Char1 charL = asciiToLower(*lhs++); //ordering: lower-case chars have higher code points than uppper-case + const Char2 charR = asciiToLower(*rhs++); // + if (charL != charR) + return makeUnsigned(charL) <=> makeUnsigned(charR); //unsigned char-comparison is the convention! + } + return std::weak_ordering::equivalent; +} +} + + +template inline +bool startsWith(const S& str, const T& prefix) +{ + const size_t pfLen = strLength(prefix); + return strLength(str) >= pfLen && impl::equalSubstring(strBegin(str), strBegin(prefix), pfLen); +} + + +template inline +bool startsWithAsciiNoCase(const S& str, const T& prefix) +{ + assert(isAsciiString(str) || isAsciiString(prefix)); + const size_t pfLen = strLength(prefix); + return strLength(str) >= pfLen && impl::strcmpAsciiNoCase(strBegin(str), strBegin(prefix), pfLen) == std::weak_ordering::equivalent; +} + + +template inline +bool endsWith(const S& str, const T& postfix) +{ + const size_t strLen = strLength(str); + const size_t pfLen = strLength(postfix); + return strLen >= pfLen && impl::equalSubstring(strBegin(str) + strLen - pfLen, strBegin(postfix), pfLen); +} + + +template inline +bool endsWithAsciiNoCase(const S& str, const T& postfix) +{ + const size_t strLen = strLength(str); + const size_t pfLen = strLength(postfix); + return strLen >= pfLen && impl::strcmpAsciiNoCase(strBegin(str) + strLen - pfLen, strBegin(postfix), pfLen) == std::weak_ordering::equivalent; +} + + +template inline +bool equalString(const S& lhs, const T& rhs) +{ + const size_t lhsLen = strLength(lhs); + return lhsLen == strLength(rhs) && impl::equalSubstring(strBegin(lhs), strBegin(rhs), lhsLen); +} + + +template inline +bool equalAsciiNoCase(const S& lhs, const T& rhs) +{ + //assert(isAsciiString(lhs) || isAsciiString(rhs)); -> no, too strict (e.g. comparing file extensions ASCII-CI) + const size_t lhsLen = strLength(lhs); + return lhsLen == strLength(rhs) && impl::strcmpAsciiNoCase(strBegin(lhs), strBegin(rhs), lhsLen) == std::weak_ordering::equivalent; +} + + +namespace impl +{ +//support embedded 0 (unlike strncmp/wcsncmp) + compare unsigned[!] char +inline std::strong_ordering strcmpWithNulls(const char* ptr1, const char* ptr2, size_t num) { return std:: memcmp(ptr1, ptr2, num) <=> 0; } +inline std::strong_ordering strcmpWithNulls(const wchar_t* ptr1, const wchar_t* ptr2, size_t num) { return std::wmemcmp(ptr1, ptr2, num) <=> 0; } +} + +template inline +std::strong_ordering compareString(const S& lhs, const T& rhs) +{ + const size_t lhsLen = strLength(lhs); + const size_t rhsLen = strLength(rhs); + + //length check *after* strcmpWithNulls(): we DO care about natural ordering + if (const std::strong_ordering cmp = impl::strcmpWithNulls(strBegin(lhs), strBegin(rhs), std::min(lhsLen, rhsLen)); + cmp != std::strong_ordering::equal) + return cmp; + return lhsLen <=> rhsLen; +} + + +template inline +std::weak_ordering compareAsciiNoCase(const S& lhs, const T& rhs) +{ + const size_t lhsLen = strLength(lhs); + const size_t rhsLen = strLength(rhs); + + if (const std::weak_ordering cmp = impl::strcmpAsciiNoCase(strBegin(lhs), strBegin(rhs), std::min(lhsLen, rhsLen)); + cmp != std::weak_ordering::equivalent) + return cmp; + return lhsLen <=> rhsLen; +} + + +template inline +bool contains(const S& str, const T& term) +{ + static_assert(std::is_same_v, GetCharTypeT>); + const size_t strLen = strLength(str); + const size_t termLen = strLength(term); + if (strLen < termLen) + return false; + + const auto* const strFirst = strBegin(str); + const auto* const strLast = strFirst + strLen; + const auto* const termFirst = strBegin(term); + + return searchFirst(strFirst, strLast, + termFirst, termFirst + termLen) != strLast; +} + + +template inline +S afterLast(const S& str, const T& term, IfNotFoundReturn infr) +{ + static_assert(std::is_same_v, GetCharTypeT>); + const size_t termLen = strLength(term); + assert(termLen > 0); + + const auto* const strFirst = strBegin(str); + const auto* const strLast = strFirst + strLength(str); + const auto* const termFirst = strBegin(term); + + const auto* it = searchLast(strFirst, strLast, + termFirst, termFirst + termLen); + if (it == strLast) + return infr == IfNotFoundReturn::all ? str : S(); + + it += termLen; + return S(it, strLast - it); +} + + +template inline +S beforeLast(const S& str, const T& term, IfNotFoundReturn infr) +{ + static_assert(std::is_same_v, GetCharTypeT>); + const size_t termLen = strLength(term); + assert(termLen > 0); + + const auto* const strFirst = strBegin(str); + const auto* const strLast = strFirst + strLength(str); + const auto* const termFirst = strBegin(term); + + const auto* it = searchLast(strFirst, strLast, + termFirst, termFirst + termLen); + if (it == strLast) + return infr == IfNotFoundReturn::all ? str : S(); + + return S(strFirst, it - strFirst); +} + + +template inline +S afterFirst(const S& str, const T& term, IfNotFoundReturn infr) +{ + static_assert(std::is_same_v, GetCharTypeT>); + const size_t termLen = strLength(term); + assert(termLen > 0); + + const auto* const strFirst = strBegin(str); + const auto* const strLast = strFirst + strLength(str); + const auto* const termFirst = strBegin(term); + + const auto* it = searchFirst(strFirst, strLast, + termFirst, termFirst + termLen); + if (it == strLast) + return infr == IfNotFoundReturn::all ? str : S(); + + it += termLen; + return S(it, strLast - it); +} + + +template inline +S beforeFirst(const S& str, const T& term, IfNotFoundReturn infr) +{ + static_assert(std::is_same_v, GetCharTypeT>); + const size_t termLen = strLength(term); + assert(termLen > 0); + + const auto* const strFirst = strBegin(str); + const auto* const strLast = strFirst + strLength(str); + const auto* const termFirst = strBegin(term); + + auto it = searchFirst(strFirst, strLast, + termFirst, termFirst + termLen); + if (it == strLast) + return infr == IfNotFoundReturn::all ? str : S(); + + return S(strFirst, it - strFirst); +} + + +template inline +void split2(const S& str, Function1 isDelimiter, Function2 onStringPart) +{ + const auto* blockFirst = strBegin(str); + const auto* const strEnd = blockFirst + strLength(str); + + for (;;) + { + const auto* const blockLast = std::find_if(blockFirst, strEnd, isDelimiter); + onStringPart(makeStringView(blockFirst, blockLast)); + + if (blockLast == strEnd) + return; + + blockFirst = blockLast + 1; + } +} + + +template inline +void split(const S& str, Char delimiter, Function onStringPart) +{ + static_assert(std::is_same_v, Char>); + split2(str, [delimiter](const Char c) { return c == delimiter; }, onStringPart); +} + + +template inline +std::vector splitCpy(const S& str, Char delimiter, SplitOnEmpty soe) +{ + static_assert(std::is_same_v, Char>); + std::vector output; + + split2(str, [delimiter](const Char c) { return c == delimiter; }, [&, soe](std::basic_string_view block) + { + if (!block.empty() || soe == SplitOnEmpty::allow) + output.emplace_back(block.data(), block.size()); + }); + return output; +} + + +namespace impl +{ +ZEN_INIT_DETECT_MEMBER(append) + +//either call operator+=(S(str, len)) or append(str, len) +template >> inline +void stringAppend(S& str, InputIterator first, InputIterator last) { str.append(first, last); } + +//inefficient append: keep disabled until really needed +//template >> inline +//void stringAppend(S& str, InputIterator first, InputIterator last) { str += S(first, last); } + + +template inline +void replace(S& str, const T& oldTerm, const U& newTerm, CharEq charEqual) +{ + static_assert(std::is_same_v, GetCharTypeT>); + static_assert(std::is_same_v, GetCharTypeT>); + const size_t oldLen = strLength(oldTerm); + const size_t newLen = strLength(newTerm); + //assert(oldLen != 0); -> reasonable check, but challenged by unit-test + if (oldLen == 0) + return; + + const auto* const oldBegin = strBegin(oldTerm); + const auto* const oldEnd = oldBegin + oldLen; + + const auto* const newBegin = strBegin(newTerm); + const auto* const newEnd = newBegin + newLen; + + using CharType = GetCharTypeT; + if (oldLen == 1 && newLen == 1) //don't use expensive std::search unless required! + return std::replace_if(str.begin(), str.end(), [charEqual, charOld = *oldBegin](CharType c) { return charEqual(c, charOld); }, *newBegin); + + auto* it = strBegin(str); //don't use str.begin() or wxString will return this wxUni* nonsense! + auto* const strEnd = it + strLength(str); + + auto itFound = searchFirst(it, strEnd, + oldBegin, oldEnd, charEqual); + if (itFound == strEnd) + return; //optimize "oldTerm not found" + + S output(it, itFound); + do + { + impl::stringAppend(output, newBegin, newEnd); + it = itFound + oldLen; +#if 0 + if (!replaceAll) + itFound = strEnd; + else +#endif + itFound = searchFirst(it, strEnd, + oldBegin, oldEnd, charEqual); + + impl::stringAppend(output, it, itFound); + } + while (itFound != strEnd); + + str = std::move(output); +} +} + + +template inline +void replace(S& str, const T& oldTerm, const U& newTerm) +{ impl::replace(str, oldTerm, newTerm, std::equal_to()); } + + +template inline +S replaceCpy(S str, const T& oldTerm, const U& newTerm) +{ + replace(str, oldTerm, newTerm); + return str; +} + + +template inline +void replaceAsciiNoCase(S& str, const T& oldTerm, const U& newTerm) +{ + using CharType = GetCharTypeT; + impl::replace(str, oldTerm, newTerm, + [](CharType charL, CharType charR) { return asciiToLower(charL) == asciiToLower(charR); }); +} + + +template inline +S replaceCpyAsciiNoCase(S str, const T& oldTerm, const U& newTerm) +{ + replaceAsciiNoCase(str, oldTerm, newTerm); + return str; +} + + +template +[[nodiscard]] inline +std::pair trimCpy2(Char* first, Char* last, TrimSide side, Function trimThisChar) +{ + if (side == TrimSide::right || side == TrimSide::both) + while (first != last && trimThisChar(last[-1])) + --last; + + if (side == TrimSide::left || side == TrimSide::both) + while (first != last && trimThisChar(*first)) + ++first; + + return {first, last}; +} + + +template inline +void trim(S& str, TrimSide side, Function trimThisChar) +{ + const auto* const oldBegin = strBegin(str); + const auto [newBegin, newEnd] = trimCpy2(oldBegin, oldBegin + strLength(str), side, trimThisChar); + + if (newBegin != oldBegin) + str = S(newBegin, newEnd); //minor inefficiency: in case "str" is not shared, we could save an allocation and do a memory move only + else + str.resize(newEnd - newBegin); +} + + +template inline +void trim(S& str, TrimSide side) +{ + using CharType = GetCharTypeT; + trim(str, side, [](CharType c) { return isWhiteSpace(c); }); +} + + +template inline +S trimCpy(const S& str, TrimSide side) +{ + using CharType = GetCharTypeT; + const auto* const oldBegin = strBegin(str); + const auto* const oldEnd = oldBegin + strLength(str); + + const auto [newBegin, newEnd] = trimCpy2(oldBegin, oldEnd, side, [](CharType c) { return isWhiteSpace(c); }); + + if (newBegin == oldBegin && newEnd == oldEnd) + return str; + else + return S(newBegin, newEnd - newBegin); +} + + +namespace impl +{ +template +struct CopyStringToString +{ + T copy(const S& src) const + { + static_assert(!std::is_same_v, std::decay_t>); + return {strBegin(src), strLength(src)}; + } +}; + +template +struct CopyStringToString //perf: we don't need a deep copy if string types match +{ + template + T copy(S&& str) const { return std::forward(str); } +}; +} + +template inline +T copyStringTo(S&& str) { return impl::CopyStringToString, T>().copy(std::forward(str)); } + + +namespace impl +{ +template inline +int saferPrintf(char* buffer, size_t bufferSize, const char* format, const Num& number) //there is no such thing as a "safe" printf ;) +{ + return std::snprintf(buffer, bufferSize, format, number); //C99: returns number of chars written if successful, < 0 or >= bufferSize on error +} + +template inline +int saferPrintf(wchar_t* buffer, size_t bufferSize, const wchar_t* format, const Num& number) +{ + return std::swprintf(buffer, bufferSize, format, number); //C99: returns number of chars written if successful, < 0 on error (including buffer too small) +} +} + +template inline +S printNumber(const T& format, const Num& number) //format a single number using ::sprintf +{ + static_assert(std::is_same_v, GetCharTypeT>); + assert(strBegin(format)[strLength(format)] == 0); //format must be null-terminated! + + S buf(128, static_cast>('0')); + const int charsWritten = impl::saferPrintf(buf.data(), buf.size(), strBegin(format), number); + + if (charsWritten < 0 || makeUnsigned(charsWritten) > buf.size()) + { + assert(false); + return S(); + } + + buf.resize(charsWritten); + return buf; +} + + +namespace impl +{ +enum class NumberType +{ + signedInt, + unsignedInt, + floatingPoint, + other, +}; + + +template S numberTo(const Num& number, std::integral_constant) = delete; +#if 0 //default number to string conversion using streams: convenient, but SLOW, SLOW, SLOW!!!! (~ factor of 20) +template inline +S numberTo(const Num& number, std::integral_constant) +{ + std::basic_ostringstream> ss; + ss << number; + return copyStringTo(ss.str()); +} +#endif + + +template inline +S numberTo(const Num& number, std::integral_constant) +{ + //don't use sprintf("%g"): way SLOWWWWWWER than std::to_chars() + + 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 = toChars(std::begin(buffer), std::end(buffer), number); + + S output; + + for (const char c : makeStringView(static_cast(buffer), strEnd)) + output += static_cast>(c); + + return output; +} + + +/* +perf: integer to string: (executed 10 mio. times) + std::stringstream - 14796 ms + std::sprintf - 3086 ms + formatInteger - 778 ms +*/ + +template inline +void formatNegativeInteger(Num n, OutputIterator& it) +{ + assert(n < 0); + using CharType = typename std::iterator_traits::value_type; + do + { + const Num tmp = n / 10; + *--it = static_cast('0' + (tmp * 10 - n)); //8% faster than using modulus operator! + n = tmp; + } + while (n != 0); + + *--it = static_cast('-'); +} + +template inline +void formatPositiveInteger(Num n, OutputIterator& it) +{ + assert(n >= 0); + using CharType = typename std::iterator_traits::value_type; + do + { + const Num tmp = n / 10; + *--it = static_cast('0' + (n - tmp * 10)); //8% faster than using modulus operator! + n = tmp; + } + while (n != 0); +} + + +template inline +S numberTo(const Num& number, std::integral_constant) +{ + GetCharTypeT buffer[2 + sizeof(Num) * 241 / 100]; //zero-initialize? + //it's generally faster to use a buffer than to rely on String::operator+=() (in)efficiency + //required chars (+ sign char): 1 + ceil(ln_10(256^sizeof(n) / 2 + 1)) -> divide by 2 for signed half-range; second +1 since one half starts with 1! + // <= 1 + ceil(ln_10(256^sizeof(n))) =~ 1 + ceil(sizeof(n) * 2.4082) <= 2 + floor(sizeof(n) * 2.41) + + //caveat: consider INT_MIN: technically -INT_MIN == INT_MIN + auto it = std::end(buffer); + if (number < 0) + formatNegativeInteger(number, it); + else + formatPositiveInteger(number, it); + assert(it >= std::begin(buffer)); + + return S(&*it, std::end(buffer) - it); +} + + +template inline +S numberTo(const Num& number, std::integral_constant) +{ + GetCharTypeT buffer[1 + sizeof(Num) * 241 / 100]; //zero-initialize? + //required chars: ceil(ln_10(256^sizeof(n))) =~ ceil(sizeof(n) * 2.4082) <= 1 + floor(sizeof(n) * 2.41) + + auto it = std::end(buffer); + formatPositiveInteger(number, it); + assert(it >= std::begin(buffer)); + + return S(&*it, std::end(buffer) - it); +} + +//-------------------------------------------------------------------------------- + +template Num stringTo(const S& str, std::integral_constant) = delete; +#if 0 //default string to number conversion using streams: convenient, but SLOW +template inline +Num stringTo(const S& str, std::integral_constant) +{ + using CharType = GetCharTypeT; + Num number = 0; + std::basic_istringstream(copyStringTo>(str)) >> number; + return number; +} +#endif + + +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 fromChars(first, last); +} + + +inline +double stringToFloat(const wchar_t* first, const wchar_t* last) +{ + std::string buf; //let's rely on SSO + + for (const wchar_t c : makeStringView(first, last)) + buf += static_cast(c); + + return fromChars(buf.c_str(), buf.c_str() + buf.size()); +} + + +template inline +Num stringTo(const S& str, std::integral_constant) +{ + const auto* const first = strBegin(str); + const auto* const last = first + strLength(str); + return static_cast(stringToFloat(first, last)); +} + + +template +Num extractInteger(const S& str, bool& hasMinusSign) //very fast conversion to integers: slightly faster than std::atoi, but more importantly: generic +{ + using CharType = GetCharTypeT; + + const CharType* first = strBegin(str); + const CharType* last = first + strLength(str); + + while (first != last && isWhiteSpace(*first)) //skip leading whitespace + ++first; + + hasMinusSign = false; + if (first != last) + { + if (*first == static_cast('-')) + { + hasMinusSign = true; + ++first; + } + else if (*first == static_cast('+')) + ++first; + } + + Num number = 0; + + for (const CharType c : makeStringView(first, last)) + if (static_cast('0') <= c && c <= static_cast('9')) + { + number *= 10; + number += c - static_cast('0'); + } + else //rest of string should contain whitespace only, it's NOT a bug if there is something else! + break; //assert(std::all_of(it, last, isWhiteSpace)); -> this is NO assert situation + + return number; +} + + +template inline +Num stringTo(const S& str, std::integral_constant) +{ + bool hasMinusSign = false; //handle minus sign + const Num number = extractInteger(str, hasMinusSign); + return hasMinusSign ? -number : number; +} + + +template inline +Num stringTo(const S& str, std::integral_constant) //very fast conversion to integers: slightly faster than std::atoi, but more importantly: generic +{ + bool hasMinusSign = false; //handle minus sign + const Num number = extractInteger(str, hasMinusSign); + if (hasMinusSign) + { + assert(false); + return -makeSigned(number); //at least make some noise + } + return number; +} +} + + +template inline +S numberTo(const Num& number) +{ + using TypeTag = std::integral_constant ? impl::NumberType::signedInt : + isUnsignedInt ? impl::NumberType::unsignedInt : + isFloat ? impl::NumberType::floatingPoint : + impl::NumberType::other>; + + return impl::numberTo(number, TypeTag()); +} + + +template inline +Num stringTo(const S& str) +{ + using TypeTag = std::integral_constant ? impl::NumberType::signedInt : + isUnsignedInt ? impl::NumberType::unsignedInt : + isFloat ? impl::NumberType::floatingPoint : + impl::NumberType::other>; + + return impl::stringTo(str, TypeTag()); +} + + +inline //hexify beats "printNumber("%02X", c)" by a nice factor of 3! +std::pair hexify(unsigned char c, bool upperCase) +{ + auto hexifyDigit = [upperCase](int num) -> char //input [0, 15], output 0-9, A-F + { + assert(0 <= num&& num <= 15); //guaranteed by design below! + if (num <= 9) + return static_cast('0' + num); //no signed/unsigned char problem here! + + if (upperCase) + return static_cast('A' + (num - 10)); + else + return static_cast('a' + (num - 10)); + }; + return {hexifyDigit(c / 16), hexifyDigit(c % 16)}; +} + + +inline //unhexify beats "::sscanf(&it[3], "%02X", &tmp)" by a factor of 3000 for ~250000 calls!!! +char unhexify(char high, char low) +{ + auto unhexifyDigit = [](const char hex) -> int //input 0-9, a-f, A-F; output range: [0, 15] + { + if ('0' <= hex && hex <= '9') //no signed/unsigned char problem here! + return hex - '0'; + else if ('A' <= hex && hex <= 'F') + return (hex - 'A') + 10; + else if ('a' <= hex && hex <= 'f') + return (hex - 'a') + 10; + assert(false); + return 0; + }; + return static_cast(16 * unhexifyDigit(high) + unhexifyDigit(low)); //[!] convert to unsigned char first, then to char (which may be signed) +} + + +inline +std::string formatAsHexString(const std::string_view& blob) +{ + std::string output; + for (const char c : blob) + { + const auto [high, low] = hexify(c, false /*upperCase*/); + output += high; + output += low; + } + return output; +} + + + + +template inline +Num hashString(const S& str) +{ + using CharType = GetCharTypeT; + const auto* const strFirst = strBegin(str); + + FNV1aHash hash; + std::for_each(strFirst, strFirst + strLength(str), [&hash](CharType c) { hash.add(c); }); + return hash.get(); +} + + +struct StringHash +{ + using is_transparent = int; //enable heterogenous lookup! + + template + size_t operator()(const String& str) const { return hashString(str); } +}; + + +struct StringEqual +{ + using is_transparent = int; //enable heterogenous lookup! + + template + bool operator()(const String1& lhs, const String2& rhs) const { return equalString(lhs, rhs); } +}; + + +struct LessAsciiNoCase +{ + template + bool operator()(const String& lhs, const String& rhs) const { return compareAsciiNoCase(lhs, rhs) < 0; } +}; + + +struct StringHashAsciiNoCase +{ + using is_transparent = int; //allow heterogenous lookup! + + template + size_t operator()(const String& str) const + { + using CharType = GetCharTypeT; + const auto* const strFirst = strBegin(str); + + FNV1aHash hash; + std::for_each(strFirst, strFirst + strLength(str), [&hash](CharType c) { hash.add(asciiToLower(c)); }); + return hash.get(); + } +}; + + +struct StringEqualAsciiNoCase +{ + using is_transparent = int; //allow heterogenous lookup! + + template + bool operator()(const String1& lhs, const String2& rhs) const + { + return equalAsciiNoCase(lhs, rhs); + } +}; +} + +#endif //STRING_TOOLS_H_213458973046 diff --git a/zen/string_traits.h b/zen/string_traits.h new file mode 100644 index 0000000..576ea2b --- /dev/null +++ b/zen/string_traits.h @@ -0,0 +1,186 @@ +// ***************************************************************************** +// * 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 STRING_TRAITS_H_813274321443234 +#define STRING_TRAITS_H_813274321443234 + +#include //strlen +#include +#include "type_traits.h" + + +//uniform access to string-like types, both classes and character arrays +namespace zen +{ +/* isStringLike<>: + isStringLike //equals "true" + isStringLike //equals "false" + + GetCharTypeT<>: + GetCharTypeT //equals wchar_t + GetCharTypeT //equals wchar_t + + strLength(): + strLength(str); //equals str.length() + strLength(array); //equals cStringLength(array) + + strBegin(): -> not null-terminated! -> may be nullptr if length is 0! + std::wstring str(L"dummy"); + char array[] = "dummy"; + strBegin(str); //returns str.c_str() + strBegin(array); //returns array */ + + + +//---------------------- implementation ---------------------- +namespace impl +{ +template //test if result of S::c_str() can convert to const Char* +class HasConversion +{ + using Yes = char[1]; + using No = char[2]; + + static Yes& hasConversion(const Char*); + static No& hasConversion(...); + +public: + enum { value = sizeof(hasConversion(std::declval().c_str())) == sizeof(Yes) }; +}; + + +template struct GetCharTypeImpl { using Type = void; }; + +template +struct GetCharTypeImpl +{ + using Type = std::conditional_t::value, wchar_t, + /**/ std::conditional_t::value, char, void>>; + + //using Type = typename S::value_type; + /*DON'T use S::value_type: + 1. support Glib::ustring: value_type is "unsigned int" but c_str() returns "const char*" + 2. wxString, wxWidgets v2.9, has some questionable string design: wxString::c_str() returns a proxy (wxCStrData) which + is implicitly convertible to *both* "const char*" and "const wchar_t*" while wxString::value_type is a wrapper around an unsigned int + */ +}; + +template <> struct GetCharTypeImpl { using Type = char; }; +template <> struct GetCharTypeImpl { using Type = wchar_t; }; + +template <> struct GetCharTypeImpl, false> { using Type = char; }; +template <> struct GetCharTypeImpl, false> { using Type = wchar_t; }; +template <> struct GetCharTypeImpl, false> { using Type = char; }; +template <> struct GetCharTypeImpl, false> { using Type = wchar_t; }; + + +ZEN_INIT_DETECT_MEMBER_TYPE(value_type) +ZEN_INIT_DETECT_MEMBER(c_str) //we don't know the exact declaration of the member attribute and it may be in a base class! +ZEN_INIT_DETECT_MEMBER(length) // + +template +class StringTraits +{ + using CleanType = std::remove_cvref_t; + using NonArrayType = std::remove_extent_t ; + using NonPtrType = std::remove_pointer_t; + using UndecoratedType = std::remove_cv_t ; //handle "const char* const" + +public: + enum + { + isStringClass = hasMemberType_value_type&& + hasMember_c_str && + hasMember_length + }; + + using CharType = typename GetCharTypeImpl::Type; + + enum + { + isStringLike = std::is_same_v || + std::is_same_v + }; +}; +} + + +template +constexpr bool isStringLike = impl::StringTraits::isStringLike; + +template +using GetCharTypeT = typename impl::StringTraits::CharType; + + +namespace impl +{ +//strlen/wcslen are vectorized since VS14 CTP3 +inline size_t cStringLength(const char* str) { return std::strlen(str); } +inline size_t cStringLength(const wchar_t* str) { return std::wcslen(str); } + +#if 0 //no significant perf difference for "comparison" test case between cStringLength/wcslen: +template inline +size_t cStringLength(const C* str) +{ + static_assert(std::is_same_v || std::is_same_v); + size_t len = 0; + while (*str++ != 0) + ++len; + return len; +} +#endif + +template ::isStringClass>> inline +const GetCharTypeT* strBegin(const S& str) //SFINAE: T must be a "string" +{ + return str.c_str(); +} + +inline const char* strBegin(const char* str) { return str; } +inline const wchar_t* strBegin(const wchar_t* str) { return str; } +inline const char* strBegin(const char& ch) { return &ch; } +inline const wchar_t* strBegin(const wchar_t& ch) { return &ch; } + +inline const char* strBegin(const std::basic_string_view& ref) { return ref.data(); } +inline const wchar_t* strBegin(const std::basic_string_view& ref) { return ref.data(); } +inline const char* strBegin(const std::basic_string_view& ref) { return ref.data(); } +inline const wchar_t* strBegin(const std::basic_string_view& ref) { return ref.data(); } + +template ::isStringClass>> inline +size_t strLength(const S& str) //SFINAE: T must be a "string" +{ + return str.length(); +} + +inline size_t strLength(const char* str) { return cStringLength(str); } +inline size_t strLength(const wchar_t* str) { return cStringLength(str); } +inline size_t strLength(char) { return 1; } +inline size_t strLength(wchar_t) { return 1; } + +inline size_t strLength(const std::basic_string_view& ref) { return ref.length(); } +inline size_t strLength(const std::basic_string_view& ref) { return ref.length(); } +inline size_t strLength(const std::basic_string_view& ref) { return ref.length(); } +inline size_t strLength(const std::basic_string_view& ref) { return ref.length(); } +} + + +template inline +auto strBegin(S&& str) +{ + static_assert(isStringLike); + return impl::strBegin(std::forward(str)); +} + + +template inline +size_t strLength(S&& str) +{ + static_assert(isStringLike); + return impl::strLength(std::forward(str)); +} +} + +#endif //STRING_TRAITS_H_813274321443234 diff --git a/zen/symlink_target.h b/zen/symlink_target.h new file mode 100644 index 0000000..97bba69 --- /dev/null +++ b/zen/symlink_target.h @@ -0,0 +1,97 @@ +// ***************************************************************************** +// * 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 SYMLINK_TARGET_H_801783470198357483 +#define SYMLINK_TARGET_H_801783470198357483 + +#include "file_error.h" +#include "file_path.h" + + #include + #include //realpath + + +namespace zen +{ + +struct SymlinkRawContent +{ + Zstring targetPath; +}; +SymlinkRawContent getSymlinkRawContent(const Zstring& linkPath); //throw FileError + +Zstring getSymlinkResolvedPath(const Zstring& linkPath); //throw FileError +} + + + + + + + + + +//################################ implementation ################################ + + +namespace zen +{ +namespace +{ +//retrieve raw target data of symlink or junction +SymlinkRawContent getSymlinkRawContent_impl(const Zstring& linkPath) //throw SysError +{ + const size_t bufSize = 10000; + std::vector buf(bufSize); + + const ssize_t bytesWritten = ::readlink(linkPath.c_str(), buf.data(), bufSize); + if (bytesWritten < 0) + THROW_LAST_SYS_ERROR("readlink"); + + ASSERT_SYSERROR(makeUnsigned(bytesWritten) <= bufSize); //better safe than sorry + + if (makeUnsigned(bytesWritten) == bufSize) //detect truncation; not an error for readlink! + throw SysError(formatSystemError("readlink", L"", L"Buffer truncated.")); + + return {.targetPath = Zstring(buf.data(), bytesWritten)}; //readlink does not append 0-termination! +} + + +Zstring getSymlinkResolvedPath_impl(const Zstring& linkPath) //throw SysError +{ + char* targetPath = ::realpath(linkPath.c_str(), nullptr /*resolved_path*/); + if (!targetPath) + THROW_LAST_SYS_ERROR("realpath"); + ZEN_ON_SCOPE_EXIT(::free(targetPath)); + return targetPath; +} +} + + +inline +SymlinkRawContent getSymlinkRawContent(const Zstring& linkPath) +{ + try + { + return getSymlinkRawContent_impl(linkPath); //throw SysError + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot resolve symbolic link %x."), L"%x", fmtPath(linkPath)), e.toString()); } +} + + +inline +Zstring getSymlinkResolvedPath(const Zstring& linkPath) +{ + try + { + return getSymlinkResolvedPath_impl(linkPath); //throw SysError + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot determine final path for %x."), L"%x", fmtPath(linkPath)), e.toString()); } +} + +} + +#endif //SYMLINK_TARGET_H_801783470198357483 diff --git a/zen/sys_error.cpp b/zen/sys_error.cpp new file mode 100644 index 0000000..90d9ee2 --- /dev/null +++ b/zen/sys_error.cpp @@ -0,0 +1,281 @@ +// ***************************************************************************** +// * 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 "sys_error.h" + #include + +using namespace zen; + + +namespace +{ +std::wstring formatSystemErrorCode(ErrorCode ec) +{ + switch (ec) //pretty much all codes currently used on CentOS 7 and macOS 10.15 + { + ZEN_CHECK_CASE_FOR_CONSTANT(EPERM); + ZEN_CHECK_CASE_FOR_CONSTANT(ENOENT); + ZEN_CHECK_CASE_FOR_CONSTANT(ESRCH); + ZEN_CHECK_CASE_FOR_CONSTANT(EINTR); + ZEN_CHECK_CASE_FOR_CONSTANT(EIO); + ZEN_CHECK_CASE_FOR_CONSTANT(ENXIO); + ZEN_CHECK_CASE_FOR_CONSTANT(E2BIG); + ZEN_CHECK_CASE_FOR_CONSTANT(ENOEXEC); + ZEN_CHECK_CASE_FOR_CONSTANT(EBADF); + ZEN_CHECK_CASE_FOR_CONSTANT(ECHILD); + ZEN_CHECK_CASE_FOR_CONSTANT(EAGAIN); + ZEN_CHECK_CASE_FOR_CONSTANT(ENOMEM); + ZEN_CHECK_CASE_FOR_CONSTANT(EACCES); + ZEN_CHECK_CASE_FOR_CONSTANT(EFAULT); + ZEN_CHECK_CASE_FOR_CONSTANT(ENOTBLK); + ZEN_CHECK_CASE_FOR_CONSTANT(EBUSY); + ZEN_CHECK_CASE_FOR_CONSTANT(EEXIST); + ZEN_CHECK_CASE_FOR_CONSTANT(EXDEV); + ZEN_CHECK_CASE_FOR_CONSTANT(ENODEV); + ZEN_CHECK_CASE_FOR_CONSTANT(ENOTDIR); + ZEN_CHECK_CASE_FOR_CONSTANT(EISDIR); + ZEN_CHECK_CASE_FOR_CONSTANT(EINVAL); + ZEN_CHECK_CASE_FOR_CONSTANT(ENFILE); + ZEN_CHECK_CASE_FOR_CONSTANT(EMFILE); + ZEN_CHECK_CASE_FOR_CONSTANT(ENOTTY); + ZEN_CHECK_CASE_FOR_CONSTANT(ETXTBSY); + ZEN_CHECK_CASE_FOR_CONSTANT(EFBIG); + ZEN_CHECK_CASE_FOR_CONSTANT(ENOSPC); + ZEN_CHECK_CASE_FOR_CONSTANT(ESPIPE); + ZEN_CHECK_CASE_FOR_CONSTANT(EROFS); + ZEN_CHECK_CASE_FOR_CONSTANT(EMLINK); + ZEN_CHECK_CASE_FOR_CONSTANT(EPIPE); + ZEN_CHECK_CASE_FOR_CONSTANT(EDOM); + ZEN_CHECK_CASE_FOR_CONSTANT(ERANGE); + ZEN_CHECK_CASE_FOR_CONSTANT(EDEADLK); + ZEN_CHECK_CASE_FOR_CONSTANT(ENAMETOOLONG); + ZEN_CHECK_CASE_FOR_CONSTANT(ENOLCK); + ZEN_CHECK_CASE_FOR_CONSTANT(ENOSYS); + ZEN_CHECK_CASE_FOR_CONSTANT(ENOTEMPTY); + ZEN_CHECK_CASE_FOR_CONSTANT(ELOOP); + ZEN_CHECK_CASE_FOR_CONSTANT(ENOMSG); + ZEN_CHECK_CASE_FOR_CONSTANT(EIDRM); + ZEN_CHECK_CASE_FOR_CONSTANT(ENOSTR); + ZEN_CHECK_CASE_FOR_CONSTANT(ENODATA); + ZEN_CHECK_CASE_FOR_CONSTANT(ETIME); + ZEN_CHECK_CASE_FOR_CONSTANT(ENOSR); + ZEN_CHECK_CASE_FOR_CONSTANT(EREMOTE); + ZEN_CHECK_CASE_FOR_CONSTANT(ENOLINK); + ZEN_CHECK_CASE_FOR_CONSTANT(EPROTO); + ZEN_CHECK_CASE_FOR_CONSTANT(EMULTIHOP); + ZEN_CHECK_CASE_FOR_CONSTANT(EBADMSG); + ZEN_CHECK_CASE_FOR_CONSTANT(EOVERFLOW); + ZEN_CHECK_CASE_FOR_CONSTANT(EILSEQ); + ZEN_CHECK_CASE_FOR_CONSTANT(EUSERS); + ZEN_CHECK_CASE_FOR_CONSTANT(ENOTSOCK); + ZEN_CHECK_CASE_FOR_CONSTANT(EDESTADDRREQ); + ZEN_CHECK_CASE_FOR_CONSTANT(EMSGSIZE); + ZEN_CHECK_CASE_FOR_CONSTANT(EPROTOTYPE); + ZEN_CHECK_CASE_FOR_CONSTANT(ENOPROTOOPT); + ZEN_CHECK_CASE_FOR_CONSTANT(EPROTONOSUPPORT); + ZEN_CHECK_CASE_FOR_CONSTANT(ESOCKTNOSUPPORT); + ZEN_CHECK_CASE_FOR_CONSTANT(ENOTSUP); + ZEN_CHECK_CASE_FOR_CONSTANT(EPFNOSUPPORT); + ZEN_CHECK_CASE_FOR_CONSTANT(EAFNOSUPPORT); + ZEN_CHECK_CASE_FOR_CONSTANT(EADDRINUSE); + ZEN_CHECK_CASE_FOR_CONSTANT(EADDRNOTAVAIL); + ZEN_CHECK_CASE_FOR_CONSTANT(ENETDOWN); + ZEN_CHECK_CASE_FOR_CONSTANT(ENETUNREACH); + ZEN_CHECK_CASE_FOR_CONSTANT(ENETRESET); + ZEN_CHECK_CASE_FOR_CONSTANT(ECONNABORTED); + ZEN_CHECK_CASE_FOR_CONSTANT(ECONNRESET); + ZEN_CHECK_CASE_FOR_CONSTANT(ENOBUFS); + ZEN_CHECK_CASE_FOR_CONSTANT(EISCONN); + ZEN_CHECK_CASE_FOR_CONSTANT(ENOTCONN); + ZEN_CHECK_CASE_FOR_CONSTANT(ESHUTDOWN); + ZEN_CHECK_CASE_FOR_CONSTANT(ETOOMANYREFS); + ZEN_CHECK_CASE_FOR_CONSTANT(ETIMEDOUT); + ZEN_CHECK_CASE_FOR_CONSTANT(ECONNREFUSED); + ZEN_CHECK_CASE_FOR_CONSTANT(EHOSTDOWN); + ZEN_CHECK_CASE_FOR_CONSTANT(EHOSTUNREACH); + ZEN_CHECK_CASE_FOR_CONSTANT(EALREADY); + ZEN_CHECK_CASE_FOR_CONSTANT(EINPROGRESS); + ZEN_CHECK_CASE_FOR_CONSTANT(ESTALE); + ZEN_CHECK_CASE_FOR_CONSTANT(EDQUOT); + ZEN_CHECK_CASE_FOR_CONSTANT(ECANCELED); + ZEN_CHECK_CASE_FOR_CONSTANT(EOWNERDEAD); + ZEN_CHECK_CASE_FOR_CONSTANT(ENOTRECOVERABLE); + + ZEN_CHECK_CASE_FOR_CONSTANT(ECHRNG); + ZEN_CHECK_CASE_FOR_CONSTANT(EL2NSYNC); + ZEN_CHECK_CASE_FOR_CONSTANT(EL3HLT); + ZEN_CHECK_CASE_FOR_CONSTANT(EL3RST); + ZEN_CHECK_CASE_FOR_CONSTANT(ELNRNG); + ZEN_CHECK_CASE_FOR_CONSTANT(EUNATCH); + ZEN_CHECK_CASE_FOR_CONSTANT(ENOCSI); + ZEN_CHECK_CASE_FOR_CONSTANT(EL2HLT); + ZEN_CHECK_CASE_FOR_CONSTANT(EBADE); + ZEN_CHECK_CASE_FOR_CONSTANT(EBADR); + ZEN_CHECK_CASE_FOR_CONSTANT(EXFULL); + ZEN_CHECK_CASE_FOR_CONSTANT(ENOANO); + ZEN_CHECK_CASE_FOR_CONSTANT(EBADRQC); + ZEN_CHECK_CASE_FOR_CONSTANT(EBADSLT); + ZEN_CHECK_CASE_FOR_CONSTANT(EBFONT); + ZEN_CHECK_CASE_FOR_CONSTANT(ENONET); + ZEN_CHECK_CASE_FOR_CONSTANT(ENOPKG); + ZEN_CHECK_CASE_FOR_CONSTANT(EADV); + ZEN_CHECK_CASE_FOR_CONSTANT(ESRMNT); + ZEN_CHECK_CASE_FOR_CONSTANT(ECOMM); + ZEN_CHECK_CASE_FOR_CONSTANT(EDOTDOT); + ZEN_CHECK_CASE_FOR_CONSTANT(ENOTUNIQ); + ZEN_CHECK_CASE_FOR_CONSTANT(EBADFD); + ZEN_CHECK_CASE_FOR_CONSTANT(EREMCHG); + ZEN_CHECK_CASE_FOR_CONSTANT(ELIBACC); + ZEN_CHECK_CASE_FOR_CONSTANT(ELIBBAD); + ZEN_CHECK_CASE_FOR_CONSTANT(ELIBSCN); + ZEN_CHECK_CASE_FOR_CONSTANT(ELIBMAX); + ZEN_CHECK_CASE_FOR_CONSTANT(ELIBEXEC); + ZEN_CHECK_CASE_FOR_CONSTANT(ERESTART); + ZEN_CHECK_CASE_FOR_CONSTANT(ESTRPIPE); + ZEN_CHECK_CASE_FOR_CONSTANT(EUCLEAN); + ZEN_CHECK_CASE_FOR_CONSTANT(ENOTNAM); + ZEN_CHECK_CASE_FOR_CONSTANT(ENAVAIL); + ZEN_CHECK_CASE_FOR_CONSTANT(EISNAM); + ZEN_CHECK_CASE_FOR_CONSTANT(EREMOTEIO); + ZEN_CHECK_CASE_FOR_CONSTANT(ENOMEDIUM); + ZEN_CHECK_CASE_FOR_CONSTANT(EMEDIUMTYPE); + ZEN_CHECK_CASE_FOR_CONSTANT(ENOKEY); + ZEN_CHECK_CASE_FOR_CONSTANT(EKEYEXPIRED); + ZEN_CHECK_CASE_FOR_CONSTANT(EKEYREVOKED); + ZEN_CHECK_CASE_FOR_CONSTANT(EKEYREJECTED); + ZEN_CHECK_CASE_FOR_CONSTANT(ERFKILL); + ZEN_CHECK_CASE_FOR_CONSTANT(EHWPOISON); + default: + return replaceCpy(_("Error code %x"), L"%x", numberTo(ec)); + } +} +} + + +std::wstring zen::formatGlibError(const std::string& functionName, GError* error) +{ + if (!error) + return formatSystemError(functionName, L"", _("Error description not available.") + L" null GError"); + + if (error->domain == G_FILE_ERROR) //"values corresponding to errno codes" + return formatSystemError(functionName, error->code); + + std::wstring errorCode; + if (error->domain == G_IO_ERROR) + errorCode = [&]() -> std::wstring + { + switch (error->code) //GIOErrorEnum: https://gitlab.gnome.org/GNOME/glib/-/blob/master/gio/gioenums.h#L530 + { + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_FAILED); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_NOT_FOUND); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_EXISTS); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_IS_DIRECTORY); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_NOT_DIRECTORY); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_NOT_EMPTY); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_NOT_REGULAR_FILE); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_NOT_SYMBOLIC_LINK); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_NOT_MOUNTABLE_FILE); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_FILENAME_TOO_LONG); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_INVALID_FILENAME); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_TOO_MANY_LINKS); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_NO_SPACE); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_INVALID_ARGUMENT); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_PERMISSION_DENIED); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_NOT_SUPPORTED); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_NOT_MOUNTED); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_ALREADY_MOUNTED); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_CLOSED); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_CANCELLED); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_PENDING); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_READ_ONLY); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_CANT_CREATE_BACKUP); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_WRONG_ETAG); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_TIMED_OUT); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_WOULD_RECURSE); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_BUSY); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_WOULD_BLOCK); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_HOST_NOT_FOUND); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_WOULD_MERGE); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_FAILED_HANDLED); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_TOO_MANY_OPEN_FILES); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_NOT_INITIALIZED); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_ADDRESS_IN_USE); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_PARTIAL_INPUT); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_INVALID_DATA); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_DBUS_ERROR); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_HOST_UNREACHABLE); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_NETWORK_UNREACHABLE); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_CONNECTION_REFUSED); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_PROXY_FAILED); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_PROXY_AUTH_FAILED); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_PROXY_NEED_AUTH); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_PROXY_NOT_ALLOWED); +#ifndef GLIB_CHECK_VERSION //e.g Debian 8 (GLib 2.42) CentOS 7 (GLib 2.56) +#error Where is GLib? +#endif +#if GLIB_CHECK_VERSION(2, 44, 0) + static_assert(G_IO_ERROR_BROKEN_PIPE == G_IO_ERROR_CONNECTION_CLOSED); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_CONNECTION_CLOSED); + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_NOT_CONNECTED); +#endif +#if GLIB_CHECK_VERSION(2, 48, 0) + ZEN_CHECK_CASE_FOR_CONSTANT(G_IO_ERROR_MESSAGE_TOO_LARGE); +#endif + default: + return replaceCpy(L"GIO error %x", L"%x", numberTo(error->code)); + } + }(); + else + { + //g-file-error-quark => g-file-error + //g-io-error-quark => g-io-error + std::wstring domain = utfTo(::g_quark_to_string(error->domain)); //e.g. "g-io-error-quark" + if (endsWith(domain, L"-quark")) + domain = beforeLast(domain, L"-", IfNotFoundReturn::none); + + errorCode = domain + L' ' + numberTo(error->code); //e.g. "g-io-error 15" + } + + const std::wstring errorMsg = utfTo(error->message); //e.g. "Unable to find or create trash directory for file.txt" + + return formatSystemError(functionName, errorCode, errorMsg); +} + + + +std::wstring zen::getSystemErrorDescription(ErrorCode ec) //return empty string on error +{ + const ErrorCode ecCurrent = getLastError(); //not necessarily == ec + ZEN_ON_SCOPE_EXIT(errno = ecCurrent); + + std::wstring errorMsg = utfTo(::g_strerror(ec)); //... vs strerror(): "marginally improves thread safety, and marginally improves consistency" + + trim(errorMsg); //Windows messages seem to end with a space... + return errorMsg; +} + + +std::wstring zen::formatSystemError(const std::string& functionName, ErrorCode ec) +{ + return formatSystemError(functionName, formatSystemErrorCode(ec), getSystemErrorDescription(ec)); +} + + +std::wstring zen::formatSystemError(const std::string& functionName, const std::wstring& errorCode, const std::wstring& errorMsg) +{ + std::wstring output = trimCpy(errorCode); + + const std::wstring errorMsgFmt = trimCpy(errorMsg); + if (!output.empty() && !errorMsgFmt.empty()) + output += L": "; + + output += errorMsgFmt; + + if (!functionName.empty()) + output += L" [" + utfTo(functionName) + L']'; + + return trimCpy(output); +} diff --git a/zen/sys_error.h b/zen/sys_error.h new file mode 100644 index 0000000..53cd284 --- /dev/null +++ b/zen/sys_error.h @@ -0,0 +1,86 @@ +// ***************************************************************************** +// * 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 SYS_ERROR_H_3284791347018951324534 +#define SYS_ERROR_H_3284791347018951324534 + +#include "scope_guard.h" // +#include "i18n.h" //not used by this header, but the "rest of the world" needs it! +#include "zstring.h" // +#include "extra_log.h" // + + #include + #include + + +namespace zen +{ +//evaluate GetLastError()/errno and assemble specific error message + using ErrorCode = int; + +ErrorCode getLastError(); + +std::wstring formatSystemError(const std::string& functionName, const std::wstring& errorCode, const std::wstring& errorMsg); +std::wstring formatSystemError(const std::string& functionName, ErrorCode ec); + std::wstring formatGlibError(const std::string& functionName, GError* error); + + +//A low-level exception class giving (non-translated) detail information only - same conceptional level like "GetLastError()"! +class SysError +{ +public: + explicit SysError(const std::wstring& msg) : msg_(msg) {} + const std::wstring& toString() const { return msg_; } + +private: + std::wstring msg_; +}; + +#define DEFINE_NEW_SYS_ERROR(X) struct X : public zen::SysError { X(const std::wstring& msg) : SysError(msg) {} }; + + + +//better leave it as a macro (see comment in file_error.h) +#define THROW_LAST_SYS_ERROR(functionName) \ + do { const ErrorCode ecInternal = getLastError(); throw zen::SysError(formatSystemError(functionName, ecInternal)); } while (false) + + +/* Example: ASSERT_SYSERROR(expr); + + Equivalent to: + if (!expr) + throw zen::SysError(L"Assertion failed: \"expr\""); */ +#define ASSERT_SYSERROR(expr) ASSERT_SYSERROR_IMPL(expr, #expr) //throw SysError + + + +//######################## implementation ######################## +inline +ErrorCode getLastError() +{ + return errno; //don't use "::" prefix, errno is a macro! +} + + +std::wstring getSystemErrorDescription(ErrorCode ec); //return empty string on error +//intentional overload ambiguity to catch usage errors with HRESULT: +std::wstring getSystemErrorDescription(long long) = delete; + + + + +namespace impl +{ +inline bool validateBool(bool b) { return b; } +inline bool validateBool(void* b) { return b; } +bool validateBool(int) = delete; //catch unintended bool conversions, e.g. HRESULT +} +#define ASSERT_SYSERROR_IMPL(expr, exprStr) \ + { if (!zen::impl::validateBool(expr)) \ + throw zen::SysError(L"Assertion failed: \"" L ## exprStr L"\""); } +} + +#endif //SYS_ERROR_H_3284791347018951324534 diff --git a/zen/sys_info.cpp b/zen/sys_info.cpp new file mode 100644 index 0000000..00b7cd2 --- /dev/null +++ b/zen/sys_info.cpp @@ -0,0 +1,296 @@ +// ***************************************************************************** +// * 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 "sys_info.h" +#include "crc.h" +#include "file_access.h" +#include "sys_version.h" + + #include "symlink_target.h" + #include "file_io.h" + #include + #include //IFF_LOOPBACK + #include //sockaddr_ll + + + #include "process_exec.h" + #include //getuid() + #include //getpwuid_r() + +using namespace zen; + + +Zstring zen::getLoginUser() //throw FileError +{ + auto tryGetNonRootUser = [](const char* varName) -> std::optional + { + if (const std::optional username = getEnvironmentVar(varName)) + if (!username->empty() && *username != "root") + return *username; + return {}; + }; + + if (const uid_t userIdNo = ::getuid(); //never fails + userIdNo != 0) //nofail; non-root + { + //ugh, the world's stupidest API: + std::vector buf(std::max(10000, ::sysconf(_SC_GETPW_R_SIZE_MAX))); //::sysconf may return long(-1) or even a too small size!! WTF! + passwd buf2 = {}; + passwd* pwEntry = nullptr; + if (const int rv = ::getpwuid_r(userIdNo, //uid_t uid + &buf2, //struct passwd* pwd + buf.data(), //char* buf + buf.size(), //size_t buflen + &pwEntry); //struct passwd** result + rv != 0 || !pwEntry) + { + //"If an error occurs, errno is set appropriately" => why the fuck, then, also return errno as return value!? + errno = rv != 0 ? rv : ENOENT; + THROW_LAST_FILE_ERROR(_("Cannot get process information."), "getpwuid_r(" + numberTo(userIdNo) + ')'); + } + + return pwEntry->pw_name; + } + //else: root(0) => consider as request for elevation, NOT impersonation! + + //getlogin() is smarter than simply evaluating $LOGNAME! even in contexts without + //$LOGNAME, e.g. "sudo su" on Ubuntu, it returns the correct non-root user! + if (const char* loginUser = ::getlogin()) //https://linux.die.net/man/3/getlogin + if (strLength(loginUser) > 0 && !equalString(loginUser, "root")) + return loginUser; + //BUT: getlogin() can fail with ENOENT on Linux Mint: https://freefilesync.org/forum/viewtopic.php?t=8181 + + //getting a little desperate: variables used by installer.sh + if (const std::optional username = tryGetNonRootUser("USER")) return *username; + if (const std::optional username = tryGetNonRootUser("SUDO_USER")) return *username; + if (const std::optional username = tryGetNonRootUser("LOGNAME")) return *username; + + + //apparently the current user really IS root: https://freefilesync.org/forum/viewtopic.php?t=8405 + assert(getuid() == 0); + return "root"; +} + + +Zstring zen::getUserDescription() //throw FileError +{ + const Zstring username = getLoginUser(); //throw FileError + const Zstring computerName = []() -> Zstring //throw FileError + { + std::vector buf(10000); + if (::gethostname(buf.data(), buf.size()) != 0) + THROW_LAST_FILE_ERROR(_("Cannot get process information."), "gethostname"); + + Zstring hostName = buf.data(); + if (endsWithAsciiNoCase(hostName, ".local")) //strip fluff (macOS) => apparently not added on Linux? + hostName = beforeLast(hostName, '.', IfNotFoundReturn::none); + + return hostName; + }(); + + if (contains(getUpperCase(computerName), getUpperCase(username))) + return username; //no need for text duplication! e.g. "Zenju (Zenju-PC)" + + return username + Zstr(" (") + computerName + Zstr(')'); //e.g. "Admin (Zenju-PC)" +} + + +namespace +{ +} + + +ComputerModel zen::getComputerModel() //throw FileError +{ + ComputerModel cm; + try + { + auto tryGetInfo = [](const Zstring& filePath) + { + try + { + const std::string stream = getFileContent(filePath, nullptr /*notifyUnbufferedIO*/); //throw FileError + return utfTo(stream); + } + catch (FileError&) + { + if (!itemExists(filePath)) //throw FileError + return std::wstring(); + + throw; + } + }; + cm.model = tryGetInfo("/sys/devices/virtual/dmi/id/product_name"); //throw FileError + cm.vendor = tryGetInfo("/sys/devices/virtual/dmi/id/sys_vendor"); // + + //clean up: + cm.model = beforeFirst(cm.model, L'\u00ff', IfNotFoundReturn::all); //fix broken BIOS entries: + cm.vendor = beforeFirst(cm.vendor, L'\u00ff', IfNotFoundReturn::all); //0xff can be considered 0 + + replace(cm.model, L'_', L' '); //e.g. "CBX3___", "SYSTEM_MANUFACTURER", or just "_" + replace(cm.vendor, L'_', L' '); //e.g. "DELL__", "Exertis_CapTech", or just "_" + + trim(cm.model); + trim(cm.vendor); + + for (const char* dummyModel : + { + "Please change product name", + "System Product Name", + "To Be Filled By O.E.M.", + "Default string", + "$(DEFAULT STRING)", + "", + "Product Name", + "Undefined", + "INVALID", + "Unknow", + "empty", + "O.E.M.", + "O.E.M", + "OEM", + "NA", + ".", + }) + if (equalAsciiNoCase(cm.model, dummyModel)) + { + cm.model.clear(); + break; + } + + for (const char* dummyVendor : + { + "OEM Manufacturer", + "System manufacturer", + "System Manufacter", + "To Be Filled By O.E.M.", + "Default string", + "$(DEFAULT STRING)", + "Undefined", + "Unknow", + "empty", + "O.E.M.", + "O.E.M", + "OEM", + "NA", + ".", + }) + if (equalAsciiNoCase(cm.vendor, dummyVendor)) + { + cm.vendor.clear(); + break; + } + + return cm; + } + catch (const SysError& e) { throw FileError(_("Cannot get process information."), e.toString()); } +} + + + + + +std::wstring zen::getOsDescription() //throw FileError +{ + try + { + const OsVersionDetail verDetail = getOsVersionDetail(); //throw SysError + return trimCpy(verDetail.osName + L" (" + verDetail.osVersionRaw) + L')'; //e.g. "CentOS (7.8.2003)" + + } + catch (const SysError& e) { throw FileError(_("Cannot get process information."), e.toString()); } +} + + + + +Zstring zen::getProcessPath() //throw FileError +{ + try + { + return getSymlinkRawContent_impl("/proc/self/exe").targetPath; //throw SysError + //path does not contain symlinks => no need for ::realpath() + + } + catch (const SysError& e) { throw FileError(_("Cannot get process information."), e.toString()); } +} + + +Zstring zen::getUserHome() //throw FileError +{ + if (::getuid() != 0) //nofail; non-root + /* https://linux.die.net/man/3/getpwuid: An application that wants to determine its user's home directory + should inspect the value of HOME (rather than the value getpwuid(getuid())->pw_dir) since this allows + the user to modify their notion of "the home directory" during a login session. */ + if (const std::optional homeDirPath = getEnvironmentVar("HOME")) + return *homeDirPath; + + //root(0) => consider as request for elevation, NOT impersonation! + //=> "HOME=/root" :( + + const Zstring loginUser = getLoginUser(); //throw FileError + + //ugh, the world's stupidest API: + std::vector buf(std::max(10000, ::sysconf(_SC_GETPW_R_SIZE_MAX))); //::sysconf may return long(-1) or even a too small size!! WTF! + passwd buf2 = {}; + passwd* pwEntry = nullptr; + if (const int rv = ::getpwnam_r(loginUser.c_str(), //const char *name + &buf2, //struct passwd* pwd + buf.data(), //char* buf + buf.size(), //size_t buflen + &pwEntry); //struct passwd** result + rv != 0 || !pwEntry) + { + //"If an error occurs, errno is set appropriately" => why the fuck, then also return errno as return value!? + errno = rv != 0 ? rv : ENOENT; + THROW_LAST_FILE_ERROR(_("Cannot get process information."), "getpwnam_r(" + utfTo(loginUser) + ')'); + } + + return pwEntry->pw_dir; //home directory +} + + +Zstring zen::getUserDataPath() //throw FileError +{ + if (::getuid() != 0) //nofail; non-root + if (const std::optional xdgCfgPath = getEnvironmentVar("XDG_CONFIG_HOME"); + xdgCfgPath&& !xdgCfgPath->empty()) + return *xdgCfgPath; + //root(0) => consider as request for elevation, NOT impersonation + + return appendPath(getUserHome(), ".config"); //throw FileError +} + + +Zstring zen::getUserDownloadsPath() //throw FileError +{ + try + { + if (::getuid() != 0) //nofail; non-root + if (const auto& [exitCode, output] = consoleExecute("xdg-user-dir DOWNLOAD", std::nullopt /*timeoutMs*/); //throw SysError + exitCode == 0) + { + const Zstring& downloadsPath = trimCpy(output); + ASSERT_SYSERROR(!downloadsPath.empty()); + return downloadsPath; + } + //root(0) => consider as request for elevation, NOT impersonation + + //fallback: probably correct 99.9% of the time anyway... + return appendPath(getUserHome(), "Downloads"); //throw FileError + } + catch (const SysError& e) { throw FileError(_("Cannot get process information."), e.toString()); } +} + + +bool zen::runningElevated() //throw FileError +{ + if (::geteuid() != 0) //nofail; non-root + return false; + + return getLoginUser() != "root"; //throw FileError + //consider "root login" like "UAC disabled" on Windows +} diff --git a/zen/sys_info.h b/zen/sys_info.h new file mode 100644 index 0000000..ed5dc1d --- /dev/null +++ b/zen/sys_info.h @@ -0,0 +1,43 @@ +// ***************************************************************************** +// * 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 + +Zstring getLoginUser(); //throw FileError +Zstring getUserDescription();//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 + + +Zstring getProcessPath(); //throw FileError + +Zstring getUserDownloadsPath(); //throw FileError +Zstring getUserDataPath(); //throw FileError + +Zstring getUserHome(); //throw FileError + +bool runningElevated(); //throw FileError +} + +#endif //SYSTEM_H_4189731847832147508915 diff --git a/zen/sys_version.cpp b/zen/sys_version.cpp new file mode 100644 index 0000000..58785a0 --- /dev/null +++ b/zen/sys_version.cpp @@ -0,0 +1,101 @@ +// ***************************************************************************** +// * 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 "sys_version.h" + #include + #include "file_io.h" + #include "process_exec.h" + +using namespace zen; + + +OsVersionDetail zen::getOsVersionDetail() //throw SysError +{ + /* prefer lsb_release: lsb_release Distributor ID: Debian + 1. terser OS name Release: 8.11 + 2. detailed version number + /etc/os-release NAME="Debian GNU/Linux" + VERSION_ID="8" */ + std::wstring osName; + std::wstring osVersion; + try + { + if (const auto [exitCode, output] = consoleExecute("lsb_release --id -s", std::nullopt); //throw SysError + exitCode != 0) + throw SysError(formatSystemError("lsb_release --id", + replaceCpy(_("Exit code %x"), L"%x", numberTo(exitCode)), utfTo(output))); + else + osName = utfTo(trimCpy(output)); + + if (const auto [exitCode, output] = consoleExecute("lsb_release --release -s", std::nullopt); //throw SysError + exitCode != 0) + throw SysError(formatSystemError("lsb_release --release", + replaceCpy(_("Exit code %x"), L"%x", numberTo(exitCode)), utfTo(output))); + else + osVersion = utfTo(trimCpy(output)); + } + //lsb_release not available on some systems: https://freefilesync.org/forum/viewtopic.php?t=7191 + catch (SysError&) // => fall back to /etc/os-release: https://www.freedesktop.org/software/systemd/man/os-release.html + { + std::string releaseInfo; + try + { + releaseInfo = getFileContent("/etc/os-release", nullptr /*notifyUnbufferedIO*/); //throw FileError + } + catch (const FileError& e) { throw SysError(replaceCpy(e.toString(), L"\n\n", L'\n')); } //errors should be further enriched by context info => SysError + + split(releaseInfo, '\n', [&](const std::string_view line) + { + if (startsWith(line, "NAME=")) + osName = utfTo(afterFirst(line, '=', IfNotFoundReturn::none)); + else if (startsWith(line, "VERSION_ID=")) + osVersion = utfTo(afterFirst(line, '=', IfNotFoundReturn::none)); + //PRETTY_NAME? too wordy! e.g. "Fedora 17 (Beefy Miracle)" + }); + trim(osName, TrimSide::both, [](const char c) { return c == L'"' || c == L'\''; }); + trim(osVersion, TrimSide::both, [](const char c) { return c == L'"' || c == L'\''; }); + } + + if (osName.empty()) + throw SysError(L"Operating system release could not be determined."); //should never happen! + //osVersion is usually available, except for Arch Linux: https://freefilesync.org/forum/viewtopic.php?t=7276 + // lsb_release Release is "rolling" + // /etc/os-release: VERSION_ID is missing, but there is BUILD_ID=rolling instead + + std::vector verDigits = splitCpy(osVersion, L'.', SplitOnEmpty::allow); //e.g. "7.7.1908" + verDigits.resize(2); + + return + { + .version + { + stringTo(verDigits[0]), + stringTo(verDigits[1]) + }, + .osVersionRaw = osVersion, + .osName = osName, + }; +} + + +OsVersion zen::getOsVersion() +{ + static const OsVersionDetail verDetail = [] + { + try + { + return getOsVersionDetail(); //throw SysError + } + catch (const SysError& e) + { + logExtraError(_("Cannot get process information.") + L"\n\n" + e.toString()); + return OsVersionDetail{}; //arrgh, it's a jungle out there: https://freefilesync.org/forum/viewtopic.php?t=7276 + } + }(); + return verDetail.version; +} + + diff --git a/zen/sys_version.h b/zen/sys_version.h new file mode 100644 index 0000000..018c353 --- /dev/null +++ b/zen/sys_version.h @@ -0,0 +1,37 @@ +// ***************************************************************************** +// * 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 SYS_VER_H_238470348254325 +#define SYS_VER_H_238470348254325 + +#include "file_error.h" + + +namespace zen +{ +struct OsVersion //keep it a POD, so that the global version constants can be used during static initialization +{ + int major = 0; + int minor = 0; + + std::strong_ordering operator<=>(const OsVersion&) const = default; +}; + + +struct OsVersionDetail +{ + OsVersion version; + std::wstring osVersionRaw; + std::wstring osName; +}; +OsVersionDetail getOsVersionDetail(); //throw SysError + +OsVersion getOsVersion(); + + +} + +#endif //SYS_VER_H_238470348254325 diff --git a/zen/thread.cpp b/zen/thread.cpp new file mode 100644 index 0000000..e14afac --- /dev/null +++ b/zen/thread.cpp @@ -0,0 +1,37 @@ +// ***************************************************************************** +// * 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 "thread.h" + #include + +using namespace zen; + + + + +void zen::setCurrentThreadName(const Zstring& threadName) +{ + ::prctl(PR_SET_NAME, threadName.c_str(), 0, 0, 0); + +} + + +namespace +{ +//don't make this a function-scope static (avoid code-gen for "magic static") +const std::thread::id globalMainThreadId = std::this_thread::get_id(); +} + + +bool zen::runningOnMainThread() +{ + if (globalMainThreadId == std::thread::id()) //if called during static initialization! + return true; + + return std::this_thread::get_id() == globalMainThreadId; +} + + diff --git a/zen/thread.h b/zen/thread.h new file mode 100644 index 0000000..64a3518 --- /dev/null +++ b/zen/thread.h @@ -0,0 +1,528 @@ +// ***************************************************************************** +// * 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 THREAD_H_7896323423432235246427 +#define THREAD_H_7896323423432235246427 + +#include +#include +#include +#include "ring_buffer.h" +#include "zstring.h" + + +namespace zen +{ +class InterruptionStatus; + +//migrate towards https://en.cppreference.com/w/cpp/thread/jthread +class InterruptibleThread +{ +public: + InterruptibleThread() {} + InterruptibleThread (InterruptibleThread&& ) noexcept = default; + InterruptibleThread& operator=(InterruptibleThread&& tmp) noexcept //don't use swap() but end stdThread_ life time immediately + { + if (joinable()) + { + requestStop(); + join(); + } + stdThread_ = std::move(tmp.stdThread_); + intStatus_ = std::move(tmp.intStatus_); + return *this; + } + + template + explicit InterruptibleThread(Function&& f); + + ~InterruptibleThread() + { + if (joinable()) + { + requestStop(); + join(); + } + } + + bool joinable () const { return stdThread_.joinable(); } + void requestStop(); + void join () { stdThread_.join(); } + void detach () { stdThread_.detach(); } + +private: + std::thread stdThread_; + std::shared_ptr intStatus_ = std::make_shared(); +}; + + +class ThreadStopRequest {}; + +//context of worker thread: +void interruptionPoint(); //throw ThreadStopRequest + +template +void interruptibleWait(std::condition_variable& cv, std::unique_lock& lock, Predicate pred); //throw ThreadStopRequest + +template +void interruptibleSleep(const std::chrono::duration& relTime); //throw ThreadStopRequest + +void setCurrentThreadName(const Zstring& threadName); + + +bool runningOnMainThread(); + +//------------------------------------------------------------------------------------------ + +/* std::async replacement without crappy semantics: + 1. guaranteed to run asynchronously + 2. does not follow C++11 [futures.async], Paragraph 5, where std::future waits for thread in destructor + + Example: + Zstring dirPath = ... + auto ft = zen::runAsync([=]{ return zen::dirExists(dirPath); }); + if (ft.wait_for(std::chrono::milliseconds(200)) == std::future_status::ready && ft.get()) + //dir existing */ +template +auto runAsync(Function&& fun); + +//wait for all with a time limit: return true if *all* results are available! +//TODO: use std::when_all when available +template +bool waitForAllTimed(InputIterator first, InputIterator last, const Duration& wait_duration); + +template inline +bool isReady(const std::future& f) { assert(f.valid()); return f.wait_for(std::chrono::seconds(0)) == std::future_status::ready; } +//------------------------------------------------------------------------------------------ + +//wait until first job is successful or all failed +//TODO: use std::when_any when available +template +class AsyncFirstResult +{ +public: + AsyncFirstResult(); + + template + void addJob(Fun&& f); //f must return a std::optional containing a value if successful + + template + bool timedWait(const Duration& duration) const; //true: "get()" is ready, false: time elapsed + + //return first value or none if all jobs failed; blocks until result is ready! + std::optional get() const; //may be called only once! + +private: + class AsyncResult; + std::shared_ptr asyncResult_; + size_t jobsTotal_ = 0; +}; + +//------------------------------------------------------------------------------------------ + +//value associated with mutex and guaranteed protected access: +//TODO: use std::synchronized_value when available https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rconc-mutex +template +class Protected +{ +public: + Protected() {} + explicit Protected(T& value) : value_(value) {} + //Protected(T&& tmp ) : value_(std::move(tmp)) {} <- wait until needed + + template + auto access(Function fun) //-> decltype(fun(std::declval())) + { + std::lock_guard dummy(lockValue_); + return fun(value_); + } + +private: + Protected (const Protected&) = delete; + Protected& operator=(const Protected&) = delete; + + std::mutex lockValue_; + T value_{}; +}; + +//------------------------------------------------------------------------------------------ + +template +class ThreadGroup +{ +public: + ThreadGroup(size_t threadCountMax, const Zstring& groupName) : threadCountMax_(threadCountMax), groupName_(groupName) + { if (threadCountMax == 0) throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); } + + ThreadGroup (ThreadGroup&& tmp) noexcept = default; //noexcept *required* to support move for reallocations in std::vector and std::swap!!! + ThreadGroup& operator=(ThreadGroup&& tmp) noexcept = default; //don't use swap() but end worker_ life time immediately + + ~ThreadGroup() + { + for (InterruptibleThread& w : worker_) + w.requestStop(); //similar, but not the same as ~InterruptibleThread: stop *all* at the same time before join! + + if (detach_) //detach() without requestStop() doesn't make sense + for (InterruptibleThread& w : worker_) + w.detach(); + } + + //context of controlling OR worker thread, non-blocking: + void run(Function&& wi /*should throw ThreadStopRequest when needed*/, bool insertFront = false) + { + { + std::lock_guard dummy(workLoad_.ref().lock); + + if (insertFront) + workLoad_.ref().tasks.push_front(std::move(wi)); + else + workLoad_.ref().tasks.push_back(std::move(wi)); + const size_t tasksPending = ++(workLoad_.ref().tasksPending); + + if (worker_.size() < std::min(tasksPending, threadCountMax_)) + addWorkerThread(); + } + workLoad_.ref().conditionNewTask.notify_all(); + } + + //context of controlling thread, blocking: + void wait() + { + //perf: no difference in xBRZ test case compared to std::condition_variable-based implementation + auto promDone = std::make_shared>(); // + std::future futDone = promDone->get_future(); + + notifyWhenDone([promDone] { promDone->set_value(); }); //std::function doesn't support construction involving move-only types! + //use reference? => potential lifetime issue, e.g. promise object theoretically might be accessed inside set_value() after future gets signalled + + futDone.get(); + } + + //non-blocking wait()-alternative: context of controlling thread: + void notifyWhenDone(const std::function& onCompletion /*noexcept! runs on worker thread!*/) + { + std::unique_lock dummy(workLoad_.ref().lock); + + if (workLoad_.ref().tasksPending == 0) + { + dummy.unlock(); + onCompletion(); + } + else + workLoad_.ref().onCompletionCallbacks.push_back(onCompletion); + } + + //context of controlling thread: + void detach() { detach_ = true; } //not expected to also interrupt! + +private: + ThreadGroup (const ThreadGroup&) = delete; + ThreadGroup& operator=(const ThreadGroup&) = delete; + + void addWorkerThread() + { + Zstring threadName = groupName_ + Zstr('[') + numberTo(worker_.size() + 1) + Zstr('/') + numberTo(threadCountMax_) + Zstr(']'); + + worker_.emplace_back([workLoad_ /*clang bug*/= workLoad_ /*share ownership!*/, threadName = std::move(threadName)]() mutable //don't capture "this"! consider detach() and move operations + { + setCurrentThreadName(threadName); + WorkLoad& workLoad = workLoad_.ref(); + + std::unique_lock dummy(workLoad.lock); + for (;;) + { + interruptibleWait(workLoad.conditionNewTask, dummy, [&tasks = workLoad.tasks] { return !tasks.empty(); }); //throw ThreadStopRequest + + Function task = std::move(workLoad.tasks. front()); //noexcept thanks to move + /**/ workLoad.tasks.pop_front(); // + + dummy.unlock(); + task(); //throw ThreadStopRequest? + dummy.lock(); + + if (--(workLoad.tasksPending) == 0) + if (!workLoad.onCompletionCallbacks.empty()) + { + std::vector> callbacks = std::exchange(workLoad.onCompletionCallbacks, {}); + + dummy.unlock(); + for (const auto& cb : callbacks) + cb(); //noexcept! + dummy.lock(); + } + } + }); + } + + struct WorkLoad + { + std::mutex lock; + RingBuffer tasks; //FIFO! :) + size_t tasksPending = 0; + std::condition_variable conditionNewTask; + std::vector> onCompletionCallbacks; + }; + + std::vector worker_; + SharedRef workLoad_ = makeSharedRef(); + bool detach_ = false; + size_t threadCountMax_; + Zstring groupName_; +}; + + + + + + + + +//###################### implementation ###################### + +namespace impl +{ +template inline +auto runAsync(Function&& fun, std::true_type /*copy-constructible*/) +{ + using ResultType = decltype(fun()); + + //note: std::packaged_task does NOT support move-only function objects! + std::packaged_task pt(std::forward(fun)); + auto fut = pt.get_future(); + std::thread(std::move(pt)).detach(); //we have to explicitly detach since C++11: [thread.thread.destr] ~thread() calls std::terminate() if joinable()!!! + return fut; +} + + +template inline +auto runAsync(Function&& fun, std::false_type /*copy-constructible*/) +{ + //support move-only function objects! + auto sharedFun = std::make_shared(std::forward(fun)); + return runAsync([sharedFun] { return (*sharedFun)(); }, std::true_type()); +} +} + + +template inline +auto runAsync(Function&& fun) +{ + return impl::runAsync(std::forward(fun), std::is_copy_constructible()); +} + + +template inline +bool waitForAllTimed(InputIterator first, InputIterator last, const Duration& duration) +{ + const std::chrono::steady_clock::time_point stopTime = std::chrono::steady_clock::now() + duration; + for (; first != last; ++first) + if (first->wait_until(stopTime) == std::future_status::timeout) + return false; + return true; +} + + +template +class AsyncFirstResult::AsyncResult +{ +public: + //context: worker threads + void reportFinished(std::optional&& result) + { + { + std::lock_guard dummy(lockResult_); + ++jobsFinished_; + if (!result_) + result_ = std::move(result); + } + conditionJobDone_.notify_all(); //better notify all, considering bugs like: https://svn.boost.org/trac/boost/ticket/7796 + } + + //context: main thread + template + bool waitForResult(size_t jobsTotal, const Duration& duration) + { + std::unique_lock dummy(lockResult_); + return conditionJobDone_.wait_for(dummy, duration, [&] { return this->jobDone(jobsTotal); }); + } + + std::optional getResult(size_t jobsTotal) + { + std::unique_lock dummy(lockResult_); + conditionJobDone_.wait(dummy, [&] { return this->jobDone(jobsTotal); }); + + return std::move(result_); + } + +private: + bool jobDone(size_t jobsTotal) const { return result_ || (jobsFinished_ >= jobsTotal); } //call while locked! + + std::mutex lockResult_; + size_t jobsFinished_ = 0; // + std::optional result_; //our condition is: "have result" or "jobsFinished_ == jobsTotal" + std::condition_variable conditionJobDone_; +}; + + + +template inline +AsyncFirstResult::AsyncFirstResult() : asyncResult_(std::make_shared()) {} + + +template +template inline +void AsyncFirstResult::addJob(Fun&& f) //f must return a std::optional containing a value on success +{ + std::thread t([asyncResult = this->asyncResult_, f = std::forward(f)] { asyncResult->reportFinished(f()); }); + ++jobsTotal_; + t.detach(); //we have to be explicit since C++11: [thread.thread.destr] ~thread() calls std::terminate() if joinable()!!! +} + + +template +template inline +bool AsyncFirstResult::timedWait(const Duration& duration) const { return asyncResult_->waitForResult(jobsTotal_, duration); } + + +template inline +std::optional AsyncFirstResult::get() const { return asyncResult_->getResult(jobsTotal_); } + +//------------------------------------------------------------------------------------------ + +class InterruptionStatus +{ +public: + //context of InterruptibleThread instance: + void requestStop() + { + stopRequested_ = true; + + { + std::lock_guard dummy(lockSleep_); //needed! makes sure the following signal is not lost! + //usually we'd make "interrupted" non-atomic, but this is already given due to interruptibleWait() handling + } + conditionSleepInterruption_.notify_all(); + + std::lock_guard dummy(lockConditionPtr_); + if (activeCondition_) + activeCondition_->notify_all(); //signal may get lost! + //alternative design locking the cv's mutex here could be dangerous: potential for dead lock! + } + + //context of worker thread: + void throwIfStopped() //throw ThreadStopRequest + { + if (stopRequested_) + throw ThreadStopRequest(); + } + + //context of worker thread: + template + void interruptibleWait(std::condition_variable& cv, std::unique_lock& lock, Predicate pred) //throw ThreadStopRequest + { + setConditionVar(&cv); + ZEN_ON_SCOPE_EXIT(setConditionVar(nullptr)); + + //"stopRequested_" is not protected by cv's mutex => signal may get lost!!! e.g. after condition was checked but before the wait begins + //=> add artifical time out to mitigate! CPU: 0.25% vs 0% for longer time out! + while (!cv.wait_for(lock, std::chrono::milliseconds(1), [&] { return this->stopRequested_ || pred(); })) + ; + + throwIfStopped(); //throw ThreadStopRequest + } + + //context of worker thread: + template + void interruptibleSleep(const std::chrono::duration& relTime) //throw ThreadStopRequest + { + std::unique_lock lock(lockSleep_); + if (conditionSleepInterruption_.wait_for(lock, relTime, [this] { return static_cast(this->stopRequested_); })) + throw ThreadStopRequest(); + } + +private: + void setConditionVar(std::condition_variable* cv) + { + std::lock_guard dummy(lockConditionPtr_); + activeCondition_ = cv; + } + + std::atomic stopRequested_{false}; //std::atomic is uninitialized by default!!! + //"The default constructor is trivial: no initialization takes place other than zero initialization of static and thread-local objects." + + std::condition_variable* activeCondition_ = nullptr; + std::mutex lockConditionPtr_; //serialize pointer access (only!) + + std::condition_variable conditionSleepInterruption_; + std::mutex lockSleep_; +}; + + +namespace impl +{ +//thread_local with non-POD seems to cause memory leaks on VS 14 => pointer only is fine: +inline thread_local InterruptionStatus* threadLocalInterruptionStatus = nullptr; +} + + +//context of worker thread: +inline +void interruptionPoint() //throw ThreadStopRequest +{ + assert(impl::threadLocalInterruptionStatus); + if (impl::threadLocalInterruptionStatus) + impl::threadLocalInterruptionStatus->throwIfStopped(); //throw ThreadStopRequest +} + + +//context of worker thread: +template inline +void interruptibleWait(std::condition_variable& cv, std::unique_lock& lock, Predicate pred) //throw ThreadStopRequest +{ + assert(impl::threadLocalInterruptionStatus); + if (impl::threadLocalInterruptionStatus) + impl::threadLocalInterruptionStatus->interruptibleWait(cv, lock, pred); + else + cv.wait(lock, pred); +} + + +//context of worker thread: +template inline +void interruptibleSleep(const std::chrono::duration& relTime) //throw ThreadStopRequest +{ + assert(impl::threadLocalInterruptionStatus); + if (impl::threadLocalInterruptionStatus) + impl::threadLocalInterruptionStatus->interruptibleSleep(relTime); + else + std::this_thread::sleep_for(relTime); +} + + +template inline +InterruptibleThread::InterruptibleThread(Function&& f) +{ + stdThread_ = std::thread([f = std::forward(f), + intStatus = this->intStatus_]() mutable + { + assert(!impl::threadLocalInterruptionStatus); + impl::threadLocalInterruptionStatus = intStatus.get(); + ZEN_ON_SCOPE_EXIT(impl::threadLocalInterruptionStatus = nullptr); + + try + { + f(); //throw ThreadStopRequest + } + catch (ThreadStopRequest&) {} + }); +} + + +inline +void InterruptibleThread::requestStop() { intStatus_->requestStop(); } +} + +#endif //THREAD_H_7896323423432235246427 diff --git a/zen/time.h b/zen/time.h new file mode 100644 index 0000000..11bfeb1 --- /dev/null +++ b/zen/time.h @@ -0,0 +1,421 @@ +// ***************************************************************************** +// * 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 TIME_H_8457092814324342453627 +#define TIME_H_8457092814324342453627 + +#include +#include "basic_math.h" +#include "zstring.h" + + +namespace zen +{ +struct TimeComp //replaces std::tm and SYSTEMTIME +{ + int year = 0; // - + int month = 0; //1-12 + int day = 0; //1-31 + int hour = 0; //0-23 + int minute = 0; //0-59 + int second = 0; //0-60 (including leap second) + + bool operator==(const TimeComp&) const = default; +}; + +TimeComp getUtcTime(time_t utc); //convert time_t (UTC) to UTC time components, returns TimeComp() on error +TimeComp getUtcTime(); //utc = std::time() +std::pair utcToTimeT(const TimeComp& tc); //convert UTC time components to time_t (UTC) + +TimeComp getLocalTime(time_t utc); //convert time_t (UTC) to local time components, returns TimeComp() on error +TimeComp getLocalTime(); //utc = std::time() +std::pair localToTimeT(const TimeComp& tc); //convert local time components to time_t (UTC) + +TimeComp getCompileTime(); //returns TimeComp() on error + +//---------------------------------------------------------------------------------------------------------------------------------- +/* format (current) date and time; example: + formatTime(Zstr("%Y|%m|%d")); -> "2011|10|29" + formatTime(formatDateTag); -> "2011-10-29" + formatTime(formatTimeTag); -> "17:55:34" */ +Zstring formatTime(const Zchar* format, const TimeComp& tc = getLocalTime()); //format as specified by "std::strftime", returns empty string on error + +//the "format" parameter of formatTime() is partially specialized with the following type tags: +const Zchar* const formatDateTag = Zstr("%x"); //locale-dependent date representation: e.g. 8/23/2001 +const Zchar* const formatTimeTag = Zstr("%X"); //locale-dependent time representation: e.g. 2:55:02 PM +const Zchar* const formatDateTimeTag = Zstr("%c"); //locale-dependent date and time: e.g. 8/23/2001 2:55:02 PM + +const Zchar* const formatIsoDateTag = Zstr("%Y-%m-%d"); //e.g. 2001-08-23 +const Zchar* const formatIsoTimeTag = Zstr("%H:%M:%S"); //e.g. 14:55:02 +const Zchar* const formatIsoDateTimeTag = Zstr("%Y-%m-%d %H:%M:%S"); //e.g. 2001-08-23 14:55:02 + +//---------------------------------------------------------------------------------------------------------------------------------- +//example: parseTime("%Y-%m-%d %H:%M:%S", "2001-08-23 14:55:02"); +// parseTime(formatIsoDateTimeTag, "2001-08-23 14:55:02"); +template +TimeComp parseTime(const String& format, const String2& str); //similar to ::strptime() +//---------------------------------------------------------------------------------------------------------------------------------- + +//format: [-][[d.]HH:]MM:SS e.g. -1.23:45:67 +Zstring formatTimeSpan(int64_t timeInSec, bool hourRequired = true); + + + + + + + + + + + +//############################ implementation ############################## +namespace impl +{ +inline +std::tm toClibTimeComponents(const TimeComp& tc) +{ + assert(1 <= tc.month && tc.month <= 12 && + 1 <= tc.day && tc.day <= 31 && + 0 <= tc.hour && tc.hour <= 23 && + 0 <= tc.minute && tc.minute <= 59 && + 0 <= tc.second && tc.second <= 61); + + return + { + .tm_sec = tc.second, //0-60 (including leap second) + .tm_min = tc.minute, //0-59 + .tm_hour = tc.hour, //0-23 + .tm_mday = tc.day, //1-31 + .tm_mon = tc.month - 1, //0-11 + .tm_year = tc.year - 1900, //years since 1900 + .tm_isdst = -1, //> 0 if DST is active, == 0 if DST is not active, < 0 if the information is not available + //.tm_wday + //.tm_yday + }; +} + +inline +TimeComp toZenTimeComponents(const std::tm& ctc) +{ + return + { + .year = ctc.tm_year + 1900, + .month = ctc.tm_mon + 1, + .day = ctc.tm_mday, + .hour = ctc.tm_hour, + .minute = ctc.tm_min, + .second = ctc.tm_sec, + }; +} + + +/* +inline +bool isValid(const std::tm& t) +{ + -> not enough! MSCRT has different limits than the C standard which even seem to change with different versions: + _VALIDATE_RETURN((( timeptr->tm_sec >=0 ) && ( timeptr->tm_sec <= 59 ) ), EINVAL, FALSE) + _VALIDATE_RETURN(( timeptr->tm_year >= -1900 ) && ( timeptr->tm_year <= 8099 ), EINVAL, FALSE) + -> also std::mktime does *not* help here at all! + + auto inRange = [](int value, int minVal, int maxVal) { return minVal <= value && value <= maxVal; }; + + //https://www.cplusplus.com/reference/clibrary/ctime/tm/ + return inRange(t.tm_sec, 0, 61) && + inRange(t.tm_min, 0, 59) && + inRange(t.tm_hour, 0, 23) && + inRange(t.tm_mday, 1, 31) && + inRange(t.tm_mon, 0, 11) && + //tm_year + inRange(t.tm_wday, 0, 6) && + inRange(t.tm_yday, 0, 365); + //tm_isdst +}; +*/ +} + + +constexpr auto daysPer400Years = 100 * (4 * 365 /*usual days per year*/ + 1 /*including leap day*/) - 3 /*no leap days for centuries, except if divisible by 400 */; +constexpr auto secsPer400Years = 3600LL * 24 * daysPer400Years; + + +inline +TimeComp getUtcTime(time_t utc) +{ + //Windows: gmtime_s() only works for years [1970, 3001] + //=> map into working 400-year range [1970, 2370) + // bonus: avoid asking for bugs for time_t(-1) + const int cycles400 = static_cast(numeric::intDivFloor(utc, secsPer400Years)); + utc -= secsPer400Years * cycles400; + + std::tm ctc = {}; + if (::gmtime_r(&utc, &ctc) == nullptr) //Linux, macOS: apparently NO limits (tested years 0 to 10.000!) + return TimeComp(); + + ctc.tm_year += 400 * cycles400; + + return impl::toZenTimeComponents(ctc); +} + + +inline +TimeComp getUtcTime() +{ + const time_t utc = std::time(nullptr); //returns -1 on error + if (utc == -1) + return TimeComp(); + + return getUtcTime(utc); +} + + +inline +TimeComp getLocalTime(time_t utc) +{ + const int cycles400 = static_cast(numeric::intDivFloor(utc, secsPer400Years)); + utc -= secsPer400Years * cycles400; + + std::tm ctc = {}; + if (::localtime_r(&utc, &ctc) == nullptr) + return TimeComp(); + + ctc.tm_year += 400 * cycles400; + + return impl::toZenTimeComponents(ctc); +} + + +inline +TimeComp getLocalTime() +{ + const time_t utc = std::time(nullptr); //returns -1 on error + if (utc == -1) + return TimeComp(); + + return getLocalTime(utc); +} + + +inline +std::pair utcToTimeT(const TimeComp& tc) +{ + if (tc == TimeComp()) + return {}; + + std::tm ctc = impl::toClibTimeComponents(tc); + ctc.tm_isdst = 0; //"Zero (0) to indicate that standard time is in effect" => unused by _mkgmtime, but take no chances + + /* Windows: _mkgmtime() only works for years [1970, 3001] + macOS: timegm() requires tm_year >= 1900; apparently no upper limit (tested until year 10.000!) + Linux, 64-bit: apparently NO limits (tested years 0 to 10.000!) + 32-bit: timegm() only works for years [1902, 2038] => sucks to be on 32-bit! :> + + => map into working 400-year range [1970, 2370) + bonus: disambiguate -1 error code from time_t(-1) */ + const int cycles400 = numeric::intDivFloor(ctc.tm_year + 1900 - 1970, 400); + ctc.tm_year -= 400 * cycles400; + + const time_t utc = ::timegm(&ctc); + if (utc == -1) + return {}; + + assert(utc >= 0); + return {utc + secsPer400Years * cycles400, true}; +} + + +inline +std::pair localToTimeT(const TimeComp& tc) //convert local time components to time_t (UTC) +{ + if (tc == TimeComp()) + return {}; + + std::tm ctc = impl::toClibTimeComponents(tc); + + const int cycles400 = numeric::intDivFloor(ctc.tm_year + 1900 - 1971/*[!]*/, 400); //see utcToTimeT() + //1971: ensures resulting time_t >= 0 after time zone, DST adaption, or std::mktime will fail on Windows! + ctc.tm_year -= 400 * cycles400; + + const time_t locTime = std::mktime(&ctc); + if (locTime == -1) + return {}; + + assert(locTime > 0); + return {locTime + secsPer400Years * cycles400, true}; +} + + +inline +TimeComp getCompileTime() +{ + //https://gcc.gnu.org/onlinedocs/cpp/Standard-Predefined-Macros.html + char compileTime[] = __DATE__ " " __TIME__; //e.g. "Aug 1 2017 01:32:26" + if (compileTime[4] == ' ') //day is space-padded, but %d expects zero-padding + compileTime[4] = '0'; + + return parseTime("%b %d %Y %H:%M:%S", compileTime); +} + + + + +inline +Zstring formatTime(const Zchar* format, const TimeComp& tc) +{ + if (tc == TimeComp()) //failure code from getLocalTime() + return Zstring(); + + std::tm ctc = impl::toClibTimeComponents(tc); + std::mktime(&ctc); //unfortunately std::strftime() needs all elements of "struct tm" filled, e.g. tm_wday, tm_yday + //note: although std::mktime() explicitly expects "local time", calculating weekday and day of year *should* be time-zone and DST independent + + Zstring buf(256, Zstr('\0')); + //strftime() craziness on invalid input: + // VS 2010: CRASH unless "_invalid_parameter_handler" is set: https://docs.microsoft.com/en-us/cpp/c-runtime-library/parameter-validation + // GCC: returns 0, apparently no crash. Still, considering some clib maintainer's comments, we should expect the worst! + // Windows: avoid char-based strftime() which uses ANSI encoding! (e.g. Greek letters for AM/PM) + const size_t charsWritten = std::strftime(buf.data(), buf.size(), format, &ctc); + buf.resize(charsWritten); + return buf; +} + + +template +TimeComp parseTime(const String& format, const String2& str) +{ + using CharType = GetCharTypeT; + static_assert(std::is_same_v>); + + const CharType* itStr = strBegin(str); + const CharType* const strLast = itStr + strLength(str); + + auto extractNumber = [&](int& result, size_t digitCount) + { + if (strLast - itStr < makeSigned(digitCount)) + return false; + + if (!std::all_of(itStr, itStr + digitCount, isDigit)) + return false; + + result = zen::stringTo(makeStringView(itStr, digitCount)); + itStr += digitCount; + return true; + }; + + TimeComp output; + + const CharType* itFmt = strBegin(format); + const CharType* const fmtLast = itFmt + strLength(format); + + for (; itFmt != fmtLast; ++itFmt) + { + const CharType fmt = *itFmt; + + if (fmt == '%') + { + ++itFmt; + if (itFmt == fmtLast) + return TimeComp(); + + switch (*itFmt) + { + case 'Y': + if (!extractNumber(output.year, 4)) + return TimeComp(); + break; + case 'm': + if (!extractNumber(output.month, 2)) + return TimeComp(); + break; + case 'b': //abbreviated month name: Jan-Dec + { + if (strLast - itStr < 3) + return TimeComp(); + + const char* months[] = {"jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"}; + auto itMonth = std::find_if(std::begin(months), std::end(months), [&](const char* month) + { + return equalAsciiNoCase(makeStringView(itStr, 3), month); + }); + if (itMonth == std::end(months)) + return TimeComp(); + + output.month = 1 + static_cast(itMonth - std::begin(months)); + itStr += 3; + } + break; + case 'd': + if (!extractNumber(output.day, 2)) + return TimeComp(); + break; + case 'H': + if (!extractNumber(output.hour, 2)) + return TimeComp(); + break; + case 'M': + if (!extractNumber(output.minute, 2)) + return TimeComp(); + break; + case 'S': + if (!extractNumber(output.second, 2)) + return TimeComp(); + break; + default: + return TimeComp(); + } + } + else if (isWhiteSpace(fmt)) //single whitespace in format => skip 0..n whitespace chars + { + while (itStr != strLast && isWhiteSpace(*itStr)) + ++itStr; + } + else + { + if (itStr == strLast || *itStr != fmt) + return TimeComp(); + ++itStr; + } + } + + if (itStr != strLast) + return TimeComp(); + + return output; +} + + +inline +Zstring formatTimeSpan(int64_t timeInSec, bool hourRequired) +{ + Zstring timespanStr; + + if (timeInSec < 0) + { + timeInSec = -timeInSec; //need to fix LLONG_MIN? + timespanStr = Zstr('-'); + } + + //check *before* subtracting days! + const Zchar* timeSpanFmt = timeInSec < 3600 && !hourRequired ? Zstr("%M:%S") : formatIsoTimeTag; + + const int secsPerDay = 24 * 3600; + const int64_t days = numeric::intDivFloor(timeInSec, secsPerDay); + if (days > 0) + { + timeInSec -= days * secsPerDay; + timespanStr += numberTo(days) + Zstr("."); //don't need zen::formatNumber(), do we? + } + + //format time span as if absolute UTC time + const TimeComp& tc = getUtcTime(timeInSec); //returns TimeComp() on error + timespanStr += formatTime(timeSpanFmt, tc); //returns empty string on error + + return timespanStr; +} +} + +#endif //TIME_H_8457092814324342453627 diff --git a/zen/type_traits.h b/zen/type_traits.h new file mode 100644 index 0000000..e373ba0 --- /dev/null +++ b/zen/type_traits.h @@ -0,0 +1,198 @@ +// ***************************************************************************** +// * 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 TYPE_TRAITS_H_3425628658765467 +#define TYPE_TRAITS_H_3425628658765467 + +#include +#include + +//https://en.cppreference.com/w/cpp/header/type_traits + +namespace zen +{ +template +struct GetFirstOf +{ + using Type = T; +}; +template using GetFirstOfT = typename GetFirstOf::Type; + + +template +class FunctionReturnType +{ + template static R dummyFun(R(*)(Args...)); +public: + using Type = decltype(dummyFun(F())); +}; +template using FunctionReturnTypeT = typename FunctionReturnType::Type; +//yes, there's std::invoke_result_t, but it requires to specify function argument types for no good reason + +//============================================================================= + +template +constexpr uint32_t arrayHash(T (&arr)[N]) //don't bother making FNV1aHash constexpr instead +{ + uint32_t hashVal = 2166136261U; //FNV-1a base + + std::for_each(&arr[0], &arr[N], [&hashVal](T n) + { + //static_assert(isInteger || std::is_same_v || std::is_same_v); + static_assert(sizeof(T) <= sizeof(hashVal)); + hashVal ^= static_cast(n); + hashVal *= 16777619U; //prime + }); + return hashVal; +} + +//Herb Sutter's signedness conversion helpers: https://herbsutter.com/2013/06/13/gotw-93-solution-auto-variables-part-2/ +template inline auto makeSigned (T t) { return static_cast>(t); } +template inline auto makeUnsigned(T t) { return static_cast>(t); } + +//################# Built-in Types ######################## +//unfortunate standardized nonsense: std::is_integral<> includes bool, char, wchar_t! => roll our own: +template constexpr bool isUnsignedInt = std::is_same_v, unsigned char> || + std::is_same_v, unsigned short int> || + std::is_same_v, unsigned int> || + std::is_same_v, unsigned long int> || + std::is_same_v, unsigned long long int>; + +template constexpr bool isSignedInt = std::is_same_v, signed char> || + std::is_same_v, short int> || + std::is_same_v, int> || + std::is_same_v, long int> || + std::is_same_v, long long int>; + +template constexpr bool isInteger = isUnsignedInt || isSignedInt; +template constexpr bool isFloat = std::is_floating_point_v; +template constexpr bool isArithmetic = isInteger || isFloat; + +//################# Class Members ######################## + +/* Detect data or function members of a class by name: ZEN_INIT_DETECT_MEMBER + hasMember_ + Example: 1. ZEN_INIT_DETECT_MEMBER(c_str); + 2. hasMember_c_str -> use boolean + + + Detect data or function members of a class by name *and* type: ZEN_INIT_DETECT_MEMBER2 + HasMember_ + + Example: 1. ZEN_INIT_DETECT_MEMBER2(size, size_t (T::*)() const); + 2. hasMember_size::value -> use as boolean + + + Detect member type of a class: ZEN_INIT_DETECT_MEMBER_TYPE + hasMemberType_ + + Example: 1. ZEN_INIT_DETECT_MEMBER_TYPE(value_type); + 2. hasMemberType_value_type -> use as boolean */ + +//########## Sorting ############################## +/* +Generate a descending binary predicate at compile time! + +Usage: + static const bool ascending = ... + makeSortDirection(old binary predicate, std::bool_constant()) -> new binary predicate +*/ + +template +struct LessDescending +{ + LessDescending(Predicate lessThan) : lessThan_(std::move(lessThan)) {} + template bool operator()(const T& lhs, const T& rhs) const { return lessThan_(rhs, lhs); } +private: + Predicate lessThan_; +}; + +template inline +/**/ Predicate makeSortDirection(Predicate pred, std::true_type) { return pred; } + +template inline +LessDescending makeSortDirection(Predicate pred, std::false_type) { return pred; } + + + + + + + +//################ implementation ###################### +#define ZEN_INIT_DETECT_MEMBER(NAME) \ + \ + template \ + struct HasMemberImpl_##NAME \ + { \ + private: \ + using Yes = char[1]; \ + using No = char[2]; \ + \ + template \ + class Helper {}; \ + \ + struct Fallback { int NAME; }; \ + \ + template \ + struct Helper2 : public U, public Fallback {}; /*this works only for class types!!!*/ \ + \ + template static No& hasMember(Helper::NAME>*); \ + template static Yes& hasMember(...); \ + public: \ + enum { value = sizeof(hasMember(nullptr)) == sizeof(Yes) }; \ + }; \ + \ + template \ + struct HasMemberImpl_##NAME : std::false_type {}; \ + \ + template constexpr bool hasMember_##NAME = HasMemberImpl_##NAME, T>::value; + +//#################################################################### + +#define ZEN_INIT_DETECT_MEMBER2(NAME, TYPE) \ + \ + template \ + class HasMember_##NAME \ + { \ + using Yes = char[1]; \ + using No = char[2]; \ + \ + template class Helper {}; \ + \ + template static Yes& hasMember(Helper*); \ + template static No& hasMember(...); \ + public: \ + enum { value = sizeof(hasMember(nullptr)) == sizeof(Yes) }; \ + }; \ + \ + template constexpr bool hasMember_##NAME = HasMember_##NAME::value; + +//#################################################################### + +#define ZEN_INIT_DETECT_MEMBER_TYPE(TYPENAME) \ + \ + template \ + class HasMemberType_##TYPENAME \ + { \ + using Yes = char[1]; \ + using No = char[2]; \ + \ + template class Helper {}; \ + \ + template static Yes& hasMemberType(Helper*); \ + template static No& hasMemberType(...); \ + public: \ + enum { value = sizeof(hasMemberType(nullptr)) == sizeof(Yes) }; \ + }; \ + \ + template constexpr bool hasMemberType_##TYPENAME = HasMemberType_##TYPENAME::value; +} + + +//--------------------------------------------------------------------------- +//ZEN macro consistency checks: => place in most-used header! + + + +#endif //TYPE_TRAITS_H_3425628658765467 diff --git a/zen/utf.h b/zen/utf.h new file mode 100644 index 0000000..0872fc8 --- /dev/null +++ b/zen/utf.h @@ -0,0 +1,369 @@ +// ***************************************************************************** +// * 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 UTF_H_01832479146991573473545 +#define UTF_H_01832479146991573473545 + +#include "string_tools.h" //copyStringTo + + +namespace zen +{ +//convert all(!) char- and wchar_t-based "string-like" objects applying UTF conversions (but only if necessary!) +template +TargetString utfTo(const SourceString& str); + +constexpr std::string_view BYTE_ORDER_MARK_UTF8 = "\xEF\xBB\xBF"; + +template +bool isValidUtf(const UtfString& str); //check for UTF-8 encoding errors + +//access unicode characters in UTF-encoded string (char- or wchar_t-based) +template +size_t unicodeLength(const UtfString& str); //return number of code points for UTF-encoded string + +template +UtfStringOut getUnicodeSubstring(const UtfStringIn& str, size_t uniPosFirst, size_t uniPosLast); + + + + + + + + + +//----------------------- implementation ---------------------------------- +namespace impl +{ +using CodePoint = uint32_t; +using Char16 = uint16_t; +using Char8 = uint8_t; + +const CodePoint LEAD_SURROGATE = 0xd800; //1101 1000 0000 0000 LEAD_SURROGATE_MAX = TRAIL_SURROGATE - 1 +const CodePoint TRAIL_SURROGATE = 0xdc00; //1101 1100 0000 0000 +const CodePoint TRAIL_SURROGATE_MAX = 0xdfff; + +const CodePoint REPLACEMENT_CHAR = 0xfffd; +const CodePoint CODE_POINT_MAX = 0x10ffff; + +static_assert(LEAD_SURROGATE + TRAIL_SURROGATE + TRAIL_SURROGATE_MAX + REPLACEMENT_CHAR + CODE_POINT_MAX == 1348603); + + +template inline +void codePointToUtf16(CodePoint cp, Function writeOutput) //"writeOutput" is a unary function taking a Char16 +{ + //https://en.wikipedia.org/wiki/UTF-16 + if (cp < LEAD_SURROGATE) + writeOutput(static_cast(cp)); + else if (cp <= TRAIL_SURROGATE_MAX) //invalid code point + writeOutput(static_cast(REPLACEMENT_CHAR)); + else if (cp <= 0xffff) + writeOutput(static_cast(cp)); + else if (cp <= CODE_POINT_MAX) + { + cp -= 0x10000; + writeOutput(static_cast( LEAD_SURROGATE + (cp >> 10))); + writeOutput(static_cast(TRAIL_SURROGATE + (cp & 0b11'1111'1111))); + } + else //invalid code point + writeOutput(static_cast(REPLACEMENT_CHAR)); +} + + +class Utf16Decoder +{ +public: + Utf16Decoder(const Char16* str, size_t len) : it_(str), last_(str + len) {} + + std::optional getNext() + { + if (it_ == last_) + return {}; + + const Char16 ch = *it_++; + CodePoint cp = ch; + + if (ch < LEAD_SURROGATE || ch > TRAIL_SURROGATE_MAX) //single Char16, no surrogates + ; + else if (ch < TRAIL_SURROGATE) //two Char16: lead and trail surrogates + decodeTrail(cp); //no range check needed: cp is inside [U+010000, U+10FFFF] by construction + else //unexpected trail surrogate + cp = REPLACEMENT_CHAR; + + return cp; + } + +private: + void decodeTrail(CodePoint& cp) + { + if (it_ != last_) //trail surrogate expected! + { + const Char16 ch = *it_; + if (TRAIL_SURROGATE <= ch && ch <= TRAIL_SURROGATE_MAX) //trail surrogate expected! + { + cp = ((cp - LEAD_SURROGATE) << 10) + (ch - TRAIL_SURROGATE) + 0x10000; + ++it_; + return; + } + } + cp = REPLACEMENT_CHAR; + } + + const Char16* it_; + const Char16* const last_; +}; + +//---------------------------------------------------------------------------------------------------------------- + +template inline +void codePointToUtf8(CodePoint cp, Function writeOutput) //"writeOutput" is a unary function taking a Char8 +{ + /* https://en.wikipedia.org/wiki/UTF-8 + "high and low surrogate halves used by UTF-16 (U+D800 through U+DFFF) and + code points not encodable by UTF-16 (those after U+10FFFF) [...] must be treated as an invalid byte sequence" */ + + if (cp <= 0b111'1111) + writeOutput(static_cast(cp)); + else if (cp <= 0b0111'1111'1111) + { + writeOutput(static_cast((cp >> 6) | 0b1100'0000)); //110x xxxx + writeOutput(static_cast((cp & 0b11'1111) | 0b1000'0000)); //10xx xxxx + } + else if (cp <= 0b1111'1111'1111'1111) + { + if (LEAD_SURROGATE <= cp && cp <= TRAIL_SURROGATE_MAX) //[0xd800, 0xdfff] + codePointToUtf8(REPLACEMENT_CHAR, writeOutput); + else + { + writeOutput(static_cast( (cp >> 12) | 0b1110'0000)); //1110 xxxx + writeOutput(static_cast(((cp >> 6) & 0b11'1111) | 0b1000'0000)); //10xx xxxx + writeOutput(static_cast( (cp & 0b11'1111) | 0b1000'0000)); //10xx xxxx + } + } + else if (cp <= CODE_POINT_MAX) + { + writeOutput(static_cast( (cp >> 18) | 0b1111'0000)); //1111 0xxx + writeOutput(static_cast(((cp >> 12) & 0b11'1111) | 0b1000'0000)); //10xx xxxx + writeOutput(static_cast(((cp >> 6) & 0b11'1111) | 0b1000'0000)); //10xx xxxx + writeOutput(static_cast( (cp & 0b11'1111) | 0b1000'0000)); //10xx xxxx + } + else //invalid code point + codePointToUtf8(REPLACEMENT_CHAR, writeOutput); //resolves to 3-byte UTF8 +} + + +class Utf8Decoder +{ +public: + Utf8Decoder(const Char8* str, size_t len) : it_(str), last_(str + len) {} + + std::optional getNext() + { + if (it_ == last_) + return std::nullopt; + + const Char8 ch = *it_++; + CodePoint cp = ch; + + if (ch < 0x80) //1 byte + ; + else if (ch >> 5 == 0b110) //2 bytes + { + cp &= 0b1'1111; + if (decodeTrail(cp)) + if (cp <= 0b111'1111) //overlong encoding: "correct encoding of a code point uses only the minimum number of bytes required" + cp = REPLACEMENT_CHAR; + } + else if (ch >> 4 == 0b1110) //3 bytes + { + cp &= 0b1111; + if (decodeTrail(cp) && decodeTrail(cp)) + if (cp <= 0b0111'1111'1111 || + (LEAD_SURROGATE <= cp && cp <= TRAIL_SURROGATE_MAX)) //[0xd800, 0xdfff] are invalid code points + cp = REPLACEMENT_CHAR; + } + else if (ch >> 3 == 0b11110) //4 bytes + { + cp &= 0b111; + if (decodeTrail(cp) && decodeTrail(cp) && decodeTrail(cp)) + if (cp <= 0b1111'1111'1111'1111 || cp > CODE_POINT_MAX) + cp = REPLACEMENT_CHAR; + } + else //invalid begin of UTF8 encoding + cp = REPLACEMENT_CHAR; + + return cp; + } + +private: + bool decodeTrail(CodePoint& cp) + { + if (it_ != last_) //trail surrogate expected! + { + const Char8 ch = *it_; + if (ch >> 6 == 0b10) //trail surrogate expected! + { + cp = (cp << 6) + (ch & 0b11'1111); + ++it_; + return true; + } + } + cp = REPLACEMENT_CHAR; + return false; + } + + const Char8* it_; + const Char8* const last_; +}; + +//---------------------------------------------------------------------------------------------------------------- + +template inline void codePointToUtfImpl(CodePoint cp, Function writeOutput, std::integral_constant) { codePointToUtf8 (cp, writeOutput); } //UTF8-char +template inline void codePointToUtfImpl(CodePoint cp, Function writeOutput, std::integral_constant) { codePointToUtf16(cp, writeOutput); } //Windows: UTF16-wchar_t +template inline void codePointToUtfImpl(CodePoint cp, Function writeOutput, std::integral_constant) { writeOutput(cp); } //other OS: UTF32-wchar_t + +//---------------------------------------------------------------------------------------------------------------- + +template +class UtfDecoderImpl; + + +template +class UtfDecoderImpl //UTF8-char +{ +public: + UtfDecoderImpl(const CharType* str, size_t len) : decoder_(reinterpret_cast(str), len) {} + std::optional getNext() { return decoder_.getNext(); } +private: + Utf8Decoder decoder_; +}; + + +template +class UtfDecoderImpl //Windows: UTF16-wchar_t +{ +public: + UtfDecoderImpl(const CharType* str, size_t len) : decoder_(reinterpret_cast(str), len) {} + std::optional getNext() { return decoder_.getNext(); } +private: + Utf16Decoder decoder_; +}; + + +template +class UtfDecoderImpl //other OS: UTF32-wchar_t +{ +public: + UtfDecoderImpl(const CharType* str, size_t len) : it_(reinterpret_cast(str)), last_(it_ + len) {} + std::optional getNext() + { + if (it_ == last_) + return {}; + return *it_++; + } +private: + const CodePoint* it_; + const CodePoint* last_; +}; +} + + +template +using UtfDecoder = impl::UtfDecoderImpl; + + +template inline +void codePointToUtf(impl::CodePoint cp, Function writeOutput) //"writeOutput" is a unary function taking a CharType +{ + return impl::codePointToUtfImpl(cp, writeOutput, std::integral_constant()); +} + + +//------------------------------------------------------------------------------------------- + +template inline +bool isValidUtf(const UtfString& str) +{ + using namespace impl; + + UtfDecoder> decoder(strBegin(str), strLength(str)); + while (const std::optional cp = decoder.getNext()) + if (*cp == REPLACEMENT_CHAR) + return false; + + return true; +} + + +template inline +size_t unicodeLength(const UtfString& str) //return number of code points (+ correctly handle broken UTF encoding) +{ + size_t uniLen = 0; + UtfDecoder> decoder(strBegin(str), strLength(str)); + while (decoder.getNext()) + ++uniLen; + return uniLen; +} + + +template inline +UtfStringOut getUnicodeSubstring(const UtfStringIn& str, size_t uniPosFirst, size_t uniPosLast) //return position of unicode char in UTF-encoded string +{ + assert(uniPosFirst <= uniPosLast && uniPosLast <= unicodeLength(str)); + using namespace impl; + using CharType = GetCharTypeT; + + UtfStringOut output; + assert(uniPosFirst <= uniPosLast); + if (uniPosFirst >= uniPosLast) //optimize for empty range + return output; + + UtfDecoder decoder(strBegin(str), strLength(str)); + for (size_t uniPos = 0; std::optional cp = decoder.getNext(); ++uniPos) //[!] declaration in condition part of the for-loop + if (uniPos >= uniPosFirst) + { + if (uniPos >= uniPosLast) + break; + codePointToUtf(*cp, [&](CharType c) { output += c; }); + } + return output; +} + +//------------------------------------------------------------------------------------------- + +namespace impl +{ +template inline +TargetString utfTo(const SourceString& str, std::true_type) { return copyStringTo(str); } + + +template inline +TargetString utfTo(const SourceString& str, std::false_type) +{ + using CharSrc = GetCharTypeT; + using CharTrg = GetCharTypeT; + static_assert(sizeof(CharSrc) != sizeof(CharTrg)); + + TargetString output; + + UtfDecoder decoder(strBegin(str), strLength(str)); + while (const std::optional cp = decoder.getNext()) + codePointToUtf(*cp, [&](CharTrg c) { output += c; }); + + return output; +} +} + + +template inline +TargetString utfTo(const SourceString& str) +{ + return impl::utfTo(str, std::bool_constant) == sizeof(GetCharTypeT)>()); +} +} + +#endif //UTF_H_01832479146991573473545 diff --git a/zen/zlib_wrap.cpp b/zen/zlib_wrap.cpp new file mode 100644 index 0000000..5810ef5 --- /dev/null +++ b/zen/zlib_wrap.cpp @@ -0,0 +1,245 @@ +// ***************************************************************************** +// * 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 "zlib_wrap.h" +//Windows: use the SAME zlib version that wxWidgets is linking against! //C:\Data\Projects\wxWidgets\Source\src\zlib\zlib.h +//Linux/macOS: use zlib system header for wxWidgets, libcurl (HTTP), libssh2 (SFTP) +// => don't compile wxWidgets with: --with-zlib=builtin +#include +#include "scope_guard.h" +#include "serialize.h" + +using namespace zen; + + +namespace +{ +std::wstring getZlibErrorLiteral(int sc) +{ + switch (sc) + { + ZEN_CHECK_CASE_FOR_CONSTANT(Z_NEED_DICT); + ZEN_CHECK_CASE_FOR_CONSTANT(Z_STREAM_END); + ZEN_CHECK_CASE_FOR_CONSTANT(Z_OK); + ZEN_CHECK_CASE_FOR_CONSTANT(Z_ERRNO); + ZEN_CHECK_CASE_FOR_CONSTANT(Z_STREAM_ERROR); + ZEN_CHECK_CASE_FOR_CONSTANT(Z_DATA_ERROR); + ZEN_CHECK_CASE_FOR_CONSTANT(Z_MEM_ERROR); + ZEN_CHECK_CASE_FOR_CONSTANT(Z_BUF_ERROR); + ZEN_CHECK_CASE_FOR_CONSTANT(Z_VERSION_ERROR); + + default: + return replaceCpy(L"zlib error %x", L"%x", numberTo(sc)); + } +} + + +size_t zlib_compressBound(size_t len) +{ + return ::compressBound(static_cast(len)); //upper limit for buffer size, larger than input size!!! +} + + +size_t zlib_compress(const void* src, size_t srcLen, void* trg, size_t trgLen, int level) //throw SysError +{ + uLongf bufSize = static_cast(trgLen); + const int rv = ::compress2(static_cast(trg), //Bytef* dest + &bufSize, //uLongf* destLen + static_cast(src), //const Bytef* source + static_cast(srcLen), //uLong sourceLen + level); //int level + // Z_OK: success + // Z_MEM_ERROR: not enough memory + // Z_BUF_ERROR: not enough room in the output buffer + if (rv != Z_OK || bufSize > trgLen) + throw SysError(formatSystemError("zlib compress2", getZlibErrorLiteral(rv), L"")); + + return bufSize; +} + + +size_t zlib_decompress(const void* src, size_t srcLen, void* trg, size_t trgLen) //throw SysError +{ + uLongf bufSize = static_cast(trgLen); + const int rv = ::uncompress(static_cast(trg), //Bytef* dest + &bufSize, //uLongf* destLen + static_cast(src), //const Bytef* source + static_cast(srcLen)); //uLong sourceLen + // Z_OK: success + // Z_MEM_ERROR: not enough memory + // Z_BUF_ERROR: not enough room in the output buffer + // Z_DATA_ERROR: input data was corrupted or incomplete + if (rv != Z_OK || bufSize > trgLen) + throw SysError(formatSystemError("zlib uncompress", getZlibErrorLiteral(rv), L"")); + + return bufSize; +} +} + + +#undef compress //mitigate zlib macro shit... + +std::string zen::compress(const std::string_view& stream, int level) //throw SysError +{ + std::string output; + if (!stream.empty()) //don't dereference iterator into empty container! + { + //save uncompressed stream size for decompression + const uint64_t uncompressedSize = stream.size(); //use portable number type! + output.resize(sizeof(uncompressedSize)); + std::memcpy(output.data(), &uncompressedSize, sizeof(uncompressedSize)); + + const size_t bufferEstimate = zlib_compressBound(stream.size()); //upper limit for buffer size, larger than input size!!! + + output.resize(output.size() + bufferEstimate); + + const size_t bytesWritten = zlib_compress(stream.data(), + stream.size(), + output.data() + output.size() - bufferEstimate, + bufferEstimate, + level); //throw SysError + if (bytesWritten < bufferEstimate) + output.resize(output.size() - bufferEstimate + bytesWritten); //caveat: unsigned arithmetics + //caveat: physical memory consumption still *unchanged*! + } + return output; +} + + +std::string zen::decompress(const std::string_view& stream) //throw SysError +{ + std::string output; + if (!stream.empty()) //don't dereference iterator into empty container! + { + //retrieve size of uncompressed data + uint64_t uncompressedSize = 0; //use portable number type! + if (stream.size() < sizeof(uncompressedSize)) + throw SysError(L"zlib error: stream size < 8"); + + std::memcpy(&uncompressedSize, stream.data(), sizeof(uncompressedSize)); + + //attention: output MUST NOT be empty! Else it will pass a nullptr to zlib_decompress() => Z_STREAM_ERROR although "uncompressedSize == 0"!!! + if (uncompressedSize == 0) //cannot be 0: compress() directly maps empty -> empty container skipping zlib! + throw SysError(L"zlib error: uncompressed size == 0"); + + try + { + output.resize(static_cast(uncompressedSize)); //throw std::bad_alloc + } + //most likely this is due to data corruption: + catch (const std::length_error& e) { throw SysError(L"zlib error: " + _("Out of memory.") + L' ' + utfTo(e.what())); } + catch (const std::bad_alloc& e) { throw SysError(L"zlib error: " + _("Out of memory.") + L' ' + utfTo(e.what())); } + + const size_t bytesWritten = zlib_decompress(stream.data() + sizeof(uncompressedSize), + stream.size() - sizeof(uncompressedSize), + output.data(), + static_cast(uncompressedSize)); //throw SysError + if (bytesWritten != static_cast(uncompressedSize)) + throw SysError(formatSystemError("zlib_decompress", L"", L"bytes written != uncompressed size.")); + } + return output; +} + + +class InputStreamAsGzip::Impl +{ +public: + Impl(const std::function& tryReadBlock /*throw X; may return short, only 0 means EOF!*/, + size_t blockSize) : //throw SysError + tryReadBlock_(tryReadBlock), + blockSize_(blockSize) + { + const int windowBits = MAX_WBITS + 16; //"add 16 to windowBits to write a simple gzip header" + + //"memLevel=1 uses minimum memory but is slow and reduces compression ratio; memLevel=9 uses maximum memory for optimal speed. + const int memLevel = 9; //test; 280 MB installer file: level 9 shrinks runtime by ~8% compared to level 8 (==DEF_MEM_LEVEL) at the cost of 128 KB extra memory + static_assert(memLevel <= MAX_MEM_LEVEL); + + const int rv = ::deflateInit2(&gzipStream_, //z_streamp strm + 3 /*see db_file.cpp*/, //int level + Z_DEFLATED, //int method + windowBits, //int windowBits + memLevel, //int memLevel + Z_DEFAULT_STRATEGY); //int strategy + if (rv != Z_OK) + throw SysError(formatSystemError("zlib deflateInit2", getZlibErrorLiteral(rv), L"")); + } + + ~Impl() + { + [[maybe_unused]] const int rv = ::deflateEnd(&gzipStream_); + assert(rv == Z_OK); + } + + size_t read(void* buffer, size_t bytesToRead) //throw SysError, X; return "bytesToRead" bytes unless end of stream! + { + if (bytesToRead == 0) //"read() with a count of 0 returns zero" => indistinguishable from end of file! => check! + throw std::logic_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Contract violation!"); + + gzipStream_.next_out = static_cast(buffer); + gzipStream_.avail_out = static_cast(bytesToRead); + + for (;;) + { + //refill input buffer once avail_in == 0: https://www.zlib.net/manual.html + if (gzipStream_.avail_in == 0 && !eof_) + { + const size_t bytesRead = tryReadBlock_(bufIn_.data(), blockSize_); //throw X; may return short, only 0 means EOF! + gzipStream_.next_in = reinterpret_cast(bufIn_.data()); + gzipStream_.avail_in = static_cast(bytesRead); + if (bytesRead == 0) + eof_ = true; + } + + const int rv = ::deflate(&gzipStream_, eof_ ? Z_FINISH : Z_NO_FLUSH); + if (eof_ && rv == Z_STREAM_END) + return bytesToRead - gzipStream_.avail_out; + if (rv != Z_OK) + throw SysError(formatSystemError("zlib deflate", getZlibErrorLiteral(rv), L"")); + + if (gzipStream_.avail_out == 0) + return bytesToRead; + } + } + + size_t getBlockSize() const { return blockSize_; } //returning input blockSize_ makes sense for low compression ratio + +private: + const std::function tryReadBlock_; //throw X + const size_t blockSize_; + bool eof_ = false; + std::vector bufIn_{blockSize_}; + z_stream gzipStream_ = {}; +}; + + +InputStreamAsGzip::InputStreamAsGzip(const std::function& tryReadBlock /*throw X*/, size_t blockSize) : + pimpl_(std::make_unique(tryReadBlock, blockSize)) {} //throw SysError + +InputStreamAsGzip::~InputStreamAsGzip() {} + +size_t InputStreamAsGzip::getBlockSize() const { return pimpl_->getBlockSize(); } + +size_t InputStreamAsGzip::read(void* buffer, size_t bytesToRead) { return pimpl_->read(buffer, bytesToRead); } //throw SysError, X + + +std::string zen::compressAsGzip(const std::string_view& stream) //throw SysError +{ + MemoryStreamIn memStream(stream); + + auto tryReadBlock = [&](void* buffer, size_t bytesToRead) //may return short, only 0 means EOF! + { + return memStream.read(buffer, bytesToRead); //return "bytesToRead" bytes unless end of stream! + }; + + InputStreamAsGzip gzipStream(tryReadBlock, 1024 * 1024 /*blockSize*/); //throw SysError + + return unbufferedLoad([&](void* buffer, size_t bytesToRead) + { + return gzipStream.read(buffer, bytesToRead); //throw SysError; return "bytesToRead" bytes unless end of stream! + }, + gzipStream.getBlockSize()); //throw SysError +} diff --git a/zen/zlib_wrap.h b/zen/zlib_wrap.h new file mode 100644 index 0000000..d672707 --- /dev/null +++ b/zen/zlib_wrap.h @@ -0,0 +1,44 @@ +// ***************************************************************************** +// * 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 ZLIB_WRAP_H_428597064566 +#define ZLIB_WRAP_H_428597064566 + +#include +#include "sys_error.h" + + +namespace zen +{ +// compression level must be between 0 and 9: +// 0: no compression +// 9: best compression +std::string compress(const std::string_view& stream, int level); //throw SysError +//caveat: output stream is physically larger than input! => strip additional reserved space if needed: "BinContainer(output.begin(), output.end())" + +std::string decompress(const std::string_view& stream); //throw SysError + + +class InputStreamAsGzip //convert input stream into gzip on the fly +{ +public: + explicit InputStreamAsGzip(const std::function& tryReadBlock /*throw X; may return short, only 0 means EOF!*/, + size_t blockSize); //throw SysError + ~InputStreamAsGzip(); + + size_t getBlockSize() const; + + size_t read(void* buffer, size_t bytesToRead); //throw SysError, X; return "bytesToRead" bytes unless end of stream! + +private: + class Impl; + const std::unique_ptr pimpl_; +}; + +std::string compressAsGzip(const std::string_view& stream); //throw SysError +} + +#endif //ZLIB_WRAP_H_428597064566 diff --git a/zen/zstring.cpp b/zen/zstring.cpp new file mode 100644 index 0000000..a70228e --- /dev/null +++ b/zen/zstring.cpp @@ -0,0 +1,315 @@ +// ***************************************************************************** +// * 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 "zstring.h" + //#include + #include "sys_error.h" + +using namespace zen; + + +namespace +{ +Zstring getUnicodeNormalForm_NonAsciiValidUtf(const Zstring& str, UnicodeNormalForm form) +{ + //Example: const char* decomposed = "\x6f\xcc\x81"; //ó + // const char* precomposed = "\xc3\xb3"; //ó + assert(!isAsciiString(str)); //includes "not-empty" check + assert(!contains(str, Zchar('\0'))); //don't expect embedded nulls! + + try + { + gchar* strNorm = ::g_utf8_normalize(str.c_str(), str.length(), form == UnicodeNormalForm::nfc ? G_NORMALIZE_NFC : G_NORMALIZE_NFD); + if (!strNorm) + throw SysError(formatSystemError("g_utf8_normalize", L"", L"Conversion failed.")); + ZEN_ON_SCOPE_EXIT(::g_free(strNorm)); + + const std::string_view strNormView(strNorm, strLength(strNorm)); + + if (equalString(str, strNormView)) //avoid extra memory allocation + return str; + + return Zstring(strNormView); + + } + catch (const SysError& e) + { + throw std::runtime_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Error normalizing string:" + '\n' + + utfTo(str) + "\n\n" + utfTo(e.toString())); + } +} + + +Zstring getValidUtf(const Zstring& str) +{ + /* 1. do NOT fail on broken UTF encoding, instead normalize using REPLACEMENT_CHAR! + 2. NormalizeString() haateeez them Unicode non-characters: ERROR_NO_UNICODE_TRANSLATION! http://www.unicode.org/faq/private_use.html#nonchar1 + - No such issue on Linux/macOS with g_utf8_normalize(), and CFStringGetFileSystemRepresentation() + -> still, probably good idea to "normalize" Unicode non-characters cross-platform + - consistency for compareNoCase(): let's *unconditionally* check before other normalization operations, not just in error case! */ + using impl::CodePoint; + auto isUnicodeNonCharacter = [](CodePoint cp) { assert(cp <= impl::CODE_POINT_MAX); return (0xfdd0 <= cp && cp <= 0xfdef) || cp % 0x10'000 >= 0xfffe; }; + + const bool invalidUtf = [&] //pre-check: avoid memory allocation if valid UTF + { + UtfDecoder decoder(str.c_str(), str.size()); + while (const std::optional cp = decoder.getNext()) + if (*cp == impl::REPLACEMENT_CHAR || //marks broken UTF encoding + isUnicodeNonCharacter(*cp)) + return true; + return false; + }(); + + if (invalidUtf) //band-aid broken UTF encoding with REPLACEMENT_CHAR + { + Zstring validStr; //don't want extra memory allocations in the standard case (valid UTF) + UtfDecoder decoder(str.c_str(), str.size()); + while (std::optional cp = decoder.getNext()) + { + if (isUnicodeNonCharacter(*cp)) // + *cp = impl::REPLACEMENT_CHAR; //"normalize" Unicode non-characters + + codePointToUtf(*cp, [&](Zchar ch) { validStr += ch; }); + } + return validStr; + } + else + return str; +} + + +Zstring getUpperCaseAscii(const Zstring& str) +{ + assert(isAsciiString(str)); + + Zstring output = str; + for (Zchar& c : output) //identical to LCMapStringEx(), g_unichar_toupper(), CFStringUppercase() [verified!] + c = asciiToUpper(c); // + return output; +} + + +Zstring getUpperCaseNonAscii(const Zstring& str) +{ + const Zstring& strValidUtf = getValidUtf(str); + try + { + const Zstring strNorm = getUnicodeNormalForm_NonAsciiValidUtf(strValidUtf, UnicodeNormalForm::native); + + Zstring output; + output.reserve(strNorm.size()); + + UtfDecoder decoder(strNorm.c_str(), strNorm.size()); + while (const std::optional cp = decoder.getNext()) + codePointToUtf(::g_unichar_toupper(*cp), [&](const char c) { output += c; }); //don't use std::towupper: *incomplete* and locale-dependent! + + static_assert(sizeof(impl::CodePoint) == sizeof(gunichar)); + return output; + + } + catch (const SysError& e) + { + throw std::runtime_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Error converting string to upper case:" + '\n' + + utfTo(str) + "\n\n" + utfTo(e.toString())); + } +} +} + + +Zstring getUnicodeNormalForm(const Zstring& str, UnicodeNormalForm form) +{ + static_assert(std::is_same_v&>, "god bless our ref-counting! => save needless memory allocation!"); + + if (isAsciiString(str)) //fast path: in the range of 3.5ns + return str; + + return getUnicodeNormalForm_NonAsciiValidUtf(getValidUtf(str), form); //slow path +} + + +Zstring getUpperCase(const Zstring& str) +{ + return isAsciiString(str) ? //fast path: in the range of 3.5ns + getUpperCaseAscii(str) : + getUpperCaseNonAscii(str); //slow path +} + + +namespace +{ +std::weak_ordering compareNoCaseUtf8(const char* lhs, size_t lhsLen, const char* rhs, size_t rhsLen) +{ + //expect Unicode normalized strings! + assert(Zstring(lhs, lhsLen) == getUnicodeNormalForm(Zstring(lhs, lhsLen), UnicodeNormalForm::nfd)); + assert(Zstring(rhs, rhsLen) == getUnicodeNormalForm(Zstring(rhs, rhsLen), UnicodeNormalForm::nfd)); + + //- strncasecmp implements ASCII CI-comparsion only! => signature is broken for UTF8-input; toupper() similarly doesn't support Unicode + //- wcsncasecmp: https://opensource.apple.com/source/Libc/Libc-763.12/string/wcsncasecmp-fbsd.c + // => re-implement comparison based on g_unichar_tolower() to avoid memory allocations + + UtfDecoder decL(lhs, lhsLen); + UtfDecoder decR(rhs, rhsLen); + for (;;) + { + const std::optional cpL = decL.getNext(); + const std::optional cpR = decR.getNext(); + if (!cpL || !cpR) + return !cpR <=> !cpL; + + static_assert(sizeof(gunichar) == sizeof(impl::CodePoint)); + static_assert(std::is_unsigned_v, "unsigned char-comparison is the convention!"); + + //ordering: "to lower" converts to higher code points than "to upper" + const gunichar charL = ::g_unichar_toupper(*cpL); //note: tolower can be ambiguous, so don't use: + const gunichar charR = ::g_unichar_toupper(*cpR); //e.g. "Σ" (upper case) can be lower-case "ς" in the end of the word or "σ" in the middle. + if (charL != charR) + return charL <=> charR; + } +} +} + + +std::weak_ordering compareNatural(const Zstring& lhs, const Zstring& rhs) +{ + try + { + /* Unicode Normalization Forms: + Windows: CompareString() ignores NFD/NFC differences and converts to NFD + Linux: g_unichar_toupper() can't ignore differences + macOS: CFStringCompare() considers differences */ + const Zstring& lhsNorm = getUnicodeNormalForm(lhs, UnicodeNormalForm::nfd); //normalize: - broken UTF encoding + const Zstring& rhsNorm = getUnicodeNormalForm(rhs, UnicodeNormalForm::nfd); // - Unicode non-characters + + const char* strL = lhsNorm.c_str(); + const char* strR = rhsNorm.c_str(); + + const char* const strEndL = strL + lhsNorm.size(); + const char* const strEndR = strR + rhsNorm.size(); + /* - compare strings after conceptually creating blocks of whitespace/numbers/text + - implement strict weak ordering! + - don't follow broken "strnatcasecmp": https://github.com/php/php-src/blob/master/ext/standard/strnatcmp.c + 1. incorrect non-ASCII CI-comparison + 2. incorrect bounds checks + 3. incorrect trimming of *all* whitespace + 4. arbitrary handling of leading 0 only at string begin + 5. incorrect handling of whitespace following a number + 6. code is a mess */ + for (;;) + { + if (strL == strEndL || strR == strEndR) + return (strL != strEndL) <=> (strR != strEndR); //"nothing" before "something" + //note: "something" never would have been condensed to "nothing" further below => can finish evaluation here + + const bool wsL = isWhiteSpace(*strL); + const bool wsR = isWhiteSpace(*strR); + if (wsL != wsR) + return !wsL <=> !wsR; //whitespace before non-ws! + if (wsL) + { + ++strL, ++strR; + while (strL != strEndL && isWhiteSpace(*strL)) ++strL; + while (strR != strEndR && isWhiteSpace(*strR)) ++strR; + continue; + } + + const bool digitL = isDigit(*strL); + const bool digitR = isDigit(*strR); + if (digitL != digitR) + return !digitL <=> !digitR; //numbers before chars! + if (digitL) + { + while (strL != strEndL && *strL == '0') ++strL; + while (strR != strEndR && *strR == '0') ++strR; + + int rv = 0; + for (;; ++strL, ++strR) + { + const bool endL = strL == strEndL || !isDigit(*strL); + const bool endR = strR == strEndR || !isDigit(*strR); + if (endL != endR) + return !endL <=> !endR; //more digits means bigger number + if (endL) + break; //same number of digits + + if (rv == 0 && *strL != *strR) + rv = *strL - *strR; //found first digit difference comparing from left + } + if (rv != 0) + return rv <=> 0; + continue; + } + + //compare full junks of text: consider Unicode encoding! + const char* textBeginL = strL++; + const char* textBeginR = strR++; //current char is neither white space nor digit at this point! + while (strL != strEndL && !isWhiteSpace(*strL) && !isDigit(*strL)) ++strL; + while (strR != strEndR && !isWhiteSpace(*strR) && !isDigit(*strR)) ++strR; + + if (const std::weak_ordering cmp = compareNoCaseUtf8(textBeginL, strL - textBeginL, textBeginR, strR - textBeginR); + cmp != std::weak_ordering::equivalent) + return cmp; + } + + } + catch (const SysError& e) + { + throw std::runtime_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] Error comparing strings:" + '\n' + + utfTo(lhs) + '\n' + utfTo(rhs) + "\n\n" + utfTo(e.toString())); + } +} + + +std::weak_ordering compareNoCase(const Zstring& lhs, const Zstring& rhs) +{ + const bool isAsciiL = isAsciiString(lhs); + const bool isAsciiR = isAsciiString(rhs); + + //fast path: no memory allocations => ~ 6x speedup + if (isAsciiL && isAsciiR) + { + const size_t minSize = std::min(lhs.size(), rhs.size()); + for (size_t i = 0; i < minSize; ++i) + { + //ordering: do NOT call compareAsciiNoCase(), which uses asciiToLower()! + const Zchar lUp = asciiToUpper(lhs[i]); // + const Zchar rUp = asciiToUpper(rhs[i]); //no surprises: emulate getUpperCase() [verified!] + if (lUp != rUp) // + return lUp <=> rUp; // + } + return lhs.size() <=> rhs.size(); + } + //-------------------------------------- + + //can't we instead skip isAsciiString() and compare chars as long as isAsciiChar()? + // => NOPE! e.g. decomposed Unicode! A seemingly single isAsciiChar() might be followed by a combining character!!! + + return (isAsciiL ? getUpperCaseAscii(lhs) : getUpperCaseNonAscii(lhs)) <=> + (isAsciiR ? getUpperCaseAscii(rhs) : getUpperCaseNonAscii(rhs)); +} + + +bool equalNoCase(const Zstring& lhs, const Zstring& rhs) +{ + const bool isAsciiL = isAsciiString(lhs); + const bool isAsciiR = isAsciiString(rhs); + + //fast-path: no extra memory allocations + //caveat: ASCII-char and non-ASCII Unicode *can* compare case-insensitive equal!!! e.g. i and ı https://freefilesync.org/forum/viewtopic.php?t=9718 + if (isAsciiL && isAsciiR) + { + if (lhs.size() != rhs.size()) + return false; + + for (size_t i = 0; i < lhs.size(); ++i) + if (asciiToUpper(lhs[i]) != + asciiToUpper(rhs[i])) + return false; + return true; + } + + return (isAsciiL ? getUpperCaseAscii(lhs) : getUpperCaseNonAscii(lhs)) == + (isAsciiR ? getUpperCaseAscii(rhs) : getUpperCaseNonAscii(rhs)); +} diff --git a/zen/zstring.h b/zen/zstring.h new file mode 100644 index 0000000..0d49331 --- /dev/null +++ b/zen/zstring.h @@ -0,0 +1,110 @@ +// ***************************************************************************** +// * 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 ZSTRING_H_73425873425789 +#define ZSTRING_H_73425873425789 + +#include //not used by this header, but the "rest of the world" needs it! +#include "utf.h" // +#include "string_base.h" + + + using Zchar = char; + #define Zstr(x) x + + +//"The reason for all the fuss above" - Loki/SmartPtr +//a high-performance string for interfacing with native OS APIs in multithreaded contexts +using Zstring = zen::Zbase; + +using ZstringView = std::basic_string_view; + +//for special UI-contexts: guaranteed exponential growth + ref-counting + COW + no SSO overhead +using Zstringc = zen::Zbase; +//using Zstringw = zen::Zbase; + + +enum class UnicodeNormalForm +{ + nfc, //precomposed + nfd, //decomposed + native = nfc, +}; +Zstring getUnicodeNormalForm(const Zstring& str, UnicodeNormalForm form = UnicodeNormalForm::native); +/* "In fact, Unicode declares that there is an equivalence relationship between decomposed and composed sequences, + and conformant software should not treat canonically equivalent sequences, whether composed or decomposed or something in between, as different." + https://www.win.tue.nl/~aeb/linux/uc/nfc_vs_nfd.html */ + +/* Caveat: don't expect input/output string sizes to match: + - different UTF-8 encoding length of upper-case chars + - different number of upper case chars (e.g. ß => "SS" on macOS) + - output is Unicode-normalized */ +Zstring getUpperCase(const Zstring& str); + +//------------------------------------------------------------------------------------------ +struct ZstringNorm //use as STL container key: better than repeated Unicode normalizations during std::map<>::find() +{ + /*explicit*/ ZstringNorm(const Zstring& str) : normStr(getUnicodeNormalForm(str)) {} + Zstring normStr; + + std::strong_ordering operator<=>(const ZstringNorm&) const = default; +}; +template<> struct std::hash { size_t operator()(const ZstringNorm& str) const { return std::hash()(str.normStr); } }; + +//struct LessUnicodeNormal { bool operator()(const Zstring& lhs, const Zstring& rhs) const { return getUnicodeNormalForm(lhs) < getUnicodeNormalForm(rhs); } }; + +//------------------------------------------------------------------------------------------ +struct ZstringNoCase //use as STL container key: better than repeated upper-case conversions during std::map<>::find() +{ + /*explicit*/ ZstringNoCase(const Zstring& str) : upperCase(getUpperCase(str)) {} + Zstring upperCase; + + std::strong_ordering operator<=>(const ZstringNoCase&) const = default; +}; +template<> struct std::hash { size_t operator()(const ZstringNoCase& str) const { return std::hash()(str.upperCase); } }; + + +std::weak_ordering compareNoCase(const Zstring& lhs, const Zstring& rhs); + +bool equalNoCase(const Zstring& lhs, const Zstring& rhs); + +//------------------------------------------------------------------------------------------ +std::weak_ordering compareNatural(const Zstring& lhs, const Zstring& rhs); + +struct LessNaturalSort { bool operator()(const Zstring& lhs, const Zstring& rhs) const { return compareNatural(lhs, rhs) < 0; } }; + + +//------------------------------------------------------------------------------------------ +//common Unicode characters +const wchar_t EN_DASH = L'\u2013'; //– +const wchar_t EM_DASH = L'\u2014'; //— + const wchar_t* const SPACED_DASH = L" \u2014 "; //using 'EM DASH' +const wchar_t* const ELLIPSIS = L"\u2026"; //… +const wchar_t MULT_SIGN = L'\u00D7'; //× +const wchar_t NOBREAK_SPACE = L'\u00A0'; +const wchar_t ZERO_WIDTH_SPACE = L'\u200B'; + +const wchar_t EN_SPACE = L'\u2002'; + +const wchar_t LTR_MARK = L'\u200E'; //UTF-8: E2 80 8E +const wchar_t RTL_MARK = L'\u200F'; //UTF-8: E2 80 8F https://www.w3.org/International/questions/qa-bidi-unicode-controls +//const wchar_t BIDI_DIR_ISOLATE_RTL = L'\u2067'; //=> not working on Win 10 +//const wchar_t BIDI_POP_DIR_ISOLATE = L'\u2069'; //=> not working on Win 10 +//const wchar_t BIDI_DIR_EMBEDDING_RTL = L'\u202B'; //=> not working on Win 10 +//const wchar_t BIDI_POP_DIR_FORMATTING = L'\u202C'; //=> not working on Win 10 + +const wchar_t RIGHT_ARROW_CURV_DOWN = L'\u2935'; //Right Arrow Curving Down: ⤵ +//Windows bug: rendered differently depending on presence of e.g. LTR_MARK! +//there is no "Left Arrow Curving Down" => WTF => better than nothing: +const wchar_t LEFT_ARROW_ANTICLOCK = L'\u2B8F'; //Anticlockwise Triangle-Headed Top U-Shaped Arrow: ⮏ + +const wchar_t* const TAB_SPACE = L" "; //4: the only sensible space count for tabs + +const wchar_t LINE_SEPARATOR = L'\u2028'; //WTF: visually indistinguishable from new line! +const wchar_t PARAGRAPH_SEPARATOR = L'\u2029'; + + +#endif //ZSTRING_H_73425873425789 diff --git a/zenXml/zenxml/cvrt_struc.h b/zenXml/zenxml/cvrt_struc.h new file mode 100644 index 0000000..57a5d09 --- /dev/null +++ b/zenXml/zenxml/cvrt_struc.h @@ -0,0 +1,202 @@ +// ***************************************************************************** +// * 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 CVRT_STRUC_H_018727409908342709743 +#define CVRT_STRUC_H_018727409908342709743 + +#include "dom.h" + + +namespace zen +{ +/** +\file +\brief Handle conversion of arbitrary types to and from XML elements. +See comments in cvrt_text.h +*/ + +///Convert XML element to structured user data +/** + \param input The input XML element. + \param value Conversion target value. + \return "true" if value was read successfully. +*/ +template bool readStruc(const XmlElement& input, T& value); +///Convert structured user data into an XML element +/** + \param value The value to be converted. + \param output The output XML element. +*/ +template void writeStruc(const T& value, XmlElement& output); + + + + + + + + + + + + +//------------------------------ implementation ------------------------------------- +namespace impl_2384343 +{ +ZEN_INIT_DETECT_MEMBER_TYPE(value_type) +ZEN_INIT_DETECT_MEMBER_TYPE(iterator) +ZEN_INIT_DETECT_MEMBER_TYPE(const_iterator) + +ZEN_INIT_DETECT_MEMBER(begin) // +ZEN_INIT_DETECT_MEMBER(end) //we don't know the exact declaration of the member attribute: may be in a base class! +ZEN_INIT_DETECT_MEMBER(insert) // +} + +template +using IsStlContainer = std::bool_constant< + impl_2384343::hasMemberType_value_type && + impl_2384343::hasMemberType_iterator && + impl_2384343::hasMemberType_const_iterator&& + impl_2384343::hasMember_begin && + impl_2384343::hasMember_end && + impl_2384343::hasMember_insert >; + + +template +struct IsStlPair +{ +private: + using Yes = char[1]; + using No = char[2]; + + template + static Yes& isPair(const std::pair&); + static No& isPair(...); +public: + enum { value = sizeof(isPair(std::declval())) == sizeof(Yes) }; +}; + +//###################################################################################### + +//Conversion from arbitrary types to an XML element +enum class ValueType +{ + stlContainer, + stlPair, + other, +}; + +template +using GetValueType = std::integral_constant::value != TextType::other ? ValueType::other : //some string classes are also STL containers, so check this first + IsStlContainer::value ? ValueType::stlContainer : + IsStlPair ::value ? ValueType::stlPair : + ValueType::other>; + + +template +struct ConvertElement; +/* -> expected interface +{ + void writeStruc(const T& value, XmlElement& output) const; + bool readStruc(const XmlElement& input, T& value) const; +}; +*/ + + +//partial specialization: handle conversion for all STL-container types! +template +struct ConvertElement +{ + void writeStruc(const T& value, XmlElement& output) const + { + for (const typename T::value_type& childVal : value) + { + XmlElement& newChild = output.addChild("Item"); + zen::writeStruc(childVal, newChild); + } + } + bool readStruc(const XmlElement& input, T& value) const + { + value.clear(); + + bool success = true; + for (const XmlElement& xmlChild : input.getChildren()) + { + typename T::value_type childVal; + if (zen::readStruc(xmlChild, childVal)) + value.insert(value.end(), std::move(childVal)); + else + success = false; + //should we support insertion of partially-loaded struct?? + } + return success; + } +}; + + +//partial specialization: handle conversion for std::pair +template +struct ConvertElement +{ + void writeStruc(const T& value, XmlElement& output) const + { + XmlElement& child1 = output.addChild("one"); //don't use "1st/2nd", this will confuse a few pedantic XML parsers + zen::writeStruc(value.first, child1); + + XmlElement& child2 = output.addChild("two"); + zen::writeStruc(value.second, child2); + } + bool readStruc(const XmlElement& input, T& value) const + { + bool success = true; + const XmlElement* child1 = input.getChild("one"); + if (!child1 || !zen::readStruc(*child1, value.first)) + success = false; + + const XmlElement* child2 = input.getChild("two"); + if (!child2 || !zen::readStruc(*child2, value.second)) + success = false; + + return success; + } +}; + + +//partial specialization: not a pure structured type, try text conversion (thereby respect user specializations of writeText()/readText()) +template +struct ConvertElement +{ + void writeStruc(const T& value, XmlElement& output) const + { + std::string tmp; + writeText(value, tmp); + output.setValue(std::move(tmp)); + } + bool readStruc(const XmlElement& input, T& value) const + { + std::string rawStr; + input.getValue(rawStr); + return readText(rawStr, value); + } +}; + + +template inline +void writeStruc(const T& value, XmlElement& output) +{ + ConvertElement::value>().writeStruc(value, output); +} + + +template inline +bool readStruc(const XmlElement& input, T& value) +{ + return ConvertElement::value>().readStruc(input, value); +} +} + +#endif //CVRT_STRUC_H_018727409908342709743 diff --git a/zenXml/zenxml/cvrt_text.h b/zenXml/zenxml/cvrt_text.h new file mode 100644 index 0000000..4fa4ec8 --- /dev/null +++ b/zenXml/zenxml/cvrt_text.h @@ -0,0 +1,251 @@ +// ***************************************************************************** +// * 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 CVRT_TEXT_H_018727339083427097434 +#define CVRT_TEXT_H_018727339083427097434 + +#include +#include + + +namespace zen +{ +/** +\file +\brief Handle conversion of string-convertible types to and from std::string. + +It is \b not required to call these functions directly. They are implicitly used by zen::XmlElement::getValue(), +zen::XmlElement::setValue(), zen::XmlElement::getAttribute() and zen::XmlElement::setAttribute(). +\n\n +Conversions for the following user types are supported by default: + - strings - std::string, std::wstring, char*, wchar_t*, char, wchar_t, etc..., all STL-compatible-string-classes + - numbers - int, double, float, bool, long, etc..., all built-in numbers + - STL containers - std::map, std::set, std::vector, std::list, etc..., all STL-compatible-containers + - std::pair + +You can add support for additional types via template specialization. \n\n +Specialize zen::readStruc() and zen::writeStruc() to enable conversion from structured user types to XML elements. +Specialize zen::readText() and zen::writeText() to enable conversion from string-convertible user types to std::string. +Prefer latter if possible since it does not only enable conversions from XML elements to user data, but also from and to XML attributes. +\n\n + Example: type "bool" +\code +namespace zen +{ +template <> inline +void writeText(const bool& value, std::string& output) +{ + output = value ? "true" : "false"; +} + +template <> inline +bool readText(const std::string& input, bool& value) +{ + const std::string tmp = trimCpy(input); + if (tmp == "true") + value = true; + else if (tmp == "false") + value = false; + else + return false; + return true; +} +} +\endcode +*/ + + +///Convert text to user data - used by XML elements and attributes +/** + \param input Input text. + \param value Conversion target value. + \return "true" if value was read successfully. +*/ +template bool readText(const std::string& input, T& value); +///Convert user data into text - used by XML elements and attributes +/** + \param value The value to be converted. + \param output Output text. +*/ +template void writeText(const T& value, std::string& output); + + +/* Different classes of data types: + +----------------------------- +| structured | readStruc/writeStruc - e.g. string-convertible types, STL containers, std::pair, structured user types +| ------------------------- | +| | to-string-convertible | | readText/writeText - e.g. string-like types, all built-in arithmetic numbers, bool +| | --------------- | | +| | | string-like | | | utfTo - e.g. std::string, wchar_t*, char[], wchar_t, wxString, MyStringClass, ... +| | --------------- | | +| ------------------------- | +----------------------------- +*/ + + + + + + + + + + + + + + + +//------------------------------ implementation ------------------------------------- +template +struct IsChronoDuration +{ +private: + using Yes = char[1]; + using No = char[2]; + + template + static Yes& isDuration(std::chrono::duration); + static No& isDuration(...); +public: + enum { value = sizeof(isDuration(std::declval())) == sizeof(Yes) }; +}; + + +//Conversion from arbitrary types to text (for use with XML elements and attributes) +enum class TextType +{ + boolean, + number, + chrono, + string, + other, +}; + +template +struct GetTextType : std::integral_constant ? TextType::boolean : + isStringLike ? TextType::string : //string before number to correctly handle char/wchar_t -> this was an issue with Loki only! + isArithmetic ? TextType::number : // + IsChronoDuration::value ? TextType::chrono : + TextType::other> {}; + +//###################################################################################### + +template +struct ConvertText; +/* -> expected interface +{ + void writeText(const T& value, std::string& output) const; + bool readText(const std::string& input, T& value) const; +}; +*/ + +//partial specialization: type bool +template +struct ConvertText +{ + void writeText(bool value, std::string& output) const + { + output = value ? "true" : "false"; + } + bool readText(const std::string& input, bool& value) const + { + const std::string tmp = trimCpy(input); + if (tmp == "true") + value = true; + else if (tmp == "false") + value = false; + else + return false; + return true; + } +}; + +//partial specialization: handle conversion for all built-in arithmetic types! +template +struct ConvertText +{ + void writeText(const T& value, std::string& output) const + { + output = numberTo(value); + } + bool readText(const std::string& input, T& value) const + { + value = stringTo(input); + return true; + } +}; + +template +struct ConvertText +{ + void writeText(const T& value, std::string& output) const + { + output = numberTo(value.count()); + } + bool readText(const std::string& input, T& value) const + { + value = T(stringTo(input)); + return true; + } +}; + +//partial specialization: handle conversion for all string-like types! +template +struct ConvertText +{ + void writeText(const T& value, std::string& output) const + { + output = utfTo(value); + } + bool readText(const std::string& input, T& value) const + { + value = utfTo(input); + return true; + } +}; + + +//partial specialization: unknown type +template +struct ConvertText +{ + //########################################################################################################################################### + static_assert(sizeof(T) == -1); + /* + ATTENTION: The data type T is yet unknown to the zen::Xml framework! + + Please provide a specialization for T of the following two functions in order to handle conversions to XML elements and attributes + + template <> void zen::writeText(const T& value, std::string& output) + template <> bool zen::readText(const std::string& input, T& value) + + If T is structured and cannot be converted to a text representation specialize these two functions to allow at least for conversions to XML elements: + + template <> void zen::writeStruc(const T& value, XmlElement& output) + template <> bool zen::readStruc(const XmlElement& input, T& value) + */ + //########################################################################################################################################### +}; + + +template inline +void writeText(const T& value, std::string& output) +{ + ConvertText::value>().writeText(value, output); +} + + +template inline +bool readText(const std::string& input, T& value) +{ + return ConvertText::value>().readText(input, value); +} +} + +#endif //CVRT_TEXT_H_018727339083427097434 diff --git a/zenXml/zenxml/dom.h b/zenXml/zenxml/dom.h new file mode 100644 index 0000000..19a07ff --- /dev/null +++ b/zenXml/zenxml/dom.h @@ -0,0 +1,270 @@ +// ***************************************************************************** +// * 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 DOM_H_82085720723894567204564256 +#define DOM_H_82085720723894567204564256 + +#include +#include +#include +#include "cvrt_text.h" //"readText/writeText" + + +namespace zen +{ +class XmlDoc; + +/// An XML element +class XmlElement +{ +public: + XmlElement() {} + + //Construct an empty XML element + explicit XmlElement(std::string name, XmlElement* parent = nullptr) : name_(std::move(name)), parent_(parent) {} + + ///Retrieve the name of this XML element. + /** + \returns Name of the XML element. + */ + const std::string& getName() const { return name_; } + + ///Get the value of this element as a user type. + /** + \tparam T Arbitrary user data type: e.g. any string class, all built-in arithmetic numbers, STL container, ... + \returns "true" if Xml element was successfully converted to value, cannot fail for string-like types + */ + template + bool getValue(T& value) const { return readStruc(*this, value); } + + ///Set the value of this element. + /** + \tparam T Arbitrary user data type: e.g. any string-like type, all built-in arithmetic numbers, STL container, ... + */ + template + void setValue(const T& value) { writeStruc(value, *this); } + + void setValue(std::string&& value) { value_ = std::move(value); } //perf + + ///Retrieve an attribute by name. + /** + \tparam T String-convertible user data type: e.g. any string class, all built-in arithmetic numbers + \param name The name of the attribute to retrieve. + \param value The value of the attribute converted to T. + \return "true" if value was retrieved successfully. + */ + template + bool getAttribute(std::string_view name, T& value) const + { + auto it = attributesByName_.find(name); + return it == attributesByName_.end() ? false : readText(it->second->value, value); + } + + bool hasAttribute(std::string_view name) const { return attributesByName_.contains(name); } + + ///Create or update an XML attribute. + /** + \tparam T String-convertible user data type: e.g. any string-like type, all built-in arithmetic numbers + \param name The name of the attribute to create or update. + \param value The value to set. + */ + template + void setAttribute(std::string&& name, const T& value) + { + std::string attrValue; + writeText(value, attrValue); + + auto it = attributesByName_.find(name); + if (it != attributesByName_.end()) + it->second->value = std::move(attrValue); + else + { + //attributes_.emplace_back(name, std::move(attrValue)); -> not yet on macOS/clang + attributes_.push_back({std::move(name), std::move(attrValue)}); + attributesByName_.emplace(attributes_.back().name, --attributes_.end()); + } + static_assert(std::is_same_v>); //must NOT invalidate references used in "attributesByName_"! + } + + ///Remove the attribute with the given name. + void removeAttribute(std::string_view name) + { + auto it = attributesByName_.find(name); + if (it != attributesByName_.end()) + { + attributes_.erase(it->second); + attributesByName_.erase(it); + } + else assert(false); + } + + ///Create a new child element and return a reference to it. + /** + \param name The name of the child element to be created. + */ + XmlElement& addChild(std::string name) + { + childElements_.emplace_back(name, this); + XmlElement& newElement = childElements_.back(); + childElementByName_.emplace(std::move(name), --childElements_.end()); + + static_assert(std::is_same_v>); //must NOT invalidate references used in "childElementByName_"! + return newElement; + } + + ///Retrieve a child element with the given name. + /** + \param name The name of the child element to be retrieved. + \return A pointer to the child element or nullptr if none was found. + */ + const XmlElement* getChild(const std::string& name) const + { + auto it = childElementByName_.find(name); + return it == childElementByName_.end() ? nullptr : &*(it->second); + } + + ///\sa getChild + XmlElement* getChild(const std::string& name) + { + return const_cast(static_cast(this)->getChild(name)); + } + + ///Access all child elements sequentially + /** + \code + for (const XmlElement& child : elem.getChildren()) + { ... } + \endcode + \return A range object supporting begin/end functions to access all child elements sequentially. */ + Range::const_iterator> getChildren() const { return {childElements_.begin(), childElements_.end()}; } + + ///\sa getChildren + Range::iterator> getChildren() { return {childElements_.begin(), childElements_.end()}; } + + ///Get parent XML element, may be nullptr for root element + XmlElement* parent() { return parent_; } + ///Get parent XML element, may be nullptr for root element + const XmlElement* parent() const { return parent_; } + + struct Attribute + { + const std::string name; + /**/ std::string value; + }; + using AttrIter = std::list::const_iterator; + + /* -> disabled documentation extraction + \brief Get all attributes associated with the element. + \code + auto itPair = elem.getAttributes(); + for (auto it = itPair.first; it != itPair.second; ++it) + std::cout << std::string("name: ") + it->name + " value: " + it->value + '\n'; + \endcode + \return A pair of STL begin/end iterators to access all attributes sequentially as a list of name/value pairs of std::string. */ + std::pair getAttributes() const { return {attributes_.begin(), attributes_.end()}; } + + //swap two elements while keeping references to parent. -> disabled documentation extraction + void swapSubtree(XmlElement& other) noexcept + { + name_ .swap(other.name_); + value_ .swap(other.value_); + attributes_ .swap(other.attributes_); + attributesByName_ .swap(other.attributesByName_); + childElements_ .swap(other.childElements_); + childElementByName_.swap(other.childElementByName_); + + for (XmlElement& child : childElements_) + child.parent_ = this; + for (XmlElement& child : other.childElements_) + child.parent_ = &other; + } + +private: + XmlElement (const XmlElement&) = delete; + XmlElement& operator=(const XmlElement&) = delete; + + std::string name_; + std::string value_; + + std::list attributes_; //attributes in order of insertion + std::unordered_map::iterator> attributesByName_; //alternate view for lookup + + std::list childElements_; //child elements in order of insertion + std::unordered_map::iterator> childElementByName_; //alternate view for lookup of (*first*) child by name + + XmlElement* parent_ = nullptr; //currently unused: YAGNI? +}; + + +//XmlElement::setValue() calls zen::writeStruc() which calls XmlElement::setValue() ... => these two specializations end the circle +template <> inline +void XmlElement::setValue(const std::string& value) { value_ = value; } + +template <> inline +bool XmlElement::getValue(std::string& value) const { value = value_; return true; } + + +///The complete XML document +class XmlDoc +{ +public: + ///Default constructor setting up an empty XML document with a standard declaration: + XmlDoc() {} + + XmlDoc(XmlDoc&& tmp) noexcept { swap(tmp); } + XmlDoc& operator=(XmlDoc&& tmp) noexcept { swap(tmp); return *this; } + + //Setup an empty XML document + /** + \param rootName The name of the XML document's root element. + */ + explicit XmlDoc(std::string rootName) : root_(std::move(rootName)) {} + + ///Get a const reference to the document's root element. + const XmlElement& root() const { return root_; } + ///Get a reference to the document's root element. + XmlElement& root() { return root_; } + + ///Get the version used in the XML declaration. + const std::string& getVersion() const { return version_; } + + ///Set the version used in the XML declaration. + void setVersion(const std::string& version) { version_ = version; } + + ///Get the encoding used in the XML declaration. + const std::string& getEncoding() const { return encoding_; } + + ///Set the encoding used in the XML declaration. + void setEncoding(const std::string& encoding) { encoding_ = encoding; } + + ///Get the standalone string used in the XML declaration. + const std::string& getStandalone() const { return standalone_; } + + ///Set the standalone string used in the XML declaration. + void setStandalone(const std::string& standalone) { standalone_ = standalone; } + + //Transactionally swap two elements. -> disabled documentation extraction + void swap(XmlDoc& other) noexcept + { + version_ .swap(other.version_); + encoding_ .swap(other.encoding_); + standalone_.swap(other.standalone_); + root_.swapSubtree(other.root_); + } + +private: + XmlDoc (const XmlDoc&) = delete; //not implemented, thanks to XmlElement::parent_ + XmlDoc& operator=(const XmlDoc&) = delete; + + std::string version_ {"1.0"}; //non-optional for valid XML + std::string encoding_{"utf-8"}; + std::string standalone_; + + XmlElement root_{"Root"}; +}; +} + +#endif //DOM_H_82085720723894567204564256 diff --git a/zenXml/zenxml/parser.h b/zenXml/zenxml/parser.h new file mode 100644 index 0000000..ed87fe0 --- /dev/null +++ b/zenXml/zenxml/parser.h @@ -0,0 +1,576 @@ +// ***************************************************************************** +// * 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 PARSER_H_81248670213764583021432 +#define PARSER_H_81248670213764583021432 + +#include //ptrdiff_t; req. on Linux +#include +#include "dom.h" + + +namespace zen +{ +/** +\file +\brief Convert an XML document object model (class XmlDoc) to and from a byte stream representation. +*/ + +///Save XML document as a byte stream +/** +\param doc Input XML document +\param lineBreak Line break, default: carriage return + new line +\param indent Indentation, default: four space characters +\return Output byte stream +*/ +std::string serializeXml(const XmlDoc& doc, + const std::string& lineBreak = "\r\n", + const std::string& indent = " "); //noexcept + + +///Exception thrown due to an XML parsing error +struct XmlParsingError +{ + XmlParsingError(size_t rowNo, size_t colNo) : row(rowNo), col(colNo) {} + ///Input file row where the parsing error occured (zero-based) + const size_t row; //beginning with 0 + ///Input file column where the parsing error occured (zero-based) + const size_t col; // +}; + +///Load XML document from a byte stream +/** +\param stream Input byte stream +\returns Output XML document +\throw XmlParsingError +*/ +XmlDoc parseXml(const std::string& stream); //throw XmlParsingError + + + + + + + + + + + + + + + + + + + + +//---------------------------- implementation ---------------------------- +//see: https://www.w3.org/TR/xml/ + +namespace xml_impl +{ +template inline +std::string normalize(const std::string_view& str, Predicate pred) //pred: unary function taking a char, return true if value shall be encoded as hex +{ + std::string output; + for (const char c : str) + switch (c) + { + case '&': output += "&"; break; // + case '<': output += "<"; break; //normalization mandatory: https://www.w3.org/TR/xml/#syntax + case '>': output += ">"; break; // + default: + if (pred(c)) + { + if (c == '\'') output += "'"; + else if (c == '"') output += """; + else + { + output += "&#x"; + const auto [high, low] = hexify(c); + output += high; + output += low; + output += ';'; + } + } + else + output += c; + break; + } + return output; +} + +inline +std::string normalizeName(const std::string& str) +{ + /*const*/ std::string nameFmt = normalize(str, [](const char c) { return isWhiteSpace(c) || c == '=' || c == '/' || c == '\'' || c == '"'; }); + assert(!nameFmt.empty()); + return nameFmt; +} + +inline +std::string normalizeElementValue(const std::string& str) +{ + return normalize(str, [](const char c) { return static_cast(c) < 32; }); +} + +inline +std::string normalizeAttribValue(const std::string& str) +{ + return normalize(str, [](const char c) { return static_cast(c) < 32 || c == '\'' || c == '"'; }); +} + + +template inline +bool checkEntity(CharIterator& first, CharIterator last, const char (&placeholder)[N]) +{ + assert(placeholder[N - 1] == 0); + const ptrdiff_t strLen = N - 1; //don't count null-terminator + if (last - first >= strLen && std::equal(first, first + strLen, placeholder)) + { + first += strLen - 1; + return true; + } + return false; +} + + +namespace +{ +std::string denormalize(const std::string_view& str) +{ + std::string output; + for (auto it = str.begin(); it != str.end(); ++it) + { + const char c = *it; + + if (c == '&') + { + if (checkEntity(it, str.end(), "&")) output += '&'; + else if (checkEntity(it, str.end(), "<")) output += '<'; + else if (checkEntity(it, str.end(), ">")) output += '>'; + else if (checkEntity(it, str.end(), "'")) output += '\''; + else if (checkEntity(it, str.end(), """)) output += '"'; + else if (str.end() - it >= 6 && + it[1] == '#' && + it[2] == 'x' && + isHexDigit(it[3]) && + isHexDigit(it[4]) && + it[5] == ';') + { + output += unhexify(it[3], it[4]); + it += 5; + } + else + output += c; //unexpected char! + } + else if (c == '\r') //map all end-of-line characters to \n https://www.w3.org/TR/xml/#sec-line-ends + { + auto itNext = it + 1; + if (itNext != str.end() && *itNext == '\n') + ++it; + output += '\n'; + } + else + output += c; + } + return output; +} + + +void serialize(const XmlElement& element, std::string& stream, + const std::string& lineBreak, + const std::string& indent, + size_t indentLevel) +{ + const std::string& nameFmt = normalizeName(element.getName()); + + for (size_t i = 0; i < indentLevel; ++i) + stream += indent; + + stream += '<' + nameFmt; + + auto attr = element.getAttributes(); + for (auto it = attr.first; it != attr.second; ++it) + stream += ' ' + normalizeName(it->name) + "=\"" + normalizeAttribValue(it->value) + '"'; + + const auto& children = element.getChildren(); + if (!children.empty()) //structured element + { + //no support for mixed-mode content + stream += '>' + lineBreak; + + for (const XmlElement& el : children) + serialize(el, stream, lineBreak, indent, indentLevel + 1); + + for (size_t i = 0; i < indentLevel; ++i) + stream += indent; + stream += "' + lineBreak; + } + else + { + std::string value; + element.getValue(value); + + if (!value.empty()) //value element + stream += '>' + normalizeElementValue(value) + "' + lineBreak; + else //empty element + stream += "/>" + lineBreak; + } +} +} +} + +inline +std::string serializeXml(const XmlDoc& doc, + const std::string& lineBreak, + const std::string& indent) +{ + std::string output = "" + lineBreak; + + xml_impl::serialize(doc.root(), output, lineBreak, indent, 0 /*indentLevel*/); + return output; +} + +/* +Grammar for XML parser +------------------------------- +document-expression: + + element-expression: + +element-expression: + + pm-expression + +element-list-expression: + + element-expression element-list-expression + +attributes-expression: + + string="string" attributes-expression + +pm-expression: + string + element-list-expression +*/ + +namespace xml_impl +{ +struct Token +{ + enum Type + { + TK_LESS, + TK_GREATER, + TK_LESS_SLASH, + TK_SLASH_GREATER, + TK_EQUAL, + TK_QUOTE, + TK_DECL_BEGIN, + TK_DECL_END, + TK_NAME, + TK_END + }; + + Token(Type t) : type(t) {} + Token(const std::string& txt) : type(TK_NAME), name(txt) {} + Token( std::string&& txt) : type(TK_NAME), name(std::move(txt)) {} + + Type type; + std::string name; //filled if type == TK_NAME +}; + +class Scanner +{ +public: + explicit Scanner(const std::string& stream) : stream_(stream), pos_(stream_.begin()) + { + if (zen::startsWith(stream_, BYTE_ORDER_MARK_UTF8)) + pos_ += BYTE_ORDER_MARK_UTF8.size(); + } + + Token getNextToken() //throw XmlParsingError + { + //skip whitespace + pos_ = std::find_if_not(pos_, stream_.end(), isWhiteSpace); + + if (pos_ == stream_.end()) + return Token::TK_END; + + //skip XML comments + if (startsWith(xmlCommentBegin_)) + { + auto it = std::search(pos_ + xmlCommentBegin_.size(), stream_.end(), xmlCommentEnd_.begin(), xmlCommentEnd_.end()); + if (it != stream_.end()) + { + pos_ = it + xmlCommentEnd_.size(); + return getNextToken(); //throw XmlParsingError + } + } + + for (auto it = tokens_.begin(); it != tokens_.end(); ++it) + if (startsWith(it->first)) + { + pos_ += it->first.size(); + return it->second; + } + + const auto itNameEnd = std::find_if(pos_, stream_.end(), [](const char c) + { + return c == '<' || + c == '>' || + c == '=' || + c == '/' || + c == '\'' || + c == '"' || + isWhiteSpace(c); + }); + + if (itNameEnd != pos_) + { + const std::string_view name = makeStringView(pos_, itNameEnd); + pos_ = itNameEnd; + return denormalize(name); + } + + //unknown token + throw XmlParsingError(posRow(), posCol()); + } + + std::string extractElementValue() + { + auto it = std::find_if(pos_, stream_.end(), [](const char c) + { + return c == '<' || + c == '>'; + }); + const std::string_view output = makeStringView(pos_, it); + pos_ = it; + return denormalize(output); + } + + std::string extractAttributeValue() + { + auto it = std::find_if(pos_, stream_.end(), [](const char c) + { + return c == '<' || + c == '>' || + c == '\'' || + c == '"'; + }); + const std::string_view output = makeStringView(pos_, it); + pos_ = it; + return denormalize(output); + } + + size_t posRow() const //current row beginning with 0 + { + const size_t crSum = std::count(stream_.begin(), pos_, '\r'); //carriage returns + const size_t nlSum = std::count(stream_.begin(), pos_, '\n'); //new lines + assert(crSum == 0 || nlSum == 0 || crSum == nlSum); + return std::max(crSum, nlSum); //be compatible with Linux/Mac/Win + } + + size_t posCol() const //current col beginning with 0 + { + //seek beginning of line + for (auto it = pos_; it != stream_.begin(); ) + { + --it; + if (isLineBreak(*it)) + return pos_ - it - 1; + } + return pos_ - stream_.begin(); + } + +private: + Scanner (const Scanner&) = delete; + Scanner& operator=(const Scanner&) = delete; + + bool startsWith(const std::string& prefix) const + { + return zen::startsWith(makeStringView(pos_, stream_.end()), prefix); + } + + using TokenList = std::vector>; + const TokenList tokens_ + { + {"", Token::TK_DECL_END }, + {"", Token::TK_SLASH_GREATER}, + {"<", Token::TK_LESS }, //evaluate after TK_DECL_BEGIN! + {">", Token::TK_GREATER }, + {"=", Token::TK_EQUAL }, + {"\"", Token::TK_QUOTE }, + {"\'", Token::TK_QUOTE }, + }; + + const std::string xmlCommentBegin_ = ""; + + const std::string stream_; + std::string::const_iterator pos_; +}; + + +class XmlParser +{ +public: + explicit XmlParser(const std::string& stream) : + scn_(stream), + tk_(scn_.getNextToken()) {} //throw XmlParsingError + + XmlDoc parse() //throw XmlParsingError + { + XmlDoc doc; + + //declaration (optional) + if (token().type == Token::TK_DECL_BEGIN) + { + nextToken(); //throw XmlParsingError + + while (token().type == Token::TK_NAME) + { + std::string attribName = token().name; + nextToken(); //throw XmlParsingError + + consumeToken(Token::TK_EQUAL); //throw XmlParsingError + expectToken (Token::TK_QUOTE); // + std::string attribValue = scn_.extractAttributeValue(); + nextToken(); //throw XmlParsingError + + consumeToken(Token::TK_QUOTE); //throw XmlParsingError + + if (attribName == "version") + doc.setVersion(attribValue); + else if (attribName == "encoding") + doc.setEncoding(attribValue); + else if (attribName == "standalone") + doc.setStandalone(attribValue); + } + consumeToken(Token::TK_DECL_END); //throw XmlParsingError + } + + XmlElement dummy; + parseChildElements(dummy); + + const auto& children = dummy.getChildren(); + if (!children.empty()) + doc.root().swapSubtree(*children.begin()); + + expectToken(Token::TK_END); //throw XmlParsingError + return doc; + } + +private: + XmlParser (const XmlParser&) = delete; + XmlParser& operator=(const XmlParser&) = delete; + + void parseChildElements(XmlElement& parent) + { + while (token().type == Token::TK_LESS) + { + nextToken(); //throw XmlParsingError + + expectToken(Token::TK_NAME); //throw XmlParsingError + const std::string elementName = token().name; + nextToken(); //throw XmlParsingError + + XmlElement& newElement = parent.addChild(elementName); + + parseAttributes(newElement); + + if (token().type == Token::TK_SLASH_GREATER) //empty element + { + nextToken(); //throw XmlParsingError + continue; + } + + expectToken(Token::TK_GREATER); //throw XmlParsingError + std::string elementValue = scn_.extractElementValue(); + nextToken(); //throw XmlParsingError + + //no support for mixed-mode content + if (token().type == Token::TK_LESS) //structure-element + parseChildElements(newElement); + else //value-element + newElement.setValue(std::move(elementValue)); + + consumeToken(Token::TK_LESS_SLASH); //throw XmlParsingError + + expectToken(Token::TK_NAME); //throw XmlParsingError + if (token().name != elementName) + throw XmlParsingError(scn_.posRow(), scn_.posCol()); + nextToken(); //throw XmlParsingError + + consumeToken(Token::TK_GREATER); //throw XmlParsingError + } + } + + void parseAttributes(XmlElement& element) + { + while (token().type == Token::TK_NAME) + { + std::string attribName = token().name; + nextToken(); //throw XmlParsingError + + consumeToken(Token::TK_EQUAL); //throw XmlParsingError + expectToken (Token::TK_QUOTE); // + std::string attribValue = scn_.extractAttributeValue(); + nextToken(); //throw XmlParsingError + + consumeToken(Token::TK_QUOTE); //throw XmlParsingError + element.setAttribute(std::move(attribName), attribValue); + } + } + + const Token& token() const { return tk_; } + + void nextToken() { tk_ = scn_.getNextToken(); } //throw XmlParsingError + + void expectToken(Token::Type t) //throw XmlParsingError + { + if (token().type != t) + throw XmlParsingError(scn_.posRow(), scn_.posCol()); + } + + void consumeToken(Token::Type t) //throw XmlParsingError + { + expectToken(t); //throw XmlParsingError + nextToken(); // + } + + Scanner scn_; + Token tk_; +}; +} + +inline +XmlDoc parseXml(const std::string& stream) //throw XmlParsingError +{ + return xml_impl::XmlParser(stream).parse(); //throw XmlParsingError +} +} + +#endif //PARSER_H_81248670213764583021432 diff --git a/zenXml/zenxml/xml.h b/zenXml/zenxml/xml.h new file mode 100644 index 0000000..03974fa --- /dev/null +++ b/zenXml/zenxml/xml.h @@ -0,0 +1,405 @@ +// ***************************************************************************** +// * 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 XML_H_349578228034572457454554 +#define XML_H_349578228034572457454554 + +#include +#include +#include "cvrt_struc.h" +#include "parser.h" + + +/// The zen::Xml namespace +namespace zen +{ +/** +\file +\brief Save and load byte streams from files +*/ + +///Load XML document from a file +/** +Load and parse XML byte stream. Quick-exit if (potentially large) input file is not an XML. + +\param filePath Input file path +\returns The loaded XML document +\throw FileError +*/ +namespace +{ +XmlDoc loadXml(const Zstring& filePath) //throw FileError +{ + FileInputPlain fileIn(filePath); //throw FileError, ErrorFileLocked + std::string headBuf; + const size_t headSizeMin = BYTE_ORDER_MARK_UTF8.size() + strLength(""); + + const std::string buf = unbufferedLoad([&](void* buffer, size_t bytesToRead) + { + const size_t bytesRead = fileIn.tryRead(buffer, bytesToRead); //throw FileError; may return short, only 0 means EOF! => CONTRACT: bytesToRead > 0! + + //quick test whether input is an XML: avoid loading large binary files up front! + if (headBuf.size() < headSizeMin) + { + headBuf.append(static_cast(buffer), std::min(headSizeMin - headBuf.size(), bytesRead)); + + if (headBuf.size() == headSizeMin) + { + std::string_view header = headBuf; + if (startsWith(header, BYTE_ORDER_MARK_UTF8)) + header.remove_prefix(BYTE_ORDER_MARK_UTF8.size()); //keep headBuf.size()! + + if (!startsWith(header, "")) + throw FileError(replaceCpy(_("File %x does not contain a valid configuration."), L"%x", fmtPath(filePath))); + } + } + return bytesRead; + }, + fileIn.getBlockSize()); //throw FileError + + try + { + return parseXml(buf); //throw XmlParsingError + } + catch (const XmlParsingError& e) + { + throw FileError( + replaceCpy(replaceCpy(replaceCpy(_("Error parsing file %x, row %y, column %z."), + L"%x", fmtPath(filePath)), + L"%y", formatNumber(e.row + 1)), + L"%z", formatNumber(e.col + 1))); + } +} +} + + +///Save XML document to a file +/** +Serialize XML to byte stream and save to file. + +\param doc The XML document to save +\param filePath Output file path +\throw FileError +*/ +inline +void saveXml(const XmlDoc& doc, const Zstring& filePath) //throw FileError +{ + const std::string stream = serializeXml(doc); //noexcept + + try //only update XML file if there are changes + { + if (getFileSize(filePath) == stream.size()) //throw FileError + if (getFileContent(filePath, nullptr /*notifyUnbufferedIO*/) == stream) //throw FileError + return; + } + catch (FileError&) {} + + setFileContent(filePath, stream, nullptr /*notifyUnbufferedIO*/); //throw FileError +} + + +///Proxy class to conveniently convert user data into XML structure +class XmlOut +{ +public: + ///Construct an output proxy for an XML document + /** + \code + zen::XmlDoc doc; + + zen::XmlOut out(doc); + out["elem1"]( 1); // + out["elem2"]( 2); //write data into XML elements + out["elem3"](-3); // + + saveXml(doc, "out.xml"); //throw FileError + \endcode + Output: + \verbatim + + + 1 + 2 + -3 + + \endverbatim + */ + explicit XmlOut(XmlDoc& doc) : ref_(doc.root()) {} + + ///Retrieve a handle to an XML child element for writing + /** + The child element will be created if it is not yet existing. + \param name The name of the child element + */ + XmlOut operator[](std::string name) const + { + XmlElement* child = ref_.getChild(name); + return XmlOut(child ? *child : ref_.addChild(std::move(name))); + } + + ///Retrieve a handle to an XML child element for writing + /** + The child element will be added, allowing for multiple elements with the same name. + \tparam String Arbitrary string-like type: e.g. std::string, wchar_t*, char[], wchar_t, wxString, MyStringClass, ... + \param name The name of the child element + */ + XmlOut addChild(std::string name) const + { + return XmlOut(ref_.addChild(std::move(name))); + } + + ///Write user data to the underlying XML element + /** + This conversion requires a specialization of zen::writeText() or zen::writeStruc() for type T. + \tparam T User type that is converted into an XML element value. + */ + template + void operator()(const T& value) { writeStruc(value, ref_); } + + ///Write user data to an XML attribute + /** + This conversion requires a specialization of zen::writeText() for type T. + \code + zen::XmlDoc doc; + + zen::XmlOut out(doc); + out["elem"].attribute("attr1", 1); // + out["elem"].attribute("attr2", 2); //write data into XML attributes + out["elem"].attribute("attr3", -3); // + + saveXml(doc, "out.xml"); //throw FileError + \endcode + Output: + \verbatim + + + + + \endverbatim + + \tparam T String-convertible user data type: e.g. any string-like type, all built-in arithmetic numbers + \sa XmlElement::setAttribute() + */ + template + void attribute(std::string name, const T& value) { ref_.setAttribute(std::move(name), value); } + +private: + ///Construct an output proxy for a single XML element + /** + \sa XmlOut(XmlDoc& doc) + */ + explicit XmlOut(XmlElement& element) : ref_(element) {} + + XmlElement& ref_; +}; + + +///Proxy class to conveniently convert XML structure to user data +class XmlIn +{ + struct ErrorLog; + +public: + ///Construct an input proxy for an XML document + /** + \code + zen::XmlDoc doc; + ... //load document + zen::XmlIn in(doc); + in["elem1"](value1); // + in["elem2"](value2); //read data from XML elements into variables "value1", "value2", "value3" + in["elem3"](value3); // + \endcode + */ + explicit XmlIn(const XmlDoc& doc) : XmlIn(&doc.root(), '<' + doc.root().getName() + '>', makeSharedRef()) {} + + ///Retrieve a handle to an XML child element for reading + /** + It is \b not an error if the child element does not exist, but only later if a conversion to user data is attempted. + \param name The name of the child element + */ + XmlIn operator[](const std::string& name) const + { + return XmlIn(elem_ ? elem_->getChild(name) : nullptr, elementNameFmt_ + " <" + name + '>', log_); + } + + ///Iterate over XML child elements + /** + Example: Loop over all XML child elements + \verbatim + + + 1 + 3 + 5 + + \endverbatim + + \code + zen::XmlIn in(doc); + ... + in.visitChildren([&](const XmlIn& inChild) + { + ... + }); + \endcode + */ + template + void visitChildren(Function fun) + { + if (!elem_) + logMissingElement(); + else if (std::string value; elem_->getValue(value) && !value.empty()) + logConversionError(); //have XML value element, not container! + else + { + size_t childIdx = 0; + for (const XmlElement& child : elem_->getChildren()) + fun(XmlIn(&child, elementNameFmt_ + " <" + child.getName() + ">[" + numberTo(++childIdx) + ']', log_)); + } + } + + ///Test whether the underlying XML element exists + /** + \code + XmlIn in(doc); + XmlIn child = in["elem1"]; + if (child) + ... + \endcode + Use member pointer as implicit conversion to bool (C++ Templates - Vandevoorde/Josuttis; chapter 20) + */ + explicit operator bool() const { return elem_; } + + ///Read user data from the underlying XML element + /** + This conversion requires a specialization of zen::readText() or zen::readStruc() for type T. + \tparam T User type that receives the data + \return "true" if data was read successfully + */ + template + bool operator()(T& value) const + { + if (elem_) + { + if (readStruc(*elem_, value)) + return true; + + logConversionError(); + } + else + logMissingElement(); + + return false; + } + + bool hasAttribute(const std::string& name) const + { + return elem_ && elem_->hasAttribute(name); + } + + ///Read user data from an XML attribute + /** + This conversion requires a specialization of zen::readText() for type T. + + \code + zen::XmlDoc doc; + ... //load document + zen::XmlIn in(doc); + in["elem"].attribute("attr1", value1); // + in["elem"].attribute("attr2", value2); //read data from XML attributes into variables "value1", "value2", "value3" + in["elem"].attribute("attr3", value3); // + \endcode + + \tparam String Arbitrary string-like type: e.g. std::string, wchar_t*, char[], wchar_t, wxString, MyStringClass, ... + \tparam T String-convertible user data type: e.g. any string-like type, all built-in arithmetic numbers + \returns "true" if the attribute was found and the conversion to the output value was successful. + \sa XmlElement::getAttribute() + */ + template + bool attribute(const std::string& name, T& value) const + { + if (elem_) + { + if (elem_->getAttribute(name, value)) + return true; + + logMissingAttribute(name); + } + else + logMissingElement(); + + return false; + } + + ///Notifies errors while mapping the XML to user data + /** + Error logging is shared by each hiearchy of XmlIn proxy instances that are created from each other. Consequently it doesn't matter which instance you query for errors: + \code + XmlIn in(doc); + XmlIn inItem = in["item1"]; + + int value = 0; + inItem(value); //let's assume this conversion failed + + assert(in.getErrors() == inItem.getErrors()); + \endcode + + Note that error logging is \b NOT global, but owned by all instances of a hierarchy of XmlIn proxies. + Therefore it's safe to use unrelated XmlIn proxies in different threads. + */ + + ///Get a list of XML element and attribute names which failed to convert to user data. + /** + \returns A list of XML element and attribute names, empty if no errors occured. + */ + const std::wstring& getErrors() const { return log_.ref().failedElements; } + + ///Retrieve the name of this XML element. + /** + \returns Name of the XML element. + */ + const std::string* getName() const + { + if (elem_) + return &elem_->getName(); + return nullptr; + } + +private: + XmlIn(const XmlElement* elem, + const std::string& elementNameFmt, + const SharedRef& sharedlog) : log_(sharedlog), elem_(elem), elementNameFmt_(elementNameFmt) {} + + struct ErrorLog + { + std::wstring failedElements; //unique list of failed elements + std::unordered_set usedElements; + }; + + void logElementError(const std::string& elementName) const + { + if (const auto [it, inserted] = log_.ref().usedElements.insert(elementName); + inserted) + { + if (!log_.ref().failedElements.empty()) + log_.ref().failedElements += L'\n'; + log_.ref().failedElements += utfTo(elementName); + } + } + + void logConversionError() const { logElementError(elementNameFmt_); } + void logMissingElement() const { logElementError(elementNameFmt_); } + void logMissingAttribute(const std::string& attribName) const { logElementError(elementNameFmt_ + " @" + attribName); } + + mutable SharedRef log_; + const XmlElement* elem_; + std::string elementNameFmt_; //e.g. " [1]" +}; +} + +#endif //XML_H_349578228034572457454554