浏览代码

Issue 2020 – Synchronize two local directories

https://winscp.net/tracker/2020

Source commit: a0ab887a23e4362bb62824434893a97540b24e4a
Martin Prikryl 2 周之前
父节点
当前提交
3cd64f2a19

+ 45 - 0
source/core/Common.h

@@ -588,6 +588,51 @@ private:
 
 };
 //---------------------------------------------------------------------------
+template <typename T>
+class TComPtr
+{
+public:
+  TComPtr(T * P = nullptr) : FP(P)
+  {
+  }
+
+  ~TComPtr()
+  {
+    Reset(nullptr);
+  }
+
+  void Reset(T * P)
+  {
+    if (FP != nullptr)
+    {
+      FP->Release();
+    }
+    FP = P;
+  }
+
+  T * Detach()
+  {
+    T * Result = FP;
+    FP = nullptr;
+    return Result;
+  }
+
+  // Address-of operator for out parameters
+  T** operator&()
+  {
+    Reset(nullptr); // ensures no leak if reused
+    return &FP;
+  }
+
+  // Accessors
+  T * operator->() const { return FP; }
+  T * Get() const { return FP; }
+  explicit operator bool() const { return FP != nullptr; }
+
+private:
+  T * FP;
+};
+//---------------------------------------------------------------------------
 typedef std::vector<UnicodeString> TUnicodeStringVector;
 //---------------------------------------------------------------------------
 #endif

+ 11 - 1
source/core/Configuration.cpp

@@ -1902,7 +1902,17 @@ UnicodeString __fastcall TConfiguration::GetDirectoryStatisticsCacheKey(
 {
   std::unique_ptr<TStringList> RawOptions(new TStringList());
   RawOptions->Add(SessionKey);
-  RawOptions->Add(UnixExcludeTrailingBackslash(Path));
+
+  UnicodeString PathKey;
+  if (SessionKey.IsEmpty())
+  {
+    PathKey = ExcludeTrailingBackslash(Path).LowerCase();
+  }
+  else
+  {
+    PathKey = UnixExcludeTrailingBackslash(Path);
+  }
+  RawOptions->Add(PathKey);
 
   TCopyParamType Defaults;
   TCopyParamType FilterCopyParam;

+ 177 - 0
source/core/RemoteFiles.cpp

@@ -5,6 +5,7 @@
 #include "RemoteFiles.h"
 #include "Terminal.h"
 #include "Cryptography.h"
+#include <System.Win.ComObj.hpp>
 /* TODO 1 : Path class instead of UnicodeString (handle relativity...) */
 //---------------------------------------------------------------------------
 const UnicodeString PartialExt(L".filepart");
@@ -2939,11 +2940,21 @@ UnicodeString TSynchronizeChecklist::TItem::GetLocalPath() const
   return CombinePaths(Info1.Directory, Info1.FileName);
 }
 //---------------------------------------------------------------------------
+UnicodeString TSynchronizeChecklist::TItem::GetLocalPath2() const
+{
+  return CombinePaths(Info2.Directory, Info2.FileName);
+}
+//---------------------------------------------------------------------------
 UnicodeString TSynchronizeChecklist::TItem::ForceGetLocalPath() const
 {
   return CombinePaths(Info1.Directory, DefaultStr(Info1.FileName, Info2.FileName));
 }
 //---------------------------------------------------------------------------
+UnicodeString TSynchronizeChecklist::TItem::ForceGetLocalPath2() const
+{
+  return CombinePaths(Info2.Directory, DefaultStr(Info2.FileName, Info1.FileName));
+}
+//---------------------------------------------------------------------------
 UnicodeString TSynchronizeChecklist::TItem::GetRemotePath() const
 {
   return UnixCombinePaths(Info2.Directory, Info2.FileName);
@@ -2959,6 +2970,11 @@ UnicodeString TSynchronizeChecklist::TItem::GetLocalTarget() const
   return IncludeTrailingBackslash(Info1.Directory);
 }
 //---------------------------------------------------------------------------
+UnicodeString TSynchronizeChecklist::TItem::GetLocalTarget2() const
+{
+  return IncludeTrailingBackslash(Info2.Directory);
+}
+//---------------------------------------------------------------------------
 UnicodeString TSynchronizeChecklist::TItem::GetRemoteTarget() const
 {
   return UnixIncludeTrailingBackslash(Info2.Directory);
@@ -3166,6 +3182,167 @@ bool __fastcall TSynchronizeChecklist::IsItemSizeIrrelevant(TAction Action)
 }
 //---------------------------------------------------------------------------
 //---------------------------------------------------------------------------
+class TFileOperationProgressSink : public TCppInterfacedObject<IFileOperationProgressSink>
+{
+public:
+  TFileOperationProgressSink(TSynchronizeChecklistFileOperation * FileOperation) : FFileOperation(FileOperation)
+  {
+  }
+
+  virtual HRESULT STDMETHODCALLTYPE StartOperations() { return S_OK; }
+
+  virtual HRESULT STDMETHODCALLTYPE FinishOperations(HRESULT) { return S_OK; }
+
+  virtual HRESULT STDMETHODCALLTYPE PreRenameItem(DWORD, IShellItem *, LPCWSTR) { return S_OK; }
+
+  virtual HRESULT STDMETHODCALLTYPE PostRenameItem(DWORD, IShellItem * Item, LPCWSTR, HRESULT, IShellItem *)
+  {
+    DebugFail();
+    FFileOperation->ProcessedItem(Item);
+    return S_OK;
+  }
+
+  virtual HRESULT STDMETHODCALLTYPE PreMoveItem(DWORD, IShellItem *, IShellItem *, LPCWSTR) { return S_OK; }
+
+  virtual HRESULT STDMETHODCALLTYPE PostMoveItem(DWORD, IShellItem * Item, IShellItem *, LPCWSTR, HRESULT, IShellItem *)
+  {
+    DebugFail();
+    FFileOperation->ProcessedItem(Item);
+    return S_OK;
+  }
+
+  virtual HRESULT STDMETHODCALLTYPE PreCopyItem(DWORD, IShellItem *, IShellItem *, LPCWSTR) { return S_OK; }
+
+  virtual HRESULT STDMETHODCALLTYPE PostCopyItem(DWORD, IShellItem * Item, IShellItem *, LPCWSTR, HRESULT, IShellItem *)
+  {
+    FFileOperation->ProcessedItem(Item);
+    return S_OK;
+  }
+
+  virtual HRESULT STDMETHODCALLTYPE PreDeleteItem(DWORD, IShellItem *) { return S_OK; }
+
+  virtual HRESULT STDMETHODCALLTYPE PostDeleteItem(DWORD, IShellItem * Item, HRESULT, IShellItem *)
+  {
+    FFileOperation->ProcessedItem(Item);
+    return S_OK;
+  }
+
+  virtual HRESULT STDMETHODCALLTYPE PreNewItem(DWORD, IShellItem *, LPCWSTR) { return S_OK; }
+
+  virtual HRESULT STDMETHODCALLTYPE PostNewItem(DWORD, IShellItem *, LPCWSTR, LPCWSTR, DWORD, HRESULT, IShellItem *)
+  {
+    DebugFail();
+    return S_OK;
+  }
+
+  virtual HRESULT STDMETHODCALLTYPE UpdateProgress(UINT, UINT) { return S_OK; }
+
+  virtual HRESULT STDMETHODCALLTYPE ResetTimer() { return S_OK; }
+
+  virtual HRESULT STDMETHODCALLTYPE PauseTimer() { return S_OK; }
+
+  virtual HRESULT STDMETHODCALLTYPE ResumeTimer() { return S_OK; }
+
+private:
+  TSynchronizeChecklistFileOperation * FFileOperation;
+};
+//---------------------------------------------------------------------------
+IShellItem * CreateShellItemFromParsingName(const UnicodeString & Path)
+{
+  IShellItem * Result;
+  OleCheck(SHCreateItemFromParsingName(Path.c_str(), nullptr, IID_PPV_ARGS(&Result)));
+  return Result;
+}
+//---------------------------------------------------------------------------
+TSynchronizeChecklistFileOperation::TSynchronizeChecklistFileOperation(
+    const TSynchronizeChecklist * Checklist, TProcessedSynchronizationChecklistItem OnProcessedItem, void * Token) :
+  FOnProcessedItem(OnProcessedItem),
+  FToken(Token)
+{
+  OleCheck(CoCreateInstance(CLSID_FileOperation, NULL, CLSCTX_ALL, IID_PPV_ARGS(&FFileOperation)));
+
+  FProgressSink = new TFileOperationProgressSink(this);
+  DWORD UnusedCookie;
+  OleCheck(FFileOperation->Advise(FProgressSink, &UnusedCookie));
+
+  int Index = 0;
+  const TSynchronizeChecklist::TItem * ChecklistItem;
+  while (Checklist->GetNextChecked(Index, ChecklistItem))
+  {
+    UnicodeString ItemPath;
+    switch (ChecklistItem->Action)
+    {
+      case TSynchronizeChecklist::saDownloadNew:
+      case TSynchronizeChecklist::saDownloadUpdate:
+      case TSynchronizeChecklist::saUploadNew:
+      case TSynchronizeChecklist::saUploadUpdate:
+        {
+          UnicodeString DestinationFolderPath;
+          if ((ChecklistItem->Action == TSynchronizeChecklist::saDownloadNew) ||
+              (ChecklistItem->Action == TSynchronizeChecklist::saDownloadUpdate))
+          {
+            ItemPath = ChecklistItem->GetLocalPath2();
+            DestinationFolderPath = ChecklistItem->GetLocalTarget();
+          }
+          else
+          {
+            ItemPath = ChecklistItem->GetLocalPath();
+            DestinationFolderPath = ChecklistItem->GetLocalTarget2();
+          }
+          TComPtr<IShellItem> Item = CreateShellItemFromParsingName(ItemPath);
+          TComPtr<IShellItem> DestinationFolder = CreateShellItemFromParsingName(DestinationFolderPath);
+          OleCheck(FFileOperation->CopyItem(Item.Get(), DestinationFolder.Get(), nullptr, nullptr));
+        }
+        break;
+
+      case TSynchronizeChecklist::saDeleteRemote:
+      case TSynchronizeChecklist::saDeleteLocal:
+        {
+          bool Left = (ChecklistItem->Action == TSynchronizeChecklist::saDeleteLocal);
+          ItemPath = Left ? ChecklistItem->GetLocalPath() : ChecklistItem->GetLocalPath2();
+          TComPtr<IShellItem> Item = CreateShellItemFromParsingName(ItemPath);
+          OleCheck(FFileOperation->DeleteItem(Item.Get(), nullptr));
+        }
+        break;
+
+      default:
+        DebugFail();
+        break;
+    }
+
+    if (!ItemPath.IsEmpty())
+    {
+      FShellItems.insert(std::make_pair(ItemPath.LowerCase(), ChecklistItem));
+    }
+  }
+}
+//---------------------------------------------------------------------------
+TSynchronizeChecklistFileOperation::~TSynchronizeChecklistFileOperation()
+{
+  FProgressSink->Release();
+}
+//---------------------------------------------------------------------------
+void TSynchronizeChecklistFileOperation::ProcessedItem(IShellItem * ShellItem)
+{
+  wchar_t * NamePtr;
+  // It's different IShellItem than what we pass to IFileOperation above, so we have to use underlying path for lookup
+  if (DebugAlwaysTrue(SUCCEEDED(ShellItem->GetDisplayName(SIGDN_FILESYSPATH, &NamePtr))))
+  {
+    UnicodeString Name = NamePtr;
+    CoTaskMemFree(NamePtr);
+    auto I = FShellItems.find(Name.LowerCase());
+    // The callback are triggered even for files in folders, so we do not get here only for top level checklist items.
+    // The folder is triggered before the files, so we unfortunatelly mark the folder as processed before its files are processed.
+    // Solution would be to wait until we get (Pre?) trigger for another checklist item (or until the end of whole operation).
+    if (I != FShellItems.end())
+    {
+      auto ChecklistItem = I->second;
+      FOnProcessedItem(FToken, ChecklistItem);
+    }
+  }
+}
+//---------------------------------------------------------------------------
+//---------------------------------------------------------------------------
 TSynchronizeProgress::TSynchronizeProgress(const TSynchronizeChecklist * Checklist)
 {
   FTotalSize = -1;

+ 32 - 0
source/core/RemoteFiles.h

@@ -497,11 +497,14 @@ public:
     __int64 __fastcall GetSize() const;
     __int64 __fastcall GetSize(TAction AAction) const;
     UnicodeString GetLocalPath() const;
+    UnicodeString GetLocalPath2() const;
     UnicodeString ForceGetLocalPath() const;
+    UnicodeString ForceGetLocalPath2() const;
     // Contrary to RemoteFile->FullFileName, this does not include trailing slash for directories
     UnicodeString GetRemotePath() const;
     UnicodeString ForceGetRemotePath() const;
     UnicodeString GetLocalTarget() const;
+    UnicodeString GetLocalTarget2() const;
     UnicodeString GetRemoteTarget() const;
     TStrings * GetFileList() const;
 
@@ -549,6 +552,35 @@ private:
   static int __fastcall Compare(void * Item1, void * Item2);
 };
 //---------------------------------------------------------------------------
+typedef void __fastcall (__closure *TProcessedSynchronizationChecklistItem)(
+  void * Token, const TSynchronizeChecklist::TItem * Item);
+struct IFileOperation;
+struct IShellItem;
+class TFileOperationProgressSink;
+//---------------------------------------------------------------------------
+class TSynchronizeChecklistFileOperation
+{
+friend class TSynchronizeChecklist;
+friend class TFileOperationProgressSink;
+public:
+  TSynchronizeChecklistFileOperation(
+    const TSynchronizeChecklist * Checklist, TProcessedSynchronizationChecklistItem OnProcessedItem, void * Token);
+  ~TSynchronizeChecklistFileOperation();
+
+  __property IFileOperation * FileOperation = { read = GetFileOperation };
+
+protected:
+  void ProcessedItem(IShellItem * ShellItem);
+  IFileOperation * GetFileOperation() { return FFileOperation.Get(); }
+
+private:
+  TComPtr<IFileOperation> FFileOperation;
+  TFileOperationProgressSink * FProgressSink;
+  std::map<UnicodeString, const TSynchronizeChecklist::TItem *> FShellItems;
+  TProcessedSynchronizationChecklistItem FOnProcessedItem;
+  void * FToken;
+};
+//---------------------------------------------------------------------------
 class TFileOperationProgressType;
 //---------------------------------------------------------------------------
 class TSynchronizeProgress

+ 123 - 27
source/core/Terminal.cpp

@@ -6141,6 +6141,7 @@ UnicodeString __fastcall TTerminal::SynchronizeParamsStr(int Params)
   AddFlagName(ParamsStr, Params, spCaseSensitive, L"CaseSensitive");
   AddFlagName(ParamsStr, Params, spSelectedOnly, L"*SelectedOnly"); // GUI only
   AddFlagName(ParamsStr, Params, spMirror, L"Mirror");
+  Params &= ~spLocalLocal; // internal
   if (Params > 0)
   {
     AddToList(ParamsStr, FORMAT(L"0x%x", (int(Params))), L", ");
@@ -6156,7 +6157,7 @@ bool __fastcall TTerminal::LocalFindFirstLoop(const UnicodeString & Path, TSearc
     const int FindAttrs = faReadOnly | faHidden | faSysFile | faDirectory | faArchive;
     Result = (FindFirstChecked(Path, FindAttrs, SearchRec) == 0);
   }
-  FILE_OPERATION_LOOP_END(FMTLOAD(LIST_DIR_ERROR, (Path)));
+  FILE_OPERATION_LOOP_END(MainInstructions(FMTLOAD(LIST_DIR_ERROR, (Path))));
   return Result;
 }
 //---------------------------------------------------------------------------
@@ -6167,7 +6168,7 @@ bool __fastcall TTerminal::LocalFindNextLoop(TSearchRecChecked & SearchRec)
   {
     Result = (FindNextChecked(SearchRec) == 0);
   }
-  FILE_OPERATION_LOOP_END(FMTLOAD(LIST_DIR_ERROR, (SearchRec.Path)));
+  FILE_OPERATION_LOOP_END(MainInstructions(FMTLOAD(LIST_DIR_ERROR, (SearchRec.Path))));
   return Result;
 }
 //---------------------------------------------------------------------------
@@ -6231,7 +6232,7 @@ void DestroyLocalFileList(TStringList * LocalFileList)
 void TTerminal::DoSynchronizeCollectDirectory(TSynchronizeData Data)
 {
   Data.Directory1 = IncludeTrailingBackslash(Data.Directory1);
-  Data.Directory2 = UnixIncludeTrailingBackslash(Data.Directory2);
+  Data.Directory2 = UniversalIncludeTrailingBackslash(FLAGCLEAR(Data.Params, spLocalLocal), Data.Directory2);
 
   LogEvent(FORMAT(
     L"Collecting synchronization list for '%s' and '%s', mode = %s, params = 0x%x (%s), file mask = '%s'",
@@ -6256,7 +6257,7 @@ void TTerminal::DoSynchronizeCollectDirectory(TSynchronizeData Data)
         UnicodeString FullLocalFileName = SearchRec.GetFilePath();
         UnicodeString RemoteFileName = ChangeFileName(Data.CopyParam, FileName, osLocal, false);
         if (SearchRec.IsRealFile() &&
-            DoAllowLocalFileTransfer(FullLocalFileName, SearchRec, Data.CopyParam, true) &&
+            DoAllowLocalFileTransfer(FullLocalFileName, SearchRec, Data.CopyParam, FLAGCLEAR(Data.Params, spLocalLocal)) &&
             (Data.MatchesFilter(FileName) || Data.MatchesFilter(RemoteFileName)))
         {
           TSynchronizeFileData * FileData = new TSynchronizeFileData;
@@ -6288,6 +6289,7 @@ void TTerminal::DoSynchronizeCollectDirectory(TSynchronizeData Data)
       // can we expect that ProcessDirectory would take so little time
       // that we can postpone showing progress window until anything actually happens?
       bool Directory2ShouldBeQuick =
+        FLAGSET(Data.Params, spLocalLocal) ||
         (FLAGSET(Data.Params, spUseCache) && SessionData->CacheDirectories && FDirectoryCache->HasFileList(Data.Directory2));
 
       if (!Directory2ShouldBeQuick && FLAGSET(Data.Params, spDelayProgress))
@@ -6295,7 +6297,26 @@ void TTerminal::DoSynchronizeCollectDirectory(TSynchronizeData Data)
         DoSynchronizeProgress(Data, true);
       }
 
-      ProcessDirectory(Data.Directory2, SynchronizeCollectFile, &Data, FLAGSET(Data.Params, spUseCache));
+      if (FLAGCLEAR(Data.Params, spLocalLocal))
+      {
+        ProcessDirectory(Data.Directory2, SynchronizeCollectFile, &Data, FLAGSET(Data.Params, spUseCache));
+      }
+      else
+      {
+        // Not using ProcessLocalDirectory for a better error handling
+        TSearchRecOwned SearchRec;
+        if (LocalFindFirstLoop(Data.Directory2 + L"*.*", SearchRec))
+        {
+          do
+          {
+            if (SearchRec.IsRealFile())
+            {
+              SynchronizeCollectLocalFile(SearchRec.GetFilePath(), SearchRec, &Data);
+            }
+          }
+          while (LocalFindNextLoop(SearchRec));
+        }
+      }
 
       TSynchronizeFileData * FileData;
       for (int Index = 0; Index < Data.LeftFileList->Count; Index++)
@@ -6437,7 +6458,8 @@ UnicodeString TTerminal::CalculateLocalFileChecksum(const UnicodeString & FileNa
   return Result;
 }
 //---------------------------------------------------------------------------
-bool TTerminal::SameFileChecksum(const UnicodeString & LeftFileName, const TRemoteFile * RightFile)
+bool TTerminal::SameFileChecksum(
+  const UnicodeString & LeftFileName, const UnicodeString & RightFileName, const TRemoteFile * RightFile)
 {
   UnicodeString DefaultAlg = Sha256ChecksumAlg;
   UnicodeString Algs =
@@ -6460,13 +6482,21 @@ bool TTerminal::SameFileChecksum(const UnicodeString & LeftFileName, const TRemo
     Alg = DefaultAlg;
   }
 
-  std::unique_ptr<TStrings> FileList(new TStringList());
-  FileList->AddObject(RightFile->FullFileName, const_cast<TRemoteFile *>(RightFile));
-  DebugAssert(FCollectedCalculatedChecksum.IsEmpty());
-  FCollectedCalculatedChecksum = EmptyStr;
-  CalculateFilesChecksum(Alg, FileList.get(), CollectCalculatedChecksum);
-  UnicodeString RightChecksum = FCollectedCalculatedChecksum;
-  FCollectedCalculatedChecksum = EmptyStr;
+  UnicodeString RightChecksum;
+  if (RightFile == NULL)
+  {
+    RightChecksum = CalculateLocalFileChecksum(RightFileName, Alg);
+  }
+  else
+  {
+    std::unique_ptr<TStrings> FileList(new TStringList());
+    FileList->AddObject(RightFile->FullFileName, const_cast<TRemoteFile *>(RightFile));
+    DebugAssert(FCollectedCalculatedChecksum.IsEmpty());
+    FCollectedCalculatedChecksum = EmptyStr;
+    CalculateFilesChecksum(Alg, FileList.get(), CollectCalculatedChecksum);
+    RightChecksum = FCollectedCalculatedChecksum;
+    FCollectedCalculatedChecksum = EmptyStr;
+  }
 
   UnicodeString LeftChecksum = CalculateLocalFileChecksum(LeftFileName, Alg);
 
@@ -6528,13 +6558,15 @@ void TTerminal::SynchronizedFileCheckModified(
   }
   else if (FLAGSET(Data->Params, spByChecksum) &&
            FLAGCLEAR(Data->Params, spTimestamp) &&
-           !SameFileChecksum(FullLeftFileName, RightFile) &&
+           !SameFileChecksum(FullLeftFileName, FullRightFileName, RightFile) &&
            FLAGCLEAR(Data->Params, spTimestamp))
   {
     Modified = true;
     LeftModified = true;
   }
 
+  const TRemoteFile * RightLinkedFile = (RightFile != NULL) ? RightFile->LinkedFile : NULL;
+
   if (LeftModified)
   {
     LocalData->Modified = true;
@@ -6542,16 +6574,16 @@ void TTerminal::SynchronizedFileCheckModified(
     LocalData->MatchingRemoteFile2ImageIndex = ChecklistItem->ImageIndex;
     // we need this for custom commands over checklist only,
     // not for sync itself
-    LocalData->MatchingRemoteFile2 = RightFile->Duplicate();
+    LocalData->MatchingRemoteFile2 = (RightFile != NULL) ? RightFile->Duplicate() : NULL;
     LogEvent(FORMAT(L"Left file %s is modified comparing to right file %s",
       (FormatFileDetailsForLog(FullLeftFileName, LocalData->Info.Modification, LocalData->Info.Size),
-       FormatFileDetailsForLog(FullRightFileName, RightFile->Modification, RightFile->Size, RightFile->LinkedFile))));
+       FormatFileDetailsForLog(FullRightFileName, ChecklistItem->Info2.Modification, ChecklistItem->Info2.Size, RightLinkedFile))));
   }
 
   if (Modified)
   {
     LogEvent(FORMAT(L"Right file %s is modified comparing to left file %s",
-      (FormatFileDetailsForLog(FullRightFileName, RightFile->Modification, RightFile->Size, RightFile->LinkedFile),
+      (FormatFileDetailsForLog(FullRightFileName, ChecklistItem->Info2.Modification, ChecklistItem->Info2.Size, RightLinkedFile),
        FormatFileDetailsForLog(FullLeftFileName, LocalData->Info.Modification, LocalData->Info.Size))));
     SynchronizedFileNewOrModified(Data, ChecklistItem, RightFile, false);
   }
@@ -6562,8 +6594,9 @@ void TTerminal::SynchronizedFileNew(
   const UnicodeString & FullRightFileName, const TRemoteFile * RightFile)
 {
   ChecklistItem->Info1.Directory = Data->Directory1;
+  const TRemoteFile * RightLinkedFile = (RightFile != NULL) ? RightFile->LinkedFile : NULL;
   LogEvent(FORMAT(L"Right file %s is new",
-    (FormatFileDetailsForLog(FullRightFileName, RightFile->Modification, RightFile->Size, RightFile->LinkedFile))));
+    (FormatFileDetailsForLog(FullRightFileName, ChecklistItem->Info2.Modification, ChecklistItem->Info2.Size, RightLinkedFile))));
   SynchronizedFileNewOrModified(Data, ChecklistItem, RightFile, true);
 }
 //---------------------------------------------------------------------------
@@ -6597,7 +6630,10 @@ void TTerminal::SynchronizedFileNewOrModified(
 
   if (ChecklistItem->Action != TSynchronizeChecklist::saNone)
   {
-    ChecklistItem->RemoteFile = File->Duplicate();
+    if (File != NULL)
+    {
+      ChecklistItem->RemoteFile = File->Duplicate();
+    }
     Data->Checklist->Add(ChecklistItem.release());
   }
 }
@@ -6653,7 +6689,7 @@ void __fastcall TTerminal::DoSynchronizeCollectFile(const UnicodeString FileName
       {
         UnicodeString FullLocalFileName = LocalData->Info.Directory + LocalData->Info.FileName;
 
-        if (!File->IsDirectory)
+        if (!ChecklistItem->IsDirectory)
         {
           ChecklistItem->Info1.Modification =
             ReduceDateTimePrecision(ChecklistItem->Info1.Modification, File->ModificationFmt);
@@ -6678,6 +6714,65 @@ void __fastcall TTerminal::DoSynchronizeCollectFile(const UnicodeString FileName
   }
 }
 //---------------------------------------------------------------------------
+void TTerminal::SynchronizeCollectLocalFile(
+  const UnicodeString & FileName, const TSearchRecSmart & Rec, TSynchronizeData * Data)
+{
+  try
+  {
+    DoSynchronizeCollectLocalFile(FileName, Rec, Data);
+  }
+  catch (ESkipFile & E)
+  {
+    if (!HandleException(&E))
+    {
+      throw;
+    }
+  }
+}
+//---------------------------------------------------------------------------
+void TTerminal::DoSynchronizeCollectLocalFile(
+  const UnicodeString & FileName, const TSearchRecSmart & SearchRec, TSynchronizeData * Data)
+{
+  DebugUsedParam(FileName);
+  Data->IncFiles();
+
+  if (DoAllowLocalFileTransfer(FileName, SearchRec, Data->CopyParam, false) &&
+      Data->MatchesFilter(SearchRec.Name))
+  {
+    std::unique_ptr<TSynchronizeChecklist::TItem> ChecklistItem(new TSynchronizeChecklist::TItem());
+    ChecklistItem->IsDirectory = SearchRec.IsDirectory();
+
+    ChecklistItem->Info2.FileName = SearchRec.Name;
+    ChecklistItem->Info2.Directory = Data->Directory2;
+    ChecklistItem->Info2.Modification = SearchRec.GetLastWriteTime();
+    ChecklistItem->Info2.Size = SearchRec.Size;
+
+    TSynchronizeFileData * LeftData = Data->MatchLeftFile(SearchRec.Name);
+    if ((LeftData != NULL) && DoFilesMatch(LeftData, ChecklistItem.get()))
+    {
+      UnicodeString FullLeftFileName = LeftData->Info.Directory + LeftData->Info.FileName;
+
+      if (!ChecklistItem->IsDirectory)
+      {
+        SynchronizedFileCheckModified(Data, ChecklistItem, FullLeftFileName, LeftData, FileName, NULL);
+      }
+      else if (FLAGCLEAR(Data->Params, spNoRecurse))
+      {
+        DoSynchronizeCollectDirectory(Data->CloneFor(FullLeftFileName, FileName));
+      }
+    }
+    else if (LeftData == NULL)
+    {
+      SynchronizedFileNew(Data, ChecklistItem, FileName, NULL);
+    }
+  }
+  else
+  {
+    LogEvent(0, FORMAT(L"Right file %s excluded from synchronization",
+      (FormatFileDetailsForLog(FileName, SearchRec.GetLastWriteTime(), SearchRec.Size, NULL))));
+  }
+}
+//---------------------------------------------------------------------------
 TCopyParamType TTerminal::GetSynchronizeCopyParam(const TCopyParamType * CopyParam, int Params)
 {
   TCopyParamType SyncCopyParam = *CopyParam;
@@ -6770,7 +6865,7 @@ void __fastcall TTerminal::SynchronizeApply(
       }
     }
 
-    SynchronizeChecklistCalculateSize(Checklist, Items, &SyncCopyParam);
+    SynchronizeChecklistCalculateSize(Checklist, Items, &SyncCopyParam, Params);
     if (OnUpdatedSynchronizationChecklistItems != NULL)
     {
       OnUpdatedSynchronizationChecklistItems(Items);
@@ -6889,7 +6984,7 @@ void __fastcall TTerminal::SynchronizeApply(
 //---------------------------------------------------------------------------
 void __fastcall TTerminal::SynchronizeChecklistCalculateSize(
   TSynchronizeChecklist * Checklist, const TSynchronizeChecklist::TItemList & Items,
-  const TCopyParamType * CopyParam)
+  const TCopyParamType * CopyParam, int Params)
 {
   std::unique_ptr<TStrings> RemoteFileList(new TStringList());
   std::unique_ptr<TStrings> LocalFileList(new TStringList());
@@ -6899,14 +6994,15 @@ void __fastcall TTerminal::SynchronizeChecklistCalculateSize(
     const TSynchronizeChecklist::TItem * ChecklistItem = Items[Index];
     if (ChecklistItem->IsDirectory)
     {
-      if (ChecklistItem->IsRemoteOnly())
+      if (ChecklistItem->IsRemoteOnly() && FLAGCLEAR(Params, spLocalLocal))
       {
         DebugAssert(UnixSamePath(ChecklistItem->RemoteFile->FullFileName, ChecklistItem->GetRemotePath()));
         RemoteFileList->AddObject(ChecklistItem->GetRemotePath(), ChecklistItem->RemoteFile);
       }
-      else if (ChecklistItem->IsLocalOnly())
+      else if (ChecklistItem->IsLocalOnly() || ChecklistItem->IsRemoteOnly())
       {
-        LocalFileList->Add(ChecklistItem->GetLocalPath());
+        UnicodeString Path = ChecklistItem->IsLocalOnly() ? ChecklistItem->GetLocalPath() : ChecklistItem->GetLocalPath2();
+        LocalFileList->Add(Path);
       }
       else
       {
@@ -6949,7 +7045,7 @@ void __fastcall TTerminal::SynchronizeChecklistCalculateSize(
       if (ChecklistItem->IsDirectory)
       {
         __int64 Size = -1;
-        if (ChecklistItem->IsRemoteOnly())
+        if (ChecklistItem->IsRemoteOnly() && FLAGCLEAR(Params, spLocalLocal))
         {
           if (RemoteIndex < RemoteCalculatedSizes.size())
           {
@@ -6957,7 +7053,7 @@ void __fastcall TTerminal::SynchronizeChecklistCalculateSize(
           }
           RemoteIndex++;
         }
-        else if (ChecklistItem->IsLocalOnly())
+        else if (ChecklistItem->IsLocalOnly() || ChecklistItem->IsRemoteOnly())
         {
           if (LocalIndex < LocalCalculatedSizes.size())
           {

+ 6 - 4
source/core/Terminal.h

@@ -61,8 +61,6 @@ typedef void __fastcall (__closure *TSynchronizeDirectory)
    bool & Continue, bool Collect, const TSynchronizeOptions * Options);
 typedef void __fastcall (__closure *TUpdatedSynchronizationChecklistItems)(
   const TSynchronizeChecklist::TItemList & Items);
-typedef void __fastcall (__closure *TProcessedSynchronizationChecklistItem)(
-  void * Token, const TSynchronizeChecklist::TItem * Item);
 typedef void __fastcall (__closure *TDeleteLocalFileEvent)(
   const UnicodeString FileName, bool Alternative, int & Deleted);
 typedef int __fastcall (__closure *TDirectoryModifiedEvent)
@@ -148,6 +146,7 @@ public:
   static const int spMirror = 0x1000;
   static const int spCaseSensitive = 0x2000;
   static const int spByChecksum = 0x4000; // cannot be combined with spTimestamp and smBoth
+  static const int spLocalLocal = 0x8000; // internal only
   static const int spDefault = TTerminal::spNoConfirmation | TTerminal::spPreviewChanges;
 
 // for ReactOnCommand()
@@ -366,7 +365,10 @@ protected:
     const TRemoteFile * File, /*TSynchronizeData*/ void * Param);
   void __fastcall SynchronizeCollectFile(const UnicodeString FileName,
     const TRemoteFile * File, /*TSynchronizeData*/ void * Param);
-  bool SameFileChecksum(const UnicodeString & LeftFileName, const TRemoteFile * RightFile);
+  void SynchronizeCollectLocalFile(
+    const UnicodeString & FileName, const TSearchRecSmart & Rec, TSynchronizeData * Data);
+  void DoSynchronizeCollectLocalFile(const UnicodeString & FileName, const TSearchRecSmart & SearchRec, TSynchronizeData * Data);
+  bool SameFileChecksum(const UnicodeString & LeftFileName, const UnicodeString & RightFileName, const TRemoteFile * RightFile);
   UnicodeString CalculateLocalFileChecksum(const UnicodeString & FileName, const UnicodeString & Alg);
   void __fastcall CollectCalculatedChecksum(
     const UnicodeString & FileName, const UnicodeString & Alg, const UnicodeString & Hash);
@@ -632,7 +634,7 @@ public:
     const TSynchronizeChecklist::TItem * ChecklistItem, const TCopyParamType * CopyParam, int Params, bool Parallel);
   void __fastcall SynchronizeChecklistCalculateSize(
     TSynchronizeChecklist * Checklist, const TSynchronizeChecklist::TItemList & Items,
-    const TCopyParamType * CopyParam);
+    const TCopyParamType * CopyParam, int Params);
   static TCopyParamType GetSynchronizeCopyParam(const TCopyParamType * CopyParam, int Params);
   static int GetSynchronizeCopyParams(int Params);
   void __fastcall FilesFind(UnicodeString Directory, const TFileMasks & FileMask,

+ 127 - 36
source/forms/CustomScpExplorer.cpp

@@ -6163,7 +6163,8 @@ void __fastcall TCustomScpExplorerForm::Synchronize(const UnicodeString LocalDir
   bool AnyOperation = false;
   TDateTime StartTime = Now();
   TSynchronizeChecklist * AChecklist = NULL;
-  TObjectReleaser<TSynchronizeProgressForm> SynchronizeProgressFormReleaser(FSynchronizeProgressForm, new TSynchronizeProgressForm(Application, true, -1));
+  TObjectReleaser<TSynchronizeProgressForm> SynchronizeProgressFormReleaser(
+    FSynchronizeProgressForm, new TSynchronizeProgressForm(Application, true, -1, false));
   try
   {
     if (FLAGCLEAR(Params, TTerminal::spDelayProgress))
@@ -6236,7 +6237,6 @@ void __fastcall TCustomScpExplorerForm::SynchronizeSessionLog(const UnicodeStrin
 void __fastcall TCustomScpExplorerForm::GetSynchronizeOptions(
   int Params, TSynchronizeOptions & Options)
 {
-  DebugAssert(!IsLocalBrowserMode());
   if (FLAGSET(Params, TTerminal::spSelectedOnly) && SynchronizeAllowSelectedOnly())
   {
     Options.Filter = new TStringList();
@@ -6305,6 +6305,60 @@ struct TSynchronizeParams
   TProcessedSynchronizationChecklistItem OnProcessedItem;
 };
 //---------------------------------------------------------------------------
+void TCustomScpExplorerForm::SynchronizeApplyLocal(TSynchronizeChecklist * Checklist, TSynchronizeParams & Params)
+{
+  DebugAssert(FLAGCLEAR(Params.Params, TTerminal::spTimestamp));
+
+  std::unique_ptr<TSynchronizeChecklistFileOperation> ChecklistFileOperation(
+    new TSynchronizeChecklistFileOperation(Checklist, SynchronizeProcessedItem, &Params));
+
+  unsigned int OperationFlags =
+    FOF_ALLOWUNDO |
+    FOF_NOCONFIRMMKDIR |
+    FOFX_RECYCLEONDELETE | // same effect as FOF_ALLOWUNDO?
+    FLAGMASK(FLAGSET(Params.Params, TTerminal::spNoConfirmation), FOF_NOCONFIRMATION);
+  IFileOperation * FileOperation = ChecklistFileOperation->FileOperation;
+  OleCheck(FileOperation->SetOperationFlags(OperationFlags));
+
+  TForm * ActiveForm = Screen->ActiveForm;
+  if (ActiveForm == this)
+  {
+    LockWindow();
+  }
+  else
+  {
+    ActiveForm->Enabled = false;
+  }
+
+  FileOperation->SetOwnerWindow(ActiveForm->Handle);
+
+  try
+  {
+    HRESULT Result = FileOperation->PerformOperations();
+    AppLogFmt(L"Performed %d", (int(Result)));
+    BOOL Aborted;
+    OleCheck(FileOperation->GetAnyOperationsAborted(&Aborted));
+    if ((Result == HRESULT_FROM_WIN32(ERROR_CANCELLED)) || Aborted)
+    {
+      AppLog(L"Aborted");
+      Abort();
+    }
+    AppLog(L"Checking");
+    OleCheck(Result);
+  }
+  __finally
+  {
+    if (ActiveForm == this)
+    {
+      UnlockWindow();
+    }
+    else
+    {
+      ActiveForm->Enabled = true;
+    }
+  }
+}
+//---------------------------------------------------------------------------
 void __fastcall TCustomScpExplorerForm::FullSynchronize(
   TSynchronizeParams & Params, TProcessedSynchronizationChecklistItem OnProcessedItem,
   TUpdatedSynchronizationChecklistItems OnUpdatedSynchronizationChecklistItems)
@@ -6317,23 +6371,38 @@ void __fastcall TCustomScpExplorerForm::FullSynchronize(
   TFileOperationStatistics Statistics;
   TDateTime Start = Now();
 
+  bool LocalLocal = FLAGSET(Params.Params, TTerminal::spLocalLocal);
+
   try
   {
     Params.OnProcessedItem = OnProcessedItem;
 
-    TSynchronizeProgress SynchronizeProgress(Params.Checklist);
-    CreateProgressForm(&SynchronizeProgress);
+    if (!LocalLocal)
+    {
+      TSynchronizeProgress SynchronizeProgress(Params.Checklist);
+      CreateProgressForm(&SynchronizeProgress);
 
-    Terminal->SynchronizeApply(
-      Params.Checklist, Params.CopyParam, Params.Params | TTerminal::spNoConfirmation,
-      TerminalSynchronizeDirectory, SynchronizeProcessedItem, OnUpdatedSynchronizationChecklistItems, &Params,
-      &Statistics);
+      try
+      {
+        Terminal->SynchronizeApply(
+          Params.Checklist, Params.CopyParam, Params.Params | TTerminal::spNoConfirmation,
+          TerminalSynchronizeDirectory, SynchronizeProcessedItem, OnUpdatedSynchronizationChecklistItems, &Params,
+          &Statistics);
+      }
+      __finally
+      {
+        DestroyProgressForm();
+      }
+    }
+    else
+    {
+      SynchronizeApplyLocal(Params.Checklist, Params);
+    }
   }
   __finally
   {
     Params.OnProcessedItem = NULL;
     FAutoOperation = false;
-    DestroyProgressForm();
     BatchEnd(BatchStorage);
     ReloadLocalDirectory();
   }
@@ -6343,7 +6412,8 @@ void __fastcall TCustomScpExplorerForm::FullSynchronize(
     UnicodeString Message = MainInstructions(LoadStr(SYNCHRONIZE_COMPLETE)) + L"\n";
 
     // The statistics should be 0 anyway in this case
-    if (FLAGCLEAR(Params.Params, TTerminal::spTimestamp))
+    if (FLAGCLEAR(Params.Params, TTerminal::spTimestamp) &&
+        !LocalLocal)
     {
       Message += L"\n";
       if (Statistics.FilesUploaded > 0)
@@ -6380,7 +6450,8 @@ void __fastcall TCustomScpExplorerForm::FullSynchronize(
 //---------------------------------------------------------------------------
 void __fastcall TCustomScpExplorerForm::SynchronizeProcessedItem(void * Token, const TSynchronizeChecklist::TItem * ChecklistItem)
 {
-  if (DebugAlwaysTrue(FProgressForm != NULL) && DebugAlwaysTrue(FProgressForm->SynchronizeProgress != NULL))
+  // There only the external shell's progress form for local-local
+  if ((FProgressForm != NULL) && DebugAlwaysTrue(FProgressForm->SynchronizeProgress != NULL))
   {
     FProgressForm->SynchronizeProgress->ItemProcessed(ChecklistItem);
   }
@@ -6392,6 +6463,11 @@ void __fastcall TCustomScpExplorerForm::SynchronizeProcessedItem(void * Token, c
     if (Params.OnProcessedItem != NULL)
     {
       Params.OnProcessedItem(NULL, ChecklistItem);
+      // in local-local, nothing pumps the message queue, so we have to do it here
+      if (FLAGSET(Params.Params, TTerminal::spLocalLocal))
+      {
+        Application->ProcessMessages();
+      }
     }
   }
 }
@@ -6427,23 +6503,22 @@ void __fastcall TCustomScpExplorerForm::DoSynchronizeChecklistCalculateSize(
   TSynchronizeChecklist * Checklist, const TSynchronizeChecklist::TItemList & Items, void * Token)
 {
   // terminal can be already closed (e.g. dropped connection)
-  if (Terminal != NULL)
+  if ((ManagedSession != NULL) && (IsLocalBrowserMode() || ManagedSession->Active))
   {
     TSynchronizeParams & Params = *static_cast<TSynchronizeParams *>(Token);
-    Terminal->SynchronizeChecklistCalculateSize(Checklist, Items, Params.CopyParam);
+    ManagedSession->SynchronizeChecklistCalculateSize(Checklist, Items, Params.CopyParam, Params.Params);
   }
 }
 //---------------------------------------------------------------------------
 void __fastcall TCustomScpExplorerForm::DoSynchronizeMove(
   TOperationSide Side, TStrings * FileList, const UnicodeString & NewFileName, bool TargetIsDirectory, void * Token)
 {
-  DebugAssert(!IsLocalBrowserMode());
   TAutoBatch AutoBatch(this);
   TAutoFlag AutoOperationFlag(FAutoOperation);
 
   TSynchronizeParams & Params = *static_cast<TSynchronizeParams *>(Token);
 
-  if (Side == osRemote)
+  if (!IsSideLocalBrowser(Side))
   {
     UnicodeString Target;
     UnicodeString FileMask;
@@ -6471,7 +6546,7 @@ void __fastcall TCustomScpExplorerForm::DoSynchronizeMove(
     }
     RemoteDirView->RestoreSelection();
   }
-  else if (DebugAlwaysTrue(Side == osLocal))
+  else
   {
     std::unique_ptr<TStrings> Directories(CreateSortedStringList());
     if (TargetIsDirectory)
@@ -6497,6 +6572,12 @@ void __fastcall TCustomScpExplorerForm::DoSynchronizeMove(
       {
         NewFilePath = TPath::Combine(NewFilePath, ExtractFileName(FileName));
       }
+      UnicodeString LogMsg = FORMAT(L"Moving file \"%s\" to \"%s\".", (FileName, NewFilePath));
+      if (Terminal != NULL)
+      {
+        Terminal->LogEvent(LogMsg);
+      }
+      AppLog(LogMsg);
       if (!MoveFile(ApiPath(FileName).c_str(), ApiPath(NewFilePath).c_str()))
       {
         throw EOSExtException(FMTLOAD(RENAME_FILE_ERROR, (FileName, NewFilePath)));
@@ -6513,15 +6594,18 @@ void __fastcall TCustomScpExplorerForm::DoSynchronizeMove(
 void __fastcall TCustomScpExplorerForm::DoSynchronizeExplore(TOperationSide Side, TSynchronizeChecklist::TAction Action, const TSynchronizeChecklist::TItem * Item)
 {
   UnicodeString Path1 = ExcludeTrailingBackslash(Item->Info1.Directory);
-  if (Side == osLocal)
+  if ((Side == osLocal) || IsLocalBrowserMode())
   {
-    if (Action == TSynchronizeChecklist::saDownloadNew)
+    if (((Side == osLocal) && (Action == TSynchronizeChecklist::saDownloadNew)) ||
+        ((Side == osRemote) && (Action == TSynchronizeChecklist::saUploadNew)))
     {
-      OpenFolderInExplorer(Path1);
+      UnicodeString Directory = (Side == osLocal) ? Path1 : ExcludeTrailingBackslash(Item->Info2.Directory);
+      OpenFolderInExplorer(Directory);
     }
     else
     {
-      OpenFileInExplorer(Item->ForceGetLocalPath());
+      UnicodeString Path = (Side == osLocal) ? Item->ForceGetLocalPath() : Item->ForceGetLocalPath2();
+      OpenFileInExplorer(Path);
     }
   }
   else if (DebugAlwaysTrue(Side == osRemote))
@@ -6562,12 +6646,16 @@ int __fastcall TCustomScpExplorerForm::DoFullSynchronizeDirectories(
   int Result;
 
   bool SaveSettings = false;
+  bool LocalLocal = IsLocalBrowserMode();
   int Options =
-    FLAGMASK(!Terminal->IsCapable[fcTimestampChanging], fsoDisableTimestamp) |
-    FLAGMASK(!CanCalculateChecksum(), fsoDisableByChecksum) |
-    FLAGMASK(SynchronizeAllowSelectedOnly(), fsoAllowSelectedOnly);
-  TCopyParamType CopyParam = GUIConfiguration->CurrentCopyParam;
-  TUsableCopyParamAttrs CopyParamAttrs = Terminal->UsableCopyParamAttrs(0);
+    FLAGMASK(LocalLocal || !Terminal->IsCapable[fcTimestampChanging], fsoDisableTimestamp) |
+    FLAGMASK(!LocalLocal && !CanCalculateChecksum(), fsoDisableByChecksum) |
+    FLAGMASK(SynchronizeAllowSelectedOnly(), fsoAllowSelectedOnly) |
+    FLAGMASK(LocalLocal, fsoLocalLocal);
+  // For local-local the file mask would still be relevant, and while it's somehat supported by SynchronizeCollect,
+  // it is not supported for (externally implemented) recursive transfers when applying folder differences.
+  TCopyParamType CopyParam = LocalLocal ? TCopyParamType() : GUIConfiguration->CurrentCopyParam;
+  TUsableCopyParamAttrs CopyParamAttrs = LocalLocal ? TUsableCopyParamAttrs() : Terminal->UsableCopyParamAttrs(0);
   bool Continue =
     ((UseDefaults == 0) ||
      DoFullSynchronizeDialog(
@@ -6577,8 +6665,11 @@ int __fastcall TCustomScpExplorerForm::DoFullSynchronizeDirectories(
   if (Continue)
   {
     Configuration->Usage->Inc(L"Synchronizations");
-    CopyParam.IncludeFileMask.SetRoots(Directory1, Directory2);
-    UpdateCopyParamCounters(CopyParam);
+    if (!LocalLocal)
+    {
+      CopyParam.IncludeFileMask.SetRoots(Directory1, Directory2);
+      UpdateCopyParamCounters(CopyParam);
+    }
 
     TSynchronizeOptions SynchronizeOptions;
     GetSynchronizeOptions(Params, SynchronizeOptions);
@@ -6592,6 +6683,8 @@ int __fastcall TCustomScpExplorerForm::DoFullSynchronizeDirectories(
       SaveMode = false;
     }
 
+    Params |= FLAGMASK(LocalLocal, TTerminal::spLocalLocal);
+
     TDateTime StartTime = Now();
 
     TSynchronizeChecklist * Checklist = NULL;
@@ -6602,7 +6695,7 @@ int __fastcall TCustomScpExplorerForm::DoFullSynchronizeDirectories(
 
       try
       {
-        UnicodeString SessionKey = Terminal->SessionData->SessionKey;
+        UnicodeString SessionKey = LocalLocal ? EmptyStr : Terminal->SessionData->SessionKey;
         std::unique_ptr<TStrings> DataList(Configuration->LoadDirectoryStatisticsCache(SessionKey, Directory2, CopyParam));
 
         int Files = -1;
@@ -6615,7 +6708,7 @@ int __fastcall TCustomScpExplorerForm::DoFullSynchronizeDirectories(
           DataList->Add(UnicodeString());
         }
 
-        FSynchronizeProgressForm = new TSynchronizeProgressForm(Application, true, Files);
+        FSynchronizeProgressForm = new TSynchronizeProgressForm(Application, true, Files, LocalLocal);
         FSynchronizeProgressForm->Start();
 
         Checklist =
@@ -6625,7 +6718,7 @@ int __fastcall TCustomScpExplorerForm::DoFullSynchronizeDirectories(
             &CopyParam, Params | TTerminal::spNoConfirmation, TerminalSynchronizeDirectory,
             &SynchronizeOptions);
 
-        if (Terminal->SessionData->CacheDirectories)
+        if (LocalLocal || Terminal->SessionData->CacheDirectories)
         {
           DataList->Strings[0] = IntToStr(SynchronizeOptions.Files);
           Configuration->SaveDirectoryStatisticsCache(SessionKey, Directory2, CopyParam, DataList.get());
@@ -6651,14 +6744,12 @@ int __fastcall TCustomScpExplorerForm::DoFullSynchronizeDirectories(
       {
         if (FLAGSET(Params, TTerminal::spPreviewChanges))
         {
-          TQueueSynchronizeEvent OnQueueSynchronize = NULL;
-          if (Visible && FLAGCLEAR(Params, TTerminal::spTimestamp))
-          {
-            OnQueueSynchronize = DoQueueSynchronize;
-          }
+          bool CanQueue = Visible && FLAGCLEAR(Params, TTerminal::spTimestamp) && !LocalLocal;
+          TQueueSynchronizeEvent OnQueueSynchronize = CanQueue ? DoQueueSynchronize : TQueueSynchronizeEvent();
+          TCustomCommandMenuEvent OnCustomCommandMenu = !LocalLocal ? CustomCommandMenu : TCustomCommandMenuEvent();
 
           if (!DoSynchronizeChecklistDialog(
-                Checklist, Mode, Params, Directory1, Directory2, CustomCommandMenu, DoFullSynchronize,
+                Checklist, Mode, Params, Directory1, Directory2, OnCustomCommandMenu, DoFullSynchronize,
                 OnQueueSynchronize, DoSynchronizeChecklistCalculateSize, DoSynchronizeMove, DoSynchronizeExplore,
                 &SynchronizeParams))
           {

+ 1 - 0
source/forms/CustomScpExplorer.h

@@ -720,6 +720,7 @@ protected:
     TSynchronizeParams & Params, TProcessedSynchronizationChecklistItem OnProcessedItem,
     TUpdatedSynchronizationChecklistItems OnUpdatedSynchronizationChecklistItems);
   void __fastcall SynchronizeProcessedItem(void * Token, const TSynchronizeChecklist::TItem * ChecklistItem);
+  void SynchronizeApplyLocal(TSynchronizeChecklist * Checklist, TSynchronizeParams & Params);
   void __fastcall CreateOpenDirMenuList(TTBCustomItem * Menu, TOperationSide Side, TBookmarkList * BookmarkList);
   void __fastcall CreateOpenDirMenu(TTBCustomItem * Menu, TOperationSide Side);
   bool __fastcall TryOpenDirectory(TOperationSide Side, const UnicodeString & Path);

+ 56 - 16
source/forms/FullSynchronize.cpp

@@ -25,7 +25,10 @@ bool DoFullSynchronizeDialog(
     Dialog->Params = Params;
     Dialog->Directory1 = Directory1;
     Dialog->Directory2 = Directory2;
-    Dialog->CopyParams = *CopyParams;
+    if (CopyParams != NULL)
+    {
+      Dialog->CopyParams = *CopyParams;
+    }
     Dialog->SaveSettings = SaveSettings;
     Dialog->SaveMode = SaveMode;
     if (AutoSubmit > 0)
@@ -39,7 +42,10 @@ bool DoFullSynchronizeDialog(
       Params = Dialog->Params;
       Directory1 = Dialog->Directory1;
       Directory2 = Dialog->Directory2;
-      *CopyParams = Dialog->CopyParams;
+      if (CopyParams != NULL)
+      {
+        *CopyParams = Dialog->CopyParams;
+      }
       SaveSettings = Dialog->SaveSettings;
       SaveMode = Dialog->SaveMode;
     }
@@ -70,24 +76,45 @@ __fastcall TFullSynchronizeDialog::~TFullSynchronizeDialog()
   delete FPresetsMenu;
 }
 //---------------------------------------------------------------------------
+bool TFullSynchronizeDialog::CanSynchronizeTimestamps()
+{
+  return FLAGCLEAR(FOptions, fsoDisableTimestamp) && FLAGCLEAR(FOptions, fsoLocalLocal);
+}
+//---------------------------------------------------------------------------
 void __fastcall TFullSynchronizeDialog::Init(
   int Options, const TUsableCopyParamAttrs & CopyParamAttrs, TFullSynchronizeInNewWindow OnFullSynchronizeInNewWindow)
 {
   FOptions = Options;
-  if (FLAGSET(Options, fsoDisableTimestamp) &&
-      SynchronizeTimestampsButton->Checked)
+  // somewhat redundant
+  if (!CanSynchronizeTimestamps() && SynchronizeTimestampsButton->Checked)
   {
     SynchronizeFilesButton->Checked = true;
   }
   FCopyParamAttrs = CopyParamAttrs;
   FOnFullSynchronizeInNewWindow = OnFullSynchronizeInNewWindow;
-  DebugAssert(FOnFullSynchronizeInNewWindow != NULL);
+  if (FLAGSET(FOptions, fsoLocalLocal))
+  {
+    LocalDirectoryLabel->Caption = LoadStr(SYNCHRONIZE_LEFT_DIR);
+    RemoteDirectoryLabel->Caption = LoadStr(SYNCHRONIZE_RIGHT_DIR);
+    RemoteDirectoryEdit->Width = LocalDirectoryEdit->Width;
+    SynchronizeRemoteButton->Caption = LoadStr(SYNCHRONIZE_RIGHT);
+    SynchronizeLocalButton->Caption = LoadStr(SYNCHRONIZE_LEFT);
+    CopyParamGroup->Visible = false;
+    ClientHeight -= OkButton->Top - CopyParamGroup->Top;
+    TransferSettingsButton->Visible = false;
+  }
+  else
+  {
+    DebugAssert(FOnFullSynchronizeInNewWindow != NULL);
+    OtherLocalDirectoryBrowseButton->Visible = false;
+  }
+
   UpdateControls();
 }
 //---------------------------------------------------------------------------
 void __fastcall TFullSynchronizeDialog::UpdateControls()
 {
-  EnableControl(SynchronizeTimestampsButton, FLAGCLEAR(FOptions, fsoDisableTimestamp));
+  EnableControl(SynchronizeTimestampsButton, CanSynchronizeTimestamps());
   if (SynchronizeTimestampsButton->Checked)
   {
     SynchronizeExistingOnlyCheck->Checked = true;
@@ -171,12 +198,17 @@ void __fastcall TFullSynchronizeDialog::ControlChange(TObject * /*Sender*/)
   UpdateControls();
 }
 //---------------------------------------------------------------------------
+UnicodeString TFullSynchronizeDialog::GetRightDirectoryHistory()
+{
+  return FLAGCLEAR(FOptions, fsoLocalLocal) ? L"RemoteDirectory" : L"LocalDirectory2";
+}
+//---------------------------------------------------------------------------
 bool __fastcall TFullSynchronizeDialog::Execute()
 {
   // at start assume that copy param is current preset
   FPreset = GUIConfiguration->CopyParamCurrent;
   LocalDirectoryEdit->Items = CustomWinConfiguration->History[L"LocalDirectory"];
-  RemoteDirectoryEdit->Items = CustomWinConfiguration->History[L"RemoteDirectory"];
+  RemoteDirectoryEdit->Items = CustomWinConfiguration->History[GetRightDirectoryHistory()];
   bool Result = (ShowModal() == DefaultResult(this));
   if (Result)
   {
@@ -190,7 +222,7 @@ void __fastcall TFullSynchronizeDialog::Submitted()
   LocalDirectoryEdit->SaveToHistory();
   CustomWinConfiguration->History[L"LocalDirectory"] = LocalDirectoryEdit->Items;
   RemoteDirectoryEdit->SaveToHistory();
-  CustomWinConfiguration->History[L"RemoteDirectory"] = RemoteDirectoryEdit->Items;
+  CustomWinConfiguration->History[GetRightDirectoryHistory()] = RemoteDirectoryEdit->Items;
 }
 //---------------------------------------------------------------------------
 void TFullSynchronizeDialog::SetDirectory2(const UnicodeString & value)
@@ -262,7 +294,7 @@ void __fastcall TFullSynchronizeDialog::SetParams(int value)
   SynchronizeExistingOnlyCheck->Checked = FLAGSET(value, TTerminal::spExistingOnly);
   SynchronizePreviewChangesCheck->Checked = FLAGSET(value, TTerminal::spPreviewChanges);
   SynchronizeSelectedOnlyCheck->Checked = FLAGSET(value, TTerminal::spSelectedOnly);
-  if (FLAGSET(value, TTerminal::spTimestamp) && FLAGCLEAR(FOptions, fsoDisableTimestamp))
+  if (FLAGSET(value, TTerminal::spTimestamp) && CanSynchronizeTimestamps())
   {
     SynchronizeTimestampsButton->Checked = true;
   }
@@ -288,8 +320,7 @@ int __fastcall TFullSynchronizeDialog::GetParams()
     FLAGMASK(SynchronizeExistingOnlyCheck->Checked, TTerminal::spExistingOnly) |
     FLAGMASK(SynchronizePreviewChangesCheck->Checked, TTerminal::spPreviewChanges) |
     FLAGMASK(SynchronizeSelectedOnlyCheck->Checked, TTerminal::spSelectedOnly) |
-    FLAGMASK(SynchronizeTimestampsButton->Checked && FLAGCLEAR(FOptions, fsoDisableTimestamp),
-      TTerminal::spTimestamp) |
+    FLAGMASK(SynchronizeTimestampsButton->Checked && CanSynchronizeTimestamps(), TTerminal::spTimestamp) |
     FLAGMASK(MirrorFilesButton->Checked, TTerminal::spMirror) |
     FLAGMASK(!SynchronizeByTimeCheck->Checked, TTerminal::spNotByTime) |
     FLAGMASK(SynchronizeBySizeCheck->Checked, TTerminal::spBySize) |
@@ -297,16 +328,25 @@ int __fastcall TFullSynchronizeDialog::GetParams()
     FLAGMASK(SynchronizeCaseSensitiveCheck->Checked, TTerminal::spCaseSensitive);
 }
 //---------------------------------------------------------------------------
-void __fastcall TFullSynchronizeDialog::LocalDirectoryBrowseButtonClick(
-      TObject * /*Sender*/)
+void TFullSynchronizeDialog::DoLocalDirectoryBrowseButtonClick(TComboBox * ComboBox)
 {
-  UnicodeString Directory = LocalDirectoryEdit->Text;
+  UnicodeString Directory = ComboBox->Text;
   if (SelectDirectory(Directory, LoadStr(SELECT_LOCAL_DIRECTORY)))
   {
-    LocalDirectoryEdit->Text = Directory;
+    ComboBox->Text = Directory;
   }
 }
 //---------------------------------------------------------------------------
+void __fastcall TFullSynchronizeDialog::LocalDirectoryBrowseButtonClick(TObject *)
+{
+  DoLocalDirectoryBrowseButtonClick(LocalDirectoryEdit);
+}
+//---------------------------------------------------------------------------
+void __fastcall TFullSynchronizeDialog::OtherLocalDirectoryBrowseButtonClick(TObject *)
+{
+  DoLocalDirectoryBrowseButtonClick(RemoteDirectoryEdit);
+}
+//---------------------------------------------------------------------------
 void __fastcall TFullSynchronizeDialog::SetSaveSettings(bool value)
 {
   SaveSettingsCheck->Checked = value;
@@ -430,7 +470,7 @@ void __fastcall TFullSynchronizeDialog::TransferSettingsButtonDropDownClick(TObj
 //---------------------------------------------------------------------------
 bool __fastcall TFullSynchronizeDialog::AllowStartInNewWindow()
 {
-  return !IsMainFormLike(this);
+  return !IsMainFormLike(this) && FLAGCLEAR(FOptions, fsoLocalLocal);
 }
 //---------------------------------------------------------------------------
 bool __fastcall TFullSynchronizeDialog::CanStartInNewWindow()

+ 10 - 0
source/forms/FullSynchronize.dfm

@@ -89,6 +89,16 @@ object FullSynchronizeDialog: TFullSynchronizeDialog
       TabOrder = 1
       OnClick = LocalDirectoryBrowseButtonClick
     end
+    object OtherLocalDirectoryBrowseButton: TButton
+      Left = 429
+      Top = 86
+      Width = 80
+      Height = 25
+      Anchors = [akTop, akRight]
+      Caption = 'Bro&wse...'
+      TabOrder = 3
+      OnClick = OtherLocalDirectoryBrowseButtonClick
+    end
   end
   object OkButton: TButton
     Left = 260

+ 5 - 0
source/forms/FullSynchronize.h

@@ -52,6 +52,7 @@ __published:
   TMenuItem *StartInNewWindowItem;
   TCheckBox *SynchronizeCaseSensitiveCheck;
   TCheckBox *SynchronizeByChecksumCheck;
+  TButton *OtherLocalDirectoryBrowseButton;
   void __fastcall ControlChange(TObject *Sender);
   void __fastcall LocalDirectoryBrowseButtonClick(TObject *Sender);
   void __fastcall TransferSettingsButtonClick(TObject *Sender);
@@ -66,6 +67,7 @@ __published:
   void __fastcall OkButtonDropDownClick(TObject *Sender);
   void __fastcall OkButtonClick(TObject *Sender);
   void __fastcall StartInNewWindowItemClick(TObject *Sender);
+  void __fastcall OtherLocalDirectoryBrowseButtonClick(TObject *Sender);
 
 private:
   int FParams;
@@ -97,6 +99,9 @@ private:
   bool __fastcall CanStartInNewWindow();
   void __fastcall Submitted();
   void __fastcall StartInNewWindow();
+  void DoLocalDirectoryBrowseButtonClick(TComboBox * ComboBox);
+  bool CanSynchronizeTimestamps();
+  UnicodeString GetRightDirectoryHistory();
 
 public:
   __fastcall TFullSynchronizeDialog(TComponent* Owner);

+ 1 - 1
source/forms/NonVisual.cpp

@@ -435,7 +435,7 @@ void __fastcall TNonVisualDataModule::ExplorerActionsUpdate(
   // COMMAND
   UPD(CompareDirectoriesAction2, HasManagedSession) // Or simply true, as the command is in Commander only and it always has a managed session
   UPD(SynchronizeAction, HasTerminal)
-  UPD(FullSynchronizeAction2, HasTerminal)
+  UPD(FullSynchronizeAction2, ScpExplorer->IsLocalBrowserMode() || HasTerminal)
   UPD(ConsoleAction, ScpExplorer->CanConsole())
   UPD(PuttyAction, HasTerminal && TTerminalManager::Instance()->CanOpenInPutty())
   UPD(SynchronizeBrowsingAction2, HasTerminal)

+ 0 - 1
source/forms/ScpCommander.cpp

@@ -1260,7 +1260,6 @@ void __fastcall TScpCommanderForm::SynchronizeDirectories()
 //---------------------------------------------------------------------------
 void __fastcall TScpCommanderForm::FullSynchronizeDirectories()
 {
-  DebugAssert(!IsLocalBrowserMode());
   UnicodeString Directory1 = LocalDirView->PathName;
   UnicodeString Directory2 = DirView(osOther)->PathName;
   bool SaveMode = !(GUIConfiguration->SynchronizeModeAuto < 0);

+ 50 - 17
source/forms/SynchronizeChecklist.cpp

@@ -42,7 +42,7 @@ __fastcall TSynchronizeChecklistDialog::TSynchronizeChecklistDialog(
   FMode = Mode;
   FParams = Params;
   FDirectory1 = ExcludeTrailingBackslash(Directory1);
-  FDirectory2 = UnixExcludeTrailingBackslash(Directory2);
+  FDirectory2 = UniversalExcludeTrailingBackslash(FLAGCLEAR(Params, TTerminal::spLocalLocal), Directory2);
   FOnCustomCommandMenu = OnCustomCommandMenu;
   FOnSynchronizeChecklistCalculateSize = OnSynchronizeChecklistCalculateSize;
   FOnSynchronizeMove = OnSynchronizeMove;
@@ -71,9 +71,6 @@ __fastcall TSynchronizeChecklistDialog::TSynchronizeChecklistDialog(
 
   UpdateImages();
 
-  CustomCommandsAction->Visible = (FOnCustomCommandMenu != NULL);
-  // button visibility cannot be bound to action visibility
-  CustomCommandsButton2->Visible = CustomCommandsAction->Visible;
   MenuButton(CustomCommandsButton2);
   MenuButton(ToolsMenuButton);
 
@@ -82,6 +79,12 @@ __fastcall TSynchronizeChecklistDialog::TSynchronizeChecklistDialog(
   {
     OkButton->Style = TCustomButton::bsSplitButton;
   }
+
+  if (FLAGSET(Params, TTerminal::spLocalLocal))
+  {
+    ListView2->Columns->Items[1]->Caption = LoadStr(SYNCHRONIZE_CHECKLIST_LEFT_DIR);
+    ListView2->Columns->Items[5]->Caption = LoadStr(SYNCHRONIZE_CHECKLIST_RIGHT_DIR);
+  }
 }
 //---------------------------------------------------------------------
 __fastcall TSynchronizeChecklistDialog::~TSynchronizeChecklistDialog()
@@ -175,7 +178,9 @@ struct TMoveActionData
     ItemsCount = 0;
   }
 
-  bool Collect(const TSynchronizeChecklist::TItem * ChecklistItem, TSynchronizeChecklist::TAction AAction, bool CollectFileList)
+  bool Collect(
+    const TSynchronizeChecklist::TItem * ChecklistItem, TSynchronizeChecklist::TAction AAction,
+    bool CollectFileList, bool LocalLocal)
   {
     bool Result = (ItemsCount == 0) || (Action == AAction);
     if (Result)
@@ -197,8 +202,15 @@ struct TMoveActionData
         TObject * Object = NULL;
         if (Action == TSynchronizeChecklist::saDeleteRemote)
         {
-          FileName = ChecklistItem->GetRemotePath();
-          Object = ChecklistItem->RemoteFile;
+          if (!LocalLocal)
+          {
+            FileName = ChecklistItem->GetRemotePath();
+            Object = ChecklistItem->RemoteFile;
+          }
+          else
+          {
+            FileName = ChecklistItem->GetLocalPath2();
+          }
         }
         else if (Action == TSynchronizeChecklist::saDeleteLocal)
         {
@@ -238,16 +250,17 @@ struct TMoveData
   TMoveActionData SecondAction;
   bool ThreeActions;
 
-  TMoveData(bool CollectFileList)
+  TMoveData(bool CollectFileList, bool LocalLocal)
   {
     ThreeActions = false;
     FCollectFileList = CollectFileList;
+    FLocalLocal = LocalLocal;
   }
 
   void Collect(const TSynchronizeChecklist::TItem * ChecklistItem, TSynchronizeChecklist::TAction Action)
   {
-    if (!FirstAction.Collect(ChecklistItem, Action, FCollectFileList) &&
-        !SecondAction.Collect(ChecklistItem, Action, FCollectFileList))
+    if (!FirstAction.Collect(ChecklistItem, Action, FCollectFileList, FLocalLocal) &&
+        !SecondAction.Collect(ChecklistItem, Action, FCollectFileList, FLocalLocal))
     {
       ThreeActions = true;
     }
@@ -255,6 +268,7 @@ struct TMoveData
 
 private:
   bool FCollectFileList;
+  bool FLocalLocal;
 };
 //---------------------------------------------------------------------
 void __fastcall TSynchronizeChecklistDialog::UpdateControls()
@@ -268,7 +282,7 @@ void __fastcall TSynchronizeChecklistDialog::UpdateControls()
   bool AnyBoth = false;
   bool AnyNonBoth = false;
   bool AnyDirectory = false;
-  TMoveData MoveData(false);
+  TMoveData MoveData(false, FLAGSET(FParams, TTerminal::spLocalLocal));
   TListItem * Item = NULL;
   while (IterateSelectedItems(Item))
   {
@@ -311,7 +325,7 @@ void __fastcall TSynchronizeChecklistDialog::UpdateControls()
   UncheckAllAction->Enabled = (FChecked[0] > 0) && !FSynchronizing;
   CheckDirectoryAction->Enabled = CheckAllAction->Enabled; // sic
   UncheckDirectoryAction->Enabled = UncheckAllAction->Enabled; // sic
-  CustomCommandsAction->Enabled = AnyBoth && !AnyNonBoth && DebugAlwaysTrue(!FSynchronizing);
+  CustomCommandsAction->Enabled = (FOnCustomCommandMenu != NULL) && AnyBoth && !AnyNonBoth && DebugAlwaysTrue(!FSynchronizing);
   ReverseAction->Enabled = (SelCount > 0) && DebugAlwaysTrue(!FSynchronizing);
   MoveAction->Enabled =
     // All actions are of exactly two and opposite types
@@ -1277,7 +1291,10 @@ void __fastcall TSynchronizeChecklistDialog::FormAfterMonitorDpiChanged(TObject
 void __fastcall TSynchronizeChecklistDialog::ProcessedItem(void * /*Token*/, const TSynchronizeChecklist::TItem * ChecklistItem)
 {
   TListItem * Item = FChecklistToListViewMap[ChecklistItem];
-  DebugAssert(Item->Checked);
+  // We can get multiple triggeres on IFileOperationProgressSink for the same file, particularly multiple errors.
+  // Do not know yet how (if possible at all) to tell what trigger is the last one, so we have to uncheck on the first trigger,
+  // and ignore the others.
+  DebugAssert(Item->Checked || FLAGSET(FParams, TTerminal::spLocalLocal));
   Item->Checked = false;
   Item->MakeVisible(false);
 }
@@ -1420,7 +1437,8 @@ void __fastcall TSynchronizeChecklistDialog::DeleteItem(const TSynchronizeCheckl
 //---------------------------------------------------------------------------
 void __fastcall TSynchronizeChecklistDialog::MoveActionExecute(TObject *)
 {
-  TMoveData MoveData(true);
+  bool LocalLocal = FLAGSET(FParams, TTerminal::spLocalLocal);
+  TMoveData MoveData(true, LocalLocal);
   TListItem * Item = NULL;
   while (IterateSelectedItems(Item))
   {
@@ -1445,7 +1463,7 @@ void __fastcall TSynchronizeChecklistDialog::MoveActionExecute(TObject *)
   if (DeleteAction->Action == TSynchronizeChecklist::saDeleteRemote)
   {
     Side = osRemote;
-    NewFileName = UnixCombinePaths(TransferChecklistItem->Info2.Directory, TransferChecklistItem->Info1.FileName);
+    NewFileName = UniversalCombinePaths(!LocalLocal, TransferChecklistItem->Info2.Directory, TransferChecklistItem->Info1.FileName);
   }
   else if (DebugAlwaysTrue(DeleteAction->Action == TSynchronizeChecklist::saDeleteLocal))
   {
@@ -1554,7 +1572,7 @@ void __fastcall TSynchronizeChecklistDialog::KeyDown(Word & Key, TShiftState Shi
 {
   TShortCut KeyShortCut = ShortCut(Key, Shift);
   TShortCut CustomShortCut = NormalizeCustomShortCut(KeyShortCut);
-  if (IsCustomShortCut(CustomShortCut))
+  if ((FOnCustomCommandMenu != NULL) && IsCustomShortCut(CustomShortCut))
   {
     TTBXItem * MenuItem = new TTBXItem(this);
     CustomCommandsAction->ActionComponent = MenuItem;
@@ -1830,7 +1848,22 @@ void TSynchronizeChecklistDialog::PathToClipboard(bool Local)
   while (IterateSelectedItems(Item))
   {
     const TSynchronizeChecklist::TItem * ChecklistItem = GetChecklistItem(Item);
-    UnicodeString Path = Local ? ChecklistItem->ForceGetLocalPath() : ChecklistItem->ForceGetRemotePath();
+    UnicodeString Path;
+    if (Local)
+    {
+      Path = ChecklistItem->ForceGetLocalPath();
+    }
+    else
+    {
+      if (FLAGCLEAR(FParams, TTerminal::spLocalLocal))
+      {
+        Path = ChecklistItem->ForceGetRemotePath();
+      }
+      else
+      {
+        Path = ChecklistItem->ForceGetLocalPath2();
+      }
+    }
     Paths->Add(Path);
   }
 

+ 7 - 1
source/forms/SynchronizeProgress.cpp

@@ -14,7 +14,7 @@
 #pragma resource "*.dfm"
 //---------------------------------------------------------------------------
 // Used for comparing only
-__fastcall TSynchronizeProgressForm::TSynchronizeProgressForm(TComponent * Owner, bool AllowMinimize, int Files)
+__fastcall TSynchronizeProgressForm::TSynchronizeProgressForm(TComponent * Owner, bool AllowMinimize, int Files, bool LocalLocal)
   : TForm(Owner)
 {
   FStarted = false;
@@ -39,6 +39,12 @@ __fastcall TSynchronizeProgressForm::TSynchronizeProgressForm(TComponent * Owner
   {
     SetGlobalMinimizeHandler(this, GlobalMinimize);
   }
+  if (LocalLocal)
+  {
+    LeftLabel->Caption = LoadStr(SYNCHRONIZE_PROGRESS_LEFT_DIR);
+    RightLabel->Caption = LoadStr(SYNCHRONIZE_PROGRESS_RIGHT_DIR);
+    RightDirectoryLabel->UnixPath = false;
+  }
   FFrameAnimation.Init(AnimationPaintBox, L"SynchronizeDirectories");
 }
 //---------------------------------------------------------------------------

+ 2 - 2
source/forms/SynchronizeProgress.dfm

@@ -34,7 +34,7 @@ object SynchronizeProgressForm: TSynchronizeProgressForm
     Caption = 'Time left:'
     ShowAccelChar = False
   end
-  object Label1: TLabel
+  object LeftLabel: TLabel
     Left = 50
     Top = 14
     Width = 31
@@ -42,7 +42,7 @@ object SynchronizeProgressForm: TSynchronizeProgressForm
     Caption = 'Local:'
     ShowAccelChar = False
   end
-  object Label2: TLabel
+  object RightLabel: TLabel
     Left = 50
     Top = 32
     Width = 44

+ 3 - 3
source/forms/SynchronizeProgress.h

@@ -21,8 +21,8 @@
 class TSynchronizeProgressForm : public TForm
 {
 __published:
-  TLabel *Label1;
-  TLabel *Label2;
+  TLabel *LeftLabel;
+  TLabel *RightLabel;
   TPathLabel *RightDirectoryLabel;
   TPathLabel *LeftDirectoryLabel;
   TLabel *StartTimeLabel;
@@ -49,7 +49,7 @@ __published:
   void __fastcall CancelItemClick(TObject *Sender);
 
 public:
-  __fastcall TSynchronizeProgressForm(TComponent * Owner, bool AllowMinimize, int Files);
+  __fastcall TSynchronizeProgressForm(TComponent * Owner, bool AllowMinimize, int Files, bool LocalLocal);
   virtual __fastcall ~TSynchronizeProgressForm();
 
   void __fastcall Start();

+ 8 - 0
source/resource/TextsWin.h

@@ -709,6 +709,14 @@
 #define TAG_ADD                 6212
 #define TAG_KEY                 6213
 #define TAG_VALUE               6214
+#define SYNCHRONIZE_LEFT_DIR    6215
+#define SYNCHRONIZE_RIGHT_DIR   6216
+#define SYNCHRONIZE_LEFT        6217
+#define SYNCHRONIZE_RIGHT       6218
+#define SYNCHRONIZE_PROGRESS_LEFT_DIR 6219
+#define SYNCHRONIZE_PROGRESS_RIGHT_DIR 6220
+#define SYNCHRONIZE_CHECKLIST_LEFT_DIR 6221
+#define SYNCHRONIZE_CHECKLIST_RIGHT_DIR 6222
 
 // 2xxx is reserved for TextsFileZilla.h
 

+ 8 - 0
source/resource/TextsWin1.rc

@@ -714,6 +714,14 @@ BEGIN
         TAG_ADD, "Add tag"
         TAG_KEY, "&Key:"
         TAG_VALUE, "&Value (optional):"
+        SYNCHRONIZE_LEFT_DIR, "Left dire&ctory:"
+        SYNCHRONIZE_RIGHT_DIR, "Right dir&ectory:"
+        SYNCHRONIZE_LEFT, "&Left"
+        SYNCHRONIZE_RIGHT, "&Right"
+        SYNCHRONIZE_PROGRESS_LEFT_DIR, "Left:"
+        SYNCHRONIZE_PROGRESS_RIGHT_DIR, "Right:"
+        SYNCHRONIZE_CHECKLIST_LEFT_DIR, "Left directory"
+        SYNCHRONIZE_CHECKLIST_RIGHT_DIR, "Right directory"
 
         WIN_VARIABLE_STRINGS, "WIN_VARIABLE"
         WINSCP_COPYRIGHT, "Copyright © 2000–2025 Martin Prikryl"

+ 2 - 1
source/windows/WinInterface.h

@@ -375,9 +375,10 @@ bool __fastcall DoSynchronizeDialog(TSynchronizeParamType & Params,
 struct TUsableCopyParamAttrs;
 enum TSynchronizeMode { smRemote, smLocal, smBoth };
 const int fsoDisableTimestamp = 0x01;
-const int fsoDoNotUsePresets =  0x02;
+const int fsoDoNotUsePresets = 0x02;
 const int fsoAllowSelectedOnly = 0x04;
 const int fsoDisableByChecksum = 0x08;
+const int fsoLocalLocal = 0x10;
 typedef void __fastcall (__closure *TFullSynchronizeInNewWindow)
   (TSynchronizeMode Mode, int Params, const UnicodeString & Directory1, const UnicodeString & Directory2,
    const TCopyParamType * CopyParams);