From b2801fb887fe40875b3ec90619b011b45c1d2796 Mon Sep 17 00:00:00 2001 From: B Stack Date: Fri, 19 Jun 2020 16:18:18 -0400 Subject: add upstream 10.25 --- Changelog.txt | 47 +- FreeFileSync/Build/Resources/Icons.zip | Bin 393061 -> 398861 bytes FreeFileSync/Build/Resources/Languages.zip | Bin 522226 -> 522253 bytes FreeFileSync/Source/Makefile | 0 FreeFileSync/Source/RealTimeSync/Makefile | 0 FreeFileSync/Source/RealTimeSync/application.cpp | 14 +- FreeFileSync/Source/RealTimeSync/application.h | 4 +- .../Source/RealTimeSync/folder_selector2.cpp | 2 +- FreeFileSync/Source/RealTimeSync/gui_generated.cpp | 404 ++--- FreeFileSync/Source/RealTimeSync/gui_generated.h | 144 +- FreeFileSync/Source/RealTimeSync/tray_menu.cpp | 2 +- FreeFileSync/Source/afs/abstract.cpp | 24 +- FreeFileSync/Source/afs/abstract.h | 87 +- FreeFileSync/Source/afs/abstract_impl.h | 2 +- FreeFileSync/Source/afs/concrete.cpp | 4 +- FreeFileSync/Source/afs/ftp.cpp | 252 +-- FreeFileSync/Source/afs/ftp.h | 8 +- FreeFileSync/Source/afs/gdrive.cpp | 1836 +++++++++++++------- FreeFileSync/Source/afs/gdrive.h | 27 +- FreeFileSync/Source/afs/native.cpp | 72 +- FreeFileSync/Source/afs/sftp.cpp | 157 +- FreeFileSync/Source/afs/sftp.h | 10 +- FreeFileSync/Source/application.cpp | 17 +- FreeFileSync/Source/application.h | 4 +- FreeFileSync/Source/base/algorithm.cpp | 8 +- FreeFileSync/Source/base/algorithm.h | 2 +- FreeFileSync/Source/base/comparison.cpp | 16 +- FreeFileSync/Source/base/db_file.cpp | 22 +- FreeFileSync/Source/base/dir_exist_async.h | 2 +- FreeFileSync/Source/base/parallel_scan.cpp | 6 +- FreeFileSync/Source/base/synchronization.cpp | 46 +- FreeFileSync/Source/base/versioning.cpp | 8 +- FreeFileSync/Source/config.cpp | 12 +- FreeFileSync/Source/config.h | 3 +- FreeFileSync/Source/icon_buffer.cpp | 2 +- FreeFileSync/Source/localization.cpp | 4 +- FreeFileSync/Source/log_file.h | 1 - FreeFileSync/Source/ui/abstract_folder_picker.cpp | 6 +- FreeFileSync/Source/ui/app_icon.h | 2 +- FreeFileSync/Source/ui/cfg_grid.cpp | 2 +- FreeFileSync/Source/ui/file_grid.cpp | 1418 +++++++-------- FreeFileSync/Source/ui/file_grid.h | 1 - FreeFileSync/Source/ui/file_view.cpp | 424 +++-- FreeFileSync/Source/ui/file_view.h | 87 +- FreeFileSync/Source/ui/folder_selector.cpp | 4 +- FreeFileSync/Source/ui/gui_generated.cpp | 113 +- FreeFileSync/Source/ui/gui_generated.h | 13 +- FreeFileSync/Source/ui/main_dlg.cpp | 61 +- FreeFileSync/Source/ui/small_dlgs.cpp | 175 +- FreeFileSync/Source/ui/small_dlgs.h | 1 + FreeFileSync/Source/ui/tray_icon.cpp | 2 +- FreeFileSync/Source/version/version.h | 2 +- libcurl/rest.cpp | 2 +- wx+/dc.h | 10 +- wx+/graph.cpp | 4 +- wx+/grid.cpp | 51 +- wx+/grid.h | 7 +- xBRZ/src/xbrz.cpp | 51 +- zen/crc.h | 101 +- zen/file_access.cpp | 43 +- zen/file_access.h | 8 +- zen/json.h | 2 +- zen/legacy_compiler.h | 1 - zen/open_ssl.cpp | 1 + zen/recycler.cpp | 2 +- zen/scope_guard.h | 3 +- zen/serialize.h | 11 +- zen/stl_tools.h | 68 +- zen/string_base.h | 2 +- zen/string_tools.h | 8 +- zen/zstring.cpp | 8 +- zen/zstring.h | 12 +- 72 files changed, 3445 insertions(+), 2510 deletions(-) mode change 100644 => 100755 FreeFileSync/Source/Makefile mode change 100644 => 100755 FreeFileSync/Source/RealTimeSync/Makefile diff --git a/Changelog.txt b/Changelog.txt index 97ed93d6..e7539426 100755 --- a/Changelog.txt +++ b/Changelog.txt @@ -1,3 +1,26 @@ +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 notificaton 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 @@ -9,7 +32,7 @@ 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 on Windows Shell errors +Show error code constants for Windows Shell errors Suppport ProFTPD with "MultilineRFC2228 on" SFTP option to enable/disable zlib compression @@ -299,7 +322,7 @@ Fixed parsing locale with unexpected format (Linux) FreeFileSync 10.6 [2018-11-12] ------------------------------ -Detect and skip traversing folder path aliases +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) @@ -745,7 +768,7 @@ Updated help file FreeFileSync 8.0 [2016-03-15] ----------------------------- -Fine-tuned buffer sizes for 70% improved SFTP stream I/O speed +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 @@ -934,7 +957,7 @@ 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 +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 @@ -1072,7 +1095,7 @@ 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 @@ -1179,7 +1202,7 @@ 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 +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 @@ -1286,7 +1309,7 @@ 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 +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 @@ -1779,7 +1802,7 @@ 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 +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 @@ -1999,7 +2022,7 @@ Merge multiple *.ffs_batch, *.ffs_gui files or combinations of both via drag & d Copy file and folder permissions (requires admin rights): - Windows: owner, group, DACL, SACL - Linux: owner, group, permissions - - correctly handle Symbolic Links + - 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 @@ -2358,7 +2381,7 @@ 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) +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 @@ -2495,7 +2518,7 @@ 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 +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) @@ -2509,7 +2532,7 @@ Maintain and load different configurations by drag&drop, load-button or command 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 +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 diff --git a/FreeFileSync/Build/Resources/Icons.zip b/FreeFileSync/Build/Resources/Icons.zip index 5448052f..2da8cf06 100644 Binary files a/FreeFileSync/Build/Resources/Icons.zip and b/FreeFileSync/Build/Resources/Icons.zip differ diff --git a/FreeFileSync/Build/Resources/Languages.zip b/FreeFileSync/Build/Resources/Languages.zip index 6222bbbd..f56421ed 100644 Binary files a/FreeFileSync/Build/Resources/Languages.zip and b/FreeFileSync/Build/Resources/Languages.zip differ diff --git a/FreeFileSync/Source/Makefile b/FreeFileSync/Source/Makefile old mode 100644 new mode 100755 diff --git a/FreeFileSync/Source/RealTimeSync/Makefile b/FreeFileSync/Source/RealTimeSync/Makefile old mode 100644 new mode 100755 diff --git a/FreeFileSync/Source/RealTimeSync/application.cpp b/FreeFileSync/Source/RealTimeSync/application.cpp index 5f2531f1..f69488dd 100644 --- a/FreeFileSync/Source/RealTimeSync/application.cpp +++ b/FreeFileSync/Source/RealTimeSync/application.cpp @@ -153,10 +153,17 @@ void Application::onEnterEventLoop(wxEvent& event) int Application::OnRun() +{ + [[maybe_unused]] const int rc = wxApp::OnRun(); + return fff::FFS_EXIT_SUCCESS; //process exit code +} + + +void Application::OnUnhandledException() //handles both wxApp::OnInit() + wxApp::OnRun() { try { - wxApp::OnRun(); + throw; //just re-throw and avoid display of additional messagebox } catch (const std::bad_alloc& e) //the only kind of exception we don't want crash dumps for { @@ -164,15 +171,12 @@ int Application::OnRun() const auto titleFmt = copyStringTo(wxTheApp->GetAppDisplayName()) + SPACED_DASH + _("An exception occurred"); std::cerr << utfTo(titleFmt + SPACED_DASH) << e.what() << '\n'; - return fff::FFS_EXIT_EXCEPTION; + terminateProcess(fff::FFS_EXIT_EXCEPTION); } //catch (...) -> let it crash and create mini dump!!! - - return fff::FFS_EXIT_SUCCESS; //program's return code } - void Application::onQueryEndSession(wxEvent& event) { if (auto mainWin = dynamic_cast(GetTopWindow())) diff --git a/FreeFileSync/Source/RealTimeSync/application.h b/FreeFileSync/Source/RealTimeSync/application.h index 9c632686..c6dbccaa 100644 --- a/FreeFileSync/Source/RealTimeSync/application.h +++ b/FreeFileSync/Source/RealTimeSync/application.h @@ -18,8 +18,8 @@ private: bool OnInit() override; int OnRun () override; int OnExit() override; - bool OnExceptionInMainLoop() override { throw; } //just re-throw and avoid display of additional messagebox: it will be caught in OnRun() - void OnUnhandledException () override { throw; } //just re-throw and avoid display of additional messagebox + bool OnExceptionInMainLoop() override { throw; } //just re-throw and avoid display of additional messagebox: it will be caught in OnUnhandledException() + void OnUnhandledException () override; wxLayoutDirection GetLayoutDirection() const override; void onEnterEventLoop(wxEvent& event); diff --git a/FreeFileSync/Source/RealTimeSync/folder_selector2.cpp b/FreeFileSync/Source/RealTimeSync/folder_selector2.cpp index e4b4a451..af93cea2 100644 --- a/FreeFileSync/Source/RealTimeSync/folder_selector2.cpp +++ b/FreeFileSync/Source/RealTimeSync/folder_selector2.cpp @@ -105,7 +105,7 @@ void FolderSelector2::onFilesDropped(FileDropEvent& event) Zstring itemPath = itemPaths[0]; try { - if (getItemType(itemPath) == ItemType::FILE) //throw FileError + if (getItemType(itemPath) == ItemType::file) //throw FileError if (std::optional parentPath = getParentFolderPath(itemPath)) itemPath = *parentPath; } diff --git a/FreeFileSync/Source/RealTimeSync/gui_generated.cpp b/FreeFileSync/Source/RealTimeSync/gui_generated.cpp index 318c8f1b..7b60ec16 100644 --- a/FreeFileSync/Source/RealTimeSync/gui_generated.cpp +++ b/FreeFileSync/Source/RealTimeSync/gui_generated.cpp @@ -13,297 +13,297 @@ 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 ) ); + 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 ); + 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_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 ); + 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_menuFile->AppendSeparator(); - m_menuItemQuit = new wxMenuItem( m_menuFile, wxID_EXIT, wxString( _("E&xit") ), wxEmptyString, wxITEM_NORMAL ); - m_menuFile->Append( m_menuItemQuit ); + 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_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 = 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_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_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") ); + m_menubar1->Append( m_menuHelp, _("&Help") ); - this->SetMenuBar( m_menubar1 ); + this->SetMenuBar( m_menubar1 ); - bSizerMain = new wxBoxSizer( wxVERTICAL ); + bSizerMain = new wxBoxSizer( wxVERTICAL ); - wxBoxSizer* bSizer161; - bSizer161 = new wxBoxSizer( wxVERTICAL ); + wxBoxSizer* bSizer161; + bSizer161 = new wxBoxSizer( wxVERTICAL ); - wxBoxSizer* bSizer16; - bSizer16 = new wxBoxSizer( wxHORIZONTAL ); + wxBoxSizer* bSizer16; + bSizer16 = new wxBoxSizer( wxHORIZONTAL ); - m_staticText9 = new wxStaticText( this, wxID_ANY, _("Usage:"), wxDefaultPosition, wxDefaultSize, 0 ); - m_staticText9->Wrap( -1 ); - m_staticText9->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + m_staticText9 = new wxStaticText( this, wxID_ANY, _("Usage:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText9->Wrap( -1 ); + m_staticText9->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); - bSizer16->Add( m_staticText9, 0, wxALL, 5 ); + bSizer16->Add( m_staticText9, 0, wxALL, 5 ); - ffgSizer111 = new wxFlexGridSizer( 0, 2, 5, 5 ); - ffgSizer111->SetFlexibleDirection( wxBOTH ); - ffgSizer111->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); + ffgSizer111 = new wxFlexGridSizer( 0, 2, 5, 5 ); + ffgSizer111->SetFlexibleDirection( wxBOTH ); + ffgSizer111->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); - m_staticText16 = new wxStaticText( this, wxID_ANY, _("1."), wxDefaultPosition, wxDefaultSize, 0 ); - m_staticText16->Wrap( -1 ); - ffgSizer111->Add( m_staticText16, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 ); + m_staticText16 = new wxStaticText( this, wxID_ANY, _("1."), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText16->Wrap( -1 ); + ffgSizer111->Add( m_staticText16, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 ); - m_staticText3 = new wxStaticText( this, wxID_ANY, _("Select folders to watch."), wxDefaultPosition, wxDefaultSize, 0 ); - m_staticText3->Wrap( -1 ); - ffgSizer111->Add( m_staticText3, 0, wxALIGN_CENTER_VERTICAL, 5 ); + m_staticText3 = new wxStaticText( this, wxID_ANY, _("Select folders to watch."), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText3->Wrap( -1 ); + ffgSizer111->Add( m_staticText3, 0, wxALIGN_CENTER_VERTICAL, 5 ); - m_staticText17 = new wxStaticText( this, wxID_ANY, _("2."), wxDefaultPosition, wxDefaultSize, 0 ); - m_staticText17->Wrap( -1 ); - ffgSizer111->Add( m_staticText17, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 ); + m_staticText17 = new wxStaticText( this, wxID_ANY, _("2."), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText17->Wrap( -1 ); + ffgSizer111->Add( m_staticText17, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 ); - m_staticText4 = new wxStaticText( this, wxID_ANY, _("Enter a command line."), wxDefaultPosition, wxDefaultSize, 0 ); - m_staticText4->Wrap( -1 ); - ffgSizer111->Add( m_staticText4, 0, wxALIGN_CENTER_VERTICAL, 5 ); + m_staticText4 = new wxStaticText( this, wxID_ANY, _("Enter a command line."), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText4->Wrap( -1 ); + ffgSizer111->Add( m_staticText4, 0, wxALIGN_CENTER_VERTICAL, 5 ); - m_staticText18 = new wxStaticText( this, wxID_ANY, _("3."), wxDefaultPosition, wxDefaultSize, 0 ); - m_staticText18->Wrap( -1 ); - ffgSizer111->Add( m_staticText18, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 ); + m_staticText18 = new wxStaticText( this, wxID_ANY, _("3."), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText18->Wrap( -1 ); + ffgSizer111->Add( m_staticText18, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 ); - m_staticText5 = new wxStaticText( this, wxID_ANY, _("Press 'Start'."), wxDefaultPosition, wxDefaultSize, 0 ); - m_staticText5->Wrap( -1 ); - ffgSizer111->Add( m_staticText5, 0, wxALIGN_CENTER_VERTICAL, 5 ); + m_staticText5 = new wxStaticText( this, wxID_ANY, _("Press 'Start'."), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText5->Wrap( -1 ); + ffgSizer111->Add( m_staticText5, 0, wxALIGN_CENTER_VERTICAL, 5 ); - bSizer16->Add( ffgSizer111, 0, wxALL, 5 ); + bSizer16->Add( ffgSizer111, 0, wxALL, 5 ); - bSizer161->Add( bSizer16, 0, 0, 5 ); + bSizer161->Add( bSizer16, 0, 0, 5 ); - wxBoxSizer* bSizer152; - bSizer152 = new wxBoxSizer( wxHORIZONTAL ); + wxBoxSizer* bSizer152; + bSizer152 = new wxBoxSizer( wxHORIZONTAL ); - 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 ) ); + 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 ); + bSizer152->Add( m_staticText811, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 ); - m_staticText10 = new wxStaticText( this, wxID_ANY, _("("), wxDefaultPosition, wxDefaultSize, 0 ); - m_staticText10->Wrap( -1 ); - m_staticText10->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + 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 ); + 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 ); + m_bitmapBatch = new wxStaticBitmap( this, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer152->Add( m_bitmapBatch, 0, wxALIGN_CENTER_VERTICAL, 5 ); - m_staticText11 = new wxStaticText( this, wxID_ANY, _(")"), wxDefaultPosition, wxDefaultSize, 0 ); - m_staticText11->Wrap( -1 ); - m_staticText11->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + 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 ); + bSizer152->Add( m_staticText11, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT, 2 ); - m_hyperlink243 = new wxHyperlinkCtrl( this, wxID_ANY, _("Show examples"), wxEmptyString, wxDefaultPosition, wxDefaultSize, wxHL_DEFAULT_STYLE ); - bSizer152->Add( m_hyperlink243, 0, wxALIGN_CENTER_VERTICAL|wxLEFT, 5 ); + m_hyperlink243 = new wxHyperlinkCtrl( this, wxID_ANY, _("Show examples"), wxEmptyString, wxDefaultPosition, wxDefaultSize, wxHL_DEFAULT_STYLE ); + bSizer152->Add( m_hyperlink243, 0, wxALIGN_CENTER_VERTICAL|wxLEFT, 5 ); - bSizer161->Add( bSizer152, 0, wxALL|wxALIGN_CENTER_HORIZONTAL, 5 ); + bSizer161->Add( bSizer152, 0, wxALL|wxALIGN_CENTER_HORIZONTAL, 5 ); - bSizerMain->Add( bSizer161, 0, wxALL|wxEXPAND, 5 ); + bSizerMain->Add( bSizer161, 0, wxALL|wxEXPAND, 5 ); - m_staticline2 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); - bSizerMain->Add( m_staticline2, 0, wxEXPAND, 5 ); + 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 ) ); + 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* bSizer1; + bSizer1 = new wxBoxSizer( wxVERTICAL ); - wxBoxSizer* bSizer151; - bSizer151 = new wxBoxSizer( wxVERTICAL ); + wxBoxSizer* bSizer151; + bSizer151 = new wxBoxSizer( wxVERTICAL ); - wxBoxSizer* bSizer142; - bSizer142 = new wxBoxSizer( wxHORIZONTAL ); + 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 ); + m_bitmapFolders = new wxStaticBitmap( m_panelMain, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer142->Add( m_bitmapFolders, 0, wxTOP|wxBOTTOM|wxLEFT, 5 ); - m_staticText7 = new wxStaticText( m_panelMain, wxID_ANY, _("Folders to watch:"), wxDefaultPosition, wxDefaultSize, 0 ); - m_staticText7->Wrap( -1 ); - bSizer142->Add( m_staticText7, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + m_staticText7 = new wxStaticText( m_panelMain, wxID_ANY, _("Folders to watch:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText7->Wrap( -1 ); + bSizer142->Add( m_staticText7, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); - bSizer151->Add( bSizer142, 0, 0, 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 ) ); + m_panelMainFolder = new wxPanel( m_panelMain, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panelMainFolder->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); - wxFlexGridSizer* fgSizer1; - fgSizer1 = new wxFlexGridSizer( 0, 2, 0, 0 ); - fgSizer1->AddGrowableCol( 1 ); - fgSizer1->SetFlexibleDirection( wxBOTH ); - fgSizer1->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_ALL ); + wxFlexGridSizer* fgSizer1; + fgSizer1 = new wxFlexGridSizer( 0, 2, 0, 0 ); + fgSizer1->AddGrowableCol( 1 ); + fgSizer1->SetFlexibleDirection( wxBOTH ); + fgSizer1->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_ALL ); - fgSizer1->Add( 0, 0, 1, wxEXPAND, 5 ); + fgSizer1->Add( 0, 0, 1, wxEXPAND, 5 ); - m_staticTextFinalPath = new wxStaticText( m_panelMainFolder, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); - m_staticTextFinalPath->Wrap( -1 ); - fgSizer1->Add( m_staticTextFinalPath, 0, wxALIGN_CENTER_VERTICAL|wxALL, 2 ); + m_staticTextFinalPath = new wxStaticText( m_panelMainFolder, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextFinalPath->Wrap( -1 ); + fgSizer1->Add( m_staticTextFinalPath, 0, wxALIGN_CENTER_VERTICAL|wxALL, 2 ); - wxBoxSizer* bSizer20; - bSizer20 = new wxBoxSizer( wxHORIZONTAL ); + wxBoxSizer* bSizer20; + bSizer20 = new wxBoxSizer( wxHORIZONTAL ); - m_bpButtonAddFolder = new wxBitmapButton( m_panelMainFolder, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); - m_bpButtonAddFolder->SetToolTip( _("Add folder") ); + m_bpButtonAddFolder = new wxBitmapButton( m_panelMainFolder, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1,-1 ), wxBU_AUTODRAW|0 ); + m_bpButtonAddFolder->SetToolTip( _("Add folder") ); - bSizer20->Add( m_bpButtonAddFolder, 0, wxEXPAND, 5 ); + bSizer20->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") ); + m_bpButtonRemoveTopFolder = new wxBitmapButton( m_panelMainFolder, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1,-1 ), wxBU_AUTODRAW|0 ); + m_bpButtonRemoveTopFolder->SetToolTip( _("Remove folder") ); - bSizer20->Add( m_bpButtonRemoveTopFolder, 0, wxEXPAND, 5 ); + bSizer20->Add( m_bpButtonRemoveTopFolder, 0, wxEXPAND, 5 ); - fgSizer1->Add( bSizer20, 0, wxEXPAND, 5 ); + fgSizer1->Add( bSizer20, 0, wxEXPAND, 5 ); - wxBoxSizer* bSizer19; - bSizer19 = new wxBoxSizer( wxHORIZONTAL ); + wxBoxSizer* bSizer19; + bSizer19 = new wxBoxSizer( wxHORIZONTAL ); - m_txtCtrlDirectoryMain = new wxTextCtrl( m_panelMainFolder, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize( -1, -1 ), 0 ); - bSizer19->Add( m_txtCtrlDirectoryMain, 1, wxALIGN_CENTER_VERTICAL, 5 ); + m_txtCtrlDirectoryMain = new wxTextCtrl( m_panelMainFolder, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize( -1,-1 ), 0 ); + bSizer19->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") ); + m_buttonSelectFolderMain = new wxButton( m_panelMainFolder, wxID_ANY, _("Browse"), wxDefaultPosition, wxDefaultSize, 0 ); + m_buttonSelectFolderMain->SetToolTip( _("Select a folder") ); - bSizer19->Add( m_buttonSelectFolderMain, 0, wxEXPAND, 5 ); + bSizer19->Add( m_buttonSelectFolderMain, 0, wxEXPAND, 5 ); - fgSizer1->Add( bSizer19, 0, wxEXPAND, 5 ); + fgSizer1->Add( bSizer19, 0, wxEXPAND, 5 ); - m_panelMainFolder->SetSizer( fgSizer1 ); - m_panelMainFolder->Layout(); - fgSizer1->Fit( m_panelMainFolder ); - bSizer151->Add( m_panelMainFolder, 0, wxRIGHT|wxLEFT|wxEXPAND, 5 ); + m_panelMainFolder->SetSizer( fgSizer1 ); + m_panelMainFolder->Layout(); + fgSizer1->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( 10, 10 ); - m_scrolledWinFolders->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + m_scrolledWinFolders = new wxScrolledWindow( m_panelMain, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL|wxVSCROLL ); + m_scrolledWinFolders->SetScrollRate( 10, 10 ); + m_scrolledWinFolders->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); - bSizerFolders = new wxBoxSizer( wxVERTICAL ); + 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 ); + 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, 5 ); + bSizer1->Add( bSizer151, 1, wxALL|wxEXPAND, 5 ); - m_staticline212 = new wxStaticLine( m_panelMain, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); - bSizer1->Add( m_staticline212, 0, wxEXPAND, 5 ); + m_staticline212 = new wxStaticLine( m_panelMain, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer1->Add( m_staticline212, 0, wxEXPAND, 5 ); - wxBoxSizer* bSizer14; - bSizer14 = new wxBoxSizer( wxHORIZONTAL ); + wxBoxSizer* bSizer14; + bSizer14 = new wxBoxSizer( wxHORIZONTAL ); - 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_staticText8 = new wxStaticText( m_panelMain, wxID_ANY, _("Minimum 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") ); + 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 ); + bSizer14->Add( m_spinCtrlDelay, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL|wxALL, 5 ); - bSizer1->Add( bSizer14, 0, wxALL|wxEXPAND, 5 ); + bSizer1->Add( bSizer14, 0, wxALL|wxEXPAND, 5 ); - m_staticline211 = new wxStaticLine( m_panelMain, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); - bSizer1->Add( m_staticline211, 0, wxEXPAND, 5 ); + 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* bSizer141; + bSizer141 = new wxBoxSizer( wxVERTICAL ); - wxBoxSizer* bSizer13; - bSizer13 = new wxBoxSizer( wxHORIZONTAL ); + 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 ); + m_bitmapConsole = new wxStaticBitmap( m_panelMain, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer13->Add( m_bitmapConsole, 0, wxTOP|wxBOTTOM|wxLEFT|wxALIGN_CENTER_VERTICAL, 5 ); - m_staticText6 = new wxStaticText( m_panelMain, wxID_ANY, _("Command line:"), wxDefaultPosition, wxDefaultSize, 0 ); - m_staticText6->Wrap( -1 ); - bSizer13->Add( m_staticText6, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + m_staticText6 = new wxStaticText( m_panelMain, wxID_ANY, _("Command line:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText6->Wrap( -1 ); + bSizer13->Add( m_staticText6, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); - bSizer141->Add( bSizer13, 0, 0, 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)") ); + 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 ); + bSizer141->Add( m_textCtrlCommand, 0, wxEXPAND|wxBOTTOM|wxRIGHT|wxLEFT, 5 ); - bSizer1->Add( bSizer141, 0, wxALL|wxEXPAND, 5 ); + bSizer1->Add( bSizer141, 0, wxALL|wxEXPAND, 5 ); - m_panelMain->SetSizer( bSizer1 ); - m_panelMain->Layout(); - bSizer1->Fit( m_panelMain ); - bSizerMain->Add( m_panelMain, 1, wxEXPAND, 5 ); + m_panelMain->SetSizer( bSizer1 ); + m_panelMain->Layout(); + bSizer1->Fit( m_panelMain ); + bSizerMain->Add( m_panelMain, 1, wxEXPAND, 5 ); - m_staticline5 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); - bSizerMain->Add( m_staticline5, 0, wxEXPAND, 5 ); + 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 = 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_BOLD, false, wxEmptyString ) ); + m_buttonStart->SetDefault(); + m_buttonStart->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); - bSizerMain->Add( m_buttonStart, 0, wxALIGN_CENTER_HORIZONTAL|wxALL, 5 ); + bSizerMain->Add( m_buttonStart, 0, wxALIGN_CENTER_HORIZONTAL|wxALL, 5 ); - this->SetSizer( bSizerMain ); - this->Layout(); - bSizerMain->Fit( this ); + this->SetSizer( bSizerMain ); + this->Layout(); + bSizerMain->Fit( this ); - this->Centre( wxBOTH ); + this->Centre( wxBOTH ); - // 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_hyperlink243->Connect( wxEVT_COMMAND_HYPERLINK, wxHyperlinkEventHandler( MainDlgGenerated::OnHelpRealTimeSync ), NULL, this ); - 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 ); + // 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_hyperlink243->Connect( wxEVT_COMMAND_HYPERLINK, wxHyperlinkEventHandler( MainDlgGenerated::OnHelpRealTimeSync ), NULL, this ); + 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() @@ -312,28 +312,28 @@ 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 ) ); + this->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); - wxBoxSizer* bSizer114; - bSizer114 = new wxBoxSizer( wxHORIZONTAL ); + 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") ); + 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 ); + 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_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") ); + m_buttonSelectFolder = new wxButton( this, wxID_ANY, _("Browse"), wxDefaultPosition, wxDefaultSize, 0 ); + m_buttonSelectFolder->SetToolTip( _("Select a folder") ); - bSizer114->Add( m_buttonSelectFolder, 0, wxEXPAND, 5 ); + bSizer114->Add( m_buttonSelectFolder, 0, wxEXPAND, 5 ); - this->SetSizer( bSizer114 ); - this->Layout(); - bSizer114->Fit( this ); + 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 index 5ecd49d3..bc8568e4 100644 --- a/FreeFileSync/Source/RealTimeSync/gui_generated.h +++ b/FreeFileSync/Source/RealTimeSync/gui_generated.h @@ -10,7 +10,7 @@ #include #include #include -namespace zen { class BitmapTextButton; } +namespace zen{ class BitmapTextButton; } #include #include @@ -43,69 +43,69 @@ namespace zen { class BitmapTextButton; } /////////////////////////////////////////////////////////////////////////////// class MainDlgGenerated : public wxFrame { -private: - -protected: - wxMenuBar* m_menubar1; - wxMenu* m_menuFile; - wxMenuItem* m_menuItemQuit; - wxMenu* m_menuHelp; - wxMenuItem* m_menuItemAbout; - wxBoxSizer* bSizerMain; - wxStaticText* m_staticText9; - wxFlexGridSizer* ffgSizer111; - wxStaticText* m_staticText16; - wxStaticText* m_staticText3; - wxStaticText* m_staticText17; - wxStaticText* m_staticText4; - wxStaticText* m_staticText18; - wxStaticText* m_staticText5; - wxStaticText* m_staticText811; - wxStaticText* m_staticText10; - wxStaticBitmap* m_bitmapBatch; - wxStaticText* m_staticText11; - wxHyperlinkCtrl* m_hyperlink243; - wxStaticLine* m_staticline2; - wxPanel* m_panelMain; - wxStaticBitmap* m_bitmapFolders; - wxStaticText* m_staticText7; - wxPanel* m_panelMainFolder; - wxStaticText* m_staticTextFinalPath; - wxBitmapButton* m_bpButtonAddFolder; - wxBitmapButton* m_bpButtonRemoveTopFolder; - wxTextCtrl* m_txtCtrlDirectoryMain; - wxButton* m_buttonSelectFolderMain; - wxScrolledWindow* m_scrolledWinFolders; - wxBoxSizer* bSizerFolders; - wxStaticLine* m_staticline212; - wxStaticText* m_staticText8; - wxSpinCtrl* m_spinCtrlDelay; - wxStaticLine* m_staticline211; - wxStaticBitmap* m_bitmapConsole; - wxStaticText* m_staticText6; - wxTextCtrl* m_textCtrlCommand; - wxStaticLine* m_staticline5; - zen::BitmapTextButton* m_buttonStart; - - // Virtual event handlers, overide 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 OnHelpRealTimeSync( wxHyperlinkEvent& 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(); + private: + + protected: + wxMenuBar* m_menubar1; + wxMenu* m_menuFile; + wxMenuItem* m_menuItemQuit; + wxMenu* m_menuHelp; + wxMenuItem* m_menuItemAbout; + wxBoxSizer* bSizerMain; + wxStaticText* m_staticText9; + wxFlexGridSizer* ffgSizer111; + wxStaticText* m_staticText16; + wxStaticText* m_staticText3; + wxStaticText* m_staticText17; + wxStaticText* m_staticText4; + wxStaticText* m_staticText18; + wxStaticText* m_staticText5; + wxStaticText* m_staticText811; + wxStaticText* m_staticText10; + wxStaticBitmap* m_bitmapBatch; + wxStaticText* m_staticText11; + wxHyperlinkCtrl* m_hyperlink243; + wxStaticLine* m_staticline2; + wxPanel* m_panelMain; + wxStaticBitmap* m_bitmapFolders; + wxStaticText* m_staticText7; + wxPanel* m_panelMainFolder; + wxStaticText* m_staticTextFinalPath; + wxBitmapButton* m_bpButtonAddFolder; + wxBitmapButton* m_bpButtonRemoveTopFolder; + wxTextCtrl* m_txtCtrlDirectoryMain; + wxButton* m_buttonSelectFolderMain; + wxScrolledWindow* m_scrolledWinFolders; + wxBoxSizer* bSizerFolders; + wxStaticLine* m_staticline212; + wxStaticText* m_staticText8; + wxSpinCtrl* m_spinCtrlDelay; + wxStaticLine* m_staticline211; + wxStaticBitmap* m_bitmapConsole; + wxStaticText* m_staticText6; + wxTextCtrl* m_textCtrlCommand; + wxStaticLine* m_staticline5; + zen::BitmapTextButton* m_buttonStart; + + // Virtual event handlers, overide 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 OnHelpRealTimeSync( wxHyperlinkEvent& 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(); }; @@ -114,17 +114,17 @@ public: /////////////////////////////////////////////////////////////////////////////// class FolderGenerated : public wxPanel { -private: + private: -protected: - wxButton* m_buttonSelectFolder; + protected: + wxButton* m_buttonSelectFolder; -public: - wxBitmapButton* m_bpButtonRemoveFolder; - wxTextCtrl* m_txtCtrlDirectory; + 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(); + 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/tray_menu.cpp b/FreeFileSync/Source/RealTimeSync/tray_menu.cpp index 2a960e7e..be28e452 100644 --- a/FreeFileSync/Source/RealTimeSync/tray_menu.cpp +++ b/FreeFileSync/Source/RealTimeSync/tray_menu.cpp @@ -193,7 +193,7 @@ private: const wxString jobName_; //RTS job name, may be empty - const wxBitmap trayBmp_ = getResourceImage("RTS_tray_24x24"); //use a 24x24 bitmap for perfect fit + const wxBitmap trayBmp_ = getResourceImage("RTS_tray_24"); //use a 24x24 bitmap for perfect fit }; diff --git a/FreeFileSync/Source/afs/abstract.cpp b/FreeFileSync/Source/afs/abstract.cpp index d6570812..f78433d8 100644 --- a/FreeFileSync/Source/afs/abstract.cpp +++ b/FreeFileSync/Source/afs/abstract.cpp @@ -50,8 +50,8 @@ int AFS::compareDevice(const AbstractFileSystem& lhs, const AbstractFileSystem& int AFS::comparePath(const AbstractPath& lhs, const AbstractPath& rhs) { - const int rv = compareDevice(lhs.afsDevice.ref(), rhs.afsDevice.ref()); - if (rv != 0) + if (const int rv = compareDevice(lhs.afsDevice.ref(), rhs.afsDevice.ref()); + rv != 0) return rv; return compareString(lhs.afsPath.value, rhs.afsPath.value); @@ -113,7 +113,7 @@ void AFS::traverseFolderFlat(const AfsPath& afsPath, //throw FileError //target existing: undefined behavior! (fail/overwrite/auto-rename) -AFS::FileCopyResult AFS::copyFileAsStream(const AfsPath& afsPathSource, const StreamAttributes& attrSource, //throw FileError, ErrorFileLocked, X +AFS::FileCopyResult AFS::copyFileAsStream(const AfsPath& afsSource, const StreamAttributes& attrSource, //throw FileError, ErrorFileLocked, X const AbstractPath& apTarget, const IOCallback& notifyUnbufferedIO /*throw X*/) const { int64_t totalUnbufferedIO = 0; @@ -125,7 +125,7 @@ AFS::FileCopyResult AFS::copyFileAsStream(const AfsPath& afsPathSource, const St auto notifyUnbufferedWrite = [&](int64_t bytesDelta) { totalBytesWritten += bytesDelta; cbd(bytesDelta); }; //-------------------------------------------------------------------------------------------------------- - auto streamIn = getInputStream(afsPathSource, notifyUnbufferedRead); //throw FileError, ErrorFileLocked + auto streamIn = getInputStream(afsSource, notifyUnbufferedRead); //throw FileError, ErrorFileLocked StreamAttributes attrSourceNew = {}; //try to get the most current attributes if possible (input file might have changed after comparison!) @@ -142,7 +142,7 @@ AFS::FileCopyResult AFS::copyFileAsStream(const AfsPath& afsPathSource, const St //check incomplete input *before* failing with (slightly) misleading error message in OutputStream::finalize() if (totalBytesRead != makeSigned(attrSourceNew.fileSize)) - throw FileError(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(getDisplayPath(afsPathSource))), + throw FileError(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(getDisplayPath(afsSource))), replaceCpy(replaceCpy(_("Unexpected size of data stream.\nExpected: %x bytes\nActual: %y bytes"), L"%x", numberTo(attrSourceNew.fileSize)), L"%y", numberTo(totalBytesRead)) + L" [notifyUnbufferedRead]"); @@ -264,7 +264,7 @@ void AFS::createFolderIfMissingRecursion(const AbstractPath& ap) //throw FileErr try //generally we expect that path already exists (see: versioning, base folder, log file path) => check first { - if (getItemType(ap) != ItemType::FILE) //throw FileError + if (getItemType(ap) != ItemType::file) //throw FileError return; } catch (FileError&) {} //not yet existing or access error? let's find out... @@ -280,7 +280,7 @@ void AFS::createFolderIfMissingRecursion(const AbstractPath& ap) //throw FileErr { try { - if (getItemType(ap) != ItemType::FILE) //throw FileError + if (getItemType(ap) != ItemType::file) //throw FileError return; //already existing => possible, if createFolderIfMissingRecursion() is run in parallel } catch (FileError&) {} //not yet existing or access error @@ -312,13 +312,13 @@ std::optional AFS::itemStillExists(const AfsPath& afsPath) const const std::optional parentType = AFS::itemStillExists(*parentAfsPath); //throw FileError - if (parentType && *parentType != ItemType::FILE /*obscure, but possible (and not an error)*/) + if (parentType && *parentType != ItemType::file /*obscure, but possible (and not an error)*/) try { traverseFolderFlat(*parentAfsPath, //throw FileError - [&](const FileInfo& fi) { if (fi.itemName == itemName) throw ItemType::FILE; }, - [&](const FolderInfo& fi) { if (fi.itemName == itemName) throw ItemType::FOLDER; }, - [&](const SymlinkInfo& si) { if (si.itemName == itemName) throw ItemType::SYMLINK; }); + [&](const FileInfo& fi) { if (fi.itemName == itemName) throw ItemType::file; }, + [&](const FolderInfo& fi) { if (fi.itemName == itemName) throw ItemType::folder; }, + [&](const SymlinkInfo& si) { if (si.itemName == itemName) throw ItemType::symlink; }); } catch (const ItemType&) //finding the item after getItemType() previously failed is exceptional { @@ -378,7 +378,7 @@ void AFS::removeFolderIfExistsRecursion(const AfsPath& afsPath, //throw FileErro //no error situation if directory is not existing! manual deletion relies on it! if (std::optional type = itemStillExists(afsPath)) //throw FileError { - if (*type == AFS::ItemType::SYMLINK) + if (*type == AFS::ItemType::symlink) { if (onBeforeFileDeletion) onBeforeFileDeletion(getDisplayPath(afsPath)); //throw X diff --git a/FreeFileSync/Source/afs/abstract.h b/FreeFileSync/Source/afs/abstract.h index f4f58310..de7a5993 100644 --- a/FreeFileSync/Source/afs/abstract.h +++ b/FreeFileSync/Source/afs/abstract.h @@ -85,9 +85,9 @@ struct AbstractFileSystem //THREAD-SAFETY: "const" member functions must model t enum class ItemType : unsigned char { - FILE, - FOLDER, - SYMLINK, + file, + folder, + symlink, }; //(hopefully) fast: does not distinguish between error/not existing //root path? => do access test @@ -124,7 +124,7 @@ struct AbstractFileSystem //THREAD-SAFETY: "const" member functions must model t //static void setModTime(const AbstractPath& ap, time_t modTime) { ap.afsDevice.ref().setModTime(ap.afsPath, modTime); } //throw FileError, follows symlinks static AbstractPath getSymlinkResolvedPath(const AbstractPath& ap) { return ap.afsDevice.ref().getSymlinkResolvedPath (ap.afsPath); } //throw FileError - static std::string getSymlinkBinaryContent(const AbstractPath& ap) { return ap.afsDevice.ref().getSymlinkBinaryContent(ap.afsPath); } //throw FileError + static bool equalSymlinkContent(const AbstractPath& apLhs, const AbstractPath& apRhs); //throw FileError //---------------------------------------------------------------------------------------------------------------- //noexcept; optional return value: static zen::ImageHolder getFileIcon (const AbstractPath& ap, int pixelSize) { return ap.afsDevice.ref().getFileIcon (ap.afsPath, pixelSize); } @@ -148,6 +148,9 @@ struct AbstractFileSystem //THREAD-SAFETY: "const" member functions must model t //only returns attributes if they are already buffered within stream handle and determination would be otherwise expensive (e.g. FTP/SFTP): virtual std::optional getAttributesBuffered() = 0; //throw FileError }; + //return value always bound: + static std::unique_ptr getInputStream(const AbstractPath& ap, const zen::IOCallback& notifyUnbufferedIO /*throw X*/) //throw FileError, ErrorFileLocked + { return ap.afsDevice.ref().getInputStream(ap.afsPath, notifyUnbufferedIO); } struct FinalizeResult @@ -178,11 +181,6 @@ struct AbstractFileSystem //THREAD-SAFETY: "const" member functions must model t const std::optional bytesExpected_; uint64_t bytesWrittenTotal_ = 0; }; - - //return value always bound: - static std::unique_ptr getInputStream(const AbstractPath& ap, const zen::IOCallback& notifyUnbufferedIO /*throw X*/) //throw FileError, ErrorFileLocked - { return ap.afsDevice.ref().getInputStream(ap.afsPath, notifyUnbufferedIO); } - //target existing: undefined behavior! (fail/overwrite/auto-rename) static std::unique_ptr getOutputStream(const AbstractPath& ap, //throw FileError std::optional streamSize, @@ -203,13 +201,13 @@ struct AbstractFileSystem //THREAD-SAFETY: "const" member functions must model t uint64_t fileSize; //unit: bytes! time_t modTime; //number of seconds since Jan. 1st 1970 UTC FileId fileId; //optional: empty if not supported! - const SymlinkInfo* symlinkInfo; //only filled if file is a followed symlink + bool isFollowedSymlink; }; struct FolderInfo { Zstring itemName; - const SymlinkInfo* symlinkInfo; //only filled if folder is a followed symlink + bool isFollowedSymlink; }; struct TraverserCallback @@ -286,7 +284,7 @@ struct AbstractFileSystem //THREAD-SAFETY: "const" member functions must model t //---------------------------------------------------------------------------------------------------------------- - static uint64_t getFreeDiskSpace(const AbstractPath& ap) { return ap.afsDevice.ref().getFreeDiskSpace(ap.afsPath); } //throw FileError, returns 0 if not available + static int64_t getFreeDiskSpace(const AbstractPath& ap) { return ap.afsDevice.ref().getFreeDiskSpace(ap.afsPath); } //throw FileError, returns < 0 if not available static bool supportsRecycleBin(const AbstractPath& ap) { return ap.afsDevice.ref().supportsRecycleBin(ap.afsPath); } //throw FileError @@ -325,7 +323,7 @@ protected: const std::function& onSymlink) const; // //target existing: undefined behavior! (fail/overwrite/auto-rename) - FileCopyResult copyFileAsStream(const AfsPath& afsPathSource, const StreamAttributes& attrSource, //throw FileError, ErrorFileLocked, X + FileCopyResult copyFileAsStream(const AfsPath& afsSource, const StreamAttributes& attrSource, //throw FileError, ErrorFileLocked, X const AbstractPath& apTarget, const zen::IOCallback& notifyUnbufferedIO /*throw X*/) const; private: @@ -355,7 +353,8 @@ private: //virtual void setModTime(const AfsPath& afsPath, time_t modTime) const = 0; //throw FileError, follows symlinks virtual AbstractPath getSymlinkResolvedPath(const AfsPath& afsPath) const = 0; //throw FileError - virtual std::string getSymlinkBinaryContent(const AfsPath& afsPath) const = 0; //throw FileError + virtual bool equalSymlinkContentForSameAfsType(const AfsPath& afsLhs, const AbstractPath& apRhs) const = 0; //throw FileError + //---------------------------------------------------------------------------------------------------------------- virtual std::unique_ptr getInputStream(const AfsPath& afsPath, const zen::IOCallback& notifyUnbufferedIO /*throw X*/) const = 0; //throw FileError, ErrorFileLocked @@ -374,7 +373,7 @@ private: //symlink handling: follow link! //target existing: undefined behavior! (fail/overwrite/auto-rename) - virtual FileCopyResult copyFileForSameAfsType(const AfsPath& afsPathSource, const StreamAttributes& attrSource, //throw FileError, ErrorFileLocked, X + virtual FileCopyResult copyFileForSameAfsType(const AfsPath& afsSource, const StreamAttributes& attrSource, //throw FileError, ErrorFileLocked, X const AbstractPath& apTarget, bool copyFilePermissions, //accummulated delta != file size! consider ADS, sparse, compressed files const zen::IOCallback& notifyUnbufferedIO /*throw X*/) const = 0; @@ -382,9 +381,9 @@ private: //target existing: fail/ignore //symlink handling: follow link! - virtual void copyNewFolderForSameAfsType(const AfsPath& afsPathSource, const AbstractPath& apTarget, bool copyFilePermissions) const = 0; //throw FileError + virtual void copyNewFolderForSameAfsType(const AfsPath& afsSource, const AbstractPath& apTarget, bool copyFilePermissions) const = 0; //throw FileError - virtual void copySymlinkForSameAfsType(const AfsPath& afsPathSource, const AbstractPath& apTarget, bool copyFilePermissions) const = 0; //throw FileError + virtual void copySymlinkForSameAfsType(const AfsPath& afsSource, const AbstractPath& apTarget, bool copyFilePermissions) const = 0; //throw FileError //---------------------------------------------------------------------------------------------------------------- virtual zen::ImageHolder getFileIcon (const AfsPath& afsPath, int pixelSize) const = 0; //noexcept; optional return value @@ -397,7 +396,7 @@ private: virtual bool hasNativeTransactionalCopy() const = 0; //---------------------------------------------------------------------------------------------------------------- - virtual uint64_t getFreeDiskSpace(const AfsPath& afsPath) const = 0; //throw FileError, returns 0 if not available + virtual int64_t getFreeDiskSpace(const AfsPath& afsPath) const = 0; //throw FileError, returns < 0 if not available virtual bool supportsRecycleBin(const AfsPath& afsPath) const = 0; //throw FileError virtual std::unique_ptr createRecyclerSession(const AfsPath& afsPath) const = 0; //throw FileError, return value must be bound! virtual void recycleItemIfExists(const AfsPath& afsPath) const = 0; //throw FileError @@ -490,17 +489,27 @@ bool AbstractFileSystem::supportPermissionCopy(const AbstractPath& apSource, con } +inline +bool AbstractFileSystem::equalSymlinkContent(const AbstractPath& apLhs, const AbstractPath& apRhs) //throw FileError +{ + if (typeid(apLhs.afsDevice.ref()) != typeid(apRhs.afsDevice.ref())) + return false; + + return apLhs.afsDevice.ref().equalSymlinkContentForSameAfsType(apLhs.afsPath, apRhs); //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())) - return pathFrom.afsDevice.ref().moveAndRenameItemForSameAfsType(pathFrom.afsPath, pathTo); //throw FileError, ErrorMoveUnsupported + if (typeid(pathFrom.afsDevice.ref()) != typeid(pathTo.afsDevice.ref())) + throw ErrorMoveUnsupported(replaceCpy(replaceCpy(_("Cannot move file %x to %y."), + L"%x", L'\n' + fmtPath(getDisplayPath(pathFrom))), + L"%y", L'\n' + fmtPath(getDisplayPath(pathTo))), _("Operation not supported between different devices.")); - throw ErrorMoveUnsupported(replaceCpy(replaceCpy(_("Cannot move file %x to %y."), - L"%x", L'\n' + fmtPath(getDisplayPath(pathFrom))), - L"%y", L'\n' + fmtPath(getDisplayPath(pathTo))), _("Operation not supported between different devices.")); + pathFrom.afsDevice.ref().moveAndRenameItemForSameAfsType(pathFrom.afsPath, pathTo); //throw FileError, ErrorMoveUnsupported } @@ -510,16 +519,18 @@ void AbstractFileSystem::copyNewFolder(const AbstractPath& apSource, const Abstr { using namespace zen; - if (typeid(apSource.afsDevice.ref()) == typeid(apTarget.afsDevice.ref())) - return apSource.afsDevice.ref().copyNewFolderForSameAfsType(apSource.afsPath, apTarget, copyFilePermissions); //throw FileError - - //fall back: - if (copyFilePermissions) - throw FileError(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(getDisplayPath(apTarget))), - _("Operation not supported between different devices.")); - - //already existing: fail/ignore - createFolderPlain(apTarget); //throw FileError + if (typeid(apSource.afsDevice.ref()) != typeid(apTarget.afsDevice.ref())) + { + //fall back: + if (copyFilePermissions) + throw FileError(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(getDisplayPath(apTarget))), + _("Operation not supported between different devices.")); + + //already existing: fail/ignore + createFolderPlain(apTarget); //throw FileError + } + else + apSource.afsDevice.ref().copyNewFolderForSameAfsType(apSource.afsPath, apTarget, copyFilePermissions); //throw FileError } @@ -528,12 +539,12 @@ void AbstractFileSystem::copySymlink(const AbstractPath& apSource, const Abstrac { using namespace zen; - if (typeid(apSource.afsDevice.ref()) == typeid(apTarget.afsDevice.ref())) - return apSource.afsDevice.ref().copySymlinkForSameAfsType(apSource.afsPath, apTarget, copyFilePermissions); //throw FileError + if (typeid(apSource.afsDevice.ref()) != typeid(apTarget.afsDevice.ref())) + throw FileError(replaceCpy(replaceCpy(_("Cannot copy symbolic link %x to %y."), + L"%x", L'\n' + fmtPath(getDisplayPath(apSource))), + L"%y", L'\n' + fmtPath(getDisplayPath(apTarget))), _("Operation not supported between different devices.")); - throw FileError(replaceCpy(replaceCpy(_("Cannot copy symbolic link %x to %y."), - L"%x", L'\n' + fmtPath(getDisplayPath(apSource))), - L"%y", L'\n' + fmtPath(getDisplayPath(apTarget))), _("Operation not supported between different devices.")); + apSource.afsDevice.ref().copySymlinkForSameAfsType(apSource.afsPath, apTarget, copyFilePermissions); //throw FileError } } diff --git a/FreeFileSync/Source/afs/abstract_impl.h b/FreeFileSync/Source/afs/abstract_impl.h index d6bec8d2..34ec4bdc 100644 --- a/FreeFileSync/Source/afs/abstract_impl.h +++ b/FreeFileSync/Source/afs/abstract_impl.h @@ -204,7 +204,7 @@ private: //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 -//=> serialize access (at path level) so that GoogleFileState access and file/folder creation act as a single operation +//=> serialize access (at path level) so that GdriveFileState access and file/folder creation act as a single operation template class PathAccessLocker { diff --git a/FreeFileSync/Source/afs/concrete.cpp b/FreeFileSync/Source/afs/concrete.cpp index 700351dd..1495a872 100644 --- a/FreeFileSync/Source/afs/concrete.cpp +++ b/FreeFileSync/Source/afs/concrete.cpp @@ -17,14 +17,14 @@ void fff::initAfs(const AfsConfig& cfg) { ftpInit(); sftpInit(); - googleDriveInit(appendSeparator(cfg.configDirPathPf) + Zstr("GoogleDrive"), + gdriveInit(appendSeparator(cfg.configDirPathPf) + Zstr("GoogleDrive"), appendSeparator(cfg.resourceDirPathPf) + Zstr("cacert.pem")); } void fff::teardownAfs() { - googleDriveTeardown(); + gdriveTeardown(); sftpTeardown(); ftpTeardown(); } diff --git a/FreeFileSync/Source/afs/ftp.cpp b/FreeFileSync/Source/afs/ftp.cpp index b8dd619b..d4a37332 100644 --- a/FreeFileSync/Source/afs/ftp.cpp +++ b/FreeFileSync/Source/afs/ftp.cpp @@ -23,7 +23,7 @@ using AFS = AbstractFileSystem; namespace { -Zstring concatenateFtpFolderPathPhrase(const FtpLoginInfo& login, const AfsPath& afsPath); //noexcept +Zstring concatenateFtpFolderPathPhrase(const FtpLogin& login, const AfsPath& afsPath); //noexcept /* Extensions to FTP: https://tools.ietf.org/html/rfc3659 @@ -46,7 +46,7 @@ enum class ServerEncoding //use all configuration data that *defines* an SFTP session as key when buffering sessions! This is what user expects, e.g. when changing settings in FTP login dialog struct FtpSessionId { - /*explicit*/ FtpSessionId(const FtpLoginInfo& login) : + /*explicit*/ FtpSessionId(const FtpLogin& login) : server(login.server), port(login.port), username(login.username), @@ -65,19 +65,19 @@ struct FtpSessionId bool operator<(const FtpSessionId& lhs, const FtpSessionId& rhs) { //exactly the type of case insensitive comparison we need for server names! - int rv = compareAsciiNoCase(lhs.server, rhs.server); //https://docs.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfow#IDNs - if (rv != 0) + if (const int rv = compareAsciiNoCase(lhs.server, rhs.server); //https://docs.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfow#IDNs + rv != 0) return rv < 0; if (lhs.port != rhs.port) return lhs.port < rhs.port; - rv = compareString(lhs.username, rhs.username); //case sensitive! - if (rv != 0) + if (const int rv = compareString(lhs.username, rhs.username); //case sensitive! + rv != 0) return rv < 0; - rv = compareString(lhs.password, rhs.password); //case sensitive! - if (rv != 0) + if (const int rv = compareString(lhs.password, rhs.password); //case sensitive! + rv != 0) return rv < 0; return lhs.useTls < rhs.useTls; @@ -756,7 +756,8 @@ private: else if (equalAsciiNoCase(line, " MFMT")) //SP "MFMT" CRLF output.mfmt = true; - else if (equalAsciiNoCase(line, " UTF8")) + else if (equalAsciiNoCase(line, " UTF8") || + equalAsciiNoCase(line, " UTF8 ON")) //support non-compliant servers: https://freefilesync.org/forum/viewtopic.php?t=7355#p24694 output.utf8 = true; else if (equalAsciiNoCase(line, " CLNT")) @@ -797,7 +798,7 @@ public: sessionCleaner_.join(); } - void access(const FtpLoginInfo& login, const std::function& useFtpSession /*throw X*/) //throw SysError, X + void access(const FtpLogin& login, const std::function& useFtpSession /*throw X*/) //throw SysError, X { Protected& sessionStore = getSessionStore(login); @@ -893,7 +894,7 @@ UniInitializer globalStartupInitFtp(*globalFtpSessionCount.get()); Global globalFtpSessionManager; //caveat: life time must be subset of static UniInitializer! //-------------------------------------------------------------------------------------- -void accessFtpSession(const FtpLoginInfo& login, const std::function& useFtpSession /*throw X*/) //throw SysError, X +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 @@ -905,7 +906,7 @@ void accessFtpSession(const FtpLoginInfo& login, const std::function execute(const FtpLoginInfo& login, const AfsPath& afsDirPath) //throw FileError + static std::vector execute(const FtpLogin& login, const AfsPath& afsDirPath) //throw FileError { std::string rawListing; //get raw FTP directory listing @@ -1007,76 +1008,82 @@ private: 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 - */ - FtpItem item; + 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. (" + utfTo(rawLine) + L')'); + 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 facts(itBegin, itBlank); - item.itemName = serverToUtfEncoding(std::string(itBlank + 1, rawLine.end()), enc); //throw SysError + const std::string facts(itBegin, itBlank); + item.itemName = serverToUtfEncoding(std::string(itBlank + 1, rawLine.end()), enc); //throw SysError - std::string typeFact; - std::optional fileSize; + std::string typeFact; + std::optional fileSize; - for (const std::string& fact : split(facts, ';', SplitType::SKIP_EMPTY)) - if (startsWithAsciiNoCase(fact, "type=")) //must be case-insensitive!!! - { - const std::string tmp = afterFirst(fact, '=', IF_MISSING_RETURN_NONE); - typeFact = beforeFirst(tmp, ':', IF_MISSING_RETURN_ALL); - } - else if (startsWithAsciiNoCase(fact, "size=")) - fileSize = stringTo(afterFirst(fact, '=', IF_MISSING_RETURN_NONE)); - else if (startsWithAsciiNoCase(fact, "modify=")) - { - std::string modifyFact = afterFirst(fact, '=', IF_MISSING_RETURN_NONE); - modifyFact = beforeLast(modifyFact, '.', IF_MISSING_RETURN_ALL); //truncate millisecond precision if available + for (const std::string& fact : split(facts, ';', SplitType::SKIP_EMPTY)) + if (startsWithAsciiNoCase(fact, "type=")) //must be case-insensitive!!! + { + const std::string tmp = afterFirst(fact, '=', IF_MISSING_RETURN_NONE); + typeFact = beforeFirst(tmp, ':', IF_MISSING_RETURN_ALL); + } + else if (startsWithAsciiNoCase(fact, "size=")) + fileSize = stringTo(afterFirst(fact, '=', IF_MISSING_RETURN_NONE)); + else if (startsWithAsciiNoCase(fact, "modify=")) + { + std::string modifyFact = afterFirst(fact, '=', IF_MISSING_RETURN_NONE); + modifyFact = beforeLast(modifyFact, '.', IF_MISSING_RETURN_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 could not be parsed. (" + utfTo(modifyFact) + L')'); + const TimeComp tc = parseTime("%Y%m%d%H%M%S", modifyFact); + if (tc == TimeComp()) + throw SysError(L"Modification time could not be parsed."); - item.modTime = utcToTimeT(tc); //returns -1 on error - if (item.modTime == -1) - { - if (tc.year == 1600 || //FTP on Windows phone: zero-initialized FILETIME equals "December 31, 1600" or "January 1, 1601" - tc.year == 1601) // => is this also relevant in this context of MLST UTC time?? - item.modTime = 0; - else - throw SysError(L"Modification time could not be parsed. (" + utfTo(modifyFact) + L')'); + item.modTime = utcToTimeT(tc); //returns -1 on error + if (item.modTime == -1) + { + if (tc.year == 1600 || //FTP on Windows phone: zero-initialized FILETIME equals "December 31, 1600" or "January 1, 1601" + tc.year == 1601) // => is this also relevant in this context of MLST UTC time?? + item.modTime = 0; + else + throw SysError(L"Modification time could not be parsed."); + } } - } - 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, "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 + 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. (" + utfTo(rawLine) + L')'); + //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) + throw SysError(L"File size not available."); + item.fileSize = *fileSize; + } - if (item.type == AFS::ItemType::FILE) + //note: as far as the RFC goes, the "unique" fact is not required to act like a persistent file id! + return item; + } + catch (const SysError& e) { - if (!fileSize) - throw SysError(L"File size not available. (" + utfTo(rawLine) + L')'); - item.fileSize = *fileSize; + throw SysError(L"Failed to parse FTP response. (" + utfTo(rawLine) + L") " + e.toString()); } - - //note: as far as the RFC goes, the "unique" fact is not required to act like a persistent file id! - return item; } static std::vector parseUnknown(const std::string& buf, ServerEncoding enc) //throw SysError @@ -1267,13 +1274,13 @@ private: 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 }; + return { AFS::ItemType::folder, utfTo(itemName), 0, 0 }; //------------------------------------------------------------------------------------ FtpItem item; if (typeTag == "d") - item.type = AFS::ItemType::FOLDER; + item.type = AFS::ItemType::folder; else if (typeTag == "l") - item.type = AFS::ItemType::SYMLINK; + item.type = AFS::ItemType::symlink; else item.fileSize = fileSize; @@ -1410,7 +1417,7 @@ private: { FtpItem item; if (isDir) - item.type = AFS::ItemType::FOLDER; + item.type = AFS::ItemType::folder; item.itemName = serverToUtfEncoding(itemName, enc); //throw SysError item.fileSize = fileSize; item.modTime = utcTime; @@ -1432,7 +1439,7 @@ private: class SingleFolderTraverser { public: - SingleFolderTraverser(const FtpLoginInfo& login, const std::vector>>& workload /*throw X*/) + SingleFolderTraverser(const FtpLogin& login, const std::vector>>& workload /*throw X*/) : workload_(workload), login_(login) { while (!workload_.empty()) @@ -1460,47 +1467,44 @@ private: switch (item.type) { - case AFS::ItemType::FILE: - cb.onFile({ item.itemName, item.fileSize, item.modTime, AFS::FileId(), nullptr /*symlinkInfo*/ }); //throw X + case AFS::ItemType::file: + cb.onFile({ item.itemName, item.fileSize, item.modTime, AFS::FileId(), false /*isFollowedSymlink*/ }); //throw X break; - case AFS::ItemType::FOLDER: - if (std::shared_ptr cbSub = cb.onFolder({ item.itemName, nullptr /*symlinkInfo*/ })) //throw X + 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: - { - const AFS::SymlinkInfo linkInfo = { item.itemName, item.modTime }; - switch (cb.onSymlink(linkInfo)) //throw X + case AFS::ItemType::symlink: + switch (cb.onSymlink({ item.itemName, item.modTime })) //throw X { case AFS::TraverserCallback::LINK_FOLLOW: - if (std::shared_ptr cbSub = cb.onFolder({ item.itemName, &linkInfo })) //throw X + if (std::shared_ptr cbSub = cb.onFolder({ item.itemName, true /*isFollowedSymlink*/ })) //throw X workload_.push_back({ itemPath, std::move(cbSub) }); break; case AFS::TraverserCallback::LINK_SKIP: break; } - } - break; + break; } } } std::vector>> workload_; - const FtpLoginInfo login_; + const FtpLogin login_; }; -void traverseFolderRecursiveFTP(const FtpLoginInfo& login, const std::vector>>& workload /*throw X*/, size_t) //throw X +void traverseFolderRecursiveFTP(const FtpLogin& login, const std::vector>>& workload /*throw X*/, size_t) //throw X { SingleFolderTraverser dummy(login, workload); //throw X } //=========================================================================================================================== //=========================================================================================================================== -void ftpFileDownload(const FtpLoginInfo& login, const AfsPath& afsFilePath, //throw FileError, X +void ftpFileDownload(const FtpLogin& login, const AfsPath& afsFilePath, //throw FileError, X const std::function& writeBlock /*throw X*/) { std::exception_ptr exception; @@ -1554,7 +1558,7 @@ File already existing: FileZilla Server: overwrites Windows IIS: overwrites */ -void ftpFileUpload(const FtpLoginInfo& login, const AfsPath& afsFilePath, //throw FileError, X +void ftpFileUpload(const FtpLogin& login, const AfsPath& afsFilePath, //throw FileError, X const std::function& readBlock /*throw X*/) //returning 0 signals EOF: Posix read() semantics { std::exception_ptr exception; @@ -1622,7 +1626,7 @@ void ftpFileUpload(const FtpLoginInfo& login, const AfsPath& afsFilePath, //thro struct InputStreamFtp : public AbstractFileSystem::InputStream { - InputStreamFtp(const FtpLoginInfo& login, + InputStreamFtp(const FtpLogin& login, const AfsPath& afsPath, const IOCallback& notifyUnbufferedIO /*throw X*/) : notifyUnbufferedIO_(notifyUnbufferedIO) @@ -1686,7 +1690,7 @@ private: struct OutputStreamFtp : public AbstractFileSystem::OutputStreamImpl { - OutputStreamFtp(const FtpLoginInfo& login, + OutputStreamFtp(const FtpLogin& login, const AfsPath& afsPath, std::optional modTime, const IOCallback& notifyUnbufferedIO /*throw X*/) : @@ -1785,7 +1789,7 @@ private: } } - const FtpLoginInfo login_; + const FtpLogin login_; const AfsPath afsPath_; const std::optional modTime_; const IOCallback notifyUnbufferedIO_; //throw X @@ -1800,9 +1804,9 @@ private: class FtpFileSystem : public AbstractFileSystem { public: - FtpFileSystem(const FtpLoginInfo& login) : login_(login) {} + FtpFileSystem(const FtpLogin& login) : login_(login) {} - const FtpLoginInfo& getLogin() const { return login_; } + const FtpLogin& getLogin() const { return login_; } private: Zstring getInitPathPhrase(const AfsPath& afsPath) const override { return concatenateFtpFolderPathPhrase(login_, afsPath); } @@ -1813,12 +1817,12 @@ private: int compareDeviceSameAfsType(const AbstractFileSystem& afsRhs) const override { - const FtpLoginInfo& lhs = login_; - const FtpLoginInfo& rhs = static_cast(afsRhs).login_; + const FtpLogin& lhs = login_; + const FtpLogin& rhs = static_cast(afsRhs).login_; //exactly the type of case insensitive comparison we need for server names! - const int rv = compareAsciiNoCase(lhs.server, rhs.server); //https://docs.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfow#IDNs - if (rv != 0) + if (const int rv = compareAsciiNoCase(lhs.server, rhs.server); //https://docs.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfow#IDNs + rv != 0) return rv; //port does NOT create a *different* data source!!! -> same thing for password! @@ -1841,7 +1845,7 @@ private: { session.testConnection(login_.timeoutSec); //throw SysError }); - return ItemType::FOLDER; + return ItemType::folder; } catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read directory %x."), L"%x", fmtPath(getCurlDisplayPath(login_.server, afsPath))), e.toString()); } @@ -1852,9 +1856,9 @@ private: //is the underlying file system case-sensitive? we don't know => assume "case-sensitive" //=> all path parts (except the base folder part!) can be expected to have the right case anyway after traversal traverseFolderFlat(*parentAfsPath, //throw FileError - [&](const FileInfo& fi) { if (fi.itemName == itemName) throw ItemType::FILE; }, - [&](const FolderInfo& fi) { if (fi.itemName == itemName) throw ItemType::FOLDER; }, - [&](const SymlinkInfo& si) { if (si.itemName == itemName) throw ItemType::SYMLINK; }); + [&](const FileInfo& fi) { if (fi.itemName == itemName) throw ItemType::file; }, + [&](const FolderInfo& fi) { if (fi.itemName == itemName) throw ItemType::folder; }, + [&](const SymlinkInfo& si) { if (si.itemName == itemName) throw ItemType::symlink; }); } catch (const ItemType& type) { return type; } //yes, exceptions for control-flow are bad design... but, but... @@ -1872,15 +1876,15 @@ private: try { traverseFolderFlat(*parentAfsPath, //throw FileError - [&](const FileInfo& fi) { if (fi.itemName == itemName) throw ItemType::FILE; }, - [&](const FolderInfo& fi) { if (fi.itemName == itemName) throw ItemType::FOLDER; }, - [&](const SymlinkInfo& si) { if (si.itemName == itemName) throw ItemType::SYMLINK; }); + [&](const FileInfo& fi) { if (fi.itemName == itemName) throw ItemType::file; }, + [&](const FolderInfo& fi) { if (fi.itemName == itemName) throw ItemType::folder; }, + [&](const SymlinkInfo& si) { if (si.itemName == itemName) throw ItemType::symlink; }); } catch (const ItemType& type) { return type; } //yes, exceptions for control-flow are bad design... but, but... catch (FileError&) { const std::optional parentType = itemStillExists(*parentAfsPath); //throw FileError - if (parentType && *parentType != ItemType::FILE) //obscure, but possible (and not an error) + if (parentType && *parentType != ItemType::file) //obscure, but possible (and not an error) throw; //parent path existing, so traversal should not have failed! } return {}; @@ -1952,7 +1956,7 @@ private: //Windows test, FileZilla Server and Windows IIS FTP: all symlinks are reported as regular folders //tested freefilesync.org: RMD will fail for symlinks! bool symlinkExists = false; - try { symlinkExists = getItemType(afsPath) == ItemType::SYMLINK; } /*throw FileError*/ catch (FileError&) {} + try { symlinkExists = getItemType(afsPath) == ItemType::symlink; } /*throw FileError*/ catch (FileError&) {} if (symlinkExists) return removeSymlinkPlain(afsPath); //throw FileError @@ -1980,9 +1984,9 @@ private: throw FileError(replaceCpy(_("Cannot determine final path for %x."), L"%x", fmtPath(getDisplayPath(afsPath))), _("Operation not supported by device.")); } - std::string getSymlinkBinaryContent(const AfsPath& afsPath) const override //throw FileError + bool equalSymlinkContentForSameAfsType(const AfsPath& afsLhs, const AbstractPath& apRhs) const override //throw FileError { - throw FileError(replaceCpy(_("Cannot resolve symbolic link %x."), L"%x", fmtPath(getDisplayPath(afsPath))), _("Operation not supported by device.")); + throw FileError(replaceCpy(_("Cannot resolve symbolic link %x."), L"%x", fmtPath(getDisplayPath(afsLhs))), _("Operation not supported by device.")); } //---------------------------------------------------------------------------------------------------------------- @@ -2013,7 +2017,7 @@ private: //symlink handling: follow link! //target existing: undefined behavior! (fail/overwrite/auto-rename) - FileCopyResult copyFileForSameAfsType(const AfsPath& afsPathSource, const StreamAttributes& attrSource, //throw FileError, (ErrorFileLocked), X + FileCopyResult copyFileForSameAfsType(const AfsPath& afsSource, const StreamAttributes& attrSource, //throw FileError, (ErrorFileLocked), X const AbstractPath& apTarget, bool copyFilePermissions, const IOCallback& notifyUnbufferedIO /*throw X*/) const override { //no native FTP file copy => use stream-based file copy: @@ -2021,12 +2025,12 @@ private: throw FileError(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(AFS::getDisplayPath(apTarget))), _("Operation not supported by device.")); //target existing: undefined behavior! (fail/overwrite/auto-rename) - return copyFileAsStream(afsPathSource, attrSource, apTarget, notifyUnbufferedIO); //throw FileError, (ErrorFileLocked), X + return copyFileAsStream(afsSource, attrSource, apTarget, notifyUnbufferedIO); //throw FileError, (ErrorFileLocked), X } //target existing: fail/ignore //symlink handling: follow link! - void copyNewFolderForSameAfsType(const AfsPath& afsPathSource, const AbstractPath& apTarget, bool copyFilePermissions) const override //throw FileError + void copyNewFolderForSameAfsType(const AfsPath& afsSource, const AbstractPath& apTarget, bool copyFilePermissions) const override //throw FileError { if (copyFilePermissions) throw FileError(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(AFS::getDisplayPath(apTarget))), _("Operation not supported by device.")); @@ -2035,10 +2039,10 @@ private: AFS::createFolderPlain(apTarget); //throw FileError } - void copySymlinkForSameAfsType(const AfsPath& afsPathSource, const AbstractPath& apTarget, bool copyFilePermissions) const override + void copySymlinkForSameAfsType(const AfsPath& afsSource, const AbstractPath& apTarget, bool copyFilePermissions) const override { throw FileError(replaceCpy(replaceCpy(_("Cannot copy symbolic link %x to %y."), - L"%x", L'\n' + fmtPath(getDisplayPath(afsPathSource))), + L"%x", L'\n' + fmtPath(getDisplayPath(afsSource))), L"%y", L'\n' + fmtPath(AFS::getDisplayPath(apTarget))), _("Operation not supported by device.")); } @@ -2093,7 +2097,7 @@ private: bool hasNativeTransactionalCopy() const override { return false; } //---------------------------------------------------------------------------------------------------------------- - uint64_t getFreeDiskSpace(const AfsPath& afsPath) const override { return 0; } //throw FileError, returns 0 if not available + int64_t getFreeDiskSpace(const AfsPath& afsPath) const override { return -1; } //throw FileError, returns < 0 if not available bool supportsRecycleBin(const AfsPath& afsPath) const override { return false; } //throw FileError @@ -2109,20 +2113,20 @@ private: throw FileError(replaceCpy(_("Unable to move %x to the recycle bin."), L"%x", fmtPath(getDisplayPath(afsPath))), _("Operation not supported by device.")); } - const FtpLoginInfo login_; + const FtpLogin login_; }; //=========================================================================================================================== //expects "clean" login data -Zstring concatenateFtpFolderPathPhrase(const FtpLoginInfo& login, const AfsPath& afsPath) //noexcept +Zstring concatenateFtpFolderPathPhrase(const FtpLogin& login, const AfsPath& afsPath) //noexcept { Zstring port; if (login.port > 0) port = Zstr(':') + numberTo(login.port); Zstring options; - if (login.timeoutSec != FtpLoginInfo().timeoutSec) + if (login.timeoutSec != FtpLogin().timeoutSec) options += Zstr("|timeout=") + numberTo(login.timeoutSec); if (login.useTls) @@ -2154,7 +2158,7 @@ void fff::ftpTeardown() } -AfsPath fff::getFtpHomePath(const FtpLoginInfo& login) //throw FileError +AfsPath fff::getFtpHomePath(const FtpLogin& login) //throw FileError { try { @@ -2170,10 +2174,10 @@ AfsPath fff::getFtpHomePath(const FtpLoginInfo& login) //throw FileError } -AfsDevice fff::condenseToFtpDevice(const FtpLoginInfo& login) //noexcept +AfsDevice fff::condenseToFtpDevice(const FtpLogin& login) //noexcept { //clean up input: - FtpLoginInfo loginTmp = login; + FtpLogin loginTmp = login; trim(loginTmp.server); trim(loginTmp.username); @@ -2191,7 +2195,7 @@ AfsDevice fff::condenseToFtpDevice(const FtpLoginInfo& login) //noexcept } -FtpLoginInfo fff::extractFtpLogin(const AfsDevice& afsDevice) //noexcept +FtpLogin fff::extractFtpLogin(const AfsDevice& afsDevice) //noexcept { if (const auto ftpDevice = dynamic_cast(&afsDevice.ref())) return ftpDevice->getLogin(); @@ -2225,7 +2229,7 @@ AbstractPath fff::createItemPathFtp(const Zstring& itemPathPhrase) //noexcept const Zstring credentials = beforeFirst(pathPhrase, Zstr('@'), IF_MISSING_RETURN_NONE); const Zstring fullPathOpt = afterFirst(pathPhrase, Zstr('@'), IF_MISSING_RETURN_ALL); - FtpLoginInfo login; + FtpLogin login; login.username = decodeFtpUsername(beforeFirst(credentials, Zstr(':'), IF_MISSING_RETURN_ALL)); //support standard FTP syntax, even though ':' login.password = afterFirst(credentials, Zstr(':'), IF_MISSING_RETURN_NONE); //is not used by concatenateFtpFolderPathPhrase()! diff --git a/FreeFileSync/Source/afs/ftp.h b/FreeFileSync/Source/afs/ftp.h index 2978cdec..91dbf47a 100644 --- a/FreeFileSync/Source/afs/ftp.h +++ b/FreeFileSync/Source/afs/ftp.h @@ -20,7 +20,7 @@ void ftpTeardown(); //------------------------------------------------------- -struct FtpLoginInfo +struct FtpLogin { Zstring server; int port = 0; // > 0 if set @@ -31,10 +31,10 @@ struct FtpLoginInfo //other settings not specific to FTP session: int timeoutSec = 15; }; -AfsDevice condenseToFtpDevice(const FtpLoginInfo& login); //noexcept; potentially messy user input -FtpLoginInfo extractFtpLogin(const AfsDevice& afsDevice); //noexcept +AfsDevice condenseToFtpDevice(const FtpLogin& login); //noexcept; potentially messy user input +FtpLogin extractFtpLogin(const AfsDevice& afsDevice); //noexcept -AfsPath getFtpHomePath(const FtpLoginInfo& login); //throw FileError +AfsPath getFtpHomePath(const FtpLogin& login); //throw FileError } #endif //FTP_H_745895742383425326568678 diff --git a/FreeFileSync/Source/afs/gdrive.cpp b/FreeFileSync/Source/afs/gdrive.cpp index 70d7412d..753c7d66 100644 --- a/FreeFileSync/Source/afs/gdrive.cpp +++ b/FreeFileSync/Source/afs/gdrive.cpp @@ -36,16 +36,20 @@ namespace fff { struct GdrivePath { - Zstring userEmail; - AfsPath itemPath; //path relative to Google Drive root + GdriveLogin gdriveLogin; + AfsPath itemPath; //path relative to drive root }; bool operator<(const GdrivePath& lhs, const GdrivePath& rhs) { - const int rv = compareAsciiNoCase(lhs.userEmail, rhs.userEmail); - if (rv != 0) + if (const int rv = compareAsciiNoCase(lhs.gdriveLogin.email, rhs.gdriveLogin.email); + rv != 0) + return rv < 0; + + //mirror GdriveFileState file path matching + if (const int rv = compareNativePath(lhs.gdriveLogin.sharedDriveName, rhs.gdriveLogin.sharedDriveName); + rv != 0) return rv < 0; - //mirror GoogleFileState file path matching return compareNativePath(lhs.itemPath.value, rhs.itemPath.value) < 0; } @@ -65,18 +69,19 @@ const Zchar* GOOGLE_REST_API_SERVER = Zstr("www.googleapis.com"); const std::chrono::seconds HTTP_SESSION_ACCESS_TIME_OUT(15); const std::chrono::seconds HTTP_SESSION_MAX_IDLE_TIME (20); const std::chrono::seconds HTTP_SESSION_CLEANUP_INTERVAL(4); -const std::chrono::seconds GOOGLE_DRIVE_SYNC_INTERVAL (5); +const std::chrono::seconds GDRIVE_SYNC_INTERVAL (5); const int GDRIVE_STREAM_BUFFER_SIZE = 512 * 1024; //unit: [byte] -const Zchar googleDrivePrefix[] = Zstr("gdrive:"); -const char googleFolderMimeType[] = "application/vnd.google-apps.folder"; +const Zchar 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: Google Drive Database"; -const int DB_FILE_VERSION = 2; //2019-12-05 +const int DB_FILE_VERSION = 3; //2020-06-11 -std::string getGoogleDriveClientId () { return ""; } // => replace with live credentials -std::string getGoogleDriveClientSecret() { return ""; } // +std::string getGdriveClientId () { return ""; } // => replace with live credentials +std::string getGdriveClientSecret() { return ""; } // @@ -96,23 +101,31 @@ bool operator<(const HttpSessionId& lhs, const HttpSessionId& rhs) //expects "clean" input data -Zstring concatenateGoogleFolderPathPhrase(const GdrivePath& gdrivePath) //noexcept +Zstring concatenateGdriveFolderPathPhrase(const GdrivePath& gdrivePath) //noexcept { - Zstring pathPhrase = Zstring(googleDrivePrefix) + FILE_NAME_SEPARATOR + gdrivePath.userEmail; + Zstring pathPhrase = Zstring(gdrivePrefix) + FILE_NAME_SEPARATOR + utfTo(gdrivePath.gdriveLogin.email); + + if (!gdrivePath.gdriveLogin.sharedDriveName.empty()) + pathPhrase += Zstr(':') + gdrivePath.gdriveLogin.sharedDriveName; + if (!gdrivePath.itemPath.value.empty()) pathPhrase += FILE_NAME_SEPARATOR + gdrivePath.itemPath.value; + + if (endsWith(pathPhrase, Zstr(' '))) //path phrase concept must survive trimming! + pathPhrase += FILE_NAME_SEPARATOR; + return pathPhrase; } -//e.g.: gdrive:/john@gmail.com/folder/file.txt -std::wstring getGoogleDisplayPath(const GdrivePath& gdrivePath) +//e.g.: gdrive:/john@gmail.com:SharedDrive/folder/file.txt +std::wstring getGdriveDisplayPath(const GdrivePath& gdrivePath) { - return utfTo(concatenateGoogleFolderPathPhrase(gdrivePath)); //noexcept + return utfTo(concatenateGdriveFolderPathPhrase(gdrivePath)); //noexcept } -std::wstring formatGoogleErrorRaw(const std::string& serverResponse) +std::wstring formatGdriveErrorRaw(const std::string& serverResponse) { /* e.g.: { "error": { "errors": [{ "domain": "global", "reason": "invalidSharingRequest", @@ -143,6 +156,9 @@ std::wstring formatGoogleErrorRaw(const std::string& serverResponse) catch (JsonParsingError&) {} //not JSON? assert(false); + if (trimCpy(serverResponse).empty()) + return L"<" + _("empty") + L">"; //at least give some indication + return utfTo(serverResponse); } @@ -281,7 +297,7 @@ Global globalHttpSessionManager; //caveat: life time must be //=========================================================================================================================== //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 googleHttpsRequest(const std::string& serverRelPath, //throw SysError +HttpSession::Result gdriveHttpsRequest(const std::string& serverRelPath, //throw SysError const std::vector& extraHeaders, const std::vector& extraOptions, const std::function& writeResponse /*throw X*/, //optional @@ -289,7 +305,7 @@ HttpSession::Result googleHttpsRequest(const std::string& serverRelPath, //throw { const std::shared_ptr mgr = globalHttpSessionManager.get(); if (!mgr) - throw SysError(formatSystemError("googleHttpsRequest", L"", L"Function call not allowed during init/shutdown.")); + throw SysError(formatSystemError("gdriveHttpsRequest", L"", L"Function call not allowed during init/shutdown.")); HttpSession::Result httpResult; @@ -310,20 +326,20 @@ HttpSession::Result googleHttpsRequest(const std::string& serverRelPath, //throw //======================================================================================================== -struct GoogleUserInfo +struct GdriveUser { std::wstring displayName; - Zstring email; + std::string email; }; -GoogleUserInfo getUserInfo(const std::string& accessToken) //throw SysError +GdriveUser getGdriveUser(const std::string& accessToken) //throw SysError { //https://developers.google.com/drive/api/v3/reference/about - const std::string queryParams = xWwwFormUrlEncode( + const std::string& queryParams = xWwwFormUrlEncode( { { "fields", "user/displayName,user/emailAddress" }, }); std::string response; - googleHttpsRequest("/drive/v3/about?" + queryParams, { "Authorization: Bearer " + accessToken }, {} /*extraOptions*/, //throw SysError + gdriveHttpsRequest("/drive/v3/about?" + queryParams, { "Authorization: Bearer " + accessToken }, {} /*extraOptions*/, //throw SysError [&](const void* buffer, size_t bytesToWrite) { response.append(static_cast(buffer), bytesToWrite); }, nullptr /*readRequest*/); JsonValue jresponse; @@ -335,10 +351,10 @@ GoogleUserInfo getUserInfo(const std::string& accessToken) //throw SysError const std::optional displayName = getPrimitiveFromJsonObject(*user, "displayName"); const std::optional email = getPrimitiveFromJsonObject(*user, "emailAddress"); if (displayName && email) - return { utfTo(*displayName), utfTo(*email) }; + return { utfTo(*displayName), *email }; } - throw SysError(formatGoogleErrorRaw(response)); + throw SysError(formatGdriveErrorRaw(response)); } @@ -369,40 +385,40 @@ const char htmlMessageTemplate[] = R"( )"; -struct GoogleAuthCode +struct GdriveAuthCode { std::string code; std::string redirectUrl; std::string codeChallenge; }; -struct GoogleAccessToken +struct GdriveAccessToken { std::string value; time_t validUntil = 0; //remaining lifetime of the access token }; -struct GoogleAccessInfo +struct GdriveAccessInfo { - GoogleAccessToken accessToken; + GdriveAccessToken accessToken; std::string refreshToken; - GoogleUserInfo userInfo; + GdriveUser userInfo; }; -GoogleAccessInfo googleDriveExchangeAuthCode(const GoogleAuthCode& authCode) //throw SysError +GdriveAccessInfo gdriveExchangeAuthCode(const GdriveAuthCode& authCode) //throw SysError { //https://developers.google.com/identity/protocols/OAuth2InstalledApp#exchange-authorization-code const std::string postBuf = xWwwFormUrlEncode( { { "code", authCode.code }, - { "client_id", getGoogleDriveClientId() }, - { "client_secret", getGoogleDriveClientSecret() }, + { "client_id", getGdriveClientId() }, + { "client_secret", getGdriveClientSecret() }, { "redirect_uri", authCode.redirectUrl }, { "grant_type", "authorization_code" }, { "code_verifier", authCode.codeChallenge }, }); std::string response; - googleHttpsRequest("/oauth2/v4/token", {} /*extraHeaders*/, { { CURLOPT_POSTFIELDS, postBuf.c_str() } }, //throw SysError + gdriveHttpsRequest("/oauth2/v4/token", {} /*extraHeaders*/, { { CURLOPT_POSTFIELDS, postBuf.c_str() } }, //throw SysError [&](const void* buffer, size_t bytesToWrite) { response.append(static_cast(buffer), bytesToWrite); }, nullptr /*readRequest*/); JsonValue jresponse; @@ -413,15 +429,15 @@ GoogleAccessInfo googleDriveExchangeAuthCode(const GoogleAuthCode& authCode) //t 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(formatGoogleErrorRaw(response)); + throw SysError(formatGdriveErrorRaw(response)); - const GoogleUserInfo userInfo = getUserInfo(*accessToken); //throw SysError + const GdriveUser userInfo = getGdriveUser(*accessToken); //throw SysError return { { *accessToken, std::time(nullptr) + stringTo(*expiresIn) }, *refreshToken, userInfo }; } -GoogleAccessInfo authorizeAccessToGoogleDrive(const Zstring& googleLoginHint, const std::function& updateGui /*throw X*/) //throw SysError, X +GdriveAccessInfo gdriveAuthorizeAccess(const std::string& gdriveLoginHint, const std::function& updateGui /*throw X*/) //throw SysError, X { //spin up a web server to wait for the HTTP GET after Google authentication ::addrinfo hints = {}; @@ -500,13 +516,13 @@ if (::listen(socket, SOMAXCONN) != 0) //authenticate Google Drive via browser: https://developers.google.com/identity/protocols/OAuth2InstalledApp#step-2-send-a-request-to-googles-oauth-20-server const std::string oauthUrl = "https://accounts.google.com/o/oauth2/v2/auth?" + xWwwFormUrlEncode( { - { "client_id", getGoogleDriveClientId() }, + { "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", utfTo(googleLoginHint) }, + { "login_hint", gdriveLoginHint }, }); try { @@ -579,7 +595,7 @@ for (;;) //::accept() blocks forever if no client connects (e.g. user just close error = value; //e.g. "access_denied" => no more detailed error info available :( } //"add explicit braces to avoid dangling else [-Wdangling-else]" - std::optional> authResult; + std::optional> authResult; //send HTTP response; https://www.w3.org/Protocols/HTTP/1.0/spec.html#Request-Line std::string httpResponse; @@ -595,7 +611,7 @@ for (;;) //::accept() blocks forever if no client connects (e.g. user just close //do as many login-related tasks as possible while we have the browser as an error output device! //see AFS::connectNetworkFolder() => errors will be lost after time out in dir_exist_async.h! - authResult = googleDriveExchangeAuthCode({ code, redirectUrl, codeChallenge }); //throw SysError + authResult = gdriveExchangeAuthCode({ code, redirectUrl, codeChallenge }); //throw SysError replace(htmlMsg, "TITLE_PLACEHOLDER", utfTo(_("Authentication completed."))); replace(htmlMsg, "MESSAGE_PLACEHOLDER", utfTo(_("You may close this page now and continue with FreeFileSync."))); } @@ -621,25 +637,25 @@ for (;;) //::accept() blocks forever if no client connects (e.g. user just close { if (const SysError* e = std::get_if(&*authResult)) throw *e; - return std::get(*authResult); + return std::get(*authResult); } } } -GoogleAccessToken refreshAccessToGoogleDrive(const std::string& refreshToken) //throw SysError +GdriveAccessToken gdriveRefreshAccess(const std::string& refreshToken) //throw SysError { //https://developers.google.com/identity/protocols/OAuth2InstalledApp#offline const std::string postBuf = xWwwFormUrlEncode( { { "refresh_token", refreshToken }, - { "client_id", getGoogleDriveClientId() }, - { "client_secret", getGoogleDriveClientSecret() }, + { "client_id", getGdriveClientId() }, + { "client_secret", getGdriveClientSecret() }, { "grant_type", "refresh_token" }, }); std::string response; - googleHttpsRequest("/oauth2/v4/token", {} /*extraHeaders*/, { { CURLOPT_POSTFIELDS, postBuf.c_str() } }, //throw SysError + gdriveHttpsRequest("/oauth2/v4/token", {} /*extraHeaders*/, { { CURLOPT_POSTFIELDS, postBuf.c_str() } }, //throw SysError [&](const void* buffer, size_t bytesToWrite) { response.append(static_cast(buffer), bytesToWrite); }, nullptr /*readRequest*/); JsonValue jresponse; @@ -649,18 +665,18 @@ GoogleAccessToken refreshAccessToGoogleDrive(const std::string& refreshToken) // 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(formatGoogleErrorRaw(response)); + throw SysError(formatGdriveErrorRaw(response)); return { *accessToken, std::time(nullptr) + stringTo(*expiresIn) }; } -void revokeAccessToGoogleDrive(const std::string& accessToken, const Zstring& googleUserEmail) //throw SysError +void gdriveRevokeAccess(const std::string& accessToken) //throw SysError { //https://developers.google.com/identity/protocols/OAuth2InstalledApp#tokenrevoke const std::shared_ptr mgr = globalHttpSessionManager.get(); if (!mgr) - throw SysError(formatSystemError("revokeAccessToGoogleDrive", L"", L"Function call not allowed during init/shutdown.")); + throw SysError(formatSystemError("gdriveRevokeAccess", L"", L"Function call not allowed during init/shutdown.")); HttpSession::Result httpResult; std::string response; @@ -672,15 +688,15 @@ void revokeAccessToGoogleDrive(const std::string& accessToken, const Zstring& go }); if (httpResult.statusCode != 200) - throw SysError(formatGoogleErrorRaw(response)); + throw SysError(formatGdriveErrorRaw(response)); } -uint64_t gdriveGetFreeDiskSpace(const std::string& accessToken) //throw SysError; returns 0 if not available +int64_t gdriveGetMyDriveFreeSpace(const std::string& accessToken) //throw SysError; returns < 0 if not available { //https://developers.google.com/drive/api/v3/reference/about std::string response; - googleHttpsRequest("/drive/v3/about?fields=storageQuota", { "Authorization: Bearer " + accessToken }, {} /*extraOptions*/, //throw SysError + gdriveHttpsRequest("/drive/v3/about?fields=storageQuota", { "Authorization: Bearer " + accessToken }, {} /*extraOptions*/, //throw SysError [&](const void* buffer, size_t bytesToWrite) { response.append(static_cast(buffer), bytesToWrite); }, nullptr /*readRequest*/); JsonValue jresponse; @@ -689,160 +705,296 @@ uint64_t gdriveGetFreeDiskSpace(const std::string& accessToken) //throw SysError if (const JsonValue* storageQuota = getChildFromJsonObject(jresponse, "storageQuota")) { - const std::optional limit = getPrimitiveFromJsonObject(*storageQuota, "limit"); const std::optional usage = getPrimitiveFromJsonObject(*storageQuota, "usage"); - - if (!limit) //"will not be present if the user has unlimited storage." - return 0; + const std::optional limit = getPrimitiveFromJsonObject(*storageQuota, "limit"); if (usage) { - const auto usageInt = stringTo(*usage); - const auto limitInt = stringTo(*limit); + 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 <= usageInt && usageInt <= limitInt) - return limitInt - usageInt; + if (0 <= bytesUsed && bytesUsed <= bytesLimit) + return bytesLimit - bytesUsed; } } - throw SysError(formatGoogleErrorRaw(response)); + throw SysError(formatGdriveErrorRaw(response)); } -struct GoogleItemDetails -{ - std::string itemName; - bool isFolder = false; - bool isShared = false; - uint64_t fileSize = 0; - time_t modTime = 0; - std::vector parentIds; -}; -bool operator==(const GoogleItemDetails& lhs, const GoogleItemDetails& rhs) -{ - return lhs.itemName == rhs.itemName && - lhs.isFolder == rhs.isFolder && - lhs.isShared == rhs.isShared && - lhs.fileSize == rhs.fileSize && - lhs.modTime == rhs.modTime && - lhs.parentIds == rhs.parentIds; -} - -struct GoogleFileItem +struct DriveDetails { - std::string itemId; - GoogleItemDetails details; + std::string driveId; + Zstring driveName; }; -std::vector readFolderContent(const std::string& folderId, const std::string& accessToken) //throw SysError -{ -//https://developers.google.com/drive/api/v3/reference/files/list -std::vector childItems; +std::vector getSharedDrives(const std::string& accessToken) //throw SysError { - std::optional nextPageToken; - do + //https://developers.google.com/drive/api/v3/reference/drives/list + std::vector sharedDrives; { - std::string queryParams = xWwwFormUrlEncode( + std::optional nextPageToken; + do { - { "spaces", "drive" }, // - { "corpora", "user" }, //"The 'user' corpus includes all files in "My Drive" and "Shared with me" https://developers.google.com/drive/api/v3/reference/files/list - { "pageSize", "1000" }, //"[1, 1000] Default: 100" - { "fields", "nextPageToken,incompleteSearch,files(name,id,mimeType,shared,size,modifiedTime,parents)" -}, //https://developers.google.com/drive/api/v3/reference/files -{ "q", "trashed=false and '" + folderId + "' in parents" }, + std::string queryParams = xWwwFormUrlEncode( + { + { "pageSize", "100" }, //"[1, 100] Default: 10" + { "fields", "nextPageToken,drives(id,name)" +} }); if (nextPageToken) queryParams += '&' + xWwwFormUrlEncode({ { "pageToken", *nextPageToken } }); std::string response; -googleHttpsRequest("/drive/v3/files?" + queryParams, { "Authorization: Bearer " + accessToken }, {} /*extraOptions*/, //throw SysError +gdriveHttpsRequest("/drive/v3/drives?" + queryParams, { "Authorization: Bearer " + accessToken }, {} /*extraOptions*/, //throw SysError [&](const void* buffer, size_t bytesToWrite) { response.append(static_cast(buffer), bytesToWrite); }, nullptr /*readRequest*/); 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(formatGoogleErrorRaw(response)); +/**/ 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) + throw SysError(formatGdriveErrorRaw(serializeJson(driveVal))); + + sharedDrives.push_back({ std::move(*driveId), utfTo(*driveName) }); + } +} +while (nextPageToken); +} +return sharedDrives; +} + + +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& lhs, const GdriveItemDetails& rhs) +{ +return lhs.itemName == rhs.itemName && + lhs.fileSize == rhs.fileSize && + lhs.modTime == rhs.modTime && + lhs.type == rhs.type && + lhs.owner == rhs.owner && + lhs.targetId == rhs.targetId && + lhs.parentIds == rhs.parentIds; +} -for (const auto& childVal : files->arrayVal) + +GdriveItemDetails extractItemDetails(JsonValue jvalue) //throw SysError { - const std::optional itemId = getPrimitiveFromJsonObject(childVal, "id"); - const std::optional itemName = getPrimitiveFromJsonObject(childVal, "name"); - const std::optional mimeType = getPrimitiveFromJsonObject(childVal, "mimeType"); - const std::optional shared = getPrimitiveFromJsonObject(childVal, "shared"); - const std::optional size = getPrimitiveFromJsonObject(childVal, "size"); - const std::optional modifiedTime = getPrimitiveFromJsonObject(childVal, "modifiedTime"); - const JsonValue* parents = getChildFromJsonObject (childVal, "parents"); +assert(jvalue.type == JsonValue::Type::object); - if (!itemId || !itemName || !mimeType || !modifiedTime || !parents) - throw SysError(formatGoogleErrorRaw(response)); + /**/ 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"); - const bool isFolder = *mimeType == googleFolderMimeType; - const bool isShared = shared && *shared == "true"; //"Not populated for items in shared drives" - const uint64_t fileSize = size ? stringTo(*size) : 0; //not available for folders + if (!itemName || !mimeType || !modifiedTime) + throw SysError(formatGdriveErrorRaw(serializeJson(jvalue))); - //RFC 3339 date-time: e.g. "2018-09-29T08:39:12.053Z" - const TimeComp tc = parseTime("%Y-%m-%dT%H:%M:%S", beforeLast(*modifiedTime, '.', IF_MISSING_RETURN_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 could not be parsed. (" + utfTo(*modifiedTime) + L')'); + const GdriveItemType type = *mimeType == gdriveFolderMimeType ? GdriveItemType::folder : + *mimeType == gdriveShortcutMimeType ? GdriveItemType::shortcut : + GdriveItemType::file; - time_t modTime = utcToTimeT(tc); //returns -1 on error - if (modTime == -1) - { - if (tc.year == 1600 || //zero-initialized FILETIME equals "December 31, 1600" or "January 1, 1601" - tc.year == 1601) // => yes, possible even on Google Drive: https://freefilesync.org/forum/viewtopic.php?t=6602 - modTime = 0; - else - throw SysError(L"Modification time could not be parsed. (" + utfTo(*modifiedTime) + L')'); - } + 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, '.', IF_MISSING_RETURN_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 could not be parsed. (" + utfTo(*modifiedTime) + L')'); + + time_t modTime = utcToTimeT(tc); //returns -1 on error + if (modTime == -1) + { + if (tc.year == 1600 || //zero-initialized FILETIME equals "December 31, 1600" or "January 1, 1601" + tc.year == 1601) // => yes, possible even on Google Drive: https://freefilesync.org/forum/viewtopic.php?t=6602 + modTime = 0; + else + throw SysError(L"Modification time could not be parsed. (" + utfTo(*modifiedTime) + L')'); + } - std::vector parentIds; - for (const auto& parentVal : parents->arrayVal) + std::vector parentIds; + if (parents) //item without parents is possible! e.g. shared item located in "Shared with me", referenced via a Shortcut + for (const JsonValue& parentVal : parents->arrayVal) { if (parentVal.type != JsonValue::Type::string) - throw SysError(formatGoogleErrorRaw(response)); - parentIds.push_back(parentVal.primVal); + throw SysError(formatGdriveErrorRaw(serializeJson(jvalue))); + parentIds.emplace_back(parentVal.primVal); } - assert(std::find(parentIds.begin(), parentIds.end(), folderId) != parentIds.end()); - childItems.push_back({ *itemId, { *itemName, isFolder, isShared, fileSize, modTime, std::move(parentIds) } }); + 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) }; } -while (nextPageToken); + + +GdriveItemDetails getItemDetails(const std::string& itemId, const std::string& accessToken) //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, { "Authorization: Bearer " + accessToken }, {} /*extraOptions*/, //throw SysError + [&](const void* buffer, size_t bytesToWrite) { response.append(static_cast(buffer), bytesToWrite); }, nullptr /*readRequest*/); + 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)); } } -return childItems; + + +struct GdriveItem +{ + std::string itemId; + GdriveItemDetails details; +}; +std::vector readFolderContent(const std::string& folderId, const std::string& accessToken) //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", "trashed=false and '" + folderId + "' in parents" }, + { "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, { "Authorization: Bearer " + accessToken }, {} /*extraOptions*/, //throw SysError + [&](const void* buffer, size_t bytesToWrite) { response.append(static_cast(buffer), bytesToWrite); }, nullptr /*readRequest*/); + + 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 ChangeItem +struct FileChange { -std::string itemId; -std::optional details; //empty if item was deleted! + 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 changes; + std::string newStartPageToken; + std::vector fileChanges; + std::vector driveChanges; }; ChangesDelta getChangesDelta(const std::string& startPageToken, const std::string& accessToken) //throw SysError { -//https://developers.google.com/drive/api/v3/reference/changes/list -ChangesDelta delta; -std::optional nextPageToken = startPageToken; -for (;;) + //https://developers.google.com/drive/api/v3/reference/changes/list + ChangesDelta delta; + std::optional nextPageToken = startPageToken; + for (;;) { - std::string queryParams = xWwwFormUrlEncode( + const std::string& queryParams = xWwwFormUrlEncode( { - { "pageToken", *nextPageToken }, + { "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" }, { "pageSize", "1000" }, //"[1, 1000] Default: 100" - { "restrictToMyDrive", "true" }, //important! otherwise we won't get "removed: true" (because file may still be accessible from other Corpora) - { "spaces", "drive" }, - { "fields", "kind,nextPageToken,newStartPageToken,changes(kind,removed,fileId,file(name,mimeType,shared,size,modifiedTime,parents,trashed))" }, + { "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" }); - std::string response; - googleHttpsRequest("/drive/v3/changes?" + queryParams, { "Authorization: Bearer " + accessToken }, {} /*extraOptions*/, //throw SysError + gdriveHttpsRequest("/drive/v3/changes?" + queryParams, { "Authorization: Bearer " + accessToken }, {} /*extraOptions*/, //throw SysError [&](const void* buffer, size_t bytesToWrite) { response.append(static_cast(buffer), bytesToWrite); }, nullptr /*readRequest*/); JsonValue jresponse; @@ -857,67 +1009,62 @@ for (;;) if (!!nextPageToken == !!newStartPageToken || //there can be only one !listKind || *listKind != "drive#changeList" || !changes || changes->type != JsonValue::Type::array) - throw SysError(formatGoogleErrorRaw(response)); + throw SysError(formatGdriveErrorRaw(response)); - for (const auto& childVal : changes->arrayVal) + for (const JsonValue& childVal : changes->arrayVal) { - const std::optional kind = getPrimitiveFromJsonObject(childVal, "kind"); - const std::optional removed = getPrimitiveFromJsonObject(childVal, "removed"); - const std::optional itemId = getPrimitiveFromJsonObject(childVal, "fileId"); - const JsonValue* file = getChildFromJsonObject (childVal, "file"); - if (!kind || *kind != "drive#change" || !removed || !itemId) - throw SysError(formatGoogleErrorRaw(response)); + 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))); - ChangeItem changeItem; - changeItem.itemId = *itemId; - if (*removed != "true") + if (*changeType == "file") { - if (!file) - throw SysError(formatGoogleErrorRaw(response)); - - const std::optional itemName = getPrimitiveFromJsonObject(*file, "name"); - const std::optional mimeType = getPrimitiveFromJsonObject(*file, "mimeType"); - const std::optional shared = getPrimitiveFromJsonObject(*file, "shared"); - const std::optional size = getPrimitiveFromJsonObject(*file, "size"); - const std::optional modifiedTime = getPrimitiveFromJsonObject(*file, "modifiedTime"); - const std::optional trashed = getPrimitiveFromJsonObject(*file, "trashed"); - const JsonValue* parents = getChildFromJsonObject (*file, "parents"); - if (!itemName || !mimeType || !modifiedTime || !trashed || !parents) - throw SysError(formatGoogleErrorRaw(response)); - - if (*trashed != "true") + 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") { - GoogleItemDetails itemDetails = {}; - itemDetails.itemName = *itemName; - itemDetails.isFolder = *mimeType == googleFolderMimeType; - itemDetails.isShared = shared && *shared == "true"; //"Not populated for items in shared drives" - itemDetails.fileSize = size ? stringTo(*size) : 0; //not available for folders - - //RFC 3339 date-time: e.g. "2018-09-29T08:39:12.053Z" - const TimeComp tc = parseTime("%Y-%m-%dT%H:%M:%S", beforeLast(*modifiedTime, '.', IF_MISSING_RETURN_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 could not be parsed. (" + utfTo(*modifiedTime) + L')'); - - itemDetails.modTime = utcToTimeT(tc); //returns -1 on error - if (itemDetails.modTime == -1) - { - if (tc.year == 1600 || //zero-initialized FILETIME equals "December 31, 1600" or "January 1, 1601" - tc.year == 1601) // => yes, possible even on Google Drive: https://freefilesync.org/forum/viewtopic.php?t=6602 - itemDetails.modTime = 0; - else - throw SysError(L"Modification time could not be parsed. (" + utfTo(*modifiedTime) + L')'); - } + const JsonValue* file = getChildFromJsonObject(childVal, "file"); + if (!file) + throw SysError(formatGdriveErrorRaw(serializeJson(childVal))); - for (const auto& parentVal : parents->arrayVal) - { - if (parentVal.type != JsonValue::Type::string) - throw SysError(formatGoogleErrorRaw(response)); - itemDetails.parentIds.push_back(parentVal.primVal); - } - changeItem.details = std::move(itemDetails); + 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)); } - delta.changes.push_back(std::move(changeItem)); + else assert(false); //no other types (yet!) } if (!nextPageToken) @@ -932,8 +1079,12 @@ for (;;) std::string /*startPageToken*/ getChangesCurrentToken(const std::string& accessToken) //throw SysError { //https://developers.google.com/drive/api/v3/reference/changes/getStartPageToken + const std::string& queryParams = xWwwFormUrlEncode( + { + { "supportsAllDrives", "true" }, + }); std::string response; - googleHttpsRequest("/drive/v3/changes/startPageToken", { "Authorization: Bearer " + accessToken }, {} /*extraOptions*/, //throw SysError + gdriveHttpsRequest("/drive/v3/changes/startPageToken?" + queryParams, { "Authorization: Bearer " + accessToken }, {} /*extraOptions*/, //throw SysError [&](const void* buffer, size_t bytesToWrite) { response.append(static_cast(buffer), bytesToWrite); }, nullptr /*readRequest*/); JsonValue jresponse; @@ -942,7 +1093,7 @@ std::string /*startPageToken*/ getChangesCurrentToken(const std::string& accessT const std::optional startPageToken = getPrimitiveFromJsonObject(jresponse, "startPageToken"); if (!startPageToken) - throw SysError(formatGoogleErrorRaw(response)); + throw SysError(formatGdriveErrorRaw(response)); return *startPageToken; } @@ -953,14 +1104,18 @@ std::string /*startPageToken*/ getChangesCurrentToken(const std::string& accessT void gdriveDeleteItem(const std::string& itemId, const std::string& accessToken) //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 = googleHttpsRequest("/drive/v3/files/" + itemId, { "Authorization: Bearer " + accessToken }, //throw SysError - { { CURLOPT_CUSTOMREQUEST, "DELETE" } }, - [&](const void* buffer, size_t bytesToWrite) { response.append(static_cast(buffer), bytesToWrite); }, nullptr /*readRequest*/); + const HttpSession::Result httpResult = gdriveHttpsRequest("/drive/v3/files/" + itemId + '?' + queryParams, { "Authorization: Bearer " + accessToken }, //throw SysError + { { CURLOPT_CUSTOMREQUEST, "DELETE" } }, [&](const void* buffer, size_t bytesToWrite) { response.append(static_cast(buffer), bytesToWrite); }, nullptr /*readRequest*/); - //"If successful, this method returns an empty response body" - if (!response.empty() || httpResult.statusCode != 204) - throw SysError(formatGoogleErrorRaw(response)); + if (response.empty() && httpResult.statusCode == 204) + return; //"If successful, this method returns an empty response body" + + throw SysError(formatGdriveErrorRaw(response)); } @@ -968,17 +1123,21 @@ void gdriveDeleteItem(const std::string& itemId, const std::string& accessToken) void gdriveUnlinkParent(const std::string& itemId, const std::string& parentFolderId, const std::string& accessToken) //throw SysError { //https://developers.google.com/drive/api/v3/reference/files/update - const std::string queryParams = xWwwFormUrlEncode( + const std::string& queryParams = xWwwFormUrlEncode( { { "removeParents", parentFolderId }, + { "supportsAllDrives", "true" }, { "fields", "id,parents" }, //for test if operation was successful }); std::string response; - googleHttpsRequest("/drive/v3/files/" + itemId + '?' + queryParams, //throw SysError + const HttpSession::Result httpResult = gdriveHttpsRequest("/drive/v3/files/" + itemId + '?' + queryParams, //throw SysError { "Authorization: Bearer " + accessToken, "Content-Type: application/json; charset=UTF-8" }, { { CURLOPT_CUSTOMREQUEST, "PATCH" }, { CURLOPT_POSTFIELDS, "{}" } }, [&](const void* buffer, size_t bytesToWrite) { response.append(static_cast(buffer), bytesToWrite); }, nullptr /*readRequest*/); + 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&) {} @@ -986,9 +1145,9 @@ void gdriveUnlinkParent(const std::string& itemId, const std::string& parentFold 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(formatGoogleErrorRaw(response)); + throw SysError(formatGdriveErrorRaw(response)); - if (parents) //when last parent is removed (=> Google deletes item permanently), Google does NOT return the parents array (not even an empty one!) + if (parents) //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 == parentFolderId; })) @@ -1001,11 +1160,15 @@ void gdriveUnlinkParent(const std::string& itemId, const std::string& parentFold void gdriveMoveToTrash(const std::string& itemId, const std::string& accessToken) //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; - googleHttpsRequest("/drive/v3/files/" + itemId + "?fields=trashed", //throw SysError - { "Authorization: Bearer " + accessToken, "Content-Type: application/json; charset=UTF-8" }, + gdriveHttpsRequest("/drive/v3/files/" + itemId + '?' + queryParams, { "Authorization: Bearer " + accessToken, "Content-Type: application/json; charset=UTF-8" }, //throw SysError { { CURLOPT_CUSTOMREQUEST, "PATCH" }, { CURLOPT_POSTFIELDS, postBuf.c_str() } }, [&](const void* buffer, size_t bytesToWrite) { response.append(static_cast(buffer), bytesToWrite); }, nullptr /*readRequest*/); @@ -1015,22 +1178,59 @@ void gdriveMoveToTrash(const std::string& itemId, const std::string& accessToken const std::optional trashed = getPrimitiveFromJsonObject(jresponse, "trashed"); if (!trashed || *trashed != "true") - throw SysError(formatGoogleErrorRaw(response)); + throw SysError(formatGdriveErrorRaw(response)); } -//folder name already existing? will (happily) create duplicate folders => caller must check! +//folder name already existing? will (happily) create duplicate => caller must check! std::string /*folderId*/ gdriveCreateFolderPlain(const Zstring& folderName, const std::string& parentFolderId, const std::string& accessToken) //throw SysError { //https://developers.google.com/drive/api/v3/folder#creating_a_folder + const std::string& queryParams = xWwwFormUrlEncode( + { + { "supportsAllDrives", "true" }, + { "fields", "id" }, + }); const std::string& postBuf = std::string("{\n") + - "\"mimeType\": \"" + std::string(googleFolderMimeType) + "\",\n" + - "\"name\": \"" + utfTo(folderName) + "\",\n" + - "\"parents\": [\"" + parentFolderId + "\"]\n" + //[!] no trailing comma! + "\"mimeType\": \"" + gdriveFolderMimeType + "\",\n" + "\"name\": \"" + utfTo(folderName) + "\",\n" + "\"parents\": [\"" + parentFolderId + "\"]\n" //[!] no trailing comma! "}"; + std::string response; + gdriveHttpsRequest("/drive/v3/files?" + queryParams, { "Authorization: Bearer " + accessToken, "Content-Type: application/json; charset=UTF-8" }, + { { CURLOPT_POSTFIELDS, postBuf.c_str() } }, + [&](const void* buffer, size_t bytesToWrite) { response.append(static_cast(buffer), bytesToWrite); }, nullptr /*readRequest*/); //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& parentFolderId, const std::string& targetId, const std::string& accessToken) //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" }, + }); + const std::string& postBuf = std::string("{\n") + + "\"mimeType\": \"" + gdriveShortcutMimeType + "\",\n" + "\"name\": \"" + utfTo(shortcutName) + "\",\n" + "\"shortcutDetails\": { \"targetId\": \"" + targetId + "\" },\n" + "\"parents\": [\"" + parentFolderId + "\"]\n" //[!] no trailing comma! + "}"; std::string response; - googleHttpsRequest("/drive/v3/files?fields=id", { "Authorization: Bearer " + accessToken, "Content-Type: application/json; charset=UTF-8" }, + gdriveHttpsRequest("/drive/v3/files?" + queryParams, { "Authorization: Bearer " + accessToken, "Content-Type: application/json; charset=UTF-8" }, { { CURLOPT_POSTFIELDS, postBuf.c_str() } }, [&](const void* buffer, size_t bytesToWrite) { response.append(static_cast(buffer), bytesToWrite); }, nullptr /*readRequest*/); //throw SysError @@ -1040,7 +1240,7 @@ std::string /*folderId*/ gdriveCreateFolderPlain(const Zstring& folderName, cons const std::optional itemId = getPrimitiveFromJsonObject(jresponse, "id"); if (!itemId) - throw SysError(formatGoogleErrorRaw(response)); + throw SysError(formatGdriveErrorRaw(response)); return *itemId; } @@ -1050,7 +1250,11 @@ void gdriveMoveAndRenameItem(const std::string& itemId, const std::string& paren const Zstring& newName, time_t newModTime, const std::string& accessToken) //throw SysError { //https://developers.google.com/drive/api/v3/folder#moving_files_between_folders - std::string queryParams = "fields=name,parents"; //for test if operation was successful + std::string queryParams = xWwwFormUrlEncode( + { + { "supportsAllDrives", "true" }, + { "fields", "name,parents" }, //for test if operation was successful + }); if (parentIdFrom != parentIdTo) queryParams += '&' + xWwwFormUrlEncode( @@ -1070,9 +1274,8 @@ void gdriveMoveAndRenameItem(const std::string& itemId, const std::string& paren "\"name\": \"" + utfTo(newName) + "\",\n" + "\"modifiedTime\": \"" + modTimeRfc + "\"\n" + //[!] no trailing comma! "}"; - std::string response; - googleHttpsRequest("/drive/v3/files/" + itemId + '?' + queryParams, //throw SysError + gdriveHttpsRequest("/drive/v3/files/" + itemId + '?' + queryParams, //throw SysError { "Authorization: Bearer " + accessToken, "Content-Type: application/json; charset=UTF-8" }, { { CURLOPT_CUSTOMREQUEST, "PATCH" }, { CURLOPT_POSTFIELDS, postBuf.c_str() } }, [&](const void* buffer, size_t bytesToWrite) { response.append(static_cast(buffer), bytesToWrite); }, nullptr /*readRequest*/); @@ -1085,7 +1288,7 @@ void gdriveMoveAndRenameItem(const std::string& itemId, const std::string& paren const JsonValue* parents = getChildFromJsonObject(jresponse, "parents"); if (!name || *name != utfTo(newName) || !parents || parents->type != JsonValue::Type::array) - throw SysError(formatGoogleErrorRaw(response)); + 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; })) @@ -1102,11 +1305,15 @@ void setModTime(const std::string& itemId, time_t modTime, const std::string& ac 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; - googleHttpsRequest("/drive/v3/files/" + itemId + "?fields=modifiedTime", //throw SysError - { "Authorization: Bearer " + accessToken, "Content-Type: application/json; charset=UTF-8" }, + gdriveHttpsRequest("/drive/v3/files/" + itemId + '?' + queryParams, { "Authorization: Bearer " + accessToken, "Content-Type: application/json; charset=UTF-8" }, //throw SysError { { CURLOPT_CUSTOMREQUEST, "PATCH" }, { CURLOPT_POSTFIELDS, postBuf.c_str() } }, [&](const void* buffer, size_t bytesToWrite) { response.append(static_cast(buffer), bytesToWrite); }, nullptr /*readRequest*/); @@ -1116,7 +1323,7 @@ void setModTime(const std::string& itemId, time_t modTime, const std::string& ac const std::optional modifiedTime = getPrimitiveFromJsonObject(jresponse, "modifiedTime"); if (!modifiedTime || *modifiedTime != modTimeRfc) - throw SysError(formatGoogleErrorRaw(response)); + throw SysError(formatGdriveErrorRaw(response)); } #endif @@ -1125,8 +1332,13 @@ void gdriveDownloadFile(const std::string& itemId, const std::function(buffer), bytesToWrite); }, nullptr /*readRequest*/); if (httpResult.statusCode != 200) - throw SysError(formatGoogleErrorRaw(response)); + throw SysError(formatGdriveErrorRaw(response)); if (!startsWith(uploadUrl, "https://www.googleapis.com/")) throw SysError(L"Invalid upload URL: " + utfTo(uploadUrl)); //user should never see this @@ -1309,7 +1531,7 @@ std::string /*itemId*/ gdriveUploadFile(const Zstring& fileName, const std::stri //returns "bytesToRead" bytes unless end of stream! => fits into "0 signals EOF: Posix read() semantics" std::string response; - googleHttpsRequest(uploadUrlRelative, { "Content-Encoding: gzip" }, {} /*extraOptions*/, //throw SysError, X + gdriveHttpsRequest(uploadUrlRelative, { "Content-Encoding: gzip" }, {} /*extraOptions*/, //throw SysError, X [&](const void* buffer, size_t bytesToWrite) { response.append(static_cast(buffer), bytesToWrite); }, readBlockAsGzip); JsonValue jresponse; @@ -1318,18 +1540,23 @@ std::string /*itemId*/ gdriveUploadFile(const Zstring& fileName, const std::stri const std::optional itemId = getPrimitiveFromJsonObject(jresponse, "id"); if (!itemId) - throw SysError(formatGoogleErrorRaw(response)); + throw SysError(formatGdriveErrorRaw(response)); return *itemId; } //instead of the "root" alias Google uses an actual ID in file metadata -std::string /*itemId*/ getRootItemId(const std::string& accessToken) //throw SysError +std::string /*itemId*/ getMyDriveId(const std::string& accessToken) //throw SysError { //https://developers.google.com/drive/api/v3/reference/files/get + const std::string& queryParams = xWwwFormUrlEncode( + { + { "supportsAllDrives", "true" }, + { "fields", "id" }, + }); std::string response; - googleHttpsRequest("/drive/v3/files/root?fields=id", { "Authorization: Bearer " + accessToken }, {} /*extraOptions*/, //throw SysError + gdriveHttpsRequest("/drive/v3/files/root?" + queryParams, { "Authorization: Bearer " + accessToken }, {} /*extraOptions*/, //throw SysError [&](const void* buffer, size_t bytesToWrite) { response.append(static_cast(buffer), bytesToWrite); }, nullptr /*readRequest*/); JsonValue jresponse; @@ -1338,24 +1565,24 @@ std::string /*itemId*/ getRootItemId(const std::string& accessToken) //throw Sys const std::optional itemId = getPrimitiveFromJsonObject(jresponse, "id"); if (!itemId) - throw SysError(formatGoogleErrorRaw(response)); + throw SysError(formatGdriveErrorRaw(response)); return *itemId; } -class GoogleAccessBuffer //per-user-session! => serialize access (perf: amortized fully buffered!) +class GdriveAccessBuffer //per-user-session! => serialize access (perf: amortized fully buffered!) { public: - GoogleAccessBuffer(const GoogleAccessInfo& accessInfo) : accessInfo_(accessInfo) {} + GdriveAccessBuffer(const GdriveAccessInfo& accessInfo) : accessInfo_(accessInfo) {} - GoogleAccessBuffer(MemoryStreamIn& stream) //throw UnexpectedEndOfStreamError + GdriveAccessBuffer(MemoryStreamIn& stream) //throw UnexpectedEndOfStreamError { accessInfo_.accessToken.validUntil = readNumber(stream); // accessInfo_.accessToken.value = readContainer(stream); // accessInfo_.refreshToken = readContainer(stream); //UnexpectedEndOfStreamError accessInfo_.userInfo.displayName = utfTo(readContainer(stream)); // - accessInfo_.userInfo.email = utfTo< Zstring>(readContainer(stream)); // + accessInfo_.userInfo.email = readContainer(stream); // } void serialize(MemoryStreamOut& stream) const @@ -1365,22 +1592,22 @@ public: writeContainer(stream, accessInfo_.accessToken.value); writeContainer(stream, accessInfo_.refreshToken); writeContainer(stream, utfTo(accessInfo_.userInfo.displayName)); - writeContainer(stream, utfTo(accessInfo_.userInfo.email)); + writeContainer(stream, accessInfo_.userInfo.email); } std::string getAccessToken() //throw SysError { if (accessInfo_.accessToken.validUntil <= std::time(nullptr) + std::chrono::seconds(HTTP_SESSION_ACCESS_TIME_OUT).count() + 5 /*some leeway*/) //expired/will expire - accessInfo_.accessToken = refreshAccessToGoogleDrive(accessInfo_.refreshToken); //throw SysError + accessInfo_.accessToken = gdriveRefreshAccess(accessInfo_.refreshToken); //throw SysError assert(accessInfo_.accessToken.validUntil > std::time(nullptr) + std::chrono::seconds(HTTP_SESSION_ACCESS_TIME_OUT).count()); return accessInfo_.accessToken.value; } //const std::wstring& getUserDisplayName() const { return accessInfo_.userInfo.displayName; } - const Zstring& getUserEmail() const { return accessInfo_.userInfo.email; } + const std::string& getUserEmail() const { return accessInfo_.userInfo.email; } - void update(const GoogleAccessInfo& accessInfo) + void update(const GdriveAccessInfo& accessInfo) { if (!equalAsciiNoCase(accessInfo.userInfo.email, accessInfo_.userInfo.email)) throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo(__LINE__)); @@ -1388,67 +1615,84 @@ public: } private: - GoogleAccessBuffer (const GoogleAccessBuffer&) = delete; - GoogleAccessBuffer& operator=(const GoogleAccessBuffer&) = delete; + GdriveAccessBuffer (const GdriveAccessBuffer&) = delete; + GdriveAccessBuffer& operator=(const GdriveAccessBuffer&) = delete; - GoogleAccessInfo accessInfo_; + GdriveAccessInfo accessInfo_; }; -class GooglePersistentSessions; +class GdrivePersistentSessions; - class GoogleFileState //per-user-session! => serialize access (perf: amortized fully buffered!) + class GdriveFileState //per-user-session! => serialize access (perf: amortized fully buffered!) { public: - GoogleFileState(GoogleAccessBuffer& accessBuf) : //throw SysError - lastSyncToken_(getChangesCurrentToken(accessBuf.getAccessToken())), // - rootId_ (getRootItemId (accessBuf.getAccessToken())), //throw SysError - accessBuf_(accessBuf) {} // + GdriveFileState(GdriveAccessBuffer& accessBuf) : //throw SysError + /* issue getChangesCurrentToken() as the very first Google Drive query!*/ + lastSyncToken_(getChangesCurrentToken(accessBuf.getAccessToken())), //throw SysError + myDriveId_ (getMyDriveId (accessBuf.getAccessToken())), // + accessBuf_(accessBuf) + { + for (const DriveDetails& drive : getSharedDrives(accessBuf.getAccessToken())) //throw SysError + sharedDrives_.emplace(drive.driveId, drive.driveName); + } - GoogleFileState(MemoryStreamIn& stream, GoogleAccessBuffer& accessBuf, int dbVersion) : accessBuf_(accessBuf) //throw UnexpectedEndOfStreamError + GdriveFileState(MemoryStreamIn& stream, GdriveAccessBuffer& accessBuf) : //throw UnexpectedEndOfStreamError + accessBuf_(accessBuf) { lastSyncToken_ = readContainer(stream); //UnexpectedEndOfStreamError - rootId_ = readContainer(stream); // + myDriveId_ = readContainer(stream); // - //TODO: remove migration code at some time! 2019-12-05 - if (dbVersion == 1) - ; //fully discard old state due to missing "isShared" attribute :( - else + size_t sharedDrivesCount = readNumber(stream); //UnexpectedEndOfStreamError + while (sharedDrivesCount-- != 0) { - for (;;) - { - const std::string folderId = readContainer(stream); //UnexpectedEndOfStreamError - if (folderId.empty()) - break; - folderContents_[folderId].isKnownFolder = true; - } - - size_t itemCount = readNumber(stream); - while (itemCount-- != 0) - { - const std::string itemId = readContainer(stream); //UnexpectedEndOfStreamError - - GoogleItemDetails details = {}; - details.itemName = readContainer(stream); // - details.isFolder = readNumber (stream) != 0; // - details.isShared = readNumber (stream) != 0; //UnexpectedEndOfStreamError - details.fileSize = readNumber (stream); // - details.modTime = readNumber (stream); // + std::string driveId = readContainer(stream); //UnexpectedEndOfStreamError + std::string driveName = readContainer(stream); // + sharedDrives_.emplace(driveId, utfTo(driveName)); + } - size_t parentsCount = readNumber(stream); //UnexpectedEndOfStreamError - while (parentsCount-- != 0) - details.parentIds.push_back(readContainer(stream)); //UnexpectedEndOfStreamError + for (;;) + { + const std::string folderId = readContainer(stream); //UnexpectedEndOfStreamError + if (folderId.empty()) + break; + folderContents_[folderId].isKnownFolder = true; + } - updateItemState(itemId, std::move(details)); - } + for (;;) + { + const std::string itemId = readContainer(stream); //UnexpectedEndOfStreamError + if (itemId.empty()) + break; + + GdriveItemDetails details = {}; + details.itemName = utfTo(readContainer(stream)); // + details.type = readNumber(stream); // + details.owner = readNumber (stream); // + details.fileSize = readNumber (stream); //UnexpectedEndOfStreamError + details.modTime = readNumber (stream); // + details.targetId = readContainer(stream); // + + size_t parentsCount = readNumber(stream); //UnexpectedEndOfStreamError + while (parentsCount-- != 0) + details.parentIds.push_back(readContainer(stream)); //UnexpectedEndOfStreamError + + updateItemState(itemId, &details); } } void serialize(MemoryStreamOut& stream) const { writeContainer(stream, lastSyncToken_); - writeContainer(stream, rootId_); + writeContainer(stream, myDriveId_); + + writeNumber(stream, static_cast(sharedDrives_.size())); + for (const auto& [driveId, driveName]: sharedDrives_) + { + writeContainer(stream, driveId); + writeContainer(stream, utfTo(driveName)); + } for (const auto& [folderId, content] : folderContents_) if (folderId.empty()) @@ -1457,65 +1701,150 @@ class GooglePersistentSessions; writeContainer(stream, folderId); writeContainer(stream, std::string()); //sentinel - writeNumber(stream, static_cast(itemDetails_.size())); - for (const auto& [itemId, details] : itemDetails_) + auto serializeItem = [&](const std::string& itemId, const GdriveItemDetails& details) { - writeContainer(stream, itemId); - writeContainer(stream, details.itemName); - writeNumber< int8_t>(stream, details.isFolder); - writeNumber< int8_t>(stream, details.isShared); - writeNumber(stream, details.fileSize); - writeNumber< int64_t>(stream, details.modTime); + 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())); + 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("Contract violation! " + std::string(__FILE__) + ':' + numberTo(__LINE__)); + serializeItem(itemId, details); + + if (details.type == GdriveItemType::shortcut) + { + if (details.targetId.empty()) + throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo(__LINE__)); + + if (auto it = itemDetails_.find(details.targetId); + it != itemDetails_.end()) + serializeItem(details.targetId, it->second); + } + } + writeContainer(stream, std::string()); //sentinel + } + + std::vector listSharedDrives() const + { + std::vector sharedDriveNames; + + for (const auto& [driveId, driveName]: sharedDrives_) + sharedDriveNames.push_back(driveName); + + return sharedDriveNames; } struct PathStatus { std::string existingItemId; - bool existingIsFolder = false; - AfsPath existingPath; //input path =: existingPath + relPath + GdriveItemType existingType = GdriveItemType::file; + AfsPath existingPath; //input path =: existingPath + relPath std::vector relPath; // }; - PathStatus getPathStatus(const AfsPath& afsPath) //throw SysError + PathStatus getPathStatus(const Zstring& sharedDriveName, const AfsPath& afsPath, bool followLeafShortcut) //throw SysError { + const std::string driveId = [&] + { + if (sharedDriveName.empty()) + return myDriveId_; + + auto itFound = sharedDrives_.cend(); + for (auto it = sharedDrives_.begin(); it != sharedDrives_.end(); ++it) + if (const auto& [sharedDriveId, driveName] = *it; + equalNativePath(driveName, sharedDriveName)) + { + if (itFound != sharedDrives_.end()) + throw SysError(replaceCpy(_("Cannot find %x."), L"%x", + fmtPath(getGdriveDisplayPath({{ accessBuf_.getUserEmail(), sharedDriveName}, AfsPath() }))) + L' ' + + replaceCpy(_("The name %x is used by more than one item in the folder."), L"%x", fmtPath(sharedDriveName))); + itFound = it; + } + if (itFound == sharedDrives_.end()) + throw SysError(replaceCpy(_("Cannot find %x."), L"%x", + fmtPath(getGdriveDisplayPath({{ accessBuf_.getUserEmail(), sharedDriveName}, AfsPath() })))); + + return itFound->first; + }(); + const std::vector relPath = split(afsPath.value, FILE_NAME_SEPARATOR, SplitType::SKIP_EMPTY); if (relPath.empty()) - return { rootId_, true /*existingIsFolder*/, AfsPath(), {} }; - - return getPathStatusSub(rootId_, AfsPath(), relPath); //throw SysError + return { driveId, GdriveItemType::folder, AfsPath(), {} }; + else + return getPathStatusSub(driveId, sharedDriveName, AfsPath(), relPath, followLeafShortcut); //throw SysError } - std::string /*itemId*/ getItemId(const AfsPath& afsPath) //throw SysError + std::string /*itemId*/ getItemId(const Zstring& sharedDriveName, const AfsPath& afsPath, bool followLeafShortcut) //throw SysError { - const GoogleFileState::PathStatus ps = getPathStatus(afsPath); //throw SysError + const GdriveFileState::PathStatus& ps = getPathStatus(sharedDriveName, afsPath, followLeafShortcut); //throw SysError if (ps.relPath.empty()) return ps.existingItemId; const AfsPath afsPathMissingChild(nativeAppendPaths(ps.existingPath.value, ps.relPath.front())); - throw SysError(replaceCpy(_("Cannot find %x."), L"%x", fmtPath(getGoogleDisplayPath({ accessBuf_.getUserEmail(), afsPathMissingChild })))); + throw SysError(replaceCpy(_("Cannot find %x."), L"%x", fmtPath(getGdriveDisplayPath({ { accessBuf_.getUserEmail(), sharedDriveName }, afsPathMissingChild })))); } - std::pair getFileAttributes(const AfsPath& afsPath) //throw SysError + std::pair getFileAttributes(const Zstring& sharedDriveName, const AfsPath& afsPath, bool followLeafShortcut) //throw SysError { - const std::string fileId = getItemId(afsPath); //throw SysError - auto it = itemDetails_.find(fileId); - if (it == itemDetails_.end()) - throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo(__LINE__)); - return *it; + const std::string itemId = getItemId(sharedDriveName, afsPath, followLeafShortcut); //throw SysError + + if (afsPath.value.empty()) //root drives obviously not covered by itemDetails_ + { + GdriveItemDetails rootDetails = {}; + rootDetails.type = GdriveItemType::folder; + if (itemId == myDriveId_) + { + rootDetails.itemName = Zstr("My Drive"); + rootDetails.owner = FileOwner::me; + return { itemId, std::move(rootDetails) }; + } + + if (auto it = sharedDrives_.find(itemId); + it != sharedDrives_.end()) + { + rootDetails.itemName = it->second; + rootDetails.owner = FileOwner::none; + return { itemId, std::move(rootDetails) }; + } + } + else if (auto it = itemDetails_.find(itemId); + it != itemDetails_.end()) + return *it; + + //itemId was found! => must either be a (shared) drive root or buffered in itemDetails_ + throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo(__LINE__)); + } + + 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 + 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; + std::vector childItems; for (auto itChild : it->second.childItems) { const auto& [childId, childDetails] = *itChild; @@ -1527,59 +1856,82 @@ class GooglePersistentSessions; //-------------- notifications -------------- using ItemIdDelta = std::unordered_set; - struct FileStateDelta //as long as instance exists, GoogleFileItem will log all changed items + struct FileStateDelta //as long as instance exists, GdriveItem will log all changed items { FileStateDelta() {} private: FileStateDelta(const std::shared_ptr& cids) : changedIds(cids) {} - friend class GoogleFileState; - std::shared_ptr changedIds; //lifetime is managed by caller; access *only* by GoogleFileState! + 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) + void notifyFolderContent(const FileStateDelta& stateDelta, const std::string& folderId, const std::vector& childItems) { folderContents_[folderId].isKnownFolder = true; - for (const GoogleFileItem& item : childItems) - notifyItemUpdate(stateDelta, item.itemId, item.details); + 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!? => notifyItemUpdate() will sort it out! + //- 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 notifyItemCreated(const FileStateDelta& stateDelta, const GoogleFileItem& item) + void notifyItemUpdated(const FileStateDelta& stateDelta, const GdriveItem& item) { - notifyItemUpdate(stateDelta, item.itemId, item.details); + notifyItemUpdated(stateDelta, item.itemId, &item.details); } void notifyFolderCreated(const FileStateDelta& stateDelta, const std::string& folderId, const Zstring& folderName, const std::string& parentId) { - GoogleItemDetails details = {}; - details.itemName = utfTo(folderName); - details.isFolder = true; - details.isShared = false; - details.modTime = std::time(nullptr); + GdriveItemDetails details = {}; + details.itemName = folderName; + details.type = GdriveItemType::folder; + details.owner = FileOwner::me; + details.modTime = std::time(nullptr); details.parentIds.push_back(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; - notifyItemUpdate(stateDelta, folderId, details); + 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 = {}; + details.itemName = shortcutName; + details.type = GdriveItemType::shortcut; + details.owner = FileOwner::me; + details.modTime = std::time(nullptr); + details.targetId = targetId; + details.parentIds.push_back(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) { - notifyItemUpdate(stateDelta, itemId, std::nullopt); + 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()) { - GoogleItemDetails detailsNew = it->second; + GdriveItemDetails detailsNew = it->second; std::erase_if(detailsNew.parentIds, [&](const std::string& id) { return id == parentIdOld; }); - notifyItemUpdate(stateDelta, itemId, detailsNew); + notifyItemUpdated(stateDelta, itemId, &detailsNew); } else //conflict!!! markSyncDue(); @@ -1589,25 +1941,25 @@ class GooglePersistentSessions; { if (auto it = itemDetails_.find(itemId); it != itemDetails_.end()) { - GoogleItemDetails detailsNew = it->second; - detailsNew.itemName = utfTo(newName); + 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 - notifyItemUpdate(stateDelta, itemId, detailsNew); + notifyItemUpdated(stateDelta, itemId, &detailsNew); } else //conflict!!! markSyncDue(); } private: - GoogleFileState (const GoogleFileState&) = delete; - GoogleFileState& operator=(const GoogleFileState&) = delete; + GdriveFileState (const GdriveFileState&) = delete; + GdriveFileState& operator=(const GdriveFileState&) = delete; - friend class GooglePersistentSessions; + friend class GdrivePersistentSessions; - void notifyItemUpdate(const FileStateDelta& stateDelta, const std::string& itemId, const std::optional& details) + void notifyItemUpdated(const FileStateDelta& stateDelta, const std::string& itemId, const GdriveItemDetails* details) { if (!contains(*stateDelta.changedIds, itemId)) //no conflicting changes in the meantime? updateItemState(itemId, details); //=> accept new state data @@ -1629,17 +1981,19 @@ class GooglePersistentSessions; return FileStateDelta(deltaPtr); } - bool syncIsDue() const { return std::chrono::steady_clock::now() >= lastSyncTime_ + GOOGLE_DRIVE_SYNC_INTERVAL; } - - void markSyncDue() { lastSyncTime_ = std::chrono::steady_clock::now() - GOOGLE_DRIVE_SYNC_INTERVAL; } + 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(lastSyncToken_, accessBuf_.getAccessToken()); //throw SysError - for (const ChangeItem& item : delta.changes) - updateItemState(item.itemId, item.details); + for (const FileChange& change : delta.fileChanges) + updateItemState(change.itemId, get(change.details)); + + for (const DriveChange& change : delta.driveChanges) + updateSharedDriveState(change.driveId, change.driveName); lastSyncToken_ = delta.newStartPageToken; lastSyncTime_ = std::chrono::steady_clock::now(); @@ -1648,52 +2002,92 @@ class GooglePersistentSessions; //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) //throw SysError + PathStatus getPathStatusSub(const std::string& folderId, const Zstring& sharedDriveName, const AfsPath& folderPath, const std::vector& relPath, bool followLeafShortcut) //throw SysError { assert(!relPath.empty()); - std::vector* childItems = nullptr; auto itKnown = folderContents_.find(folderId); - if (itKnown != folderContents_.end() && itKnown->second.isKnownFolder) - childItems = &(itKnown->second.childItems); - else + if (itKnown == folderContents_.end() || !itKnown->second.isKnownFolder) { notifyFolderContent(registerFileStateDelta(), folderId, readFolderContent(folderId, accessBuf_.getAccessToken())); //throw SysError - - if (!folderContents_[folderId].isKnownFolder) + //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("Contract violation! " + std::string(__FILE__) + ':' + numberTo(__LINE__)); - childItems = &folderContents_[folderId].childItems; } auto itFound = itemDetails_.cend(); - for (const DetailsIterator& itDetails : *childItems) + 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(utfTo(itDetails->second.itemName), relPath.front())) + if (equalNativePath(itChild->second.itemName, relPath.front())) { if (itFound != itemDetails_.end()) throw SysError(replaceCpy(_("Cannot find %x."), L"%x", - fmtPath(getGoogleDisplayPath({ accessBuf_.getUserEmail(), AfsPath(nativeAppendPaths(folderPath.value, relPath.front())) }))) + L' ' + - replaceCpy(_("The name %x is used by more than one item in the folder."), L"%x", fmtPath(relPath.front()))); + fmtPath(getGdriveDisplayPath({{ accessBuf_.getUserEmail(), sharedDriveName}, AfsPath(nativeAppendPaths(folderPath.value, relPath.front())) }))) + L' ' + + replaceCpy(_("The name %x is used by more than one item in the folder."), L"%x", fmtPath(relPath.front()))); - itFound = itDetails; + itFound = itChild; } if (itFound == itemDetails_.end()) - return { folderId, true /*existingIsFolder*/, folderPath, relPath }; //always a folder, see check before recursion above + 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(nativeAppendPaths(folderPath.value, relPath.front())); const std::vector childRelPath(relPath.begin() + 1, relPath.end()); - if (childRelPath.empty() || !childDetails.isFolder /*obscure, but possible (and not an error)*/) - return { childId, childDetails.isFolder, childItemPath, childRelPath }; + 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 (and not an error) + return { childId, childDetails.type, childItemPath, childRelPath }; + + case GdriveItemType::folder: + return getPathStatusSub(childId, sharedDriveName, childItemPath, childRelPath, followLeafShortcut); //throw SysError - return getPathStatusSub(childId, childItemPath, childRelPath); //throw SysError + case GdriveItemType::shortcut: + switch (getItemDetailsBuffered(childDetails.targetId).type) + { + case GdriveItemType::file: //parent/file-symlink/child-rel-path... => obscure, but possible (and not an error) + return { childDetails.targetId, GdriveItemType::file, childItemPath, childRelPath }; //resolve symlinks if in the *middle* of a path! + + case GdriveItemType::folder: //parent/folder-symlink/child-rel-path... => always follow + return getPathStatusSub(childDetails.targetId, sharedDriveName, childItemPath, childRelPath, followLeafShortcut); //throw SysError + + case GdriveItemType::shortcut: //should never happen: creating shortcuts to shortcuts fails with "Internal Error" + throw SysError(replaceCpy(_("Cannot resolve symbolic link %x."), L"%x", + fmtPath(getGdriveDisplayPath({{ accessBuf_.getUserEmail(), sharedDriveName}, AfsPath(nativeAppendPaths(folderPath.value, relPath.front())) }))) + L' ' + + L"Google Drive Shortcut points to another Shortcut."); + } + break; + } + throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo(__LINE__)); } } - void updateItemState(const std::string& itemId, const std::optional& details) + void updateItemState(const std::string& itemId, const GdriveItemDetails* details) { auto it = itemDetails_.find(itemId); if (!details == (it == itemDetails_.end())) @@ -1717,7 +2111,7 @@ class GooglePersistentSessions; { if (it != itemDetails_.end()) //update { - if (it->second.isFolder != details->isFolder) + if (it->second.type != details->type) throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo(__LINE__)); //WTF!? std::vector parentIdsNew = details->parentIds; @@ -1732,6 +2126,7 @@ class GooglePersistentSessions; if (auto itP = folderContents_.find(parentId); itP != folderContents_.end()) std::erase_if(itP->second.childItems, [&](auto itChild) { return itChild == 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; } @@ -1754,7 +2149,8 @@ class GooglePersistentSessions; itemDetails_.erase(it); } - if (auto itP = folderContents_.find(itemId); itP != folderContents_.end()) + if (auto itP = folderContents_.find(itemId); + itP != folderContents_.end()) { for (auto itChild : itP->second.childItems) //2. delete as parent from child items (don't wait for change notifications of children) std::erase_if(itChild->second.parentIds, [&](const std::string& id) { return id == itemId; }); @@ -1763,7 +2159,26 @@ class GooglePersistentSessions; } } - using DetailsIterator = std::unordered_map::iterator; + void updateSharedDriveState(const std::string& driveId, const Zstring& driveName /*empty if shared drive was deleted*/) + { + if (!driveName.empty()) + sharedDrives_[driveId] = driveName; + else //delete + { + sharedDrives_.erase(driveId); + + //when a shared drive is deleted, we also receive change notifications for the contained files: nice! + if (auto itP = folderContents_.find(driveId); + itP != folderContents_.end()) + { + for (auto itChild : itP->second.childItems) //delete as parent from child items (don't wait for change notifications of children) + std::erase_if(itChild->second.parentIds, [&](const std::string& id) { return id == driveId; }); + folderContents_.erase(itP); + } + } + } + + using DetailsIterator = std::unordered_map::iterator; struct FolderContent { @@ -1771,31 +2186,32 @@ class GooglePersistentSessions; std::vector childItems; }; std::unordered_map folderContents_; - std::unordered_map itemDetails_; //contains ALL known, existing items! + std::unordered_map itemDetails_; //contains ALL known, existing items! std::string lastSyncToken_; //marker corresponding to last sync with Google's change notifications - std::chrono::steady_clock::time_point lastSyncTime_ = std::chrono::steady_clock::now() - GOOGLE_DRIVE_SYNC_INTERVAL; //... with Google Drive (default: sync is due) + 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 rootId_; - GoogleAccessBuffer& accessBuf_; + std::string myDriveId_; + std::unordered_map sharedDrives_; + GdriveAccessBuffer& accessBuf_; }; //========================================================================================== //========================================================================================== -class GooglePersistentSessions +class GdrivePersistentSessions { public: - GooglePersistentSessions(const Zstring& configDirPath) : configDirPath_(configDirPath) {} + GdrivePersistentSessions(const Zstring& configDirPath) : configDirPath_(configDirPath) {} void saveActiveSessions() //throw FileError { std::vector*> protectedSessions; //pointers remain stable, thanks to std::map<> globalSessions_.access([&](GlobalSessions& sessions) { - for (auto& [googleUserEmail, protectedSession] : sessions) + for (auto& [accountEmail, protectedSession] : sessions) protectedSessions.push_back(&protectedSession); }); @@ -1833,9 +2249,9 @@ public: } } - Zstring addUserSession(const Zstring& googleLoginHint, const std::function& updateGui /*throw X*/) //throw SysError, X + std::string addUserSession(const std::string& gdriveLoginHint, const std::function& updateGui /*throw X*/) //throw SysError, X { - const GoogleAccessInfo accessInfo = authorizeAccessToGoogleDrive(googleLoginHint, updateGui); //throw SysError, X + const GdriveAccessInfo accessInfo = gdriveAuthorizeAccess(gdriveLoginHint, updateGui); //throw SysError, X accessUserSession(accessInfo.userInfo.email, [&](std::optional& userSession) //throw SysError { @@ -1843,8 +2259,8 @@ public: userSession->accessBuf.ref().update(accessInfo); //redundant? else { - auto accessBuf = makeSharedRef(accessInfo); - auto fileState = makeSharedRef(accessBuf.ref()); //throw SysError + auto accessBuf = makeSharedRef(accessInfo); + auto fileState = makeSharedRef(accessBuf.ref()); //throw SysError userSession = { accessBuf, fileState }; } }); @@ -1852,23 +2268,23 @@ public: return accessInfo.userInfo.email; } - void removeUserSession(const Zstring& googleUserEmail) //throw SysError + void removeUserSession(const std::string& accountEmail) //throw SysError { try { - accessUserSession(googleUserEmail, [&](std::optional& userSession) //throw SysError + accessUserSession(accountEmail, [&](std::optional& userSession) //throw SysError { if (userSession) - revokeAccessToGoogleDrive(userSession->accessBuf.ref().getAccessToken(), googleUserEmail); //throw SysError + gdriveRevokeAccess(userSession->accessBuf.ref().getAccessToken()); //throw SysError }); } - catch (SysError&) { assert(false); } //best effort: try to invalidate the access token + catch (SysError&) { assert(false); } //best effort: try to invalidate the access token //=> expected to fail if offline => not worse than removing FFS via "Uninstall Programs" try { //start with deleting the DB file (1. maybe it's corrupted? 2. skip unnecessary lazy-load) - const Zstring dbFilePath = getDbFilePath(googleUserEmail); + const Zstring dbFilePath = getDbFilePath(accountEmail); try { removeFilePlain(dbFilePath); //throw FileError @@ -1882,20 +2298,20 @@ public: catch (const FileError& e) { throw SysError(e.toString()); } //file access errors should be further enriched by context info => SysError - accessUserSession(googleUserEmail, [&](std::optional& userSession) //throw SysError + accessUserSession(accountEmail, [&](std::optional& userSession) //throw SysError { userSession.reset(); }); } - std::vector /*Google user email*/ listUserSessions() //throw SysError + std::vector listAccounts() //throw SysError { - std::vector emails; + std::vector emails; std::vector*> protectedSessions; //pointers remain stable, thanks to std::map<> globalSessions_.access([&](GlobalSessions& sessions) { - for (auto& [googleUserEmail, protectedSession] : sessions) + for (auto& [accountEmail, protectedSession] : sessions) protectedSessions.push_back(&protectedSession); }); @@ -1909,7 +2325,7 @@ public: //also include available, but not-yet-loaded sessions traverseFolder(configDirPath_, - [&](const FileInfo& fi) { if (endsWith(fi.itemName, Zstr(".db"))) emails.push_back(beforeLast(fi.itemName, Zstr('.'), IF_MISSING_RETURN_NONE)); }, + [&](const FileInfo& fi) { if (endsWith(fi.itemName, Zstr(".db"))) emails.push_back(utfTo(beforeLast(fi.itemName, Zstr('.'), IF_MISSING_RETURN_NONE))); }, [&](const FolderInfo& fi) {}, [&](const SymlinkInfo& si) {}, [&](const std::wstring& errorMsg) @@ -1929,20 +2345,20 @@ public: struct AsyncAccessInfo { std::string accessToken; //don't allow (long-running) web requests while holding the global session lock! - GoogleFileState::FileStateDelta stateDelta; + GdriveFileState::FileStateDelta stateDelta; }; //perf: amortized fully buffered! - AsyncAccessInfo accessGlobalFileState(const Zstring& googleUserEmail, const std::function& useFileState /*throw X*/) //throw SysError, X + AsyncAccessInfo accessGlobalFileState(const std::string& accountEmail, const std::function& useFileState /*throw X*/) //throw SysError, X { std::string accessToken; - GoogleFileState::FileStateDelta stateDelta; + GdriveFileState::FileStateDelta stateDelta; - accessUserSession(googleUserEmail, [&](std::optional& userSession) //throw SysError + accessUserSession(accountEmail, [&](std::optional& userSession) //throw SysError { if (!userSession) - throw SysError(replaceCpy(_("Please authorize access to user account %x."), L"%x", fmtPath(googleUserEmail))); + throw SysError(replaceCpy(_("Please authorize access to user account %x."), L"%x", utfTo(accountEmail))); - //manage last sync time here rather than in GoogleFileState, so that "lastSyncToken" remains stable while accessing GoogleFileState in the callback + //manage last sync time here rather than in GdriveFileState, so that "lastSyncToken" remains stable while accessing GdriveFileState in the callback if (userSession->fileState.ref().syncIsDue()) userSession->fileState.ref().syncWithGoogle(); //throw SysError @@ -1955,43 +2371,31 @@ public: } private: - GooglePersistentSessions (const GooglePersistentSessions&) = delete; - GooglePersistentSessions& operator=(const GooglePersistentSessions&) = delete; + GdrivePersistentSessions (const GdrivePersistentSessions&) = delete; + GdrivePersistentSessions& operator=(const GdrivePersistentSessions&) = delete; struct UserSession; - Zstring getDbFilePath(Zstring googleUserEmail) const + Zstring getDbFilePath(std::string accountEmail) const { - for (Zchar& c : googleUserEmail) + for (char& c : accountEmail) c = asciiToLower(c); - //return appendSeparator(configDirPath_) + utfTo(formatAsHexString(getMd5(utfTo(googleUserEmail)))) + Zstr(".db"); - return appendSeparator(configDirPath_) + googleUserEmail + Zstr(".db"); + //return appendSeparator(configDirPath_) + utfTo(formatAsHexString(getMd5(utfTo(accountEmail)))) + Zstr(".db"); + return appendSeparator(configDirPath_) + utfTo(accountEmail) + Zstr(".db"); } - void accessUserSession(const Zstring& googleUserEmail, const std::function& userSession)>& useSession) //throw SysError + void accessUserSession(const std::string& accountEmail, const std::function& userSession)>& useSession /*throw X*/) //throw SysError, X { Protected* protectedSession = nullptr; //pointers remain stable, thanks to std::map<> - globalSessions_.access([&](GlobalSessions& sessions) { protectedSession = &sessions[googleUserEmail]; }); + globalSessions_.access([&](GlobalSessions& sessions) { protectedSession = &sessions[accountEmail]; }); - try + protectedSession->access([&](SessionHolder& holder) { - 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(googleUserEmail)); //throw FileError - } - catch (FileError&) - { - if (itemStillExists(getDbFilePath(googleUserEmail))) //throw FileError - throw; - } - holder.dbWasLoaded = true; - useSession(holder.session); - }); - } - catch (const FileError& e) { throw SysError(e.toString()); } //GooglePersistentSessions errors should be further enriched by context info => SysError + if (!holder.dbWasLoaded) //let's NOT load the DB files under the globalSessions_ lock, but the session-specific one! + holder.session = loadSession(getDbFilePath(accountEmail)); //throw SysError + holder.dbWasLoaded = true; + useSession(holder.session); //throw X + }); } static void saveSession(const Zstring& dbFilePath, const UserSession& userSession) //throw FileError @@ -2014,45 +2418,65 @@ private: saveBinContainer(dbFilePath, zstreamOut, nullptr /*notifyUnbufferedIO*/); //throw FileError } - static UserSession loadSession(const Zstring& dbFilePath) //throw FileError + static std::optional loadSession(const Zstring& dbFilePath) //throw SysError { - const std::string zstream = loadBinContainer(dbFilePath, nullptr /*notifyUnbufferedIO*/); //throw FileError + std::string zstream; + try + { + zstream = loadBinContainer(dbFilePath, nullptr /*notifyUnbufferedIO*/); //throw FileError + } + catch (FileError&) + { + try + { + if (itemStillExists(dbFilePath)) //throw FileError + throw; + } + catch (const FileError& e) { throw SysError(e.toString()); } //GdrivePersistentSessions errors should be further enriched with context info => SysError + + return std::nullopt; + } + std::string rawStream; try { rawStream = decompress(zstream); //throw SysError } - catch (const SysError& e) { throw FileError(_("Database file is corrupted:") + L' ' + fmtPath(dbFilePath), e.toString()); } + catch (const SysError& e) { throw SysError(_("Database file is corrupted:") + L' ' + fmtPath(dbFilePath) + L'\n' + e.toString()); } - MemoryStreamIn streamIn(rawStream); try { + MemoryStreamIn streamIn(rawStream); //-------- file format header -------- char tmp[sizeof(DB_FILE_DESCR)] = {}; readArray(streamIn, &tmp, sizeof(tmp)); //throw UnexpectedEndOfStreamError if (!std::equal(std::begin(tmp), std::end(tmp), std::begin(DB_FILE_DESCR))) - throw FileError(_("Database file is corrupted:") + L' ' + fmtPath(dbFilePath), L"Invalid header."); + throw SysError(_("Database file is corrupted:") + L' ' + fmtPath(dbFilePath) + L'\n' + L"Invalid header."); const int version = readNumber(streamIn); - - //TODO: remove migration code at some time! 2019-12-05 - if (version != 1 && + 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 != DB_FILE_VERSION) - throw FileError(replaceCpy(_("Database file %x is incompatible."), L"%x", fmtPath(dbFilePath)), - replaceCpy(_("Version: %x"), L"%x", numberTo(version))); + throw SysError(replaceCpy(_("Database file %x is incompatible."), L"%x", fmtPath(dbFilePath)) + L'\n' + + replaceCpy(_("Version: %x"), L"%x", numberTo(version))); + + auto accessBuf = makeSharedRef(streamIn); //throw UnexpectedEndOfStreamError + auto fileState = + //TODO: remove migration code at some time! 2020-06-11 + version <= 2 ? //fully discard old state due to missing "ownedByMe" attribute + shortcut support + makeSharedRef(accessBuf.ref()) : //throw SysError + makeSharedRef(streamIn, accessBuf.ref()); //throw UnexpectedEndOfStreamError - auto accessBuf = makeSharedRef(streamIn); //throw UnexpectedEndOfStreamError - auto fileState = makeSharedRef(streamIn, accessBuf.ref(), version); //throw UnexpectedEndOfStreamError - return { accessBuf, fileState }; + return UserSession{ accessBuf, fileState }; } - catch (UnexpectedEndOfStreamError&) { throw FileError(_("Database file is corrupted:") + L' ' + fmtPath(dbFilePath), L"Unexpected end of stream."); } + catch (UnexpectedEndOfStreamError&) { throw SysError(_("Database file is corrupted:") + L' ' + fmtPath(dbFilePath) + L'\n' + L"Unexpected end of stream."); } } struct UserSession { - SharedRef accessBuf; - SharedRef fileState; + SharedRef accessBuf; + SharedRef fileState; }; struct SessionHolder @@ -2060,19 +2484,19 @@ private: bool dbWasLoaded = false; std::optional session; }; - using GlobalSessions = std::map, LessAsciiNoCase>; + using GlobalSessions = std::map, LessAsciiNoCase>; Protected globalSessions_; const Zstring configDirPath_; }; //========================================================================================== -Global globalGoogleSessions; +Global globalGdriveSessions; //========================================================================================== -GooglePersistentSessions::AsyncAccessInfo accessGlobalFileState(const Zstring& googleUserEmail, const std::function& useFileState /*throw X*/) //throw SysError, X +GdrivePersistentSessions::AsyncAccessInfo accessGlobalFileState(const std::string& accountEmail, const std::function& useFileState /*throw X*/) //throw SysError, X { - if (const std::shared_ptr gps = globalGoogleSessions.get()) - return gps->accessGlobalFileState(googleUserEmail, useFileState); //throw SysError, X + if (const std::shared_ptr gps = globalGdriveSessions.get()) + return gps->accessGlobalFileState(accountEmail, useFileState); //throw SysError, X throw SysError(formatSystemError("accessGlobalFileState", L"", L"Function call not allowed during init/shutdown.")); } @@ -2082,57 +2506,104 @@ GooglePersistentSessions::AsyncAccessInfo accessGlobalFileState(const Zstring& g struct GetDirDetails { - GetDirDetails(const GdrivePath& gdriveFolderPath) : gdriveFolderPath_(gdriveFolderPath) {} + GetDirDetails(const GdrivePath& folderPath) : folderPath_(folderPath) {} struct Result { - std::vector childItems; - GdrivePath gdriveFolderPath; + std::vector childItems; + GdrivePath folderPath; }; Result operator()() const { try { std::string folderId; - std::optional> childItemsBuffered; - const GooglePersistentSessions::AsyncAccessInfo aai = accessGlobalFileState(gdriveFolderPath_.userEmail, [&](GoogleFileState& fileState) //throw SysError + std::optional> childItemsBuf; + const GdrivePersistentSessions::AsyncAccessInfo aai = accessGlobalFileState(folderPath_.gdriveLogin.email, [&](GdriveFileState& fileState) //throw SysError { - folderId = fileState.getItemId(gdriveFolderPath_.itemPath); //throw SysError - childItemsBuffered = fileState.tryGetBufferedFolderContent(folderId); + const auto& [itemId, itemDetails] = fileState.getFileAttributes(folderPath_.gdriveLogin.sharedDriveName, 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.tryGetBufferedFolderContent(folderId); }); - std::vector childItems; - if (childItemsBuffered) - childItems = std::move(*childItemsBuffered); - else + if (!childItemsBuf) { - childItems = readFolderContent(folderId, aai.accessToken); //throw SysError + childItemsBuf = readFolderContent(folderId, aai.accessToken); //throw SysError //buffer new file state ASAP => make sure accessGlobalFileState() has amortized constant access (despite the occasional internal readFolderContent() on non-leaf folders) - accessGlobalFileState(gdriveFolderPath_.userEmail, [&](GoogleFileState& fileState) //throw SysError + accessGlobalFileState(folderPath_.gdriveLogin.email, [&](GdriveFileState& fileState) //throw SysError { - fileState.notifyFolderContent(aai.stateDelta, folderId, childItems); + fileState.notifyFolderContent(aai.stateDelta, folderId, *childItemsBuf); }); } - for (const GoogleFileItem& item : childItems) + for (const GdriveItem& item : *childItemsBuf) if (item.details.itemName.empty()) - throw SysError(L"Folder contains child item without a name."); //mostly an issue for FFS's folder traversal, but NOT for globalGoogleSessions! + throw SysError(L"Folder contains child item without a name."); //mostly an issue for FFS's folder traversal, but NOT for globalGdriveSessions! - return { std::move(childItems), gdriveFolderPath_ }; + return { std::move(*childItemsBuf), folderPath_ }; } - catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read directory %x."), L"%x", fmtPath(getGoogleDisplayPath(gdriveFolderPath_))), e.toString()); } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read directory %x."), L"%x", fmtPath(getGdriveDisplayPath(folderPath_))), e.toString()); } } private: - GdrivePath gdriveFolderPath_; + 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.email, [&](GdriveFileState& fileState) //throw SysError + { + targetDetailsBuf = fileState.tryGetBufferedItemDetails(shortcutDetails_.targetId); + }); + if (!targetDetailsBuf) + { + targetDetailsBuf = getItemDetails(shortcutDetails_.targetId, aai.accessToken); //throw SysError + + //buffer new file state ASAP + accessGlobalFileState(shortcutPath_.gdriveLogin.email, [&](GdriveFileState& fileState) //throw SysError + { + fileState.notifyItemUpdated(aai.stateDelta, { shortcutDetails_.targetId, *targetDetailsBuf }); + }); + } + + 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 Zstring& googleUserEmail, const std::vector>>& workload /*throw X*/) : - workload_(workload), googleUserEmail_(googleUserEmail) + SingleFolderTraverser(const GdriveLogin& gdriveLogin, const std::vector>>& workload /*throw X*/) : + gdriveLogin_(gdriveLogin), workload_(workload) { while (!workload_.empty()) { @@ -2153,34 +2624,66 @@ private: void traverseWithException(const AfsPath& folderPath, AFS::TraverserCallback& cb) //throw FileError, X { - const GetDirDetails::Result r = GetDirDetails({ googleUserEmail_, folderPath })(); //throw FileError + const std::vector& childItems = GetDirDetails({ gdriveLogin_, folderPath })().childItems; //throw FileError - for (const GoogleFileItem& item : r.childItems) + for (const GdriveItem& item : childItems) { const Zstring itemName = utfTo(item.details.itemName); - if (item.details.isFolder) - { - const AfsPath afsItemPath(nativeAppendPaths(r.gdriveFolderPath.itemPath.value, itemName)); - if (std::shared_ptr cbSub = cb.onFolder({ itemName, nullptr /*symlinkInfo*/ })) //throw X - workload_.push_back({ afsItemPath, std::move(cbSub) }); - } - else + switch (item.details.type) { - AFS::FileId fileId = item.itemId; - cb.onFile({ itemName, item.details.fileSize, item.details.modTime, fileId, nullptr /*symlinkInfo*/ }); //throw X + case GdriveItemType::file: + cb.onFile({ itemName, item.details.fileSize, item.details.modTime, 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(nativeAppendPaths(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::LINK_FOLLOW: + { + const AfsPath afsItemPath(nativeAppendPaths(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, item.details.targetId, true /*isFollowedSymlink*/ }); //throw X + } + break; + + case AFS::TraverserCallback::LINK_SKIP: + break; + } + break; } } } + const GdriveLogin gdriveLogin_; std::vector>> workload_; - const Zstring googleUserEmail_; }; -void gdriveTraverseFolderRecursive(const Zstring& googleUserEmail, const std::vector>>& workload /*throw X*/, size_t) //throw X +void gdriveTraverseFolderRecursive(const GdriveLogin& gdriveLogin, const std::vector>>& workload /*throw X*/, size_t) //throw X { - SingleFolderTraverser dummy(googleUserEmail, workload); //throw X + SingleFolderTraverser dummy(gdriveLogin, workload); //throw X } //========================================================================================== //========================================================================================== @@ -2193,19 +2696,19 @@ struct InputStreamGdrive : public AbstractFileSystem::InputStream { worker_ = InterruptibleThread([asyncStreamOut = this->asyncStreamIn_, gdrivePath] { - setCurrentThreadName(("Istream[Gdrive] " + utfTo(getGoogleDisplayPath(gdrivePath))). c_str()); + setCurrentThreadName(("Istream[Gdrive] " + utfTo(getGdriveDisplayPath(gdrivePath))). c_str()); try { std::string accessToken; std::string fileId; try { - accessToken = accessGlobalFileState(gdrivePath.userEmail, [&](GoogleFileState& fileState) //throw SysError + accessToken = accessGlobalFileState(gdrivePath.gdriveLogin.email, [&](GdriveFileState& fileState) //throw SysError { - fileId = fileState.getItemId(gdrivePath.itemPath); //throw SysError + fileId = fileState.getItemId(gdrivePath.gdriveLogin.sharedDriveName, gdrivePath.itemPath, true /*followLeafShortcut*/); //throw SysError }).accessToken; } - catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot open file %x."), L"%x", fmtPath(getGoogleDisplayPath(gdrivePath))), e.toString()); } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot open file %x."), L"%x", fmtPath(getGdriveDisplayPath(gdrivePath))), e.toString()); } try { @@ -2216,7 +2719,7 @@ struct InputStreamGdrive : public AbstractFileSystem::InputStream gdriveDownloadFile(fileId, writeBlock, accessToken); //throw SysError, ThreadInterruption } - catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(getGoogleDisplayPath(gdrivePath))), e.toString()); } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(getGdriveDisplayPath(gdrivePath))), e.toString()); } asyncStreamOut->closeStream(); } @@ -2245,15 +2748,15 @@ struct InputStreamGdrive : public AbstractFileSystem::InputStream AFS::StreamAttributes attr = {}; try { - accessGlobalFileState(gdrivePath_.userEmail, [&](GoogleFileState& fileState) //throw SysError + accessGlobalFileState(gdrivePath_.gdriveLogin.email, [&](GdriveFileState& fileState) //throw SysError { - const auto& [itemId, gdriveAttr] = fileState.getFileAttributes(gdrivePath_.itemPath); //throw SysError - attr.modTime = gdriveAttr.modTime; - attr.fileSize = gdriveAttr.fileSize; + const auto& [itemId, itemDetails] = fileState.getFileAttributes(gdrivePath_.gdriveLogin.sharedDriveName, gdrivePath_.itemPath, true /*followLeafShortcut*/); //throw SysError + attr.modTime = itemDetails.modTime; + attr.fileSize = itemDetails.fileSize; attr.fileId = itemId; }); } - catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(getGoogleDisplayPath(gdrivePath_))), e.toString()); } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(getGdriveDisplayPath(gdrivePath_))), e.toString()); } return std::move(attr); //[!] } @@ -2291,26 +2794,25 @@ struct OutputStreamGdrive : public AbstractFileSystem::OutputStreamImpl worker_ = InterruptibleThread([asyncStreamIn = this->asyncStreamOut_, gdrivePath, modTime, pFileId = std::move(pFileId)]() mutable { - setCurrentThreadName(("Ostream[Gdrive] " + utfTo(getGoogleDisplayPath(gdrivePath))). c_str()); + setCurrentThreadName(("Ostream[Gdrive] " + utfTo(getGdriveDisplayPath(gdrivePath))). c_str()); try { try { const Zstring fileName = AFS::getItemName(gdrivePath.itemPath); - GoogleFileState::PathStatus ps; - GooglePersistentSessions::AsyncAccessInfo aai = accessGlobalFileState(gdrivePath.userEmail, [&](GoogleFileState& fileState) //throw SysError + std::string parentFolderId; + GdrivePersistentSessions::AsyncAccessInfo aai = accessGlobalFileState(gdrivePath.gdriveLogin.email, [&](GdriveFileState& fileState) //throw SysError { - ps = fileState.getPathStatus(gdrivePath.itemPath); //throw SysError + const GdriveFileState::PathStatus& ps = fileState.getPathStatus(gdrivePath.gdriveLogin.sharedDriveName, 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(_("Cannot find %x."), L"%x", + fmtPath(getGdriveDisplayPath({ gdrivePath.gdriveLogin, AfsPath(nativeAppendPaths(ps.existingPath.value, ps.relPath.front()))})))); + parentFolderId = ps.existingItemId; }); - 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(_("Cannot find %x."), L"%x", - fmtPath(getGoogleDisplayPath({ gdrivePath.userEmail, AfsPath(nativeAppendPaths(ps.existingPath.value, ps.relPath.front()))})))); - - const std::string& parentFolderId = ps.existingItemId; auto readBlock = [&](void* buffer, size_t bytesToRead) { @@ -2325,25 +2827,25 @@ struct OutputStreamGdrive : public AbstractFileSystem::OutputStreamImpl gdriveUploadFile (fileName, parentFolderId, modTime, readBlock, aai.accessToken); //throw SysError, ThreadInterruption assert(asyncStreamIn->getTotalBytesRead() == asyncStreamIn->getTotalBytesWritten()); - //buffer new file state ASAP (don't wait GOOGLE_DRIVE_SYNC_INTERVAL) - GoogleFileItem newFileItem = {}; + //buffer new file state ASAP (don't wait GDRIVE_SYNC_INTERVAL) + GdriveItem newFileItem = {}; newFileItem.itemId = fileIdNew; - newFileItem.details.itemName = utfTo(fileName); - newFileItem.details.isFolder = false; - newFileItem.details.isShared = false; + newFileItem.details.itemName = fileName; + newFileItem.details.type = GdriveItemType::file; + newFileItem.details.owner = FileOwner::me; newFileItem.details.fileSize = asyncStreamIn->getTotalBytesRead(); - if (modTime) //else: whatever modTime Google Drive selects will be notified after GOOGLE_DRIVE_SYNC_INTERVAL + if (modTime) //else: whatever modTime Google Drive selects will be notified after GDRIVE_SYNC_INTERVAL newFileItem.details.modTime = *modTime; newFileItem.details.parentIds.push_back(parentFolderId); - accessGlobalFileState(gdrivePath.userEmail, [&](GoogleFileState& fileState) //throw SysError + accessGlobalFileState(gdrivePath.gdriveLogin.email, [&](GdriveFileState& fileState) //throw SysError { fileState.notifyItemCreated(aai.stateDelta, newFileItem); }); pFileId.set_value(fileIdNew); } - catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(getGoogleDisplayPath(gdrivePath))), e.toString()); } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(getGdriveDisplayPath(gdrivePath))), e.toString()); } } catch (FileError&) { asyncStreamIn->setReadError(std::current_exception()); } //let ThreadInterruption pass through! }); @@ -2402,34 +2904,35 @@ private: class GdriveFileSystem : public AbstractFileSystem { public: - GdriveFileSystem(const Zstring& googleUserEmail) : googleUserEmail_(googleUserEmail) {} + GdriveFileSystem(const GdriveLogin& gdriveLogin) : gdriveLogin_(gdriveLogin) {} - const Zstring& getEmail() const { return googleUserEmail_; } + const GdriveLogin& getGdriveLogin() const { return gdriveLogin_; } private: - GdrivePath getGdrivePath(const AfsPath& afsPath) const { return { googleUserEmail_, afsPath }; } + GdrivePath getGdrivePath(const AfsPath& afsPath) const { return { gdriveLogin_, afsPath }; } - Zstring getInitPathPhrase(const AfsPath& afsPath) const override - { - Zstring initPathPhrase = concatenateGoogleFolderPathPhrase(getGdrivePath(afsPath)); - if (endsWith(initPathPhrase, Zstr(' '))) //path prase concept must survive trimming! - initPathPhrase += FILE_NAME_SEPARATOR; - return initPathPhrase; - } + Zstring getInitPathPhrase(const AfsPath& afsPath) const override { return concatenateGdriveFolderPathPhrase(getGdrivePath(afsPath)); } - std::wstring getDisplayPath(const AfsPath& afsPath) const override { return getGoogleDisplayPath(getGdrivePath(afsPath)); } + std::wstring getDisplayPath(const AfsPath& afsPath) const override { return getGdriveDisplayPath(getGdrivePath(afsPath)); } - bool isNullFileSystem() const override { return googleUserEmail_.empty(); } + bool isNullFileSystem() const override { return gdriveLogin_.email.empty(); } int compareDeviceSameAfsType(const AbstractFileSystem& afsRhs) const override { - return compareAsciiNoCase(googleUserEmail_, static_cast(afsRhs).googleUserEmail_); + const GdriveLogin& lhs = gdriveLogin_; + const GdriveLogin& rhs = static_cast(afsRhs).gdriveLogin_; + + if (const int rv = compareAsciiNoCase(lhs.email, rhs.email); + rv != 0) + return rv; + + return compareNativePath(lhs.sharedDriveName, rhs.sharedDriveName); } //---------------------------------------------------------------------------------------------------------------- ItemType getItemType(const AfsPath& afsPath) const override //throw FileError { - if (std::optional type = itemStillExists(afsPath)) //throw FileError + if (const std::optional type = itemStillExists(afsPath)) //throw FileError return *type; throw FileError(replaceCpy(_("Cannot find %x."), L"%x", fmtPath(getDisplayPath(afsPath)))); } @@ -2438,13 +2941,20 @@ private: { try { - GoogleFileState::PathStatus ps; - accessGlobalFileState(googleUserEmail_, [&](GoogleFileState& fileState) //throw SysError + GdriveFileState::PathStatus ps; + accessGlobalFileState(gdriveLogin_.email, [&](GdriveFileState& fileState) //throw SysError { - ps = fileState.getPathStatus(afsPath); //throw SysError + ps = fileState.getPathStatus(gdriveLogin_.sharedDriveName, afsPath, false /*followLeafShortcut*/); //throw SysError }); if (ps.relPath.empty()) - return ps.existingIsFolder ? ItemType::FOLDER : ItemType::FILE; + switch (ps.existingType) + { + //*INDENT-OFF* + case GdriveItemType::file: return ItemType::file; + case GdriveItemType::folder: return ItemType::folder; + case GdriveItemType::shortcut: return ItemType::symlink; + //*INDENT-ON* + } return {}; } catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(getDisplayPath(afsPath))), e.toString()); } @@ -2462,24 +2972,22 @@ private: const Zstring folderName = getItemName(afsPath); - GoogleFileState::PathStatus ps; - const GooglePersistentSessions::AsyncAccessInfo aai = accessGlobalFileState(googleUserEmail_, [&](GoogleFileState& fileState) //throw SysError + std::string parentFolderId; + const GdrivePersistentSessions::AsyncAccessInfo aai = accessGlobalFileState(gdriveLogin_.email, [&](GdriveFileState& fileState) //throw SysError { - ps = fileState.getPathStatus(afsPath); //throw SysError - }); + const GdriveFileState::PathStatus& ps = fileState.getPathStatus(gdriveLogin_.sharedDriveName, afsPath, 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.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(_("Cannot find %x."), L"%x", fmtPath(getDisplayPath(AfsPath(nativeAppendPaths(ps.existingPath.value, ps.relPath.front())))))); - - const std::string& parentFolderId = ps.existingItemId; + if (ps.relPath.size() > 1) //parent folder missing + throw SysError(replaceCpy(_("Cannot find %x."), L"%x", fmtPath(getDisplayPath(AfsPath(nativeAppendPaths(ps.existingPath.value, ps.relPath.front())))))); + parentFolderId = ps.existingItemId; + }); const std::string folderIdNew = gdriveCreateFolderPlain(folderName, parentFolderId, aai.accessToken); //throw SysError - //buffer new file state ASAP (don't wait GOOGLE_DRIVE_SYNC_INTERVAL) - accessGlobalFileState(googleUserEmail_, [&](GoogleFileState& fileState) //throw SysError + //buffer new file state ASAP (don't wait GDRIVE_SYNC_INTERVAL) + accessGlobalFileState(gdriveLogin_.email, [&](GdriveFileState& fileState) //throw SysError { fileState.notifyFolderCreated(aai.stateDelta, folderIdNew, folderName, parentFolderId); }); @@ -2491,26 +2999,26 @@ private: { std::string itemId; std::optional parentIdToUnlink; - const GooglePersistentSessions::AsyncAccessInfo aai = accessGlobalFileState(googleUserEmail_, [&](GoogleFileState& fileState) //throw SysError + const GdrivePersistentSessions::AsyncAccessInfo aai = accessGlobalFileState(gdriveLogin_.email, [&](GdriveFileState& fileState) //throw SysError { const std::optional parentPath = getParentPath(afsPath); if (!parentPath) throw SysError(L"Item is device root"); - GoogleItemDetails gdriveAttr; - std::tie(itemId, gdriveAttr) = fileState.getFileAttributes(afsPath); //throw SysError - assert(std::find(gdriveAttr.parentIds.begin(), gdriveAttr.parentIds.end(), fileState.getItemId(*parentPath)) != gdriveAttr.parentIds.end()); + GdriveItemDetails itemDetails; + std::tie(itemId, itemDetails) = fileState.getFileAttributes(gdriveLogin_.sharedDriveName, afsPath, false /*followLeafShortcut*/); //throw SysError + assert(std::find(itemDetails.parentIds.begin(), itemDetails.parentIds.end(), fileState.getItemId(gdriveLogin_.sharedDriveName, *parentPath, true /*followLeafShortcut*/)) != itemDetails.parentIds.end()); - //hard-link handling applies to shared files as well: 1. it's the right thing (TM) 2. deleting would fail anyway because we're not the owner - if (gdriveAttr.parentIds.size() > 1 || gdriveAttr.isShared) - parentIdToUnlink = fileState.getItemId(*parentPath); //throw SysError + //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(gdriveLogin_.sharedDriveName, *parentPath, true /*followLeafShortcut*/); //throw SysError }); if (parentIdToUnlink) { gdriveUnlinkParent(itemId, *parentIdToUnlink, aai.accessToken); //throw SysError - //buffer new file state ASAP (don't wait GOOGLE_DRIVE_SYNC_INTERVAL) - accessGlobalFileState(googleUserEmail_, [&](GoogleFileState& fileState) //throw SysError + //buffer new file state ASAP (don't wait GDRIVE_SYNC_INTERVAL) + accessGlobalFileState(gdriveLogin_.email, [&](GdriveFileState& fileState) //throw SysError { fileState.notifyParentRemoved(aai.stateDelta, itemId, *parentIdToUnlink); }); @@ -2522,8 +3030,8 @@ private: else gdriveMoveToTrash(itemId, aai.accessToken); //throw SysError - //buffer new file state ASAP (don't wait GOOGLE_DRIVE_SYNC_INTERVAL) - accessGlobalFileState(googleUserEmail_, [&](GoogleFileState& fileState) //throw SysError + //buffer new file state ASAP (don't wait GDRIVE_SYNC_INTERVAL) + accessGlobalFileState(gdriveLogin_.email, [&](GdriveFileState& fileState) //throw SysError { fileState.notifyItemDeleted(aai.stateDelta, itemId); }); @@ -2532,24 +3040,19 @@ private: void removeFilePlain(const AfsPath& afsPath) const override //throw FileError { - try - { - removeItemPlainImpl(afsPath, true /*permanent*/); //throw SysError - } + try { removeItemPlainImpl(afsPath, true /*permanent*/); /*throw SysError*/ } catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot delete file %x."), L"%x", fmtPath(getDisplayPath(afsPath))), e.toString()); } } void removeSymlinkPlain(const AfsPath& afsPath) const override //throw FileError { - throw FileError(replaceCpy(_("Cannot delete symbolic link %x."), L"%x", fmtPath(getDisplayPath(afsPath))), _("Operation not supported by device.")); + try { removeItemPlainImpl(afsPath, true /*permanent*/); /*throw SysError*/ } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot delete symbolic link %x."), L"%x", fmtPath(getDisplayPath(afsPath))), e.toString()); } } void removeFolderPlain(const AfsPath& afsPath) const override //throw FileError { - try - { - removeItemPlainImpl(afsPath, true /*permanent*/); //throw SysError - } + try { removeItemPlainImpl(afsPath, true /*permanent*/); /*throw SysError*/ } catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot delete directory %x."), L"%x", fmtPath(getDisplayPath(afsPath))), e.toString()); } } @@ -2573,13 +3076,33 @@ private: //---------------------------------------------------------------------------------------------------------------- AbstractPath getSymlinkResolvedPath(const AfsPath& afsPath) const override //throw FileError { + warn_static("implement") throw FileError(replaceCpy(_("Cannot determine final path for %x."), L"%x", fmtPath(getDisplayPath(afsPath))), _("Operation not supported by device.")); } - std::string getSymlinkBinaryContent(const AfsPath& afsPath) const override //throw FileError + bool equalSymlinkContentForSameAfsType(const AfsPath& afsLhs, const AbstractPath& apRhs) const override //throw FileError { - throw FileError(replaceCpy(_("Cannot resolve symbolic link %x."), L"%x", fmtPath(getDisplayPath(afsPath))), _("Operation not supported by device.")); + auto getTargetId = [](const GdriveFileSystem& gdriveFs, const AfsPath& afsPath) + { + try + { + std::string targetId; + const GdrivePersistentSessions::AsyncAccessInfo aai = accessGlobalFileState(gdriveFs.gdriveLogin_.email, [&](GdriveFileState& fileState) //throw SysError + { + const GdriveItemDetails& itemDetails = fileState.getFileAttributes(gdriveFs.gdriveLogin_.sharedDriveName, afsPath, 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(afsPath))), e.toString()); } + }; + + return getTargetId(*this, afsLhs) == getTargetId(static_cast(apRhs.afsDevice.ref()), apRhs.afsPath); } + //---------------------------------------------------------------------------------------------------------------- //return value always bound: @@ -2603,13 +3126,13 @@ private: //---------------------------------------------------------------------------------------------------------------- void traverseFolderRecursive(const TraverserWorkload& workload /*throw X*/, size_t parallelOps) const override { - gdriveTraverseFolderRecursive(googleUserEmail_, workload, parallelOps); //throw X + gdriveTraverseFolderRecursive(gdriveLogin_, workload, parallelOps); //throw X } //---------------------------------------------------------------------------------------------------------------- //symlink handling: follow link! //target existing: undefined behavior! (fail/overwrite/auto-rename) - FileCopyResult copyFileForSameAfsType(const AfsPath& afsPathSource, const StreamAttributes& attrSource, //throw FileError, (ErrorFileLocked), X + FileCopyResult copyFileForSameAfsType(const AfsPath& afsSource, const StreamAttributes& attrSource, //throw FileError, (ErrorFileLocked), X const AbstractPath& apTarget, bool copyFilePermissions, const IOCallback& notifyUnbufferedIO /*throw X*/) const override { //no native Google Drive file copy => use stream-based file copy: @@ -2617,12 +3140,12 @@ private: throw FileError(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(AFS::getDisplayPath(apTarget))), _("Operation not supported by device.")); //target existing: undefined behavior! (fail/overwrite/auto-rename) - return copyFileAsStream(afsPathSource, attrSource, apTarget, notifyUnbufferedIO); //throw FileError, (ErrorFileLocked), X + return copyFileAsStream(afsSource, attrSource, apTarget, notifyUnbufferedIO); //throw FileError, (ErrorFileLocked), X } //already existing: fail/ignore //symlink handling: follow link! - void copyNewFolderForSameAfsType(const AfsPath& afsPathSource, const AbstractPath& apTarget, bool copyFilePermissions) const override //throw FileError + void copyNewFolderForSameAfsType(const AfsPath& afsSource, const AbstractPath& apTarget, bool copyFilePermissions) const override //throw FileError { if (copyFilePermissions) throw FileError(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(AFS::getDisplayPath(apTarget))), _("Operation not supported by device.")); @@ -2631,11 +3154,54 @@ private: AFS::createFolderPlain(apTarget); //throw FileError } - void copySymlinkForSameAfsType(const AfsPath& afsPathSource, const AbstractPath& apTarget, bool copyFilePermissions) const override //throw FileError + void copySymlinkForSameAfsType(const AfsPath& afsSource, const AbstractPath& apTarget, bool copyFilePermissions) const override //throw FileError { - throw FileError(replaceCpy(replaceCpy(_("Cannot copy symbolic link %x to %y."), - L"%x", L'\n' + fmtPath(getDisplayPath(afsPathSource))), - L"%y", L'\n' + fmtPath(AFS::getDisplayPath(apTarget))), _("Operation not supported by device.")); + auto generateErrorMsg = [&] + { + return replaceCpy(replaceCpy(_("Cannot copy symbolic link %x to %y."), + L"%x", L'\n' + fmtPath(getDisplayPath(afsSource))), + L"%y", L'\n' + fmtPath(AFS::getDisplayPath(apTarget))); + }; + + if (compareDeviceSameAfsType(apTarget.afsDevice.ref()) != 0) + throw ErrorMoveUnsupported(generateErrorMsg(), _("Operation not supported between different devices.")); + warn_static("is this really true!??? => check for other cases of -Operation not supported-") + + try + { + //avoid duplicate Google Drive item creation by multiple threads + PathAccessLock pal(getGdrivePath(apTarget.afsPath)); //throw SysError + + const Zstring shortcutName = getItemName(apTarget.afsPath); + + std::string parentFolderId; + std::string targetId; + const GdrivePersistentSessions::AsyncAccessInfo aai = accessGlobalFileState(gdriveLogin_.email, [&](GdriveFileState& fileState) //throw SysError + { + const GdriveFileState::PathStatus& ps = fileState.getPathStatus(gdriveLogin_.sharedDriveName, apTarget.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(_("Cannot find %x."), L"%x", fmtPath(getDisplayPath(AfsPath(nativeAppendPaths(ps.existingPath.value, ps.relPath.front())))))); + parentFolderId = ps.existingItemId; + + const GdriveItemDetails& itemDetails = fileState.getFileAttributes(gdriveLogin_.sharedDriveName, afsSource, false /*followLeafShortcut*/).second; //throw SysError + if (itemDetails.type != GdriveItemType::shortcut) + throw SysError(L"Not a Google Drive Shortcut."); + + targetId = itemDetails.targetId; + }); + + const std::string shortcutIdNew = gdriveCreateShortcutPlain(shortcutName, parentFolderId, targetId, aai.accessToken); //throw SysError + + //buffer new file state ASAP (don't wait GDRIVE_SYNC_INTERVAL) + accessGlobalFileState(gdriveLogin_.email, [&](GdriveFileState& fileState) //throw SysError + { + fileState.notifyShortcutCreated(aai.stateDelta, shortcutIdNew, shortcutName, parentFolderId, targetId); + }); + } + catch (const SysError& e) { throw FileError(generateErrorMsg(), e.toString()); } } //target existing: undefined behavior! (fail/overwrite/auto-rename) @@ -2649,6 +3215,7 @@ private: if (compareDeviceSameAfsType(pathTo.afsDevice.ref()) != 0) throw ErrorMoveUnsupported(generateErrorMsg(), _("Operation not supported between different devices.")); + warn_static("check if true") try { @@ -2667,19 +3234,19 @@ private: time_t modTimeFrom = 0; std::string parentIdFrom; std::string parentIdTo; - const GooglePersistentSessions::AsyncAccessInfo aai = accessGlobalFileState(googleUserEmail_, [&](GoogleFileState& fileState) //throw SysError + const GdrivePersistentSessions::AsyncAccessInfo aai = accessGlobalFileState(gdriveLogin_.email, [&](GdriveFileState& fileState) //throw SysError { - GoogleItemDetails gdriveAttr; - std::tie(itemIdFrom, gdriveAttr) = fileState.getFileAttributes(pathFrom); //throw SysError + GdriveItemDetails itemDetails; + std::tie(itemIdFrom, itemDetails) = fileState.getFileAttributes(gdriveLogin_.sharedDriveName, pathFrom, false /*followLeafShortcut*/); //throw SysError - modTimeFrom = gdriveAttr.modTime; - parentIdFrom = fileState.getItemId(*parentPathFrom); //throw SysError - GoogleFileState::PathStatus psTo = fileState.getPathStatus(pathTo.afsPath); //throw SysError + modTimeFrom = itemDetails.modTime; + parentIdFrom = fileState.getItemId(gdriveLogin_.sharedDriveName, *parentPathFrom, true /*followLeafShortcut*/); //throw SysError + GdriveFileState::PathStatus psTo = fileState.getPathStatus(gdriveLogin_.sharedDriveName, 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 == itemIdFrom) - parentIdTo = fileState.getItemId(*parentPathTo); //throw SysError + parentIdTo = fileState.getItemId(gdriveLogin_.sharedDriveName, *parentPathTo, true /*followLeafShortcut*/); //throw SysError else { if (psTo.relPath.empty()) @@ -2698,8 +3265,8 @@ private: //target name already existing? will (happily) create duplicate items gdriveMoveAndRenameItem(itemIdFrom, parentIdFrom, parentIdTo, itemNameNew, modTimeFrom, aai.accessToken); //throw SysError - //buffer new file state ASAP (don't wait GOOGLE_DRIVE_SYNC_INTERVAL) - accessGlobalFileState(googleUserEmail_, [&](GoogleFileState& fileState) //throw SysError + //buffer new file state ASAP (don't wait GDRIVE_SYNC_INTERVAL) + accessGlobalFileState(gdriveLogin_.email, [&](GdriveFileState& fileState) //throw SysError { fileState.notifyMoveAndRename(aai.stateDelta, itemIdFrom, parentIdFrom, parentIdTo, itemNameNew); }); @@ -2718,16 +3285,16 @@ private: if (allowUserInteraction) try { - const std::shared_ptr gps = globalGoogleSessions.get(); + 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 Zstring& email : gps->listUserSessions()) //throw SysError - if (equalAsciiNoCase(email, googleUserEmail_)) + for (const std::string& accountEmail : gps->listAccounts()) //throw SysError + if (equalAsciiNoCase(accountEmail, gdriveLogin_.email)) return; - gps->addUserSession(googleUserEmail_ /*googleLoginHint*/, nullptr /*updateGui*/); //throw SysError + gps->addUserSession(gdriveLogin_.email /*gdriveLoginHint*/, nullptr /*updateGui*/); //throw SysError //error messages will be lost after time out in dir_exist_async.h! However: - //The most-likely-to-fail parts (web access) are reported by authorizeAccessToGoogleDrive() via the browser! + //The most-likely-to-fail parts (web access) are reported by gdriveAuthorizeAccess() via the browser! } catch (const SysError& e) { throw FileError(replaceCpy(_("Unable to connect to %x."), L"%x", fmtPath(getDisplayPath(AfsPath()))), e.toString()); } } @@ -2737,12 +3304,15 @@ private: bool hasNativeTransactionalCopy() const override { return true; } //---------------------------------------------------------------------------------------------------------------- - uint64_t getFreeDiskSpace(const AfsPath& afsPath) const override //throw FileError, returns 0 if not available + int64_t getFreeDiskSpace(const AfsPath& afsPath) const override //throw FileError, returns < 0 if not available { - try + if (!gdriveLogin_.sharedDriveName.empty()) + return -1; + + try { - const std::string& accessToken = accessGlobalFileState(googleUserEmail_, [](GoogleFileState& fileState) {}).accessToken; //throw SysError - return gdriveGetFreeDiskSpace(accessToken); //throw SysError; returns 0 if not available + const std::string& accessToken = accessGlobalFileState(gdriveLogin_.email, [](GdriveFileState& fileState) {}).accessToken; //throw SysError + return gdriveGetMyDriveFreeSpace(accessToken); //throw SysError; returns < 0 if not available } catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot determine free disk space for %x."), L"%x", fmtPath(getDisplayPath(afsPath))), e.toString()); } } @@ -2773,88 +3343,107 @@ private: } } - const Zstring googleUserEmail_; + const GdriveLogin gdriveLogin_; }; //=========================================================================================================================== } -void fff::googleDriveInit(const Zstring& configDirPath, const Zstring& caCertFilePath) +void fff::gdriveInit(const Zstring& configDirPath, const Zstring& caCertFilePath) { assert(!globalHttpSessionManager.get()); globalHttpSessionManager.set(std::make_unique(caCertFilePath)); - assert(!globalGoogleSessions.get()); - globalGoogleSessions.set(std::make_unique(configDirPath)); + assert(!globalGdriveSessions.get()); + globalGdriveSessions.set(std::make_unique(configDirPath)); } -void fff::googleDriveTeardown() +void fff::gdriveTeardown() { - try //don't use ~GooglePersistentSessions() to save! Might never happen, e.g. detached thread waiting for Google Drive authentication; terminated on exit! + 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 = globalGoogleSessions.get()) + if (const std::shared_ptr gps = globalGdriveSessions.get()) gps->saveActiveSessions(); //throw FileError } catch (FileError&) { assert(false); } - assert(globalGoogleSessions.get()); - globalGoogleSessions.set(nullptr); + assert(globalGdriveSessions.get()); + globalGdriveSessions.set(nullptr); assert(globalHttpSessionManager.get()); globalHttpSessionManager.set(nullptr); } -Zstring fff::googleAddUser(const std::function& updateGui /*throw X*/) //throw FileError, X +std::string fff::gdriveAddUser(const std::function& updateGui /*throw X*/) //throw FileError, X { try { - if (const std::shared_ptr gps = globalGoogleSessions.get()) - return gps->addUserSession(Zstr("") /*googleLoginHint*/, updateGui); //throw SysError, X + if (const std::shared_ptr gps = globalGdriveSessions.get()) + return gps->addUserSession("" /*gdriveLoginHint*/, updateGui); //throw SysError, X - throw SysError(formatSystemError("googleAddUser", L"", L"Function call not allowed during init/shutdown.")); + 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::googleRemoveUser(const Zstring& googleUserEmail) //throw FileError +void fff::gdriveRemoveUser(const std::string& accountEmail) //throw FileError { try { - if (const std::shared_ptr gps = globalGoogleSessions.get()) - return gps->removeUserSession(googleUserEmail); //throw SysError + if (const std::shared_ptr gps = globalGdriveSessions.get()) + return gps->removeUserSession(accountEmail); //throw SysError - throw SysError(formatSystemError("googleRemoveUser", L"", L"Function call not allowed during init/shutdown.")); + 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(getGoogleDisplayPath({ googleUserEmail, AfsPath() }))), e.toString()); } + catch (const SysError& e) { throw FileError(replaceCpy(_("Unable to disconnect from %x."), L"%x", fmtPath(getGdriveDisplayPath({{ accountEmail, Zstr("")}, AfsPath() }))), e.toString()); } } -std::vector /*Google user email*/ fff::googleListConnectedUsers() //throw FileError +std::vector fff::gdriveListAccounts() //throw FileError { try { - if (const std::shared_ptr gps = globalGoogleSessions.get()) - return gps->listUserSessions(); //throw SysError + if (const std::shared_ptr gps = globalGdriveSessions.get()) + return gps->listAccounts(); //throw SysError - throw SysError(formatSystemError("googleListConnectedUsers", L"", L"Function call not allowed during init/shutdown.")); + throw SysError(formatSystemError("gdriveListAccounts", L"", L"Function call not allowed during init/shutdown.")); } catch (const SysError& e) { throw FileError(replaceCpy(_("Unable to access %x."), L"%x", L"Google Drive"), e.toString()); } } -AfsDevice fff::condenseToGdriveDevice(const Zstring& userEmail) //noexcept +std::vector fff::gdriveListSharedDrives(const std::string& accountEmail) //throw FileError { - return makeSharedRef(trimCpy(userEmail)); + try + { + std::vector sharedDriveNames; + accessGlobalFileState(accountEmail, [&](GdriveFileState& fileState) //throw SysError + { + sharedDriveNames = fileState.listSharedDrives(); + }); + return sharedDriveNames; + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Unable to access %x."), L"%x", fmtPath(getGdriveDisplayPath({{ accountEmail, Zstr("")}, AfsPath() }))), e.toString()); } } -Zstring fff::extractGdriveEmail(const AfsDevice& afsDevice) //noexcept +AfsDevice fff::condenseToGdriveDevice(const GdriveLogin& login) //noexcept +{ + //clean up input: + GdriveLogin loginTmp = login; + trim(loginTmp.email); + + return makeSharedRef(loginTmp); +} + + +GdriveLogin fff::extractGdriveLogin(const AfsDevice& afsDevice) //noexcept { if (const auto gdriveDevice = dynamic_cast(&afsDevice.ref())) - return gdriveDevice ->getEmail(); + return gdriveDevice ->getGdriveLogin(); assert(false); return {}; @@ -2865,24 +3454,27 @@ bool fff::acceptsItemPathPhraseGdrive(const Zstring& itemPathPhrase) //noexcept { Zstring path = expandMacros(itemPathPhrase); //expand before trimming! trim(path); - return startsWithAsciiNoCase(path, googleDrivePrefix); + return startsWithAsciiNoCase(path, gdrivePrefix); } -//e.g.: gdrive:/john@gmail.com/folder/file.txt +//e.g.: gdrive:/john@gmail.com:SharedDrive/folder/file.txt AbstractPath fff::createItemPathGdrive(const Zstring& itemPathPhrase) //noexcept { Zstring path = itemPathPhrase; path = expandMacros(path); //expand before trimming! trim(path); - if (startsWithAsciiNoCase(path, googleDrivePrefix)) - path = path.c_str() + strLength(googleDrivePrefix); + if (startsWithAsciiNoCase(path, gdrivePrefix)) + path = path.c_str() + strLength(gdrivePrefix); const AfsPath& sanPath = sanitizeDeviceRelativePath(path); //Win/macOS compatibility: let's ignore slash/backslash differences - const Zstring& userEmail = beforeFirst(sanPath.value, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_ALL); - const AfsPath afsPath (afterFirst(sanPath.value, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_NONE)); + const Zstring& accountEmailAndDrive = beforeFirst(sanPath.value, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_ALL); + const AfsPath afsPath (afterFirst(sanPath.value, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_NONE)); + + const Zstring& accountEmail = beforeFirst(accountEmailAndDrive, Zstr(':'), IF_MISSING_RETURN_ALL); + const Zstring& sharedDrive = afterFirst (accountEmailAndDrive, Zstr(':'), IF_MISSING_RETURN_NONE); - return AbstractPath(makeSharedRef(userEmail), afsPath); + return AbstractPath(makeSharedRef(GdriveLogin{utfTo(accountEmail), sharedDrive}), afsPath); } diff --git a/FreeFileSync/Source/afs/gdrive.h b/FreeFileSync/Source/afs/gdrive.h index fb90a2ab..abca18ad 100644 --- a/FreeFileSync/Source/afs/gdrive.h +++ b/FreeFileSync/Source/afs/gdrive.h @@ -11,21 +11,28 @@ namespace fff { -bool acceptsItemPathPhraseGdrive(const Zstring& itemPathPhrase); //noexcept -AbstractPath createItemPathGdrive(const Zstring& itemPathPhrase); //noexcept +bool acceptsItemPathPhraseGdrive(const Zstring& itemPathPhrase); //noexcept +AbstractPath createItemPathGdrive (const Zstring& itemPathPhrase); //noexcept -void googleDriveInit(const Zstring& configDirPath, //directory to store Google-Drive-specific files - const Zstring& caCertFilePath); //cacert.pem -void googleDriveTeardown(); +void gdriveInit(const Zstring& configDirPath, //directory to store Google-Drive-specific files + const Zstring& caCertFilePath); //cacert.pem +void gdriveTeardown(); //------------------------------------------------------- -Zstring /*Google user email*/ googleAddUser(const std::function& updateGui /*throw X*/); //throw FileError, X -void googleRemoveUser(const Zstring& googleUserEmail); //throw FileError -std::vector /*Google user email*/ googleListConnectedUsers(); //throw FileError +std::string /*account email*/ gdriveAddUser(const std::function& updateGui /*throw X*/); //throw FileError, X +void gdriveRemoveUser(const std::string& accountEmail); //throw FileError -AfsDevice condenseToGdriveDevice(const Zstring& userEmail); //noexcept; potentially messy user input -Zstring extractGdriveEmail(const AfsDevice& afsDevice); //noexcept +std::vector gdriveListAccounts(); //throw FileError +std::vector gdriveListSharedDrives(const std::string& accountEmail); //throw FileError + +struct GdriveLogin +{ + std::string email; + Zstring sharedDriveName; //empty for "My Drive" +}; +AfsDevice condenseToGdriveDevice(const GdriveLogin& login); //noexcept; potentially messy user input +GdriveLogin extractGdriveLogin(const AfsDevice& afsDevice); //noexcept } #endif //FS_GDRIVE_9238425018342701356 diff --git a/FreeFileSync/Source/afs/native.cpp b/FreeFileSync/Source/afs/native.cpp index 4adf4945..f5986604 100644 --- a/FreeFileSync/Source/afs/native.cpp +++ b/FreeFileSync/Source/afs/native.cpp @@ -160,8 +160,8 @@ FsItemDetails getItemDetails(const Zstring& itemPath) //throw FileError 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"); - return { S_ISLNK(statData.st_mode) ? ItemType::SYMLINK : //on Linux there is no distinction between file and directory symlinks! - /**/ (S_ISDIR(statData.st_mode) ? ItemType::FOLDER : ItemType::FILE), //a file or named pipe, etc. => dont't check using S_ISREG(): see comment in file_traverser.cpp + return { S_ISLNK(statData.st_mode) ? ItemType::symlink : //on Linux there is no distinction between file and directory symlinks! + /**/ (S_ISDIR(statData.st_mode) ? ItemType::folder : ItemType::file), //a file or named pipe, etc. => dont't check using S_ISREG(): see comment in file_traverser.cpp statData.st_mtime, makeUnsigned(statData.st_size), generateFileId(statData) }; @@ -174,7 +174,7 @@ FsItemDetails getSymlinkTargetDetails(const Zstring& linkPath) //throw FileError if (::stat(linkPath.c_str(), &statData) != 0) THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot resolve symbolic link %x."), L"%x", fmtPath(linkPath)), "stat"); - return { S_ISDIR(statData.st_mode) ? ItemType::FOLDER : ItemType::FILE, + return { S_ISDIR(statData.st_mode) ? ItemType::folder : ItemType::file, statData.st_mtime, makeUnsigned(statData.st_size), generateFileId(statData) }; @@ -220,36 +220,34 @@ private: switch (itemDetails.type) { - case ItemType::FILE: - cb.onFile({ itemName, itemDetails.fileSize, itemDetails.modTime, convertToAbstractFileId(itemDetails.fileId), nullptr /*symlinkInfo*/ }); //throw X + case ItemType::file: + cb.onFile({ itemName, itemDetails.fileSize, itemDetails.modTime, convertToAbstractFileId(itemDetails.fileId), false /*isFollowedSymlink*/ }); //throw X break; - case ItemType::FOLDER: - if (std::shared_ptr cbSub = cb.onFolder({ itemName, nullptr /*symlinkInfo*/ })) //throw X + 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: + case ItemType::symlink: switch (cb.onSymlink({ itemName, itemDetails.modTime })) //throw X { case AFS::TraverserCallback::LINK_FOLLOW: { - FsItemDetails linkDetails = {}; + FsItemDetails targetDetails = {}; if (!tryReportingItemError([&] //throw X { - linkDetails = getSymlinkTargetDetails(itemPath); //throw FileError + targetDetails = getSymlinkTargetDetails(itemPath); //throw FileError }, cb, itemName)) continue; - const AFS::SymlinkInfo linkInfo = { itemName, linkDetails.modTime }; - - if (linkDetails.type == ItemType::FOLDER) + if (targetDetails.type == ItemType::folder) { - if (std::shared_ptr cbSub = cb.onFolder({ itemName, &linkInfo })) //throw X + if (std::shared_ptr cbSub = cb.onFolder({ itemName, true /*isFollowedSymlink*/ })) //throw X workload_.push_back({ itemPath, std::move(cbSub) }); } else //a file or named pipe, etc. - cb.onFile({ itemName, linkDetails.fileSize, linkDetails.modTime, convertToAbstractFileId(linkDetails.fileId), &linkInfo }); //throw X + cb.onFile({ itemName, targetDetails.fileSize, targetDetails.modTime, convertToAbstractFileId(targetDetails.fileId), true /*isFollowedSymlink*/ }); //throw X } break; @@ -398,15 +396,15 @@ private: initComForThread(); //throw FileError switch (zen::getItemType(getNativePath(afsPath))) //throw FileError { - case zen::ItemType::FILE: - return AFS::ItemType::FILE; - case zen::ItemType::FOLDER: - return AFS::ItemType::FOLDER; - case zen::ItemType::SYMLINK: - return AFS::ItemType::SYMLINK; + 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 AFS::ItemType::FILE; + return AFS::ItemType::file; } std::optional itemStillExists(const AfsPath& afsPath) const override //throw FileError @@ -465,13 +463,19 @@ private: return AbstractPath(makeSharedRef(comp->rootPath), AfsPath(comp->relPath)); } - std::string getSymlinkBinaryContent(const AfsPath& afsPath) const override //throw FileError + bool equalSymlinkContentForSameAfsType(const AfsPath& afsLhs, const AbstractPath& apRhs) const override //throw FileError { initComForThread(); //throw FileError - const Zstring nativePath = getNativePath(afsPath); - std::string content = utfTo(getSymlinkTargetRaw(nativePath)); //throw FileError - return content; + auto getTargetBlob = [](const NativeFileSystem& nativeFs, const AfsPath& afsPath) + { + const Zstring nativePath = nativeFs.getNativePath(afsPath); + + std::string contentBlob = utfTo(getSymlinkTargetRaw(nativePath)); //throw FileError + return contentBlob; + }; + + return getTargetBlob(*this, afsLhs) == getTargetBlob(static_cast(apRhs.afsDevice.ref()), apRhs.afsPath); } //---------------------------------------------------------------------------------------------------------------- @@ -507,14 +511,14 @@ private: //symlink handling: follow link! //target existing: undefined behavior! (fail/overwrite/auto-rename) => Native will fail and give a clear error message - FileCopyResult copyFileForSameAfsType(const AfsPath& afsPathSource, const StreamAttributes& attrSource, //throw FileError, ErrorFileLocked, X + FileCopyResult copyFileForSameAfsType(const AfsPath& afsSource, const StreamAttributes& attrSource, //throw FileError, ErrorFileLocked, X const AbstractPath& apTarget, bool copyFilePermissions, const IOCallback& notifyUnbufferedIO /*throw X*/) const override { const Zstring nativePathTarget = static_cast(apTarget.afsDevice.ref()).getNativePath(apTarget.afsPath); initComForThread(); //throw FileError - const zen::FileCopyResult nativeResult = copyNewFile(getNativePath(afsPathSource), nativePathTarget, //throw FileError, ErrorTargetExisting, ErrorFileLocked, X + const zen::FileCopyResult nativeResult = copyNewFile(getNativePath(afsSource), nativePathTarget, //throw FileError, ErrorTargetExisting, ErrorFileLocked, X copyFilePermissions, notifyUnbufferedIO); FileCopyResult result; result.fileSize = nativeResult.fileSize; @@ -527,11 +531,11 @@ private: //target existing: fail/ignore => Native will fail and give a clear error message //symlink handling: follow link! - void copyNewFolderForSameAfsType(const AfsPath& afsPathSource, const AbstractPath& apTarget, bool copyFilePermissions) const override //throw FileError + void copyNewFolderForSameAfsType(const AfsPath& afsSource, const AbstractPath& apTarget, bool copyFilePermissions) const override //throw FileError { initComForThread(); //throw FileError - const Zstring& sourcePath = getNativePath(afsPathSource); + const Zstring& sourcePath = getNativePath(afsSource); const Zstring& targetPath = static_cast(apTarget.afsDevice.ref()).getNativePath(apTarget.afsPath); zen::createDirectory(targetPath); //throw FileError, ErrorTargetExisting @@ -541,19 +545,19 @@ private: //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 (getParentPath(afsPathSource)) //=> not a root path + if (getParentPath(afsSource)) //=> not a root path tryCopyDirectoryAttributes(sourcePath, targetPath); //throw FileError if (copyFilePermissions) copyItemPermissions(sourcePath, targetPath, ProcSymlink::FOLLOW); //throw FileError } - void copySymlinkForSameAfsType(const AfsPath& afsPathSource, const AbstractPath& apTarget, bool copyFilePermissions) const override //throw FileError + void copySymlinkForSameAfsType(const AfsPath& afsSource, const AbstractPath& apTarget, bool copyFilePermissions) const override //throw FileError { const Zstring nativePathTarget = static_cast(apTarget.afsDevice.ref()).getNativePath(apTarget.afsPath); initComForThread(); //throw FileError - zen::copySymlink(getNativePath(afsPathSource), nativePathTarget, copyFilePermissions); //throw FileError + zen::copySymlink(getNativePath(afsSource), nativePathTarget, copyFilePermissions); //throw FileError } //target existing: undefined behavior! (fail/overwrite/auto-rename) => Native will fail and give a clear error message @@ -607,7 +611,7 @@ private: bool hasNativeTransactionalCopy() const override { return false; } //---------------------------------------------------------------------------------------------------------------- - uint64_t getFreeDiskSpace(const AfsPath& afsPath) const override //throw FileError, returns 0 if not available + int64_t getFreeDiskSpace(const AfsPath& afsPath) const override //throw FileError, returns < 0 if not available { initComForThread(); //throw FileError return zen::getFreeDiskSpace(getNativePath(afsPath)); //throw FileError diff --git a/FreeFileSync/Source/afs/sftp.cpp b/FreeFileSync/Source/afs/sftp.cpp index 54eb99ad..b9c97ac7 100644 --- a/FreeFileSync/Source/afs/sftp.cpp +++ b/FreeFileSync/Source/afs/sftp.cpp @@ -25,7 +25,7 @@ using AFS = AbstractFileSystem; namespace { -Zstring concatenateSftpFolderPathPhrase(const SftpLoginInfo& login, const AfsPath& afsPath); //noexcept +Zstring concatenateSftpFolderPathPhrase(const SftpLogin& login, const AfsPath& afsPath); //noexcept /* SFTP specification version 3 (implemented by libssh2): https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt @@ -103,7 +103,7 @@ SFTP_OPTIMAL_BLOCK_SIZE_READ: SFTP_OPTIMAL_BLOCK_SIZE_WRITE: //use all configuration data that *defines* an SSH session as key when buffering sessions! This is what user expects, e.g. when changing settings in SFTP login dialog struct SshSessionId { - /*explicit*/ SshSessionId(const SftpLoginInfo& login) : + /*explicit*/ SshSessionId(const SftpLogin& login) : server(login.server), port(login.port), username(login.username), @@ -126,15 +126,15 @@ struct SshSessionId bool operator<(const SshSessionId& lhs, const SshSessionId& rhs) { //exactly the type of case insensitive comparison we need for server names! - int rv = compareAsciiNoCase(lhs.server, rhs.server); //https://docs.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfow#IDNs - if (rv != 0) + if (const int rv = compareAsciiNoCase(lhs.server, rhs.server); //https://docs.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfow#IDNs + rv != 0) return rv < 0; if (lhs.port != rhs.port) return lhs.port < rhs.port; - rv = compareString(lhs.username, rhs.username); //case sensitive! - if (rv != 0) + if (const int rv = compareString(lhs.username, rhs.username); //case sensitive! + rv != 0) return rv < 0; if (lhs.allowZlib != rhs.allowZlib) @@ -149,8 +149,8 @@ bool operator<(const SshSessionId& lhs, const SshSessionId& rhs) return compareString(lhs.password, rhs.password) < 0; //case sensitive! case SftpAuthType::keyFile: - rv = compareString(lhs.password, rhs.password); //case sensitive! - if (rv != 0) + if (const int rv = compareString(lhs.password, rhs.password); //case sensitive! + rv != 0) return rv < 0; return compareString(lhs.privateKeyFilePath, rhs.privateKeyFilePath) < 0; //case sensitive! @@ -835,7 +835,7 @@ public: }; - std::shared_ptr getSharedSession(const SftpLoginInfo& login) //throw SysError + std::shared_ptr getSharedSession(const SftpLogin& login) //throw SysError { Protected& sessionStore = getSessionStore(login); @@ -881,7 +881,7 @@ public: } - std::unique_ptr getExclusiveSession(const SftpLoginInfo& login) //throw SysError + std::unique_ptr getExclusiveSession(const SftpLogin& login) //throw SysError { Protected& sessionStore = getSessionStore(login); @@ -999,7 +999,7 @@ void SftpSessionManager::ReUseOnDelete::operator()(SshSession* s) const } -std::shared_ptr getSharedSftpSession(const SftpLoginInfo& login) //throw SysError +std::shared_ptr getSharedSftpSession(const SftpLogin& login) //throw SysError { if (const std::shared_ptr mgr = globalSftpSessionManager.get()) return mgr->getSharedSession(login); //throw SysError @@ -1008,7 +1008,7 @@ std::shared_ptr getSharedSftpSession(const } -std::unique_ptr getExclusiveSftpSession(const SftpLoginInfo& login) //throw SysError +std::unique_ptr getExclusiveSftpSession(const SftpLogin& login) //throw SysError { if (const std::shared_ptr mgr = globalSftpSessionManager.get()) return mgr->getExclusiveSession(login); //throw SysError @@ -1017,7 +1017,7 @@ std::unique_ptr getExclusiveSftpSession } -void runSftpCommand(const SftpLoginInfo& login, const char* functionName, +void runSftpCommand(const SftpLogin& login, const char* functionName, const std::function& sftpCommand /*noexcept!*/) //throw SysError { std::shared_ptr asyncSession = getSharedSftpSession(login); //throw SysError @@ -1042,7 +1042,7 @@ struct SftpItem Zstring itemName; SftpItemDetails details; }; -std::vector getDirContentFlat(const SftpLoginInfo& login, const AfsPath& dirPath) //throw FileError +std::vector getDirContentFlat(const SftpLogin& login, const AfsPath& dirPath) //throw FileError { LIBSSH2_SFTP_HANDLE* dirHandle = nullptr; try @@ -1096,23 +1096,23 @@ std::vector getDirContentFlat(const SftpLoginInfo& login, const AfsPat { 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.server, itemPath))), L"Modification time not supported."); - output.push_back({ itemName, { AFS::ItemType::SYMLINK, 0, static_cast(attribs.mtime) }}); + 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) }}); + 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.server, 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.server, itemPath))), L"File size not supported."); - output.push_back({ itemName, { AFS::ItemType::FILE, attribs.filesize, static_cast(attribs.mtime) }}); + output.push_back({ itemName, { AFS::ItemType::file, attribs.filesize, static_cast(attribs.mtime) }}); } } } -SftpItemDetails getSymlinkTargetDetails(const SftpLoginInfo& login, const AfsPath& linkPath) //throw FileError +SftpItemDetails getSymlinkTargetDetails(const SftpLogin& login, const AfsPath& linkPath) //throw FileError { LIBSSH2_SFTP_ATTRIBUTES attribsTrg = {}; try @@ -1126,7 +1126,7 @@ SftpItemDetails getSymlinkTargetDetails(const SftpLoginInfo& login, const AfsPat throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(getSftpDisplayPath(login.server, linkPath))), L"File attributes not available."); if (LIBSSH2_SFTP_S_ISDIR(attribsTrg.permissions)) - return { AFS::ItemType::FOLDER, 0, static_cast(attribsTrg.mtime) }; + 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! @@ -1134,7 +1134,7 @@ SftpItemDetails getSymlinkTargetDetails(const SftpLoginInfo& login, const AfsPat if ((attribsTrg.flags & LIBSSH2_SFTP_ATTR_SIZE) == 0) throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(getSftpDisplayPath(login.server, linkPath))), L"File size not supported."); - return { AFS::ItemType::FILE, attribsTrg.filesize, static_cast(attribsTrg.mtime) }; + return { AFS::ItemType::file, attribsTrg.filesize, static_cast(attribsTrg.mtime) }; } } @@ -1142,7 +1142,7 @@ SftpItemDetails getSymlinkTargetDetails(const SftpLoginInfo& login, const AfsPat class SingleFolderTraverser { public: - SingleFolderTraverser(const SftpLoginInfo& login, const std::vector>>& workload /*throw X*/) : + SingleFolderTraverser(const SftpLogin& login, const std::vector>>& workload /*throw X*/) : workload_(workload), login_(login) { while (!workload_.empty()) @@ -1170,16 +1170,16 @@ private: switch (item.details.type) { - case AFS::ItemType::FILE: - cb.onFile({ item.itemName, item.details.fileSize, item.details.modTime, AFS::FileId(), nullptr /*symlinkInfo*/ }); //throw X + case AFS::ItemType::file: + cb.onFile({ item.itemName, item.details.fileSize, item.details.modTime, AFS::FileId(), false /*isFollowedSymlink*/ }); //throw X break; - case AFS::ItemType::FOLDER: - if (std::shared_ptr cbSub = cb.onFolder({ item.itemName, nullptr /*symlinkInfo*/ })) //throw X + 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: + case AFS::ItemType::symlink: switch (cb.onSymlink({ item.itemName, item.details.modTime })) //throw X { case AFS::TraverserCallback::LINK_FOLLOW: @@ -1191,15 +1191,13 @@ private: }, cb, item.itemName)) continue; - const AFS::SymlinkInfo linkInfo = { item.itemName, targetDetails.modTime }; - - if (targetDetails.type == AFS::ItemType::FOLDER) + if (targetDetails.type == AFS::ItemType::folder) { - if (std::shared_ptr cbSub = cb.onFolder({ item.itemName, &linkInfo })) //throw X + 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, targetDetails.fileSize, targetDetails.modTime, AFS::FileId(), &linkInfo }); //throw X + cb.onFile({ item.itemName, targetDetails.fileSize, targetDetails.modTime, AFS::FileId(), true /*isFollowedSymlink*/ }); //throw X } break; @@ -1212,11 +1210,11 @@ private: } std::vector>> workload_; - const SftpLoginInfo login_; + const SftpLogin login_; }; -void traverseFolderRecursiveSftp(const SftpLoginInfo& login, const std::vector>>& workload /*throw X*/, size_t) //throw X +void traverseFolderRecursiveSftp(const SftpLogin& login, const std::vector>>& workload /*throw X*/, size_t) //throw X { SingleFolderTraverser dummy(login, workload); //throw X } @@ -1225,7 +1223,7 @@ void traverseFolderRecursiveSftp(const SftpLoginInfo& login, const std::vector modTime, const IOCallback& notifyUnbufferedIO /*throw X*/) : @@ -1521,9 +1519,9 @@ private: class SftpFileSystem : public AbstractFileSystem { public: - SftpFileSystem(const SftpLoginInfo& login) : login_(login) {} + SftpFileSystem(const SftpLogin& login) : login_(login) {} - const SftpLoginInfo& getLogin() const { return login_; } + const SftpLogin& getLogin() const { return login_; } AfsPath getHomePath() const //throw FileError { @@ -1545,12 +1543,12 @@ private: int compareDeviceSameAfsType(const AbstractFileSystem& afsRhs) const override { - const SftpLoginInfo& lhs = login_; - const SftpLoginInfo& rhs = static_cast(afsRhs).login_; + const SftpLogin& lhs = login_; + const SftpLogin& rhs = static_cast(afsRhs).login_; //exactly the type of case insensitive comparison we need for server names! - const int rv = compareAsciiNoCase(lhs.server, rhs.server); //https://docs.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfow#IDNs - if (rv != 0) + if (const int rv = compareAsciiNoCase(lhs.server, rhs.server); //https://docs.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfow#IDNs + rv != 0) return rv; //port does NOT create a *different* data source!!! -> same thing for password! @@ -1573,10 +1571,10 @@ private: throw SysError(formatSystemError("libssh2_sftp_lstat", L"", L"File attributes not available.")); if (LIBSSH2_SFTP_S_ISLNK(attr.permissions)) - return ItemType::SYMLINK; + 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 + 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 } catch (const SysError& e) { @@ -1643,7 +1641,7 @@ private: { //tested: libssh2_sftp_rmdir will fail for symlinks! bool symlinkExists = false; - try { symlinkExists = getItemType(afsPath) == ItemType::SYMLINK; } /*throw FileError*/ catch (FileError&) {} //previous exception is more relevant + try { symlinkExists = getItemType(afsPath) == ItemType::symlink; } /*throw FileError*/ catch (FileError&) {} //previous exception is more relevant if (symlinkExists) return removeSymlinkPlain(afsPath); //throw FileError @@ -1687,19 +1685,24 @@ private: catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot determine final path for %x."), L"%x", fmtPath(getDisplayPath(afsPath))), e.toString()); } } - std::string getSymlinkBinaryContent(const AfsPath& afsPath) const override //throw FileError + bool equalSymlinkContentForSameAfsType(const AfsPath& afsLhs, const AbstractPath& apRhs) const override //throw FileError { - const unsigned int bufSize = 10000; - std::string buf(bufSize + 1, '\0'); //ensure buffer is always null-terminated since we don't evaluate the byte count returned by libssh2_sftp_readlink()! - try + auto getTargetPath = [](const SftpFileSystem& sftpFs, const AfsPath& afsPath) { - runSftpCommand(login_, "libssh2_sftp_readlink", //throw SysError - [&](const SshSession::Details& sd) { return ::libssh2_sftp_readlink(sd.sftpChannel, getLibssh2Path(afsPath), &buf[0], bufSize); }); //noexcept! - } - catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot resolve symbolic link %x."), L"%x", fmtPath(getDisplayPath(afsPath))), e.toString()); } + const unsigned int bufSize = 10000; + std::string buf(bufSize + 1, '\0'); //ensure buffer is always null-terminated since we don't evaluate the byte count returned by libssh2_sftp_readlink()! + try + { + runSftpCommand(sftpFs.login_, "libssh2_sftp_readlink", //throw SysError + [&](const SshSession::Details& sd) { return ::libssh2_sftp_readlink(sd.sftpChannel, getLibssh2Path(afsPath), &buf[0], bufSize); }); //noexcept! + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot resolve symbolic link %x."), L"%x", fmtPath(sftpFs.getDisplayPath(afsPath))), e.toString()); } + + buf.resize(strLength(&buf[0])); + return buf; + }; - buf.resize(strLength(&buf[0])); - return buf; + return getTargetPath(*this, afsLhs) == getTargetPath(static_cast(apRhs.afsDevice.ref()), apRhs.afsPath); } //---------------------------------------------------------------------------------------------------------------- @@ -1727,7 +1730,7 @@ private: //target existing: undefined behavior! (fail/overwrite/auto-rename) //symlink handling: follow link! - FileCopyResult copyFileForSameAfsType(const AfsPath& afsPathSource, const StreamAttributes& attrSource, //throw FileError, (ErrorFileLocked), X + FileCopyResult copyFileForSameAfsType(const AfsPath& afsSource, const StreamAttributes& attrSource, //throw FileError, (ErrorFileLocked), X const AbstractPath& apTarget, bool copyFilePermissions, const IOCallback& notifyUnbufferedIO /*throw X*/) const override { //no native SFTP file copy => use stream-based file copy: @@ -1735,12 +1738,12 @@ private: throw FileError(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(AFS::getDisplayPath(apTarget))), _("Operation not supported by device.")); //target existing: undefined behavior! (fail/overwrite/auto-rename) - return copyFileAsStream(afsPathSource, attrSource, apTarget, notifyUnbufferedIO); //throw FileError, (ErrorFileLocked), X + return copyFileAsStream(afsSource, attrSource, apTarget, notifyUnbufferedIO); //throw FileError, (ErrorFileLocked), X } //already existing: fail/ignore //symlink handling: follow link! - void copyNewFolderForSameAfsType(const AfsPath& afsPathSource, const AbstractPath& apTarget, bool copyFilePermissions) const override //throw FileError + void copyNewFolderForSameAfsType(const AfsPath& afsSource, const AbstractPath& apTarget, bool copyFilePermissions) const override //throw FileError { if (copyFilePermissions) throw FileError(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(AFS::getDisplayPath(apTarget))), _("Operation not supported by device.")); @@ -1749,10 +1752,10 @@ private: AFS::createFolderPlain(apTarget); //throw FileError } - void copySymlinkForSameAfsType(const AfsPath& afsPathSource, const AbstractPath& apTarget, bool copyFilePermissions) const override + void copySymlinkForSameAfsType(const AfsPath& afsSource, const AbstractPath& apTarget, bool copyFilePermissions) const override { throw FileError(replaceCpy(replaceCpy(_("Cannot copy symbolic link %x to %y."), - L"%x", L'\n' + fmtPath(getDisplayPath(afsPathSource))), + L"%x", L'\n' + fmtPath(getDisplayPath(afsSource))), L"%y", L'\n' + fmtPath(AFS::getDisplayPath(apTarget))), _("Operation not supported by device.")); } @@ -1772,16 +1775,14 @@ private: runSftpCommand(login_, "libssh2_sftp_rename", //throw SysError [&](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_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" http://www.greenend.org.uk/rjk/sftp/sftpversions.html - LIBSSH2_SFTP_RENAME_OVERWRITE: "No overwriting rename in [SFTP] v3/v4" http://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 + 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! - */ + "... 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); @@ -1808,14 +1809,14 @@ private: bool hasNativeTransactionalCopy() const override { return false; } //---------------------------------------------------------------------------------------------------------------- - uint64_t getFreeDiskSpace(const AfsPath& afsPath) const override //throw FileError, returns 0 if not available + int64_t getFreeDiskSpace(const AfsPath& afsPath) 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 0; + 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!! @@ -1848,19 +1849,19 @@ private: throw FileError(replaceCpy(_("Unable to move %x to the recycle bin."), L"%x", fmtPath(getDisplayPath(afsPath))), _("Operation not supported by device.")); } - const SftpLoginInfo login_; + const SftpLogin login_; }; //=========================================================================================================================== //expects "clean" login data -Zstring concatenateSftpFolderPathPhrase(const SftpLoginInfo& login, const AfsPath& afsPath) //noexcept +Zstring concatenateSftpFolderPathPhrase(const SftpLogin& login, const AfsPath& afsPath) //noexcept { Zstring port; if (login.port > 0) port = Zstr(':') + numberTo(login.port); - const SftpLoginInfo loginDefault; + const SftpLogin loginDefault; Zstring options; if (login.timeoutSec != loginDefault.timeoutSec) @@ -1909,16 +1910,16 @@ void fff::sftpTeardown() } -AfsPath fff::getSftpHomePath(const SftpLoginInfo& login) //throw FileError +AfsPath fff::getSftpHomePath(const SftpLogin& login) //throw FileError { return SftpFileSystem(login).getHomePath(); //throw FileError } -AfsDevice fff::condenseToSftpDevice(const SftpLoginInfo& login) //noexcept +AfsDevice fff::condenseToSftpDevice(const SftpLogin& login) //noexcept { //clean up input: - SftpLoginInfo loginTmp = login; + SftpLogin loginTmp = login; trim(loginTmp.server); trim(loginTmp.username); trim(loginTmp.privateKeyFilePath); @@ -1938,7 +1939,7 @@ AfsDevice fff::condenseToSftpDevice(const SftpLoginInfo& login) //noexcept } -SftpLoginInfo fff::extractSftpLogin(const AfsDevice& afsDevice) //noexcept +SftpLogin fff::extractSftpLogin(const AfsDevice& afsDevice) //noexcept { if (const auto sftpDevice = dynamic_cast(&afsDevice.ref())) return sftpDevice->getLogin(); @@ -1948,7 +1949,7 @@ SftpLoginInfo fff::extractSftpLogin(const AfsDevice& afsDevice) //noexcept } -int fff::getServerMaxChannelsPerConnection(const SftpLoginInfo& login) //throw FileError +int fff::getServerMaxChannelsPerConnection(const SftpLogin& login) //throw FileError { try { @@ -2004,7 +2005,7 @@ AbstractPath fff::createItemPathSftp(const Zstring& itemPathPhrase) //noexcept const Zstring credentials = beforeFirst(pathPhrase, Zstr('@'), IF_MISSING_RETURN_NONE); const Zstring fullPathOpt = afterFirst(pathPhrase, Zstr('@'), IF_MISSING_RETURN_ALL); - SftpLoginInfo login; + SftpLogin login; login.username = decodeFtpUsername(beforeFirst(credentials, Zstr(':'), IF_MISSING_RETURN_ALL)); //support standard FTP syntax, even though ':' login.password = afterFirst(credentials, Zstr(':'), IF_MISSING_RETURN_NONE); //is not used by our concatenateSftpFolderPathPhrase()! diff --git a/FreeFileSync/Source/afs/sftp.h b/FreeFileSync/Source/afs/sftp.h index 5dfb66b9..fce5fc67 100644 --- a/FreeFileSync/Source/afs/sftp.h +++ b/FreeFileSync/Source/afs/sftp.h @@ -27,7 +27,7 @@ enum class SftpAuthType agent, }; -struct SftpLoginInfo +struct SftpLogin { Zstring server; int port = 0; // > 0 if set @@ -43,12 +43,12 @@ struct SftpLoginInfo int timeoutSec = 15; //valid range: [1, inf) int traverserChannelsPerConnection = 1; //valid range: [1, inf) }; -AfsDevice condenseToSftpDevice(const SftpLoginInfo& login); //noexcept; potentially messy user input -SftpLoginInfo extractSftpLogin(const AfsDevice& afsDevice); //noexcept +AfsDevice condenseToSftpDevice(const SftpLogin& login); //noexcept; potentially messy user input +SftpLogin extractSftpLogin(const AfsDevice& afsDevice); //noexcept -int getServerMaxChannelsPerConnection(const SftpLoginInfo& login); //throw FileError +int getServerMaxChannelsPerConnection(const SftpLogin& login); //throw FileError -AfsPath getSftpHomePath(const SftpLoginInfo& 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 index ff0cf4d6..03bc779b 100644 --- a/FreeFileSync/Source/application.cpp +++ b/FreeFileSync/Source/application.cpp @@ -154,10 +154,17 @@ void Application::onEnterEventLoop(wxEvent& event) int Application::OnRun() +{ + [[maybe_unused]] const int rc = wxApp::OnRun(); + return exitCode_; +} + + +void Application::OnUnhandledException() //handles both wxApp::OnInit() + wxApp::OnRun() { try { - wxApp::OnRun(); + throw; //just re-throw and avoid display of additional messagebox } catch (const std::bad_alloc& e) //the only kind of exception we don't want crash dumps for { @@ -165,11 +172,9 @@ int Application::OnRun() const auto& titleFmt = copyStringTo(wxTheApp->GetAppDisplayName()) + SPACED_DASH + _("An exception occurred"); std::cerr << utfTo(titleFmt + SPACED_DASH) << e.what() << '\n'; - return FFS_EXIT_EXCEPTION; + terminateProcess(FFS_EXIT_EXCEPTION); } //catch (...) -> let it crash and create mini dump!!! - - return exitCode_; } @@ -178,7 +183,7 @@ void Application::onQueryEndSession(wxEvent& event) if (auto mainWin = dynamic_cast(GetTopWindow())) mainWin->onQueryEndSession(); //it's futile to try and clean up while the process is in full swing (CRASH!) => just terminate! - //also: avoid wxCloseEvent::Veto() cancelling shutdown when some dialogs receive a close event from the system + //also: avoid wxCloseEvent::Veto() cancels shutdown when dialogs receive a close event from the system terminateProcess(FFS_EXIT_ABORTED); } @@ -269,7 +274,7 @@ void Application::launch(const std::vector& commandArgs) { try { - if (getItemType(itemPath) == ItemType::FILE) //throw FileError + if (getItemType(itemPath) == ItemType::file) //throw FileError if (std::optional parentPath = getParentFolderPath(itemPath)) return *parentPath; } diff --git a/FreeFileSync/Source/application.h b/FreeFileSync/Source/application.h index 0b8c4d79..78025120 100644 --- a/FreeFileSync/Source/application.h +++ b/FreeFileSync/Source/application.h @@ -21,8 +21,8 @@ private: bool OnInit() override; int OnRun () override; int OnExit() override; - bool OnExceptionInMainLoop() override { throw; } //just re-throw and avoid display of additional messagebox: it will be caught in OnRun() - void OnUnhandledException () override { throw; } //just re-throw and avoid display of additional messagebox + bool OnExceptionInMainLoop() override { throw; } //just re-throw and avoid display of additional messagebox: it will be caught in OnUnhandledException() + void OnUnhandledException () override; wxLayoutDirection GetLayoutDirection() const override; void onEnterEventLoop(wxEvent& event); void onQueryEndSession(wxEvent& event); diff --git a/FreeFileSync/Source/base/algorithm.cpp b/FreeFileSync/Source/base/algorithm.cpp index ae4f9b2b..d1dac7e7 100644 --- a/FreeFileSync/Source/base/algorithm.cpp +++ b/FreeFileSync/Source/base/algorithm.cpp @@ -461,8 +461,8 @@ private: template FilePair* getAssocFilePair(const InSyncFile& dbFile) const { - const std::unordered_map& exOneSideById = SelectParam::ref(exLeftOnlyById_, exRightOnlyById_); - const std::unordered_map& exOneSideByPath = SelectParam::ref(exLeftOnlyByPath_, exRightOnlyByPath_); + const std::unordered_map& exOneSideById = SelectParam::ref(exLeftOnlyById_, exRightOnlyById_); + const std::unordered_map& exOneSideByPath = SelectParam::ref(exLeftOnlyByPath_, exRightOnlyByPath_); { auto it = exOneSideByPath.find(&dbFile); if (it != exOneSideByPath.end()) @@ -501,8 +501,8 @@ private: const int fileTimeTolerance_; const std::vector ignoreTimeShiftMinutes_; - std::unordered_map exLeftOnlyById_; //FilePair* == nullptr for duplicate ids! => consider aliasing through symlinks! - std::unordered_map exRightOnlyById_; //=> avoid ambiguity for mixtures of files/symlinks on one side and allow 1-1 mapping only! + std::unordered_map exLeftOnlyById_; //FilePair* == nullptr for duplicate ids! => consider aliasing through symlinks! + std::unordered_map exRightOnlyById_; //=> avoid ambiguity for mixtures of files/symlinks on one side and allow 1-1 mapping only! //MSVC: std::unordered_map: about twice as fast as std::map for 1 million items! std::unordered_map exLeftOnlyByPath_; //MSVC: only 4% faster than std::map for 1 million items! diff --git a/FreeFileSync/Source/base/algorithm.h b/FreeFileSync/Source/base/algorithm.h index 48d8b7c1..8be25f7e 100644 --- a/FreeFileSync/Source/base/algorithm.h +++ b/FreeFileSync/Source/base/algorithm.h @@ -8,7 +8,7 @@ #define ALGORITHM_H_34218518475321452548 #include -//#include "config.h" +#include #include "structures.h" #include "file_hierarchy.h" #include "soft_filter.h" diff --git a/FreeFileSync/Source/base/comparison.cpp b/FreeFileSync/Source/base/comparison.cpp index 27e23867..e730343e 100644 --- a/FreeFileSync/Source/base/comparison.cpp +++ b/FreeFileSync/Source/base/comparison.cpp @@ -380,28 +380,26 @@ namespace void categorizeSymlinkByContent(SymlinkPair& symlink, PhaseCallback& callback) { //categorize symlinks that exist on both sides - std::string binaryContentL; - std::string binaryContentR; + callback.updateStatus(replaceCpy(_("Resolving symbolic link %x"), L"%x", fmtPath(AFS::getDisplayPath(symlink.getAbstractPath< LEFT_SIDE>())))); //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([&] { - callback.updateStatus(replaceCpy(_("Resolving symbolic link %x"), L"%x", fmtPath(AFS::getDisplayPath(symlink.getAbstractPath())))); //throw X - binaryContentL = AFS::getSymlinkBinaryContent(symlink.getAbstractPath()); //throw FileError - - callback.updateStatus(replaceCpy(_("Resolving symbolic link %x"), L"%x", fmtPath(AFS::getDisplayPath(symlink.getAbstractPath())))); //throw X - binaryContentR = AFS::getSymlinkBinaryContent(symlink.getAbstractPath()); //throw FileError + equalContent = AFS::equalSymlinkContent(symlink.getAbstractPath< LEFT_SIDE>(), + symlink.getAbstractPath()); //throw FileError }, callback); //throw X if (!errMsg.empty()) symlink.setCategoryConflict(utfTo(errMsg)); else { - if (binaryContentL == binaryContentR) + if (equalContent) { //Caveat: //1. SYMLINK_EQUAL may only be set if short names match in case: InSyncFolder's mapping tables use short name as a key! see db_file.cpp //2. harmonize with "bool stillInSync()" in algorithm.cpp, FilePair::setSyncedTo() in file_hierarchy.h - //symlinks have same "content" if (getUnicodeNormalForm(symlink.getItemName< LEFT_SIDE>()) != getUnicodeNormalForm(symlink.getItemName())) symlink.setCategoryDiffMetadata(getDescrDiffMetaShortnameCase(symlink)); diff --git a/FreeFileSync/Source/base/db_file.cpp b/FreeFileSync/Source/base/db_file.cpp index 16638fb6..7062a250 100644 --- a/FreeFileSync/Source/base/db_file.cpp +++ b/FreeFileSync/Source/base/db_file.cpp @@ -127,16 +127,10 @@ DbStreams loadStreams(const AbstractPath& dbPath, const IOCallback& notifyUnbuff throw FileError(_("Database file is corrupted:") + L' ' + fmtPath(AFS::getDisplayPath(dbPath)), L"Invalid header."); const int version = readNumber(memStreamIn); //throw UnexpectedEndOfStreamError - if (version != 9 && //TODO: remove migration code at some time! v9 used until 2017-02-01 - version != 10 && //TODO: remove migration code at some time! v10 used until 2020-02-07 - version != DB_FILE_VERSION) - throw FileError(replaceCpy(_("Database file %x is incompatible."), L"%x", fmtPath(AFS::getDisplayPath(dbPath))), - replaceCpy(_("Version: %x"), L"%x", numberTo(version))); - if (version == 9 || //TODO: remove migration code at some time! v9 used until 2017-02-01 version == 10) //TODO: remove migration code at some time! v10 used until 2020-02-07 ; - else //catch data corruption ASAP + don't rely on std::bad_alloc for consistency checking + 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! @@ -146,6 +140,9 @@ DbStreams loadStreams(const AbstractPath& dbPath, const IOCallback& notifyUnbuff if (!endsWith(byteStream, crcStreamOut.ref())) throw FileError(_("Database file is corrupted:") + L' ' + fmtPath(AFS::getDisplayPath(dbPath)), L"Invalid checksum."); } + else + throw FileError(replaceCpy(_("Database file %x is incompatible."), L"%x", fmtPath(AFS::getDisplayPath(dbPath))), + replaceCpy(_("Version: %x"), L"%x", numberTo(version))); DbStreams output; @@ -342,12 +339,6 @@ public: if (streamVersion != streamVersionR) throw FileError(_("Database file is corrupted:") + L'\n' + fmtPath(displayFilePathL) + L'\n' + fmtPath(displayFilePathR), L"Different stream formats"); - //TODO: remove migration code at some time! 2017-02-01 - if (streamVersion != 2 && - streamVersion != DB_STREAM_VERSION) - throw FileError(replaceCpy(_("Database file %x is incompatible."), L"%x", fmtPath(displayFilePathL)), - L"Unsupported stream format: " + numberTo(streamVersion)); - //TODO: remove migration code at some time! 2017-02-01 if (streamVersion == 2) { @@ -379,7 +370,7 @@ public: parser.recurse(output.ref()); //throw UnexpectedEndOfStreamError return output; } - else + else if (streamVersion == DB_STREAM_VERSION) { MemoryStreamIn& streamInPart1 = leadStreamLeft ? streamInL : streamInR; MemoryStreamIn& streamInPart2 = leadStreamLeft ? streamInR : streamInL; @@ -407,6 +398,9 @@ public: parser.recurse(output.ref()); //throw UnexpectedEndOfStreamError return output; } + else + throw FileError(replaceCpy(_("Database file %x is incompatible."), L"%x", fmtPath(displayFilePathL)), + L"Unsupported stream format: " + numberTo(streamVersion)); } catch (UnexpectedEndOfStreamError&) { diff --git a/FreeFileSync/Source/base/dir_exist_async.h b/FreeFileSync/Source/base/dir_exist_async.h index ff431902..93a4aa96 100644 --- a/FreeFileSync/Source/base/dir_exist_async.h +++ b/FreeFileSync/Source/base/dir_exist_async.h @@ -71,7 +71,7 @@ FolderStatus getFolderStatusNonBlocking(const std::set& folderPath => 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 static_cast(AFS::itemStillExists(folderPath)); //throw FileError - //consider ItemType::FILE a failure instead? Meanwhile: return "false" IFF nothing (of any type) exists + //consider ItemType::file a failure instead? Meanwhile: return "false" IFF nothing (of any type) exists }); auto fut = pt.get_future(); threadGroup.run(std::move(pt)); diff --git a/FreeFileSync/Source/base/parallel_scan.cpp b/FreeFileSync/Source/base/parallel_scan.cpp index 05a28e15..13ff16e2 100644 --- a/FreeFileSync/Source/base/parallel_scan.cpp +++ b/FreeFileSync/Source/base/parallel_scan.cpp @@ -272,7 +272,6 @@ void DirCallback::onFile(const AFS::FileInfo& fi) //throw ThreadInterruption interruptionPoint(); //throw ThreadInterruption const Zstring& relPath = parentRelPathPf_ + fi.itemName; - assert(!fi.symlinkInfo || fi.symlinkInfo->itemName == fi.itemName); //update status information no matter whether item is excluded or not! if (cfg_.acb.mayReportCurrentFile(cfg_.threadIdx, cfg_.lastReportTime)) @@ -295,7 +294,7 @@ void DirCallback::onFile(const AFS::FileInfo& fi) //throw ThreadInterruption Linux: retrieveFileID takes about 50% longer in VM! (avoidable because of redundant stat() call!) */ - output_.addSubFile(fi.itemName, FileAttributes(fi.modTime, fi.fileSize, fi.fileId, fi.symlinkInfo != nullptr)); + output_.addSubFile(fi.itemName, FileAttributes(fi.modTime, fi.fileSize, fi.fileId, fi.isFollowedSymlink )); cfg_.acb.incItemsScanned(); //add 1 element to the progress indicator } @@ -306,7 +305,6 @@ std::shared_ptr DirCallback::onFolder(const AFS::FolderI interruptionPoint(); //throw ThreadInterruption const Zstring& relPath = parentRelPathPf_ + fi.itemName; - assert(!fi.symlinkInfo || fi.symlinkInfo->itemName == fi.itemName); //update status information no matter whether item is excluded or not! if (cfg_.acb.mayReportCurrentFile(cfg_.threadIdx, cfg_.lastReportTime)) @@ -320,7 +318,7 @@ std::shared_ptr DirCallback::onFolder(const AFS::FolderI return nullptr; //do NOT traverse subdirs //else: attention! ensure directory filtering is applied later to exclude actually filtered directories - FolderContainer& subFolder = output_.addSubFolder(fi.itemName, FolderAttributes(fi.symlinkInfo != nullptr)); + FolderContainer& subFolder = output_.addSubFolder(fi.itemName, FolderAttributes(fi.isFollowedSymlink)); if (passFilter) cfg_.acb.incItemsScanned(); //add 1 element to the progress indicator diff --git a/FreeFileSync/Source/base/synchronization.cpp b/FreeFileSync/Source/base/synchronization.cpp index 7c4c4244..e3083158 100644 --- a/FreeFileSync/Source/base/synchronization.cpp +++ b/FreeFileSync/Source/base/synchronization.cpp @@ -1577,8 +1577,8 @@ void FolderPairSyncer::synchronizeFileInt(FilePair& file, SyncOperation syncOp) //update FilePair assert(fileFrom->getFileSize() == fileTo->getFileSize()); - fileTo->setSyncedTo(fileTo->getItemName(), - fileTo->getFileSize(), + fileTo->setSyncedTo(fileTo ->getItemName(), + fileTo ->getFileSize(), fileFrom->getLastWriteTime(), fileTo ->getLastWriteTime(), fileFrom->getFileId(), @@ -1887,7 +1887,7 @@ void FolderPairSyncer::synchronizeFolderInt(FolderPair& folder, SyncOperation sy catch (FileError&) { bool folderAlreadyExists = false; - try { folderAlreadyExists = parallel::getItemType(targetPath, singleThread_) == AFS::ItemType::FOLDER; } /*throw FileError*/ catch (FileError&) {} + 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) @@ -2329,9 +2329,9 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime if (!AFS::isNullPath(baseFolderPath)) try { - const int64_t freeSpace = AFS::getFreeDiskSpace(baseFolderPath); //throw FileError, returns 0 if not available + const int64_t freeSpace = AFS::getFreeDiskSpace(baseFolderPath); //throw FileError, returns < 0 if not available - if (0 < freeSpace && //zero means "request not supported" (e.g. see WebDav) + if (0 <= freeSpace && freeSpace < minSpaceNeeded) checkDiskSpaceMissing.push_back({ baseFolderPath, { minSpaceNeeded, freeSpace } }); } @@ -2344,7 +2344,7 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime checkSpace(baseFolder.getAbstractPath< LEFT_SIDE>(), spaceNeeded.first); checkSpace(baseFolder.getAbstractPath(), spaceNeeded.second); - //windows: check if recycle bin really exists; if not, Windows will silently delete, which is wrong + //Windows: check if recycle bin really exists; if not, Windows will silently delete, which is just wrong auto checkRecycler = [&](const AbstractPath& baseFolderPath) { assert(!AFS::isNullPath(baseFolderPath)); @@ -2521,24 +2521,24 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime const int exeptionCount = std::uncaught_exceptions(); ZEN_ON_SCOPE_EXIT ( - //*INDENT-OFF* + //*INDENT-OFF* if (!errorsModTime.empty()) - { - std::wstring msg; - for (const FileError& e : errorsModTime) - { - std::wstring singleMsg = replaceCpy(e.toString(), L"\n\n", L'\n'); - msg += singleMsg + L"\n\n"; - } - msg.resize(msg.size() - 2); - - const bool scopeFail = std::uncaught_exceptions() > exeptionCount; - if (!scopeFail) - callback.reportWarning(msg, warnings.warnModificationTimeError); //throw X - else //at least log warnings when sync is cancelled - try { callback.reportInfo(msg); /*throw X*/} catch (...) {}; - } - //*INDENT-ON* + { + std::wstring msg; + for (const FileError& e : errorsModTime) + { + std::wstring singleMsg = replaceCpy(e.toString(), L"\n\n", L'\n'); + msg += singleMsg + L"\n\n"; + } + msg.resize(msg.size() - 2); + + const bool scopeFail = std::uncaught_exceptions() > exeptionCount; + if (!scopeFail) + callback.reportWarning(msg, warnings.warnModificationTimeError); //throw X + else //at least log warnings when sync is cancelled + try { callback.reportInfo(msg); /*throw X*/} catch (...) {}; + } + //*INDENT-ON* ); //---------------------------------------------------------------------------------------------- class PcbNoThrow : public PhaseCallback diff --git a/FreeFileSync/Source/base/versioning.cpp b/FreeFileSync/Source/base/versioning.cpp index ee9f62a2..3f7a6176 100644 --- a/FreeFileSync/Source/base/versioning.cpp +++ b/FreeFileSync/Source/base/versioning.cpp @@ -113,7 +113,7 @@ void moveExistingItemToVersioning(const AbstractPath& sourcePath, const Abstract 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 + //overwrite AFS::ItemType::folder with FILE? => highly dubious, do not allow auto fixTargetPathIssues = [&](const FileError& prevEx) //throw FileError { @@ -183,7 +183,7 @@ void FileVersioner::revisionFile(const FileDescriptor& fileDescr, const Zstring& { if (std::optional type = AFS::itemStillExists(fileDescr.path)) //throw FileError { - if (*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 @@ -245,7 +245,7 @@ void FileVersioner::revisionFolder(const AbstractPath& folderPath, const Zstring //no error situation if directory is not existing! manual deletion relies on it! if (std::optional type = AFS::itemStillExists(folderPath)) //throw FileError { - 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! + 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 @@ -267,7 +267,7 @@ void FileVersioner::revisionFolderImpl(const AbstractPath& folderPath, const Zst std::vector symlinks; AFS::traverseFolderFlat(folderPath, //throw FileError - [&](const AFS::FileInfo& fi) { files .push_back(fi); assert(!files.back().symlinkInfo); }, + [&](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); }); diff --git a/FreeFileSync/Source/config.cpp b/FreeFileSync/Source/config.cpp index f69d200e..222bd82a 100644 --- a/FreeFileSync/Source/config.cpp +++ b/FreeFileSync/Source/config.cpp @@ -21,7 +21,7 @@ using namespace fff; //functionally needed for correct overload resolution!!! namespace { //------------------------------------------------------------------------------------------------------------------------------- -const int XML_FORMAT_GLOBAL_CFG = 17; //2020-04-15 +const int XML_FORMAT_GLOBAL_CFG = 18; //2020-06-13 const int XML_FORMAT_SYNC_CFG = 16; //2020-04-24 //------------------------------------------------------------------------------------------------------------------------------- } @@ -1856,6 +1856,14 @@ void readConfig(const XmlIn& in, XmlGlobalSettings& cfg, int formatVer) replace(item.cmdLine, Zstr("%folder_path2%"), Zstr("%parent_path2%")); } + //TODO: remove after migration! 2020-06-13 + if (formatVer < 18) + for (ExternalApp& item : cfg.gui.externalApps) + { + trim(item.cmdLine); + if (item.cmdLine == "xdg-open \"%parent_path%\"") + item.cmdLine = "xdg-open \"$(dirname \"%local_path%\")\""; + } //last update check inGui["LastOnlineCheck" ](cfg.gui.lastUpdateCheck); @@ -2226,7 +2234,7 @@ void writeConfig(const XmlGlobalSettings& cfg, XmlOut& out) outWnd.attribute("Maximized", cfg.gui.mainDlg.isMaximized); //########################################################### - outWnd["SearchPanel" ].attribute("CaseSensitive", cfg.gui.mainDlg.textSearchRespectCase); + outWnd["SearchPanel"].attribute("CaseSensitive", cfg.gui.mainDlg.textSearchRespectCase); //########################################################### XmlOut outConfig = outWnd["ConfigPanel"]; diff --git a/FreeFileSync/Source/config.h b/FreeFileSync/Source/config.h index 72bc3990..58f26d99 100644 --- a/FreeFileSync/Source/config.h +++ b/FreeFileSync/Source/config.h @@ -224,7 +224,8 @@ struct XmlGlobalSettings /* CONTRACT: first entry: show item in file browser default external app descriptions will be translated "on the fly"!!! */ - { L"Browse directory", "xdg-open \"%parent_path%\"" }, + //"xdg-open \"%parent_path%\"" -> not good enough: we need %local_path% for proper MTP/Google Drive handling + { L"Browse directory", "xdg-open \"$(dirname \"%local_path%\")\"" }, { L"Open with default application", "xdg-open \"%local_path%\"" }, //mark for extraction: _("Browse directory") Linux doesn't use the term "folder" }; diff --git a/FreeFileSync/Source/icon_buffer.cpp b/FreeFileSync/Source/icon_buffer.cpp index 8ee08396..9ae3ce83 100644 --- a/FreeFileSync/Source/icon_buffer.cpp +++ b/FreeFileSync/Source/icon_buffer.cpp @@ -24,7 +24,7 @@ namespace const size_t BUFFER_SIZE_MAX = 800; //maximum number of icons to hold in buffer: must be big enough to hold visible icons + preload buffer! Consider OS limit on GDI resources (wxBitmap)!!! -//destroys raw icon! Call from GUI thread only! +//invalidates image holder! call from GUI thread only! wxBitmap extractWxBitmap(ImageHolder&& ih) { assert(runningMainThread()); diff --git a/FreeFileSync/Source/localization.cpp b/FreeFileSync/Source/localization.cpp index 064de824..f3c751f8 100644 --- a/FreeFileSync/Source/localization.cpp +++ b/FreeFileSync/Source/localization.cpp @@ -102,6 +102,7 @@ FFSTranslation::FFSTranslation(const std::string& lngStream) //throw lng::Parsin std::vector loadTranslations() { const Zstring& zipPath = getResourceDirPf() + Zstr("Languages.zip"); + std::vector> streams; try //to load from ZIP first: @@ -111,7 +112,8 @@ std::vector loadTranslations() wxZipInputStream zipStream(memStream, wxConvUTF8); while (const auto& entry = std::unique_ptr(zipStream.GetNextEntry())) //take ownership! - if (std::string stream(entry->GetSize(), '\0'); !stream.empty() && zipStream.ReadAll(&stream[0], stream.size())) + if (std::string stream(entry->GetSize(), '\0'); + !stream.empty() && zipStream.ReadAll(&stream[0], stream.size())) streams.emplace_back(utfTo(entry->GetName()), std::move(stream)); else assert(false); diff --git a/FreeFileSync/Source/log_file.h b/FreeFileSync/Source/log_file.h index 5eec06ad..424f019e 100644 --- a/FreeFileSync/Source/log_file.h +++ b/FreeFileSync/Source/log_file.h @@ -24,7 +24,6 @@ enum class LogFileFormat text }; - AbstractPath generateLogFilePath(LogFileFormat logFormat, const ProcessSummary& summary, const Zstring& altLogFolderPathPhrase /*optional*/); void saveLogFile(const AbstractPath& logFilePath, //throw FileError, X diff --git a/FreeFileSync/Source/ui/abstract_folder_picker.cpp b/FreeFileSync/Source/ui/abstract_folder_picker.cpp index 0d5276d9..c18c51fc 100644 --- a/FreeFileSync/Source/ui/abstract_folder_picker.cpp +++ b/FreeFileSync/Source/ui/abstract_folder_picker.cpp @@ -167,7 +167,7 @@ struct FlatTraverserCallback : public AFS::TraverserCallback private: void onFile (const AFS::FileInfo& fi) override {} - std::shared_ptr onFolder (const AFS::FolderInfo& fi) override { result_.folderNames.emplace(fi.itemName, fi.symlinkInfo != nullptr); return nullptr; } + 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 LINK_FOLLOW; } HandleError reportDirError (const std::wstring& msg, size_t retryNumber) override { logError(msg); return ON_ERROR_CONTINUE; } HandleError reportItemError(const std::wstring& msg, size_t retryNumber, const Zstring& itemName) override { logError(msg); return ON_ERROR_CONTINUE; } @@ -292,7 +292,7 @@ void AbstractFolderPickerDlg::findAndNavigateToExistingPath(const AbstractPath& 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 + (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? @@ -335,7 +335,7 @@ void AbstractFolderPickerDlg::navigateToExistingPath(const wxTreeItemId& 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 ? + static_cast(childFolderRelPath.empty() && leafType == AFS::ItemType::symlink ? TreeNodeImage::folderSymlink : TreeNodeImage::folder), -1, new AfsTreeItemData(childFolderPath)); m_treeCtrlFileSystem->SetItemHasChildren(childIdMatch); diff --git a/FreeFileSync/Source/ui/app_icon.h b/FreeFileSync/Source/ui/app_icon.h index b4178a04..81ada20c 100644 --- a/FreeFileSync/Source/ui/app_icon.h +++ b/FreeFileSync/Source/ui/app_icon.h @@ -19,7 +19,7 @@ wxIcon getFfsIcon() using namespace zen; //wxWidgets' bitmap to icon conversion on macOS can only deal with very specific sizes => check on all platforms! assert(getResourceImage("FreeFileSync").GetWidth () == getResourceImage("FreeFileSync").GetHeight() && - getResourceImage("FreeFileSync").GetWidth() == 128); + getResourceImage("FreeFileSync").GetWidth() == fastFromDIP(128)); wxIcon icon; //Ubuntu-Linux does a bad job at down-scaling in Unity dash (blocky icons!) => prepare: icon.CopyFromBitmap(getResourceImage("FreeFileSync").ConvertToImage().Scale(fastFromDIP(64), fastFromDIP(64), wxIMAGE_QUALITY_HIGH)); //no discernable difference bewteen wxIMAGE_QUALITY_HIGH/wxIMAGE_QUALITY_BILINEAR in this case diff --git a/FreeFileSync/Source/ui/cfg_grid.cpp b/FreeFileSync/Source/ui/cfg_grid.cpp index 6229be7b..0e225f7a 100644 --- a/FreeFileSync/Source/ui/cfg_grid.cpp +++ b/FreeFileSync/Source/ui/cfg_grid.cpp @@ -226,7 +226,7 @@ void ConfigView::sortListViewImpl() //pre-sort by name std::sort(cfgListView_.begin(), cfgListView_.end(), lessCfgName); - //aggregate groups by color + //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(), diff --git a/FreeFileSync/Source/ui/file_grid.cpp b/FreeFileSync/Source/ui/file_grid.cpp index 412e1945..01e4a037 100644 --- a/FreeFileSync/Source/ui/file_grid.cpp +++ b/FreeFileSync/Source/ui/file_grid.cpp @@ -152,39 +152,35 @@ private: class GridDataBase : public GridData { public: - GridDataBase(Grid& grid, const std::shared_ptr& gridDataView) : grid_(grid), gridDataView_(gridDataView) {} + GridDataBase(Grid& grid, const SharedRef& gridDataView) : grid_(grid), gridDataView_(gridDataView) {} void holdOwnership(const std::shared_ptr& evtMgr) { evtMgr_ = evtMgr; } + GridEventManager* getEventManager() { return evtMgr_.get(); } - FileView& getDataView() { return *gridDataView_; } + /**/ FileView& getDataView() { return gridDataView_.ref(); } + const FileView& getDataView() const { return gridDataView_.ref(); } protected: - Grid& refGrid() { return grid_; } + /**/ + Grid& refGrid() { return grid_; } const Grid& refGrid() const { return grid_; } - const FileView* getGridDataView() const { return gridDataView_.get(); } - - const FileSystemObject* getRawData(size_t row) const - { - if (const FileView* view = getGridDataView()) - return view->getObject(row); - return nullptr; - } + const FileSystemObject* getFsObject(size_t row) const { return getDataView().getFsObject(row); } private: size_t getRowCount() const override { - if (!gridDataView_ || gridDataView_->rowsTotal() == 0) + if (gridDataView_.ref().rowsTotal() == 0) return ROW_COUNT_IF_NO_DATA; - return gridDataView_->rowsOnView(); + return gridDataView_.ref().rowsOnView(); //return std::max(MIN_ROW_COUNT, gridDataView_ ? gridDataView_->rowsOnView() : 0); } std::shared_ptr evtMgr_; Grid& grid_; - const std::shared_ptr gridDataView_; + SharedRef gridDataView_; }; //######################################################################################################## @@ -193,7 +189,7 @@ template class GridDataRim : public GridDataBase { public: - GridDataRim(const std::shared_ptr& gridDataView, Grid& grid) : GridDataBase(grid, gridDataView) {} + GridDataRim(const SharedRef& gridDataView, Grid& grid) : GridDataBase(grid, gridDataView) {} void setIconManager(const std::shared_ptr& iconMgr) { iconMgr_ = iconMgr; } @@ -236,9 +232,8 @@ public: const ptrdiff_t currentRow = rowsOnScreen.first + getAlternatingPos(i, visibleRowCount); if (isFailedLoad(currentRow)) //find failed attempts to load icon - { - const IconInfo ii = getIconInfo(currentRow); - if (ii.type == IconInfo::ICON_PATH) + if (const IconInfo ii = getIconInfo(currentRow); + ii.type == IconInfo::ICON_PATH) { //test if they are already loaded in buffer: if (iconMgr_->refIconBuffer().readyForRetrieval(ii.fsObj->template getAbstractPath())) @@ -250,7 +245,6 @@ public: else //not yet in buffer: mark for async. loading newLoad.push_back(ii.fsObj->template getAbstractPath()); } - } } } } @@ -279,19 +273,20 @@ protected: { if (enabled && !selected) { - //alternate background color to improve readability (while lacking cell borders) - if (getRowDisplayType(row) == DisplayType::NORMAL) + if (const DisplayType dispTp = getRowDisplayType(row); + dispTp == DisplayType::NORMAL) + //alternate background color to improve readability (while lacking cell borders) fillBackgroundDefaultColorAlternating(dc, rect, row % 2 == 0); else + { clearArea(dc, rect, getBackGroundColor(row)); - //draw horizontal border if required - DisplayType dispTp = getRowDisplayType(row); - if (dispTp != DisplayType::NORMAL && - dispTp == getRowDisplayType(row + 1)) - { - wxDCPenChanger dummy2(dc, getColorGridLine()); - dc.DrawLine(rect.GetBottomLeft(), rect.GetBottomRight() + wxPoint(1, 0)); + //draw horizontal border if required + if (dispTp == getRowDisplayType(row + 1)) + { + wxDCPenChanger dummy2(dc, getColorGridLine()); + dc.DrawLine(rect.GetBottomLeft(), rect.GetBottomRight() + wxPoint(1, 0)); + } } } else @@ -328,7 +323,7 @@ private: DisplayType getRowDisplayType(size_t row) const { - const FileSystemObject* fsObj = getRawData(row); + const FileSystemObject* fsObj = getFsObject(row); if (!fsObj ) return DisplayType::NORMAL; @@ -350,300 +345,377 @@ private: std::wstring getValue(size_t row, ColumnType colType) const override { - if (const FileSystemObject* fsObj = getRawData(row)) + std::wstring value; + if (const FileSystemObject* fsObj = getFsObject(row)) + if (!fsObj->isEmpty()) + switch (static_cast(colType)) + { + case ColumnTypeRim::ITEM_PATH: + switch (itemPathFormat_) + { + case ItemPathFormat::FULL_PATH: + return AFS::getDisplayPath(fsObj->getAbstractPath()); + case ItemPathFormat::RELATIVE_PATH: + return utfTo(fsObj->getRelativePath()); + case ItemPathFormat::ITEM_NAME: + return utfTo(fsObj->getItemName()); + } + assert(false); + break; + + case ColumnTypeRim::SIZE: + visitFSObject(*fsObj, [&](const FolderPair& folder) { value = L"<" + _("Folder") + L">"; }, + [&](const FilePair& file) { value = formatNumber(file.getFileSize()); }, + //[&](const FilePair& file) { value = utfTo(file.getFileId()); }, // -> 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; + } + + 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) { - const ColumnTypeRim colTypeRim = static_cast(colType); + wxDCTextColourChanger textColor(dc); + if (!pdi.fsObj->isActive()) + textColor.Set(wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT)); + else if (getRowDisplayType(row) != DisplayType::NORMAL) + textColor.Set(*wxBLACK); //accessibility: always set both foreground AND background colors! - std::wstring value; - visitFSObject(*fsObj, [&](const FolderPair& folder) + wxRect rectTmp = rect; + + switch (static_cast(colType)) { - value = [&] + case ColumnTypeRim::ITEM_PATH: { - if (folder.isEmpty()) - return std::wstring(); + const bool isTopRow = row == refGrid().getTopRow(); - switch (colTypeRim) - { - case ColumnTypeRim::ITEM_PATH: - switch (itemPathFormat_) - { - case ItemPathFormat::FULL_PATH: - return AFS::getDisplayPath(folder.getAbstractPath()); - case ItemPathFormat::RELATIVE_PATH: - return utfTo(folder.getRelativePath()); - case ItemPathFormat::ITEM_NAME: - return utfTo(folder.getItemName()); - } - break; - case ColumnTypeRim::SIZE: - return L"<" + _("Folder") + L">"; - case ColumnTypeRim::DATE: - return std::wstring(); - case ColumnTypeRim::EXTENSION: - return std::wstring(); - } - assert(false); - return std::wstring(); - }(); - }, + std::wstring itemName; + if (!pdi.fsObj->isEmpty()) + itemName = utfTo(pdi.fsObj->getItemName()); - [&](const FilePair& file) - { - value = [&] - { - if (file.isEmpty()) - return std::wstring(); + std::vector parentComponents; //excluding leaf component + std::span pathDrawInfo; //including leaf component - switch (colTypeRim) + switch (itemPathFormat_) { - case ColumnTypeRim::ITEM_PATH: - switch (itemPathFormat_) + case ItemPathFormat::FULL_PATH: + for (const FileSystemObject* fsObj2 = pdi.fsObj;;) { - case ItemPathFormat::FULL_PATH: - return AFS::getDisplayPath(file.getAbstractPath()); - case ItemPathFormat::RELATIVE_PATH: - return utfTo(file.getRelativePath()); - case ItemPathFormat::ITEM_NAME: - return utfTo(file.getItemName()); + const ContainerObject& parent = fsObj2->parent(); + parentComponents.push_back(&parent); + + fsObj2 = dynamic_cast(&parent); + if (!fsObj2) + break; } - break; - case ColumnTypeRim::SIZE: - //return utfTo(file.getFileId()); // -> test file id - return formatNumber(file.getFileSize()); - case ColumnTypeRim::DATE: - return formatUtcToLocalTime(file.getLastWriteTime()); - case ColumnTypeRim::EXTENSION: - return utfTo(getFileExtension(file.getItemName())); - } - assert(false); - return std::wstring(); - }(); - }, + std::reverse(parentComponents.begin(), parentComponents.end()); - [&](const SymlinkPair& symlink) - { - value = [&] - { - if (symlink.isEmpty()) - return std::wstring(); + assert(pdi.pathDrawInfo.size() == parentComponents.size() + 1); + pathDrawInfo = pdi.pathDrawInfo; + break; - switch (colTypeRim) - { - case ColumnTypeRim::ITEM_PATH: - switch (itemPathFormat_) + case ItemPathFormat::RELATIVE_PATH: + for (const FileSystemObject* fsObj2 = pdi.fsObj;;) { - case ItemPathFormat::FULL_PATH: - return AFS::getDisplayPath(symlink.getAbstractPath()); - case ItemPathFormat::RELATIVE_PATH: - return utfTo(symlink.getRelativePath()); - case ItemPathFormat::ITEM_NAME: - return utfTo(symlink.getItemName()); - } - break; - case ColumnTypeRim::SIZE: - return L"<" + _("Symlink") + L">"; - case ColumnTypeRim::DATE: - return formatUtcToLocalTime(symlink.getLastWriteTime()); - case ColumnTypeRim::EXTENSION: - return utfTo(getFileExtension(symlink.getItemName())); - } - assert(false); - return std::wstring(); - }(); - }); - return value; - } - //if data is not found: - return std::wstring(); - } + const ContainerObject& parent = fsObj2->parent(); - void renderCell(wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover) override - { - //don't forget to harmonize with getBestSize()!!! + fsObj2 = dynamic_cast(&parent); + if (!fsObj2) + break; + parentComponents.push_back(&parent); + } + std::reverse(parentComponents.begin(), parentComponents.end()); - const bool isActive = [&] - { - if (const FileSystemObject* fsObj = this->getRawData(row)) - return fsObj->isActive(); - return true; - }(); + assert(pdi.pathDrawInfo.size() == parentComponents.size() + 2); + if (!pdi.pathDrawInfo.empty()) + pathDrawInfo = pdi.pathDrawInfo.subspan(1); + break; - wxDCTextColourChanger textColor(dc); - if (!isActive) - textColor.Set(wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT)); - else if (getRowDisplayType(row) != DisplayType::NORMAL) - textColor.Set(*wxBLACK); //accessibility: always set both foreground AND background colors! + case ItemPathFormat::ITEM_NAME: + assert(!pdi.pathDrawInfo.empty()); + if (!pdi.pathDrawInfo.empty()) + pathDrawInfo = pdi.pathDrawInfo.subspan(pdi.pathDrawInfo.size() - 1); + break; + } - wxRect rectTmp = rect; + /* Partitioning: + _________________________________________________________________ + | (gap | component name | gap | dash)* | icon | gap | item name | + ----------------------------------------------------------------- */ - auto drawTextBlock = [&](const std::wstring& text) - { - rectTmp.x += gridGap_; - rectTmp.width -= gridGap_; - const wxSize extent = drawCellText(dc, rectTmp, text, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); - rectTmp.x += extent.GetWidth(); - rectTmp.width -= extent.GetWidth(); - }; - auto drawFilePath = [&](std::wstring path) - { - //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(path, slashBidi_) && !contains(path, bslashBidi_)); - replace(path, L"/", slashBidi_); - replace(path, L"\\", bslashBidi_); - drawTextBlock(path); - }; + //calculate parent component render details + std::vector> parentNameExtents; + parentNameExtents.reserve(parentComponents.size()); + int parentsRenderWidth = 0; //total width of rendering parent components + for (const ContainerObject* parent : parentComponents) + { + const FolderPair* folder = dynamic_cast(parent); + std::wstring compName = folder ? utfTo(folder->getItemName()) : + AFS::getDisplayPath(parent->getAbstractPath()); + const wxSize compExt = getTextExtentBuffered(dc, compName); - const std::wstring& cellValue = getValue(row, colType); + parentNameExtents.emplace_back(std::move(compName), compExt); + parentsRenderWidth += gridGap_ + compExt.GetWidth() + gridGap_ + compLineExtent_.GetWidth(); + } - switch (static_cast(colType)) - { - case ColumnTypeRim::ITEM_PATH: - { - if (!iconMgr_) - drawFilePath(cellValue); - else - { - auto it = cellValue.end(); - while (it != cellValue.begin()) //reverse iteration: 1. check 2. decrement 3. evaluate + //limit space for parent components: prioritize item name rendering! + int itemRenderWidth = 0; + if (!itemName.empty()) { - --it; - if (*it == L'\\' || *it == L'/') - { - ++it; - break; - } + if (iconMgr_) + itemRenderWidth += iconMgr_->refIconBuffer().getSize(); + itemRenderWidth += gridGap_ + getTextExtentBuffered(dc, itemName).GetWidth(); } - /*const */std::wstring pathPrefix(cellValue.begin(), it); - const std::wstring itemName(it, cellValue.end()); - if (!pathPrefix.empty()) - pathPrefix.pop_back(); //don't really need the trailing slash + wxRect rectParents = rectTmp; + rectParents.width = std::min(rectTmp.width - itemRenderWidth, parentsRenderWidth); - // Partitioning: - // __________________________________________________ - // | gap | path prefix | gap | icon | gap | item name | - // -------------------------------------------------- - if (!pathPrefix.empty()) - drawFilePath(pathPrefix); + wxRect rectItem = rectTmp; + rectItem.x += std::max(0, rectParents.width); + rectItem.width -= std::max(0, rectParents.width); - //draw file icon - rectTmp.x += gridGap_; - rectTmp.width -= gridGap_; - const int iconSize = iconMgr_->refIconBuffer().getSize(); - if (rectTmp.GetWidth() >= iconSize) + assert(pathDrawInfo.size() == parentComponents.size() + 1); + if (pathDrawInfo.size() == parentComponents.size() + 1 && rectParents.width > 0) { - //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!!! - iconMgr_->startIconUpdater(); + //clear background below components => harmonize with renderRowBackgound() + if (enabled && !selected) //clearArea() is surprisingly expensive => call just once! + clearArea(dc, rectParents, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); - const IconInfo ii = getIconInfo(row); + wxDCPenChanger dummy(dc, wxPen(dc.GetTextForeground() /*treat component lines like text*/, compLineExtent_.GetHeight())); - wxBitmap fileIcon; - switch (ii.type) + auto itPdi = pathDrawInfo.begin(); + for (const auto& [compName, compExt] : parentNameExtents) { - case IconInfo::FOLDER: - fileIcon = iconMgr_->getGenericDirIcon(); - break; + rectParents.x += gridGap_; + rectParents.width -= gridGap_; - case IconInfo::ICON_PATH: - if (std::optional tmpIco = iconMgr_->refIconBuffer().retrieveFileIcon(ii.fsObj->template getAbstractPath())) - fileIcon = *tmpIco; - else - { - setFailedLoad(row); //save status of failed icon load -> used for async. icon loading - //falsify only! we want to avoid writing incorrect success values when only partially updating the DC, e.g. when scrolling, - //see repaint behavior of ::ScrollWindow() function! - fileIcon = iconMgr_->refIconBuffer().getIconByExtension(ii.fsObj->template getItemName()); //better than nothing - } + if (rectParents.width <= 0) break; - case IconInfo::EMPTY: + if (*itPdi & FileView::PathDrawInfo::DRAW_COMPONENT || isTopRow) + drawCellText(dc, rectParents, compName, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, &compExt); //GridData::drawCellText() + + rectParents.x += compExt.GetWidth() + gridGap_; + rectParents.width -= compExt.GetWidth() + gridGap_; + + if (rectParents.width <= 0) break; + + ++itPdi; //start drawing connections of *next* component + + const wxPoint mid = rectParents.GetTopLeft() + wxPoint(0, rectParents.height / 2); + + if (*itPdi & FileView::PathDrawInfo::CONNECT_PREV) + dc.DrawLine(rectParents.GetTopLeft(), mid); + + if (*itPdi & FileView::PathDrawInfo::CONNECT_NEXT) //for some (fucking) reason, drawing from bottom to top flips + dc.DrawLine(mid, rectParents.GetBottomLeft() + wxPoint(0, 1)); //our input points, so always draw top -> down + + if (*itPdi & FileView::PathDrawInfo::DRAW_COMPONENT || isTopRow) + dc.DrawLine(mid, mid + wxPoint(compLineExtent_.GetWidth() + + //extend line to icon center in case icon is smaller than default + (itPdi == pathDrawInfo.end() - 1 && iconMgr_ && !itemName.empty() ? + iconMgr_->refIconBuffer().getSize() / 2 : 0), 0)); + + rectParents.x += compLineExtent_.GetWidth(); + rectParents.width -= compLineExtent_.GetWidth(); } + } - if (fileIcon.IsOk()) + if (!itemName.empty()) + { + if (iconMgr_) //draw file icon { - wxRect rectIcon = rectTmp; - rectIcon.width = iconSize; //support small thumbnail centering - - auto drawIcon = [&](const wxBitmap& icon) + if (parentComponents.empty()) { - if (isActive) - drawBitmapRtlNoMirror(dc, icon, rectIcon, wxALIGN_CENTER); - else - drawBitmapRtlNoMirror(dc, wxBitmap(icon.ConvertToImage().ConvertToGreyscale(1.0 / 3, 1.0 / 3, 1.0 / 3)), //treat all channels equally! - rectIcon, wxALIGN_CENTER); - }; + rectItem.x += gridGap_; + rectItem.width -= gridGap_; + } + if (rectItem.width > 0) + { + //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!!! + iconMgr_->startIconUpdater(); + + wxBitmap fileIcon; - drawIcon(fileIcon); + const IconInfo ii = getIconInfo(row); + switch (ii.type) + { + case IconInfo::FOLDER: + fileIcon = iconMgr_->getGenericDirIcon(); + break; + + case IconInfo::ICON_PATH: + if (std::optional tmpIco = iconMgr_->refIconBuffer().retrieveFileIcon(ii.fsObj->template getAbstractPath())) + fileIcon = *tmpIco; + else + { + setFailedLoad(row); //save status of failed icon load -> used for async. icon loading + //falsify only! we want to avoid writing incorrect success values when only partially updating the DC, e.g. when scrolling, + //see repaint behavior of ::ScrollWindow() function! + fileIcon = iconMgr_->refIconBuffer().getIconByExtension(ii.fsObj->template getItemName()); //better than nothing + } + break; + + case IconInfo::EMPTY: + break; + } - if (ii.drawAsLink) - drawIcon(iconMgr_->getLinkOverlayIcon()); + const int iconSize = iconMgr_->refIconBuffer().getSize(); + if (fileIcon.IsOk()) + { + wxRect rectIcon = rectItem; + rectIcon.width = iconSize; //support small thumbnail centering + + auto drawIcon = [&](const wxBitmap& icon) + { + if (pdi.fsObj->isActive()) + drawBitmapRtlNoMirror(dc, icon, rectIcon, wxALIGN_CENTER); + else + drawBitmapRtlNoMirror(dc, wxBitmap(icon.ConvertToImage().ConvertToGreyscale(1.0 / 3, 1.0 / 3, 1.0 / 3)), //treat all channels equally! + rectIcon, wxALIGN_CENTER); + }; + + drawIcon(fileIcon); + + if (ii.drawAsLink) + drawIcon(iconMgr_->getLinkOverlayIcon()); + } + rectItem.x += iconSize; + rectItem.width -= iconSize; + } } - } - rectTmp.x += iconSize; - rectTmp.width -= iconSize; - drawFilePath(itemName); - } - } - break; + rectItem.x += gridGap_; + rectItem.width -= gridGap_; - case ColumnTypeRim::SIZE: - if (refGrid().GetLayoutDirection() != wxLayout_RightToLeft) - { - rectTmp.width -= gridGap_; //have file size right-justified (but don't change for RTL languages) - drawCellText(dc, rectTmp, cellValue, wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL); + if (rectItem.width > 0) + { + const wxSize& itemExtent = getTextExtentBuffered(dc, itemName); + drawCellText(dc, rectItem, itemName, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, &itemExtent); + } + } } - else - drawTextBlock(cellValue); break; - case ColumnTypeRim::DATE: - case ColumnTypeRim::EXTENSION: - drawTextBlock(cellValue); - break; + case ColumnTypeRim::SIZE: + if (refGrid().GetLayoutDirection() != wxLayout_RightToLeft) + { + rectTmp.width -= gridGap_; //have file size right-justified (but don't change for RTL languages) + drawCellText(dc, rectTmp, getValue(row, colType), wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL); + } + else + { + rectTmp.x += gridGap_; + rectTmp.width -= gridGap_; + drawCellText(dc, rectTmp, getValue(row, colType), wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); + } + break; + + case ColumnTypeRim::DATE: + case ColumnTypeRim::EXTENSION: + rectTmp.x += gridGap_; + rectTmp.width -= gridGap_; + drawCellText(dc, rectTmp, getValue(row, colType), wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); + break; + } } } int getBestSize(wxDC& dc, size_t row, ColumnType colType) override { - // Partitioning: - // ________________________________________________________ - // | gap | path prefix | gap | icon | gap | item name | gap | - // -------------------------------------------------------- - - const std::wstring cellValue = getValue(row, colType); - - if (static_cast(colType) == ColumnTypeRim::ITEM_PATH && iconMgr_) + if (static_cast(colType) == ColumnTypeRim::ITEM_PATH) { - auto it = cellValue.end(); - while (it != cellValue.begin()) //reverse iteration: 1. check 2. decrement 3. evaluate + int bestSize = 0; + + if (const FileView::PathDrawInfo pdi = getDataView().getDrawInfo(row); + pdi.fsObj) { - --it; - if (*it == '\\' || *it == '/') + const std::wstring& itemName = utfTo(pdi.fsObj->getItemName()); //don't care if FileSystemObject::isEmpty() + std::vector parentComponents; + + switch (itemPathFormat_) { - ++it; - break; + case ItemPathFormat::FULL_PATH: + for (const FileSystemObject* fsObj2 = pdi.fsObj;;) + { + const ContainerObject& parent = fsObj2->parent(); + parentComponents.push_back(&parent); + + fsObj2 = dynamic_cast(&parent); + if (!fsObj2) + break; + } + break; + + case ItemPathFormat::RELATIVE_PATH: + for (const FileSystemObject* fsObj2 = pdi.fsObj;;) + { + const ContainerObject& parent = fsObj2->parent(); + + fsObj2 = dynamic_cast(&parent); + if (!fsObj2) + break; + parentComponents.push_back(&parent); + } + break; + + case ItemPathFormat::ITEM_NAME: + break; } - } - const std::wstring pathPrefix(cellValue.begin(), it); - const std::wstring itemName(it, cellValue.end()); - int bestSize = 0; - if (!pathPrefix.empty()) - bestSize += gridGap_ + dc.GetTextExtent(pathPrefix).GetWidth(); + /* Partitioning: + _______________________________________________________________________ + | (gap | component name | gap | dash)* | icon | gap | item name | gap | + ----------------------------------------------------------------------- */ + for (const ContainerObject* parent : parentComponents) + { + const FolderPair* folder = dynamic_cast(parent); + const std::wstring& compName = folder ? utfTo(folder->getItemName()) : + AFS::getDisplayPath(parent->getAbstractPath()); + + bestSize += gridGap_ + getTextExtentBuffered(dc, compName).GetWidth() + gridGap_ + compLineExtent_.GetWidth(); + } - bestSize += gridGap_ + iconMgr_->refIconBuffer().getSize(); - bestSize += gridGap_ + dc.GetTextExtent(itemName).GetWidth() + gridGap_; + if (iconMgr_) + { + if (parentComponents.empty()) + bestSize += gridGap_; + bestSize += iconMgr_->refIconBuffer().getSize(); + } + + bestSize += gridGap_ + getTextExtentBuffered(dc, itemName).GetWidth() + gridGap_ /*for best size*/; + } return bestSize; } else + { + const std::wstring cellValue = getValue(row, colType); return gridGap_ + dc.GetTextExtent(cellValue).GetWidth() + gridGap_; + } // + 1 pix for cell border line ? -> not used anymore! } @@ -684,14 +756,47 @@ private: drawColumnLabelText(dc, rectRemain, getColumnLabel(colType), enabled); //draw sort marker - if (const FileView* view = getGridDataView()) - if (auto sortInfo = view->getSortInfo()) - if (const ColumnTypeRim* sortType = std::get_if(&sortInfo->sortCol)) - if (*sortType == static_cast(colType) && sortInfo->onLeft == (side == LEFT_SIDE)) - { - const wxBitmap sortMarker = getResourceImage(sortInfo->ascending ? "sort_ascending" : "sort_descending"); - drawBitmapRtlNoMirror(dc, enabled ? sortMarker : sortMarker.ConvertToDisabled(), rectInner, wxALIGN_CENTER_HORIZONTAL); - } + if (auto sortInfo = getDataView().getSortInfo()) + if (const ColumnTypeRim* sortType = std::get_if(&sortInfo->sortCol)) + if (*sortType == static_cast(colType) && sortInfo->onLeft == (side == LEFT_SIDE)) + { + const wxBitmap sortMarker = getResourceImage(sortInfo->ascending ? "sort_ascending" : "sort_descending"); + drawBitmapRtlNoMirror(dc, enabled ? sortMarker : sortMarker.ConvertToDisabled(), rectInner, wxALIGN_CENTER_HORIZONTAL); + } + } + + std::wstring getToolTip(size_t row, ColumnType colType) const override + { + std::wstring toolTip; + + if (const FileSystemObject* fsObj = getFsObject(row)) + if (!fsObj->isEmpty()) + { + toolTip = getDataView().getEffectiveFolderPairCount() > 1 ? + AFS::getDisplayPath(fsObj->getAbstractPath()) : + utfTo(fsObj->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_); + + visitFSObject(*fsObj, [](const FolderPair& folder) {}, + [&](const FilePair& file) + { + toolTip += L'\n' + + _("Size:") + L' ' + formatFilesizeShort(file.getFileSize()) + L'\n' + + _("Date:") + L' ' + formatUtcToLocalTime(file.getLastWriteTime()); + }, + + [&](const SymlinkPair& symlink) + { + toolTip += L'\n' + + _("Date:") + L' ' + formatUtcToLocalTime(symlink.getLastWriteTime()); + }); + } + return toolTip; } struct IconInfo @@ -711,8 +816,8 @@ private: { IconInfo out; - const FileSystemObject* fsObj = getRawData(row); - if (fsObj && !fsObj->isEmpty()) + if (const FileSystemObject* fsObj = getFsObject(row); + fsObj && !fsObj->isEmpty()) { out.fsObj = fsObj; @@ -737,45 +842,26 @@ private: return out; } - std::wstring getToolTip(size_t row, ColumnType colType) const override + wxSize getTextExtentBuffered(wxDC& dc, const std::wstring& text) { - std::wstring toolTip; - - if (const FileSystemObject* fsObj = getRawData(row)) - if (!fsObj->isEmpty()) - { - toolTip = getGridDataView() && getGridDataView()->getFolderPairCount() > 1 ? - AFS::getDisplayPath(fsObj->getAbstractPath()) : - utfTo(fsObj->getRelativePath()); - - assert(!contains(toolTip, slashBidi_) && !contains(toolTip, bslashBidi_)); - replace(toolTip, L"/", slashBidi_); - replace(toolTip, L"\\", bslashBidi_); - - visitFSObject(*fsObj, [](const FolderPair& folder) {}, - [&](const FilePair& file) - { - toolTip += L'\n' + - _("Size:") + L' ' + formatFilesizeShort(file.getFileSize()) + L'\n' + - _("Date:") + L' ' + formatUtcToLocalTime(file.getLastWriteTime()); - }, - - [&](const SymlinkPair& symlink) - { - toolTip += L'\n' + - _("Date:") + L' ' + formatUtcToLocalTime(symlink.getLastWriteTime()); - }); - } - return toolTip; + auto& compExtentsBuf = getDataView().refCompExtentsBuf(); + //- shared between GridDataLeft/GridDataRight + //- only used for parent component names and file names on view => should not grow "too big" + //- cleaned up during FileView::setData() + + auto it = compExtentsBuf.find(text); + if (it == compExtentsBuf.end()) + it = compExtentsBuf.emplace(text, dc.GetTextExtent(text)).first; + return it->second; } const int gridGap_ = fastFromDIP(FILE_GRID_GAP_SIZE_DIP); + const wxSize compLineExtent_{ fastFromDIP(5), fastFromDIP(1) }; std::shared_ptr iconMgr_; //optional ItemPathFormat itemPathFormat_ = ItemPathFormat::FULL_PATH; - std::vector failedLoads_; //effectively a vector of size "number of rows" - std::optional renderBuf_; //avoid costs of recreating this temporary variable + 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"\\"; @@ -786,7 +872,7 @@ private: class GridDataLeft : public GridDataRim { public: - GridDataLeft(const std::shared_ptr& gridDataView, Grid& grid) : GridDataRim(gridDataView, grid) {} + GridDataLeft(const SharedRef& gridDataView, Grid& grid) : GridDataRim(gridDataView, grid) {} void setNavigationMarker(std::unordered_set&& markedFilesAndLinks, std::unordered_set&& markedContainer) @@ -805,27 +891,24 @@ private: { const bool markRow = [&] { - if (const FileSystemObject* fsObj = getRawData(row)) + if (const FileSystemObject* fsObj = getFsObject(row)) { if (contains(markedFilesAndLinks_, fsObj)) //mark files/links directly return true; if (auto folder = dynamic_cast(fsObj)) - { - if (contains(markedContainer_, folder)) //mark directories which *are* the given ContainerObject* + if (contains(markedContainer_, folder)) //mark folders which *are* the given ContainerObject* return true; - } - //mark all objects which have the ContainerObject as *any* matching ancestor - const ContainerObject* parent = &(fsObj->parent()); - for (;;) + //also mark all items with any matching ancestors + for (const FileSystemObject* fsObj2 = fsObj;;) { - if (contains(markedContainer_, parent)) + const ContainerObject& parent = fsObj2->parent(); + if (contains(markedContainer_, &parent)) return true; - if (auto folder = dynamic_cast(parent)) - parent = &(folder->parent()); - else + fsObj2 = dynamic_cast(&parent); + if (!fsObj2) break; } } @@ -835,8 +918,9 @@ private: if (markRow) { wxRect rectTmp = rect; - rectTmp.width /= 20; - dc.GradientFillLinear(rectTmp, getColorSelectionGradientFrom(), getBackGroundColor(row), wxEAST); + rectTmp.width = fastFromDIP(15); + rectTmp.x += rect.width - rectTmp.width; + dc.GradientFillLinear(rectTmp, getColorSelectionGradientFrom(), getBackGroundColor(row), wxWEST); } } } @@ -850,7 +934,7 @@ private: class GridDataRight : public GridDataRim { public: - GridDataRight(const std::shared_ptr& gridDataView, Grid& grid) : GridDataRim(gridDataView, grid) {} + GridDataRight(const SharedRef& gridDataView, Grid& grid) : GridDataRim(gridDataView, grid) {} }; //######################################################################################################## @@ -858,7 +942,7 @@ public: class GridDataCenter : public GridDataBase { public: - GridDataCenter(const std::shared_ptr& gridDataView, Grid& grid) : + GridDataCenter(const SharedRef& gridDataView, Grid& grid) : GridDataBase(grid, gridDataView), toolTip_(grid) {} //tool tip must not live longer than grid! @@ -880,7 +964,7 @@ public: switch (static_cast(rowHover)) { case HoverAreaCenter::CHECK_BOX: - if (const FileSystemObject* fsObj = getRawData(clickInitRow)) + if (const FileSystemObject* fsObj = getFsObject(clickInitRow)) { const bool setIncluded = !fsObj->isActive(); CheckRowsEvent evt(rowFirst, rowLast, setIncluded); @@ -940,7 +1024,7 @@ public: private: std::wstring getValue(size_t row, ColumnType colType) const override { - if (const FileSystemObject* fsObj = getRawData(row)) + if (const FileSystemObject* fsObj = getFsObject(row)) switch (static_cast(colType)) { case ColumnTypeCenter::CHECKBOX: @@ -957,7 +1041,7 @@ private: { if (enabled && !selected) { - if (const FileSystemObject* fsObj = getRawData(row)) + if (const FileSystemObject* fsObj = getFsObject(row)) { if (fsObj->isActive()) fillBackgroundDefaultColorAlternating(dc, rect, row % 2 == 0); @@ -990,7 +1074,7 @@ private: switch (static_cast(colType)) { case ColumnTypeCenter::CHECKBOX: - if (const FileSystemObject* fsObj = getRawData(row)) + if (const FileSystemObject* fsObj = getFsObject(row)) { const bool drawMouseHover = static_cast(rowHover) == HoverAreaCenter::CHECK_BOX; @@ -1002,7 +1086,7 @@ private: break; case ColumnTypeCenter::CMP_CATEGORY: - if (const FileSystemObject* fsObj = getRawData(row)) + if (const FileSystemObject* fsObj = getFsObject(row)) { if (!highlightSyncAction_) drawHighlightBackground(*fsObj, getBackGroundColorCmpCategory(fsObj)); @@ -1010,8 +1094,8 @@ private: wxRect rectTmp = rect; { //draw notch on left side - if (notch_.GetHeight() != rectTmp.GetHeight()) - notch_.Rescale(notch_.GetWidth(), rectTmp.GetHeight()); + if (notch_.GetHeight() != rectTmp.height) + notch_.Rescale(notch_.GetWidth(), 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 - notch_.GetWidth(), rectTmp.y, notch_.GetWidth(), rectTmp.height); @@ -1020,14 +1104,14 @@ private: } if (!highlightSyncAction_) - drawBitmapRtlMirror(dc, getCmpResultImage(fsObj->getCategory()), rectTmp, wxALIGN_CENTER, renderBuf_); + drawBitmapRtlMirror(dc, getCmpResultImage(fsObj->getCategory()), rectTmp, wxALIGN_CENTER, renderBufCmp_); else if (fsObj->getCategory() != FILE_EQUAL) //don't show = in both middle columns - drawBitmapRtlMirror(dc, greyScale(getCmpResultImage(fsObj->getCategory())), rectTmp, wxALIGN_CENTER, renderBuf_); + drawBitmapRtlMirror(dc, greyScale(getCmpResultImage(fsObj->getCategory())), rectTmp, wxALIGN_CENTER, renderBufCmp_); } break; case ColumnTypeCenter::SYNC_ACTION: - if (const FileSystemObject* fsObj = getRawData(row)) + if (const FileSystemObject* fsObj = getFsObject(row)) { if (highlightSyncAction_) drawHighlightBackground(*fsObj, getBackGroundColorSyncAction(fsObj)); @@ -1037,19 +1121,19 @@ private: switch (rowHoverCenter) { case HoverAreaCenter::DIR_LEFT: - drawBitmapRtlMirror(dc, getSyncOpImage(fsObj->testSyncOperation(SyncDirection::LEFT)), rect, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, renderBuf_); + drawBitmapRtlMirror(dc, getSyncOpImage(fsObj->testSyncOperation(SyncDirection::LEFT)), rect, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, renderBufSync_); break; case HoverAreaCenter::DIR_NONE: drawBitmapRtlNoMirror(dc, getSyncOpImage(fsObj->testSyncOperation(SyncDirection::NONE)), rect, wxALIGN_CENTER); break; case HoverAreaCenter::DIR_RIGHT: - drawBitmapRtlMirror(dc, getSyncOpImage(fsObj->testSyncOperation(SyncDirection::RIGHT)), rect, wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL, renderBuf_); + drawBitmapRtlMirror(dc, getSyncOpImage(fsObj->testSyncOperation(SyncDirection::RIGHT)), rect, wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL, renderBufSync_); break; case HoverAreaCenter::CHECK_BOX: if (highlightSyncAction_) - drawBitmapRtlMirror(dc, getSyncOpImage(fsObj->getSyncOperation()), rect, wxALIGN_CENTER, renderBuf_); + drawBitmapRtlMirror(dc, getSyncOpImage(fsObj->getSyncOperation()), rect, wxALIGN_CENTER, renderBufSync_); else if (fsObj->getSyncOperation() != SO_EQUAL) //don't show = in both middle columns - drawBitmapRtlMirror(dc, greyScale(getSyncOpImage(fsObj->getSyncOperation())), rect, wxALIGN_CENTER, renderBuf_); + drawBitmapRtlMirror(dc, greyScale(getSyncOpImage(fsObj->getSyncOperation())), rect, wxALIGN_CENTER, renderBufSync_); break; } } @@ -1059,7 +1143,7 @@ private: HoverArea getRowMouseHover(size_t row, ColumnType colType, int cellRelativePosX, int cellWidth) override { - if (const FileSystemObject* const fsObj = getRawData(row)) + if (const FileSystemObject* const fsObj = getFsObject(row)) switch (static_cast(colType)) { case ColumnTypeCenter::CHECKBOX: @@ -1128,19 +1212,18 @@ private: drawBitmapRtlNoMirror(dc, enabled ? colIcon : colIcon.ConvertToDisabled(), rectInner, wxALIGN_CENTER); //draw sort marker - if (const FileView* view = getGridDataView()) - if (auto sortInfo = view->getSortInfo()) - if (const ColumnTypeCenter* sortType = std::get_if(&sortInfo->sortCol)) - if (*sortType == colTypeCenter) - { - const int gapLeft = (rectInner.width + colIcon.GetWidth()) / 2; - wxRect rectRemain = rectInner; - rectRemain.x += gapLeft; - rectRemain.width -= gapLeft; + if (auto sortInfo = getDataView().getSortInfo()) + if (const ColumnTypeCenter* sortType = std::get_if(&sortInfo->sortCol)) + if (*sortType == colTypeCenter) + { + const int gapLeft = (rectInner.width + colIcon.GetWidth()) / 2; + wxRect rectRemain = rectInner; + rectRemain.x += gapLeft; + rectRemain.width -= gapLeft; - const wxBitmap sortMarker = getResourceImage(sortInfo->ascending ? "sort_ascending" : "sort_descending"); - drawBitmapRtlNoMirror(dc, enabled ? sortMarker : sortMarker.ConvertToDisabled(), rectRemain, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); - } + const wxBitmap sortMarker = getResourceImage(sortInfo->ascending ? "sort_ascending" : "sort_descending"); + drawBitmapRtlNoMirror(dc, enabled ? sortMarker : sortMarker.ConvertToDisabled(), rectRemain, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); + } } static wxColor getBackGroundColorSyncAction(const FileSystemObject* fsObj) @@ -1212,7 +1295,7 @@ private: void showToolTip(size_t row, ColumnTypeCenter colType, wxPoint posScreen) { - if (const FileSystemObject* fsObj = getRawData(row)) + if (const FileSystemObject* fsObj = getFsObject(row)) { switch (colType) { @@ -1223,22 +1306,17 @@ private: { const CompareFileResult cmpRes = fsObj->getCategory(); switch (cmpRes) - { - case FILE_LEFT_SIDE_ONLY: - return "cat_left_only"; - case FILE_RIGHT_SIDE_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"; + { + //*INDENT-OFF* + case FILE_LEFT_SIDE_ONLY: return "cat_left_only"; + case FILE_RIGHT_SIDE_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_EQUAL: - case FILE_DIFFERENT_METADATA: //= sub-category of equal - return "cat_equal"; - case FILE_CONFLICT: - return "cat_conflict"; + case FILE_DIFFERENT_METADATA: return "cat_equal"; //= sub-category of equal +case FILE_CONFLICT: return "cat_conflict"; +//*INDENT-ON* } assert(false); return ""; @@ -1248,61 +1326,49 @@ private: } break; - case ColumnTypeCenter::SYNC_ACTION: + case ColumnTypeCenter::SYNC_ACTION: + { + const char* imageName = [&] { - const char* imageName = [&] - { - const SyncOperation syncOp = fsObj->getSyncOperation(); - switch (syncOp) - { - case SO_CREATE_NEW_LEFT: - return "so_create_left"; - case SO_CREATE_NEW_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_COPY_METADATA_TO_LEFT: - return "so_move_left"; - case SO_COPY_METADATA_TO_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(getResourceImage(imageName)); - toolTip_.show(getSyncOpDescription(*fsObj), posScreen, &img); - } - break; + const SyncOperation syncOp = fsObj->getSyncOperation(); + switch (syncOp) + { + //*INDENT-OFF* + case SO_CREATE_NEW_LEFT: return "so_create_left"; + case SO_CREATE_NEW_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_COPY_METADATA_TO_LEFT: return "so_move_left"; + case SO_COPY_METADATA_TO_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"; + //*INDENT-ON* + }; + assert(false); + return ""; + }(); + const auto& img = mirrorIfRtl(getResourceImage(imageName)); + toolTip_.show(getSyncOpDescription(*fsObj), posScreen, &img); } - } - else - toolTip_.hide(); //if invalid row... + break; + } +} +else + toolTip_.hide(); //if invalid row... } bool highlightSyncAction_ = false; bool selectionInProgress_ = false; - std::optional renderBuf_; //avoid costs of recreating this temporary variable + std::optional renderBufCmp_; //avoid costs of recreating this temporary variable + std::optional renderBufSync_; Tooltip toolTip_; wxImage notch_ = getResourceImage("notch").ConvertToImage(); }; @@ -1315,67 +1381,67 @@ class GridEventManager : private wxEvtHandler { public: GridEventManager(Grid& gridL, - Grid& gridC, - Grid& gridR, - GridDataCenter& provCenter) : - gridL_(gridL), gridC_(gridC), gridR_(gridR), - provCenter_(provCenter) + Grid& gridC, + Grid& gridR, + GridDataCenter& provCenter) : +gridL_(gridL), gridC_(gridC), gridR_(gridR), +provCenter_(provCenter) { - gridL_.Connect(EVENT_GRID_COL_RESIZE, GridColumnResizeEventHandler(GridEventManager::onResizeColumnL), nullptr, this); - gridR_.Connect(EVENT_GRID_COL_RESIZE, GridColumnResizeEventHandler(GridEventManager::onResizeColumnR), nullptr, this); +gridL_.Connect(EVENT_GRID_COL_RESIZE, GridColumnResizeEventHandler(GridEventManager::onResizeColumnL), nullptr, this); +gridR_.Connect(EVENT_GRID_COL_RESIZE, GridColumnResizeEventHandler(GridEventManager::onResizeColumnR), nullptr, this); - gridL_.getMainWin().Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(GridEventManager::onKeyDownL), nullptr, this); - gridC_.getMainWin().Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(GridEventManager::onKeyDownC), nullptr, this); - gridR_.getMainWin().Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(GridEventManager::onKeyDownR), nullptr, this); +gridL_.getMainWin().Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(GridEventManager::onKeyDownL), nullptr, this); +gridC_.getMainWin().Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(GridEventManager::onKeyDownC), nullptr, this); +gridR_.getMainWin().Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(GridEventManager::onKeyDownR), nullptr, this); - gridC_.getMainWin().Connect(wxEVT_MOTION, wxMouseEventHandler(GridEventManager::onCenterMouseMovement), nullptr, this); - gridC_.getMainWin().Connect(wxEVT_LEAVE_WINDOW, wxMouseEventHandler(GridEventManager::onCenterMouseLeave ), nullptr, this); +gridC_.getMainWin().Connect(wxEVT_MOTION, wxMouseEventHandler(GridEventManager::onCenterMouseMovement), nullptr, this); +gridC_.getMainWin().Connect(wxEVT_LEAVE_WINDOW, wxMouseEventHandler(GridEventManager::onCenterMouseLeave ), nullptr, this); - gridC_.Connect(EVENT_GRID_MOUSE_LEFT_DOWN, GridClickEventHandler (GridEventManager::onCenterSelectBegin), nullptr, this); - gridC_.Connect(EVENT_GRID_SELECT_RANGE, GridSelectEventHandler(GridEventManager::onCenterSelectEnd ), nullptr, this); +gridC_.Connect(EVENT_GRID_MOUSE_LEFT_DOWN, GridClickEventHandler (GridEventManager::onCenterSelectBegin), nullptr, this); +gridC_.Connect(EVENT_GRID_SELECT_RANGE, GridSelectEventHandler(GridEventManager::onCenterSelectEnd ), nullptr, this); - //clear selection of other grid when selecting on - gridL_.Connect(EVENT_GRID_SELECT_RANGE, GridSelectEventHandler(GridEventManager::onGridSelectionL), nullptr, this); - gridR_.Connect(EVENT_GRID_SELECT_RANGE, GridSelectEventHandler(GridEventManager::onGridSelectionR), nullptr, this); +//clear selection of other grid when selecting on +gridL_.Connect(EVENT_GRID_SELECT_RANGE, GridSelectEventHandler(GridEventManager::onGridSelectionL), nullptr, this); +gridR_.Connect(EVENT_GRID_SELECT_RANGE, GridSelectEventHandler(GridEventManager::onGridSelectionR), nullptr, this); - //parallel grid scrolling: do NOT use DoPrepareDC() to align grids! GDI resource leak! Use regular paint event instead: - gridL_.getMainWin().Connect(wxEVT_PAINT, wxEventHandler(GridEventManager::onPaintGridL), nullptr, this); - gridC_.getMainWin().Connect(wxEVT_PAINT, wxEventHandler(GridEventManager::onPaintGridC), nullptr, this); - gridR_.getMainWin().Connect(wxEVT_PAINT, wxEventHandler(GridEventManager::onPaintGridR), nullptr, this); +//parallel grid scrolling: do NOT use DoPrepareDC() to align grids! GDI resource leak! Use regular paint event instead: +gridL_.getMainWin().Connect(wxEVT_PAINT, wxEventHandler(GridEventManager::onPaintGridL), nullptr, this); +gridC_.getMainWin().Connect(wxEVT_PAINT, wxEventHandler(GridEventManager::onPaintGridC), nullptr, this); +gridR_.getMainWin().Connect(wxEVT_PAINT, wxEventHandler(GridEventManager::onPaintGridR), nullptr, this); - auto connectGridAccess = [&](Grid& grid, wxObjectEventFunction func) - { - grid.Connect(wxEVT_SCROLLWIN_TOP, func, nullptr, this); - grid.Connect(wxEVT_SCROLLWIN_BOTTOM, func, nullptr, this); - grid.Connect(wxEVT_SCROLLWIN_LINEUP, func, nullptr, this); - grid.Connect(wxEVT_SCROLLWIN_LINEDOWN, func, nullptr, this); - grid.Connect(wxEVT_SCROLLWIN_PAGEUP, func, nullptr, this); - grid.Connect(wxEVT_SCROLLWIN_PAGEDOWN, func, nullptr, this); - grid.Connect(wxEVT_SCROLLWIN_THUMBTRACK, func, nullptr, this); - //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().Connect(wxEVT_CHAR, func, nullptr, this); - grid.getMainWin().Connect(wxEVT_KEY_UP, func, nullptr, this); - grid.getMainWin().Connect(wxEVT_KEY_DOWN, func, nullptr, this); - - grid.getMainWin().Connect(wxEVT_LEFT_DOWN, func, nullptr, this); - grid.getMainWin().Connect(wxEVT_LEFT_DCLICK, func, nullptr, this); - grid.getMainWin().Connect(wxEVT_RIGHT_DOWN, func, nullptr, this); - //grid.getMainWin().Connect(wxEVT_MOUSEWHEEL, func, nullptr, this); -> should be covered by wxEVT_SCROLLWIN_* - }; - connectGridAccess(gridL_, wxEventHandler(GridEventManager::onGridAccessL)); // - connectGridAccess(gridC_, wxEventHandler(GridEventManager::onGridAccessC)); //connect *after* onKeyDown() in order to receive callback *before*!!! - connectGridAccess(gridR_, wxEventHandler(GridEventManager::onGridAccessR)); // +auto connectGridAccess = [&](Grid& grid, wxObjectEventFunction func) +{ + grid.Connect(wxEVT_SCROLLWIN_TOP, func, nullptr, this); + grid.Connect(wxEVT_SCROLLWIN_BOTTOM, func, nullptr, this); + grid.Connect(wxEVT_SCROLLWIN_LINEUP, func, nullptr, this); + grid.Connect(wxEVT_SCROLLWIN_LINEDOWN, func, nullptr, this); + grid.Connect(wxEVT_SCROLLWIN_PAGEUP, func, nullptr, this); + grid.Connect(wxEVT_SCROLLWIN_PAGEDOWN, func, nullptr, this); + grid.Connect(wxEVT_SCROLLWIN_THUMBTRACK, func, nullptr, this); + //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().Connect(wxEVT_CHAR, func, nullptr, this); + grid.getMainWin().Connect(wxEVT_KEY_UP, func, nullptr, this); + grid.getMainWin().Connect(wxEVT_KEY_DOWN, func, nullptr, this); + + grid.getMainWin().Connect(wxEVT_LEFT_DOWN, func, nullptr, this); + grid.getMainWin().Connect(wxEVT_LEFT_DCLICK, func, nullptr, this); + grid.getMainWin().Connect(wxEVT_RIGHT_DOWN, func, nullptr, this); + //grid.getMainWin().Connect(wxEVT_MOUSEWHEEL, func, nullptr, this); -> should be covered by wxEVT_SCROLLWIN_* +}; +connectGridAccess(gridL_, wxEventHandler(GridEventManager::onGridAccessL)); // +connectGridAccess(gridC_, wxEventHandler(GridEventManager::onGridAccessC)); //connect *after* onKeyDown() in order to receive callback *before*!!! +connectGridAccess(gridR_, wxEventHandler(GridEventManager::onGridAccessR)); // - Connect(EVENT_ALIGN_SCROLLBARS, wxEventHandler(GridEventManager::onAlignScrollBars), NULL, this); +Connect(EVENT_ALIGN_SCROLLBARS, wxEventHandler(GridEventManager::onAlignScrollBars), NULL, this); } ~GridEventManager() { - //assert(!scrollbarUpdatePending_); => false-positives: e.g. start ffs, right-click on grid, close by clicking X +//assert(!scrollbarUpdatePending_); => false-positives: e.g. start ffs, right-click on grid, close by clicking X } void setScrollMaster(const Grid& grid) { scrollMaster_ = &grid; } @@ -1383,32 +1449,32 @@ public: private: void onCenterSelectBegin(GridClickEvent& event) { - provCenter_.onSelectBegin(); - event.Skip(); +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(); +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_.onMouseMovement(event.GetPosition()); - event.Skip(); +provCenter_.onMouseMovement(event.GetPosition()); +event.Skip(); } void onCenterMouseLeave(wxMouseEvent& event) { - provCenter_.onMouseLeave(); - event.Skip(); +provCenter_.onMouseLeave(); +event.Skip(); } void onGridSelectionL(GridSelectEvent& event) { onGridSelection(gridL_, gridR_); event.Skip(); } @@ -1416,8 +1482,8 @@ private: void onGridSelection(const Grid& grid, Grid& other) { - if (!wxGetKeyState(WXK_CONTROL)) //clear other grid unless user is holding CTRL - other.clearSelection(GridEventPolicy::DENY); //don't emit event, prevent recursion! +if (!wxGetKeyState(WXK_CONTROL)) //clear other grid unless user is holding CTRL + other.clearSelection(GridEventPolicy::DENY); //don't emit event, prevent recursion! } void onKeyDownL(wxKeyEvent& event) { onKeyDown(event, gridL_); } @@ -1426,42 +1492,42 @@ private: 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; - } +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(); +//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! - scrollMaster_ = &gridL_; //onKeyDown is called *after* onGridAccessL()! - return; //swallow event - - case WXK_RIGHT: - case WXK_NUMPAD_RIGHT: - gridR_.setGridCursor(row, GridEventPolicy::ALLOW); - gridR_.SetFocus(); - scrollMaster_ = &gridR_; - return; //swallow event - } +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! + scrollMaster_ = &gridL_; //onKeyDown is called *after* onGridAccessL()! + return; //swallow event + + case WXK_RIGHT: + case WXK_NUMPAD_RIGHT: + gridR_.setGridCursor(row, GridEventPolicy::ALLOW); + gridR_.SetFocus(); + scrollMaster_ = &gridR_; + return; //swallow event + } - event.Skip(); +event.Skip(); } void onResizeColumnL(GridColumnResizeEvent& event) { resizeOtherSide(gridL_, gridR_, event.colType_, event.offset_); } @@ -1469,23 +1535,23 @@ private: void resizeOtherSide(const Grid& src, Grid& trg, ColumnType type, int offset) { - //find stretch factor of resized column: type is unique due to makeConsistent()! - std::vector cfgSrc = src.getColumnConfig(); - auto it = std::find_if(cfgSrc.begin(), cfgSrc.end(), [&](Grid::ColAttributes& ca) { return ca.type == type; }); - 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 = trg.getColumnConfig(); - for (Grid::ColAttributes& ca : cfgTrg) - if (ca.type == type && ca.stretch == stretchSrc) - ca.offset = offset; - trg.setColumnConfig(cfgTrg); +//find stretch factor of resized column: type is unique due to makeConsistent()! +std::vector cfgSrc = src.getColumnConfig(); +auto it = std::find_if(cfgSrc.begin(), cfgSrc.end(), [&](Grid::ColAttributes& ca) { return ca.type == type; }); +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 = trg.getColumnConfig(); +for (Grid::ColAttributes& ca : cfgTrg) + if (ca.type == type && ca.stretch == stretchSrc) + ca.offset = offset; +trg.setColumnConfig(cfgTrg); } void onGridAccessL(wxEvent& event) { scrollMaster_ = &gridL_; event.Skip(); } @@ -1498,74 +1564,74 @@ private: void onPaintGrid(const Grid& grid) { - //align scroll positions of all three grids *synchronously* during paint event! (wxGTK has visible delay when this is done asynchronously, no delay on Windows) - - //determine lead grid - const Grid* lead = nullptr; - Grid* follow1 = nullptr; - Grid* follow2 = nullptr; - auto setGrids = [&](const Grid& l, Grid& f1, Grid& f2) { lead = &l; follow1 = &f1; follow2 = &f2; }; - - if (&gridC_ == scrollMaster_) - setGrids(gridC_, gridL_, gridR_); - else if (&gridR_ == scrollMaster_) - setGrids(gridR_, gridL_, gridC_); - else //default: left panel - setGrids(gridL_, gridC_, gridR_); - - //align other grids only while repainting the lead grid to avoid scrolling and updating a grid at the same time! - if (lead == &grid) - { - auto scroll = [](Grid& target, int y) //support polling - { - //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! - // an this while we're still in our WM_PAINT handler! => no recursion, fine (hopefully) - }; - int y = 0; - lead->GetViewStart(nullptr, &y); - scroll(*follow1, y); - scroll(*follow2, y); - } +//align scroll positions of all three grids *synchronously* during paint event! (wxGTK has visible delay when this is done asynchronously, no delay on Windows) + +//determine lead grid +const Grid* lead = nullptr; +Grid* follow1 = nullptr; +Grid* follow2 = nullptr; +auto setGrids = [&](const Grid& l, Grid& f1, Grid& f2) { lead = &l; follow1 = &f1; follow2 = &f2; }; + +if (&gridC_ == scrollMaster_) + setGrids(gridC_, gridL_, gridR_); +else if (&gridR_ == scrollMaster_) + setGrids(gridR_, gridL_, gridC_); +else //default: left panel + setGrids(gridL_, gridC_, gridR_); + +//align other grids only while repainting the lead grid to avoid scrolling and updating a grid at the same time! +if (lead == &grid) +{ + auto scroll = [](Grid& target, int y) //support polling + { + //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! + // an this while we're still in our WM_PAINT handler! => no recursion, fine (hopefully) + }; + int y = 0; + lead->GetViewStart(nullptr, &y); + scroll(*follow1, y); + scroll(*follow2, y); +} - //harmonize placement of horizontal scrollbar to avoid grids getting out of sync! - //since this affects the grid that is currently repainted as well, we do work asynchronously! - if (!scrollbarUpdatePending_) //send one async event at most, else they may accumulate and create perf issues, see grid.cpp - { - scrollbarUpdatePending_ = true; - wxCommandEvent alignEvent(EVENT_ALIGN_SCROLLBARS); - AddPendingEvent(alignEvent); //waits until next idle event - may take up to a second if the app is busy on wxGTK! - } +//harmonize placement of horizontal scrollbar to avoid grids getting out of sync! +//since this affects the grid that is currently repainted as well, we do work asynchronously! +if (!scrollbarUpdatePending_) //send one async event at most, else they may accumulate and create perf issues, see grid.cpp +{ + scrollbarUpdatePending_ = true; + wxCommandEvent alignEvent(EVENT_ALIGN_SCROLLBARS); + AddPendingEvent(alignEvent); //waits until next idle event - may take up to a second if the app is busy on wxGTK! +} } void onAlignScrollBars(wxEvent& event) { - assert(scrollbarUpdatePending_); - ZEN_ON_SCOPE_EXIT(scrollbarUpdatePending_ = false); +assert(scrollbarUpdatePending_); +ZEN_ON_SCOPE_EXIT(scrollbarUpdatePending_ = false); - auto needsHorizontalScrollbars = [](const Grid& grid) -> bool - { - const wxWindow& mainWin = grid.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 - }; +auto needsHorizontalScrollbars = [](const Grid& grid) -> bool +{ + const wxWindow& mainWin = grid.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); +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); } Grid& gridL_; @@ -1584,7 +1650,7 @@ private: void filegrid::init(Grid& gridLeft, Grid& gridCenter, Grid& gridRight) { - const auto gridDataView = std::make_shared(); + const auto gridDataView = makeSharedRef(); auto provLeft_ = std::make_shared(gridDataView, gridLeft); auto provCenter_ = std::make_shared(gridDataView, gridCenter); @@ -1615,9 +1681,9 @@ void filegrid::init(Grid& gridLeft, Grid& gridCenter, Grid& gridRight) gridCenter.setColumnConfig( { - { static_cast(ColumnTypeCenter::CHECKBOX ), widthCheckbox, 0, true }, - { static_cast(ColumnTypeCenter::CMP_CATEGORY), widthCategory, 0, true }, - { static_cast(ColumnTypeCenter::SYNC_ACTION ), widthAction, 0, true }, +{ static_cast(ColumnTypeCenter::CHECKBOX ), widthCheckbox, 0, true }, +{ static_cast(ColumnTypeCenter::CMP_CATEGORY), widthCategory, 0, true }, +{ static_cast(ColumnTypeCenter::SYNC_ACTION ), widthAction, 0, true }, }); } @@ -1625,7 +1691,7 @@ void filegrid::init(Grid& gridLeft, Grid& gridCenter, Grid& gridRight) FileView& filegrid::getDataView(Grid& grid) { if (auto* prov = dynamic_cast(grid.getDataProvider())) - return prov->getDataView(); +return prov->getDataView(); throw std::runtime_error("filegrid was not initialized! " + std::string(__FILE__) + ':' + numberTo(__LINE__)); } @@ -1638,7 +1704,7 @@ class IconUpdater : private wxEvtHandler //update file icons periodically: use S public: IconUpdater(GridDataLeft& provLeft, GridDataRight& provRight, IconBuffer& iconBuffer) : provLeft_(provLeft), provRight_(provRight), iconBuffer_(iconBuffer) { - timer_.Connect(wxEVT_TIMER, wxEventHandler(IconUpdater::loadIconsAsynchronously), nullptr, this); +timer_.Connect(wxEVT_TIMER, wxEventHandler(IconUpdater::loadIconsAsynchronously), nullptr, this); } void start() { if (!timer_.IsRunning()) timer_.Start(100); } //timer interval in [ms] @@ -1649,26 +1715,26 @@ private: void loadIconsAsynchronously(wxEvent& event) //loads all (not yet) drawn icons { - std::vector> prefetchLoad; - provLeft_ .getUnbufferedIconsForPreload(prefetchLoad); - provRight_.getUnbufferedIconsForPreload(prefetchLoad); +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; }); +//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); +//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); +provRight_.updateNewAndGetUnbufferedIcons(newLoad); +provLeft_ .updateNewAndGetUnbufferedIcons(newLoad); - iconBuffer_.setWorkload(newLoad); +iconBuffer_.setWorkload(newLoad); - if (newLoad.empty()) //let's only pay for IconUpdater while needed - stop(); +if (newLoad.empty()) //let's only pay for IconUpdater while needed + stop(); } GridDataLeft& provLeft_; @@ -1691,40 +1757,40 @@ void filegrid::setupIcons(Grid& gridLeft, Grid& gridCenter, Grid& gridRight, boo if (provLeft && provRight) { - int iconHeight = 0; - if (show) - { - auto iconMgr = std::make_shared(*provLeft, *provRight, sz); - provLeft ->setIconManager(iconMgr); - provRight->setIconManager(iconMgr); - iconHeight = iconMgr->refIconBuffer().getSize(); - } - else - { - provLeft ->setIconManager(nullptr); - provRight->setIconManager(nullptr); - iconHeight = IconBuffer::getSize(IconBuffer::SIZE_SMALL); - } +int iconHeight = 0; +if (show) +{ + auto iconMgr = std::make_shared(*provLeft, *provRight, sz); + provLeft ->setIconManager(iconMgr); + provRight->setIconManager(iconMgr); + iconHeight = iconMgr->refIconBuffer().getSize(); +} +else +{ + provLeft ->setIconManager(nullptr); + provRight->setIconManager(nullptr); + iconHeight = IconBuffer::getSize(IconBuffer::SIZE_SMALL); +} - const int newRowHeight = std::max(iconHeight, gridLeft.getMainWin().GetCharHeight()) + fastFromDIP(1); //add some space +const int newRowHeight = std::max(iconHeight, gridLeft.getMainWin().GetCharHeight()) + fastFromDIP(1); //add some space - gridLeft .setRowHeight(newRowHeight); - gridCenter.setRowHeight(newRowHeight); - gridRight .setRowHeight(newRowHeight); +gridLeft .setRowHeight(newRowHeight); +gridCenter.setRowHeight(newRowHeight); +gridRight .setRowHeight(newRowHeight); } else - assert(false); +assert(false); } void filegrid::setItemPathForm(Grid& grid, ItemPathFormat fmt) { if (auto* provLeft = dynamic_cast(grid.getDataProvider())) - provLeft->setItemPathForm(fmt); +provLeft->setItemPathForm(fmt); else if (auto* provRight = dynamic_cast(grid.getDataProvider())) - provRight->setItemPathForm(fmt); +provRight->setItemPathForm(fmt); else - assert(false); +assert(false); grid.Refresh(); } @@ -1740,23 +1806,23 @@ void filegrid::refresh(Grid& gridLeft, Grid& gridCenter, Grid& gridRight) void filegrid::setScrollMaster(Grid& grid) { if (auto prov = dynamic_cast(grid.getDataProvider())) - if (auto evtMgr = prov->getEventManager()) - { - evtMgr->setScrollMaster(grid); - return; - } +if (auto evtMgr = prov->getEventManager()) +{ + evtMgr->setScrollMaster(grid); + return; +} assert(false); } void filegrid::setNavigationMarker(Grid& gridLeft, - std::unordered_set&& markedFilesAndLinks, - std::unordered_set&& markedContainer) + std::unordered_set&& markedFilesAndLinks, + std::unordered_set&& markedContainer) { if (auto provLeft = dynamic_cast(gridLeft.getDataProvider())) - provLeft->setNavigationMarker(std::move(markedFilesAndLinks), std::move(markedContainer)); +provLeft->setNavigationMarker(std::move(markedFilesAndLinks), std::move(markedContainer)); else - assert(false); +assert(false); gridLeft.Refresh(); } @@ -1764,9 +1830,9 @@ void filegrid::setNavigationMarker(Grid& gridLeft, void filegrid::highlightSyncAction(Grid& gridCenter, bool value) { if (auto provCenter = dynamic_cast(gridCenter.getDataProvider())) - provCenter->highlightSyncAction(value); +provCenter->highlightSyncAction(value); else - assert(false); +assert(false); gridCenter.Refresh(); } @@ -1775,36 +1841,23 @@ wxBitmap fff::getSyncOpImage(SyncOperation syncOp) { switch (syncOp) //evaluate comparison result and sync direction { - case SO_CREATE_NEW_LEFT: - return getResourceImage("so_create_left_sicon"); - case SO_CREATE_NEW_RIGHT: - return getResourceImage("so_create_right_sicon"); - case SO_DELETE_LEFT: - return getResourceImage("so_delete_left_sicon"); - case SO_DELETE_RIGHT: - return getResourceImage("so_delete_right_sicon"); - case SO_MOVE_LEFT_FROM: - return getResourceImage("so_move_left_source_sicon"); - case SO_MOVE_LEFT_TO: - return getResourceImage("so_move_left_target_sicon"); - case SO_MOVE_RIGHT_FROM: - return getResourceImage("so_move_right_source_sicon"); - case SO_MOVE_RIGHT_TO: - return getResourceImage("so_move_right_target_sicon"); - case SO_OVERWRITE_LEFT: - return getResourceImage("so_update_left_sicon"); - case SO_OVERWRITE_RIGHT: - return getResourceImage("so_update_right_sicon"); - case SO_COPY_METADATA_TO_LEFT: - return getResourceImage("so_move_left_sicon"); - case SO_COPY_METADATA_TO_RIGHT: - return getResourceImage("so_move_right_sicon"); - case SO_DO_NOTHING: - return getResourceImage("so_none_sicon"); - case SO_EQUAL: - return getResourceImage("cat_equal_sicon"); - case SO_UNRESOLVED_CONFLICT: - return getResourceImage("cat_conflict_small"); + //*INDENT-OFF* + case SO_CREATE_NEW_LEFT: return getResourceImage("so_create_left_sicon"); + case SO_CREATE_NEW_RIGHT: return getResourceImage("so_create_right_sicon"); + case SO_DELETE_LEFT: return getResourceImage("so_delete_left_sicon"); + case SO_DELETE_RIGHT: return getResourceImage("so_delete_right_sicon"); + case SO_MOVE_LEFT_FROM: return getResourceImage("so_move_left_source_sicon"); + case SO_MOVE_LEFT_TO: return getResourceImage("so_move_left_target_sicon"); + case SO_MOVE_RIGHT_FROM: return getResourceImage("so_move_right_source_sicon"); + case SO_MOVE_RIGHT_TO: return getResourceImage("so_move_right_target_sicon"); + case SO_OVERWRITE_LEFT: return getResourceImage("so_update_left_sicon"); + case SO_OVERWRITE_RIGHT: return getResourceImage("so_update_right_sicon"); + case SO_COPY_METADATA_TO_LEFT: return getResourceImage("so_move_left_sicon"); + case SO_COPY_METADATA_TO_RIGHT: return getResourceImage("so_move_right_sicon"); + case SO_DO_NOTHING: return getResourceImage("so_none_sicon"); + case SO_EQUAL: return getResourceImage("cat_equal_sicon"); +case SO_UNRESOLVED_CONFLICT: return getResourceImage("cat_conflict_small"); +//*INDENT-ON* } assert(false); return wxNullBitmap; @@ -1815,21 +1868,16 @@ wxBitmap fff::getCmpResultImage(CompareFileResult cmpResult) { switch (cmpResult) { - case FILE_LEFT_SIDE_ONLY: - return getResourceImage("cat_left_only_sicon"); - case FILE_RIGHT_SIDE_ONLY: - return getResourceImage("cat_right_only_sicon"); - case FILE_LEFT_NEWER: - return getResourceImage("cat_left_newer_sicon"); - case FILE_RIGHT_NEWER: - return getResourceImage("cat_right_newer_sicon"); - case FILE_DIFFERENT_CONTENT: - return getResourceImage("cat_different_sicon"); - case FILE_EQUAL: - case FILE_DIFFERENT_METADATA: //= sub-category of equal - return getResourceImage("cat_equal_sicon"); - case FILE_CONFLICT: - return getResourceImage("cat_conflict_small"); + //*INDENT-OFF* + case FILE_LEFT_SIDE_ONLY: return getResourceImage("cat_left_only_sicon"); + case FILE_RIGHT_SIDE_ONLY: return getResourceImage("cat_right_only_sicon"); + case FILE_LEFT_NEWER: return getResourceImage("cat_left_newer_sicon"); + case FILE_RIGHT_NEWER: return getResourceImage("cat_right_newer_sicon"); + case FILE_DIFFERENT_CONTENT: return getResourceImage("cat_different_sicon"); + case FILE_EQUAL: + case FILE_DIFFERENT_METADATA: return getResourceImage("cat_equal_sicon"); //= sub-category of equal +case FILE_CONFLICT: return getResourceImage("cat_conflict_small"); +//*INDENT-ON* } assert(false); return wxNullBitmap; diff --git a/FreeFileSync/Source/ui/file_grid.h b/FreeFileSync/Source/ui/file_grid.h index 2fadb7c8..8b948d55 100644 --- a/FreeFileSync/Source/ui/file_grid.h +++ b/FreeFileSync/Source/ui/file_grid.h @@ -21,7 +21,6 @@ namespace filegrid void init(zen::Grid& gridLeft, zen::Grid& gridCenter, zen::Grid& gridRight); FileView& getDataView(zen::Grid& grid); - void highlightSyncAction(zen::Grid& gridCenter, bool value); void setupIcons(zen::Grid& gridLeft, zen::Grid& gridCenter, zen::Grid& gridRight, bool show, IconBuffer::IconSize sz); diff --git a/FreeFileSync/Source/ui/file_view.cpp b/FreeFileSync/Source/ui/file_view.cpp index 4b878ad2..ef24e3f1 100644 --- a/FreeFileSync/Source/ui/file_view.cpp +++ b/FreeFileSync/Source/ui/file_view.cpp @@ -56,35 +56,106 @@ void addNumbers(const FileSystemObject& fsObj, ViewStats& stats) template void FileView::updateView(Predicate pred) { - viewRef_.clear(); - rowPositions_.clear(); + viewRef_ .clear(); + rowPositions_ .clear(); rowPositionsFirstChild_.clear(); + pathDrawBlob_ .clear(); - for (const RefIndex& ref : sortedRef_) - if (const FileSystemObject* fsObj = FileSystemObject::retrieve(ref.objId)) + std::vector componentsBlob; + std::vector parentsBuf; //from bottom to top of hierarchy + + for (const FileSystemObject::ObjectId& objId : sortedRef_) + if (const FileSystemObject* const fsObj = FileSystemObject::retrieve(objId)) if (pred(*fsObj)) { //save row position for direct random access to FilePair or FolderPair - this->rowPositions_.emplace(ref.objId, viewRef_.size()); //costs: 0.28 µs per call - MSVC based on std::set + rowPositions_.emplace(objId, viewRef_.size()); //costs: 0.28 µs per call - MSVC based on std::set //"this->" required by two-pass lookup as enforced by GCC 4.7 - //save row position to identify first child *on sorted subview* of FolderPair or BaseFolderPair in case latter are filtered out - const ContainerObject* parent = &fsObj->parent(); - for (;;) //map all yet unassociated parents to this row + parentsBuf.clear(); + for (const FileSystemObject* fsObj2 = fsObj;;) { - const auto [it, inserted] = this->rowPositionsFirstChild_.emplace(parent, viewRef_.size()); - if (!inserted) + const ContainerObject& parent = fsObj2->parent(); + parentsBuf.push_back(&parent); + + fsObj2 = dynamic_cast(&parent); + if (!fsObj2) break; + } - if (auto folder = dynamic_cast(parent)) - parent = &(folder->parent()); - else + //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, viewRef_.size()); + !inserted) //=> parents further up in hierarchy already inserted! break; + + //------ prepare generation of tree render info ------ + componentsBlob.insert(componentsBlob.end(), parentsBuf.rbegin(), parentsBuf.rend()); + componentsBlob.push_back(fsObj); + //---------------------------------------------------- + + //save filtered view + viewRef_.push_back({ objId, componentsBlob.size() }); + } + + //--------------- generate tree render info ------------------ + size_t startPosPrev = 0; + size_t endPosPrev = 0; + + for (auto itV = viewRef_.begin(); itV != viewRef_.end(); ++itV) + { + const size_t startPos = endPosPrev; + const size_t endPos = itV->pathDrawEndPos; + + const std::span componentsPrev(&componentsBlob[startPosPrev], endPosPrev - startPosPrev); + const std::span components (&componentsBlob[startPos ], endPos - startPos); + + //find first mismatching component to draw + assert(!components.empty()); + const auto& [it, itPrev] = std::mismatch(components .begin(), components .end() - 1 /*no need to check leaf component!*/, + componentsPrev.begin(), componentsPrev.end()); //but DO check previous row's leaf: might be a folder! + const size_t iDraw = it - components.begin(); + + pathDrawBlob_.resize(pathDrawBlob_.size() + iDraw); + pathDrawBlob_.resize(pathDrawBlob_.size() + (components.size() - iDraw), PathDrawInfo::DRAW_COMPONENT); + + //connect with first of previous rows' component that is drawn + if (iDraw != 0) //... not needed for base folder component + { + (pathDrawBlob_.end() - components.size())[iDraw] |= PathDrawInfo::CONNECT_PREV; + + assert(itV != viewRef_.begin()); //because iDraw != 0 + for (auto itV2 = itV - 1;; ) //iterate backwards + { + const size_t endPos2 = itV2->pathDrawEndPos; + + size_t startPos2 = 0; + if (itV2 != viewRef_.begin()) + { + --itV2; + startPos2 = itV2->pathDrawEndPos; } + const std::span components2(&pathDrawBlob_[startPos2], endPos2 - startPos2); + assert(iDraw <= components2.size()); - //build subview - this->viewRef_.push_back(ref.objId); + if (iDraw >= components2.size()) + break; //parent folder! + + components2[iDraw] |= PathDrawInfo::CONNECT_NEXT; + + if (components2[iDraw] & PathDrawInfo::DRAW_COMPONENT) + break; + + components2[iDraw] |= PathDrawInfo::CONNECT_PREV; + + assert(startPos2 != 0); //all components of first raw are drawn => expect break! } + } + + startPosPrev = startPos; + endPosPrev = endPos; + } + //------------------------------------------------------------ } @@ -240,7 +311,7 @@ std::vector FileView::getAllFileRef(const std::vector for (size_t pos : rows) if (pos < viewSize) - if (FileSystemObject* fsObj = FileSystemObject::retrieve(viewRef_[pos])) + if (FileSystemObject* fsObj = FileSystemObject::retrieve(viewRef_[pos].objId)) output.push_back(fsObj); return output; @@ -249,24 +320,30 @@ std::vector FileView::getAllFileRef(const std::vector void FileView::removeInvalidRows() { - viewRef_.clear(); - rowPositions_.clear(); - rowPositionsFirstChild_.clear(); - //remove rows that have been deleted meanwhile - std::erase_if(sortedRef_, [&](const RefIndex& refIdx) { return !FileSystemObject::retrieve(refIdx.objId); }); + std::erase_if(sortedRef_, [&](const FileSystemObject::ObjectId& objId) { return !FileSystemObject::retrieve(objId); }); + + viewRef_ .clear(); + rowPositions_ .clear(); + rowPositionsFirstChild_.clear(); + pathDrawBlob_ .clear(); } -class FileView::SerializeHierarchy +void serializeHierarchy(ContainerObject& hierObj, std::vector& output) { -public: - static void execute(ContainerObject& hierObj, std::vector& sortedRef, size_t index) { SerializeHierarchy(sortedRef, index).recurse(hierObj); } + for (FilePair& file : hierObj.refSubFiles()) + output.push_back(file.getId()); + + for (SymlinkPair& symlink : hierObj.refSubLinks()) + output.push_back(symlink.getId()); + + for (FolderPair& folder : hierObj.refSubFolders()) + { + output.push_back(folder.getId()); + serializeHierarchy(folder, output); //add recursion here to list sub-objects directly below parent! + } -private: - SerializeHierarchy(std::vector& sortedRef, size_t index) : - index_(index), - output_(sortedRef) {} #if 0 /* Spend additional CPU cycles to sort the standard file list? @@ -275,8 +352,8 @@ private: CmpNaturalSort: 850 ms CmpLocalPath: 233 ms CmpAsciiNoCase: 189 ms - No sorting: 30 ms - */ + No sorting: 30 ms */ + template static std::vector getItemsSorted(std::list& itemList) { @@ -288,42 +365,41 @@ private: return output; } #endif - void recurse(ContainerObject& hierObj) - { - for (FilePair& file : hierObj.refSubFiles()) - output_.push_back({ index_, file.getId() }); - - for (SymlinkPair& symlink : hierObj.refSubLinks()) - output_.push_back({ index_, symlink.getId() }); - - for (FolderPair& folder : hierObj.refSubFolders()) - { - output_.push_back({ index_, folder.getId() }); - recurse(folder); //add recursion here to list sub-objects directly below parent! - } - } - - const size_t index_; - std::vector& output_; -}; +} void FileView::setData(FolderComparison& folderCmp) { //clear everything - std::vector().swap(viewRef_); //free mem - std::vector().swap(sortedRef_); // + std::unordered_map().swap(rowPositions_); + std::unordered_map().swap(rowPositionsFirstChild_); + std::vector().swap(pathDrawBlob_); + std::vector().swap(viewRef_); //+ free mem + std::vector().swap(sortedRef_); // + folderPairs_.clear(); currentSort_ = {}; + std::unordered_map().swap(compExtentsBuf_); //ensure buffer size does not get out of hand! - folderPairCount_ = std::count_if(begin(folderCmp), end(folderCmp), - [](const BaseFolderPair& baseObj) //count non-empty pairs to distinguish single/multiple folder pair cases + std::for_each(begin(folderCmp), end(folderCmp), [&](BaseFolderPair& baseObj) { - return !AFS::isNullPath(baseObj.getAbstractPath< LEFT_SIDE>()) || - !AFS::isNullPath(baseObj.getAbstractPath()); + serializeHierarchy(baseObj, sortedRef_); + + folderPairs_.emplace_back(&baseObj, + baseObj.getAbstractPath< LEFT_SIDE>(), + baseObj.getAbstractPath()); }); +} + + +size_t FileView::getEffectiveFolderPairCount() const +{ + return std::count_if(folderPairs_.begin(), folderPairs_.end(), [](const auto& folderPair) + { + const auto& [baseObj, basePathL, basePathR] = folderPair; - for (auto it = begin(folderCmp); it != end(folderCmp); ++it) - SerializeHierarchy::execute(*it, sortedRef_, it - begin(folderCmp)); + return !AFS::isNullPath(basePathL) || + !AFS::isNullPath(basePathR); + }); } @@ -346,7 +422,7 @@ bool isDirectoryPair(const FileSystemObject& fsObj) template inline -bool lessShortFileName(const FileSystemObject& lhs, const FileSystemObject& rhs) +bool lessFileName(const FileSystemObject& lhs, const FileSystemObject& rhs) { //sort order: first files/symlinks, then directories then empty rows @@ -370,46 +446,95 @@ bool lessShortFileName(const FileSystemObject& lhs, const FileSystemObject& rhs) } -template inline -bool lessFullPath(const FileSystemObject& lhs, const FileSystemObject& rhs) +template inline //side currently unused! +bool lessFilePath(const FileSystemObject::ObjectId& lhs, const FileSystemObject::ObjectId& rhs, + const std::unordered_map& sortedPos, + std::vector& tempBuf) { - //empty rows always last - if (lhs.isEmpty()) + const FileSystemObject* fsObjL = FileSystemObject::retrieve(lhs); + const FileSystemObject* fsObjR = FileSystemObject::retrieve(rhs); + if (!fsObjL) //invalid rows shall appear at the end return false; - else if (rhs.isEmpty()) + else if (!fsObjR) return true; - return zen::makeSortDirection(LessNaturalSort() /*even on Linux*/, std::bool_constant())( - zen::utfTo(AFS::getDisplayPath(lhs.getAbstractPath())), - zen::utfTo(AFS::getDisplayPath(rhs.getAbstractPath()))); -} + //------- 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; -template inline //side currently unused! -bool lessRelativeFolder(const FileSystemObject& lhs, const FileSystemObject& rhs) -{ - const bool isDirectoryL = isDirectoryPair(lhs); - const Zstring& relFolderL = isDirectoryL ? - lhs.getRelativePathAny() : - lhs.parent().getRelativePathAny(); + if (basePosL != basePosR) + return zen::makeSortDirection(std::less<>(), std::bool_constant())(basePosL, basePosR); + } - const bool isDirectoryR = isDirectoryPair(rhs); - const Zstring& relFolderR = isDirectoryR ? - rhs.getRelativePathAny() : - rhs.parent().getRelativePathAny(); + //------- sort component-wise ---------- + const auto folderL = dynamic_cast(fsObjL); + const auto folderR = dynamic_cast(fsObjR); - //compare relative names without filepaths first - const int rv = compareNatural(relFolderL, relFolderR); - if (rv != 0) - return zen::makeSortDirection(std::less(), std::bool_constant())(rv, 0); + std::vector& parentsBuf = tempBuf; //from bottom to top of hierarchy, excluding base + parentsBuf.clear(); - //make directories always appear before contained files - if (isDirectoryR) - return false; - else if (isDirectoryL) - return true; + 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); - return zen::makeSortDirection(LessNaturalSort(), std::bool_constant())(lhs.getItemNameAny(), rhs.getItemNameAny()); + 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->getItemNameAny(), fsObjR->getItemNameAny()); + } + else + return true; + } + else if (itR == parentsR.rend()) + return false; + else //different components... + { + if (const int rv = compareNatural((*itL)->getItemNameAny(), (*itR)->getItemNameAny()); + rv != 0) + 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; + } } @@ -518,59 +643,72 @@ bool lessSyncDirection(const FileSystemObject& lhs, const FileSystemObject& rhs) template struct FileView::LessFullPath { - bool operator()(const RefIndex a, const RefIndex b) const + LessFullPath(std::vector> folderPairs) { - const FileSystemObject* fsObjA = FileSystemObject::retrieve(a.objId); - const FileSystemObject* fsObjB = FileSystemObject::retrieve(b.objId); - if (!fsObjA) //invalid rows shall appear at the end - return false; - else if (!fsObjB) - return true; + //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::ref(basePathLA, basePathRA); + const AbstractPath& basePathB = SelectParam::ref(basePathLB, basePathRB); + + return LessNaturalSort()/*even on Linux*/(zen::utfTo(AFS::getDisplayPath(basePathA)), + zen::utfTo(AFS::getDisplayPath(basePathB))); + }); - return lessFullPath(*fsObjA, *fsObjB); + size_t pos = 0; + for (const auto& [baseObj, basePathL, basePathR] : folderPairs) + sortedPos_.ref().emplace(baseObj, pos++); + } + + bool operator()(const FileSystemObject::ObjectId& lhs, const FileSystemObject::ObjectId& rhs) const + { + return lessFilePath(lhs, rhs, sortedPos_.ref(), tempBuf_); } + +private: + SharedRef> sortedPos_ = makeSharedRef>(); + mutable std::vector tempBuf_; //avoid repeated memory allocation in lessFilePath() }; template struct FileView::LessRelativeFolder { - bool operator()(const RefIndex a, const RefIndex b) const + LessRelativeFolder(const std::vector>& folderPairs) { - const FileSystemObject* fsObjA = FileSystemObject::retrieve(a.objId); - const FileSystemObject* fsObjB = FileSystemObject::retrieve(b.objId); - if (!fsObjA) //invalid rows shall appear at the end - return false; - else if (!fsObjB) - return true; - - //presort by folder pair - if (a.folderIndex != b.folderIndex) - { - if constexpr (ascending) - return a.folderIndex < b.folderIndex; - else - return a.folderIndex > b.folderIndex; - } + //take over positions of base folders as set up by user + size_t pos = 0; + for (const auto& [baseObj, basePathL, basePathR] : folderPairs) + sortedPos_.ref().emplace(baseObj, pos++); + } - return lessRelativeFolder(*fsObjA, *fsObjB); + bool operator()(const FileSystemObject::ObjectId& lhs, const FileSystemObject::ObjectId& rhs) const + { + return lessFilePath(lhs, rhs, sortedPos_.ref(), tempBuf_); } + +private: + SharedRef> sortedPos_ = makeSharedRef>(); + mutable std::vector tempBuf_; //avoid repeated memory allocation in lessFilePath() }; template -struct FileView::LessShortFileName +struct FileView::LessFileName { - bool operator()(const RefIndex a, const RefIndex b) const + bool operator()(const FileSystemObject::ObjectId& lhs, const FileSystemObject::ObjectId& rhs) const { - const FileSystemObject* fsObjA = FileSystemObject::retrieve(a.objId); - const FileSystemObject* fsObjB = FileSystemObject::retrieve(b.objId); + const FileSystemObject* fsObjA = FileSystemObject::retrieve(lhs); + const FileSystemObject* fsObjB = FileSystemObject::retrieve(rhs); if (!fsObjA) //invalid rows shall appear at the end return false; else if (!fsObjB) return true; - return lessShortFileName(*fsObjA, *fsObjB); + return lessFileName(*fsObjA, *fsObjB); } }; @@ -578,10 +716,10 @@ struct FileView::LessShortFileName template struct FileView::LessFilesize { - bool operator()(const RefIndex a, const RefIndex b) const + bool operator()(const FileSystemObject::ObjectId& lhs, const FileSystemObject::ObjectId& rhs) const { - const FileSystemObject* fsObjA = FileSystemObject::retrieve(a.objId); - const FileSystemObject* fsObjB = FileSystemObject::retrieve(b.objId); + const FileSystemObject* fsObjA = FileSystemObject::retrieve(lhs); + const FileSystemObject* fsObjB = FileSystemObject::retrieve(rhs); if (!fsObjA) //invalid rows shall appear at the end return false; else if (!fsObjB) @@ -595,10 +733,10 @@ struct FileView::LessFilesize template struct FileView::LessFiletime { - bool operator()(const RefIndex a, const RefIndex b) const + bool operator()(const FileSystemObject::ObjectId& lhs, const FileSystemObject::ObjectId& rhs) const { - const FileSystemObject* fsObjA = FileSystemObject::retrieve(a.objId); - const FileSystemObject* fsObjB = FileSystemObject::retrieve(b.objId); + const FileSystemObject* fsObjA = FileSystemObject::retrieve(lhs); + const FileSystemObject* fsObjB = FileSystemObject::retrieve(rhs); if (!fsObjA) //invalid rows shall appear at the end return false; else if (!fsObjB) @@ -612,10 +750,10 @@ struct FileView::LessFiletime template struct FileView::LessExtension { - bool operator()(const RefIndex a, const RefIndex b) const + bool operator()(const FileSystemObject::ObjectId& lhs, const FileSystemObject::ObjectId& rhs) const { - const FileSystemObject* fsObjA = FileSystemObject::retrieve(a.objId); - const FileSystemObject* fsObjB = FileSystemObject::retrieve(b.objId); + const FileSystemObject* fsObjA = FileSystemObject::retrieve(lhs); + const FileSystemObject* fsObjB = FileSystemObject::retrieve(rhs); if (!fsObjA) //invalid rows shall appear at the end return false; else if (!fsObjB) @@ -629,10 +767,10 @@ struct FileView::LessExtension template struct FileView::LessCmpResult { - bool operator()(const RefIndex a, const RefIndex b) const + bool operator()(const FileSystemObject::ObjectId& lhs, const FileSystemObject::ObjectId& rhs) const { - const FileSystemObject* fsObjA = FileSystemObject::retrieve(a.objId); - const FileSystemObject* fsObjB = FileSystemObject::retrieve(b.objId); + const FileSystemObject* fsObjA = FileSystemObject::retrieve(lhs); + const FileSystemObject* fsObjB = FileSystemObject::retrieve(rhs); if (!fsObjA) //invalid rows shall appear at the end return false; else if (!fsObjB) @@ -646,10 +784,10 @@ struct FileView::LessCmpResult template struct FileView::LessSyncDirection { - bool operator()(const RefIndex a, const RefIndex b) const + bool operator()(const FileSystemObject::ObjectId& lhs, const FileSystemObject::ObjectId& rhs) const { - const FileSystemObject* fsObjA = FileSystemObject::retrieve(a.objId); - const FileSystemObject* fsObjB = FileSystemObject::retrieve(b.objId); + const FileSystemObject* fsObjA = FileSystemObject::retrieve(lhs); + const FileSystemObject* fsObjB = FileSystemObject::retrieve(rhs); if (!fsObjA) //invalid rows shall appear at the end return false; else if (!fsObjB) @@ -666,6 +804,7 @@ void FileView::sortView(ColumnTypeRim type, ItemPathFormat pathFmt, bool onLeft, viewRef_ .clear(); rowPositions_ .clear(); rowPositionsFirstChild_.clear(); + pathDrawBlob_ .clear(); currentSort_ = SortInfo({ type, onLeft, ascending }); switch (type) @@ -674,22 +813,22 @@ void FileView::sortView(ColumnTypeRim type, ItemPathFormat pathFmt, bool onLeft, switch (pathFmt) { case ItemPathFormat::FULL_PATH: - if ( ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFullPath()); - else if ( ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFullPath()); - else if (!ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFullPath()); - else if (!ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFullPath()); + 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; case ItemPathFormat::RELATIVE_PATH: - if ( ascending) std::sort(sortedRef_.begin(), sortedRef_.end(), LessRelativeFolder()); - else if (!ascending) std::sort(sortedRef_.begin(), sortedRef_.end(), LessRelativeFolder()); + if ( ascending) std::sort(sortedRef_.begin(), sortedRef_.end(), LessRelativeFolder(folderPairs_)); + else if (!ascending) std::sort(sortedRef_.begin(), sortedRef_.end(), LessRelativeFolder(folderPairs_)); break; case ItemPathFormat::ITEM_NAME: - if ( ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessShortFileName()); - else if ( ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessShortFileName()); - else if (!ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessShortFileName()); - else if (!ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessShortFileName()); + 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; } break; @@ -721,6 +860,7 @@ void FileView::sortView(ColumnTypeCenter type, bool ascending) viewRef_ .clear(); rowPositions_ .clear(); rowPositionsFirstChild_.clear(); + pathDrawBlob_ .clear(); currentSort_ = SortInfo({ type, false, ascending }); switch (type) diff --git a/FreeFileSync/Source/ui/file_view.h b/FreeFileSync/Source/ui/file_view.h index 86c967c4..df70b53b 100644 --- a/FreeFileSync/Source/ui/file_view.h +++ b/FreeFileSync/Source/ui/file_view.h @@ -7,6 +7,7 @@ #ifndef GRID_VIEW_H_9285028345703475842569 #define GRID_VIEW_H_9285028345703475842569 +#include #include #include #include @@ -22,13 +23,27 @@ class FileView //grid view of FolderComparison public: FileView() {} - //direct data access via row number - const FileSystemObject* getObject(size_t row) const; //returns nullptr if object is not found; complexity: constant! - /**/ - FileSystemObject* getObject(size_t row); // size_t rowsOnView() const { return viewRef_ .size(); } //only visible elements size_t rowsTotal () const { return sortedRef_.size(); } //total rows available + //direct data access via row number + const FileSystemObject* getFsObject(size_t row) const; //returns nullptr if object is not found; complexity: constant! + /**/ FileSystemObject* getFsObject(size_t row); // + + struct PathDrawInfo + { + enum + { + CONNECT_PREV = 0x1, + CONNECT_NEXT = 0x2, + DRAW_COMPONENT = 0x4, + }; + std::span pathDrawInfo; //... of path components (including base folder which counts as *single* component) + + const FileSystemObject* fsObj; //nullptr if object is not found + }; + PathDrawInfo getDrawInfo(size_t row) const; //complexity: constant! + //get references to FileSystemObject: no nullptr-check needed! Everything's bound. std::vector getAllFileRef(const std::vector& rows); @@ -110,36 +125,41 @@ public: ptrdiff_t findRowFirstChild(const ContainerObject* hierObj) const; // find first child of FolderPair or BaseFolderPair *on sorted sub view* //"hierObj" may be invalid, it is NOT dereferenced, return < 0 if not found - size_t getFolderPairCount() const { return folderPairCount_; } //count non-empty pairs to distinguish single/multiple folder pair cases + //count non-empty pairs to distinguish single/multiple folder pair cases + size_t getEffectiveFolderPairCount() const; + + //buffer expensive wxDC::GetTextExtent() calls! + //=> shared between GridDataLeft/GridDataRight + std::unordered_map& refCompExtentsBuf() { return compExtentsBuf_; } private: FileView (const FileView&) = delete; FileView& operator=(const FileView&) = delete; - struct RefIndex - { - size_t folderIndex = 0; //because of alignment there's no benefit in using "unsigned int" in 64-bit code here! - FileSystemObject::ObjectId objId = nullptr; - }; - template void updateView(Predicate pred); - std::unordered_map rowPositions_; //find row positions on sortedRef directly - std::unordered_map rowPositionsFirstChild_; //find first child on sortedRef of a hierarchy object + std::unordered_map rowPositions_; //find row positions on viewRef_ directly + std::unordered_map rowPositionsFirstChild_; //find first child on sortedRef of a hierarchy object //void* instead of ContainerObject*: these are weak pointers and should *never be dereferenced*! - std::vector viewRef_; //partial view on sortedRef + struct ViewRow + { + FileSystemObject::ObjectId objId = nullptr; + size_t pathDrawEndPos; //index into pathDrawBlob_; start position defined by previous row's end position + }; + std::vector pathDrawBlob_; //draw info for components of all rows (including base folder which counts as *single* component) + + + std::vector viewRef_; //partial view on sortedRef_ /* /|\ - | (update...) - | */ - std::vector sortedRef_; //flat view of weak pointers on folderCmp; may be sorted + | (applyFilterBy...) */ + std::vector sortedRef_; //flat view of weak pointers on folderCmp; may be sorted /* /|\ | (setData...) - | */ - //std::shared_ptr folderCmp; //actual comparison data: owned by FileView! - size_t folderPairCount_ = 0; //number of non-empty folder pairs + FolderComparison folderCmp */ + std::vector> folderPairs_; class SerializeHierarchy; @@ -151,7 +171,7 @@ private: struct LessRelativeFolder; template - struct LessShortFileName; + struct LessFileName; template struct LessFilesize; @@ -169,6 +189,8 @@ private: struct LessSyncDirection; std::optional currentSort_; + + std::unordered_map compExtentsBuf_; //buffer expensive wxDC::GetTextExtent() calls! }; @@ -180,17 +202,32 @@ private: //##################### implementation ######################################### inline -const FileSystemObject* FileView::getObject(size_t row) const +const FileSystemObject* FileView::getFsObject(size_t row) const { return row < viewRef_.size() ? - FileSystemObject::retrieve(viewRef_[row]) : nullptr; + FileSystemObject::retrieve(viewRef_[row].objId) : nullptr; } + inline -FileSystemObject* FileView::getObject(size_t row) +FileSystemObject* FileView::getFsObject(size_t row) { //code re-use of const method: see Meyers Effective C++ - return const_cast(static_cast(*this).getObject(row)); + return const_cast(static_cast(*this).getFsObject(row)); +} + + +inline +FileView::PathDrawInfo FileView::getDrawInfo(size_t row) const +{ + if (row < viewRef_.size()) + if (const FileSystemObject* fsObj = FileSystemObject::retrieve(viewRef_[row].objId)) + { + const std::span pathDrawInfo(&pathDrawBlob_[row == 0 ? 0 : viewRef_[row - 1].pathDrawEndPos], + &pathDrawBlob_[0] + viewRef_[row].pathDrawEndPos); //WTF: can't use iterators with std::span on clang!? + return { pathDrawInfo, fsObj }; + } + return {}; } } diff --git a/FreeFileSync/Source/ui/folder_selector.cpp b/FreeFileSync/Source/ui/folder_selector.cpp index 8edf0056..ec114b3b 100644 --- a/FreeFileSync/Source/ui/folder_selector.cpp +++ b/FreeFileSync/Source/ui/folder_selector.cpp @@ -150,7 +150,7 @@ void FolderSelector::onItemPathDropped(FileDropEvent& event) const AbstractPath itemPath = createAbstractPath(shellItemPath); try { - if (AFS::getItemType(itemPath) == AFS::ItemType::FILE) //throw FileError + if (AFS::getItemType(itemPath) == AFS::ItemType::file) //throw FileError if (const std::optional parentPath = AFS::getParentPath(itemPath)) return AFS::getInitPathPhrase(*parentPath); } @@ -194,7 +194,7 @@ void FolderSelector::onSelectFolder(wxCommandEvent& event) { try { - return AFS::getItemType(folderPath) != AFS::ItemType::FILE; //throw FileError + return AFS::getItemType(folderPath) != AFS::ItemType::file; //throw FileError } catch (FileError&) { return false; } }); diff --git a/FreeFileSync/Source/ui/gui_generated.cpp b/FreeFileSync/Source/ui/gui_generated.cpp index 340a16ba..2bc3518c 100644 --- a/FreeFileSync/Source/ui/gui_generated.cpp +++ b/FreeFileSync/Source/ui/gui_generated.cpp @@ -2548,84 +2548,73 @@ CloudSetupDlgGenerated::CloudSetupDlgGenerated( wxWindow* parent, wxWindowID id, wxBoxSizer* bSizer284; bSizer284 = new wxBoxSizer( wxHORIZONTAL ); - wxBoxSizer* bSizer285; - bSizer285 = new wxBoxSizer( wxVERTICAL ); + wxBoxSizer* bSizer307; + bSizer307 = new wxBoxSizer( wxVERTICAL ); - m_staticText166 = new wxStaticText( m_panel41, wxID_ANY, _("Connected user accounts:"), wxDefaultPosition, wxDefaultSize, 0 ); - m_staticText166->Wrap( -1 ); - bSizer285->Add( m_staticText166, 0, wxTOP|wxRIGHT|wxLEFT, 5 ); + wxBoxSizer* bSizer306; + bSizer306 = new wxBoxSizer( wxHORIZONTAL ); - m_listBoxGdriveUsers = new wxListBox( m_panel41, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0, NULL, wxLB_NEEDED_SB|wxLB_SINGLE|wxLB_SORT ); - bSizer285->Add( m_listBoxGdriveUsers, 1, wxTOP|wxBOTTOM|wxLEFT|wxEXPAND, 5 ); + m_bitmapGdriveUser = new wxStaticBitmap( m_panel41, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer306->Add( m_bitmapGdriveUser, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 ); + 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 ); - bSizer284->Add( bSizer285, 0, wxEXPAND|wxTOP|wxBOTTOM|wxLEFT, 5 ); - wxBoxSizer* bSizer286; - bSizer286 = new wxBoxSizer( wxVERTICAL ); + bSizer307->Add( bSizer306, 0, wxALL|wxALIGN_CENTER_HORIZONTAL, 5 ); - wxBoxSizer* bSizer289; - bSizer289 = new wxBoxSizer( wxHORIZONTAL ); - - wxBoxSizer* bSizer288; - bSizer288 = new wxBoxSizer( wxVERTICAL ); + 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 ); - m_staticText167 = new wxStaticText( m_panel41, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0 ); - m_staticText167->Wrap( -1 ); - bSizer288->Add( m_staticText167, 0, wxTOP|wxRIGHT|wxLEFT, 5 ); + wxBoxSizer* bSizer3002; + bSizer3002 = new wxBoxSizer( wxHORIZONTAL ); m_buttonGdriveAddUser = new zen::BitmapTextButton( m_panel41, wxID_ANY, _("&Add connection"), wxDefaultPosition, wxSize( -1,-1 ), 0 ); - bSizer288->Add( m_buttonGdriveAddUser, 0, wxEXPAND|wxTOP|wxRIGHT|wxLEFT, 5 ); + bSizer3002->Add( m_buttonGdriveAddUser, 1, wxALIGN_CENTER_VERTICAL, 5 ); m_buttonGdriveRemoveUser = new zen::BitmapTextButton( m_panel41, wxID_ANY, _("&Disconnect"), wxDefaultPosition, wxSize( -1,-1 ), 0 ); - bSizer288->Add( m_buttonGdriveRemoveUser, 0, wxEXPAND|wxALL, 5 ); - - - bSizer289->Add( bSizer288, 0, wxTOP|wxBOTTOM|wxRIGHT, 5 ); + bSizer3002->Add( m_buttonGdriveRemoveUser, 1, wxALIGN_CENTER_VERTICAL|wxLEFT, 5 ); - m_staticline76 = new wxStaticLine( m_panel41, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL ); - bSizer289->Add( m_staticline76, 0, wxEXPAND, 5 ); + bSizer307->Add( bSizer3002, 0, wxBOTTOM|wxRIGHT|wxLEFT|wxALIGN_CENTER_HORIZONTAL, 5 ); - bSizer286->Add( bSizer289, 0, 0, 5 ); - m_staticline74 = new wxStaticLine( m_panel41, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); - bSizer286->Add( m_staticline74, 0, wxEXPAND, 5 ); + bSizer284->Add( bSizer307, 0, wxALL|wxEXPAND, 5 ); - wxBoxSizer* bSizer287; - bSizer287 = new wxBoxSizer( wxVERTICAL ); - - m_staticText165 = new wxStaticText( m_panel41, wxID_ANY, _("Selected user account:"), wxDefaultPosition, wxDefaultSize, 0 ); - m_staticText165->Wrap( -1 ); - bSizer287->Add( m_staticText165, 0, wxTOP|wxRIGHT|wxLEFT, 5 ); + m_staticline841 = new wxStaticLine( m_panel41, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL ); + bSizer284->Add( m_staticline841, 0, wxEXPAND, 5 ); - wxBoxSizer* bSizer279; - bSizer279 = new wxBoxSizer( wxHORIZONTAL ); + wxBoxSizer* bSizer3041; + bSizer3041 = new wxBoxSizer( wxVERTICAL ); - m_bitmapGdriveSelectedUser = new wxStaticBitmap( m_panel41, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); - bSizer279->Add( m_bitmapGdriveSelectedUser, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 5 ); + wxBoxSizer* bSizer305; + bSizer305 = new wxBoxSizer( wxHORIZONTAL ); - m_staticTextGdriveUser = new wxStaticText( m_panel41, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); - m_staticTextGdriveUser->Wrap( -1 ); - bSizer279->Add( m_staticTextGdriveUser, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + m_bitmapGdriveDrive = new wxStaticBitmap( m_panel41, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer305->Add( m_bitmapGdriveDrive, 0, wxRIGHT|wxALIGN_CENTER_VERTICAL, 5 ); + 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 ); - bSizer287->Add( bSizer279, 0, 0, 5 ); + bSizer3041->Add( bSizer305, 0, wxALL, 5 ); - bSizer286->Add( bSizer287, 0, wxALL, 5 ); + m_listBoxGdriveDrives = new wxListBox( m_panel41, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0, NULL, wxLB_NEEDED_SB|wxLB_SINGLE|wxLB_SORT ); + bSizer3041->Add( m_listBoxGdriveDrives, 1, wxEXPAND|wxBOTTOM|wxRIGHT|wxLEFT, 5 ); - bSizer284->Add( bSizer286, 1, 0, 5 ); + bSizer284->Add( bSizer3041, 1, wxALL|wxEXPAND, 5 ); - bSizerGdrive->Add( bSizer284, 0, wxEXPAND, 5 ); + bSizerGdrive->Add( bSizer284, 1, wxEXPAND, 5 ); m_staticline73 = new wxStaticLine( m_panel41, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); bSizerGdrive->Add( m_staticline73, 0, wxEXPAND, 5 ); - bSizer185->Add( bSizerGdrive, 0, wxEXPAND, 5 ); + bSizer185->Add( bSizerGdrive, 1, wxEXPAND, 5 ); bSizerServer = new wxBoxSizer( wxVERTICAL ); @@ -2633,7 +2622,7 @@ CloudSetupDlgGenerated::CloudSetupDlgGenerated( wxWindow* parent, wxWindowID id, bSizer276 = new wxBoxSizer( wxHORIZONTAL ); m_bitmapServer = new wxStaticBitmap( m_panel41, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); - bSizer276->Add( m_bitmapServer, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 5 ); + bSizer276->Add( m_bitmapServer, 0, wxTOP|wxBOTTOM|wxLEFT|wxALIGN_CENTER_VERTICAL, 5 ); m_staticText12311 = new wxStaticText( m_panel41, wxID_ANY, _("Server name or IP address:"), wxDefaultPosition, wxDefaultSize, 0 ); m_staticText12311->Wrap( -1 ); @@ -2825,24 +2814,15 @@ CloudSetupDlgGenerated::CloudSetupDlgGenerated( wxWindow* parent, wxWindowID id, bSizerAccessTimeout = new wxBoxSizer( wxHORIZONTAL ); - wxBoxSizer* bSizer273; - bSizer273 = new wxBoxSizer( wxHORIZONTAL ); - m_staticTextTimeout = new wxStaticText( m_panel41, wxID_ANY, _("Access timeout (in seconds):"), wxDefaultPosition, wxDefaultSize, 0 ); m_staticTextTimeout->Wrap( -1 ); - bSizer273->Add( m_staticTextTimeout, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 5 ); + bSizerAccessTimeout->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 ); - bSizer273->Add( m_spinCtrlTimeout, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); - - - bSizerAccessTimeout->Add( bSizer273, 0, wxALL, 5 ); + bSizerAccessTimeout->Add( m_spinCtrlTimeout, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); - m_staticline72 = new wxStaticLine( m_panel41, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL ); - bSizerAccessTimeout->Add( m_staticline72, 0, wxEXPAND, 5 ); - - bSizer298->Add( bSizerAccessTimeout, 0, wxALIGN_CENTER_VERTICAL, 5 ); + bSizer298->Add( bSizerAccessTimeout, 0, wxALL, 5 ); bSizer269->Add( bSizer298, 0, 0, 5 ); @@ -2854,12 +2834,10 @@ CloudSetupDlgGenerated::CloudSetupDlgGenerated( wxWindow* parent, wxWindowID id, m_panel41->SetSizer( bSizer185 ); m_panel41->Layout(); bSizer185->Fit( m_panel41 ); - bSizer134->Add( m_panel41, 0, wxEXPAND, 5 ); - - bSizer255 = new wxBoxSizer( wxVERTICAL ); + bSizer134->Add( m_panel41, 1, wxEXPAND, 5 ); m_staticline571 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); - bSizer255->Add( m_staticline571, 0, wxEXPAND, 5 ); + bSizer134->Add( m_staticline571, 0, wxEXPAND, 5 ); wxBoxSizer* bSizer219; bSizer219 = new wxBoxSizer( wxHORIZONTAL ); @@ -2881,10 +2859,10 @@ CloudSetupDlgGenerated::CloudSetupDlgGenerated( wxWindow* parent, wxWindowID id, bSizer219->Add( m_hyperlink171, 0, wxALL|wxALIGN_CENTER_VERTICAL, 10 ); - bSizer255->Add( bSizer219, 0, wxEXPAND, 5 ); + bSizer134->Add( bSizer219, 0, wxEXPAND, 5 ); m_staticline57 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); - bSizer255->Add( m_staticline57, 0, wxEXPAND, 5 ); + bSizer134->Add( m_staticline57, 0, wxEXPAND, 5 ); m_panel411 = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); m_panel411->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); @@ -2967,10 +2945,7 @@ CloudSetupDlgGenerated::CloudSetupDlgGenerated( wxWindow* parent, wxWindowID id, m_panel411->SetSizer( bSizer1851 ); m_panel411->Layout(); bSizer1851->Fit( m_panel411 ); - bSizer255->Add( m_panel411, 1, wxEXPAND, 5 ); - - - bSizer134->Add( bSizer255, 1, wxEXPAND, 5 ); + bSizer134->Add( m_panel411, 0, wxEXPAND, 5 ); m_staticline12 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); bSizer134->Add( m_staticline12, 0, wxEXPAND, 5 ); diff --git a/FreeFileSync/Source/ui/gui_generated.h b/FreeFileSync/Source/ui/gui_generated.h index eefc3b4d..29c620a0 100644 --- a/FreeFileSync/Source/ui/gui_generated.h +++ b/FreeFileSync/Source/ui/gui_generated.h @@ -566,16 +566,15 @@ class CloudSetupDlgGenerated : public wxDialog wxStaticLine* m_staticline371; wxPanel* m_panel41; wxBoxSizer* bSizerGdrive; + wxStaticBitmap* m_bitmapGdriveUser; wxStaticText* m_staticText166; wxListBox* m_listBoxGdriveUsers; - wxStaticText* m_staticText167; zen::BitmapTextButton* m_buttonGdriveAddUser; zen::BitmapTextButton* m_buttonGdriveRemoveUser; - wxStaticLine* m_staticline76; - wxStaticLine* m_staticline74; - wxStaticText* m_staticText165; - wxStaticBitmap* m_bitmapGdriveSelectedUser; - wxStaticText* m_staticTextGdriveUser; + wxStaticLine* m_staticline841; + wxStaticBitmap* m_bitmapGdriveDrive; + wxStaticText* m_staticText186; + wxListBox* m_listBoxGdriveDrives; wxStaticLine* m_staticline73; wxBoxSizer* bSizerServer; wxStaticBitmap* m_bitmapServer; @@ -617,8 +616,6 @@ class CloudSetupDlgGenerated : public wxDialog wxBoxSizer* bSizerAccessTimeout; wxStaticText* m_staticTextTimeout; wxSpinCtrl* m_spinCtrlTimeout; - wxStaticLine* m_staticline72; - wxBoxSizer* bSizer255; wxStaticLine* m_staticline571; wxStaticBitmap* m_bitmapPerf; wxStaticText* m_staticText1361; diff --git a/FreeFileSync/Source/ui/main_dlg.cpp b/FreeFileSync/Source/ui/main_dlg.cpp index 4d6d10c9..02b22d65 100644 --- a/FreeFileSync/Source/ui/main_dlg.cpp +++ b/FreeFileSync/Source/ui/main_dlg.cpp @@ -829,7 +829,7 @@ MainDialog::MainDialog(const Zstring& globalConfigFilePath, { try { - if (AFS::getItemType(folderPath) != AFS::ItemType::FILE) //throw FileError + if (AFS::getItemType(folderPath) != AFS::ItemType::file) //throw FileError return {}; } catch (FileError&) {} @@ -1331,7 +1331,7 @@ void MainDialog::copyToAlternateFolder(const std::vector& sel { if (std::all_of(selectionLeft .begin(), selectionLeft .end(), [](const FileSystemObject* fsObj) { return fsObj->isEmpty< LEFT_SIDE>(); }) && /**/std::all_of(selectionRight.begin(), selectionRight.end(), [](const FileSystemObject* fsObj) { return fsObj->isEmpty(); })) - return; //harmonize with onMainGridContextRim(): this function should be a no-op iff context menu option is disabled! + /**/return; //harmonize with onMainGridContextRim(): this function should be a no-op iff context menu option is disabled! FocusPreserver fp; @@ -1379,7 +1379,7 @@ void MainDialog::deleteSelectedFiles(const std::vector& selec { if (std::all_of(selectionLeft .begin(), selectionLeft .end(), [](const FileSystemObject* fsObj) { return fsObj->isEmpty< LEFT_SIDE>(); }) && /**/std::all_of(selectionRight.begin(), selectionRight.end(), [](const FileSystemObject* fsObj) { return fsObj->isEmpty(); })) - return; //harmonize with onMainGridContextRim(): this function should be a no-op iff context menu option is disabled! + /**/return; //harmonize with onMainGridContextRim(): this function should be a no-op iff context menu option is disabled! FocusPreserver fp; @@ -1448,7 +1448,8 @@ void extractFileDescriptor(const FileSystemObject& fsObj, Function onDescriptor) { const FileDescriptor descr = { file.getAbstractPath(), file.getAttributes() }; onDescriptor(descr); - }, [](const SymlinkPair& symlink) {}); + }, + [](const SymlinkPair& symlink) {}); } @@ -3296,34 +3297,42 @@ void MainDialog::renameSelectedCfgHistoryItem() if (!cfgExtPf.empty()) cfgExtPf = Zstr('.') + cfgExtPf; - wxTextEntryDialog cfgRenameDlg(this, _("New name:"), _("Rename Configuration"), utfTo(cfgNameOld)); + for (;;) + { + wxTextEntryDialog cfgRenameDlg(this, _("New name:"), _("Rename Configuration"), utfTo(cfgNameOld)); - wxTextValidator inputValidator(wxFILTER_EXCLUDE_CHAR_LIST); - inputValidator.SetCharExcludes(LR"(/\":*?<>|)"); //forbidden chars for file names (at least on Windows) - cfgRenameDlg.SetTextValidator(inputValidator); + wxTextValidator inputValidator(wxFILTER_EXCLUDE_CHAR_LIST); + inputValidator.SetCharExcludes(LR"(/\":*?<>|)"); //forbidden chars for file names (at least on Windows) + cfgRenameDlg.SetTextValidator(inputValidator); - if (cfgRenameDlg.ShowModal() != wxID_OK) - return; + if (cfgRenameDlg.ShowModal() != wxID_OK) + return; - const Zstring cfgNameNew = utfTo(trimCpy(cfgRenameDlg.GetValue())); - if (cfgNameNew == cfgNameOld) - return; + const Zstring cfgNameNew = utfTo(trimCpy(cfgRenameDlg.GetValue())); + if (cfgNameNew == cfgNameOld) + return; - const Zstring cfgPathNew = folderPathPf + cfgNameNew + cfgExtPf; - try - { - if (cfgNameNew.empty()) //better error message + check than wxFILTER_EMPTY, e.g. trimCpy()! - throw FileError(_("Configuration name must not be empty.")); + const Zstring cfgPathNew = folderPathPf + cfgNameNew + cfgExtPf; + 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) { return showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); } + 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).removeItems({ cfgPathOld }); - m_gridCfgHistory->Refresh(); //grid size changed => clears selection! + cfggrid::getDataView(*m_gridCfgHistory).removeItems({ cfgPathOld }); + m_gridCfgHistory->Refresh(); //grid size changed => clears selection! - //keep current cfg and just swap the file name: see previous "loadConfiguration({ cfgPathOld }"! - setLastUsedConfig(lastSavedCfg_, { cfgPathNew }); + //keep current cfg and just swap the file name: see previous "loadConfiguration({ cfgPathOld }"! + setLastUsedConfig(lastSavedCfg_, { cfgPathNew }); + return; + } } } @@ -4490,7 +4499,7 @@ void MainDialog::onGridDoubleClickRim(size_t row, bool leftSide) { std::vector selectionLeft; std::vector selectionRight; - if (FileSystemObject* fsObj = filegrid::getDataView(*m_gridMainC).getObject(row)) //selection must be a list of BOUND pointers! + if (FileSystemObject* fsObj = filegrid::getDataView(*m_gridMainC).getFsObject(row)) //selection must be a list of BOUND pointers! (leftSide ? selectionLeft : selectionRight) = { fsObj }; openExternalApplication(globalCfg_.gui.externalApps[0].cmdLine, leftSide, selectionLeft, selectionRight); diff --git a/FreeFileSync/Source/ui/small_dlgs.cpp b/FreeFileSync/Source/ui/small_dlgs.cpp index e46e3424..7adfcc57 100644 --- a/FreeFileSync/Source/ui/small_dlgs.cpp +++ b/FreeFileSync/Source/ui/small_dlgs.cpp @@ -190,6 +190,8 @@ private: void OnGdriveUserAdd (wxCommandEvent& event) override; void OnGdriveUserRemove(wxCommandEvent& event) override; void OnGdriveUserSelect(wxCommandEvent& event) override; + void gdriveUpdateDrivesAndSelect(const std::string& accountEmail, const Zstring& sharedDriveName); + void OnDetectServerChannelLimit(wxCommandEvent& event) override; void OnToggleShowPassword(wxCommandEvent& event) override; void OnBrowseCloudFolder (wxCommandEvent& event) override; @@ -223,7 +225,10 @@ private: }; CloudType type_ = CloudType::gdrive; - const SftpLoginInfo sftpDefault_; + const wxString textLoading_ = L'(' + _("Loading...") + L')'; + const wxString textMyDrive_ = _("My Drive"); + + const SftpLogin sftpDefault_; SftpAuthType sftpAuthType_ = sftpDefault_.authType; @@ -249,15 +254,15 @@ CloudSetupDlg::CloudSetupDlg(wxWindow* parent, Zstring& folderPathPhrase, size_t setRelativeFontSize(*m_toggleBtnGdrive, 1.25); setRelativeFontSize(*m_toggleBtnSftp, 1.25); setRelativeFontSize(*m_toggleBtnFtp, 1.25); - setRelativeFontSize(*m_staticTextGdriveUser, 1.25); - setBitmapTextLabel(*m_buttonGdriveAddUser, getResourceImage("user_add" ).ConvertToImage(), m_buttonGdriveAddUser ->GetLabel()); - setBitmapTextLabel(*m_buttonGdriveRemoveUser, getResourceImage("user_remove").ConvertToImage(), m_buttonGdriveRemoveUser->GetLabel()); + setBitmapTextLabel(*m_buttonGdriveAddUser, shrinkImage(getResourceImage("user_add" ).ConvertToImage(), fastFromDIP(20)), m_buttonGdriveAddUser ->GetLabel()); + setBitmapTextLabel(*m_buttonGdriveRemoveUser, shrinkImage(getResourceImage("user_remove").ConvertToImage(), fastFromDIP(20)), m_buttonGdriveRemoveUser->GetLabel()); - m_bitmapGdriveSelectedUser->SetBitmap(getResourceImage("user_selected")); - m_bitmapServer->SetBitmap(shrinkImage(getResourceImage("server").ConvertToImage(), fastFromDIP(24))); - m_bitmapCloud ->SetBitmap(getResourceImage("cloud")); - m_bitmapPerf ->SetBitmap(getResourceImage("speed")); + m_bitmapGdriveUser ->SetBitmap(shrinkImage(getResourceImage("user" ).ConvertToImage(), fastFromDIP(20))); + m_bitmapGdriveDrive->SetBitmap(shrinkImage(getResourceImage("drive" ).ConvertToImage(), fastFromDIP(20))); + m_bitmapServer ->SetBitmap(shrinkImage(getResourceImage("server").ConvertToImage(), fastFromDIP(20))); + m_bitmapCloud ->SetBitmap(getResourceImage("cloud")); + m_bitmapPerf ->SetBitmap(getResourceImage("speed")); m_bitmapServerDir->SetBitmap(IconBuffer::genericDirIcon(IconBuffer::SIZE_SMALL)); m_checkBoxShowPassword->SetValue(false); @@ -278,52 +283,55 @@ CloudSetupDlg::CloudSetupDlg(wxWindow* parent, Zstring& folderPathPhrase, size_t bSizerAuthInner->Add(0, m_panelAuth->GetSize().y); //--------------------------------------------------------- - wxArrayString googleUsers; + std::vector gdriveAccounts; try { - for (const Zstring& googleUser: googleListConnectedUsers()) //throw FileError - googleUsers.push_back(utfTo(googleUser)); + 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(googleUsers); + m_listBoxGdriveUsers->Append(gdriveAccounts); //set default values for Google Drive: use first item of m_listBoxGdriveUsers - m_staticTextGdriveUser->SetLabel(L""); - if (!googleUsers.empty()) + if (!gdriveAccounts.empty() && !acceptsItemPathPhraseGdrive(folderPathPhrase)) { m_listBoxGdriveUsers->SetSelection(0); - m_staticTextGdriveUser->SetLabel(googleUsers[0]); + gdriveUpdateDrivesAndSelect(utfTo(gdriveAccounts[0]), Zstring() /*My Drive*/); } m_spinCtrlTimeout->SetValue(sftpDefault_.timeoutSec); - assert(sftpDefault_.timeoutSec == FtpLoginInfo().timeoutSec); //make sure the default values are in sync + 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 Zstring userEmail = extractGdriveEmail(folderPath.afsDevice); //noexcept + const GdriveLogin login = extractGdriveLogin(folderPath.afsDevice); //noexcept - if (const int selIdx = m_listBoxGdriveUsers->FindString(utfTo(userEmail), false /*caseSensitive*/); - selIdx != wxNOT_FOUND) + if (const int selPos = m_listBoxGdriveUsers->FindString(utfTo(login.email), false /*caseSensitive*/); + selPos != wxNOT_FOUND) { - m_listBoxGdriveUsers->EnsureVisible(selIdx); - m_listBoxGdriveUsers->SetSelection(selIdx); + m_listBoxGdriveUsers->EnsureVisible(selPos); + m_listBoxGdriveUsers->SetSelection(selPos); + gdriveUpdateDrivesAndSelect(login.email, login.sharedDriveName); } else + { m_listBoxGdriveUsers->DeselectAll(); - m_staticTextGdriveUser->SetLabel (utfTo(userEmail)); - m_textCtrlServerPath ->ChangeValue(utfTo(FILE_NAME_SEPARATOR + folderPath.afsPath.value)); + m_listBoxGdriveDrives->Clear(); + } + + m_textCtrlServerPath->ChangeValue(utfTo(FILE_NAME_SEPARATOR + folderPath.afsPath.value)); } else if (acceptsItemPathPhraseSftp(folderPathPhrase)) { type_ = CloudType::sftp; const AbstractPath folderPath = createItemPathSftp(folderPathPhrase); - const SftpLoginInfo login = extractSftpLogin(folderPath.afsDevice); //noexcept + const SftpLogin login = extractSftpLogin(folderPath.afsDevice); //noexcept if (login.port > 0) m_textCtrlPort->ChangeValue(numberTo(login.port)); @@ -341,7 +349,7 @@ CloudSetupDlg::CloudSetupDlg(wxWindow* parent, Zstring& folderPathPhrase, size_t { type_ = CloudType::ftp; const AbstractPath folderPath = createItemPathFtp(folderPathPhrase); - const FtpLoginInfo login = extractFtpLogin(folderPath.afsDevice); //noexcept + const FtpLogin login = extractFtpLogin(folderPath.afsDevice); //noexcept if (login.port > 0) m_textCtrlPort->ChangeValue(numberTo(login.port)); @@ -386,30 +394,30 @@ CloudSetupDlg::CloudSetupDlg(wxWindow* parent, Zstring& folderPathPhrase, size_t void CloudSetupDlg::OnGdriveUserAdd(wxCommandEvent& event) { - guiQueue_.processAsync([]() -> std::variant + guiQueue_.processAsync([]() -> std::variant { try { - return googleAddUser(nullptr /*updateGui*/); //throw FileError + return gdriveAddUser(nullptr /*updateGui*/); //throw FileError } catch (const FileError& e) { return e; } }, - [this](const std::variant& result) + [this](const std::variant& result) { if (const FileError* e = std::get_if(&result)) showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e->toString())); else { - const wxString googleUser = utfTo(std::get(result)); + const std::string& loginEmail = std::get(result); - int selIdx = m_listBoxGdriveUsers->FindString(googleUser, false /*caseSensitive*/); - if (selIdx == wxNOT_FOUND) - selIdx = m_listBoxGdriveUsers->Append(googleUser); + int selPos = m_listBoxGdriveUsers->FindString(utfTo(loginEmail), false /*caseSensitive*/); + if (selPos == wxNOT_FOUND) + selPos = m_listBoxGdriveUsers->Append(utfTo(loginEmail)); - m_listBoxGdriveUsers->EnsureVisible(selIdx); - m_listBoxGdriveUsers->SetSelection(selIdx); - m_staticTextGdriveUser->SetLabel(googleUser); + m_listBoxGdriveUsers->EnsureVisible(selPos); + m_listBoxGdriveUsers->SetSelection(selPos); updateGui(); //enable remove user button + gdriveUpdateDrivesAndSelect(loginEmail, Zstring() /*My Drive*/); } }); } @@ -417,22 +425,22 @@ void CloudSetupDlg::OnGdriveUserAdd(wxCommandEvent& event) void CloudSetupDlg::OnGdriveUserRemove(wxCommandEvent& event) { - const int selIdx = m_listBoxGdriveUsers->GetSelection(); - assert(selIdx != wxNOT_FOUND); - if (selIdx != wxNOT_FOUND) + const int selPos = m_listBoxGdriveUsers->GetSelection(); + assert(selPos != wxNOT_FOUND); + if (selPos != wxNOT_FOUND) try { - const wxString googleUser = m_listBoxGdriveUsers->GetString(selIdx); + 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", googleUser)), + setMainInstructions(replaceCpy(_("Do you really want to disconnect from user account %x?"), L"%x", utfTo(loginEmail))), _("&Disconnect")) != ConfirmationButton::accept) return; - googleRemoveUser(utfTo(googleUser)); //throw FileError - m_listBoxGdriveUsers->Delete(selIdx); + gdriveRemoveUser(loginEmail); //throw FileError + m_listBoxGdriveUsers->Delete(selPos); updateGui(); //disable remove user button - //no need to also clear m_staticTextGdriveUser + m_listBoxGdriveDrives->Clear(); } catch (const FileError& e) { @@ -443,16 +451,56 @@ void CloudSetupDlg::OnGdriveUserRemove(wxCommandEvent& event) void CloudSetupDlg::OnGdriveUserSelect(wxCommandEvent& event) { - const int selIdx = m_listBoxGdriveUsers->GetSelection(); - assert(selIdx != wxNOT_FOUND); - if (selIdx != wxNOT_FOUND) + const int selPos = m_listBoxGdriveUsers->GetSelection(); + assert(selPos != wxNOT_FOUND); + if (selPos != wxNOT_FOUND) { - m_staticTextGdriveUser->SetLabel(m_listBoxGdriveUsers->GetString(selIdx)); + 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& sharedDriveName) +{ + m_listBoxGdriveDrives->Clear(); + m_listBoxGdriveDrives->Append(textLoading_); + + guiQueue_.processAsync([accountEmail]() -> std::variant, FileError> + { + try + { + return gdriveListSharedDrives(accountEmail); //throw FileError + } + catch (const FileError& e) { return e; } + }, + [this, sharedDriveName](const std::variant, FileError>& result) + { + m_listBoxGdriveDrives->Clear(); + + if (const FileError* e = std::get_if(&result)) + showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e->toString())); + else + { + m_listBoxGdriveDrives->Append(textMyDrive_); + + for (const Zstring& driveName : std::get>(result)) + m_listBoxGdriveDrives->Append(utfTo(driveName)); + + const wxString driveNameLabel = sharedDriveName.empty() ? textMyDrive_ : utfTo(sharedDriveName); + + if (const int selPos = m_listBoxGdriveDrives->FindString(driveNameLabel, true /*caseSensitive*/); + selPos != wxNOT_FOUND) + { + m_listBoxGdriveDrives->EnsureVisible(selPos); + m_listBoxGdriveDrives->SetSelection (selPos); + } + } + }); +} + + void CloudSetupDlg::OnDetectServerChannelLimit(wxCommandEvent& event) { assert (type_ == CloudType::sftp); @@ -599,11 +647,28 @@ AbstractPath CloudSetupDlg::getFolderPath() const switch (type_) { case CloudType::gdrive: - return AbstractPath(condenseToGdriveDevice(utfTo(m_staticTextGdriveUser->GetLabel())), serverRelPath); //noexcept + { + 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& sharedDriveName = m_listBoxGdriveDrives->GetString(selPos2); + sharedDriveName != textMyDrive_ && + sharedDriveName != textLoading_) + login.sharedDriveName = utfTo(sharedDriveName); + } + } + return AbstractPath(condenseToGdriveDevice(login), serverRelPath); //noexcept + } case CloudType::sftp: { - SftpLoginInfo login; + SftpLogin login; login.server = utfTo(m_textCtrlServer ->GetValue()); login.port = stringTo (m_textCtrlPort ->GetValue()); //0 if empty login.username = utfTo(m_textCtrlUserName->GetValue()); @@ -618,7 +683,7 @@ AbstractPath CloudSetupDlg::getFolderPath() const case CloudType::ftp: { - FtpLoginInfo login; + FtpLogin login; login.server = utfTo(m_textCtrlServer ->GetValue()); login.port = stringTo (m_textCtrlPort ->GetValue()); //0 if empty login.username = utfTo(m_textCtrlUserName->GetValue()); @@ -1367,6 +1432,8 @@ void OptionsDlg::OnAddRow(wxCommandEvent& event) wxSizeEvent dummy2; onResize(dummy2); + + m_gridCustomCommand->SetFocus(); //make grid cursor visible } @@ -1379,10 +1446,12 @@ void OptionsDlg::OnRemoveRow(wxCommandEvent& event) m_gridCustomCommand->DeleteRows(selectedRow); else m_gridCustomCommand->DeleteRows(m_gridCustomCommand->GetNumberRows() - 1); - } - wxSizeEvent dummy2; - onResize(dummy2); + wxSizeEvent dummy2; + onResize(dummy2); + + m_gridCustomCommand->SetFocus(); //make grid cursor visible + } } diff --git a/FreeFileSync/Source/ui/small_dlgs.h b/FreeFileSync/Source/ui/small_dlgs.h index 1849dc18..921d83e6 100644 --- a/FreeFileSync/Source/ui/small_dlgs.h +++ b/FreeFileSync/Source/ui/small_dlgs.h @@ -7,6 +7,7 @@ #ifndef SMALL_DLGS_H_8321790875018750245 #define SMALL_DLGS_H_8321790875018750245 +#include #include #include "../base/synchronization.h" #include "../config.h" diff --git a/FreeFileSync/Source/ui/tray_icon.cpp b/FreeFileSync/Source/ui/tray_icon.cpp index 1777f200..fbd3fd38 100644 --- a/FreeFileSync/Source/ui/tray_icon.cpp +++ b/FreeFileSync/Source/ui/tray_icon.cpp @@ -197,7 +197,7 @@ private: FfsTrayIcon::FfsTrayIcon(const std::function& requestResume) : trayIcon_(new TaskBarImpl(requestResume)), - iconGenerator_(std::make_unique(getResourceImage("FFS_tray_24x24").ConvertToImage())) + iconGenerator_(std::make_unique(getResourceImage("FFS_tray_24").ConvertToImage())) { trayIcon_->SetIcon(iconGenerator_->get(activeFraction_), activeToolTip_); } diff --git a/FreeFileSync/Source/version/version.h b/FreeFileSync/Source/version/version.h index 0e6cdd0d..ae27463a 100644 --- a/FreeFileSync/Source/version/version.h +++ b/FreeFileSync/Source/version/version.h @@ -3,7 +3,7 @@ namespace fff { -const char ffsVersion[] = "10.24"; //internal linkage! +const char ffsVersion[] = "10.25"; //internal linkage! const char FFS_VERSION_SEPARATOR = '.'; } diff --git a/libcurl/rest.cpp b/libcurl/rest.cpp index 9b609935..1f430ce3 100644 --- a/libcurl/rest.cpp +++ b/libcurl/rest.cpp @@ -119,7 +119,7 @@ HttpSession::Result HttpSession::perform(const std::string& serverRelPath, if (readRequest) { if (std::all_of(extraOptions.begin(), extraOptions.end(), [](const CurlOption& o) { return o.option != CURLOPT_POST; })) - options.emplace_back(CURLOPT_UPLOAD, 1L); //issues HTTP PUT + /**/options.emplace_back(CURLOPT_UPLOAD, 1L); //issues HTTP PUT options.emplace_back(CURLOPT_READDATA, &getBytesToSend); options.emplace_back(CURLOPT_READFUNCTION, getBytesToSendWrapper); } diff --git a/wx+/dc.h b/wx+/dc.h index f1b067ac..f6f5518b 100644 --- a/wx+/dc.h +++ b/wx+/dc.h @@ -73,8 +73,8 @@ class RecursiveDcClipper public: RecursiveDcClipper(wxDC& dc, const wxRect& r) : dc_(dc) { - auto it = clippingAreas.find(&dc); - if (it != clippingAreas.end()) + if (auto it = clippingAreas.find(&dc); + it != clippingAreas.end()) { oldRect_ = it->second; @@ -103,6 +103,9 @@ public: } private: + RecursiveDcClipper (const RecursiveDcClipper&) = delete; + RecursiveDcClipper& operator=(const RecursiveDcClipper&) = delete; + //associate "active" clipping area with each DC inline static std::unordered_map clippingAreas; @@ -156,6 +159,9 @@ public: } private: + BufferedPaintDC (const BufferedPaintDC&) = delete; + BufferedPaintDC& operator=(const BufferedPaintDC&) = delete; + std::optional& buffer_; wxPaintDC paintDc_; }; diff --git a/wx+/graph.cpp b/wx+/graph.cpp index ec338e99..1fb3775c 100644 --- a/wx+/graph.cpp +++ b/wx+/graph.cpp @@ -831,7 +831,7 @@ void Graph2D::render(wxDC& dc) const size_t drawIndexFirst = 0; while (drawIndexFirst < points.size()) { - size_t drawIndexLast = std::find(marker.begin() + drawIndexFirst, marker.end(), true) - marker.begin(); + 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); @@ -841,7 +841,7 @@ void Graph2D::render(wxDC& dc) const 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(), false) - marker.begin(); + drawIndexFirst = std::find(marker.begin() + drawIndexLast, marker.end(), static_cast(false)) - marker.begin(); } } } diff --git a/wx+/grid.cpp b/wx+/grid.cpp index c7b43d4a..0ce5c978 100644 --- a/wx+/grid.cpp +++ b/wx+/grid.cpp @@ -145,24 +145,25 @@ void GridData::drawCellBackground(wxDC& dc, const wxRect& rect, bool enabled, bo } -wxSize GridData::drawCellText(wxDC& dc, const wxRect& rect, const std::wstring& text, int alignment) +void GridData::drawCellText(wxDC& dc, const wxRect& rect, const std::wstring& 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!!! - => skip the wxDC::DrawLabel() cruft and directly call wxDC::DrawText! - */ + /* 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!!! + => skip the wxDC::DrawLabel() cruft and directly call wxDC::DrawText()! */ + assert(!contains(text, L'\n')); + if (text.empty()) + return; //truncate large texts and add ellipsis - assert(!contains(text, L'\n')); + wxString textTrunc = text; + wxSize extentTrunc = textExtentHint ? *textExtentHint : dc.GetTextExtent(text); + assert(!textExtentHint || *textExtentHint == dc.GetTextExtent(text)); //"trust, but verify" :> - std::wstring textTrunc = text; - wxSize extentTrunc = dc.GetTextExtent(textTrunc); if (extentTrunc.GetWidth() > rect.width) { //unlike Windows 7 Explorer, we truncate UTF-16 correctly: e.g. CJK-Ideogramm encodes to TWO wchar_t: utfTo("\xf0\xa4\xbd\x9c"); @@ -182,7 +183,7 @@ wxSize GridData::drawCellText(wxDC& dc, const wxRect& rect, const std::wstring& } const size_t middle = (low + high) / 2; //=> never 0 when "high - low > 1" - const std::wstring& candidate = getUnicodeSubstring(text, 0, middle) + ELLIPSIS; + 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) @@ -207,9 +208,11 @@ wxSize GridData::drawCellText(wxDC& dc, const wxRect& rect, const std::wstring& else if (alignment & wxALIGN_CENTER_VERTICAL) pt.y += static_cast(std::floor((rect.height - extentTrunc.GetHeight()) / 2.0)); //round down negative values, too! - RecursiveDcClipper clip(dc, rect); + //std::unique_ptr clip; -> redundant!? RecursiveDcClipper already used during Grid cell rendering + //if (extentTrunc.GetWidth() > rect.width) + // clip = std::make_unique(dc, rect); + dc.DrawText(textTrunc, pt); - return extentTrunc; } @@ -660,7 +663,7 @@ private: void drawColumnLabel(wxDC& dc, const wxRect& rect, size_t col, ColumnType colType, bool enabled) { - if (auto dataView = refParent().getDataProvider()) + if (auto prov = refParent().getDataProvider()) { const bool isHighlighted = activeResizing_ ? col == activeResizing_ ->getColumn () : //highlight_ column on mouse-over activeClickOrMove_ ? col == activeClickOrMove_->getColumnFrom() : @@ -668,7 +671,7 @@ private: false; RecursiveDcClipper clip(dc, rect); - dataView->renderColumnLabel(dc, rect, colType, enabled, isHighlighted); + prov->renderColumnLabel(dc, rect, colType, enabled, isHighlighted); //draw move target location if (refParent().allowColumnMove_) @@ -1688,21 +1691,21 @@ void Grid::onKeyDown(wxKeyEvent& event) case WXK_PAGEUP: case WXK_NUMPAD_PAGEUP: if (event.ShiftDown()) - selectWithCursorTo(cursorRow - GetClientSize().GetHeight() / rowLabelWin_->getRowHeight()); + selectWithCursorTo(cursorRow - rowLabelWin_->GetClientSize().GetHeight() / rowLabelWin_->getRowHeight()); //else if (event.ControlDown()) // ; else - moveCursorTo(cursorRow - GetClientSize().GetHeight() / rowLabelWin_->getRowHeight()); + moveCursorTo(cursorRow - rowLabelWin_->GetClientSize().GetHeight() / rowLabelWin_->getRowHeight()); return; case WXK_PAGEDOWN: case WXK_NUMPAD_PAGEDOWN: if (event.ShiftDown()) - selectWithCursorTo(cursorRow + GetClientSize().GetHeight() / rowLabelWin_->getRowHeight()); + selectWithCursorTo(cursorRow + rowLabelWin_->GetClientSize().GetHeight() / rowLabelWin_->getRowHeight()); //else if (event.ControlDown()) // ; else - moveCursorTo(cursorRow + GetClientSize().GetHeight() / rowLabelWin_->getRowHeight()); + moveCursorTo(cursorRow + rowLabelWin_->GetClientSize().GetHeight() / rowLabelWin_->getRowHeight()); return; case 'A': //Ctrl + A - select all @@ -2163,7 +2166,7 @@ void Grid::scrollTo(size_t row) size_t Grid::getTopRow() const { - const wxPoint absPos = CalcUnscrolledPosition(wxPoint(0, 0)); + const wxPoint absPos = CalcUnscrolledPosition(wxPoint(0, 0)); const ptrdiff_t row = rowLabelWin_->getRowAtPos(absPos.y); //return -1 for invalid position; >= rowCount if out of range assert((getRowCount() == 0 && row == 0) || (0 <= row && row < static_cast(getRowCount()))); return row; diff --git a/wx+/grid.h b/wx+/grid.h index 662c4fc1..05710e3f 100644 --- a/wx+/grid.h +++ b/wx+/grid.h @@ -111,7 +111,7 @@ public: virtual void renderCell (wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover); virtual int getBestSize (wxDC& dc, size_t row, ColumnType colType); //must correspond to renderCell()! virtual std::wstring getToolTip (size_t row, ColumnType colType) const { return std::wstring(); } - virtual HoverArea getRowMouseHover(size_t row, ColumnType colType, int cellRelativePosX, int cellWidth) { return HoverArea::NONE; } + virtual HoverArea getRowMouseHover (size_t row, ColumnType colType, int cellRelativePosX, int cellWidth) { return HoverArea::NONE; } //label area: virtual std::wstring getColumnLabel(ColumnType colType) const = 0; @@ -123,7 +123,8 @@ public: static wxColor getColorSelectionGradientTo(); //optional helper routines: - static wxSize drawCellText (wxDC& dc, const wxRect& rect, const std::wstring& text, int alignment = wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); //returns text extent + static void drawCellText(wxDC& dc, const wxRect& rect, const std::wstring& text, + int alignment = wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, const wxSize* textExtentHint = nullptr); //returns text extent static wxRect drawCellBorder (wxDC& dc, const wxRect& rect); //returns inner rectangle static void drawCellBackground(wxDC& dc, const wxRect& rect, bool enabled, bool selected, const wxColor& backgroundColor); @@ -285,7 +286,7 @@ private: } private: - std::vector selected_; //effectively a vector of size "number of rows" + std::vector selected_; //effectively a vector of size "number of rows" }; struct VisibleColumn diff --git a/xBRZ/src/xbrz.cpp b/xBRZ/src/xbrz.cpp index 81153375..4e9fe6dc 100644 --- a/xBRZ/src/xbrz.cpp +++ b/xBRZ/src/xbrz.cpp @@ -1148,48 +1148,33 @@ void xbrz::scale(size_t factor, const uint32_t* src, uint32_t* trg, int srcWidth 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); + 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); + 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::ARGB_UNBUFFERED: 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); + 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; } @@ -1201,10 +1186,8 @@ bool xbrz::equalColorTest(uint32_t col1, uint32_t col2, ColorFormat colFmt, doub { switch (colFmt) { - case ColorFormat::RGB: - return ColorDistanceRGB::dist(col1, col2, luminanceWeight) < equalColorTolerance; - case ColorFormat::ARGB: - return ColorDistanceARGB::dist(col1, col2, luminanceWeight) < equalColorTolerance; + case ColorFormat::RGB: return ColorDistanceRGB::dist(col1, col2, luminanceWeight) < equalColorTolerance; + case ColorFormat::ARGB: return ColorDistanceARGB::dist(col1, col2, luminanceWeight) < equalColorTolerance; case ColorFormat::ARGB_UNBUFFERED: return ColorDistanceUnbufferedARGB::dist(col1, col2, luminanceWeight) < equalColorTolerance; } diff --git a/zen/crc.h b/zen/crc.h index 0570cced..1ff22999 100644 --- a/zen/crc.h +++ b/zen/crc.h @@ -28,31 +28,31 @@ inline uint32_t getCrc32(const std::string& str) { return getCrc32(str.begin(), template inline uint16_t getCrc16(ByteIterator first, ByteIterator last) //http://www.sunshine2k.de/articles/coding/crc/understanding_crc.html { - 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(arraySize(crcTable) == 256 && arrayAccumulate(crcTable) == 8380544); 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(arraySize(crcTable) == 256 && arrayAccumulate(crcTable) == 8380544); crc = (crc >> 8) ^ crcTable[(crc ^ b) & 0xFF]; }); return crc; @@ -62,41 +62,42 @@ uint16_t getCrc16(ByteIterator first, ByteIterator last) //http://www.sunshine2k template inline uint32_t getCrc32(ByteIterator first, ByteIterator last) //https://en.wikipedia.org/wiki/Cyclic_redundancy_check { - 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(arraySize(crcTable) == 256 && arrayAccumulate(crcTable) == 549755813760); 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(arraySize(crcTable) == 256 && arrayAccumulate(crcTable) == 549755813760); + crc = (crc >> 8) ^ crcTable[(crc ^ b) & 0xFF]; }); return crc ^ 0xFFFFFFFF; diff --git a/zen/file_access.cpp b/zen/file_access.cpp index 9a49cd55..8f021843 100644 --- a/zen/file_access.cpp +++ b/zen/file_access.cpp @@ -101,10 +101,10 @@ ItemType zen::getItemType(const Zstring& itemPath) //throw FileError THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(itemPath)), "lstat"); if (S_ISLNK(itemInfo.st_mode)) - return ItemType::SYMLINK; + 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 + return ItemType::folder; + return ItemType::file; //S_ISREG || S_ISCHR || S_ISBLK || S_ISFIFO || S_ISSOCK } @@ -128,13 +128,13 @@ std::optional zen::itemStillExists(const Zstring& itemPath) //throw Fi const std::optional parentType = itemStillExists(*parentPath); //throw FileError - if (parentType && *parentType != ItemType::FILE /*obscure, but possible (and not an error)*/) + if (parentType && *parentType != ItemType::file /*obscure, but possible (and not an error)*/) try { traverseFolder(*parentPath, - [&](const FileInfo& fi) { if (fi.itemName == itemName) throw ItemType::FILE; }, - [&](const FolderInfo& fi) { if (fi.itemName == itemName) throw ItemType::FOLDER; }, - [&](const SymlinkInfo& si) { if (si.itemName == itemName) throw ItemType::SYMLINK; }, + [&](const FileInfo& fi) { if (fi.itemName == itemName) throw ItemType::file; }, + [&](const FolderInfo& fi) { if (fi.itemName == itemName) throw ItemType::folder; }, + [&](const SymlinkInfo& si) { if (si.itemName == itemName) throw ItemType::symlink; }, [](const std::wstring& errorMsg) { throw FileError(errorMsg); }); } catch (const ItemType&) //finding the item after getItemType() previously failed is exceptional @@ -171,13 +171,13 @@ namespace } -uint64_t zen::getFreeDiskSpace(const Zstring& path) //throw FileError, returns 0 if not available +int64_t zen::getFreeDiskSpace(const Zstring& path) //throw FileError, returns < 0 if not available { struct ::statfs info = {}; if (::statfs(path.c_str(), &info) != 0) THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot determine free disk space for %x."), L"%x", fmtPath(path)), "statfs"); - - return static_cast(info.f_bsize) * info.f_bavail; + + return static_cast(info.f_bsize) * info.f_bavail; } @@ -237,7 +237,7 @@ void zen::removeDirectoryPlain(const Zstring& dirPath) //throw FileError { ErrorCode ec = getLastError(); //copy before making other system calls! bool symlinkExists = false; - try { symlinkExists = getItemType(dirPath) == ItemType::SYMLINK; } /*throw FileError*/ catch (FileError&) {} //previous exception is more relevant + try { symlinkExists = getItemType(dirPath) == ItemType::symlink; } /*throw FileError*/ catch (FileError&) {} //previous exception is more relevant if (symlinkExists) { @@ -288,7 +288,7 @@ void removeDirectoryImpl(const Zstring& folderPath) //throw FileError void zen::removeDirectoryPlainRecursion(const Zstring& dirPath) //throw FileError { - if (getItemType(dirPath) == ItemType::SYMLINK) //throw FileError + if (getItemType(dirPath) == ItemType::symlink) //throw FileError removeSymlinkPlain(dirPath); //throw FileError else removeDirectoryImpl(dirPath); //throw FileError @@ -503,7 +503,7 @@ void zen::copyItemPermissions(const Zstring& sourcePath, const Zstring& targetPa 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"); - const bool isSymlinkTarget = getItemType(targetPath) == ItemType::SYMLINK; //throw FileError + const bool isSymlinkTarget = getItemType(targetPath) == ItemType::symlink; //throw FileError if (!isSymlinkTarget && //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_FILE_ERROR(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(targetPath)), "chmod"); @@ -516,10 +516,15 @@ void zen::createDirectory(const Zstring& dirPath) //throw FileError, ErrorTarget { auto getErrorMsg = [&] { return replaceCpy(_("Cannot create directory %x."), L"%x", fmtPath(dirPath)); }; - //deliberately don't support creating irregular folders like "...." https://social.technet.microsoft.com/Forums/windows/en-US/ffee2322-bb6b-4fdf-86f9-8f93cf1fa6cb/ - if (endsWith(dirPath, Zstr(' ')) || - endsWith(dirPath, Zstr('.'))) - throw FileError(getErrorMsg(), replaceCpy(L"Invalid trailing character \"%x\".", L"%x", utfTo(dirPath.end()[-1]))); + //don't allow creating irregular folders like "...." https://social.technet.microsoft.com/Forums/windows/en-US/ffee2322-bb6b-4fdf-86f9-8f93cf1fa6cb/ + const Zstring dirName = afterLast(dirPath, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_ALL); + if (std::all_of(dirName.begin(), dirName.end(), [](Zchar c) { return c == Zstr('.'); })) + /**/throw FileError(getErrorMsg(), replaceCpy(L"Invalid folder name %x.", L"%x", fmtPath(dirName))); + + //not critical, but will visually confuse user sooner or later: + if (startsWith(dirName, Zstr(' ')) || + endsWith (dirName, Zstr(' '))) + throw FileError(getErrorMsg(), replaceCpy(L"Folder name %x starts/ends with space character.", L"%x", fmtPath(dirName))); const mode_t mode = S_IRWXU | S_IRWXG | S_IRWXO; //0777, default for newly created directories @@ -545,7 +550,7 @@ void zen::createDirectoryIfMissingRecursion(const Zstring& dirPath) //throw File try //generally we expect that path already exists (see: ffs_paths.cpp) => check first { - if (getItemType(dirPath) != ItemType::FILE) //throw FileError + if (getItemType(dirPath) != ItemType::file) //throw FileError return; } catch (FileError&) {} //not yet existing or access error? let's find out... @@ -560,7 +565,7 @@ void zen::createDirectoryIfMissingRecursion(const Zstring& dirPath) //throw File { try { - if (getItemType(dirPath) != ItemType::FILE) //throw FileError + if (getItemType(dirPath) != ItemType::file) //throw FileError return; //already existing => possible, if createDirectoryIfMissingRecursion() is run in parallel } catch (FileError&) {} //not yet existing or access error diff --git a/zen/file_access.h b/zen/file_access.h index 8fdbde68..66c41fbd 100644 --- a/zen/file_access.h +++ b/zen/file_access.h @@ -33,9 +33,9 @@ bool dirAvailable (const Zstring& dirPath ); // enum class ItemType { - FILE, - FOLDER, - SYMLINK, + file, + folder, + symlink, }; //(hopefully) fast: does not distinguish between error/not existing ItemType getItemType(const Zstring& itemPath); //throw FileError @@ -53,7 +53,7 @@ enum class ProcSymlink void setFileTime(const Zstring& filePath, time_t modTime, ProcSymlink procSl); //throw FileError //symlink handling: always follow: -uint64_t getFreeDiskSpace(const Zstring& path); //throw FileError, returns 0 if not available +int64_t getFreeDiskSpace(const Zstring& path); //throw FileError, returns < 0 if not available VolumeId getVolumeId(const Zstring& itemPath); //throw FileError uint64_t getFileSize(const Zstring& filePath); //throw FileError diff --git a/zen/json.h b/zen/json.h index 725874f7..0d23719c 100644 --- a/zen/json.h +++ b/zen/json.h @@ -43,7 +43,7 @@ struct JsonValue std::string serializeJson(const JsonValue& jval, - const std::string& lineBreak = "\r\n", + const std::string& lineBreak = "\n", const std::string& indent = " "); //noexcept diff --git a/zen/legacy_compiler.h b/zen/legacy_compiler.h index b480aa6d..13cdd8d0 100644 --- a/zen/legacy_compiler.h +++ b/zen/legacy_compiler.h @@ -9,7 +9,6 @@ #include //C++20 - #include //requires C++20 diff --git a/zen/open_ssl.cpp b/zen/open_ssl.cpp index 64b20bb3..f436875b 100644 --- a/zen/open_ssl.cpp +++ b/zen/open_ssl.cpp @@ -5,6 +5,7 @@ // ***************************************************************************** #include "open_ssl.h" +#include #include "base64.h" #include "build_info.h" #include diff --git a/zen/recycler.cpp b/zen/recycler.cpp index 4d6ea1fd..28b2d6c1 100644 --- a/zen/recycler.cpp +++ b/zen/recycler.cpp @@ -34,7 +34,7 @@ bool zen::recycleOrDeleteIfExists(const Zstring& itemPath) //throw FileError //implement same behavior as in Windows: if recycler is not existing, delete permanently if (error && error->code == G_IO_ERROR_NOT_SUPPORTED) { - if (*type == ItemType::FOLDER) + if (*type == ItemType::folder) removeDirectoryPlainRecursion(itemPath); //throw FileError else removeFilePlain(itemPath); //throw FileError diff --git a/zen/scope_guard.h b/zen/scope_guard.h index 5d3ac411..846d5663 100644 --- a/zen/scope_guard.h +++ b/zen/scope_guard.h @@ -60,8 +60,7 @@ template inline void runScopeGuardDestructor(F& fun, bool failed, std::integral_constant) noexcept { if (failed) - try { fun(); } - catch (...) { assert(false); } + try { fun(); } catch (...) { assert(false); } } diff --git a/zen/serialize.h b/zen/serialize.h index 1eabcdec..a34f91a7 100644 --- a/zen/serialize.h +++ b/zen/serialize.h @@ -183,6 +183,11 @@ BinContainer bufferedLoad(BufferedInputStream& streamIn) //throw X if (bytesRead < blockSize) //end of file { buffer.resize(buffer.size() - (blockSize - bytesRead)); //caveat: unsigned arithmetics + + //caveat: memory consumption of returned string! + if (buffer.capacity() > buffer.size() * 3 / 2) //reference: in worst case, std::vector with growth factor 1.5 "wastes" 50% of its size as unused capacity + buffer.shrink_to_fit(); //=> shrink if buffer is wasting more than that! + return buffer; } } @@ -199,7 +204,7 @@ void writeArray(BufferedOutputStream& stream, const void* buffer, size_t len) template inline void writeNumber(BufferedOutputStream& stream, const N& num) { - static_assert(IsArithmetic::value || std::is_same_v); + static_assert(IsArithmetic::value || std::is_same_v || std::is_enum_v); writeArray(stream, &num, sizeof(N)); } @@ -227,8 +232,8 @@ void readArray(BufferedInputStream& stream, void* buffer, size_t len) //throw Un template inline N readNumber(BufferedInputStream& stream) //throw UnexpectedEndOfStreamError { - static_assert(IsArithmetic::value || std::is_same_v); - N num = 0; + static_assert(IsArithmetic::value || std::is_same_v || std::is_enum_v); + N num{}; readArray(stream, &num, sizeof(N)); //throw UnexpectedEndOfStreamError return num; } diff --git a/zen/stl_tools.h b/zen/stl_tools.h index 8856ce84..5fd29b6b 100644 --- a/zen/stl_tools.h +++ b/zen/stl_tools.h @@ -55,23 +55,6 @@ void mergeTraversal(Iterator first1, Iterator last1, Iterator first2, Iterator last2, FunctionLeftOnly lo, FunctionBoth bo, FunctionRightOnly ro); - -template Num hashBytes (ByteIterator first, ByteIterator last); -template Num hashBytesAppend(Num hashVal, ByteIterator first, ByteIterator last); - -//support for custom string classes in std::unordered_set/map -struct StringHash -{ - template - size_t operator()(const String& str) const - { - const auto* strFirst = strBegin(str); - return hashBytes(reinterpret_cast(strFirst), - reinterpret_cast(strFirst + strLength(str))); - } -}; - - //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; } @@ -255,30 +238,53 @@ void mergeTraversal(Iterator first1, Iterator last1, //FNV-1a: https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function -template inline -Num hashBytes(ByteIterator first, ByteIterator last) +template +class FNV1aHash { - static_assert(IsInteger::value); +public: + FNV1aHash() {} + + void add(Num n) + { + hashVal_ ^= n; + hashVal_ *= prime_; + } + + Num get() const { return hashVal_; } + +private: + static_assert(IsUnsignedInt::value); static_assert(sizeof(Num) == 4 || sizeof(Num) == 8); //macOS: size_t is "unsigned long" - constexpr Num base = sizeof(Num) == 4 ? 2166136261U : 14695981039346656037ULL; + static constexpr Num base_ = sizeof(Num) == 4 ? 2166136261U : 14695981039346656037ULL; + static constexpr Num prime_ = sizeof(Num) == 4 ? 16777619U : 1099511628211ULL; - return hashBytesAppend(base, first, last); -} + Num hashVal_ = base_; +}; template inline -Num hashBytesAppend(Num hashVal, ByteIterator first, ByteIterator last) +Num hashArray(ByteIterator first, ByteIterator last) { - static_assert(sizeof(typename std::iterator_traits::value_type) == 1); - constexpr Num prime = sizeof(Num) == 4 ? 16777619U : 1099511628211ULL; + using ValType = typename std::iterator_traits::value_type; + static_assert(sizeof(ValType) <= sizeof(Num)); + static_assert(IsInteger::value || std::is_same_v || std::is_same_v); + + FNV1aHash hash; + std::for_each(first, last, [&hash](ValType v) { hash.add(static_cast(v)); }); + return hash.get(); +} + - for (; first != last; ++first) +//support for custom string classes in std::unordered_set/map +struct StringHash +{ + template + size_t operator()(const String& str) const { - hashVal ^= static_cast(*first); - hashVal *= prime; + const auto* strFirst = strBegin(str); + return hashArray(strFirst, strFirst + strLength(str)); } - return hashVal; -} +}; } #endif //STL_TOOLS_H_84567184321434 diff --git a/zen/string_base.h b/zen/string_base.h index 58e5d43a..615c7d2c 100644 --- a/zen/string_base.h +++ b/zen/string_base.h @@ -11,8 +11,8 @@ #include #include #include -#include "string_tools.h" #include +#include "string_tools.h" //Zbase - a policy based string class optimizing performance and flexibility diff --git a/zen/string_tools.h b/zen/string_tools.h index cd26f5fd..eaf1a700 100644 --- a/zen/string_tools.h +++ b/zen/string_tools.h @@ -256,8 +256,8 @@ int compareString(const S& lhs, const T& rhs) const size_t rhsLen = strLength(rhs); //length check *after* strcmpWithNulls(): we do care about natural ordering: e.g. for "compareString(makeUpperCopy(lhs), makeUpperCopy(rhs))" - const int rv = impl::strcmpWithNulls(strBegin(lhs), strBegin(rhs), std::min(lhsLen, rhsLen)); - if (rv != 0) + if (const int rv = impl::strcmpWithNulls(strBegin(lhs), strBegin(rhs), std::min(lhsLen, rhsLen)); + rv != 0) return rv; return static_cast(lhsLen) - static_cast(rhsLen); } @@ -269,8 +269,8 @@ int compareAsciiNoCase(const S& lhs, const T& rhs) const size_t lhsLen = strLength(lhs); const size_t rhsLen = strLength(rhs); - const int rv = impl::strcmpAsciiNoCase(strBegin(lhs), strBegin(rhs), std::min(lhsLen, rhsLen)); - if (rv != 0) + if (const int rv = impl::strcmpAsciiNoCase(strBegin(lhs), strBegin(rhs), std::min(lhsLen, rhsLen)); + rv != 0) return rv; return static_cast(lhsLen) - static_cast(rhsLen); } diff --git a/zen/zstring.cpp b/zen/zstring.cpp index 8b16e02d..690f004b 100644 --- a/zen/zstring.cpp +++ b/zen/zstring.cpp @@ -64,7 +64,7 @@ Zstring getUnicodeNormalForm(const Zstring& str) return outStr; } - catch (SysError&) + catch ([[maybe_unused]] const SysError& e) { assert(false); return str; @@ -177,8 +177,7 @@ int compareNatural(const Zstring& lhs, const Zstring& rhs) const char* const strEndL = strL + lhsNorm.size(); const char* const strEndR = strR + rhsNorm.size(); - /* - - compare strings after conceptually creating blocks of whitespace/numbers/text + /* - 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 @@ -186,8 +185,7 @@ int compareNatural(const Zstring& lhs, const Zstring& rhs) 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 - */ + 6. code is a mess */ for (;;) { if (strL == strEndL || strR == strEndR) diff --git a/zen/zstring.h b/zen/zstring.h index e34d14a3..e262603e 100644 --- a/zen/zstring.h +++ b/zen/zstring.h @@ -26,7 +26,7 @@ using Zstringc = zen::Zbase; //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) +// - different number of upper case chars (e.g. "ߢ => "SS" on macOS) // - output is Unicode-normalized Zstring makeUpperCopy(const Zstring& str); @@ -37,7 +37,7 @@ Zstring getUnicodeNormalForm(const Zstring& str); // 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 -struct LessUnicodeNormal { bool operator()(const Zstring& lhs, const Zstring& rhs) const { return getUnicodeNormalForm(lhs) < getUnicodeNormalForm(rhs);} }; +struct LessUnicodeNormal { bool operator()(const Zstring& lhs, const Zstring& rhs) const { return getUnicodeNormalForm(lhs) < getUnicodeNormalForm(rhs); } }; Zstring replaceCpyAsciiNoCase(const Zstring& str, const Zstring& oldTerm, const Zstring& newTerm); @@ -49,8 +49,10 @@ struct ZstringNoCase //use as STL container key: avoid needless upper-case conve { ZstringNoCase(const Zstring& str) : upperCase(makeUpperCopy(str)) {} Zstring upperCase; + + std::strong_ordering operator<=>(const ZstringNoCase& other) const = default; }; -inline bool operator<(const ZstringNoCase& lhs, const ZstringNoCase& rhs) { return lhs.upperCase < rhs.upperCase; } + //------------------------------------------------------------------------------------------ @@ -60,9 +62,9 @@ inline bool operator<(const ZstringNoCase& lhs, const ZstringNoCase& rhs) { retu // macOS: ignore case + Unicode normalization forms int compareNativePath(const Zstring& lhs, const Zstring& rhs); -inline bool equalNativePath(const Zstring& lhs, const Zstring& rhs) { return compareNativePath(lhs, rhs) == 0; } +inline bool equalNativePath(const Zstring& lhs, const Zstring& rhs) { return compareNativePath(lhs, rhs) == 0; } -struct LessNativePath { bool operator()(const Zstring& lhs, const Zstring& rhs) const { return compareNativePath(lhs, rhs) < 0; } }; +struct LessNativePath { bool operator()(const Zstring& lhs, const Zstring& rhs) const { return compareNativePath(lhs, rhs) < 0; } }; //------------------------------------------------------------------------------------------ int compareNatural(const Zstring& lhs, const Zstring& rhs); -- cgit