1
0
Эх сурвалжийг харах

Bug 207: Do not synchronize/transfer empty directories

https://winscp.net/tracker/207

Source commit: ab606eeb44335834ce2795b16b474a25ed30716a
Martin Prikryl 7 жил өмнө
parent
commit
5c121f0706

+ 22 - 1
source/core/CopyParam.cpp

@@ -56,6 +56,7 @@ void __fastcall TCopyParamType::Default()
   NewerOnly = false;
   NewerOnly = false;
   EncryptNewFiles = true;
   EncryptNewFiles = true;
   ExcludeHiddenFiles = false;
   ExcludeHiddenFiles = false;
+  ExcludeEmptyDirectories = false;
 }
 }
 //---------------------------------------------------------------------------
 //---------------------------------------------------------------------------
 UnicodeString __fastcall TCopyParamType::GetInfoStr(
 UnicodeString __fastcall TCopyParamType::GetInfoStr(
@@ -456,6 +457,20 @@ void __fastcall TCopyParamType::DoGetInfoStr(
     }
     }
   }
   }
 
 
+  if (ExcludeEmptyDirectories != Defaults.ExcludeEmptyDirectories)
+  {
+    if (DebugAlwaysTrue(ExcludeEmptyDirectories))
+    {
+      const int Except = 0;
+      ADD(StripHotkey(LoadStr(COPY_INFO_EXCLUDE_EMPTY_DIRS)), Except);
+      if (FLAGCLEAR(Options, Except))
+      {
+        NoScriptArgs = true;
+        NoCodeProperties = true;
+      }
+    }
+  }
+
   bool ResumeThresholdDiffers = ((ResumeSupport == rsSmart) && (ResumeThreshold != Defaults.ResumeThreshold));
   bool ResumeThresholdDiffers = ((ResumeSupport == rsSmart) && (ResumeThreshold != Defaults.ResumeThreshold));
   if (((ResumeSupport != Defaults.ResumeSupport) || ResumeThresholdDiffers) &&
   if (((ResumeSupport != Defaults.ResumeSupport) || ResumeThresholdDiffers) &&
       (TransferMode != tmAscii) && FLAGCLEAR(Options, cpaNoResumeSupport))
       (TransferMode != tmAscii) && FLAGCLEAR(Options, cpaNoResumeSupport))
@@ -541,6 +556,7 @@ void __fastcall TCopyParamType::Assign(const TCopyParamType * Source)
   COPY(NewerOnly);
   COPY(NewerOnly);
   COPY(EncryptNewFiles);
   COPY(EncryptNewFiles);
   COPY(ExcludeHiddenFiles);
   COPY(ExcludeHiddenFiles);
+  COPY(ExcludeEmptyDirectories);
   #undef COPY
   #undef COPY
 }
 }
 //---------------------------------------------------------------------------
 //---------------------------------------------------------------------------
@@ -711,7 +727,7 @@ UnicodeString __fastcall TCopyParamType::GetLogStr() const
        BooleanToEngStr(CalculateSize),
        BooleanToEngStr(CalculateSize),
        FileMask)) +
        FileMask)) +
     FORMAT(
     FORMAT(
-      L"  TM: %s; ClAr: %s; RemEOF: %s; RemBOM: %s; CPS: %u; NewerOnly: %s; EncryptNewFiles: %s; ExcludeHiddenFiles: %s; InclM: %s; ResumeL: %d\n"
+      L"  TM: %s; ClAr: %s; RemEOF: %s; RemBOM: %s; CPS: %u; NewerOnly: %s; EncryptNewFiles: %s; ExcludeHiddenFiles: %s; ExcludeEmptyDirectories: %s; InclM: %s; ResumeL: %d\n"
        "  AscM: %s\n",
        "  AscM: %s\n",
       (ModeC[TransferMode],
       (ModeC[TransferMode],
        BooleanToEngStr(ClearArchive),
        BooleanToEngStr(ClearArchive),
@@ -721,6 +737,7 @@ UnicodeString __fastcall TCopyParamType::GetLogStr() const
        BooleanToEngStr(NewerOnly),
        BooleanToEngStr(NewerOnly),
        BooleanToEngStr(EncryptNewFiles),
        BooleanToEngStr(EncryptNewFiles),
        BooleanToEngStr(ExcludeHiddenFiles),
        BooleanToEngStr(ExcludeHiddenFiles),
+       BooleanToEngStr(ExcludeEmptyDirectories),
        IncludeFileMask.Masks,
        IncludeFileMask.Masks,
        ((FTransferSkipList.get() != NULL) ? FTransferSkipList->Count : 0) + (!FTransferResumeFile.IsEmpty() ? 1 : 0),
        ((FTransferSkipList.get() != NULL) ? FTransferSkipList->Count : 0) + (!FTransferResumeFile.IsEmpty() ? 1 : 0),
        AsciiFileMask.Masks));
        AsciiFileMask.Masks));
@@ -752,6 +769,7 @@ bool __fastcall TCopyParamType::AllowAnyTransfer() const
   return
   return
     IncludeFileMask.Masks.IsEmpty() &&
     IncludeFileMask.Masks.IsEmpty() &&
     !ExcludeHiddenFiles &&
     !ExcludeHiddenFiles &&
+    !ExcludeEmptyDirectories &&
     ((FTransferSkipList.get() == NULL) || (FTransferSkipList->Count == 0)) &&
     ((FTransferSkipList.get() == NULL) || (FTransferSkipList->Count == 0)) &&
     FTransferResumeFile.IsEmpty();
     FTransferResumeFile.IsEmpty();
 }
 }
@@ -861,6 +879,7 @@ void __fastcall TCopyParamType::Load(THierarchicalStorage * Storage)
   NewerOnly = Storage->ReadBool(L"NewerOnly", NewerOnly);
   NewerOnly = Storage->ReadBool(L"NewerOnly", NewerOnly);
   EncryptNewFiles = Storage->ReadBool(L"EncryptNewFiles", EncryptNewFiles);
   EncryptNewFiles = Storage->ReadBool(L"EncryptNewFiles", EncryptNewFiles);
   ExcludeHiddenFiles = Storage->ReadBool(L"ExcludeHiddenFiles", ExcludeHiddenFiles);
   ExcludeHiddenFiles = Storage->ReadBool(L"ExcludeHiddenFiles", ExcludeHiddenFiles);
+  ExcludeEmptyDirectories = Storage->ReadBool(L"ExcludeEmptyDirectories", ExcludeEmptyDirectories);
 }
 }
 //---------------------------------------------------------------------------
 //---------------------------------------------------------------------------
 void __fastcall TCopyParamType::Save(THierarchicalStorage * Storage, const TCopyParamType * Defaults) const
 void __fastcall TCopyParamType::Save(THierarchicalStorage * Storage, const TCopyParamType * Defaults) const
@@ -906,6 +925,7 @@ void __fastcall TCopyParamType::Save(THierarchicalStorage * Storage, const TCopy
   WRITE_DATA(Bool, NewerOnly);
   WRITE_DATA(Bool, NewerOnly);
   WRITE_DATA(Bool, EncryptNewFiles);
   WRITE_DATA(Bool, EncryptNewFiles);
   WRITE_DATA(Bool, ExcludeHiddenFiles);
   WRITE_DATA(Bool, ExcludeHiddenFiles);
+  WRITE_DATA(Bool, ExcludeEmptyDirectories);
 }
 }
 //---------------------------------------------------------------------------
 //---------------------------------------------------------------------------
 #define C(Property) (Property == rhp.Property)
 #define C(Property) (Property == rhp.Property)
@@ -939,6 +959,7 @@ bool __fastcall TCopyParamType::operator==(const TCopyParamType & rhp) const
     C(NewerOnly) &&
     C(NewerOnly) &&
     C(EncryptNewFiles) &&
     C(EncryptNewFiles) &&
     C(ExcludeHiddenFiles) &&
     C(ExcludeHiddenFiles) &&
+    C(ExcludeEmptyDirectories) &&
     true;
     true;
 }
 }
 #undef C
 #undef C

+ 2 - 0
source/core/CopyParam.h

@@ -65,6 +65,7 @@ private:
   bool FNewerOnly;
   bool FNewerOnly;
   bool FEncryptNewFiles;
   bool FEncryptNewFiles;
   bool FExcludeHiddenFiles;
   bool FExcludeHiddenFiles;
+  bool FExcludeEmptyDirectories;
   static const wchar_t TokenPrefix = L'%';
   static const wchar_t TokenPrefix = L'%';
   static const wchar_t NoReplacement = wchar_t(false);
   static const wchar_t NoReplacement = wchar_t(false);
   static const wchar_t TokenReplacement = wchar_t(true);
   static const wchar_t TokenReplacement = wchar_t(true);
@@ -140,6 +141,7 @@ public:
   __property bool NewerOnly = { read = FNewerOnly, write = FNewerOnly };
   __property bool NewerOnly = { read = FNewerOnly, write = FNewerOnly };
   __property bool EncryptNewFiles = { read = FEncryptNewFiles, write = FEncryptNewFiles };
   __property bool EncryptNewFiles = { read = FEncryptNewFiles, write = FEncryptNewFiles };
   __property bool ExcludeHiddenFiles = { read = FExcludeHiddenFiles, write = FExcludeHiddenFiles };
   __property bool ExcludeHiddenFiles = { read = FExcludeHiddenFiles, write = FExcludeHiddenFiles };
+  __property bool ExcludeEmptyDirectories = { read = FExcludeEmptyDirectories, write = FExcludeEmptyDirectories };
 };
 };
 //---------------------------------------------------------------------------
 //---------------------------------------------------------------------------
 unsigned long __fastcall GetSpeedLimit(const UnicodeString & Text);
 unsigned long __fastcall GetSpeedLimit(const UnicodeString & Text);

+ 135 - 46
source/core/Terminal.cpp

@@ -93,6 +93,13 @@ TCalculateSizeStats::TCalculateSizeStats()
   memset(this, 0, sizeof(*this));
   memset(this, 0, sizeof(*this));
 }
 }
 //---------------------------------------------------------------------------
 //---------------------------------------------------------------------------
+TCalculateSizeParams::TCalculateSizeParams()
+{
+  memset(this, 0, sizeof(*this));
+  Result = true;
+  AllowDirs = true;
+}
+//---------------------------------------------------------------------------
 TSynchronizeOptions::TSynchronizeOptions()
 TSynchronizeOptions::TSynchronizeOptions()
 {
 {
   memset(this, 0, sizeof(*this));
   memset(this, 0, sizeof(*this));
@@ -4316,20 +4323,8 @@ void __fastcall TTerminal::CalculateFileSize(UnicodeString FileName,
     Abort();
     Abort();
   }
   }
 
 
-  bool AllowTransfer = (AParams->CopyParam == NULL);
-  if (!AllowTransfer)
-  {
-    TFileMasks::TParams MaskParams;
-    MaskParams.Size = File->Size;
-    MaskParams.Modification = File->Modification;
-
-    UnicodeString BaseFileName =
-      GetBaseFileName(UnixExcludeTrailingBackslash(File->FullFileName));
-    AllowTransfer = AParams->CopyParam->AllowTransfer(
-      BaseFileName, osRemote, File->IsDirectory, MaskParams, File->IsHidden);
-  }
-
-  if (AllowTransfer)
+  if ((AParams->CopyParam == NULL) ||
+      DoAllowRemoteFileTransfer(File, AParams->CopyParam, FLAGSET(AParams->Params, csDisallowTemporaryTransferFiles)))
   {
   {
     int CollectionIndex = -1;
     int CollectionIndex = -1;
     if (AParams->Files != NULL)
     if (AParams->Files != NULL)
@@ -4346,11 +4341,19 @@ void __fastcall TTerminal::CalculateFileSize(UnicodeString FileName,
         {
         {
           AParams->Result = false;
           AParams->Result = false;
         }
         }
+        else if (FLAGSET(AParams->Params, csStopOnFirstFile) && (AParams->Stats->Files > 0))
+        {
+          // do not waste time recursing into a folder, if we already found some files
+        }
         else
         else
         {
         {
-          LogEvent(FORMAT(L"Getting size of directory \"%s\"", (FileName)));
+          if (FLAGCLEAR(AParams->Params, csStopOnFirstFile))
+          {
+            LogEvent(FORMAT(L"Getting size of directory \"%s\"", (FileName)));
+          }
+
           // pass in full path so we get it back in file list for AllowTransfer() exclusion
           // pass in full path so we get it back in file list for AllowTransfer() exclusion
-          if (!DoCalculateDirectorySize(File->FullFileName, File, AParams))
+          if (!DoCalculateDirectorySize(File->FullFileName, AParams))
           {
           {
             if (CollectionIndex >= 0)
             if (CollectionIndex >= 0)
             {
             {
@@ -4376,10 +4379,14 @@ void __fastcall TTerminal::CalculateFileSize(UnicodeString FileName,
   }
   }
 }
 }
 //---------------------------------------------------------------------------
 //---------------------------------------------------------------------------
-bool __fastcall TTerminal::DoCalculateDirectorySize(const UnicodeString FileName,
-  const TRemoteFile * /*File*/, TCalculateSizeParams * Params)
+bool __fastcall TTerminal::DoCalculateDirectorySize(const UnicodeString & FileName, TCalculateSizeParams * Params)
 {
 {
   bool Result = false;
   bool Result = false;
+  if (FLAGSET(Params->Params, csStopOnFirstFile) && (Configuration->ActualLogProtocol >= 1))
+  {
+    LogEvent(FORMAT(L"Checking if remote directory \"%s\" is empty", (FileName)));
+  }
+
   TRetryOperationLoop RetryLoop(this);
   TRetryOperationLoop RetryLoop(this);
   do
   do
   {
   {
@@ -4398,6 +4405,19 @@ bool __fastcall TTerminal::DoCalculateDirectorySize(const UnicodeString FileName
     }
     }
   }
   }
   while (RetryLoop.Retry());
   while (RetryLoop.Retry());
+
+  if (Configuration->ActualLogProtocol >= 1)
+  {
+    if (Params->Stats->Files == 0)
+    {
+      LogEvent(FORMAT(L"Remote directory \"%s\" is empty", (FileName)));
+    }
+    else
+    {
+      LogEvent(FORMAT(L"Remote directory \"%s\" is not empty", (FileName)));
+    }
+  }
+
   return Result;
   return Result;
 }
 }
 //---------------------------------------------------------------------------
 //---------------------------------------------------------------------------
@@ -4413,13 +4433,10 @@ bool __fastcall TTerminal::CalculateFilesSize(TStrings * FileList,
   FUseBusyCursor = false;
   FUseBusyCursor = false;
 
 
   TCalculateSizeParams Param;
   TCalculateSizeParams Param;
-  Param.Size = 0;
   Param.Params = Params;
   Param.Params = Params;
   Param.CopyParam = CopyParam;
   Param.CopyParam = CopyParam;
   Param.Stats = &Stats;
   Param.Stats = &Stats;
   Param.AllowDirs = AllowDirs;
   Param.AllowDirs = AllowDirs;
-  Param.Result = true;
-  Param.Files = NULL;
   ProcessFiles(FileList, foCalculateSize, DoCalculateFileSize, &Param);
   ProcessFiles(FileList, foCalculateSize, DoCalculateFileSize, &Param);
   Size = Param.Size;
   Size = Param.Size;
   return Param.Result;
   return Param.Result;
@@ -5171,20 +5188,41 @@ void __fastcall TTerminal::OpenLocalFile(
 }
 }
 //---------------------------------------------------------------------------
 //---------------------------------------------------------------------------
 bool __fastcall TTerminal::DoAllowLocalFileTransfer(
 bool __fastcall TTerminal::DoAllowLocalFileTransfer(
-  const UnicodeString & FileName, const TSearchRecSmart & SearchRec, const TCopyParamType * CopyParam)
+  const UnicodeString & FileName, const TSearchRecSmart & SearchRec, const TCopyParamType * CopyParam, bool DisallowTemporaryTransferFiles)
 {
 {
   TFileMasks::TParams Params;
   TFileMasks::TParams Params;
   Params.Size = SearchRec.Size;
   Params.Size = SearchRec.Size;
   Params.Modification = SearchRec.GetLastWriteTime();
   Params.Modification = SearchRec.GetLastWriteTime();
   UnicodeString BaseFileName = GetBaseFileName(FileName);
   UnicodeString BaseFileName = GetBaseFileName(FileName);
-  return CopyParam->AllowTransfer(BaseFileName, osLocal, SearchRec.IsDirectory(), Params, SearchRec.IsHidden());
+  // Note that for synchronization, we do not need to check "TSynchronizeOptions::MatchesFilter" here as
+  // that is checked for top-level entries only and we are never top-level here.
+  return
+    CopyParam->AllowTransfer(BaseFileName, osLocal, SearchRec.IsDirectory(), Params, SearchRec.IsHidden()) &&
+    (!DisallowTemporaryTransferFiles || !FFileSystem->TemporaryTransferFile(FileName)) &&
+    (!SearchRec.IsDirectory() || !CopyParam->ExcludeEmptyDirectories ||
+       !IsEmptyLocalDirectory(FileName, CopyParam, DisallowTemporaryTransferFiles));
+}
+//---------------------------------------------------------------------------
+bool __fastcall TTerminal::DoAllowRemoteFileTransfer(
+  const TRemoteFile * File, const TCopyParamType * CopyParam, bool DisallowTemporaryTransferFiles)
+{
+  TFileMasks::TParams MaskParams;
+  MaskParams.Size = File->Size;
+  MaskParams.Modification = File->Modification;
+  UnicodeString FullRemoteFileName = UnixExcludeTrailingBackslash(File->FullFileName);
+  UnicodeString BaseFileName = GetBaseFileName(FullRemoteFileName);
+  return
+    CopyParam->AllowTransfer(BaseFileName, osRemote, File->IsDirectory, MaskParams, File->IsHidden) &&
+    (!DisallowTemporaryTransferFiles || !FFileSystem->TemporaryTransferFile(File->FileName)) &&
+    (!File->IsDirectory || !CopyParam->ExcludeEmptyDirectories ||
+       !IsEmptyRemoteDirectory(File, CopyParam, DisallowTemporaryTransferFiles));
 }
 }
 //---------------------------------------------------------------------------
 //---------------------------------------------------------------------------
 bool __fastcall TTerminal::AllowLocalFileTransfer(UnicodeString FileName,
 bool __fastcall TTerminal::AllowLocalFileTransfer(UnicodeString FileName,
   const TCopyParamType * CopyParam, TFileOperationProgressType * OperationProgress)
   const TCopyParamType * CopyParam, TFileOperationProgressType * OperationProgress)
 {
 {
   bool Result = true;
   bool Result = true;
-  // optimization
+  // optimization (though in most uses of the method, the caller actually knows TSearchRec already, so it could pass it here)
   if (Log->Logging || !CopyParam->AllowAnyTransfer())
   if (Log->Logging || !CopyParam->AllowAnyTransfer())
   {
   {
     TSearchRecSmart SearchRec;
     TSearchRecSmart SearchRec;
@@ -5197,7 +5235,7 @@ bool __fastcall TTerminal::AllowLocalFileTransfer(UnicodeString FileName,
     }
     }
     FILE_OPERATION_LOOP_END(FMTLOAD(FILE_NOT_EXISTS, (FileName)));
     FILE_OPERATION_LOOP_END(FMTLOAD(FILE_NOT_EXISTS, (FileName)));
 
 
-    if (!DoAllowLocalFileTransfer(FileName, SearchRec, CopyParam))
+    if (!DoAllowLocalFileTransfer(FileName, SearchRec, CopyParam, false))
     {
     {
       LogEvent(FORMAT(L"File \"%s\" excluded from transfer", (FileName)));
       LogEvent(FORMAT(L"File \"%s\" excluded from transfer", (FileName)));
       Result = false;
       Result = false;
@@ -5250,7 +5288,7 @@ void __fastcall TTerminal::CalculateLocalFileSize(
     try
     try
     {
     {
       if ((AParams->CopyParam == NULL) ||
       if ((AParams->CopyParam == NULL) ||
-          DoAllowLocalFileTransfer(FileName, Rec, AParams->CopyParam))
+          DoAllowLocalFileTransfer(FileName, Rec, AParams->CopyParam, false))
       {
       {
         int CollectionIndex = -1;
         int CollectionIndex = -1;
         if (AParams->Files != NULL)
         if (AParams->Files != NULL)
@@ -5480,6 +5518,49 @@ bool __fastcall TTerminal::LocalFindNextLoop(TSearchRecChecked & SearchRec)
   return Result;
   return Result;
 }
 }
 //---------------------------------------------------------------------------
 //---------------------------------------------------------------------------
+bool __fastcall TTerminal::IsEmptyLocalDirectory(
+  const UnicodeString & Path, const TCopyParamType * CopyParam, bool DisallowTemporaryTransferFiles)
+{
+  UnicodeString Contents;
+  if (Configuration->ActualLogProtocol >= 1)
+  {
+    LogEvent(FORMAT(L"Checking if local directory \"%s\" is empty", (Path)));
+  }
+
+  TSearchRecOwned SearchRec;
+  if (LocalFindFirstLoop(IncludeTrailingBackslash(Path) + L"*.*", SearchRec))
+  {
+    do
+    {
+      UnicodeString FullLocalFileName = IncludeTrailingBackslash(Path) + SearchRec.Name;
+      if (SearchRec.IsRealFile() &&
+          DoAllowLocalFileTransfer(FullLocalFileName, SearchRec, CopyParam, true))
+      {
+        if (!SearchRec.IsDirectory() ||
+            !IsEmptyLocalDirectory(FullLocalFileName, CopyParam, DisallowTemporaryTransferFiles))
+        {
+          Contents = SearchRec.Name;
+        }
+      }
+    }
+    while (Contents.IsEmpty() && LocalFindNextLoop(SearchRec));
+  }
+
+  if (Configuration->ActualLogProtocol >= 1)
+  {
+    if (Contents.IsEmpty())
+    {
+      LogEvent(FORMAT(L"Local directory \"%s\" is empty", (Path)));
+    }
+    else
+    {
+      LogEvent(FORMAT(L"Local directory \"%s\" is not empty, it contains \"%s\"", (Path, Contents)));
+    }
+  }
+
+  return Contents.IsEmpty();
+}
+//---------------------------------------------------------------------------
 void __fastcall TTerminal::DoSynchronizeCollectDirectory(const UnicodeString LocalDirectory,
 void __fastcall TTerminal::DoSynchronizeCollectDirectory(const UnicodeString LocalDirectory,
   const UnicodeString RemoteDirectory, TSynchronizeMode Mode,
   const UnicodeString RemoteDirectory, TSynchronizeMode Mode,
   const TCopyParamType * CopyParam, int Params,
   const TCopyParamType * CopyParam, int Params,
@@ -5518,12 +5599,10 @@ void __fastcall TTerminal::DoSynchronizeCollectDirectory(const UnicodeString Loc
       do
       do
       {
       {
         UnicodeString FileName = SearchRec.Name;
         UnicodeString FileName = SearchRec.Name;
-        UnicodeString RemoteFileName =
-          ChangeFileName(CopyParam, FileName, osLocal, false);
         UnicodeString FullLocalFileName = Data.LocalDirectory + FileName;
         UnicodeString FullLocalFileName = Data.LocalDirectory + FileName;
+        UnicodeString RemoteFileName = ChangeFileName(CopyParam, FileName, osLocal, false);
         if (SearchRec.IsRealFile() &&
         if (SearchRec.IsRealFile() &&
-            DoAllowLocalFileTransfer(FullLocalFileName, SearchRec, CopyParam) &&
-            !FFileSystem->TemporaryTransferFile(FileName) &&
+            DoAllowLocalFileTransfer(FullLocalFileName, SearchRec, CopyParam, true) &&
             (FLAGCLEAR(Flags, sfFirstLevel) ||
             (FLAGCLEAR(Flags, sfFirstLevel) ||
              (Options == NULL) ||
              (Options == NULL) ||
              Options->MatchesFilter(FileName) ||
              Options->MatchesFilter(FileName) ||
@@ -5687,21 +5766,32 @@ void __fastcall TTerminal::SynchronizeCollectFile(const UnicodeString FileName,
   }
   }
 }
 }
 //---------------------------------------------------------------------------
 //---------------------------------------------------------------------------
+bool __fastcall TTerminal::IsEmptyRemoteDirectory(
+  const TRemoteFile * File, const TCopyParamType * ACopyParam, bool DisallowTemporaryTransferFiles)
+{
+  TCalculateSizeStats Stats;
+
+  TCopyParamType CopyParam(*ACopyParam);
+  CopyParam.ExcludeEmptyDirectories = false; // to avoid endless recursion
+
+  TCalculateSizeParams Params;
+  Params.Params = csStopOnFirstFile | csIgnoreErrors | FLAGMASK(DisallowTemporaryTransferFiles, csDisallowTemporaryTransferFiles);
+  Params.CopyParam = &CopyParam;
+  Params.Stats = &Stats;
+
+  DoCalculateDirectorySize(UnixExcludeTrailingBackslash(File->FullFileName), &Params);
+
+  return Params.Result && (Stats.Files == 0);
+}
+//---------------------------------------------------------------------------
 void __fastcall TTerminal::DoSynchronizeCollectFile(const UnicodeString FileName,
 void __fastcall TTerminal::DoSynchronizeCollectFile(const UnicodeString FileName,
   const TRemoteFile * File, /*TSynchronizeData*/ void * Param)
   const TRemoteFile * File, /*TSynchronizeData*/ void * Param)
 {
 {
   TSynchronizeData * Data = static_cast<TSynchronizeData *>(Param);
   TSynchronizeData * Data = static_cast<TSynchronizeData *>(Param);
 
 
-  TFileMasks::TParams MaskParams;
-  MaskParams.Size = File->Size;
-  MaskParams.Modification = File->Modification;
-  UnicodeString LocalFileName =
-    ChangeFileName(Data->CopyParam, File->FileName, osRemote, false);
-  UnicodeString FullRemoteFileName =
-    UnixExcludeTrailingBackslash(File->FullFileName);
-  UnicodeString BaseFileName = GetBaseFileName(FullRemoteFileName);
-  if (Data->CopyParam->AllowTransfer(BaseFileName, osRemote, File->IsDirectory, MaskParams, File->IsHidden) &&
-      !FFileSystem->TemporaryTransferFile(File->FileName) &&
+  UnicodeString LocalFileName = ChangeFileName(Data->CopyParam, File->FileName, osRemote, false);
+  UnicodeString FullRemoteFileName = UnixExcludeTrailingBackslash(File->FullFileName);
+  if (DoAllowRemoteFileTransfer(File, Data->CopyParam, true) &&
       (FLAGCLEAR(Data->Flags, sfFirstLevel) ||
       (FLAGCLEAR(Data->Flags, sfFirstLevel) ||
        (Data->Options == NULL) ||
        (Data->Options == NULL) ||
         Data->Options->MatchesFilter(File->FileName) ||
         Data->Options->MatchesFilter(File->FileName) ||
@@ -7198,14 +7288,9 @@ void __fastcall TTerminal::Sink(
   TDownloadSessionAction & Action)
   TDownloadSessionAction & Action)
 {
 {
   Action.FileName(FileName);
   Action.FileName(FileName);
-
-  TFileMasks::TParams MaskParams;
   DebugAssert(File);
   DebugAssert(File);
-  MaskParams.Size = File->Size;
-  MaskParams.Modification = File->Modification;
 
 
-  UnicodeString BaseFileName = GetBaseFileName(FileName);
-  if (!CopyParam->AllowTransfer(BaseFileName, osRemote, File->IsDirectory, MaskParams, File->IsHidden))
+  if (!DoAllowRemoteFileTransfer(File, CopyParam, false))
   {
   {
     LogEvent(FORMAT(L"File \"%s\" excluded from transfer", (FileName)));
     LogEvent(FORMAT(L"File \"%s\" excluded from transfer", (FileName)));
     throw ESkipFile();
     throw ESkipFile();
@@ -7279,6 +7364,10 @@ void __fastcall TTerminal::Sink(
     LogEvent(FORMAT(L"Copying \"%s\" to local directory started.", (FileName)));
     LogEvent(FORMAT(L"Copying \"%s\" to local directory started.", (FileName)));
 
 
     // Will we use ASCII of BINARY file transfer?
     // Will we use ASCII of BINARY file transfer?
+    UnicodeString BaseFileName = GetBaseFileName(FileName);
+    TFileMasks::TParams MaskParams;
+    MaskParams.Size = File->Size;
+    MaskParams.Modification = File->Modification;
     SelectTransferMode(BaseFileName, osRemote, CopyParam, MaskParams);
     SelectTransferMode(BaseFileName, osRemote, CopyParam, MaskParams);
 
 
     // Suppose same data size to transfer as to write
     // Suppose same data size to transfer as to write

+ 12 - 3
source/core/Terminal.h

@@ -106,6 +106,8 @@ const int ccRecursive = 0x02;
 const int ccUser = 0x100;
 const int ccUser = 0x100;
 //---------------------------------------------------------------------------
 //---------------------------------------------------------------------------
 const int csIgnoreErrors = 0x01;
 const int csIgnoreErrors = 0x01;
+const int csStopOnFirstFile = 0x02;
+const int csDisallowTemporaryTransferFiles = 0x04;
 //---------------------------------------------------------------------------
 //---------------------------------------------------------------------------
 const int ropNoReadDirectory = 0x02;
 const int ropNoReadDirectory = 0x02;
 //---------------------------------------------------------------------------
 //---------------------------------------------------------------------------
@@ -311,8 +313,7 @@ protected:
     const TRemoteFile * File, /*TCalculateSizeParams*/ void * Size);
     const TRemoteFile * File, /*TCalculateSizeParams*/ void * Size);
   void __fastcall DoCalculateFileSize(UnicodeString FileName,
   void __fastcall DoCalculateFileSize(UnicodeString FileName,
     const TRemoteFile * File, void * Param);
     const TRemoteFile * File, void * Param);
-  bool __fastcall DoCalculateDirectorySize(const UnicodeString FileName,
-    const TRemoteFile * File, TCalculateSizeParams * Params);
+  bool __fastcall DoCalculateDirectorySize(const UnicodeString & FileName, TCalculateSizeParams * Params);
   void __fastcall CalculateLocalFileSize(
   void __fastcall CalculateLocalFileSize(
     const UnicodeString & FileName, const TSearchRecSmart & Rec, /*__int64*/ void * Size);
     const UnicodeString & FileName, const TSearchRecSmart & Rec, /*__int64*/ void * Size);
   bool __fastcall CalculateLocalFilesSize(TStrings * FileList, __int64 & Size,
   bool __fastcall CalculateLocalFilesSize(TStrings * FileList, __int64 & Size,
@@ -336,7 +337,13 @@ protected:
   bool __fastcall LocalFindFirstLoop(const UnicodeString & Directory, TSearchRecChecked & SearchRec);
   bool __fastcall LocalFindFirstLoop(const UnicodeString & Directory, TSearchRecChecked & SearchRec);
   bool __fastcall LocalFindNextLoop(TSearchRecChecked & SearchRec);
   bool __fastcall LocalFindNextLoop(TSearchRecChecked & SearchRec);
   bool __fastcall DoAllowLocalFileTransfer(
   bool __fastcall DoAllowLocalFileTransfer(
-    const UnicodeString & FileName, const TSearchRecSmart & SearchRec, const TCopyParamType * CopyParam);
+    const UnicodeString & FileName, const TSearchRecSmart & SearchRec, const TCopyParamType * CopyParam, bool DisallowTemporaryTransferFiles);
+  bool __fastcall DoAllowRemoteFileTransfer(
+    const TRemoteFile * File, const TCopyParamType * CopyParam, bool DisallowTemporaryTransferFiles);
+  bool __fastcall IsEmptyLocalDirectory(
+    const UnicodeString & LocalDirectory, const TCopyParamType * CopyParam, bool DisallowTemporaryTransferFiles);
+  bool __fastcall IsEmptyRemoteDirectory(
+    const TRemoteFile * File, const TCopyParamType * CopyParam, bool DisallowTemporaryTransferFiles);
   void __fastcall DoSynchronizeCollectFile(const UnicodeString FileName,
   void __fastcall DoSynchronizeCollectFile(const UnicodeString FileName,
     const TRemoteFile * File, /*TSynchronizeData*/ void * Param);
     const TRemoteFile * File, /*TSynchronizeData*/ void * Param);
   void __fastcall SynchronizeCollectFile(const UnicodeString FileName,
   void __fastcall SynchronizeCollectFile(const UnicodeString FileName,
@@ -694,6 +701,8 @@ struct TCalculateSizeStats
 //---------------------------------------------------------------------------
 //---------------------------------------------------------------------------
 struct TCalculateSizeParams
 struct TCalculateSizeParams
 {
 {
+  TCalculateSizeParams();
+
   __int64 Size;
   __int64 Size;
   int Params;
   int Params;
   const TCopyParamType * CopyParam;
   const TCopyParamType * CopyParam;

+ 6 - 6
source/forms/CopyParamCustom.dfm

@@ -6,7 +6,7 @@ object CopyParamCustomDialog: TCopyParamCustomDialog
   BorderIcons = [biSystemMenu, biMinimize, biMaximize, biHelp]
   BorderIcons = [biSystemMenu, biMinimize, biMaximize, biHelp]
   BorderStyle = bsDialog
   BorderStyle = bsDialog
   Caption = 'Transfer settings'
   Caption = 'Transfer settings'
-  ClientHeight = 501
+  ClientHeight = 527
   ClientWidth = 420
   ClientWidth = 420
   Color = clBtnFace
   Color = clBtnFace
   ParentFont = True
   ParentFont = True
@@ -15,12 +15,12 @@ object CopyParamCustomDialog: TCopyParamCustomDialog
   OnCloseQuery = FormCloseQuery
   OnCloseQuery = FormCloseQuery
   DesignSize = (
   DesignSize = (
     420
     420
-    501)
+    527)
   PixelsPerInch = 96
   PixelsPerInch = 96
   TextHeight = 13
   TextHeight = 13
   object OkButton: TButton
   object OkButton: TButton
     Left = 168
     Left = 168
-    Top = 468
+    Top = 494
     Width = 75
     Width = 75
     Height = 25
     Height = 25
     Anchors = [akRight, akBottom]
     Anchors = [akRight, akBottom]
@@ -31,7 +31,7 @@ object CopyParamCustomDialog: TCopyParamCustomDialog
   end
   end
   object CancelButton: TButton
   object CancelButton: TButton
     Left = 252
     Left = 252
-    Top = 468
+    Top = 494
     Width = 75
     Width = 75
     Height = 25
     Height = 25
     Anchors = [akRight, akBottom]
     Anchors = [akRight, akBottom]
@@ -44,13 +44,13 @@ object CopyParamCustomDialog: TCopyParamCustomDialog
     Left = 0
     Left = 0
     Top = 0
     Top = 0
     Width = 420
     Width = 420
-    Height = 463
+    Height = 489
     HelpType = htKeyword
     HelpType = htKeyword
     TabOrder = 0
     TabOrder = 0
   end
   end
   object HelpButton: TButton
   object HelpButton: TButton
     Left = 336
     Left = 336
-    Top = 468
+    Top = 494
     Width = 75
     Width = 75
     Height = 25
     Height = 25
     Anchors = [akRight, akBottom]
     Anchors = [akRight, akBottom]

+ 8 - 8
source/forms/CopyParamPreset.dfm

@@ -6,7 +6,7 @@ object CopyParamPresetDialog: TCopyParamPresetDialog
   BorderIcons = [biSystemMenu, biMinimize, biMaximize, biHelp]
   BorderIcons = [biSystemMenu, biMinimize, biMaximize, biHelp]
   BorderStyle = bsDialog
   BorderStyle = bsDialog
   Caption = 'CopyParamPresetDialog'
   Caption = 'CopyParamPresetDialog'
-  ClientHeight = 546
+  ClientHeight = 572
   ClientWidth = 675
   ClientWidth = 675
   Color = clBtnFace
   Color = clBtnFace
   ParentFont = True
   ParentFont = True
@@ -16,7 +16,7 @@ object CopyParamPresetDialog: TCopyParamPresetDialog
   OnShow = FormShow
   OnShow = FormShow
   DesignSize = (
   DesignSize = (
     675
     675
-    546)
+    572)
   PixelsPerInch = 96
   PixelsPerInch = 96
   TextHeight = 13
   TextHeight = 13
   object Label1: TLabel
   object Label1: TLabel
@@ -29,7 +29,7 @@ object CopyParamPresetDialog: TCopyParamPresetDialog
   end
   end
   object OkButton: TButton
   object OkButton: TButton
     Left = 423
     Left = 423
-    Top = 513
+    Top = 539
     Width = 75
     Width = 75
     Height = 25
     Height = 25
     Anchors = [akRight, akBottom]
     Anchors = [akRight, akBottom]
@@ -40,7 +40,7 @@ object CopyParamPresetDialog: TCopyParamPresetDialog
   end
   end
   object CancelButton: TButton
   object CancelButton: TButton
     Left = 507
     Left = 507
-    Top = 513
+    Top = 539
     Width = 75
     Width = 75
     Height = 25
     Height = 25
     Anchors = [akRight, akBottom]
     Anchors = [akRight, akBottom]
@@ -62,7 +62,7 @@ object CopyParamPresetDialog: TCopyParamPresetDialog
     Left = 2
     Left = 2
     Top = 51
     Top = 51
     Width = 420
     Width = 420
-    Height = 458
+    Height = 489
     HelpType = htKeyword
     HelpType = htKeyword
     TabOrder = 1
     TabOrder = 1
   end
   end
@@ -70,13 +70,13 @@ object CopyParamPresetDialog: TCopyParamPresetDialog
     Left = 426
     Left = 426
     Top = 91
     Top = 91
     Width = 240
     Width = 240
-    Height = 416
+    Height = 442
     Anchors = [akLeft, akTop, akRight, akBottom]
     Anchors = [akLeft, akTop, akRight, akBottom]
     Caption = 'Autoselection rule'
     Caption = 'Autoselection rule'
     TabOrder = 3
     TabOrder = 3
     DesignSize = (
     DesignSize = (
       240
       240
-      416)
+      442)
     object Label2: TLabel
     object Label2: TLabel
       Left = 16
       Left = 16
       Top = 20
       Top = 20
@@ -187,7 +187,7 @@ object CopyParamPresetDialog: TCopyParamPresetDialog
   end
   end
   object HelpButton: TButton
   object HelpButton: TButton
     Left = 591
     Left = 591
-    Top = 513
+    Top = 539
     Width = 75
     Width = 75
     Height = 25
     Height = 25
     Anchors = [akRight, akBottom]
     Anchors = [akRight, akBottom]

+ 3 - 0
source/forms/CopyParams.cpp

@@ -101,6 +101,7 @@ void __fastcall TCopyParamsFrame::SetParams(TCopyParamType value)
   NewerOnlyCheck->Checked = value.NewerOnly;
   NewerOnlyCheck->Checked = value.NewerOnly;
   EncryptNewFilesCheck->Checked = value.EncryptNewFiles;
   EncryptNewFilesCheck->Checked = value.EncryptNewFiles;
   ExcludeHiddenFilesCheck->Checked = value.ExcludeHiddenFiles;
   ExcludeHiddenFilesCheck->Checked = value.ExcludeHiddenFiles;
+  ExcludeEmptyDirectoriesCheck->Checked = value.ExcludeEmptyDirectories;
 
 
   *FParams = value;
   *FParams = value;
 
 
@@ -157,6 +158,7 @@ TCopyParamType __fastcall TCopyParamsFrame::GetParams()
   Result.NewerOnly = NewerOnlyCheck->Checked;
   Result.NewerOnly = NewerOnlyCheck->Checked;
   Result.EncryptNewFiles = EncryptNewFilesCheck->Checked;
   Result.EncryptNewFiles = EncryptNewFilesCheck->Checked;
   Result.ExcludeHiddenFiles = ExcludeHiddenFilesCheck->Checked;
   Result.ExcludeHiddenFiles = ExcludeHiddenFilesCheck->Checked;
+  Result.ExcludeEmptyDirectories = ExcludeEmptyDirectoriesCheck->Checked;
 
 
   return Result;
   return Result;
 }
 }
@@ -219,6 +221,7 @@ void __fastcall TCopyParamsFrame::UpdateControls()
   EnableControl(EncryptNewFilesCheck, FLAGCLEAR(CopyParamAttrs, cpaIncludeMaskOnly) &&
   EnableControl(EncryptNewFilesCheck, FLAGCLEAR(CopyParamAttrs, cpaIncludeMaskOnly) &&
     FLAGCLEAR(CopyParamAttrs, cpaNoEncryptNewFiles) && Enabled);
     FLAGCLEAR(CopyParamAttrs, cpaNoEncryptNewFiles) && Enabled);
   EnableControl(ExcludeHiddenFilesCheck, IncludeFileMaskCombo->Enabled);
   EnableControl(ExcludeHiddenFilesCheck, IncludeFileMaskCombo->Enabled);
+  EnableControl(ExcludeEmptyDirectoriesCheck, FLAGCLEAR(CopyParamAttrs, cpaIncludeMaskOnly));
 }
 }
 //---------------------------------------------------------------------------
 //---------------------------------------------------------------------------
 void __fastcall TCopyParamsFrame::ControlChange(TObject * /*Sender*/)
 void __fastcall TCopyParamsFrame::ControlChange(TObject * /*Sender*/)

+ 15 - 4
source/forms/CopyParams.dfm

@@ -2,7 +2,7 @@ object CopyParamsFrame: TCopyParamsFrame
   Left = 0
   Left = 0
   Top = 0
   Top = 0
   Width = 420
   Width = 420
-  Height = 463
+  Height = 489
   HelpType = htKeyword
   HelpType = htKeyword
   TabOrder = 0
   TabOrder = 0
   object CommonPropertiesGroup: TGroupBox
   object CommonPropertiesGroup: TGroupBox
@@ -295,12 +295,12 @@ object CopyParamsFrame: TCopyParamsFrame
     Left = 8
     Left = 8
     Top = 347
     Top = 347
     Width = 405
     Width = 405
-    Height = 109
+    Height = 135
     Caption = 'Other'
     Caption = 'Other'
     TabOrder = 5
     TabOrder = 5
     DesignSize = (
     DesignSize = (
       405
       405
-      109)
+      135)
     object IncludeFileMaskLabel: TLabel
     object IncludeFileMaskLabel: TLabel
       Left = 16
       Left = 16
       Top = 20
       Top = 20
@@ -357,11 +357,22 @@ object CopyParamsFrame: TCopyParamsFrame
       Top = 79
       Top = 79
       Width = 176
       Width = 176
       Height = 17
       Height = 17
-      Caption = 'E&xclude hidden files'
+      Caption = 'Exclude h&idden files'
       ParentShowHint = False
       ParentShowHint = False
       ShowHint = True
       ShowHint = True
       TabOrder = 4
       TabOrder = 4
       OnClick = ControlChange
       OnClick = ControlChange
     end
     end
+    object ExcludeEmptyDirectoriesCheck: TCheckBox
+      Left = 16
+      Top = 105
+      Width = 197
+      Height = 17
+      Caption = 'E&xclude empty directories'
+      ParentShowHint = False
+      ShowHint = True
+      TabOrder = 5
+      OnClick = ControlChange
+    end
   end
   end
 end
 end

+ 1 - 0
source/forms/CopyParams.h

@@ -52,6 +52,7 @@ __published:
   TCheckBox *PreserveTimeDirsCheck;
   TCheckBox *PreserveTimeDirsCheck;
   TCheckBox *EncryptNewFilesCheck;
   TCheckBox *EncryptNewFilesCheck;
   TCheckBox *ExcludeHiddenFilesCheck;
   TCheckBox *ExcludeHiddenFilesCheck;
+  TCheckBox *ExcludeEmptyDirectoriesCheck;
   void __fastcall ControlChange(TObject *Sender);
   void __fastcall ControlChange(TObject *Sender);
   void __fastcall ValidateMaskComboExit(TObject *Sender);
   void __fastcall ValidateMaskComboExit(TObject *Sender);
   void __fastcall RightsEditButtonClick(TObject *Sender);
   void __fastcall RightsEditButtonClick(TObject *Sender);

+ 1 - 0
source/resource/TextsCore.h

@@ -480,6 +480,7 @@
 #define COPY_CERTIFICATE_ACTION 563
 #define COPY_CERTIFICATE_ACTION 563
 #define COPY_INFO_DONT_ENCRYPT_NEW_FILES 564
 #define COPY_INFO_DONT_ENCRYPT_NEW_FILES 564
 #define COPY_INFO_EXCLUDE_HIDDEN_FILES 565
 #define COPY_INFO_EXCLUDE_HIDDEN_FILES 565
+#define COPY_INFO_EXCLUDE_EMPTY_DIRS 566
 
 
 #define CORE_VARIABLE_STRINGS   600
 #define CORE_VARIABLE_STRINGS   600
 #define PUTTY_BASED_ON          601
 #define PUTTY_BASED_ON          601

+ 1 - 0
source/resource/TextsCore1.rc

@@ -449,6 +449,7 @@ BEGIN
   COPY_CERTIFICATE_ACTION, "&Copy certificate fingerprint to clipboard"
   COPY_CERTIFICATE_ACTION, "&Copy certificate fingerprint to clipboard"
   COPY_INFO_DONT_ENCRYPT_NEW_FILES, "Do not encrypt new files"
   COPY_INFO_DONT_ENCRYPT_NEW_FILES, "Do not encrypt new files"
   COPY_INFO_EXCLUDE_HIDDEN_FILES, "Exclude hidden files"
   COPY_INFO_EXCLUDE_HIDDEN_FILES, "Exclude hidden files"
+  COPY_INFO_EXCLUDE_EMPTY_DIRS, "Exclude empty directories"
 
 
   CORE_VARIABLE_STRINGS, "CORE_VARIABLE"
   CORE_VARIABLE_STRINGS, "CORE_VARIABLE"
   PUTTY_BASED_ON, "SSH and SCP code based on PuTTY %s"
   PUTTY_BASED_ON, "SSH and SCP code based on PuTTY %s"