Prechádzať zdrojové kódy

Bug 52: Use file hash as criterion for synchronization

https://winscp.net/tracker/52

Source commit: 66fd353f8e26a7bd3317ee7cc4e6d15e7d6cfb52
Martin Prikryl 1 rok pred
rodič
commit
804ec9c9c9

+ 26 - 14
dotnet/Session.cs

@@ -36,6 +36,8 @@ namespace WinSCP
         None = 0x00,
         Time = 0x01,
         Size = 0x02,
+        Checksum = 0x04,
+        [Obsolete("Use Time | Size")]
         Either = Time | Size,
     }
 
@@ -1265,22 +1267,32 @@ namespace WinSCP
             }
 
             string criteriaName;
-            switch (criteria)
+            if (criteria == SynchronizationCriteria.None)
             {
-                case SynchronizationCriteria.None:
-                    criteriaName = "none";
-                    break;
-                case SynchronizationCriteria.Time:
-                    criteriaName = "time";
-                    break;
-                case SynchronizationCriteria.Size:
-                    criteriaName = "size";
-                    break;
-                case SynchronizationCriteria.Either:
-                    criteriaName = "either";
-                    break;
-                default:
+                criteriaName = "none";
+            }
+            else
+            {
+                var names = new Dictionary<SynchronizationCriteria, string>
+                {
+                    { SynchronizationCriteria.Time, "time" },
+                    { SynchronizationCriteria.Size, "size" },
+                    { SynchronizationCriteria.Checksum, "checksum" }
+                };
+                var c = criteria;
+                criteriaName = string.Empty;
+                foreach (var name in names)
+                {
+                    if (c.HasFlag(name.Key))
+                    {
+                        c -= name.Key;
+                        criteriaName += (criteriaName.Length > 0 ? "," : string.Empty) + name.Value;
+                    }
+                }
+                if (c != 0)
+                {
                     throw Logger.WriteException(new ArgumentOutOfRangeException(nameof(criteria)));
+                }
             }
 
             WriteCommand(

+ 4 - 16
source/core/Common.cpp

@@ -3463,29 +3463,17 @@ TStringList * __fastcall CreateSortedStringList(bool CaseSensitive, System::Type
   return Result;
 }
 //---------------------------------------------------------------------------
-static UnicodeString __fastcall NormalizeIdent(UnicodeString Ident)
+bool SameIdent(const UnicodeString & Ident1, const UnicodeString & Ident2)
 {
-  int Index = 1;
-  while (Index <= Ident.Length())
-  {
-    if (Ident[Index] == L'-')
-    {
-      Ident.Delete(Index, 1);
-    }
-    else
-    {
-      Index++;
-    }
-  }
-  return Ident;
+  const UnicodeString Dash(L"-");
+  return SameText(ReplaceStr(Ident1, Dash, EmptyStr), ReplaceStr(Ident2, Dash, EmptyStr));
 }
 //---------------------------------------------------------------------------
 UnicodeString __fastcall FindIdent(const UnicodeString & Ident, TStrings * Idents)
 {
-  UnicodeString NormalizedIdent(NormalizeIdent(Ident));
   for (int Index = 0; Index < Idents->Count; Index++)
   {
-    if (SameText(NormalizedIdent, NormalizeIdent(Idents->Strings[Index])))
+    if (SameIdent(Ident, Idents->Strings[Index]))
     {
       return Idents->Strings[Index];
     }

+ 1 - 0
source/core/Common.h

@@ -179,6 +179,7 @@ TFormatSettings __fastcall GetEngFormatSettings();
 int __fastcall ParseShortEngMonthName(const UnicodeString & MonthStr);
 // The defaults are equal to defaults of TStringList class (except for Sorted)
 TStringList * __fastcall CreateSortedStringList(bool CaseSensitive = false, System::Types::TDuplicates Duplicates = dupIgnore);
+bool SameIdent(const UnicodeString & Ident1, const UnicodeString & Ident2);
 UnicodeString __fastcall FindIdent(const UnicodeString & Ident, TStrings * Idents);
 void __fastcall CheckCertificate(const UnicodeString & Path);
 typedef struct x509_st X509;

+ 2 - 0
source/core/Configuration.cpp

@@ -251,6 +251,7 @@ void __fastcall TConfiguration::Default()
   RefreshPuttySshHostCAList();
   FSshHostCAsFromPuTTY = false;
   FHttpsCertificateValidation = 0;
+  FSynchronizationChecksumAlgs = EmptyStr;
   CollectUsage = FDefaultCollectUsage;
 
   FLogging = false;
@@ -391,6 +392,7 @@ UnicodeString __fastcall TConfiguration::PropertyToKey(const UnicodeString & Pro
     KEY(Integer,  KeyVersion); \
     KEY(Bool,     SshHostCAsFromPuTTY); \
     KEY(Integer,  HttpsCertificateValidation); \
+    KEY(String,   SynchronizationChecksumAlgs); \
     KEY(Bool,     CollectUsage); \
     KEY(String,   CertificateStorage); \
     KEY(String,   AWSMetadataService); \

+ 2 - 0
source/core/Configuration.h

@@ -126,6 +126,7 @@ private:
   std::unique_ptr<TSshHostCAList> FPuttySshHostCAList;
   bool FSshHostCAsFromPuTTY;
   int FHttpsCertificateValidation;
+  UnicodeString FSynchronizationChecksumAlgs;
 
   bool FDisablePasswordStoring;
   bool FForceBanners;
@@ -402,6 +403,7 @@ public:
   __property TSshHostCAList * ActiveSshHostCAList = { read = GetActiveSshHostCAList };
   __property bool SshHostCAsFromPuTTY = { read = FSshHostCAsFromPuTTY, write = FSshHostCAsFromPuTTY };
   __property int HttpsCertificateValidation = { read = FHttpsCertificateValidation, write = FHttpsCertificateValidation };
+  __property UnicodeString SynchronizationChecksumAlgs = { read = FSynchronizationChecksumAlgs, write = FSynchronizationChecksumAlgs };
 
   __property UnicodeString TimeFormat = { read = GetTimeFormat };
   __property TStorage Storage  = { read=GetStorage };

+ 49 - 0
source/core/PuttyIntf.cpp

@@ -1140,6 +1140,55 @@ UnicodeString __fastcall Sha256(const char * Data, size_t Size)
   return Result;
 }
 //---------------------------------------------------------------------------
+UnicodeString CalculateFileChecksum(TStream * Stream, const UnicodeString & Alg)
+{
+  const ssh_hashalg * HashAlg;
+  if (SameIdent(Alg, Sha256ChecksumAlg))
+  {
+    HashAlg = &ssh_sha256;
+  }
+  else if (SameIdent(Alg, Sha1ChecksumAlg))
+  {
+    HashAlg = &ssh_sha1;
+  }
+  else if (SameIdent(Alg, Md5ChecksumAlg))
+  {
+    HashAlg = &ssh_md5;
+  }
+  else
+  {
+    throw Exception(FMTLOAD(UNKNOWN_CHECKSUM, (Alg)));
+  }
+
+  UnicodeString Result;
+  ssh_hash * Hash = ssh_hash_new(HashAlg);
+  try
+  {
+    const int BlockSize = 32 * 1024;
+    TFileBuffer Buffer;
+    DWORD Read;
+    do
+    {
+      Buffer.Reset();
+      Read = Buffer.LoadStream(Stream, BlockSize, false);
+      if (Read > 0)
+      {
+        put_datapl(Hash, make_ptrlen(Buffer.Data, Read));
+      }
+    }
+    while (Read > 0);
+  }
+  __finally
+  {
+    RawByteString Buf;
+    Buf.SetLength(ssh_hash_alg(Hash)->hlen);
+    ssh_hash_final(Hash, reinterpret_cast<unsigned char *>(Buf.c_str()));
+    Result = BytesToHex(Buf);
+  }
+
+  return Result;
+}
+//---------------------------------------------------------------------------
 UnicodeString __fastcall ParseOpenSshPubLine(const UnicodeString & Line, const struct ssh_keyalg *& Algorithm)
 {
   UTF8String UtfLine = UTF8String(Line);

+ 1 - 0
source/core/PuttyTools.h

@@ -37,6 +37,7 @@ UnicodeString __fastcall KeyTypeFromFingerprint(UnicodeString Fingerprint);
 UnicodeString __fastcall GetPuTTYVersion();
 //---------------------------------------------------------------------------
 UnicodeString __fastcall Sha256(const char * Data, size_t Size);
+UnicodeString CalculateFileChecksum(TStream * Stream, const UnicodeString & Alg);
 //---------------------------------------------------------------------------
 UnicodeString __fastcall ParseOpenSshPubLine(const UnicodeString & Line, const struct ssh_keyalg *& Algorithm);
 void ParseCertificatePublicKey(const UnicodeString & Str, RawByteString & PublicKey, UnicodeString & Fingerprint);

+ 51 - 21
source/core/Script.cpp

@@ -429,7 +429,7 @@ void __fastcall TScript::SetSynchronizeParams(int value)
 {
   const int AcceptedParams =
     TTerminal::spExistingOnly | TTerminal::spTimestamp |
-    TTerminal::spNotByTime | TTerminal::spBySize | TTerminal::spCaseSensitive;
+    TTerminal::spNotByTime | TTerminal::spBySize | TTerminal::spByChecksum | TTerminal::spCaseSensitive;
   FSynchronizeParams = (value & AcceptedParams);
   FWarnNonDefaultSynchronizeParams =
     (FSynchronizeParams != (TTerminal::spDefault & AcceptedParams));
@@ -2002,37 +2002,67 @@ void __fastcall TScript::SynchronizeProc(TScriptProcParams * Parameters)
     }
     if (Parameters->FindSwitch(L"criteria", Value))
     {
-      enum { None, Time, Size, Either, EitherBoth };
-      static const wchar_t * CriteriaNames[] = { L"none", L"time", L"size", L"either", L"both" };
+      enum { None, Either, EitherBoth };
+      static const wchar_t * CriteriaNames[] = { L"none", L"either", L"both" };
       int Criteria = TScriptCommands::FindCommand(CriteriaNames, LENOF(CriteriaNames), Value);
-      switch (Criteria)
+      if (Criteria >= 0)
       {
-        case None:
-          SynchronizeParams |= TTerminal::spNotByTime;
-          SynchronizeParams &= ~TTerminal::spBySize;
-          break;
+        switch (Criteria)
+        {
+          case None:
+            SynchronizeParams |= TTerminal::spNotByTime;
+            SynchronizeParams &= ~TTerminal::spBySize;
+            break;
+
+          case Either:
+          case EitherBoth:
+            SynchronizeParams &= ~TTerminal::spNotByTime;
+            SynchronizeParams |= TTerminal::spBySize;
+            break;
+        }
+      }
+      else
+      {
+        int Params = TTerminal::spNotByTime;
+        while ((Params >= 0) && !Value.IsEmpty())
+        {
+          UnicodeString Token = CutToChar(Value, L',', true);
+          enum { Time, Size, Checksum };
+          static const wchar_t * CriteriaNames[] = { L"time", L"size", L"checksum" };
+          int Criteria = TScriptCommands::FindCommand(CriteriaNames, LENOF(CriteriaNames), Token);
+          switch (Criteria)
+          {
+            case Time:
+              Params &= ~TTerminal::spNotByTime;
+              break;
 
-        case Time:
-          SynchronizeParams &= ~(TTerminal::spNotByTime | TTerminal::spBySize);
-          break;
+            case Size:
+              Params |= TTerminal::spBySize;
+              break;
 
-        case Size:
-          SynchronizeParams |= TTerminal::spNotByTime | TTerminal::spBySize;
-          break;
+            case Checksum:
+              Params |= TTerminal::spByChecksum;
+              break;
 
-        case Either:
-        case EitherBoth:
-          SynchronizeParams &= ~TTerminal::spNotByTime;
-          SynchronizeParams |= TTerminal::spBySize;
-          break;
+            default:
+              Params = -1;
+              break;
+          }
+        }
+
+        if (Params >= 0)
+        {
+          SynchronizeParams &= ~(TTerminal::spNotByTime | TTerminal::spBySize | TTerminal::spByChecksum);
+          SynchronizeParams |= Params;
+        }
       }
     }
     bool Preview = Parameters->FindSwitch(L"preview");
 
     // enforce rules
-    if (FSynchronizeMode  == TTerminal::smBoth)
+    if (FSynchronizeMode == TTerminal::smBoth)
     {
-      SynchronizeParams &= ~(TTerminal::spNotByTime | TTerminal::spBySize);
+      SynchronizeParams &= ~(TTerminal::spNotByTime | TTerminal::spBySize | TTerminal::spByChecksum);
     }
 
     CheckParams(Parameters);

+ 64 - 8
source/core/Terminal.cpp

@@ -26,6 +26,7 @@
 #include "Queue.h"
 #include "Cryptography.h"
 #include "NeonIntf.h"
+#include <PuttyTools.h>
 #include <openssl/pkcs12.h>
 #include <openssl/err.h>
 #include <algorithm>
@@ -5994,6 +5995,7 @@ UnicodeString __fastcall TTerminal::SynchronizeParamsStr(int Params)
   AddFlagName(ParamsStr, Params, spTimestamp, L"Timestamp");
   AddFlagName(ParamsStr, Params, spNotByTime, L"NotByTime");
   AddFlagName(ParamsStr, Params, spBySize, L"BySize");
+  AddFlagName(ParamsStr, Params, spByChecksum, L"ByChecksum");
   AddFlagName(ParamsStr, Params, spCaseSensitive, L"CaseSensitive");
   AddFlagName(ParamsStr, Params, spSelectedOnly, L"*SelectedOnly"); // GUI only
   AddFlagName(ParamsStr, Params, spMirror, L"Mirror");
@@ -6287,6 +6289,55 @@ bool __fastcall TTerminal::IsEmptyRemoteDirectory(
   return Params.Result && (Stats.Files == 0);
 }
 //---------------------------------------------------------------------------
+void __fastcall TTerminal::CollectCalculatedChecksum(
+  const UnicodeString & DebugUsedArg(FileName), const UnicodeString & DebugUsedArg(Alg), const UnicodeString & Hash)
+{
+  DebugAssert(FCollectedCalculatedChecksum.IsEmpty());
+  FCollectedCalculatedChecksum = Hash;
+}
+//---------------------------------------------------------------------------
+bool TTerminal::SameFileChecksum(const UnicodeString & LocalFileName, const TRemoteFile * File)
+{
+  UnicodeString DefaultAlg = Sha256ChecksumAlg;
+  UnicodeString Algs =
+    DefaultStr(Configuration->SynchronizationChecksumAlgs, DefaultAlg + L"," + Sha1ChecksumAlg);
+  std::unique_ptr<TStrings> SupportedAlgs(CreateSortedStringList());
+  GetSupportedChecksumAlgs(SupportedAlgs.get());
+  UnicodeString Alg;
+  while (Alg.IsEmpty() && !Algs.IsEmpty())
+  {
+    UnicodeString A = CutToChar(Algs, L',', true);
+
+    if (SupportedAlgs->IndexOf(A) >= 0)
+    {
+      Alg = A;
+    }
+  }
+
+  if (Alg.IsEmpty())
+  {
+    Alg = DefaultAlg;
+  }
+
+  std::unique_ptr<TStrings> FileList(new TStringList());
+  FileList->AddObject(File->FullFileName, File);
+  DebugAssert(FCollectedCalculatedChecksum.IsEmpty());
+  FCollectedCalculatedChecksum = EmptyStr;
+  CalculateFilesChecksum(Alg, FileList.get(), CollectCalculatedChecksum);
+  UnicodeString RemoteChecksum = FCollectedCalculatedChecksum;
+  FCollectedCalculatedChecksum = EmptyStr;
+
+  UnicodeString LocalChecksum;
+  FILE_OPERATION_LOOP_BEGIN
+  {
+    std::unique_ptr<THandleStream> Stream(TSafeHandleStream::CreateFromFile(LocalFileName, fmOpenRead | fmShareDenyWrite));
+    LocalChecksum = CalculateFileChecksum(Stream.get(), Alg);
+  }
+  FILE_OPERATION_LOOP_END(FMTLOAD(CHECKSUM_ERROR, (LocalFileName)));
+
+  return SameText(RemoteChecksum, LocalChecksum);
+}
+//---------------------------------------------------------------------------
 void __fastcall TTerminal::DoSynchronizeCollectFile(const UnicodeString FileName,
   const TRemoteFile * File, /*TSynchronizeData*/ void * Param)
 {
@@ -6334,6 +6385,7 @@ void __fastcall TTerminal::DoSynchronizeCollectFile(const UnicodeString FileName
             reinterpret_cast<TSynchronizeFileData *>(Data->LocalFileList->Objects[LocalIndex]);
 
           LocalData->New = false;
+          UnicodeString FullLocalFileName = LocalData->Info.Directory + LocalData->Info.FileName;
 
           if (File->IsDirectory != LocalData->IsDirectory)
           {
@@ -6394,6 +6446,14 @@ void __fastcall TTerminal::DoSynchronizeCollectFile(const UnicodeString FileName
               Modified = true;
               LocalModified = true;
             }
+            else if (FLAGSET(Data->Params, spByChecksum) &&
+                     FLAGCLEAR(Data->Params, spTimestamp) &&
+                     !SameFileChecksum(FullLocalFileName, File) &&
+                     FLAGCLEAR(Data->Params, spTimestamp))
+            {
+              Modified = true;
+              LocalModified = true;
+            }
 
             if (LocalModified)
             {
@@ -6404,25 +6464,21 @@ void __fastcall TTerminal::DoSynchronizeCollectFile(const UnicodeString FileName
               // not for sync itself
               LocalData->MatchingRemoteFileFile = File->Duplicate();
               LogEvent(FORMAT(L"Local file %s is modified comparing to remote file %s",
-                (FormatFileDetailsForLog(LocalData->Info.Directory + LocalData->Info.FileName,
-                   LocalData->Info.Modification, LocalData->Info.Size),
-                 FormatFileDetailsForLog(FullRemoteFileName,
-                   File->Modification, File->Size, File->LinkedFile))));
+                (FormatFileDetailsForLog(FullLocalFileName, LocalData->Info.Modification, LocalData->Info.Size),
+                 FormatFileDetailsForLog(FullRemoteFileName, File->Modification, File->Size, File->LinkedFile))));
             }
 
             if (Modified)
             {
               LogEvent(FORMAT(L"Remote file %s is modified comparing to local file %s",
                 (FormatFileDetailsForLog(FullRemoteFileName, File->Modification, File->Size, File->LinkedFile),
-                 FormatFileDetailsForLog(LocalData->Info.Directory + LocalData->Info.FileName,
-                   LocalData->Info.Modification, LocalData->Info.Size))));
+                 FormatFileDetailsForLog(FullLocalFileName, LocalData->Info.Modification, LocalData->Info.Size))));
             }
           }
           else if (FLAGCLEAR(Data->Params, spNoRecurse))
           {
             DoSynchronizeCollectDirectory(
-              Data->LocalDirectory + LocalData->Info.FileName,
-              Data->RemoteDirectory + File->FileName,
+              FullLocalFileName, FullRemoteFileName,
               Data->Mode, Data->CopyParam, Data->Params, Data->OnSynchronizeDirectory,
               Data->Options, (Data->Flags & ~sfFirstLevel),
               Data->Checklist);

+ 5 - 0
source/core/Terminal.h

@@ -144,6 +144,7 @@ public:
   static const int spSelectedOnly = 0x800; // not used by core
   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 spDefault = TTerminal::spNoConfirmation | TTerminal::spPreviewChanges;
 
 // for ReactOnCommand()
@@ -233,6 +234,7 @@ private:
   RawByteString FEncryptKey;
   TFileOperationProgressType::TPersistence * FOperationProgressPersistence;
   TOnceDoneOperation FOperationProgressOnceDoneOperation;
+  UnicodeString FCollectedCalculatedChecksum;
 
   void __fastcall CommandError(Exception * E, const UnicodeString Msg);
   unsigned int __fastcall CommandError(Exception * E, const UnicodeString Msg,
@@ -362,6 +364,9 @@ protected:
     const TRemoteFile * File, /*TSynchronizeData*/ void * Param);
   void __fastcall SynchronizeCollectFile(const UnicodeString FileName,
     const TRemoteFile * File, /*TSynchronizeData*/ void * Param);
+  bool SameFileChecksum(const UnicodeString & LocalFileName, const TRemoteFile * File);
+  void __fastcall CollectCalculatedChecksum(
+    const UnicodeString & FileName, const UnicodeString & Alg, const UnicodeString & Hash);
   void __fastcall SynchronizeRemoteTimestamp(const UnicodeString FileName,
     const TRemoteFile * File, void * Param);
   void __fastcall SynchronizeLocalTimestamp(const UnicodeString FileName,

+ 14 - 6
source/forms/CustomScpExplorer.cpp

@@ -4809,6 +4809,13 @@ void TCustomScpExplorerForm::LoadFilesProperties(TStrings * FileList)
   }
 }
 //---------------------------------------------------------------------------
+bool TCustomScpExplorerForm::CanCalculateChecksum()
+{
+  return
+    Terminal->IsCapable[fcCalculatingChecksum] ||
+    (Terminal->IsCapable[fcSecondaryShell] && !Terminal->IsEncryptingFiles());
+}
+//---------------------------------------------------------------------------
 bool __fastcall TCustomScpExplorerForm::SetProperties(TOperationSide Side, TStrings * FileList)
 {
   bool Result;
@@ -4890,8 +4897,7 @@ bool __fastcall TCustomScpExplorerForm::SetProperties(TOperationSide Side, TStri
       if (CapableGroupChanging) Flags |= cpGroup;
 
       TCalculateChecksumEvent CalculateChecksumEvent = NULL;
-      if (Terminal->IsCapable[fcCalculatingChecksum] ||
-          (Terminal->IsCapable[fcSecondaryShell] && !Terminal->IsEncryptingFiles()))
+      if (CanCalculateChecksum())
       {
         CalculateChecksumEvent = CalculateChecksum;
       }
@@ -5949,7 +5955,7 @@ bool __fastcall TCustomScpExplorerForm::DoSynchronizeDirectories(
   int UnusedParams =
     (GUIConfiguration->SynchronizeParams &
       (TTerminal::spPreviewChanges | TTerminal::spTimestamp | TTerminal::spNotByTime | TTerminal::spBySize |
-       TTerminal::spCaseSensitive));
+       TTerminal::spCaseSensitive | TTerminal::spMirror | TTerminal::spByChecksum));
   Params.Params = GUIConfiguration->SynchronizeParams & ~UnusedParams;
   Params.Options = GUIConfiguration->SynchronizeOptions;
   bool SaveSettings = false;
@@ -6476,13 +6482,15 @@ int __fastcall TCustomScpExplorerForm::DoFullSynchronizeDirectories(
   bool SaveSettings = false;
   int Options =
     FLAGMASK(!Terminal->IsCapable[fcTimestampChanging], fsoDisableTimestamp) |
+    FLAGMASK(!CanCalculateChecksum(), fsoDisableByChecksum) |
     FLAGMASK(SynchronizeAllowSelectedOnly(), fsoAllowSelectedOnly);
   TCopyParamType CopyParam = GUIConfiguration->CurrentCopyParam;
   TUsableCopyParamAttrs CopyParamAttrs = Terminal->UsableCopyParamAttrs(0);
   bool Continue =
-    (UseDefaults == 0) ||
-    DoFullSynchronizeDialog(Mode, Params, LocalDirectory, RemoteDirectory,
-      &CopyParam, SaveSettings, SaveMode, Options, CopyParamAttrs, FullSynchronizeInNewWindow, UseDefaults);
+    ((UseDefaults == 0) ||
+     DoFullSynchronizeDialog(Mode, Params, LocalDirectory, RemoteDirectory,
+       &CopyParam, SaveSettings, SaveMode, Options, CopyParamAttrs, FullSynchronizeInNewWindow, UseDefaults)) &&
+    (FLAGCLEAR(Params, TTerminal::spByChecksum) || EnsureCommandSessionFallback(fcCalculatingChecksum));
   if (Continue)
   {
     Configuration->Usage->Inc(L"Synchronizations");

+ 1 - 0
source/forms/CustomScpExplorer.h

@@ -760,6 +760,7 @@ protected:
   void DoBrowseFile(TCustomDirView * DirView, const UnicodeString & FileName);
   bool NeedSecondarySessionForRemoteCopy(TStrings * FileList);
   void ReleaseHiContrastTheme();
+  bool CanCalculateChecksum();
 
 public:
   virtual __fastcall ~TCustomScpExplorerForm();

+ 7 - 1
source/forms/FullSynchronize.cpp

@@ -106,11 +106,13 @@ void __fastcall TFullSynchronizeDialog::UpdateControls()
     SynchronizeExistingOnlyCheck->Checked = true;
     SynchronizeDeleteCheck->Checked = false;
     SynchronizeByTimeCheck->Checked = true;
+    SynchronizeByChecksumCheck->Checked = false;
   }
   if (SynchronizeBothButton->Checked)
   {
     SynchronizeByTimeCheck->Checked = true;
     SynchronizeBySizeCheck->Checked = false;
+    SynchronizeByChecksumCheck->Checked = false;
     if (MirrorFilesButton->Checked)
     {
       SynchronizeFilesButton->Checked = true;
@@ -123,6 +125,8 @@ void __fastcall TFullSynchronizeDialog::UpdateControls()
   EnableControl(SynchronizeByTimeCheck, !SynchronizeBothButton->Checked &&
     !SynchronizeTimestampsButton->Checked);
   EnableControl(SynchronizeBySizeCheck, !SynchronizeBothButton->Checked);
+  EnableControl(SynchronizeByChecksumCheck,
+    FLAGCLEAR(FOptions, fsoDisableByChecksum) && !SynchronizeBothButton->Checked && !SynchronizeTimestampsButton->Checked);
   EnableControl(SynchronizeSelectedOnlyCheck, FLAGSET(FOptions, fsoAllowSelectedOnly));
 
   EnableControl(OkButton, !LocalDirectoryEdit->Text.IsEmpty() &&
@@ -266,7 +270,7 @@ void __fastcall TFullSynchronizeDialog::SetParams(int value)
   FParams = value &
     ~(TTerminal::spDelete | TTerminal::spExistingOnly | TTerminal::spPreviewChanges | TTerminal::spTimestamp |
       TTerminal::spNotByTime | TTerminal::spBySize | TTerminal::spSelectedOnly | TTerminal::spMirror |
-      TTerminal::spCaseSensitive);
+      TTerminal::spCaseSensitive | TTerminal::spByChecksum);
   SynchronizeDeleteCheck->Checked = FLAGSET(value, TTerminal::spDelete);
   SynchronizeExistingOnlyCheck->Checked = FLAGSET(value, TTerminal::spExistingOnly);
   SynchronizePreviewChangesCheck->Checked = FLAGSET(value, TTerminal::spPreviewChanges);
@@ -285,6 +289,7 @@ void __fastcall TFullSynchronizeDialog::SetParams(int value)
   }
   SynchronizeByTimeCheck->Checked = FLAGCLEAR(value, TTerminal::spNotByTime);
   SynchronizeBySizeCheck->Checked = FLAGSET(value, TTerminal::spBySize);
+  SynchronizeByChecksumCheck->Checked = FLAGSET(value, TTerminal::spByChecksum);
   SynchronizeCaseSensitiveCheck->Checked = FLAGSET(value, TTerminal::spCaseSensitive);
   UpdateControls();
 }
@@ -301,6 +306,7 @@ int __fastcall TFullSynchronizeDialog::GetParams()
     FLAGMASK(MirrorFilesButton->Checked, TTerminal::spMirror) |
     FLAGMASK(!SynchronizeByTimeCheck->Checked, TTerminal::spNotByTime) |
     FLAGMASK(SynchronizeBySizeCheck->Checked, TTerminal::spBySize) |
+    FLAGMASK(SynchronizeByChecksumCheck->Enabled && SynchronizeByChecksumCheck->Checked, TTerminal::spByChecksum) |
     FLAGMASK(SynchronizeCaseSensitiveCheck->Checked, TTerminal::spCaseSensitive);
 }
 //---------------------------------------------------------------------------

+ 44 - 31
source/forms/FullSynchronize.dfm

@@ -6,7 +6,7 @@ object FullSynchronizeDialog: TFullSynchronizeDialog
   BorderIcons = [biSystemMenu, biMinimize, biMaximize, biHelp]
   BorderStyle = bsDialog
   Caption = 'Synchronize'
-  ClientHeight = 453
+  ClientHeight = 477
   ClientWidth = 481
   Color = clBtnFace
   ParentFont = True
@@ -16,7 +16,7 @@ object FullSynchronizeDialog: TFullSynchronizeDialog
   OnShow = FormShow
   DesignSize = (
     481
-    453)
+    477)
   PixelsPerInch = 96
   TextHeight = 13
   object DirectoriesGroup: TGroupBox
@@ -90,7 +90,7 @@ object FullSynchronizeDialog: TFullSynchronizeDialog
   end
   object OkButton: TButton
     Left = 220
-    Top = 420
+    Top = 444
     Width = 88
     Height = 25
     Anchors = [akRight, akBottom]
@@ -103,7 +103,7 @@ object FullSynchronizeDialog: TFullSynchronizeDialog
   end
   object CancelButton: TButton
     Left = 315
-    Top = 420
+    Top = 444
     Width = 75
     Height = 25
     Anchors = [akRight, akBottom]
@@ -115,26 +115,27 @@ object FullSynchronizeDialog: TFullSynchronizeDialog
   object OptionsGroup: TGroupBox
     Left = 8
     Top = 238
-    Width = 303
-    Height = 97
+    Width = 227
+    Height = 121
     Caption = 'Synchronize options'
     TabOrder = 3
     DesignSize = (
-      303
-      97)
+      227
+      121)
     object SynchronizeDeleteCheck: TCheckBox
       Left = 11
       Top = 20
-      Width = 130
+      Width = 210
       Height = 17
+      Anchors = [akLeft, akTop, akRight]
       Caption = '&Delete files'
       TabOrder = 0
       OnClick = ControlChange
     end
     object SynchronizeSelectedOnlyCheck: TCheckBox
-      Left = 155
-      Top = 44
-      Width = 141
+      Left = 11
+      Top = 92
+      Width = 210
       Height = 17
       Anchors = [akLeft, akTop, akRight]
       Caption = 'Selected files o&nly'
@@ -142,20 +143,21 @@ object FullSynchronizeDialog: TFullSynchronizeDialog
       OnClick = ControlChange
     end
     object SynchronizeExistingOnlyCheck: TCheckBox
-      Left = 155
-      Top = 20
-      Width = 141
+      Left = 11
+      Top = 68
+      Width = 210
       Height = 17
       Anchors = [akLeft, akTop, akRight]
-      Caption = '&Existing files only'
+      Caption = 'E&xisting files only'
       TabOrder = 1
       OnClick = ControlChange
     end
     object SynchronizePreviewChangesCheck: TCheckBox
       Left = 11
       Top = 44
-      Width = 130
+      Width = 210
       Height = 17
+      Anchors = [akLeft, akTop, akRight]
       Caption = 'Pre&view changes'
       TabOrder = 2
       OnClick = ControlChange
@@ -163,7 +165,7 @@ object FullSynchronizeDialog: TFullSynchronizeDialog
   end
   object TransferSettingsButton: TButton
     Left = 8
-    Top = 420
+    Top = 444
     Width = 161
     Height = 25
     Anchors = [akLeft, akBottom]
@@ -188,6 +190,7 @@ object FullSynchronizeDialog: TFullSynchronizeDialog
       Caption = '&Both'
       Checked = True
       TabOrder = 0
+      TabStop = True
       OnClick = ControlChange
     end
     object SynchronizeRemoteButton: TRadioButton
@@ -210,20 +213,20 @@ object FullSynchronizeDialog: TFullSynchronizeDialog
     end
   end
   object CompareCriterionsGroup: TGroupBox
-    Left = 317
+    Left = 245
     Top = 238
-    Width = 156
-    Height = 97
+    Width = 228
+    Height = 121
     Anchors = [akLeft, akTop, akRight]
     Caption = 'Comparison criteria'
     TabOrder = 4
     DesignSize = (
-      156
-      97)
+      228
+      121)
     object SynchronizeByTimeCheck: TCheckBox
       Left = 11
       Top = 20
-      Width = 138
+      Width = 210
       Height = 17
       Anchors = [akLeft, akTop, akRight]
       Caption = 'M&odification time'
@@ -233,7 +236,7 @@ object FullSynchronizeDialog: TFullSynchronizeDialog
     object SynchronizeBySizeCheck: TCheckBox
       Left = 11
       Top = 44
-      Width = 138
+      Width = 210
       Height = 17
       Anchors = [akLeft, akTop, akRight]
       Caption = 'File si&ze'
@@ -242,18 +245,28 @@ object FullSynchronizeDialog: TFullSynchronizeDialog
     end
     object SynchronizeCaseSensitiveCheck: TCheckBox
       Left = 11
-      Top = 68
-      Width = 138
+      Top = 92
+      Width = 210
       Height = 17
       Anchors = [akLeft, akTop, akRight]
       Caption = 'C&ase sensitive'
+      TabOrder = 3
+      OnClick = ControlChange
+    end
+    object SynchronizeByChecksumCheck: TCheckBox
+      Left = 11
+      Top = 68
+      Width = 210
+      Height = 17
+      Anchors = [akLeft, akTop, akRight]
+      Caption = 'C&hecksum'
       TabOrder = 2
       OnClick = ControlChange
     end
   end
   object SaveSettingsCheck: TCheckBox
     Left = 19
-    Top = 341
+    Top = 365
     Width = 454
     Height = 17
     Anchors = [akLeft, akTop, akRight]
@@ -262,7 +275,7 @@ object FullSynchronizeDialog: TFullSynchronizeDialog
   end
   object CopyParamGroup: TGroupBox
     Left = 8
-    Top = 362
+    Top = 386
     Width = 465
     Height = 50
     Anchors = [akLeft, akTop, akRight]
@@ -288,7 +301,7 @@ object FullSynchronizeDialog: TFullSynchronizeDialog
   end
   object HelpButton: TButton
     Left = 397
-    Top = 420
+    Top = 444
     Width = 75
     Height = 25
     Anchors = [akRight, akBottom]
@@ -334,7 +347,7 @@ object FullSynchronizeDialog: TFullSynchronizeDialog
   end
   object OkMenu: TPopupMenu
     Left = 416
-    Top = 360
+    Top = 384
     object Start1: TMenuItem
       Caption = '&Start'
       Default = True

+ 1 - 0
source/forms/FullSynchronize.h

@@ -52,6 +52,7 @@ __published:
   TMenuItem *Start1;
   TMenuItem *StartInNewWindowItem;
   TCheckBox *SynchronizeCaseSensitiveCheck;
+  TCheckBox *SynchronizeByChecksumCheck;
   void __fastcall ControlChange(TObject *Sender);
   void __fastcall LocalDirectoryBrowseButtonClick(TObject *Sender);
   void __fastcall TransferSettingsButtonClick(TObject *Sender);

+ 2 - 2
source/forms/SynchronizeChecklist.cpp

@@ -638,13 +638,13 @@ void __fastcall TSynchronizeChecklistDialog::StatusBarDrawPanel(
       case TSynchronizeChecklist::saUploadUpdate:
         Possible =
           ((FMode == smRemote) || (FMode == smBoth)) &&
-          (FLAGCLEAR(FParams, TTerminal::spNotByTime) || FLAGSET(FParams, TTerminal::spBySize));
+          (FLAGCLEAR(FParams, TTerminal::spNotByTime) || FLAGSET(FParams, TTerminal::spBySize) || FLAGSET(FParams, TTerminal::spByChecksum));
         break;
 
       case TSynchronizeChecklist::saDownloadUpdate:
         Possible =
           ((FMode == smLocal) || (FMode == smBoth)) &&
-          (FLAGCLEAR(FParams, TTerminal::spNotByTime) || FLAGSET(FParams, TTerminal::spBySize));
+          (FLAGCLEAR(FParams, TTerminal::spNotByTime) || FLAGSET(FParams, TTerminal::spBySize) || FLAGSET(FParams, TTerminal::spByChecksum));
         break;
 
       case TSynchronizeChecklist::saDeleteRemote:

+ 1 - 0
source/windows/WinInterface.h

@@ -374,6 +374,7 @@ enum TSynchronizeMode { smRemote, smLocal, smBoth };
 const fsoDisableTimestamp = 0x01;
 const fsoDoNotUsePresets =  0x02;
 const fsoAllowSelectedOnly = 0x04;
+const fsoDisableByChecksum = 0x08;
 typedef void __fastcall (__closure *TFullSynchronizeInNewWindow)
   (TSynchronizeMode Mode, int Params, const UnicodeString & LocalDirectory, const UnicodeString & RemoteDirectory,
    const TCopyParamType * CopyParams);