diff options
37 files changed, 442 insertions, 265 deletions
diff --git a/Changelog.txt b/Changelog.txt index 01773371..9e4c4fc9 100644 --- a/Changelog.txt +++ b/Changelog.txt @@ -1,3 +1,13 @@ +FreeFileSync 13.9 [2024-12-07] +------------------------------ +Fixed CURLE_SEND_ERROR: OpenSSL SSL_write: SSL_ERROR_SYSCALL, errno 0 +Added comparison and sync context menu options for multiple folder pairs +Show file include/exclude filter directly in tooltip +Fixed file not found error when cancelling file up-/download +Fixed showing cancelled config log status after nothing to sync +Updated translation files + + FreeFileSync 13.8 [2024-11-04] ------------------------------ Support raw IPv6 server address for (S)FTP diff --git a/FreeFileSync/Build/Resources/Icons.zip b/FreeFileSync/Build/Resources/Icons.zip Binary files differindex 22025c9f..54b3e15d 100644 --- a/FreeFileSync/Build/Resources/Icons.zip +++ b/FreeFileSync/Build/Resources/Icons.zip diff --git a/FreeFileSync/Build/Resources/Languages.zip b/FreeFileSync/Build/Resources/Languages.zip Binary files differindex f10706f7..01541b62 100644 --- a/FreeFileSync/Build/Resources/Languages.zip +++ b/FreeFileSync/Build/Resources/Languages.zip diff --git a/FreeFileSync/Build/Resources/cacert.pem b/FreeFileSync/Build/Resources/cacert.pem index f2c24a58..eb11b2fd 100644 --- a/FreeFileSync/Build/Resources/cacert.pem +++ b/FreeFileSync/Build/Resources/cacert.pem @@ -1,7 +1,7 @@ ## ## Bundle of CA Root Certificates ## -## Certificate data from Mozilla as of: Tue Sep 24 03:12:04 2024 GMT +## Certificate data from Mozilla as of: Tue Nov 26 13:58:25 2024 GMT ## ## Find updated versions here: https://curl.se/docs/caextract.html ## @@ -2602,6 +2602,36 @@ vLtoURMMA/cVi4RguYv/Uo7njLwcAjA8+RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+ CAezNIm8BZ/3Hobui3A= -----END CERTIFICATE----- +GLOBALTRUST 2020 +================ +-----BEGIN CERTIFICATE----- +MIIFgjCCA2qgAwIBAgILWku9WvtPilv6ZeUwDQYJKoZIhvcNAQELBQAwTTELMAkGA1UEBhMCQVQx +IzAhBgNVBAoTGmUtY29tbWVyY2UgbW9uaXRvcmluZyBHbWJIMRkwFwYDVQQDExBHTE9CQUxUUlVT +VCAyMDIwMB4XDTIwMDIxMDAwMDAwMFoXDTQwMDYxMDAwMDAwMFowTTELMAkGA1UEBhMCQVQxIzAh +BgNVBAoTGmUtY29tbWVyY2UgbW9uaXRvcmluZyBHbWJIMRkwFwYDVQQDExBHTE9CQUxUUlVTVCAy +MDIwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAri5WrRsc7/aVj6B3GyvTY4+ETUWi +D59bRatZe1E0+eyLinjF3WuvvcTfk0Uev5E4C64OFudBc/jbu9G4UeDLgztzOG53ig9ZYybNpyrO +VPu44sB8R85gfD+yc/LAGbaKkoc1DZAoouQVBGM+uq/ufF7MpotQsjj3QWPKzv9pj2gOlTblzLmM +CcpL3TGQlsjMH/1WljTbjhzqLL6FLmPdqqmV0/0plRPwyJiT2S0WR5ARg6I6IqIoV6Lr/sCMKKCm +fecqQjuCgGOlYx8ZzHyyZqjC0203b+J+BlHZRYQfEs4kUmSFC0iAToexIiIwquuuvuAC4EDosEKA +A1GqtH6qRNdDYfOiaxaJSaSjpCuKAsR49GiKweR6NrFvG5Ybd0mN1MkGco/PU+PcF4UgStyYJ9OR +JitHHmkHr96i5OTUawuzXnzUJIBHKWk7buis/UDr2O1xcSvy6Fgd60GXIsUf1DnQJ4+H4xj04KlG +DfV0OoIu0G4skaMxXDtG6nsEEFZegB31pWXogvziB4xiRfUg3kZwhqG8k9MedKZssCz3AwyIDMvU +clOGvGBG85hqwvG/Q/lwIHfKN0F5VVJjjVsSn8VoxIidrPIwq7ejMZdnrY8XD2zHc+0klGvIg5rQ +mjdJBKuxFshsSUktq6HQjJLyQUp5ISXbY9e2nKd+Qmn7OmMCAwEAAaNjMGEwDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFNwuH9FhN3nkq9XVsxJxaD1qaJwiMB8GA1Ud +IwQYMBaAFNwuH9FhN3nkq9XVsxJxaD1qaJwiMA0GCSqGSIb3DQEBCwUAA4ICAQCR8EICaEDuw2jA +VC/f7GLDw56KoDEoqoOOpFaWEhCGVrqXctJUMHytGdUdaG/7FELYjQ7ztdGl4wJCXtzoRlgHNQIw +4Lx0SsFDKv/bGtCwr2zD/cuz9X9tAy5ZVp0tLTWMstZDFyySCstd6IwPS3BD0IL/qMy/pJTAvoe9 +iuOTe8aPmxadJ2W8esVCgmxcB9CpwYhgROmYhRZf+I/KARDOJcP5YBugxZfD0yyIMaK9MOzQ0MAS +8cE54+X1+NZK3TTN+2/BT+MAi1bikvcoskJ3ciNnxz8RFbLEAwW+uxF7Cr+obuf/WEPPm2eggAe2 +HcqtbepBEX4tdJP7wry+UUTF72glJ4DjyKDUEuzZpTcdN3y0kcra1LGWge9oXHYQSa9+pTeAsRxS +vTOBTI/53WXZFM2KJVj04sWDpQmQ1GwUY7VA3+vA/MRYfg0UFodUJ25W5HCEuGwyEn6CMUO+1918 +oa2u1qsgEu8KwxCMSZY13At1XrFP1U80DhEgB3VDRemjEdqso5nCtnkn4rnvyOL2NSl6dPrFf4IF +YqYK6miyeUcGbvJXqBUzxvd4Sj1Ce2t+/vdG6tHrju+IaFvowdlxfv1k7/9nR4hYJS8+hge9+6jl +gqispdNpQ80xiEmEU5LAsTkbOYMBMMTyqfrQA71yN2BWHzZ8vTmR9W0Nv3vXkg== +-----END CERTIFICATE----- + ANF Secure Server Root CA ========================= -----BEGIN CERTIFICATE----- diff --git a/FreeFileSync/Source/RealTimeSync/gui_generated.cpp b/FreeFileSync/Source/RealTimeSync/gui_generated.cpp index 7ab659cd..c3e7d1cd 100644 --- a/FreeFileSync/Source/RealTimeSync/gui_generated.cpp +++ b/FreeFileSync/Source/RealTimeSync/gui_generated.cpp @@ -164,6 +164,36 @@ MainDlgGenerated::MainDlgGenerated( wxWindow* parent, wxWindowID id, const wxStr m_staticline212 = new wxStaticLine( m_panelMain, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); bSizer1->Add( m_staticline212, 0, wxEXPAND, 5 ); + wxBoxSizer* bSizer131; + bSizer131 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer14; + bSizer14 = new wxBoxSizer( wxHORIZONTAL ); + + m_staticText8 = new wxStaticText( m_panelMain, wxID_ANY, _("Idle time (in seconds):"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText8->Wrap( -1 ); + bSizer14->Add( m_staticText8, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 5 ); + + m_spinCtrlDelay = new wxSpinCtrl( m_panelMain, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 0, 2000000000, 0 ); + m_spinCtrlDelay->SetToolTip( _("Idle time between last detected change and execution of command") ); + + bSizer14->Add( m_spinCtrlDelay, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL|wxALL, 5 ); + + + bSizer131->Add( bSizer14, 0, 0, 5 ); + + m_staticText71 = new wxStaticText( m_panelMain, wxID_ANY, _("Ensures folders are not in heavy use when running the command."), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText71->Wrap( -1 ); + m_staticText71->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + bSizer131->Add( m_staticText71, 0, wxBOTTOM|wxRIGHT|wxLEFT, 5 ); + + + bSizer1->Add( bSizer131, 0, wxALL, 10 ); + + 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 ); @@ -188,24 +218,6 @@ MainDlgGenerated::MainDlgGenerated( wxWindow* parent, wxWindowID id, const wxStr bSizer1->Add( bSizer141, 0, wxALL|wxEXPAND, 10 ); - m_staticline211 = new wxStaticLine( m_panelMain, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); - bSizer1->Add( m_staticline211, 0, wxEXPAND, 5 ); - - wxBoxSizer* bSizer14; - bSizer14 = new wxBoxSizer( wxHORIZONTAL ); - - m_staticText8 = new wxStaticText( m_panelMain, wxID_ANY, _("Minimum idle time (in seconds) before running command:"), wxDefaultPosition, wxDefaultSize, 0 ); - m_staticText8->Wrap( -1 ); - bSizer14->Add( m_staticText8, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 5 ); - - m_spinCtrlDelay = new wxSpinCtrl( m_panelMain, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 0, 2000000000, 0 ); - m_spinCtrlDelay->SetToolTip( _("Idle time between last detected change and execution of command") ); - - bSizer14->Add( m_spinCtrlDelay, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL|wxALL, 5 ); - - - bSizer1->Add( bSizer14, 0, wxALL|wxEXPAND, 10 ); - m_panelMain->SetSizer( bSizer1 ); m_panelMain->Layout(); diff --git a/FreeFileSync/Source/RealTimeSync/gui_generated.h b/FreeFileSync/Source/RealTimeSync/gui_generated.h index b3367031..70830bff 100644 --- a/FreeFileSync/Source/RealTimeSync/gui_generated.h +++ b/FreeFileSync/Source/RealTimeSync/gui_generated.h @@ -69,12 +69,13 @@ protected: wxScrolledWindow* m_scrolledWinFolders; wxBoxSizer* bSizerFolders; wxStaticLine* m_staticline212; + wxStaticText* m_staticText8; + wxSpinCtrl* m_spinCtrlDelay; + wxStaticText* m_staticText71; + wxStaticLine* m_staticline211; wxStaticBitmap* m_bitmapConsole; wxStaticText* m_staticText6; wxTextCtrl* m_textCtrlCommand; - wxStaticLine* m_staticline211; - wxStaticText* m_staticText8; - wxSpinCtrl* m_spinCtrlDelay; wxStaticLine* m_staticline5; zen::BitmapTextButton* m_buttonStart; diff --git a/FreeFileSync/Source/RealTimeSync/main_dlg.cpp b/FreeFileSync/Source/RealTimeSync/main_dlg.cpp index 7e955915..4a4812e9 100644 --- a/FreeFileSync/Source/RealTimeSync/main_dlg.cpp +++ b/FreeFileSync/Source/RealTimeSync/main_dlg.cpp @@ -456,7 +456,7 @@ void MainDialog::insertAddFolder(const std::vector<Zstring>& newFolders, size_t const size_t visibleRows = std::min(additionalFolderPanels_.size(), MAX_ADD_FOLDERS); //up to MAX_ADD_FOLDERS additional folders shall be shown m_scrolledWinFolders->SetMinSize({-1, folderHeight * static_cast<int>(visibleRows)}); - m_panelMain->Layout(); //[!] get scrollbars to update correctly + m_panelMain->Layout(); //[!] get scrollbars to update correctly //adapt delete top folder pair button m_bpButtonRemoveTopFolder->Show(!additionalFolderPanels_.empty()); diff --git a/FreeFileSync/Source/RealTimeSync/monitor.cpp b/FreeFileSync/Source/RealTimeSync/monitor.cpp index 63b35c91..da1061d3 100644 --- a/FreeFileSync/Source/RealTimeSync/monitor.cpp +++ b/FreeFileSync/Source/RealTimeSync/monitor.cpp @@ -118,7 +118,7 @@ std::set<Zstring, LessNativePath> waitForMissingDirs(const std::vector<Zstring>& DirWatcher::Change waitForChanges(const std::set<Zstring, LessNativePath>& folderPaths, //throw FileError const std::function<void(bool readyForSync)>& requestUiUpdate, std::chrono::milliseconds cbInterval) { - if (folderPaths.empty()) //pathological case, but we have to check else this function will wait endlessly + if (folderPaths.empty()) //pathological case, but we have to check or this function waits forever throw FileError(_("A folder input field is empty.")); //should have been checked by caller! std::vector<std::pair<Zstring, std::unique_ptr<DirWatcher>>> watches; diff --git a/FreeFileSync/Source/afs/abstract.h b/FreeFileSync/Source/afs/abstract.h index b9292d33..94c14ead 100644 --- a/FreeFileSync/Source/afs/abstract.h +++ b/FreeFileSync/Source/afs/abstract.h @@ -170,18 +170,18 @@ struct AbstractFileSystem //THREAD-SAFETY: "const" member functions must model t virtual FinalizeResult finalize(const zen::IoCallback& notifyUnbufferedIO /*throw X*/) = 0; //throw FileError, X }; - struct OutputStream //call finalize when done! + struct OutputStream { OutputStream(std::unique_ptr<OutputStreamImpl>&& outStream, const AbstractPath& filePath, std::optional<uint64_t> streamSize); ~OutputStream(); size_t getBlockSize() { return outStream_->getBlockSize(); } //throw FileError size_t tryWrite(const void* buffer, size_t bytesToWrite, const zen::IoCallback& notifyUnbufferedIO /*throw X*/); //throw FileError, X may return short! FinalizeResult finalize(const zen::IoCallback& notifyUnbufferedIO /*throw X*/); //throw FileError, X + //call finalize when done!() when done, or (incomplete) file will be automatically deleted private: std::unique_ptr<OutputStreamImpl> outStream_; //bound! const AbstractPath filePath_; - bool finalizeSucceeded_ = false; const std::optional<uint64_t> bytesExpected_; uint64_t bytesWrittenTotal_ = 0; }; @@ -479,17 +479,6 @@ AbstractFileSystem::OutputStream::OutputStream(std::unique_ptr<OutputStreamImpl> inline AbstractFileSystem::OutputStream::~OutputStream() { - //we delete the file on errors: => file should not have existed prior to creating OutputStream instance!! - outStream_.reset(); //close file handle *before* remove! - - if (!finalizeSucceeded_) //transactional output stream! => clean up! - //- needed for Google Drive: e.g. user might cancel during OutputStreamImpl::finalize(), just after file was written transactionally - //- also for Native: setFileTime() may fail *after* FileOutput::finalize() - try { AbstractFileSystem::removeFilePlain(filePath_); /*throw FileError*/ } - catch (const zen::FileError& e) { zen::logExtraError(e.toString()); } - - warn_static("we should not log if not existing anymore!?: ERROR_FILE_NOT_FOUND: ddddddddddd [DeleteFile]") - //solution: integrate cleanup into ~OutputStreamImpl() including appropriate loggin! } @@ -514,7 +503,6 @@ AbstractFileSystem::FinalizeResult AbstractFileSystem::OutputStream::finalize(co _("Expected:") + L' ' + formatNumber(*bytesExpected_)); const FinalizeResult result = outStream_->finalize(notifyUnbufferedIO); //throw FileError, X - finalizeSucceeded_ = true; return result; } diff --git a/FreeFileSync/Source/afs/ftp.cpp b/FreeFileSync/Source/afs/ftp.cpp index f77e3c8b..b9bbe24b 100644 --- a/FreeFileSync/Source/afs/ftp.cpp +++ b/FreeFileSync/Source/afs/ftp.cpp @@ -182,7 +182,7 @@ std::vector<std::string_view> splitFtpResponse(const std::string& buf) { std::vector<std::string_view> lines; - split2(buf, [](char c) { return isLineBreak(c) || c == '\0'; }, //is 0-char check even needed? + split2(buf, [](const char c) { return isLineBreak(c) || c == '\0'; }, //is 0-char check even needed? [&lines](const std::string_view block) { if (!block.empty()) //consider Windows' <CR><LF> @@ -1522,13 +1522,13 @@ private: { FtpLineParser parser(rawLine); - const std::string_view typeTag = parser.readRange(1, [](char c) //throw SysError + const std::string_view typeTag = parser.readRange(1, [](const char c) //throw SysError { return c == '-' || c == 'b' || c == 'c' || c == 'd' || c == 'l' || c == 'p' || c == 's'; }); //------------------------------------------------------------------------------------ //permissions - parser.readRange(9, [](char c) //throw SysError + parser.readRange(9, [](const char c) //throw SysError { return c == '-' || c == 'r' || c == 'w' || c == 'x' || c == 's' || c == 'S' || c == 't' || c == 'T'; }); @@ -1563,7 +1563,7 @@ private: if (day < 1 || day > 31) throw SysError(L"Failed to parse day of month."); //------------------------------------------------------------------------------------ - const std::string_view timeOrYear = parser.readRange([](char c) { return c == ':' || isDigit(c); }); //throw SysError + const std::string_view timeOrYear = parser.readRange([](const char c) { return c == ':' || isDigit(c); }); //throw SysError parser.readRange(&isWhiteSpace<char>); //throw SysError TimeComp timeComp; @@ -1677,9 +1677,9 @@ private: FtpLineParser parser(line); const int month = stringTo<int>(parser.readRange(2, &isDigit<char>)); //throw SysError - parser.readRange(1, [](char c) { return c == '-' || c == '/'; }); //throw SysError + parser.readRange(1, [](const char c) { return c == '-' || c == '/'; }); //throw SysError const int day = stringTo<int>(parser.readRange(2, &isDigit<char>)); //throw SysError - parser.readRange(1, [](char c) { return c == '-' || c == '/'; }); //throw SysError + parser.readRange(1, [](const char c) { return c == '-' || c == '/'; }); //throw SysError const std::string_view yearString = parser.readRange(&isDigit<char>); //throw SysError parser.readRange(&isWhiteSpace<char>); //throw SysError @@ -1699,11 +1699,11 @@ private: throw SysError(L"Failed to parse modification time."); //------------------------------------------------------------------------------------ int hour = stringTo<int>(parser.readRange(2, &isDigit<char>)); //throw SysError - parser.readRange(1, [](char c) { return c == ':'; }); //throw SysError + parser.readRange(1, [](const char c) { return c == ':'; }); //throw SysError const int minute = stringTo<int>(parser.readRange(2, &isDigit<char>)); //throw SysError if (!isWhiteSpace(parser.peekNextChar())) { - const std::string_view period = parser.readRange(2, [](char c) { return c == 'A' || c == 'P' || c == 'M'; }); //throw SysError + const std::string_view period = parser.readRange(2, [](const char c) { return c == 'A' || c == 'P' || c == 'M'; }); //throw SysError if (period == "PM") { if (0 <= hour && hour < 12) @@ -2087,8 +2087,23 @@ struct OutputStreamFtp : public AFS::OutputStreamImpl ~OutputStreamFtp() { - if (asyncStreamOut_) //finalize() was not called (successfully) + if (asyncStreamOut_) //=> cleanup non-finalized output file + { asyncStreamOut_->setWriteError(std::make_exception_ptr(ThreadStopRequest())); + worker_.join(); + + try //see removeFilePlain() + { + accessFtpSession(login_, [&](FtpSession& session) //throw SysError + { + session.runSingleFtpCommand("DELE " + session.getServerPathInternal(filePath_), true /*requestUtf8*/); //throw SysError, SysErrorFtpProtocol + }); + } + catch (const SysError& e) + { + logExtraError(replaceCpy(_("Cannot delete file %x."), L"%x", fmtPath(getCurlDisplayPath(login_, filePath_))) + L"\n\n" + e.toString()); + } + } } size_t getBlockSize() override { return FTP_BLOCK_SIZE_UPLOAD; } //throw (FileError) @@ -2115,7 +2130,7 @@ struct OutputStreamFtp : public AFS::OutputStreamImpl futUploadDone_.get(); //throw FileError //asyncStreamOut_->checkReadErrors(); //throw FileError -> not needed after *successful* upload - asyncStreamOut_.reset(); //do NOT reset on error, so that ~OutputStreamFtp() will request worker thread to stop + asyncStreamOut_.reset(); //output finalized => no more exceptions from here on! //-------------------------------------------------------------------- AFS::FinalizeResult result; diff --git a/FreeFileSync/Source/afs/gdrive.cpp b/FreeFileSync/Source/afs/gdrive.cpp index 6652b644..6ba67f77 100644 --- a/FreeFileSync/Source/afs/gdrive.cpp +++ b/FreeFileSync/Source/afs/gdrive.cpp @@ -2034,7 +2034,7 @@ public: { FileStateDelta() {} private: - FileStateDelta(const std::shared_ptr<const ItemIdDelta>& cids) : changedIds(cids) {} + explicit FileStateDelta(const std::shared_ptr<const ItemIdDelta>& cids) : changedIds(cids) {} friend class GdriveFileState; std::shared_ptr<const ItemIdDelta> changedIds; //lifetime is managed by caller; access *only* by GdriveFileState! }; @@ -3178,7 +3178,8 @@ struct OutputStreamGdrive : public AFS::OutputStreamImpl OutputStreamGdrive(const GdrivePath& gdrivePath, std::optional<uint64_t> /*streamSize*/, std::optional<time_t> modTime, - std::unique_ptr<PathAccessLock>&& pal) //throw SysError + std::unique_ptr<PathAccessLock>&& pal) : //throw SysError + gdrivePath_(gdrivePath) { std::promise<AFS::FingerPrint> promFilePrint; futFilePrint_ = promFilePrint.get_future(); @@ -3256,8 +3257,36 @@ struct OutputStreamGdrive : public AFS::OutputStreamImpl ~OutputStreamGdrive() { - if (asyncStreamOut_) //finalize() was not called (successfully) + if (asyncStreamOut_) //=> cleanup non-finalized output file + { asyncStreamOut_->setWriteError(std::make_exception_ptr(ThreadStopRequest())); + worker_.join(); + + try //see removeFilePlain() + { + std::optional<std::string> itemId; + const GdrivePersistentSessions::AsyncAccessInfo aai = accessGlobalFileState(gdrivePath_.gdriveLogin, [&](GdriveFileStateAtLocation& fileState) //throw SysError + { + const GdriveFileState::PathStatus ps = fileState.getPathStatus(gdrivePath_.itemPath, false /*followLeafShortcut*/); //throw SysError + if (ps.relPath.empty()) + itemId = ps.existingItemId; + }); + if (itemId) + { + gdriveDeleteItem(*itemId, aai.access); //throw SysError + + //buffer new file state ASAP (don't wait GDRIVE_SYNC_INTERVAL) + accessGlobalFileState(gdrivePath_.gdriveLogin, [&](GdriveFileStateAtLocation& fileState) //throw SysError + { + fileState.all().notifyItemDeleted(aai.stateDelta, *itemId); + }); + } + } + catch (const SysError& e) + { + logExtraError(replaceCpy(_("Cannot delete file %x."), L"%x", fmtPath(getGdriveDisplayPath(gdrivePath_))) + L"\n\n" + e.toString()); + } + } } size_t getBlockSize() override { return GDRIVE_BLOCK_SIZE_UPLOAD; } //throw (FileError) @@ -3285,7 +3314,7 @@ struct OutputStreamGdrive : public AFS::OutputStreamImpl result.filePrint = futFilePrint_.get(); //throw FileError //asyncStreamOut_->checkReadErrors(); //throw FileError -> not needed after *successful* upload - asyncStreamOut_.reset(); //do NOT reset on error, so that ~OutputStreamGdrive() will request worker thread to stop + asyncStreamOut_.reset(); //output finalized => no more exceptions from here on! //-------------------------------------------------------------------- //result.errorModTime -> already (successfully) set during file creation @@ -3300,6 +3329,7 @@ private: if (notifyUnbufferedIO) notifyUnbufferedIO(bytesDelta); //throw X } + const GdrivePath gdrivePath_; int64_t totalBytesReported_ = 0; std::shared_ptr<AsyncStreamBuffer> asyncStreamOut_ = std::make_shared<AsyncStreamBuffer>(GDRIVE_STREAM_BUFFER_SIZE); InterruptibleThread worker_; diff --git a/FreeFileSync/Source/afs/native.cpp b/FreeFileSync/Source/afs/native.cpp index fd6fa928..592a9e4b 100644 --- a/FreeFileSync/Source/afs/native.cpp +++ b/FreeFileSync/Source/afs/native.cpp @@ -379,6 +379,9 @@ struct OutputStreamNative : public AFS::OutputStreamImpl result.filePrint = getNativeFileInfo(fileOut_).filePrint; //throw FileError fileOut_.close(); //throw FileError + //output finalized => no more exceptions from here on! + //-------------------------------------------------------------------- + /* is setting modtime after closing the file handle a pessimization? no, needed for functional correctness, see file_access.cpp::copyNewFile() for macOS/Linux even required on Windows: https://freefilesync.org/forum/viewtopic.php?t=10781 */ diff --git a/FreeFileSync/Source/afs/sftp.cpp b/FreeFileSync/Source/afs/sftp.cpp index 61e3ae3b..37d8f77a 100644 --- a/FreeFileSync/Source/afs/sftp.cpp +++ b/FreeFileSync/Source/afs/sftp.cpp @@ -381,7 +381,7 @@ public: return L"OpenSSH public key"; //OpenSSH SSH-2 public key if (std::count(pkStream.begin(), pkStream.end(), ' ') == 2 && - /**/std::all_of(pkStream.begin(), pkStream.end(), [](char c) { return isDigit(c) || c == ' '; })) + /**/std::all_of(pkStream.begin(), pkStream.end(), [](const char c) { return isDigit(c) || c == ' '; })) return L"SSH-1 public key"; //"-----BEGIN PRIVATE KEY-----" => OpenSSH SSH-2 private key (PKCS#8 PrivateKeyInfo) => should work @@ -1385,8 +1385,8 @@ struct OutputStreamSftp : public AFS::OutputStreamImpl OutputStreamSftp(const SftpLogin& login, //throw FileError const AfsPath& filePath, std::optional<time_t> modTime) : + login_(login), filePath_(filePath), - displayPath_(getSftpDisplayPath(login, filePath)), modTime_(modTime) { try @@ -1404,7 +1404,7 @@ struct OutputStreamSftp : public AFS::OutputStreamImpl return LIBSSH2_ERROR_NONE; }); } - catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(displayPath_)), e.toString()); } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(getSftpDisplayPath(login_, filePath_))), e.toString()); } //NOTE: fileHandle_ still unowned until end of constructor!!! @@ -1413,12 +1413,24 @@ struct OutputStreamSftp : public AFS::OutputStreamImpl ~OutputStreamSftp() { - if (fileHandle_) - try + if (fileHandle_) //=> cleanup non-finalized output file + { + if (!closeFailed_) //otherwise there's no much point in calling libssh2_sftp_close() a second time => let it leak!? + try { close(); /*throw FileError*/ } + catch (const FileError& e) { logExtraError(e.toString()); } + + session_.reset(); //reset before file deletion to potentially get new session if !SshSession::isHealthy() + + try //see removeFilePlain() { - close(); //throw FileError + runSftpCommand(login_, "libssh2_sftp_unlink", //throw SysError, SysErrorSftpProtocol + [&](const SshSession::Details& sd) { return ::libssh2_sftp_unlink(sd.sftpChannel, getLibssh2Path(filePath_)); }); //noexcept! } - catch (const FileError& e) { logExtraError(e.toString()); } + catch (const SysError& e) + { + logExtraError(replaceCpy(_("Cannot delete file %x."), L"%x", fmtPath(getSftpDisplayPath(login_, filePath_))) + L"\n\n" + e.toString()); + } + } } size_t getBlockSize() override { return SFTP_OPTIMAL_BLOCK_SIZE_WRITE; } //throw (FileError) @@ -1445,7 +1457,7 @@ struct OutputStreamSftp : public AFS::OutputStreamImpl ASSERT_SYSERROR(makeUnsigned(bytesWritten) <= bytesToWrite); //better safe than sorry } - catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(displayPath_)), e.toString()); } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(getSftpDisplayPath(login_, filePath_))), e.toString()); } if (notifyUnbufferedIO) notifyUnbufferedIO(bytesWritten); //throw X! @@ -1454,10 +1466,9 @@ struct OutputStreamSftp : public AFS::OutputStreamImpl AFS::FinalizeResult finalize(const IoCallback& notifyUnbufferedIO /*throw X*/) override //throw FileError, X { - //~OutputStreamSftp() would call this one, too, but we want to propagate errors if any: close(); //throw FileError - - //it seems libssh2_sftp_fsetstat() triggers bugs on synology server => set mtime by path! https://freefilesync.org/forum/viewtopic.php?t=1281 + //output finalized => no more exceptions from here on! + //-------------------------------------------------------------------- AFS::FinalizeResult result; //result.filePrint = ... -> not supported by SFTP @@ -1478,13 +1489,18 @@ private: if (!fileHandle_) throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); - ZEN_ON_SCOPE_EXIT(fileHandle_ = nullptr); //reset on error, too! there's no point in, calling libssh2_sftp_close() a second time in ~OutputStreamSftp() try { session_->executeBlocking("libssh2_sftp_close", //throw SysError, SysErrorSftpProtocol [&](const SshSession::Details& sd) { return ::libssh2_sftp_close(fileHandle_); }); //noexcept! + + fileHandle_ = nullptr; + } + catch (const SysError& e) + { + closeFailed_ = true; + throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(getSftpDisplayPath(login_, filePath_))), e.toString()); } - catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(displayPath_)), e.toString()); } } void setModTimeIfAvailable() const //throw FileError, follows symlinks @@ -1497,19 +1513,21 @@ private: attribNew.mtime = static_cast<decltype(attribNew.mtime)>(*modTime_); //32-bit target! loss of data! attribNew.atime = static_cast<decltype(attribNew.atime)>(::time(nullptr)); // + //it seems libssh2_sftp_fsetstat() triggers bugs on synology server => set mtime by path! https://freefilesync.org/forum/viewtopic.php?t=1281 try { session_->executeBlocking("libssh2_sftp_setstat", //throw SysError, SysErrorSftpProtocol [&](const SshSession::Details& sd) { return ::libssh2_sftp_setstat(sd.sftpChannel, getLibssh2Path(filePath_), &attribNew); }); //noexcept! } - catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot write modification time of %x."), L"%x", fmtPath(displayPath_)), e.toString()); } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot write modification time of %x."), L"%x", fmtPath(getSftpDisplayPath(login_, filePath_))), e.toString()); } } } + const SftpLogin login_; const AfsPath filePath_; - const std::wstring displayPath_; - LIBSSH2_SFTP_HANDLE* fileHandle_ = nullptr; const std::optional<time_t> modTime_; + LIBSSH2_SFTP_HANDLE* fileHandle_ = nullptr; + bool closeFailed_ = false; std::shared_ptr<SftpSessionManager::SshSessionShared> session_; }; diff --git a/FreeFileSync/Source/application.cpp b/FreeFileSync/Source/application.cpp index fc9032f3..0f82caa6 100644 --- a/FreeFileSync/Source/application.cpp +++ b/FreeFileSync/Source/application.cpp @@ -524,7 +524,7 @@ void Application::runBatchMode(const Zstring& globalConfigFilePath, const XmlBat //all settings have been read successfully... - /* regular check for program updates -> disabled for batch + /* regular check for software updates -> disabled for batch if (batchCfg.showProgress && manualProgramUpdateRequired()) checkForUpdatePeriodically(globalCfg.lastUpdateCheck); -> WinInet not working when FFS is running as a service!!! https://support.microsoft.com/en-us/help/238425/info-wininet-not-supported-for-use-in-services */ @@ -694,13 +694,13 @@ void Application::runBatchMode(const Zstring& globalConfigFilePath, const XmlBat for (ConfigFileItem& cfi : globalCfg.mainDlg.config.fileHistory) if (equalNativePath(cfi.cfgFilePath, cfgFilePath)) { - assert(!AFS::isNullPath(logFilePath)); assert(r.summary.startTime == syncStartTime); + assert(!AFS::isNullPath(logFilePath)); cfi.lastRunStats = { - logFilePath, std::chrono::system_clock::to_time_t(r.summary.startTime), + logFilePath, r.summary.result, r.summary.statsProcessed.items, r.summary.statsProcessed.bytes, diff --git a/FreeFileSync/Source/base/db_file.cpp b/FreeFileSync/Source/base/db_file.cpp index eb62ccb2..e75f7cdb 100644 --- a/FreeFileSync/Source/base/db_file.cpp +++ b/FreeFileSync/Source/base/db_file.cpp @@ -87,17 +87,17 @@ void saveStreams(const DbStreams& streamList, const AbstractPath& dbPath, const //------------------------------------------------------------------------------------------------------------------------ //already existing: undefined behavior! (e.g. fail/overwrite/auto-rename) - const std::unique_ptr<AFS::OutputStream> fileStreamOut = AFS::getOutputStream(dbPath, + const std::unique_ptr<AFS::OutputStream> byteStreamOut = AFS::getOutputStream(dbPath, memStreamOut.ref().size(), std::nullopt /*modTime*/); //throw FileError unbufferedSave(memStreamOut.ref(), [&](const void* buffer, size_t bytesToWrite) { - return fileStreamOut->tryWrite(buffer, bytesToWrite, notifyUnbufferedIO); //throw FileError, X + return byteStreamOut->tryWrite(buffer, bytesToWrite, notifyUnbufferedIO); //throw FileError, X }, - fileStreamOut->getBlockSize()); //throw FileError, X + byteStreamOut->getBlockSize()); //throw FileError, X - fileStreamOut->finalize(notifyUnbufferedIO); //throw FileError, X + byteStreamOut->finalize(notifyUnbufferedIO); //throw FileError, X } diff --git a/FreeFileSync/Source/base/path_filter.cpp b/FreeFileSync/Source/base/path_filter.cpp index 55cc0cf5..16a206ea 100644 --- a/FreeFileSync/Source/base/path_filter.cpp +++ b/FreeFileSync/Source/base/path_filter.cpp @@ -293,7 +293,7 @@ bool NameFilter::passDirFilter(const Zstring& relDirPath, bool* childItemMightMa bool NameFilter::isNull(const Zstring& includePhrase, const Zstring& excludePhrase) { - return trimCpy(includePhrase) == Zstr("*") && + return trimCpy(includePhrase) == Zstr("*") && //harmonize with ui/folder_pair.cpp tooltip trimCpy(excludePhrase).empty(); //return NameFilter(includePhrase, excludePhrase).isNull(); -> very expensive for huge lists } diff --git a/FreeFileSync/Source/base/synchronization.cpp b/FreeFileSync/Source/base/synchronization.cpp index f632faa2..b241e81a 100644 --- a/FreeFileSync/Source/base/synchronization.cpp +++ b/FreeFileSync/Source/base/synchronization.cpp @@ -2834,7 +2834,7 @@ break2: } } if (!msg.empty()) - callback.reportWarning(_("The versioning folder is part of the synchronization.") + + callback.reportWarning(_("The versioning folder must not be part of the synchronization.") + (shouldExclude ? L' ' + _("The folder should be excluded via filter.") : L"") + msg, warnings.warnVersioningFolderPartOfSync); //throw X } diff --git a/FreeFileSync/Source/localization.cpp b/FreeFileSync/Source/localization.cpp index 22eab503..c6490954 100644 --- a/FreeFileSync/Source/localization.cpp +++ b/FreeFileSync/Source/localization.cpp @@ -11,7 +11,6 @@ #include <wx/zipstrm.h> #include <wx/mstream.h> #include <wx/uilocale.h> -#include "parse_plural.h" #include "parse_lng.h" using namespace zen; @@ -91,36 +90,49 @@ FFSTranslation::FFSTranslation(const std::string& lngStream, bool haveRtlLayout) std::vector<TranslationInfo> loadTranslations(const Zstring& zipPath) //throw FileError { - std::vector<std::pair<Zstring /*file name*/, std::string /*byte stream*/>> streams; - - try //to load from ZIP first: + std::vector<std::pair<Zstring /*file path*/, std::string /*byte stream*/>> streams; + [&] { - const std::string rawStream = getFileContent(zipPath, nullptr /*notifyUnbufferedIO*/); //throw FileError - wxMemoryInputStream memStream(rawStream.c_str(), rawStream.size()); //does not take ownership - wxZipInputStream zipStream(memStream, wxConvUTF8); + std::string rawStream; + try //to load from ZIP first: + { + rawStream = getFileContent(zipPath, nullptr /*notifyUnbufferedIO*/); //throw FileError + } + catch (FileError&) //fall back to folder: dev build (only!?) + { + const Zstring fallbackFolder = beforeLast(zipPath, Zstr(".zip"), IfNotFoundReturn::none); + if (!itemExists(fallbackFolder)) //throw FileError + throw; + + traverseFolder(fallbackFolder, [&](const FileInfo& fi) + { + if (endsWith(fi.fullPath, Zstr(".lng"))) + { + std::string stream = getFileContent(fi.fullPath, nullptr /*notifyUnbufferedIO*/); //throw FileError + streams.emplace_back(fi.fullPath, std::move(stream)); + } + }, nullptr, nullptr); //throw FileError + return; + } + //------------------------------------------------------------- + + wxMemoryInputStream byteStream(rawStream.c_str(), rawStream.size()); //does not take ownership + wxZipInputStream zipStream(byteStream, wxConvUTF8); while (const auto& entry = std::unique_ptr<wxZipEntry>(zipStream.GetNextEntry())) //take ownership! + { + if (entry->IsDir()) //e.g. translators accidentally ZIPing "Languages" directory + throw FileError(replaceCpy(replaceCpy<std::wstring>(L"ZIP file %x contains unexpected sub directory %y.", + L"%x", fmtPath(zipPath)), + L"%y", fmtPath(utfTo<std::wstring>(entry->GetName())))); + if (std::string stream(entry->GetSize(), '\0'); zipStream.ReadAll(stream.data(), stream.size())) - streams.emplace_back(utfTo<Zstring>(entry->GetName()), std::move(stream)); + streams.emplace_back(zipPath + Zstr(':') + utfTo<Zstring>(entry->GetName()), std::move(stream)); else assert(false); - } - catch (FileError&) //fall back to folder: dev build (only!?) - { - const Zstring fallbackFolder = beforeLast(zipPath, Zstr(".zip"), IfNotFoundReturn::none); - if (!itemExists(fallbackFolder)) //throw FileError - throw; - - traverseFolder(fallbackFolder, [&](const FileInfo& fi) - { - if (endsWith(fi.fullPath, Zstr(".lng"))) - { - std::string stream = getFileContent(fi.fullPath, nullptr /*notifyUnbufferedIO*/); //throw FileError - streams.emplace_back(fi.itemName, std::move(stream)); - } - }, nullptr, nullptr); //throw FileError - } + } + }(); //-------------------------------------------------------------------- std::vector<TranslationInfo> translations @@ -137,7 +149,7 @@ std::vector<TranslationInfo> loadTranslations(const Zstring& zipPath) //throw Fi } }; - for (/*const*/ auto& [fileName, stream] : streams) + for (/*const*/ auto& [filePath, stream] : streams) try { const lng::TransHeader lngHeader = lng::parseHeader(stream); //throw ParsingError @@ -156,14 +168,14 @@ std::vector<TranslationInfo> loadTranslations(const Zstring& zipPath) //throw Fi .languageName = utfTo<std::wstring>(lngHeader.languageName), .translatorName = utfTo<std::wstring>(lngHeader.translatorName), .languageFlag = lngHeader.flagFile, - .lngFileName = fileName, + .lngFileName = filePath, .lngStream = std::move(stream), }); } catch (const lng::ParsingError& e) { throw FileError(replaceCpy(replaceCpy(replaceCpy(_("Error parsing file %x, row %y, column %z."), - L"%x", fmtPath(fileName)), + L"%x", fmtPath(filePath)), L"%y", formatNumber(e.row + 1)), L"%z", formatNumber(e.col + 1)) + L"\n\n" + e.msg); diff --git a/FreeFileSync/Source/parse_lng.h b/FreeFileSync/Source/parse_lng.h index 2d3dae91..01582e17 100644 --- a/FreeFileSync/Source/parse_lng.h +++ b/FreeFileSync/Source/parse_lng.h @@ -39,11 +39,11 @@ struct ParsingError size_t row = 0; //starting with 0 size_t col = 0; // }; -TransHeader parseHeader(const std::string& fileStream); //throw ParsingError -void parseLng(const std::string& fileStream, TransHeader& header, TranslationMap& out, TranslationPluralMap& pluralOut); //throw ParsingError +TransHeader parseHeader(const std::string& byteStream); //throw ParsingError +void parseLng(const std::string& byteStream, TransHeader& header, TranslationMap& out, TranslationPluralMap& pluralOut); //throw ParsingError class TranslationUnorderedList; //unordered list of unique translation items -std::string generateLng(const TranslationUnorderedList& in, const TransHeader& header); +std::string generateLng(const TranslationUnorderedList& in, const TransHeader& header, bool untranslatedToTop); @@ -147,23 +147,8 @@ private: enum class TokenType { - //header information headerBegin, headerEnd, - langNameBegin, - langNameEnd, - transNameBegin, - transNameEnd, - localeBegin, - localeEnd, - flagFileBegin, - flagFileEnd, - pluralCountBegin, - pluralCountEnd, - pluralDefBegin, - pluralDefEnd, - - //item level srcBegin, srcEnd, trgBegin, @@ -204,23 +189,8 @@ public: private: const TokenMap tokens_ = { - //header details - {TokenType::headerBegin, "<header>"}, - {TokenType::headerEnd, "</header>"}, - {TokenType::langNameBegin, "<language>"}, - {TokenType::langNameEnd, "</language>"}, - {TokenType::transNameBegin, "<translator>"}, - {TokenType::transNameEnd, "</translator>"}, - {TokenType::localeBegin, "<locale>"}, - {TokenType::localeEnd, "</locale>"}, - {TokenType::flagFileBegin, "<image>"}, - {TokenType::flagFileEnd, "</image>"}, - {TokenType::pluralCountBegin, "<plural_count>"}, - {TokenType::pluralCountEnd, "</plural_count>"}, - {TokenType::pluralDefBegin, "<plural_definition>"}, - {TokenType::pluralDefEnd, "</plural_definition>"}, - - //item level + {TokenType::headerBegin, "<header>"}, + {TokenType::headerEnd, "</header>"}, {TokenType::srcBegin, "<source>"}, {TokenType::srcEnd, "</source>"}, {TokenType::trgBegin, "<target>"}, @@ -234,7 +204,7 @@ private: class Scanner { public: - Scanner(const std::string& byteStream) : stream_(byteStream), pos_(stream_.begin()) + explicit Scanner(const std::string& byteStream) : stream_(byteStream), pos_(stream_.begin()) { if (zen::startsWith(stream_, zen::BYTE_ORDER_MARK_UTF8)) pos_ += zen::strLength(zen::BYTE_ORDER_MARK_UTF8); @@ -327,11 +297,11 @@ private: class LngParser { public: - explicit LngParser(const std::string& fileStream) : scn_(fileStream), tk_(scn_.getNextToken()) {} + explicit LngParser(const std::string& byteStream) : scn_(byteStream), tk_(scn_.getNextToken()) {} - void parse(TranslationMap& out, TranslationPluralMap& pluralOut, TransHeader& header) + void parse(TranslationMap& out, TranslationPluralMap& pluralOut, TransHeader& header) //throw ParsingError { - parseHeader(header); + parseHeader(header); //throw ParsingError try { @@ -339,7 +309,7 @@ public: //items while (token().type != TokenType::end) - parseRegular(out, pluralOut, pi); + parseRegular(out, pluralOut, pi); //throw ParsingError } catch (const plural::InvalidPluralForm&) { @@ -347,50 +317,50 @@ public: } } - void parseHeader(TransHeader& header) + void parseHeader(TransHeader& header) //throw ParsingError { - consumeToken(TokenType::headerBegin); //throw ParsingError + using namespace zen; - consumeToken(TokenType::langNameBegin); //throw ParsingError - header.languageName = token().text; - consumeToken(TokenType::text); //throw ParsingError - consumeToken(TokenType::langNameEnd); // + consumeToken(TokenType::headerBegin); //throw ParsingError - consumeToken(TokenType::transNameBegin); //throw ParsingError - header.translatorName = token().text; - consumeToken(TokenType::text); //throw ParsingError - consumeToken(TokenType::transNameEnd); // + const std::string headerRaw = token().text; + consumeToken(TokenType::text); //throw ParsingError - consumeToken(TokenType::localeBegin); //throw ParsingError - header.locale = token().text; - consumeToken(TokenType::text); //throw ParsingError - consumeToken(TokenType::localeEnd); // + std::unordered_map<std::string_view, std::string_view> items; - consumeToken(TokenType::flagFileBegin); //throw ParsingError - header.flagFile = token().text; - consumeToken(TokenType::text); //throw ParsingError - consumeToken(TokenType::flagFileEnd); // + split2(headerRaw, [](const char c) { return isLineBreak(c); }, + [&items](const std::string_view block) + { + const std::string_view name = trimCpy(beforeFirst(block, ':', IfNotFoundReturn::none)); + if (!name.empty()) + items.emplace(name, trimCpy(afterFirst(block, ':', IfNotFoundReturn::none))); + }); - consumeToken(TokenType::pluralCountBegin); //throw ParsingError - header.pluralCount = zen::stringTo<int>(token().text); - consumeToken(TokenType::text); //throw ParsingError - consumeToken(TokenType::pluralCountEnd); // + auto getValue = [&](const std::string_view name) + { + auto it = items.find(name); + if (it == items.end()) + throw ParsingError({replaceCpy<std::wstring>(L"Cannot find header item \"%x:\"", L"%x", utfTo<std::wstring>(name)), scn_.posRow(), scn_.posCol()}); + return it->second; + }; - consumeToken(TokenType::pluralDefBegin); //throw ParsingError - header.pluralDefinition = token().text; - consumeToken(TokenType::text); //throw ParsingError - consumeToken(TokenType::pluralDefEnd); // + header.languageName = getValue("language"); //throw ParsingError + header.locale = getValue("locale"); //throw ParsingError + header.flagFile = getValue("image"); //throw ParsingError + header.pluralCount = stringTo<int>(getValue("plural_count")); //throw ParsingError + header.pluralDefinition = getValue("plural_definition"); //throw ParsingError + header.translatorName = getValue("translator"); //throw ParsingError consumeToken(TokenType::headerEnd); //throw ParsingError } private: - void parseRegular(TranslationMap& out, TranslationPluralMap& pluralOut, const plural::PluralFormInfo& pluralInfo) + void parseRegular(TranslationMap& out, TranslationPluralMap& pluralOut, const plural::PluralFormInfo& pluralInfo) //throw ParsingError { consumeToken(TokenType::srcBegin); //throw ParsingError if (token().type == TokenType::pluralBegin) - return parsePlural(pluralOut, pluralInfo); + return parsePlural(pluralOut, pluralInfo); //throw ParsingError std::string original = token().text; consumeToken(TokenType::text); //throw ParsingError @@ -409,7 +379,7 @@ private: out.emplace(original, std::move(translation)); } - void parsePlural(TranslationPluralMap& pluralOut, const plural::PluralFormInfo& pluralInfo) + void parsePlural(TranslationPluralMap& pluralOut, const plural::PluralFormInfo& pluralInfo) //throw ParsingError { //TokenType::srcBegin already consumed @@ -576,7 +546,7 @@ private: contains(original.second, placeholder)) for (const std::string& str : allTexts) if (!contains(str, placeholder)) - throw ParsingError({zen::replaceCpy<std::wstring>(L"Placeholder %x missing in text", L"%x", zen::utfTo<std::wstring>(placeholder)), scn_.posRow(), scn_.posCol()}); + throw ParsingError({replaceCpy<std::wstring>(L"Placeholder %x missing in text", L"%x", utfTo<std::wstring>(placeholder)), scn_.posRow(), scn_.posCol()}); }; checkSecondaryPlaceholder("%y"); checkSecondaryPlaceholder("%z"); @@ -667,8 +637,8 @@ private: endsWith(s, "\xe3\x80\x82")) //chinese period && (!endsWith(s, "..") && - !endsWith(s, "\xe0\xa5\xa4\xe0\xa5\xa4") && //hindi period - !endsWith(s, "\xe3\x80\x82\xe3\x80\x82")); //chinese period + !endsWith(s, "\xe0\xa5\xa4" "\xe0\xa5\xa4") && //hindi period + !endsWith(s, "\xe3\x80\x82" "\xe3\x80\x82")); //chinese period } @@ -694,20 +664,20 @@ private: inline -void parseLng(const std::string& fileStream, TransHeader& header, TranslationMap& out, TranslationPluralMap& pluralOut) //throw ParsingError +void parseLng(const std::string& byteStream, TransHeader& header, TranslationMap& out, TranslationPluralMap& pluralOut) //throw ParsingError { out.clear(); pluralOut.clear(); - LngParser(fileStream).parse(out, pluralOut, header); + LngParser(byteStream).parse(out, pluralOut, header); //throw ParsingError } inline -TransHeader parseHeader(const std::string& fileStream) //throw ParsingError +TransHeader parseHeader(const std::string& byteStream) //throw ParsingError { TransHeader header; - LngParser(fileStream).parseHeader(header); + LngParser(byteStream).parseHeader(header); //throw ParsingError return header; } @@ -730,17 +700,19 @@ void formatMultiLineText(std::string& text) inline std::string generateLng(const TranslationUnorderedList& in, const TransHeader& header, bool untranslatedToTop) { + using namespace zen; + const KnownTokens tokens; //no need for static non-POD! std::string headerLines; headerLines += tokens.text(TokenType::headerBegin) + '\n'; - headerLines += '\t' + tokens.text(TokenType::langNameBegin) + header.languageName + tokens.text(TokenType::langNameEnd) + '\n'; - headerLines += '\t' + tokens.text(TokenType::transNameBegin) + header.translatorName + tokens.text(TokenType::transNameEnd) + '\n'; - headerLines += '\t' + tokens.text(TokenType::localeBegin) + header.locale + tokens.text(TokenType::localeEnd) + '\n'; - headerLines += '\t' + tokens.text(TokenType::flagFileBegin) + header.flagFile + tokens.text(TokenType::flagFileEnd) + '\n'; - headerLines += '\t' + tokens.text(TokenType::pluralCountBegin) + zen::numberTo<std::string>(header.pluralCount) + tokens.text(TokenType::pluralCountEnd) + '\n'; - headerLines += '\t' + tokens.text(TokenType::pluralDefBegin) + header.pluralDefinition + tokens.text(TokenType::pluralDefEnd) + '\n'; + headerLines += "\t" "language: " + header.languageName + '\n'; + headerLines += "\t" "locale: " + header.locale + '\n'; + headerLines += "\t" "image: " + header.flagFile + '\n'; + headerLines += "\t" "plural_count: " + numberTo<std::string>(header.pluralCount) + '\n'; + headerLines += "\t" "plural_definition: " + header.pluralDefinition + '\n'; + headerLines += "\t" "translator: " + header.translatorName + '\n'; headerLines += tokens.text(TokenType::headerEnd) + "\n\n"; @@ -789,8 +761,8 @@ std::string generateLng(const TranslationUnorderedList& in, const TransHeader& h }); std::string output = headerLines + topLines + mainLines; - assert(!zen::contains(output, "\r\n") && !zen::contains(output, "\r")); - return zen::replaceCpy(output, '\n', "\r\n"); //back to Windows line endings + assert(!contains(output, "\r\n") && !contains(output, "\r")); + return replaceCpy(output, '\n', "\r\n"); //back to Windows line endings } } diff --git a/FreeFileSync/Source/ui/cfg_grid.cpp b/FreeFileSync/Source/ui/cfg_grid.cpp index ff9ae7d6..2d5f8665 100644 --- a/FreeFileSync/Source/ui/cfg_grid.cpp +++ b/FreeFileSync/Source/ui/cfg_grid.cpp @@ -268,13 +268,13 @@ void ConfigView::sortListViewImpl() if (lhs->second.isLastRunCfg != rhs->second.isLastRunCfg) return lhs->second.isLastRunCfg < rhs->second.isLastRunCfg; //"last session" label should be (always) last - const bool wasRunL = lhs->second.cfgItem.lastRunStats.startTime != 0; - const bool wasRunR = rhs->second.cfgItem.lastRunStats.startTime != 0; - if (wasRunL != wasRunR) - return wasRunL > wasRunR; //move sync jobs that were never run to the back + const bool haveResultL = !AFS::isNullPath(lhs->second.cfgItem.lastRunStats.logFilePath); + const bool haveResultR = !AFS::isNullPath(rhs->second.cfgItem.lastRunStats.logFilePath); + if (haveResultL != haveResultR) + return haveResultL > haveResultR; //move sync jobs that were never run to the back //primary sort order - if (wasRunL && lhs->second.cfgItem.lastRunStats.syncResult != rhs->second.cfgItem.lastRunStats.syncResult) + if (haveResultL && lhs->second.cfgItem.lastRunStats.syncResult != rhs->second.cfgItem.lastRunStats.syncResult) return makeSortDirection(std::greater(), std::bool_constant<ascending>())(lhs->second.cfgItem.lastRunStats.syncResult, rhs->second.cfgItem.lastRunStats.syncResult); //secondary sort order @@ -375,10 +375,10 @@ private: return utfTo<std::wstring>(item->name); case ColumnTypeCfg::lastSync: - if (!item->isLastRunCfg) + if (!item->isLastRunCfg && item->cfgItem.lastRunStats.startTime > 0) { - if (item->cfgItem.lastRunStats.startTime == 0) - return std::wstring(1, EN_DASH); + //if (item->cfgItem.lastRunStats.startTime == 0) + // return std::wstring(1, EN_DASH); //return utfTo<std::wstring>(formatTime(formatDateTimeTag, getLocalTime(item->cfgItem.lastRunStats.startTime))); @@ -391,7 +391,7 @@ private: break; case ColumnTypeCfg::lastLog: - if (!item->isLastRunCfg && item->cfgItem.lastRunStats.startTime != 0) + if (!item->isLastRunCfg && !AFS::isNullPath(item->cfgItem.lastRunStats.logFilePath)) return getSyncResultLabel(item->cfgItem.lastRunStats.syncResult); break; } @@ -506,7 +506,7 @@ private: break; case ColumnTypeCfg::lastLog: - if (!item->isLastRunCfg && item->cfgItem.lastRunStats.startTime != 0) + if (!item->isLastRunCfg && !AFS::isNullPath(item->cfgItem.lastRunStats.logFilePath)) { const wxImage statusIcon = [&] { @@ -654,7 +654,7 @@ private: break; case ColumnTypeCfg::lastLog: - if (!item->isLastRunCfg && item->cfgItem.lastRunStats.startTime != 0) + if (!item->isLastRunCfg && !AFS::isNullPath(item->cfgItem.lastRunStats.logFilePath)) { std::wstring tooltip = getSyncResultLabel(item->cfgItem.lastRunStats.syncResult) + L"\n"; @@ -667,8 +667,10 @@ private: const int64_t totalTimeSec = std::chrono::duration_cast<std::chrono::seconds>(item->cfgItem.lastRunStats.totalTime).count(); tooltip += TAB_SPACE + _("Total time:") + L' ' + utfTo<std::wstring>(formatTimeSpan(totalTimeSec)); - //if (!AFS::isNullPath(item->cfgItem.lastRunStats.logFilePath)) - // tooltip += L"\n" + AFS::getDisplayPath(item->cfgItem.lastRunStats.logFilePath); + //non-native path won't be clickable => at least show in tooltip: + if (getNativeItemPath(item->cfgItem.lastRunStats.logFilePath).empty()) + tooltip += L"\n" + AFS::getDisplayPath(item->cfgItem.lastRunStats.logFilePath); + return tooltip; } break; diff --git a/FreeFileSync/Source/ui/cfg_grid.h b/FreeFileSync/Source/ui/cfg_grid.h index 6a40f190..4a757602 100644 --- a/FreeFileSync/Source/ui/cfg_grid.h +++ b/FreeFileSync/Source/ui/cfg_grid.h @@ -18,14 +18,15 @@ namespace fff { struct LastRunStats { - AbstractPath logFilePath = getNullPath(); //optional - time_t startTime = 0; + time_t startTime = 0; //may be updated separately from log file, e.g. "nothing to sync" after comparison + //---------------------------------------------- + AbstractPath logFilePath = getNullPath(); //optional: available <=> sync took place TaskResult syncResult = TaskResult::cancelled; - int itemsProcessed = 0; - int64_t bytesProcessed = 0; + int itemsProcessed = -1; + int64_t bytesProcessed = -1; std::chrono::milliseconds totalTime{}; - int errors = 0; - int warnings = 0; + int errors = -1; + int warnings = -1; }; struct ConfigFileItem diff --git a/FreeFileSync/Source/ui/folder_pair.h b/FreeFileSync/Source/ui/folder_pair.h index 9df67c82..73ace920 100644 --- a/FreeFileSync/Source/ui/folder_pair.h +++ b/FreeFileSync/Source/ui/folder_pair.h @@ -13,6 +13,7 @@ //#include <wx+/bitmap_button.h> #include <wx+/image_tools.h> #include <wx+/image_resources.h> +//#include <wx+/std_button_layout.h> //#include "folder_selector.h" //#include "small_dlgs.h" //#include "sync_cfg.h" @@ -23,6 +24,8 @@ namespace fff { //basic functionality for handling alternate folder pair configuration: change sync-cfg/filter cfg, right-click context menu, button icons... +std::wstring getFilterSummaryForTooltip(const FilterConfig& filterCfg); + template <class GuiPanel> class FolderPairPanelBasic : private wxEvtHandler @@ -61,44 +64,95 @@ private: setImage(*basicPanel_.m_bpButtonLocalCompCfg, greyScaleIfDisabled(imgCmp_, !!localCmpCfg_)); basicPanel_.m_bpButtonLocalCompCfg->SetToolTip(localCmpCfg_ ? - _("Local comparison settings") + L" (" + getVariantName(localCmpCfg_->compareVar) + L')' : + _("Local comparison settings") + L"\n(" + getVariantName(localCmpCfg_->compareVar) + L')' : _("Local comparison settings")); setImage(*basicPanel_.m_bpButtonLocalSyncCfg, greyScaleIfDisabled(imgSync_, !!localSyncCfg_)); basicPanel_.m_bpButtonLocalSyncCfg->SetToolTip(localSyncCfg_ ? - _("Local synchronization settings") + L" (" + getVariantName(getSyncVariant(localSyncCfg_->directionCfg)) + L')' : + _("Local synchronization settings") + L"\n(" + getVariantName(getSyncVariant(localSyncCfg_->directionCfg)) + L')' : _("Local synchronization settings")); setImage(*basicPanel_.m_bpButtonLocalFilter, greyScaleIfDisabled(imgFilter_, !isNullFilter(localFilter_))); - basicPanel_.m_bpButtonLocalFilter->SetToolTip(!isNullFilter(localFilter_) ? - _("Local filter") + L" (" + _("Active") + L')' : - _("Local filter") + L" (" + _("None") + L')'); + basicPanel_.m_bpButtonLocalFilter->SetToolTip(_("Local filter") + getFilterSummaryForTooltip(localFilter_)); } void onLocalCompCfgContext(wxEvent& event) { + using namespace zen; + + ContextMenu menu; + + auto setVariant = [&](CompareVariant var) + { + if (!this->localCmpCfg_) + this->localCmpCfg_ = CompConfig(); + this->localCmpCfg_->compareVar = var; + + this->refreshButtons(); + this->onLocalCompCfgChange(); + }; + + auto addVariantItem = [&](CompareVariant cmpVar, const char* iconName) + { + const wxImage imgSel = loadImage(iconName, -1 /*maxWidth*/, dipToScreen(getMenuIconDipSize())); + + menu.addItem(getVariantName(cmpVar), [&setVariant, cmpVar] { setVariant(cmpVar); }, + greyScaleIfDisabled(imgSel, this->localCmpCfg_ && this->localCmpCfg_->compareVar == cmpVar)); + }; + addVariantItem(CompareVariant::timeSize, "cmp_time"); + addVariantItem(CompareVariant::content, "cmp_content"); + addVariantItem(CompareVariant::size, "cmp_size"); + + //---------------------------------------------------------------------------------------- + menu.addSeparator(); + auto removeLocalCompCfg = [&] { this->localCmpCfg_ = {}; //"this->" galore: workaround GCC compiler bugs this->refreshButtons(); this->onLocalCompCfgChange(); }; - - zen::ContextMenu menu; menu.addItem(_("Remove local settings"), removeLocalCompCfg, wxNullImage, static_cast<bool>(localCmpCfg_)); menu.popup(*basicPanel_.m_bpButtonLocalCompCfg, {basicPanel_.m_bpButtonLocalCompCfg->GetSize().x, 0}); } void onLocalSyncCfgContext(wxEvent& event) { + using namespace zen; + + ContextMenu menu; + + auto setVariant = [&](SyncVariant var) + { + if (!this->localSyncCfg_) + this->localSyncCfg_ = SyncConfig(); + this->localSyncCfg_->directionCfg = getDefaultSyncCfg(var); + + this->refreshButtons(); + this->onLocalSyncCfgChange(); + }; + + auto addVariantItem = [&](SyncVariant syncVar, const char* iconName) + { + const wxImage imgSel = mirrorIfRtl(loadImage(iconName, -1 /*maxWidth*/, dipToScreen(getMenuIconDipSize()))); + + menu.addItem(getVariantName(syncVar), [&setVariant, syncVar] { setVariant(syncVar); }, + greyScaleIfDisabled(imgSel, this->localSyncCfg_ && getSyncVariant(this->localSyncCfg_->directionCfg) == syncVar)); + }; + addVariantItem(SyncVariant::twoWay, "sync_twoway"); + addVariantItem(SyncVariant::mirror, "sync_mirror"); + addVariantItem(SyncVariant::update, "sync_update"); + //addVariantItem(SyncVariant::custom, "sync_custom"); -> doesn't make sense, does it? + + //---------------------------------------------------------------------------------------- + menu.addSeparator(); + auto removeLocalSyncCfg = [&] { this->localSyncCfg_ = {}; this->refreshButtons(); this->onLocalSyncCfgChange(); }; - - zen::ContextMenu menu; menu.addItem(_("Remove local settings"), removeLocalSyncCfg, wxNullImage, static_cast<bool>(localSyncCfg_)); menu.popup(*basicPanel_.m_bpButtonLocalSyncCfg, {basicPanel_.m_bpButtonLocalSyncCfg->GetSize().x, 0}); } @@ -129,10 +183,10 @@ private: }; zen::ContextMenu menu; - menu.addItem( _("Cu&t"), cutFilter, loadImage("item_cut_sicon"), !isNullFilter(localFilter_)); - menu.addSeparator(); menu.addItem( _("&Copy"), copyFilter, loadImage("item_copy_sicon"), !isNullFilter(localFilter_)); menu.addItem( _("&Paste"), pasteFilter, loadImage("item_paste_sicon"), filterCfgOnClipboard.has_value()); + menu.addSeparator(); + menu.addItem( _("Cu&t"), cutFilter, loadImage("item_cut_sicon"), !isNullFilter(localFilter_)); menu.popup(*basicPanel_.m_bpButtonLocalFilter, {basicPanel_.m_bpButtonLocalFilter->GetSize().x, 0}); } @@ -156,6 +210,38 @@ private: const wxImage imgSync_ = zen::loadImage("options_sync", zen::dipToScreen(20)); const wxImage imgFilter_ = zen::loadImage("options_filter", zen::dipToScreen(20)); }; + + +inline +std::wstring getFilterSummaryForTooltip(const FilterConfig& filterCfg) +{ + using namespace zen; + + auto indentLines = [](Zstring str) + { + std::wstring out; + split(str, Zstr('\n'), [&out](ZstringView block) + { + block = trimCpy(block); + if (!block.empty()) + { + out += L'\n'; + out += TAB_SPACE; + out += utfTo<std::wstring>(block); + } + }); + return out; + }; + + std::wstring filterSummary; + if (trimCpy(filterCfg.includeFilter) != Zstr("*")) //harmonize with base/path_filter.cpp NameFilter::isNull + filterSummary += L"\n\n" + _("Include:") + indentLines(filterCfg.includeFilter); + + if (!trimCpy(filterCfg.excludeFilter).empty()) + filterSummary += L"\n\n" + _("Exclude:") + indentLines(filterCfg.excludeFilter); + + return filterSummary; +} } #endif //FOLDER_PAIR_H_89341750847252345 diff --git a/FreeFileSync/Source/ui/gui_generated.cpp b/FreeFileSync/Source/ui/gui_generated.cpp index c377ab5b..b552f0eb 100644 --- a/FreeFileSync/Source/ui/gui_generated.cpp +++ b/FreeFileSync/Source/ui/gui_generated.cpp @@ -1633,7 +1633,7 @@ ConfigDlgGenerated::ConfigDlgGenerated( wxWindow* parent, wxWindowID id, const w m_panelCompSettingsTab->SetSizer( bSizer275 ); m_panelCompSettingsTab->Layout(); bSizer275->Fit( m_panelCompSettingsTab ); - m_notebook->AddPage( m_panelCompSettingsTab, _("dummy"), true ); + m_notebook->AddPage( m_panelCompSettingsTab, _("dummy"), false ); m_panelFilterSettingsTab = new wxPanel( m_notebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); m_panelFilterSettingsTab->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); @@ -1841,7 +1841,7 @@ ConfigDlgGenerated::ConfigDlgGenerated( wxWindow* parent, wxWindowID id, const w wxBoxSizer* bSizer302; bSizer302 = new wxBoxSizer( wxHORIZONTAL ); - m_staticTextFilterDescr = new wxStaticText( m_panel571, wxID_ANY, _("Select filter rules to exclude certain files from synchronization. Enter file paths relative to their corresponding folder pair."), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_staticTextFilterDescr = new wxStaticText( m_panel571, wxID_ANY, _("Select filter rules to exclude certain files from synchronization.\nEnter file paths relative to their corresponding folder pair."), wxDefaultPosition, wxSize( -1, -1 ), 0 ); m_staticTextFilterDescr->Wrap( -1 ); m_staticTextFilterDescr->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); @@ -1994,18 +1994,18 @@ ConfigDlgGenerated::ConfigDlgGenerated( wxWindow* parent, wxWindowID id, const w bSizer3121 = new wxBoxSizer( wxHORIZONTAL ); m_bitmapMoveLeft = new wxStaticBitmap( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), 0 ); - m_bitmapMoveLeft->SetToolTip( _("- Not supported by all file systems\n- Requires and creates database files\n- Detection not available for first sync") ); + m_bitmapMoveLeft->SetToolTip( _("- Available not before the *second* synchronization\n- Not supported by all file systems") ); bSizer3121->Add( m_bitmapMoveLeft, 0, wxALIGN_CENTER_VERTICAL, 5 ); m_bitmapMoveRight = new wxStaticBitmap( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), 0 ); - m_bitmapMoveRight->SetToolTip( _("- Not supported by all file systems\n- Requires and creates database files\n- Detection not available for first sync") ); + m_bitmapMoveRight->SetToolTip( _("- Available not before the *second* synchronization\n- Not supported by all file systems") ); bSizer3121->Add( m_bitmapMoveRight, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 ); m_staticTextDetectMove = new wxStaticText( m_panelSyncSettings, wxID_ANY, _("Detect moved files"), wxDefaultPosition, wxDefaultSize, 0 ); m_staticTextDetectMove->Wrap( -1 ); - m_staticTextDetectMove->SetToolTip( _("- Not supported by all file systems\n- Requires and creates database files\n- Detection not available for first sync") ); + m_staticTextDetectMove->SetToolTip( _("- Available not before the *second* synchronization\n- Not supported by all file systems") ); bSizer3121->Add( m_staticTextDetectMove, 0, wxALIGN_CENTER_VERTICAL, 5 ); @@ -2536,7 +2536,7 @@ ConfigDlgGenerated::ConfigDlgGenerated( wxWindow* parent, wxWindowID id, const w m_panelSyncSettingsTab->SetSizer( bSizer276 ); m_panelSyncSettingsTab->Layout(); bSizer276->Fit( m_panelSyncSettingsTab ); - m_notebook->AddPage( m_panelSyncSettingsTab, _("dummy"), false ); + m_notebook->AddPage( m_panelSyncSettingsTab, _("dummy"), true ); bSizer190->Add( m_notebook, 1, wxEXPAND, 5 ); diff --git a/FreeFileSync/Source/ui/log_panel.cpp b/FreeFileSync/Source/ui/log_panel.cpp index e69e19c7..524ebff2 100644 --- a/FreeFileSync/Source/ui/log_panel.cpp +++ b/FreeFileSync/Source/ui/log_panel.cpp @@ -53,7 +53,7 @@ enum class ColumnTypeLog class fff::MessageView { public: - MessageView(const SharedRef<const ErrorLog>& log) : log_(log) {} + explicit MessageView(const SharedRef<const ErrorLog>& log) : log_(log) {} size_t rowsOnView() const { return viewRef_.size(); } @@ -114,7 +114,7 @@ private: auto it1 = message.begin(); for (;;) { - auto it2 = std::find_if(it1, message.end(), [](char c) { return c == '\n'; }); + auto it2 = std::find_if(it1, message.end(), [](const char c) { return c == '\n'; }); if (textRow == 0) return makeStringView(it1, it2 - it1); diff --git a/FreeFileSync/Source/ui/main_dlg.cpp b/FreeFileSync/Source/ui/main_dlg.cpp index 6ea78d9b..6a3c9660 100644 --- a/FreeFileSync/Source/ui/main_dlg.cpp +++ b/FreeFileSync/Source/ui/main_dlg.cpp @@ -4431,10 +4431,10 @@ void MainDialog::onGlobalFilterContext(wxEvent& event) }; ContextMenu menu; - menu.addItem( _("Cu&t"), cutFilter, loadImage("item_cut_sicon"), !isNullFilter(currentCfg_.mainCfg.globalFilter)); - menu.addSeparator(); menu.addItem( _("&Copy"), copyFilter, loadImage("item_copy_sicon"), !isNullFilter(currentCfg_.mainCfg.globalFilter)); menu.addItem( _("&Paste"), pasteFilter, loadImage("item_paste_sicon"), filterCfgOnClipboard.has_value()); + menu.addSeparator(); + menu.addItem( _("Cu&t"), cutFilter, loadImage("item_cut_sicon"), !isNullFilter(currentCfg_.mainCfg.globalFilter)); menu.popup(*m_bpButtonFilterContext, {m_bpButtonFilterContext->GetSize().x, 0}); } @@ -4550,8 +4550,7 @@ void MainDialog::updateGlobalFilterButton() //global filter: test for Null-filter setImage(*m_bpButtonFilter, greyScaleIfDisabled(loadImage("options_filter"), !isNullFilter(currentCfg_.mainCfg.globalFilter))); - const std::wstring status = !isNullFilter(currentCfg_.mainCfg.globalFilter) ? _("Active") : _("None"); - m_bpButtonFilter->SetToolTip(_("Filter") + L" (F7) (" + status + L')'); + m_bpButtonFilter->SetToolTip(_("Filter") + L" (F7)" + getFilterSummaryForTooltip(currentCfg_.mainCfg.globalFilter)); //m_bpButtonFilterContext->SetToolTip(m_bpButtonFilter->GetToolTipText()); } @@ -5029,8 +5028,8 @@ void MainDialog::onStartSync(wxCommandEvent& event) cfggrid::getDataView(*m_gridCfgHistory).setLastRunStats(activeConfigFiles_, { - logFilePath, std::chrono::system_clock::to_time_t(fullSummary.startTime), + logFilePath, fullSummary.result, fullSummary.statsProcessed.items, fullSummary.statsProcessed.bytes, @@ -6366,7 +6365,7 @@ void MainDialog::onStartupUpdateCheck(wxIdleEvent& event) if (automaticUpdateCheckDue(globalCfg_.lastUpdateCheck)) { - flashStatusInfo(_("Searching for program updates...")); + flashStatusInfo(_("Searching for software updates...")); guiQueue_.processAsync([resultPrep = automaticUpdateCheckPrepare(*this) /*prepare on main thread*/] { return automaticUpdateCheckRunAsync(resultPrep.ref()); }, //run on worker thread: (long-running part of the check) @@ -6379,7 +6378,7 @@ void MainDialog::onStartupUpdateCheck(wxIdleEvent& event) showNewVersionReminder(); if (globalCfg_.lastUpdateCheck == lastUpdateCheckOld) - flashStatusInfo(_("Update check failed!")); + flashStatusInfo(_("Software update check failed!")); }); } else diff --git a/FreeFileSync/Source/ui/small_dlgs.cpp b/FreeFileSync/Source/ui/small_dlgs.cpp index 374c7493..ac4d2405 100644 --- a/FreeFileSync/Source/ui/small_dlgs.cpp +++ b/FreeFileSync/Source/ui/small_dlgs.cpp @@ -1357,7 +1357,7 @@ private: {[](const XmlGlobalSettings& gs){ return gs.warnDlgs.warnDirectoryLockFailed; }, []( XmlGlobalSettings& gs, bool show){ gs.warnDlgs.warnDirectoryLockFailed = show; }, _("Cannot set directory locks for the following folders:")}, {[](const XmlGlobalSettings& gs){ return gs.warnDlgs.warnVersioningFolderPartOfSync; }, - []( XmlGlobalSettings& gs, bool show){ gs.warnDlgs.warnVersioningFolderPartOfSync = show; }, _("The versioning folder is part of the synchronization.") + L' ' + _("The folder should be excluded via filter.")}, + []( XmlGlobalSettings& gs, bool show){ gs.warnDlgs.warnVersioningFolderPartOfSync = show; }, _("The versioning folder must not be part of the synchronization.") + L' ' + _("The folder should be excluded via filter.")}, //*INDENT-ON* }; diff --git a/FreeFileSync/Source/ui/sync_cfg.cpp b/FreeFileSync/Source/ui/sync_cfg.cpp index 790825ac..c7da4516 100644 --- a/FreeFileSync/Source/ui/sync_cfg.cpp +++ b/FreeFileSync/Source/ui/sync_cfg.cpp @@ -34,7 +34,7 @@ using namespace fff; namespace { -const int CFG_DESCRIPTION_WIDTH_DIP = 230; +const int CFG_DESCRIPTION_WIDTH_DIP = 250; const wchar_t arrowRight[] = L"\u2192"; //"RIGHTWARDS ARROW" @@ -567,8 +567,6 @@ globalLogFolderPhrase_(globalLogFolderPhrase) setDefaultWidth(*m_spinCtrlMaxSize); setDefaultWidth(*m_spinCtrlTimespan); - m_staticTextFilterDescr->Wrap(dipToWxsize(450)); - setImage(*m_bpButtonDefaultContext, mirrorIfRtl(loadImage("button_arrow_right"))); enumTimeDescr_. diff --git a/FreeFileSync/Source/ui/version_check.cpp b/FreeFileSync/Source/ui/version_check.cpp index 72f35168..6219d083 100644 --- a/FreeFileSync/Source/ui/version_check.cpp +++ b/FreeFileSync/Source/ui/version_check.cpp @@ -148,7 +148,7 @@ void showUpdateAvailableDialog(wxWindow* parent, const std::string& onlineVersio switch (showConfirmationDialog(parent, DialogInfoType::info, PopupDialogCfg(). setIcon(loadImage("FreeFileSync", dipToScreen(48))). - setTitle(_("Check for Program Updates")). + setTitle(_("Check for Software Updates")). setMainInstructions(replaceCpy(_("FreeFileSync %x is available!"), L"%x", utfTo<std::wstring>(onlineVersion)) + L"\n\n" + _("Download now?")). setDetailInstructions(updateDetailsMsg), _("&Download"))) { @@ -167,7 +167,7 @@ std::string getOnlineVersion(const std::vector<std::pair<std::string, std::strin ffsUpdateCheckUserAgent, Zstring() /*caCertFilePath*/).readAll(nullptr /*notifyUnbufferedIO*/); //throw SysError if (response.empty() || - !std::all_of(response.begin(), response.end(), [](char c) { return isDigit(c) || c == FFS_VERSION_SEPARATOR; }) || + !std::all_of(response.begin(), response.end(), [](const char c) { return isDigit(c) || c == FFS_VERSION_SEPARATOR; }) || startsWith(response, FFS_VERSION_SEPARATOR) || endsWith(response, FFS_VERSION_SEPARATOR) || contains(response, std::string() + FFS_VERSION_SEPARATOR + FFS_VERSION_SEPARATOR)) @@ -180,7 +180,7 @@ std::string getOnlineVersion(const std::vector<std::pair<std::string, std::strin std::string getUnknownVersionTag() { - return '<' + utfTo<std::string>(_("version unknown")) + '>'; + return '<' + utfTo<std::string>(_("unknown version")) + '>'; } } @@ -217,7 +217,7 @@ void fff::checkForUpdateNow(wxWindow& parent, std::string& lastOnlineVersion) else showNotificationDialog(&parent, DialogInfoType::info, PopupDialogCfg(). setIcon(loadImage("update_check")). - setTitle(_("Check for Program Updates")). + setTitle(_("Check for Software Updates")). setMainInstructions(_("FreeFileSync is up-to-date."))); } catch (const SysError& e) @@ -227,7 +227,7 @@ void fff::checkForUpdateNow(wxWindow& parent, std::string& lastOnlineVersion) lastOnlineVersion = getUnknownVersionTag(); switch (showConfirmationDialog(&parent, DialogInfoType::error, PopupDialogCfg(). - setTitle(_("Check for Program Updates")). + setTitle(_("Check for Software Updates")). setMainInstructions(_("Cannot find current FreeFileSync version number online. A newer version is likely available. Check manually now?")). setDetailInstructions(e.toString()), _("&Check"), _("&Retry"))) { @@ -243,7 +243,7 @@ void fff::checkForUpdateNow(wxWindow& parent, std::string& lastOnlineVersion) } else switch (showConfirmationDialog(&parent, DialogInfoType::error, PopupDialogCfg(). - setTitle(_("Check for Program Updates")). + setTitle(_("Check for Software Updates")). setMainInstructions(replaceCpy(_("Unable to connect to %x."), L"%x", L"freefilesync.org")). setDetailInstructions(e.toString()), _("&Retry"))) { @@ -327,7 +327,7 @@ void fff::automaticUpdateCheckEval(wxWindow& parent, time_t& lastUpdateCheck, st { if (lastOnlineVersion != getUnknownVersionTag()) switch (showConfirmationDialog(&parent, DialogInfoType::error, PopupDialogCfg(). - setTitle(_("Check for Program Updates")). + setTitle(_("Check for Software Updates")). setMainInstructions(_("Cannot find current FreeFileSync version number online. A newer version is likely available. Check manually now?")). setDetailInstructions(result.error->toString()), _("&Check"), _("&Retry"))) diff --git a/FreeFileSync/Source/version/version.h b/FreeFileSync/Source/version/version.h index 852cbd04..6d4418f9 100644 --- a/FreeFileSync/Source/version/version.h +++ b/FreeFileSync/Source/version/version.h @@ -3,7 +3,7 @@ namespace fff { -const char ffsVersion[] = "13.8"; //internal linkage! +const char ffsVersion[] = "13.9"; //internal linkage! const char FFS_VERSION_SEPARATOR = '.'; } diff --git a/zen/file_path.cpp b/zen/file_path.cpp index 5941689e..b19aba45 100644 --- a/zen/file_path.cpp +++ b/zen/file_path.cpp @@ -50,7 +50,7 @@ std::optional<PathComponents> zen::parsePathComponents(const Zstring& itemPath) { Zstring tmp(itemPath.begin() + strLength("/run/user/"), itemPath.end()); tmp = beforeFirst(tmp, "/gvfs/", IfNotFoundReturn::none); - if (!tmp.empty() && std::all_of(tmp.begin(), tmp.end(), [](char c) { return isDigit(c); })) + if (!tmp.empty() && std::all_of(tmp.begin(), tmp.end(), [](const char c) { return isDigit(c); })) /**/pc = doParse(6 /*sepCountVolumeRoot*/, false /*rootWithSep*/); } diff --git a/zen/file_traverser.cpp b/zen/file_traverser.cpp index 8564e612..75075b80 100644 --- a/zen/file_traverser.cpp +++ b/zen/file_traverser.cpp @@ -6,7 +6,7 @@ #include "file_traverser.h" #include "file_error.h" - #include "file_access.h" +#include "file_access.h" #include <sys/stat.h> diff --git a/zen/http.cpp b/zen/http.cpp index fcf69fb3..04ee42fd 100644 --- a/zen/http.cpp +++ b/zen/http.cpp @@ -513,7 +513,7 @@ bool zen::isValidEmail(const std::string_view& email) contains(local, '\\'); //e.g. "t\@st@email.com" if (!quoted) for (const std::string_view& comp : splitCpy(local, '.', SplitOnEmpty::allow)) - if (comp.empty() || !std::all_of(comp.begin(), comp.end(), [](char c) + if (comp.empty() || !std::all_of(comp.begin(), comp.end(), [](const char c) { constexpr std::string_view printable("!#$%&'*+-/=?^_`{|}~"); return isAsciiAlpha(c) || isDigit(c) || !isAsciiChar(c) || @@ -531,7 +531,7 @@ bool zen::isValidEmail(const std::string_view& email) for (const std::string_view& comp : splitCpy(domain, '.', SplitOnEmpty::allow)) if (comp.empty() || comp.size() > 63 || - !std::all_of(comp.begin(), comp.end(), [](char c) { return isAsciiAlpha(c) ||isDigit(c) || !isAsciiChar(c) || c == '-'; })) + !std::all_of(comp.begin(), comp.end(), [](const char c) { return isAsciiAlpha(c) ||isDigit(c) || !isAsciiChar(c) || c == '-'; })) return false; } @@ -140,11 +140,11 @@ namespace { UtfDecoder<impl::Char16> decoder(utf16Buf.c_str(), utf16Buf.size()); while (std::optional<impl::CodePoint> cp = decoder.getNext()) - codePointToUtf<char>(*cp, [&](char c) { output += c; }); + codePointToUtf<char>(*cp, [&](const char c) { output += c; }); utf16Buf.clear(); } }; - auto writeOut = [&](char c) + auto writeOut = [&](const char c) { flushUtf16(); output += c; @@ -419,8 +419,8 @@ private: Scanner (const Scanner&) = delete; Scanner& operator=(const Scanner&) = delete; - static bool isJsonWhiteSpace(char c) { return c == ' ' || c == '\t' || c == '\r' || c == '\n'; } - static bool isJsonNumDigit (char c) { return ('0' <= c && c <= '9') || c == '-' || c == '+' || c == '.' || c == 'e'|| c == 'E'; } + static bool isJsonWhiteSpace(const char c) { return c == ' ' || c == '\t' || c == '\r' || c == '\n'; } + static bool isJsonNumDigit (const char c) { return ('0' <= c && c <= '9') || c == '-' || c == '+' || c == '.' || c == 'e'|| c == 'E'; } bool startsWith(const std::string& prefix) const { diff --git a/zen/string_tools.h b/zen/string_tools.h index b95a2055..d7d0804f 100644 --- a/zen/string_tools.h +++ b/zen/string_tools.h @@ -431,7 +431,7 @@ template <class S, class Char, class Function> inline void split(const S& str, Char delimiter, Function onStringPart) { static_assert(std::is_same_v<GetCharTypeT<S>, Char>); - split2(str, [delimiter](Char c) { return c == delimiter; }, onStringPart); + split2(str, [delimiter](const Char c) { return c == delimiter; }, onStringPart); } @@ -441,7 +441,7 @@ std::vector<S> splitCpy(const S& str, Char delimiter, SplitOnEmpty soe) static_assert(std::is_same_v<GetCharTypeT<S>, Char>); std::vector<S> output; - split2(str, [delimiter](Char c) { return c == delimiter; }, [&, soe](std::basic_string_view<Char> block) + split2(str, [delimiter](const Char c) { return c == delimiter; }, [&, soe](std::basic_string_view<Char> block) { if (!block.empty() || soe == SplitOnEmpty::allow) output.emplace_back(block.data(), block.size()); @@ -921,7 +921,7 @@ std::pair<char, char> hexify(unsigned char c, bool upperCase) inline //unhexify beats "::sscanf(&it[3], "%02X", &tmp)" by a factor of 3000 for ~250000 calls!!! char unhexify(char high, char low) { - auto unhexifyDigit = [](char hex) -> int //input 0-9, a-f, A-F; output range: [0, 15] + auto unhexifyDigit = [](const char hex) -> int //input 0-9, a-f, A-F; output range: [0, 15] { if ('0' <= hex && hex <= '9') //no signed/unsigned char problem here! return hex - '0'; diff --git a/zen/sys_version.cpp b/zen/sys_version.cpp index 705fbade..03668709 100644 --- a/zen/sys_version.cpp +++ b/zen/sys_version.cpp @@ -55,8 +55,8 @@ OsVersionDetail zen::getOsVersionDetail() //throw SysError osVersion = utfTo<std::wstring>(afterFirst(line, '=', IfNotFoundReturn::none)); //PRETTY_NAME? too wordy! e.g. "Fedora 17 (Beefy Miracle)" }); - trim(osName, TrimSide::both, [](char c) { return c == L'"' || c == L'\''; }); - trim(osVersion, TrimSide::both, [](char c) { return c == L'"' || c == L'\''; }); + trim(osName, TrimSide::both, [](const char c) { return c == L'"' || c == L'\''; }); + trim(osVersion, TrimSide::both, [](const char c) { return c == L'"' || c == L'\''; }); } if (osName.empty()) diff --git a/zen/zstring.cpp b/zen/zstring.cpp index 019973e9..d011eda0 100644 --- a/zen/zstring.cpp +++ b/zen/zstring.cpp @@ -104,7 +104,7 @@ Zstring getUpperCaseNonAscii(const Zstring& str) UtfDecoder<char> decoder(strNorm.c_str(), strNorm.size()); while (const std::optional<impl::CodePoint> cp = decoder.getNext()) - codePointToUtf<char>(::g_unichar_toupper(*cp), [&](char c) { output += c; }); //don't use std::towupper: *incomplete* and locale-dependent! + codePointToUtf<char>(::g_unichar_toupper(*cp), [&](const char c) { output += c; }); //don't use std::towupper: *incomplete* and locale-dependent! static_assert(sizeof(impl::CodePoint) == sizeof(gunichar)); return output; diff --git a/zenXml/zenxml/parser.h b/zenXml/zenxml/parser.h index d34b2fff..6c7959b4 100644 --- a/zenXml/zenxml/parser.h +++ b/zenXml/zenxml/parser.h @@ -110,7 +110,7 @@ std::string normalize(const std::string_view& str, Predicate pred) //pred: unary inline std::string normalizeName(const std::string& str) { - /*const*/ std::string nameFmt = normalize(str, [](char c) { return isWhiteSpace(c) || c == '=' || c == '/' || c == '\'' || c == '"'; }); + /*const*/ std::string nameFmt = normalize(str, [](const char c) { return isWhiteSpace(c) || c == '=' || c == '/' || c == '\'' || c == '"'; }); assert(!nameFmt.empty()); return nameFmt; } @@ -118,13 +118,13 @@ std::string normalizeName(const std::string& str) inline std::string normalizeElementValue(const std::string& str) { - return normalize(str, [](char c) { return static_cast<unsigned char>(c) < 32; }); + return normalize(str, [](const char c) { return static_cast<unsigned char>(c) < 32; }); } inline std::string normalizeAttribValue(const std::string& str) { - return normalize(str, [](char c) { return static_cast<unsigned char>(c) < 32 || c == '\'' || c == '"'; }); + return normalize(str, [](const char c) { return static_cast<unsigned char>(c) < 32 || c == '\'' || c == '"'; }); } @@ -343,7 +343,7 @@ public: return it->second; } - const auto itNameEnd = std::find_if(pos_, stream_.end(), [](char c) + const auto itNameEnd = std::find_if(pos_, stream_.end(), [](const char c) { return c == '<' || c == '>' || @@ -367,7 +367,7 @@ public: std::string extractElementValue() { - auto it = std::find_if(pos_, stream_.end(), [](char c) + auto it = std::find_if(pos_, stream_.end(), [](const char c) { return c == '<' || c == '>'; @@ -379,7 +379,7 @@ public: std::string extractAttributeValue() { - auto it = std::find_if(pos_, stream_.end(), [](char c) + auto it = std::find_if(pos_, stream_.end(), [](const char c) { return c == '<' || c == '>' || |