浏览代码

Parallel background uploads

Source commit: 1015897f6614be286e8d8fbb2e059fc831350fcf
Martin Prikryl 9 年之前
父节点
当前提交
f8018d0f98

+ 347 - 62
source/core/FileOperationProgress.cpp

@@ -12,15 +12,20 @@ __fastcall TFileOperationProgressType::TFileOperationProgressType()
 {
   FOnProgress = NULL;
   FOnFinished = NULL;
+  FParent = NULL;
+  Init();
   Clear();
 }
 //---------------------------------------------------------------------------
 __fastcall TFileOperationProgressType::TFileOperationProgressType(
-  TFileOperationProgressEvent AOnProgress, TFileOperationFinished AOnFinished)
+  TFileOperationProgressEvent AOnProgress, TFileOperationFinished AOnFinished,
+  TFileOperationProgressType * Parent)
 {
   FOnProgress = AOnProgress;
   FOnFinished = AOnFinished;
+  FParent = Parent;
   FReset = false;
+  Init();
   Clear();
 }
 //---------------------------------------------------------------------------
@@ -28,14 +33,33 @@ __fastcall TFileOperationProgressType::~TFileOperationProgressType()
 {
   DebugAssert(!InProgress || FReset);
   DebugAssert(!Suspended || FReset);
+  SAFE_DESTROY(FSection);
+  SAFE_DESTROY(FUserSelectionsSection);
+}
+//---------------------------------------------------------------------------
+void __fastcall TFileOperationProgressType::Init()
+{
+  FSection = new TCriticalSection();
+  FUserSelectionsSection = new TCriticalSection();
+}
+//---------------------------------------------------------------------------
+void __fastcall TFileOperationProgressType::Assign(const TFileOperationProgressType & Other)
+{
+  TValueRestorer<TCriticalSection *> SectionRestorer(FSection);
+  TValueRestorer<TCriticalSection *> UserSelectionsSectionRestorer(FUserSelectionsSection);
+  TGuard Guard(FSection);
+  TGuard OtherGuard(Other.FSection);
+
+  *this = Other;
 }
 //---------------------------------------------------------------------------
 void __fastcall TFileOperationProgressType::AssignButKeepSuspendState(const TFileOperationProgressType & Other)
 {
+  TGuard Guard(FSection);
   TValueRestorer<unsigned int> SuspendTimeRestorer(FSuspendTime);
   TValueRestorer<bool> SuspendedRestorer(FSuspended);
 
-  *this = Other;
+  Assign(Other);
 }
 //---------------------------------------------------------------------------
 void __fastcall TFileOperationProgressType::Clear()
@@ -46,10 +70,12 @@ void __fastcall TFileOperationProgressType::Clear()
   FAsciiTransfer = false;
   FCount = -1;
   FFilesFinished = 0;
+  FFilesFinishedSuccessfully = 0;
   FStartTime = Now();
   FSuspended = false;
   FSuspendTime = 0;
   FInProgress = false;
+  FDone = false;
   FFileInProgress = false;
   FTotalTransferred = 0;
   FTotalSkipped = 0;
@@ -73,8 +99,9 @@ void __fastcall TFileOperationProgressType::ClearTransfer()
 {
   if ((TransferSize > 0) && (TransferredSize < TransferSize))
   {
+    TGuard Guard(FSection);
     __int64 RemainingSize = (TransferSize - TransferredSize);
-    FTotalSkipped += RemainingSize;
+    AddSkipped(RemainingSize);
   }
   FLocalSize = 0;
   FTransferSize = 0;
@@ -95,15 +122,20 @@ void __fastcall TFileOperationProgressType::Start(TFileOperation AOperation,
   TOperationSide ASide, int ACount, bool ATemp,
   const UnicodeString ADirectory, unsigned long ACPSLimit)
 {
-  Clear();
-  FOperation = AOperation;
-  FSide = ASide;
-  FCount = ACount;
-  FInProgress = true;
-  FCancel = csContinue;
-  FDirectory = ADirectory;
-  FTemp = ATemp;
-  FCPSLimit = ACPSLimit;
+
+  {
+    TGuard Guard(FSection); // not really needed, just for consistency
+    Clear();
+    FOperation = AOperation;
+    FSide = ASide;
+    FCount = ACount;
+    FInProgress = true;
+    FCancel = csContinue;
+    FDirectory = ADirectory;
+    FTemp = ATemp;
+    FCPSLimit = ACPSLimit;
+  }
+
   try
   {
     DoProgress();
@@ -131,27 +163,42 @@ void __fastcall TFileOperationProgressType::Stop()
   DoProgress();
 }
 //---------------------------------------------------------------------------
+void __fastcall TFileOperationProgressType::SetDone()
+{
+  FDone = true;
+  DoProgress();
+}
+//---------------------------------------------------------------------------
 void __fastcall TFileOperationProgressType::Suspend()
 {
-  DebugAssert(!Suspended);
-  FSuspended = true;
-  FSuspendTime = GetTickCount();
+
+  {
+    TGuard Guard(FSection);
+    DebugAssert(!Suspended);
+    FSuspended = true;
+    FSuspendTime = GetTickCount();
+  }
+
   DoProgress();
 }
 //---------------------------------------------------------------------------
 void __fastcall TFileOperationProgressType::Resume()
 {
-  DebugAssert(Suspended);
-  FSuspended = false;
 
-  // shift timestamps for CPS calculation in advance
-  // by the time the progress was suspended
-  unsigned long Stopped = (GetTickCount() - FSuspendTime);
-  size_t i = 0;
-  while (i < FTicks.size())
   {
-    FTicks[i] += Stopped;
-    ++i;
+    TGuard Guard(FSection);
+    DebugAssert(Suspended);
+    FSuspended = false;
+
+    // shift timestamps for CPS calculation in advance
+    // by the time the progress was suspended
+    unsigned long Stopped = (GetTickCount() - FSuspendTime);
+    size_t i = 0;
+    while (i < FTicks.size())
+    {
+      FTicks[i] += Stopped;
+      ++i;
+    }
   }
 
   DoProgress();
@@ -159,8 +206,15 @@ void __fastcall TFileOperationProgressType::Resume()
 //---------------------------------------------------------------------------
 int __fastcall TFileOperationProgressType::OperationProgress()
 {
-  DebugAssert(FCount > 0);
-  int Result = (FFilesFinished * 100)/FCount;
+  int Result;
+  if (FCount > 0)
+  {
+    Result = (FFilesFinished * 100)/FCount;
+  }
+  else
+  {
+    Result = 0;
+  }
   return Result;
 }
 //---------------------------------------------------------------------------
@@ -180,8 +234,9 @@ int __fastcall TFileOperationProgressType::TransferProgress()
 //---------------------------------------------------------------------------
 int __fastcall TFileOperationProgressType::TotalTransferProgress()
 {
+  TGuard Guard(FSection);
   DebugAssert(TotalSizeSet);
-  int Result = TotalSize > 0 ? (int)(((TotalTransferred + TotalSkipped) * 100)/TotalSize) : 0;
+  int Result = FTotalSize > 0 ? (int)(((FTotalTransferred + FTotalSkipped) * 100)/FTotalSize) : 0;
   return Result < 100 ? Result : 100;
 }
 //---------------------------------------------------------------------------
@@ -214,10 +269,14 @@ void __fastcall TFileOperationProgressType::Finish(UnicodeString FileName,
 {
   DebugAssert(InProgress);
 
+  // Cancel reader is guarded
   FOnFinished(Operation, Side, Temp, FileName,
-    /* TODO : There wasn't 'Success' condition, was it by mistake or by purpose? */
     Success && (Cancel == csContinue), OnceDoneOperation);
   FFilesFinished++;
+  if (Success)
+  {
+    FFilesFinishedSuccessfully++;
+  }
   DoProgress();
 }
 //---------------------------------------------------------------------------
@@ -276,6 +335,7 @@ void __fastcall TFileOperationProgressType::SetSpeedCounters()
   }
 }
 //---------------------------------------------------------------------------
+// Used in WebDAV protocol
 void __fastcall TFileOperationProgressType::ThrottleToCPSLimit(
   unsigned long Size)
 {
@@ -291,6 +351,7 @@ unsigned long __fastcall TFileOperationProgressType::AdjustToCPSLimit(
 {
   SetSpeedCounters();
 
+  // CPSLimit reader is guarded, we cannot block whole method as it can last long.
   if (CPSLimit > 0)
   {
     // we must not return 0, hence, if we reach zero,
@@ -327,6 +388,7 @@ unsigned long __fastcall TFileOperationProgressType::AdjustToCPSLimit(
   return Size;
 }
 //---------------------------------------------------------------------------
+// Use in SCP protocol only
 unsigned long __fastcall TFileOperationProgressType::LocalBlockSize()
 {
   unsigned long Result = TRANSFER_BUF_SIZE;
@@ -340,8 +402,15 @@ unsigned long __fastcall TFileOperationProgressType::LocalBlockSize()
 //---------------------------------------------------------------------------
 void __fastcall TFileOperationProgressType::SetTotalSize(__int64 ASize)
 {
+  TGuard Guard(FSection); // not really needed, just for consistency
+
   FTotalSize = ASize;
   FTotalSizeSet = true;
+  // parent has its own totals
+  if (FParent != NULL)
+  {
+    DebugAssert(FParent->TotalSizeSet);
+  }
   DoProgress();
 }
 //---------------------------------------------------------------------------
@@ -356,42 +425,163 @@ void __fastcall TFileOperationProgressType::SetTransferringFile(bool ATransferri
   FTransferringFile = ATransferringFile;
 }
 //---------------------------------------------------------------------------
+bool __fastcall TFileOperationProgressType::PassCancelToParent(TCancelStatus ACancel)
+{
+  bool Result;
+  if (ACancel < csCancel)
+  {
+    // do not propagate csCancelFile,
+    // though it's not supported for queue atm, so we do not expect it here
+    DebugFail();
+    Result = false;
+  }
+  else if (ACancel == csCancel)
+  {
+    Result = true;
+  }
+  else
+  {
+    // csCancelTransfer and csRemoteAbort are used with SCP only, which does not use parallel transfers
+    DebugFail();
+    Result = false;
+  }
+  return Result;
+}
+//---------------------------------------------------------------------------
 void __fastcall TFileOperationProgressType::SetCancel(TCancelStatus ACancel)
 {
+  TGuard Guard(FSection);
   FCancel = ACancel;
+
+  if ((FParent != NULL) && PassCancelToParent(ACancel))
+  {
+    FParent->SetCancel(ACancel);
+  }
 }
 //---------------------------------------------------------------------------
 void __fastcall TFileOperationProgressType::SetCancelAtLeast(TCancelStatus ACancel)
 {
+  TGuard Guard(FSection);
   if (FCancel < ACancel)
   {
     FCancel = ACancel;
   }
+
+  if ((FParent != NULL) && PassCancelToParent(ACancel))
+  {
+    FParent->SetCancelAtLeast(ACancel);
+  }
+}
+//---------------------------------------------------------------------------
+TCancelStatus __fastcall TFileOperationProgressType::GetCancel()
+{
+  TCancelStatus Result = FCancel;
+  if (FParent != NULL)
+  {
+    TGuard Guard(FSection);
+    TCancelStatus ParentCancel = FParent->Cancel;
+    if (ParentCancel > Result)
+    {
+      Result = ParentCancel;
+    }
+  }
+  return Result;
 }
 //---------------------------------------------------------------------------
 bool __fastcall TFileOperationProgressType::ClearCancelFile()
 {
+  TGuard Guard(FSection);
+  // Not propagated to parent, as this is local flag, see also PassCancelToParent
   bool Result = (Cancel == csCancelFile);
   if (Result)
   {
-    SetCancel(csContinue);
+    FCancel = csContinue;
+  }
+  return Result;
+}
+//---------------------------------------------------------------------------
+unsigned long __fastcall TFileOperationProgressType::GetCPSLimit()
+{
+  unsigned int Result;
+  if (FParent != NULL)
+  {
+    Result = FParent->CPSLimit;
+  }
+  else
+  {
+    TGuard Guard(FSection);
+    Result = FCPSLimit;
   }
   return Result;
 }
 //---------------------------------------------------------------------------
 void __fastcall TFileOperationProgressType::SetCPSLimit(unsigned long ACPSLimit)
 {
-  FCPSLimit = ACPSLimit;
+  if (FParent != NULL)
+  {
+    FParent->SetCPSLimit(ACPSLimit);
+  }
+  else
+  {
+    TGuard Guard(FSection);
+    FCPSLimit = ACPSLimit;
+  }
+}
+//---------------------------------------------------------------------------
+TBatchOverwrite __fastcall TFileOperationProgressType::GetBatchOverwrite()
+{
+  TBatchOverwrite Result;
+  if (FParent != NULL)
+  {
+    Result = FParent->BatchOverwrite;
+  }
+  else
+  {
+    TGuard Guard(FSection); // not really needed
+    Result = FBatchOverwrite;
+  }
+  return Result;
 }
 //---------------------------------------------------------------------------
 void __fastcall TFileOperationProgressType::SetBatchOverwrite(TBatchOverwrite ABatchOverwrite)
 {
-  FBatchOverwrite = ABatchOverwrite;
+  if (FParent != NULL)
+  {
+    FParent->SetBatchOverwrite(ABatchOverwrite);
+  }
+  else
+  {
+    TGuard Guard(FSection); // not really needed
+    FBatchOverwrite = ABatchOverwrite;;
+  }
+}
+//---------------------------------------------------------------------------
+bool __fastcall TFileOperationProgressType::GetSkipToAll()
+{
+  bool Result;
+  if (FParent != NULL)
+  {
+    Result = FParent->SkipToAll;
+  }
+  else
+  {
+    TGuard Guard(FSection); // not really needed
+    Result = FSkipToAll;
+  }
+  return Result;
 }
 //---------------------------------------------------------------------------
 void __fastcall TFileOperationProgressType::SetSkipToAll()
 {
-  FSkipToAll = true;
+  if (FParent != NULL)
+  {
+    FParent->SetSkipToAll();
+  }
+  else
+  {
+    TGuard Guard(FSection); // not really needed
+    FSkipToAll = true;
+  }
 }
 //---------------------------------------------------------------------------
 void __fastcall TFileOperationProgressType::ChangeTransferSize(__int64 ASize)
@@ -400,27 +590,79 @@ void __fastcall TFileOperationProgressType::ChangeTransferSize(__int64 ASize)
   // on total transfer size
   if (TotalSizeSet)
   {
-    FTotalSize += (ASize - TransferSize);
+    AddTotalSize(ASize - TransferSize);
   }
   FTransferSize = ASize;
   DoProgress();
 }
 //---------------------------------------------------------------------------
-void __fastcall TFileOperationProgressType::RollbackTransfer()
+void __fastcall TFileOperationProgressType::RollbackTransferFromTotals(__int64 ATransferredSize, __int64 ASkippedSize)
 {
-  FTransferredSize -= SkippedSize;
-  DebugAssert(TransferredSize <= TotalTransferred);
-  FTotalTransferred -= TransferredSize;
-  DebugAssert(SkippedSize <= TotalSkipped);
+  TGuard Guard(FSection);
+
+  DebugAssert(ATransferredSize <= FTotalTransferred);
+  DebugAssert(ASkippedSize <= FTotalSkipped);
+  FTotalTransferred -= ATransferredSize;
   FTicks.clear();
   FTotalTransferredThen.clear();
-  FTotalSkipped -= SkippedSize;
+  FTotalSkipped -= ASkippedSize;
+
+  if (FParent != NULL)
+  {
+    FParent->RollbackTransferFromTotals(ATransferredSize, ASkippedSize);
+  }
+}
+//---------------------------------------------------------------------------
+void __fastcall TFileOperationProgressType::RollbackTransfer()
+{
+  FTransferredSize -= FSkippedSize;
+  RollbackTransferFromTotals(FTransferredSize, FSkippedSize);
   FSkippedSize = 0;
   FTransferredSize = 0;
   FTransferSize = 0;
   FLocallyUsed = 0;
 }
 //---------------------------------------------------------------------------
+void __fastcall TFileOperationProgressType::AddTransferredToTotals(__int64 ASize)
+{
+  TGuard Guard(FSection);
+
+  FTotalTransferred += ASize;
+  unsigned long Ticks = GetTickCount();
+  if (FTicks.empty() ||
+      (FTicks.back() > Ticks) || // ticks wrap after 49.7 days
+      ((Ticks - FTicks.back()) >= MSecsPerSec))
+  {
+    FTicks.push_back(Ticks);
+    FTotalTransferredThen.push_back(FTotalTransferred);
+  }
+
+  if (FTicks.size() > 10)
+  {
+    FTicks.erase(FTicks.begin());
+    FTotalTransferredThen.erase(FTotalTransferredThen.begin());
+  }
+
+  if (FParent != NULL)
+  {
+    FParent->AddTransferredToTotals(ASize);
+  }
+}
+//---------------------------------------------------------------------------
+void __fastcall TFileOperationProgressType::AddTotalSize(__int64 ASize)
+{
+  if (ASize != 0)
+  {
+    TGuard Guard(FSection);
+    FTotalSize += ASize;
+
+    if (FParent != NULL)
+    {
+      FParent->AddTotalSize(ASize);
+    }
+  }
+}
+//---------------------------------------------------------------------------
 void __fastcall TFileOperationProgressType::AddTransferred(__int64 ASize,
   bool AddToTotals)
 {
@@ -432,34 +674,32 @@ void __fastcall TFileOperationProgressType::AddTransferred(__int64 ASize,
     if (TotalSizeSet)
     {
       // we should probably guard this with AddToTotals
-      FTotalSize += (TransferredSize - TransferSize);
+      AddTotalSize(TransferredSize - TransferSize);
     }
     FTransferSize = TransferredSize;
   }
   if (AddToTotals)
   {
-    FTotalTransferred += ASize;
-    unsigned long Ticks = GetTickCount();
-    if (FTicks.empty() ||
-        (FTicks.back() > Ticks) || // ticks wrap after 49.7 days
-        ((Ticks - FTicks.back()) >= MSecsPerSec))
-    {
-      FTicks.push_back(Ticks);
-      FTotalTransferredThen.push_back(TotalTransferred);
-    }
-
-    if (FTicks.size() > 10)
-    {
-      FTicks.erase(FTicks.begin());
-      FTotalTransferredThen.erase(FTotalTransferredThen.begin());
-    }
+    AddTransferredToTotals(ASize);
   }
   DoProgress();
 }
 //---------------------------------------------------------------------------
-void __fastcall TFileOperationProgressType::AddResumed(__int64 ASize)
+void __fastcall TFileOperationProgressType::AddSkipped(__int64 ASize)
 {
+  TGuard Guard(FSection);
+
   FTotalSkipped += ASize;
+
+  if (FParent != NULL)
+  {
+    FParent->AddSkipped(ASize);
+  }
+}
+//---------------------------------------------------------------------------
+void __fastcall TFileOperationProgressType::AddResumed(__int64 ASize)
+{
+  AddSkipped(ASize);
   FSkippedSize += ASize;
   AddTransferred(ASize, false);
   AddLocallyUsed(ASize);
@@ -467,10 +707,11 @@ void __fastcall TFileOperationProgressType::AddResumed(__int64 ASize)
 //---------------------------------------------------------------------------
 void __fastcall TFileOperationProgressType::AddSkippedFileSize(__int64 ASize)
 {
-  FTotalSkipped += ASize;
+  AddSkipped(ASize);
   DoProgress();
 }
 //---------------------------------------------------------------------------
+// Use in SCP protocol only
 unsigned long __fastcall TFileOperationProgressType::TransferBlockSize()
 {
   unsigned long Result = TRANSFER_BUF_SIZE;
@@ -505,6 +746,13 @@ TDateTime __fastcall TFileOperationProgressType::TimeElapsed()
 }
 //---------------------------------------------------------------------------
 unsigned int __fastcall TFileOperationProgressType::CPS()
+{
+  TGuard Guard(FSection);
+  return GetCPS();
+}
+//---------------------------------------------------------------------------
+// Has to be called from a guarded method
+unsigned int __fastcall TFileOperationProgressType::GetCPS()
 {
   unsigned int Result;
   if (FTicks.empty())
@@ -531,7 +779,7 @@ unsigned int __fastcall TFileOperationProgressType::CPS()
     }
     else
     {
-      __int64 Transferred = (TotalTransferred - FTotalTransferredThen.front());
+      __int64 Transferred = (FTotalTransferred - FTotalTransferredThen.front());
       Result = (unsigned int)(Transferred * MSecsPerSec / TimeSpan);
     }
   }
@@ -553,12 +801,13 @@ TDateTime __fastcall TFileOperationProgressType::TimeExpected()
 //---------------------------------------------------------------------------
 TDateTime __fastcall TFileOperationProgressType::TotalTimeLeft()
 {
-  DebugAssert(TotalSizeSet);
-  unsigned int CurCps = CPS();
+  TGuard Guard(FSection);
+  DebugAssert(FTotalSizeSet);
+  unsigned int CurCps = GetCPS();
   // sanity check
-  if ((CurCps > 0) && (TotalSize > TotalSkipped + TotalTransferred))
+  if ((CurCps > 0) && (FTotalSize > FTotalSkipped + FTotalTransferred))
   {
-    return TDateTime((double)((double)(TotalSize - TotalSkipped - TotalTransferred) / CurCps) /
+    return TDateTime((double)((double)(FTotalSize - FTotalSkipped - FTotalTransferred) / CurCps) /
       SecsPerDay);
   }
   else
@@ -566,3 +815,39 @@ TDateTime __fastcall TFileOperationProgressType::TotalTimeLeft()
     return 0;
   }
 }
+//---------------------------------------------------------------------------
+__int64 __fastcall TFileOperationProgressType::GetTotalTransferred()
+{
+  TGuard Guard(FSection);
+  return FTotalTransferred;
+}
+//---------------------------------------------------------------------------
+__int64 __fastcall TFileOperationProgressType::GetTotalSize()
+{
+  TGuard Guard(FSection);
+  return FTotalSize;
+}
+//---------------------------------------------------------------------------
+void __fastcall TFileOperationProgressType::LockUserSelections()
+{
+  if (FParent != NULL)
+  {
+    FParent->LockUserSelections();
+  }
+  else
+  {
+    FUserSelectionsSection->Enter();
+  }
+}
+//---------------------------------------------------------------------------
+void __fastcall TFileOperationProgressType::UnlockUserSelections()
+{
+  if (FParent != NULL)
+  {
+    FParent->UnlockUserSelections();
+  }
+  else
+  {
+    FUserSelectionsSection->Leave();
+  }
+}

+ 33 - 8
source/core/FileOperationProgress.h

@@ -37,6 +37,7 @@ private:
   __int64 FTransferredSize;
   __int64 FSkippedSize;
   bool FInProgress;
+  bool FDone;
   bool FFileInProgress;
   TCancelStatus FCancel;
   int FCount;
@@ -49,12 +50,14 @@ private:
   unsigned long FCPSLimit;
   bool FTotalSizeSet;
   bool FSuspended;
+  TFileOperationProgressType * FParent;
 
   // when it was last time suspended (to calculate suspend time in Resume())
   unsigned int FSuspendTime;
   // when current file was started being transferred
   TDateTime FFileStartTime;
   int FFilesFinished;
+  int FFilesFinishedSuccessfully;
   TFileOperationProgressEvent FOnProgress;
   TFileOperationFinished FOnFinished;
   bool FReset;
@@ -63,11 +66,26 @@ private:
   bool FCounterSet;
   std::vector<unsigned long> FTicks;
   std::vector<__int64> FTotalTransferredThen;
+  TCriticalSection * FSection;
+  TCriticalSection * FUserSelectionsSection;
+
+  __int64 __fastcall GetTotalTransferred();
+  __int64 __fastcall GetTotalSize();
+  unsigned long __fastcall GetCPSLimit();
+  TBatchOverwrite __fastcall GetBatchOverwrite();
+  bool __fastcall GetSkipToAll();
 
 protected:
   void __fastcall ClearTransfer();
   inline void __fastcall DoProgress();
   int __fastcall OperationProgress();
+  void __fastcall AddTransferredToTotals(__int64 ASize);
+  void __fastcall AddSkipped(__int64 ASize);
+  void __fastcall AddTotalSize(__int64 ASize);
+  void __fastcall RollbackTransferFromTotals(__int64 ATransferredSize, __int64 ASkippedSize);
+  unsigned int __fastcall GetCPS();
+  void __fastcall Init();
+  static bool __fastcall PassCancelToParent(TCancelStatus ACancel);
 
 public:
   // common data
@@ -89,18 +107,19 @@ public:
   __property __int64 TransferredSize = { read = FTransferredSize };
   __property __int64 SkippedSize = { read = FSkippedSize };
   __property bool InProgress = { read = FInProgress };
+  __property bool Done = { read = FDone };
   __property bool FileInProgress = { read = FFileInProgress };
-  __property TCancelStatus Cancel = { read = FCancel };
+  __property TCancelStatus Cancel = { read = GetCancel };
   // when operation started
   __property TDateTime StartTime = { read = FStartTime };
   // bytes transferred
-  __property __int64 TotalTransferred = { read = FTotalTransferred };
-  __property __int64 TotalSkipped = { read = FTotalSkipped };
-  __property __int64 TotalSize = { read = FTotalSize };
+  __property __int64 TotalTransferred = { read = GetTotalTransferred };
+  __property __int64 TotalSize = { read = GetTotalSize };
+  __property int FilesFinishedSuccessfully = { read = FFilesFinishedSuccessfully };
 
-  __property TBatchOverwrite BatchOverwrite = { read = FBatchOverwrite };
-  __property bool SkipToAll = { read = FSkipToAll };
-  __property unsigned long CPSLimit = { read = FCPSLimit };
+  __property TBatchOverwrite BatchOverwrite = { read = GetBatchOverwrite };
+  __property bool SkipToAll = { read = GetSkipToAll };
+  __property unsigned long CPSLimit = { read = GetCPSLimit };
 
   __property bool TotalSizeSet = { read = FTotalSizeSet };
 
@@ -108,8 +127,10 @@ public:
 
   __fastcall TFileOperationProgressType();
   __fastcall TFileOperationProgressType(
-    TFileOperationProgressEvent AOnProgress, TFileOperationFinished AOnFinished);
+    TFileOperationProgressEvent AOnProgress, TFileOperationFinished AOnFinished,
+    TFileOperationProgressType * Parent = NULL);
   __fastcall ~TFileOperationProgressType();
+  void __fastcall Assign(const TFileOperationProgressType & Other);
   void __fastcall AssignButKeepSuspendState(const TFileOperationProgressType & Other);
   void __fastcall AddLocallyUsed(__int64 ASize);
   void __fastcall AddTransferred(__int64 ASize, bool AddToTotals = true);
@@ -142,7 +163,10 @@ public:
     TOperationSide ASide, int ACount, bool ATemp, const UnicodeString ADirectory,
     unsigned long ACPSLimit);
   void __fastcall Stop();
+  void __fastcall SetDone();
   void __fastcall Suspend();
+  void __fastcall LockUserSelections();
+  void __fastcall UnlockUserSelections();
   // whole operation
   TDateTime __fastcall TimeElapsed();
   // only current file
@@ -153,6 +177,7 @@ public:
   int __fastcall TotalTransferProgress();
   void __fastcall SetSpeedCounters();
   void __fastcall SetTransferringFile(bool ATransferringFile);
+  TCancelStatus __fastcall GetCancel();
   void __fastcall SetCancel(TCancelStatus ACancel);
   void __fastcall SetCancelAtLeast(TCancelStatus ACancel);
   bool __fastcall ClearCancelFile();

+ 65 - 57
source/core/FtpFileSystem.cpp

@@ -1659,22 +1659,25 @@ void __fastcall TFTPFileSystem::Sink(const UnicodeString FileName,
       }
       FILE_OPERATION_LOOP_END(FMTLOAD(CREATE_DIR_ERROR, (DestFullName)));
 
-      TSinkFileParams SinkFileParams;
-      SinkFileParams.TargetDir = IncludeTrailingBackslash(DestFullName);
-      SinkFileParams.CopyParam = CopyParam;
-      SinkFileParams.Params = Params;
-      SinkFileParams.OperationProgress = OperationProgress;
-      SinkFileParams.Skipped = false;
-      SinkFileParams.Flags = Flags & ~(tfFirstLevel | tfAutoResume);
-
-      FTerminal->ProcessDirectory(FileName, SinkFile, &SinkFileParams);
-
-      // Do not delete directory if some of its files were skip.
-      // Throw "skip file" for the directory to avoid attempt to deletion
-      // of any parent directory
-      if (FLAGSET(Params, cpDelete) && SinkFileParams.Skipped)
+      if (FLAGCLEAR(Params, cpNoRecurse))
       {
-        THROW_SKIP_FILE_NULL;
+        TSinkFileParams SinkFileParams;
+        SinkFileParams.TargetDir = IncludeTrailingBackslash(DestFullName);
+        SinkFileParams.CopyParam = CopyParam;
+        SinkFileParams.Params = Params;
+        SinkFileParams.OperationProgress = OperationProgress;
+        SinkFileParams.Skipped = false;
+        SinkFileParams.Flags = Flags & ~(tfFirstLevel | tfAutoResume);
+
+        FTerminal->ProcessDirectory(FileName, SinkFile, &SinkFileParams);
+
+        // Do not delete directory if some of its files were skip.
+        // Throw "skip file" for the directory to avoid attempt to deletion
+        // of any parent directory
+        if (FLAGSET(Params, cpDelete) && SinkFileParams.Skipped)
+        {
+          THROW_SKIP_FILE_NULL;
+        }
       }
     }
     else
@@ -1762,6 +1765,7 @@ void __fastcall TFTPFileSystem::Sink(const UnicodeString FileName,
 
   if (FLAGSET(Params, cpDelete))
   {
+    DebugAssert(FLAGCLEAR(Params, cpNoRecurse));
     // If file is directory, do not delete it recursively, because it should be
     // empty already. If not, it should not be deleted (some files were
     // skipped or some new files were copied to it, while we were downloading)
@@ -2067,60 +2071,62 @@ void __fastcall TFTPFileSystem::DirectorySource(const UnicodeString DirectoryNam
 
   OperationProgress->SetFile(DirectoryName);
 
-  int FindAttrs = faReadOnly | faHidden | faSysFile | faDirectory | faArchive;
-  TSearchRecChecked SearchRec;
-  bool FindOK;
-
-  FILE_OPERATION_LOOP_BEGIN
+  bool CreateDir = true;
+  if (FLAGCLEAR(Params, cpNoRecurse))
   {
-    FindOK =
-      (FindFirstChecked(DirectoryName + L"*.*", FindAttrs, SearchRec) == 0);
-  }
-  FILE_OPERATION_LOOP_END(FMTLOAD(LIST_DIR_ERROR, (DirectoryName)));
+    int FindAttrs = faReadOnly | faHidden | faSysFile | faDirectory | faArchive;
+    TSearchRecChecked SearchRec;
+    bool FindOK;
 
-  bool CreateDir = true;
+    FILE_OPERATION_LOOP_BEGIN
+    {
+      FindOK =
+        (FindFirstChecked(DirectoryName + L"*.*", FindAttrs, SearchRec) == 0);
+    }
+    FILE_OPERATION_LOOP_END(FMTLOAD(LIST_DIR_ERROR, (DirectoryName)));
 
-  try
-  {
-    while (FindOK && !OperationProgress->Cancel)
+    try
     {
-      UnicodeString FileName = DirectoryName + SearchRec.Name;
-      try
+      while (FindOK && !OperationProgress->Cancel)
       {
-        if ((SearchRec.Name != L".") && (SearchRec.Name != L".."))
+        UnicodeString FileName = DirectoryName + SearchRec.Name;
+        try
         {
-          SourceRobust(FileName, DestFullName, CopyParam, Params, OperationProgress,
-            Flags & ~(tfFirstLevel | tfAutoResume));
-          // if any file got uploaded (i.e. there were any file in the
-          // directory and at least one was not skipped),
-          // do not try to create the directory,
-          // as it should be already created by FZAPI during upload
-          CreateDir = false;
+          if ((SearchRec.Name != L".") && (SearchRec.Name != L".."))
+          {
+            SourceRobust(FileName, DestFullName, CopyParam, Params, OperationProgress,
+              Flags & ~(tfFirstLevel | tfAutoResume));
+            // if any file got uploaded (i.e. there were any file in the
+            // directory and at least one was not skipped),
+            // do not try to create the directory,
+            // as it should be already created by FZAPI during upload
+            CreateDir = false;
+          }
         }
-      }
-      catch (EScpSkipFile &E)
-      {
-        // If ESkipFile occurs, just log it and continue with next file
-        TSuspendFileOperationProgress Suspend(OperationProgress);
-        // here a message to user was displayed, which was not appropriate
-        // when user refused to overwrite the file in subdirectory.
-        // hopefully it won't be missing in other situations.
-        if (!FTerminal->HandleException(&E))
+        catch (EScpSkipFile &E)
         {
-          throw;
+          // If ESkipFile occurs, just log it and continue with next file
+          TSuspendFileOperationProgress Suspend(OperationProgress);
+          // here a message to user was displayed, which was not appropriate
+          // when user refused to overwrite the file in subdirectory.
+          // hopefully it won't be missing in other situations.
+          if (!FTerminal->HandleException(&E))
+          {
+            throw;
+          }
         }
-      }
 
-      FILE_OPERATION_LOOP_BEGIN
-      {
-        FindOK = (FindNextChecked(SearchRec) == 0);
+        FILE_OPERATION_LOOP_BEGIN
+        {
+          FindOK = (FindNextChecked(SearchRec) == 0);
+        }
+        FILE_OPERATION_LOOP_END(FMTLOAD(LIST_DIR_ERROR, (DirectoryName)));
       }
-      FILE_OPERATION_LOOP_END(FMTLOAD(LIST_DIR_ERROR, (DirectoryName)));
     }
-  }
-  __finally
-  {
-    FindClose(SearchRec);
+    __finally
+    {
+      FindClose(SearchRec);
+    }
   }
 
   if (CreateDir)
@@ -2164,6 +2170,7 @@ void __fastcall TFTPFileSystem::DirectorySource(const UnicodeString DirectoryNam
   /* TODO : Show error message on failure. */
   if (!OperationProgress->Cancel)
   {
+    DebugAssert(FLAGCLEAR(Params, cpNoRecurse));
     if (FLAGSET(Params, cpDelete))
     {
       RemoveDir(ApiPath(DirectoryName));
@@ -2318,6 +2325,7 @@ bool __fastcall TFTPFileSystem::IsCapable(int Capability) const
     case fcRemoveBOMUpload:
     case fcMoveToQueue:
     case fsSkipTransfer:
+    case fsParallelTransfers:
       return true;
 
     case fcPreservingTimestampUpload:

+ 200 - 9
source/core/Queue.cpp

@@ -12,6 +12,18 @@
 //---------------------------------------------------------------------------
 class TBackgroundTerminal;
 //---------------------------------------------------------------------------
+class TParallelUploadQueueItem : public TLocatedQueueItem
+{
+public:
+  __fastcall TParallelUploadQueueItem(const TUploadQueueItem * ParentItem, TParallelOperation * ParallelOperation);
+
+protected:
+  virtual void __fastcall DoExecute(TTerminal * Terminal);
+
+private:
+  TParallelOperation * FParallelOperation;
+};
+//---------------------------------------------------------------------------
 class TUserAction
 {
 public:
@@ -589,10 +601,9 @@ void __fastcall TTerminalQueue::DeleteItem(TQueueItem * Item, bool CanKeep)
       FForcedItems->Remove(Item);
       // =0  do not keep
       // <0  infinity
-      if ((FKeepDoneItemsFor != 0) && CanKeep)
+      if ((FKeepDoneItemsFor != 0) && CanKeep && Item->Complete())
       {
         DebugAssert(Item->Status == TQueueItem::qsDone);
-        Item->Complete();
         FDoneItems->Add(Item);
       }
       else
@@ -1126,6 +1137,29 @@ bool __fastcall TTerminalQueue::GetIsEmpty()
   return (FItems->Count == 0);
 }
 //---------------------------------------------------------------------------
+bool __fastcall TTerminalQueue::TryAddParallelOperation(TQueueItem * Item, bool Force)
+{
+  TGuard Guard(FItemsSection);
+
+  bool Result =
+    (FFreeTerminals > 0) ||
+    (Force && (FItemsInProcess < FTransfersLimit));
+
+  if (Result)
+  {
+    AddItem(DebugNotNull(Item->CreateParallelOperation()));
+  }
+
+  return Result;
+}
+//---------------------------------------------------------------------------
+bool __fastcall TTerminalQueue::ContinueParallelOperation()
+{
+  TGuard Guard(FItemsSection);
+
+  return (FItems->Count <= FItemsInProcess);
+}
+//---------------------------------------------------------------------------
 // TBackgroundItem
 //---------------------------------------------------------------------------
 class TBackgroundTerminal : public TSecondaryTerminal
@@ -1548,6 +1582,8 @@ __fastcall TQueueItem::TQueueItem() :
   FSection = new TCriticalSection();
   FInfo = new TInfo();
   FInfo->SingleFile = false;
+  FInfo->Primary = true;
+  FInfo->GroupToken = this;
 }
 //---------------------------------------------------------------------------
 __fastcall TQueueItem::~TQueueItem()
@@ -1561,7 +1597,7 @@ __fastcall TQueueItem::~TQueueItem()
   delete FInfo;
 }
 //---------------------------------------------------------------------------
-void __fastcall TQueueItem::Complete()
+bool __fastcall TQueueItem::Complete()
 {
   TGuard Guard(FSection);
 
@@ -1570,6 +1606,8 @@ void __fastcall TQueueItem::Complete()
     SetEvent(FCompleteEvent);
     FCompleteEvent = INVALID_HANDLE_VALUE;
   }
+
+  return FInfo->Primary;
 }
 //---------------------------------------------------------------------------
 bool __fastcall TQueueItem::IsUserActionStatus(TStatus Status)
@@ -1603,6 +1641,11 @@ void __fastcall TQueueItem::SetStatus(TStatus Status)
   }
 }
 //---------------------------------------------------------------------------
+void __fastcall TQueueItem::ProgressUpdated()
+{
+  // noop
+}
+//---------------------------------------------------------------------------
 void __fastcall TQueueItem::SetProgress(
   TFileOperationProgressType & ProgressData)
 {
@@ -1618,9 +1661,10 @@ void __fastcall TQueueItem::SetProgress(
     }
 
     DebugAssert(FProgressData != NULL);
-    *FProgressData = ProgressData;
+    FProgressData->Assign(ProgressData);
     FProgressData->Reset();
   }
+  ProgressUpdated();
   FQueue->DoQueueItemUpdate(this);
 }
 //---------------------------------------------------------------------------
@@ -1631,7 +1675,7 @@ void __fastcall TQueueItem::GetData(TQueueItemProxy * Proxy)
   DebugAssert(Proxy->FProgressData != NULL);
   if (FProgressData != NULL)
   {
-    *Proxy->FProgressData = *FProgressData;
+    Proxy->FProgressData->Assign(*FProgressData);
   }
   else
   {
@@ -1683,6 +1727,23 @@ unsigned long __fastcall TQueueItem::GetCPSLimit()
   return Result;
 }
 //---------------------------------------------------------------------------
+TQueueItem * __fastcall TQueueItem::CreateParallelOperation()
+{
+  return NULL;
+}
+//---------------------------------------------------------------------------
+void __fastcall TQueueItem::InitParallelOperationInfo(TQueueItem * Item)
+{
+  // deliberately not copying the ModifiedLocal and ModifiedRemote, not to trigger panel refresh, when sub-item completes
+  FInfo->Operation = Item->FInfo->Operation;
+  FInfo->Side = Item->FInfo->Side;
+  FInfo->Source = Item->FInfo->Source;
+  FInfo->Destination = Item->FInfo->Destination;
+  FInfo->SingleFile = DebugAlwaysFalse(Item->FInfo->SingleFile);
+  FInfo->Primary = false;
+  FInfo->GroupToken = Item->FInfo->GroupToken;
+}
+//---------------------------------------------------------------------------
 // TQueueItemProxy
 //---------------------------------------------------------------------------
 __fastcall TQueueItemProxy::TQueueItemProxy(TTerminalQueue * Queue,
@@ -1875,7 +1936,22 @@ int __fastcall TTerminalQueueStatus::GetActiveAndPendingCount()
 void __fastcall TTerminalQueueStatus::Add(TQueueItemProxy * ItemProxy)
 {
   ItemProxy->FQueueStatus = this;
-  FList->Add(ItemProxy);
+
+  int Index = FList->Count;
+  if (!ItemProxy->Info->Primary)
+  {
+    for (int I = 0; I < FList->Count; I++)
+    {
+      if (Items[I]->Info->GroupToken == ItemProxy->Info->GroupToken)
+      {
+        Index = I + 1;
+      }
+    }
+
+    DebugAssert(Index >= 0);
+  }
+
+  FList->Insert(Index, ItemProxy);
   ResetStats();
 }
 //---------------------------------------------------------------------------
@@ -1920,7 +1996,13 @@ __fastcall TLocatedQueueItem::TLocatedQueueItem(TTerminal * Terminal) :
   FCurrentDir = Terminal->CurrentDirectory;
 }
 //---------------------------------------------------------------------------
-UnicodeString __fastcall TLocatedQueueItem::StartupDirectory()
+__fastcall TLocatedQueueItem::TLocatedQueueItem(const TLocatedQueueItem & Source) :
+  TQueueItem()
+{
+  FCurrentDir = Source.FCurrentDir;
+}
+//---------------------------------------------------------------------------
+UnicodeString __fastcall TLocatedQueueItem::StartupDirectory() const
 {
   return FCurrentDir;
 }
@@ -1979,7 +2061,7 @@ unsigned long __fastcall TTransferQueueItem::DefaultCPSLimit()
 //---------------------------------------------------------------------------
 __fastcall TUploadQueueItem::TUploadQueueItem(TTerminal * Terminal,
   TStrings * FilesToCopy, const UnicodeString & TargetDir,
-  const TCopyParamType * CopyParam, int Params, bool SingleFile) :
+  const TCopyParamType * CopyParam, int Params, bool SingleFile, bool Parallel) :
   TTransferQueueItem(Terminal, FilesToCopy, TargetDir, CopyParam, Params, osLocal, SingleFile)
 {
   if (FilesToCopy->Count > 1)
@@ -2017,6 +2099,8 @@ __fastcall TUploadQueueItem::TUploadQueueItem(TTerminal * Terminal,
   FInfo->Destination =
     UnixIncludeTrailingBackslash(TargetDir) + CopyParam->FileMask;
   FInfo->ModifiedRemote = UnixIncludeTrailingBackslash(TargetDir);
+  FParallel = Parallel;
+  FLastParallelOperationAdded = GetTickCount();
 }
 //---------------------------------------------------------------------------
 void __fastcall TUploadQueueItem::DoExecute(TTerminal * Terminal)
@@ -2024,7 +2108,114 @@ void __fastcall TUploadQueueItem::DoExecute(TTerminal * Terminal)
   TTransferQueueItem::DoExecute(Terminal);
 
   DebugAssert(Terminal != NULL);
-  Terminal->CopyToRemote(FFilesToCopy, FTargetDir, FCopyParam, FParams);
+  TParallelOperation ParallelOperation;
+  FParallelOperation = &ParallelOperation;
+  try
+  {
+    Terminal->CopyToRemote(FFilesToCopy, FTargetDir, FCopyParam, FParams, &ParallelOperation);
+  }
+  __finally
+  {
+    FParallelOperation = NULL;
+  }
+}
+//---------------------------------------------------------------------------
+void __fastcall TUploadQueueItem::ProgressUpdated()
+{
+  TTransferQueueItem::ProgressUpdated();
+
+  if (FParallel)
+  {
+    bool Add = false;
+    bool Force = false;
+    DWORD LastParallelOperationAddedPrev = 0;
+
+    {
+      TGuard Guard(FSection);
+      // Move this to TTransferQueueItem once implemented for downloads too
+      DebugAssert(FParallelOperation != NULL);
+      DebugAssert((FProgressData->Operation == foCopy) || (FProgressData->Operation == foCalculateSize));
+      if (FProgressData->Operation == foCopy)
+      {
+        Add = FParallelOperation->ShouldAddClient();
+        if (Add)
+        {
+          DWORD Now = GetTickCount();
+          Force =
+            (Now - FLastParallelOperationAdded >= 5*1000) &&
+            (TimeToSeconds(FProgressData->TotalTimeLeft()) >= 20);
+          LastParallelOperationAddedPrev = FLastParallelOperationAdded;
+          // update now already to prevent race condition, but we will have to rollback it back,
+          // if we actually do not add the parallel operation
+          FLastParallelOperationAdded = Now;
+        }
+      }
+    }
+
+    if (Add)
+    {
+      if (!FQueue->TryAddParallelOperation(this, Force))
+      {
+        TGuard Guard(FSection);
+        FLastParallelOperationAdded = LastParallelOperationAddedPrev;
+      }
+    }
+  }
+}
+//---------------------------------------------------------------------------
+TQueueItem * __fastcall TUploadQueueItem::CreateParallelOperation()
+{
+  DebugAssert(FParallelOperation != NULL);
+
+  FParallelOperation->AddClient();
+  return new TParallelUploadQueueItem(this, FParallelOperation);
+}
+//---------------------------------------------------------------------------
+// TDownloadQueueItem
+//---------------------------------------------------------------------------
+__fastcall TParallelUploadQueueItem::TParallelUploadQueueItem(
+    const TUploadQueueItem * ParentItem, TParallelOperation * ParallelOperation) :
+  TLocatedQueueItem(*ParentItem),
+  FParallelOperation(ParallelOperation)
+{
+  InitParallelOperationInfo(ParentItem);
+}
+//---------------------------------------------------------------------------
+void __fastcall TParallelUploadQueueItem::DoExecute(TTerminal * Terminal)
+{
+  TLocatedQueueItem::DoExecute(Terminal);
+
+  TFileOperationProgressType OperationProgress(Terminal->OnProgress, Terminal->OnFinished, FParallelOperation->MainOperationProgress);
+  TFileOperation Operation = (FLAGSET(FParallelOperation->Params, cpDelete) ? foMove : foCopy);
+  bool Temp = FLAGSET(FParallelOperation->Params, cpTemporary);
+
+  OperationProgress.Start(
+    // CPS limit inherited from parent OperationProgress.
+    // Count not known and won't be needed as we will always have TotalSize as  we always transfer a single file at a time.
+    Operation, osLocal, -1, Temp, FParallelOperation->TargetDir, 0);
+
+  try
+  {
+    bool Continue = true;
+    do
+    {
+      int GotNext = Terminal->CopyToRemoteParallel(FParallelOperation, &OperationProgress);
+      if (GotNext < 0)
+      {
+        Continue = false;
+      }
+      else if (!FQueue->ContinueParallelOperation())
+      {
+        Continue = false;
+      }
+    }
+    while (Continue);
+  }
+  __finally
+  {
+    OperationProgress.Stop();
+    FParallelOperation->RemoveClient();
+  }
 }
 //---------------------------------------------------------------------------
 // TDownloadQueueItem

+ 21 - 4
source/core/Queue.h

@@ -66,6 +66,8 @@ class TTerminalQueue : public TSignalThread
 {
 friend class TQueueItem;
 friend class TQueueItemProxy;
+friend class TUploadQueueItem; //???
+friend class TParallelUploadQueueItem; //???
 
 public:
   __fastcall TTerminalQueue(TTerminal * Terminal, TConfiguration * Configuration);
@@ -146,6 +148,9 @@ protected:
   void __fastcall SetKeepDoneItemsFor(int value);
   void __fastcall SetEnabled(bool value);
   bool __fastcall GetIsEmpty();
+
+  bool __fastcall TryAddParallelOperation(TQueueItem * Item, bool Force);
+  bool __fastcall ContinueParallelOperation();
 };
 //---------------------------------------------------------------------------
 class TQueueItem
@@ -166,6 +171,8 @@ public:
     UnicodeString ModifiedLocal;
     UnicodeString ModifiedRemote;
     bool SingleFile;
+    bool Primary;
+    void * GroupToken;
   };
 
   static bool __fastcall IsUserActionStatus(TStatus Status);
@@ -196,8 +203,11 @@ protected:
   void __fastcall SetCPSLimit(unsigned long CPSLimit);
   unsigned long __fastcall GetCPSLimit();
   virtual unsigned long __fastcall DefaultCPSLimit();
-  virtual UnicodeString __fastcall StartupDirectory() = 0;
-  void __fastcall Complete();
+  virtual UnicodeString __fastcall StartupDirectory() const = 0;
+  virtual void __fastcall ProgressUpdated();
+  virtual TQueueItem * __fastcall CreateParallelOperation();
+  void __fastcall InitParallelOperationInfo(TQueueItem * Item);
+  bool __fastcall Complete();
 };
 //---------------------------------------------------------------------------
 class TQueueItemProxy
@@ -284,9 +294,10 @@ class TLocatedQueueItem : public TQueueItem
 {
 protected:
   __fastcall TLocatedQueueItem(TTerminal * Terminal);
+  __fastcall TLocatedQueueItem(const TLocatedQueueItem & Source);
 
   virtual void __fastcall DoExecute(TTerminal * Terminal);
-  virtual UnicodeString __fastcall StartupDirectory();
+  virtual UnicodeString __fastcall StartupDirectory() const;
 
 private:
   UnicodeString FCurrentDir;
@@ -315,10 +326,16 @@ class TUploadQueueItem : public TTransferQueueItem
 public:
   __fastcall TUploadQueueItem(TTerminal * Terminal,
     TStrings * FilesToCopy, const UnicodeString & TargetDir,
-    const TCopyParamType * CopyParam, int Params, bool SingleFile);
+    const TCopyParamType * CopyParam, int Params, bool SingleFile, bool Parallel);
 
 protected:
+  bool FParallel;
+  DWORD FLastParallelOperationAdded;
+  TParallelOperation * FParallelOperation;
+
   virtual void __fastcall DoExecute(TTerminal * Terminal);
+  virtual void __fastcall ProgressUpdated();
+  virtual TQueueItem * __fastcall CreateParallelOperation();
 };
 //---------------------------------------------------------------------------
 class TDownloadQueueItem : public TTransferQueueItem

+ 5 - 0
source/core/RemoteFiles.cpp

@@ -63,6 +63,11 @@ UnicodeString __fastcall SimpleUnixExcludeTrailingBackslash(const UnicodeString
   return UnixExcludeTrailingBackslash(Path, true);
 }
 //---------------------------------------------------------------------------
+UnicodeString __fastcall UnixCombinePaths(const UnicodeString & Path1, const UnicodeString & Path2)
+{
+  return UnixIncludeTrailingBackslash(Path1) + Path2;
+}
+//---------------------------------------------------------------------------
 Boolean __fastcall UnixSamePath(const UnicodeString Path1, const UnicodeString Path2)
 {
   return (UnixIncludeTrailingBackslash(Path1) == UnixIncludeTrailingBackslash(Path2));

+ 1 - 0
source/core/RemoteFiles.h

@@ -443,6 +443,7 @@ bool __fastcall UnixIsAbsolutePath(const UnicodeString & Path);
 UnicodeString __fastcall UnixIncludeTrailingBackslash(const UnicodeString Path);
 UnicodeString __fastcall UnixExcludeTrailingBackslash(const UnicodeString Path, bool Simple = false);
 UnicodeString __fastcall SimpleUnixExcludeTrailingBackslash(const UnicodeString Path);
+UnicodeString __fastcall UnixCombinePaths(const UnicodeString & Path1, const UnicodeString & Path2);
 UnicodeString __fastcall UnixExtractFileDir(const UnicodeString Path);
 UnicodeString __fastcall UnixExtractFilePath(const UnicodeString Path);
 UnicodeString __fastcall UnixExtractFileName(const UnicodeString Path);

+ 1 - 0
source/core/ScpFileSystem.cpp

@@ -463,6 +463,7 @@ bool __fastcall TSCPFileSystem::IsCapable(int Capability) const
     case fcPreservingTimestampDirs:
     case fcResumeSupport:
     case fsSkipTransfer:
+    case fsParallelTransfers: // does not implement cpNoRecurse
       return false;
 
     case fcChangePassword:

+ 1 - 1
source/core/Script.cpp

@@ -1479,7 +1479,7 @@ void __fastcall TScript::PutProc(TScriptProcParams * Parameters)
     CopyParamParams(CopyParam, Parameters);
     CheckParams(Parameters);
 
-    FTerminal->CopyToRemote(FileList, TargetDirectory, &CopyParam, Params);
+    FTerminal->CopyToRemote(FileList, TargetDirectory, &CopyParam, Params, NULL);
   }
   __finally
   {

+ 1 - 1
source/core/SessionInfo.h

@@ -42,7 +42,7 @@ enum TFSCapability { fcUserGroupListing, fcModeChanging, fcGroupChanging,
   fcModeChangingUpload, fcPreservingTimestampUpload, fcShellAnyCommand,
   fcSecondaryShell, fcRemoveCtrlZUpload, fcRemoveBOMUpload, fcMoveToQueue,
   fcLocking, fcPreservingTimestampDirs, fcResumeSupport,
-  fcChangePassword, fsSkipTransfer,
+  fcChangePassword, fsSkipTransfer, fsParallelTransfers,
   fcCount };
 //---------------------------------------------------------------------------
 struct TFileSystemInfo

+ 156 - 140
source/core/SftpFileSystem.cpp

@@ -2076,6 +2076,7 @@ bool __fastcall TSFTPFileSystem::IsCapable(int Capability) const
     case fcPreservingTimestampDirs:
     case fcResumeSupport:
     case fsSkipTransfer:
+    case fsParallelTransfers:
       return true;
 
     case fcRename:
@@ -4357,58 +4358,66 @@ void __fastcall TSFTPFileSystem::SFTPConfirmOverwrite(
   if (CanAppend &&
       ((Answer == qaRetry) || (Answer == qaSkip)))
   {
-    // duplicated in TTerminal::ConfirmFileOverwrite
-    bool CanAlternateResume =
-      (FileParams->DestSize < FileParams->SourceSize) && !OperationProgress->AsciiTransfer;
-    TBatchOverwrite BatchOverwrite =
-      FTerminal->EffectiveBatchOverwrite(SourceFullFileName, CopyParam, Params, OperationProgress, true);
-    // when mode is forced by batch, never query user
-    if (BatchOverwrite == boAppend)
-    {
-      OverwriteMode = omAppend;
-    }
-    else if (CanAlternateResume &&
-             ((BatchOverwrite == boResume) || (BatchOverwrite == boAlternateResume)))
-    {
-      OverwriteMode = omResume;
-    }
-    // no other option, but append
-    else if (!CanAlternateResume)
-    {
-      OverwriteMode = omAppend;
-    }
-    else
+    OperationProgress->LockUserSelections();
+    try
     {
-      TQueryParams Params(0, HELP_APPEND_OR_RESUME);
-
+      // duplicated in TTerminal::ConfirmFileOverwrite
+      bool CanAlternateResume =
+        (FileParams->DestSize < FileParams->SourceSize) && !OperationProgress->AsciiTransfer;
+      TBatchOverwrite BatchOverwrite =
+        FTerminal->EffectiveBatchOverwrite(SourceFullFileName, CopyParam, Params, OperationProgress, true);
+      // when mode is forced by batch, never query user
+      if (BatchOverwrite == boAppend)
       {
-        TSuspendFileOperationProgress Suspend(OperationProgress);
-        Answer = FTerminal->QueryUser(FORMAT(LoadStr(APPEND_OR_RESUME2), (SourceFullFileName)),
-          NULL, qaYes | qaNo | qaNoToAll | qaCancel, &Params);
+        OverwriteMode = omAppend;
       }
-
-      switch (Answer)
+      else if (CanAlternateResume &&
+               ((BatchOverwrite == boResume) || (BatchOverwrite == boAlternateResume)))
       {
-        case qaYes:
-          OverwriteMode = omAppend;
-          break;
-
-        case qaNo:
-          OverwriteMode = omResume;
-          break;
+        OverwriteMode = omResume;
+      }
+      // no other option, but append
+      else if (!CanAlternateResume)
+      {
+        OverwriteMode = omAppend;
+      }
+      else
+      {
+        TQueryParams Params(0, HELP_APPEND_OR_RESUME);
 
-        case qaNoToAll:
-          OverwriteMode = omResume;
-          OperationProgress->SetBatchOverwrite(boAlternateResume);
-          break;
+        {
+          TSuspendFileOperationProgress Suspend(OperationProgress);
+          Answer = FTerminal->QueryUser(FORMAT(LoadStr(APPEND_OR_RESUME2), (SourceFullFileName)),
+            NULL, qaYes | qaNo | qaNoToAll | qaCancel, &Params);
+        }
 
-        default: DebugFail(); //fallthru
-        case qaCancel:
-          OperationProgress->SetCancelAtLeast(csCancel);
-          Abort();
-          break;
+        switch (Answer)
+        {
+          case qaYes:
+            OverwriteMode = omAppend;
+            break;
+
+          case qaNo:
+            OverwriteMode = omResume;
+            break;
+
+          case qaNoToAll:
+            OverwriteMode = omResume;
+            OperationProgress->SetBatchOverwrite(boAlternateResume);
+            break;
+
+          default: DebugFail(); //fallthru
+          case qaCancel:
+            OperationProgress->SetCancelAtLeast(csCancel);
+            Abort();
+            break;
+        }
       }
     }
+    __finally
+    {
+      OperationProgress->UnlockUserSelections();
+    }
   }
   else if (Answer == qaIgnore)
   {
@@ -5341,82 +5350,85 @@ void __fastcall TSFTPFileSystem::SFTPDirectorySource(const UnicodeString Directo
     Flags |= tfNewDirectory;
   }
 
-  int FindAttrs = faReadOnly | faHidden | faSysFile | faDirectory | faArchive;
-  TSearchRecChecked SearchRec;
-  bool FindOK;
-
-  FILE_OPERATION_LOOP_BEGIN
+  if (FLAGCLEAR(Params, cpNoRecurse))
   {
-    FindOK =
-      (FindFirstChecked(DirectoryName + L"*.*", FindAttrs, SearchRec) == 0);
-  }
-  FILE_OPERATION_LOOP_END(FMTLOAD(LIST_DIR_ERROR, (DirectoryName)));
+    int FindAttrs = faReadOnly | faHidden | faSysFile | faDirectory | faArchive;
+    TSearchRecChecked SearchRec;
+    bool FindOK;
 
-  try
-  {
-    while (FindOK && !OperationProgress->Cancel)
+    FILE_OPERATION_LOOP_BEGIN
     {
-      UnicodeString FileName = DirectoryName + SearchRec.Name;
-      try
+      FindOK =
+        (FindFirstChecked(DirectoryName + L"*.*", FindAttrs, SearchRec) == 0);
+    }
+    FILE_OPERATION_LOOP_END(FMTLOAD(LIST_DIR_ERROR, (DirectoryName)));
+
+    try
+    {
+      while (FindOK && !OperationProgress->Cancel)
       {
-        if ((SearchRec.Name != L".") && (SearchRec.Name != L".."))
+        UnicodeString FileName = DirectoryName + SearchRec.Name;
+        try
         {
-          SFTPSourceRobust(FileName, DestFullName, CopyParam, Params, OperationProgress,
-            Flags & ~tfFirstLevel);
+          if ((SearchRec.Name != L".") && (SearchRec.Name != L".."))
+          {
+            SFTPSourceRobust(FileName, DestFullName, CopyParam, Params, OperationProgress,
+              Flags & ~tfFirstLevel);
+          }
         }
-      }
-      catch (EScpSkipFile &E)
-      {
-        // If ESkipFile occurs, just log it and continue with next file
-        TSuspendFileOperationProgress Suspend(OperationProgress);
-        // here a message to user was displayed, which was not appropriate
-        // when user refused to overwrite the file in subdirectory.
-        // hopefully it won't be missing in other situations.
-        if (!FTerminal->HandleException(&E))
+        catch (EScpSkipFile &E)
         {
-          throw;
+          // If ESkipFile occurs, just log it and continue with next file
+          TSuspendFileOperationProgress Suspend(OperationProgress);
+          // here a message to user was displayed, which was not appropriate
+          // when user refused to overwrite the file in subdirectory.
+          // hopefully it won't be missing in other situations.
+          if (!FTerminal->HandleException(&E))
+          {
+            throw;
+          }
         }
-      }
 
-      FILE_OPERATION_LOOP_BEGIN
-      {
-        FindOK = (FindNextChecked(SearchRec) == 0);
-      }
-      FILE_OPERATION_LOOP_END(FMTLOAD(LIST_DIR_ERROR, (DirectoryName)));
-    };
-  }
-  __finally
-  {
-    FindClose(SearchRec);
-  }
+        FILE_OPERATION_LOOP_BEGIN
+        {
+          FindOK = (FindNextChecked(SearchRec) == 0);
+        }
+        FILE_OPERATION_LOOP_END(FMTLOAD(LIST_DIR_ERROR, (DirectoryName)));
+      };
+    }
+    __finally
+    {
+      FindClose(SearchRec);
+    }
 
-  /* TODO : Delete also read-only directories. */
-  /* TODO : Show error message on failure. */
-  if (!OperationProgress->Cancel)
-  {
-    if (CopyParam->PreserveTime && CopyParam->PreserveTimeDirs)
+    /* TODO : Delete also read-only directories. */
+    /* TODO : Show error message on failure. */
+    if (!OperationProgress->Cancel)
     {
-      TRemoteProperties Properties;
-      Properties.Valid << vpModification;
+      if (CopyParam->PreserveTime && CopyParam->PreserveTimeDirs)
+      {
+        TRemoteProperties Properties;
+        Properties.Valid << vpModification;
 
-      FTerminal->OpenLocalFile(
-        ExcludeTrailingBackslash(DirectoryName), GENERIC_READ, NULL, NULL, NULL,
-        &Properties.Modification, &Properties.LastAccess, NULL);
+        FTerminal->OpenLocalFile(
+          ExcludeTrailingBackslash(DirectoryName), GENERIC_READ, NULL, NULL, NULL,
+          &Properties.Modification, &Properties.LastAccess, NULL);
 
-      FTerminal->ChangeFileProperties(DestFullName, NULL, &Properties);
-    }
+        FTerminal->ChangeFileProperties(DestFullName, NULL, &Properties);
+      }
 
-    if (FLAGSET(Params, cpDelete))
-    {
-      RemoveDir(ApiPath(DirectoryName));
-    }
-    else if (CopyParam->ClearArchive && FLAGSET(Attrs, faArchive))
-    {
-      FILE_OPERATION_LOOP_BEGIN
+      if (FLAGSET(Params, cpDelete))
       {
-        THROWOSIFFALSE(FileSetAttr(ApiPath(DirectoryName), Attrs & ~faArchive) == 0);
+        RemoveDir(ApiPath(DirectoryName));
+      }
+      else if (CopyParam->ClearArchive && FLAGSET(Attrs, faArchive))
+      {
+        FILE_OPERATION_LOOP_BEGIN
+        {
+          THROWOSIFFALSE(FileSetAttr(ApiPath(DirectoryName), Attrs & ~faArchive) == 0);
+        }
+        FILE_OPERATION_LOOP_END(FMTLOAD(CANT_SET_ATTRS, (DirectoryName)));
       }
-      FILE_OPERATION_LOOP_END(FMTLOAD(CANT_SET_ATTRS, (DirectoryName)));
     }
   }
 }
@@ -5574,53 +5586,56 @@ void __fastcall TSFTPFileSystem::SFTPSink(const UnicodeString FileName,
       }
       FILE_OPERATION_LOOP_END(FMTLOAD(CREATE_DIR_ERROR, (DestFullName)));
 
-      TSinkFileParams SinkFileParams;
-      SinkFileParams.TargetDir = IncludeTrailingBackslash(DestFullName);
-      SinkFileParams.CopyParam = CopyParam;
-      SinkFileParams.Params = Params;
-      SinkFileParams.OperationProgress = OperationProgress;
-      SinkFileParams.Skipped = false;
-      SinkFileParams.Flags = Flags & ~tfFirstLevel;
+      if (FLAGCLEAR(Params, cpNoRecurse))
+      {
+        TSinkFileParams SinkFileParams;
+        SinkFileParams.TargetDir = IncludeTrailingBackslash(DestFullName);
+        SinkFileParams.CopyParam = CopyParam;
+        SinkFileParams.Params = Params;
+        SinkFileParams.OperationProgress = OperationProgress;
+        SinkFileParams.Skipped = false;
+        SinkFileParams.Flags = Flags & ~tfFirstLevel;
 
-      FTerminal->ProcessDirectory(FileName, SFTPSinkFile, &SinkFileParams);
+        FTerminal->ProcessDirectory(FileName, SFTPSinkFile, &SinkFileParams);
 
-      if (CopyParam->PreserveTime && CopyParam->PreserveTimeDirs)
-      {
-        FTerminal->LogEvent(FORMAT(L"Preserving directory timestamp [%s]",
-          (StandardTimestamp(File->Modification))));
-        int SetFileTimeError = ERROR_SUCCESS;
-        // FILE_FLAG_BACKUP_SEMANTICS is needed to "open" directory
-        HANDLE LocalHandle = CreateFile(ApiPath(DestFullName).c_str(), GENERIC_WRITE,
-          FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0);
-        if (LocalHandle == INVALID_HANDLE_VALUE)
+        if (CopyParam->PreserveTime && CopyParam->PreserveTimeDirs)
         {
-          SetFileTimeError = GetLastError();
-        }
-        else
-        {
-          FILETIME AcTime = DateTimeToFileTime(File->LastAccess, FTerminal->SessionData->DSTMode);
-          FILETIME WrTime = DateTimeToFileTime(File->Modification, FTerminal->SessionData->DSTMode);
-          if (!SetFileTime(LocalHandle, NULL, &AcTime, &WrTime))
+          FTerminal->LogEvent(FORMAT(L"Preserving directory timestamp [%s]",
+            (StandardTimestamp(File->Modification))));
+          int SetFileTimeError = ERROR_SUCCESS;
+          // FILE_FLAG_BACKUP_SEMANTICS is needed to "open" directory
+          HANDLE LocalHandle = CreateFile(ApiPath(DestFullName).c_str(), GENERIC_WRITE,
+            FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0);
+          if (LocalHandle == INVALID_HANDLE_VALUE)
           {
             SetFileTimeError = GetLastError();
           }
-          CloseHandle(LocalHandle);
+          else
+          {
+            FILETIME AcTime = DateTimeToFileTime(File->LastAccess, FTerminal->SessionData->DSTMode);
+            FILETIME WrTime = DateTimeToFileTime(File->Modification, FTerminal->SessionData->DSTMode);
+            if (!SetFileTime(LocalHandle, NULL, &AcTime, &WrTime))
+            {
+              SetFileTimeError = GetLastError();
+            }
+            CloseHandle(LocalHandle);
+          }
+
+          if (SetFileTimeError != ERROR_SUCCESS)
+          {
+            FTerminal->LogEvent(FORMAT(L"Preserving timestamp failed, ignoring: %s",
+              (SysErrorMessageForError(SetFileTimeError))));
+          }
         }
 
-        if (SetFileTimeError != ERROR_SUCCESS)
+        // Do not delete directory if some of its files were skip.
+        // Throw "skip file" for the directory to avoid attempt to deletion
+        // of any parent directory
+        if ((Params & cpDelete) && SinkFileParams.Skipped)
         {
-          FTerminal->LogEvent(FORMAT(L"Preserving timestamp failed, ignoring: %s",
-            (SysErrorMessageForError(SetFileTimeError))));
+          THROW_SKIP_FILE_NULL;
         }
       }
-
-      // Do not delete directory if some of its files were skip.
-      // Throw "skip file" for the directory to avoid attempt to deletion
-      // of any parent directory
-      if ((Params & cpDelete) && SinkFileParams.Skipped)
-      {
-        THROW_SKIP_FILE_NULL;
-      }
     }
     else
     {
@@ -6084,6 +6099,7 @@ void __fastcall TSFTPFileSystem::SFTPSink(const UnicodeString FileName,
 
   if (Params & cpDelete)
   {
+    DebugAssert(FLAGCLEAR(Params, cpNoRecurse));
     ChildError = true;
     // If file is directory, do not delete it recursively, because it should be
     // empty already. If not, it should not be deleted (some files were

+ 443 - 86
source/core/Terminal.cpp

@@ -6,6 +6,7 @@
 
 #include <SysUtils.hpp>
 #include <FileCtrl.hpp>
+#include <StrUtils.hpp>
 
 #include "Common.h"
 #include "PuttyTools.h"
@@ -679,6 +680,215 @@ bool TRetryOperationLoop::Retry()
   return Result;
 }
 //---------------------------------------------------------------------------
+TParallelOperation::TParallelOperation()
+{
+  FCopyParam = NULL;
+  FParams = 0;
+  FProbablyEmpty = false;
+  FClients = 0;
+  FMainOperationProgress = NULL;
+}
+//---------------------------------------------------------------------------
+void TParallelOperation::Init(
+  TStrings * AFileList, const UnicodeString & TargetDir, const TCopyParamType * CopyParam, int Params,
+  TFileOperationProgressType * MainOperationProgress)
+{
+  DebugAssert(FFileList.get() == NULL);
+  // More lists should really happen in scripting only, which does not support parallel transfers atm.
+  // But in general the code should work with more lists anyway, it just was not tested for it.
+  DebugAssert(AFileList->Count == 1);
+  FFileList.reset(AFileList);
+  FSection.reset(new TCriticalSection());
+  FTargetDir = TargetDir;
+  FCopyParam = CopyParam;
+  FParams = Params;
+  FMainOperationProgress = MainOperationProgress;
+}
+//---------------------------------------------------------------------------
+TParallelOperation::~TParallelOperation()
+{
+  WaitFor();
+}
+//---------------------------------------------------------------------------
+bool TParallelOperation::ShouldAddClient()
+{
+  bool Result;
+  // initialized already?
+  if (FSection.get() == NULL)
+  {
+    Result = false;
+  }
+  else
+  {
+    TGuard Guard(FSection.get());
+    Result = !FProbablyEmpty && (FMainOperationProgress->Cancel < csCancel);
+  }
+  return Result;
+}
+//---------------------------------------------------------------------------
+void TParallelOperation::AddClient()
+{
+  TGuard Guard(FSection.get());
+  FClients++;
+}
+//---------------------------------------------------------------------------
+void TParallelOperation::RemoveClient()
+{
+  TGuard Guard(FSection.get());
+  FClients--;
+}
+//---------------------------------------------------------------------------
+void TParallelOperation::WaitFor()
+{
+  bool Done;
+
+  do
+  {
+    {
+      TGuard Guard(FSection.get());
+      Done = (FClients == 0);
+    }
+
+    if (!Done)
+    {
+      // propagate the total progress incremented by the parallel operations
+      FMainOperationProgress->Progress();
+      Sleep(200);
+    }
+  }
+  while (!Done);
+
+  FProbablyEmpty = true;
+}
+//---------------------------------------------------------------------------
+void TParallelOperation::Done(const UnicodeString & FileName, bool Dir, bool Success)
+{
+  if (Dir)
+  {
+    TGuard Guard(FSection.get());
+
+    TDirectories::iterator DirectoryIterator = FDirectories.find(FileName);
+    if (DebugAlwaysTrue(DirectoryIterator != FDirectories.end()))
+    {
+      if (Success)
+      {
+        DebugAssert(!DirectoryIterator->second.Exists);
+        DirectoryIterator->second.Exists = true;
+      }
+      else
+      {
+        // This is actually not useful at the moment, as when creating directory fails and "Skip" is pressed,
+        // the current code in CopyToRemote/CreateDirectory will behave as, if it succedded, so Successs will be true here.
+        FDirectories.erase(DirectoryIterator);
+        if (FFileList->Count > 0)
+        {
+          UnicodeString FileNameWithSlash = IncludeTrailingBackslash(FileName);
+
+          // It can actually be a different list than the one the directory was taken from,
+          // but that does not maatter that much. It should not happen anyway, as more lists should be in scripting only.
+          TStrings * Files = DebugNotNull(dynamic_cast<TStrings *>(FFileList->Objects[0]));
+          for (int Index = 0; Index < Files->Count; Index++)
+          {
+            if (StartsText(Files->Strings[Index], FileNameWithSlash))
+            {
+              // We should add the file to "skip" counters in the OperationProgress,
+              // but an interactive foreground transfer is not doing that either yet.
+              FFileList->Delete(Index);
+            }
+          }
+        }
+      }
+    }
+  }
+}
+//---------------------------------------------------------------------------
+int TParallelOperation::GetNext(TTerminal * Terminal, UnicodeString & FileName, UnicodeString & TargetDir, bool & Dir)
+{
+  int Result = 1;
+  TStrings * Files;
+  do
+  {
+    if (FFileList->Count > 0)
+    {
+      Files = DebugNotNull(dynamic_cast<TStrings *>(FFileList->Objects[0]));
+      // can happen if the file was excluded by file mask
+      if (Files->Count == 0)
+      {
+        FFileList->Delete(0);
+        Files = NULL;
+      }
+    }
+    else
+    {
+      Files = NULL;
+      Result = -1;
+    }
+  }
+  while ((Result == 1) && (Files == NULL));
+
+  if (Files != NULL)
+  {
+    UnicodeString RootPath = FFileList->Strings[0];
+
+    FileName = Files->Strings[0];
+    Dir = (FileName[FileName.Length()] == L'\\');
+    if (Dir)
+    {
+      FileName.SetLength(FileName.Length() - 1);
+    }
+    UnicodeString DirPath = ExtractFileDir(FileName);
+
+    bool FirstLevel = SamePaths(DirPath, RootPath);
+    if (FirstLevel)
+    {
+      TargetDir = FTargetDir;
+    }
+    else
+    {
+      TDirectories::const_iterator DirectoryIterator = FDirectories.find(DirPath);
+      if (DebugAlwaysFalse(DirectoryIterator == FDirectories.end()))
+      {
+        throw EInvalidOperation(L"Parent path not known");
+      }
+      const TDirectoryData & DirectoryData = DirectoryIterator->second;
+      if (!DirectoryData.Exists)
+      {
+        Result = 0; // wait for parent directory to be created
+      }
+      else
+      {
+        TargetDir = DirectoryData.RemotePath;
+      }
+    }
+
+    if (!TargetDir.IsEmpty())
+    {
+      if (Dir)
+      {
+        TDirectoryData DirectoryData;
+
+        UnicodeString OnlyFileName = ExtractFileName(FileName);
+        OnlyFileName = Terminal->ChangeFileName(FCopyParam, OnlyFileName, osRemote, FirstLevel);
+        DirectoryData.RemotePath = UnixCombinePaths(TargetDir, OnlyFileName);
+
+        DirectoryData.Exists = false;
+
+        FDirectories.insert(std::make_pair(FileName, DirectoryData));
+      }
+
+      Files->Delete(0);
+      if (Files->Count == 0)
+      {
+        FFileList->Delete(0);
+      }
+    }
+  }
+
+  FProbablyEmpty = (FFileList->Count == 0);
+
+  return Result;
+}
+//---------------------------------------------------------------------------
 //---------------------------------------------------------------------------
 __fastcall TTerminal::TTerminal(TSessionData * SessionData,
   TConfiguration * Configuration)
@@ -2335,7 +2545,17 @@ TBatchOverwrite __fastcall TTerminal::EffectiveBatchOverwrite(
 bool __fastcall TTerminal::CheckRemoteFile(
   const UnicodeString & FileName, const TCopyParamType * CopyParam, int Params, TFileOperationProgressType * OperationProgress)
 {
-  return (EffectiveBatchOverwrite(FileName, CopyParam, Params, OperationProgress, true) != boAll);
+  bool Result;
+  OperationProgress->LockUserSelections();
+  try
+  {
+    Result = (EffectiveBatchOverwrite(FileName, CopyParam, Params, OperationProgress, true) != boAll);
+  }
+  __finally
+  {
+    OperationProgress->UnlockUserSelections();
+  }
+  return Result;
 }
 //---------------------------------------------------------------------------
 unsigned int __fastcall TTerminal::ConfirmFileOverwrite(
@@ -2345,85 +2565,103 @@ unsigned int __fastcall TTerminal::ConfirmFileOverwrite(
   UnicodeString Message)
 {
   unsigned int Result;
-  // duplicated in TSFTPFileSystem::SFTPConfirmOverwrite
+  TBatchOverwrite BatchOverwrite;
   bool CanAlternateResume =
     (FileParams != NULL) &&
     (FileParams->DestSize < FileParams->SourceSize) &&
     !OperationProgress->AsciiTransfer;
-  TBatchOverwrite BatchOverwrite = EffectiveBatchOverwrite(SourceFullFileName, CopyParam, Params, OperationProgress, true);
-  bool Applicable = true;
-  switch (BatchOverwrite)
-  {
-    case boOlder:
-      Applicable = (FileParams != NULL);
-      break;
-
-    case boAlternateResume:
-      Applicable = CanAlternateResume;
-      break;
 
-    case boResume:
-      Applicable = CanAlternateResume;
-      break;
-  }
-
-  if (!Applicable)
-  {
-    TBatchOverwrite ABatchOverwrite = EffectiveBatchOverwrite(SourceFullFileName, CopyParam, Params, OperationProgress, false);
-    DebugAssert(BatchOverwrite != ABatchOverwrite);
-    BatchOverwrite = ABatchOverwrite;
-  }
-
-  if (BatchOverwrite == boNo)
+  OperationProgress->LockUserSelections();
+  try
   {
-    if (Message.IsEmpty())
-    {
-      // Side refers to destination side here
-      Message = FMTLOAD((Side == osLocal ? LOCAL_FILE_OVERWRITE2 :
-        REMOTE_FILE_OVERWRITE2), (TargetFileName, TargetFileName));
-    }
-    if (FileParams != NULL)
-    {
-      Message = FMTLOAD(FILE_OVERWRITE_DETAILS, (Message,
-        FormatSize(FileParams->SourceSize),
-        UserModificationStr(FileParams->SourceTimestamp, FileParams->SourcePrecision),
-        FormatSize(FileParams->DestSize),
-        UserModificationStr(FileParams->DestTimestamp, FileParams->DestPrecision)));
-    }
-    if (DebugAlwaysTrue(QueryParams->HelpKeyword.IsEmpty()))
-    {
-      QueryParams->HelpKeyword = HELP_OVERWRITE;
-    }
-    Result = QueryUser(Message, NULL, Answers, QueryParams);
-    switch (Result)
+    // duplicated in TSFTPFileSystem::SFTPConfirmOverwrite
+    BatchOverwrite = EffectiveBatchOverwrite(SourceFullFileName, CopyParam, Params, OperationProgress, true);
+    bool Applicable = true;
+    switch (BatchOverwrite)
     {
-      case qaNeverAskAgain:
-        Configuration->ConfirmOverwriting = false;
-        Result = qaYes;
+      case boOlder:
+        Applicable = (FileParams != NULL);
         break;
 
-      case qaYesToAll:
-        BatchOverwrite = boAll;
+      case boAlternateResume:
+        Applicable = CanAlternateResume;
         break;
 
-      case qaAll:
-        BatchOverwrite = boOlder;
+      case boResume:
+        Applicable = CanAlternateResume;
         break;
+    }
 
-      case qaNoToAll:
-        BatchOverwrite = boNone;
-        break;
+    if (!Applicable)
+    {
+      TBatchOverwrite ABatchOverwrite = EffectiveBatchOverwrite(SourceFullFileName, CopyParam, Params, OperationProgress, false);
+      DebugAssert(BatchOverwrite != ABatchOverwrite);
+      BatchOverwrite = ABatchOverwrite;
     }
 
-    // we user has not selected another batch overwrite mode,
-    // keep the current one. note that we may get here even
-    // when batch overwrite was selected already, but it could not be applied
-    // to current transfer (see condition above)
-    if (BatchOverwrite != boNo)
+    if (BatchOverwrite == boNo)
     {
-      OperationProgress->SetBatchOverwrite(BatchOverwrite);
+      // particularly with parallel transfers, the overal operation can be already cancelled by other parallel operation
+      if (OperationProgress->Cancel > csContinue)
+      {
+        Result = qaCancel;
+      }
+      else
+      {
+        if (Message.IsEmpty())
+        {
+          // Side refers to destination side here
+          Message = FMTLOAD((Side == osLocal ? LOCAL_FILE_OVERWRITE2 :
+            REMOTE_FILE_OVERWRITE2), (TargetFileName, TargetFileName));
+        }
+        if (FileParams != NULL)
+        {
+          Message = FMTLOAD(FILE_OVERWRITE_DETAILS, (Message,
+            FormatSize(FileParams->SourceSize),
+            UserModificationStr(FileParams->SourceTimestamp, FileParams->SourcePrecision),
+            FormatSize(FileParams->DestSize),
+            UserModificationStr(FileParams->DestTimestamp, FileParams->DestPrecision)));
+        }
+        if (DebugAlwaysTrue(QueryParams->HelpKeyword.IsEmpty()))
+        {
+          QueryParams->HelpKeyword = HELP_OVERWRITE;
+        }
+        Result = QueryUser(Message, NULL, Answers, QueryParams);
+        switch (Result)
+        {
+          case qaNeverAskAgain:
+            Configuration->ConfirmOverwriting = false;
+            Result = qaYes;
+            break;
+
+          case qaYesToAll:
+            BatchOverwrite = boAll;
+            break;
+
+          case qaAll:
+            BatchOverwrite = boOlder;
+            break;
+
+          case qaNoToAll:
+            BatchOverwrite = boNone;
+            break;
+        }
+
+        // we user has not selected another batch overwrite mode,
+        // keep the current one. note that we may get here even
+        // when batch overwrite was selected already, but it could not be applied
+        // to current transfer (see condition above)
+        if (BatchOverwrite != boNo)
+        {
+          OperationProgress->SetBatchOverwrite(BatchOverwrite);
+        }
+      }
     }
   }
+  __finally
+  {
+    OperationProgress->UnlockUserSelections();
+  }
 
   if (BatchOverwrite != boNo)
   {
@@ -4289,28 +4527,36 @@ bool __fastcall TTerminal::DoCreateLocalFile(const UnicodeString FileName,
       {
         if (FLAGSET(FileAttr, faReadOnly))
         {
-          if (OperationProgress->BatchOverwrite == boNone)
-          {
-            Result = false;
-          }
-          else if ((OperationProgress->BatchOverwrite != boAll) && !NoConfirmation)
+          OperationProgress->LockUserSelections();
+          try
           {
-            unsigned int Answer;
-
+            if (OperationProgress->BatchOverwrite == boNone)
             {
-              TSuspendFileOperationProgress Suspend(OperationProgress);
-              Answer = QueryUser(
-                MainInstructions(FMTLOAD(READ_ONLY_OVERWRITE, (FileName))), NULL,
-                qaYes | qaNo | qaCancel | qaYesToAll | qaNoToAll, 0);
+              Result = false;
             }
+            else if ((OperationProgress->BatchOverwrite != boAll) && !NoConfirmation)
+            {
+              unsigned int Answer;
+
+              {
+                TSuspendFileOperationProgress Suspend(OperationProgress);
+                Answer = QueryUser(
+                  MainInstructions(FMTLOAD(READ_ONLY_OVERWRITE, (FileName))), NULL,
+                  qaYes | qaNo | qaCancel | qaYesToAll | qaNoToAll, 0);
+              }
 
-            switch (Answer) {
-              case qaYesToAll: OperationProgress->SetBatchOverwrite(boAll); break;
-              case qaCancel: OperationProgress->SetCancel(csCancel); // continue on next case
-              case qaNoToAll: OperationProgress->SetBatchOverwrite(boNone);
-              case qaNo: Result = false; break;
+              switch (Answer) {
+                case qaYesToAll: OperationProgress->SetBatchOverwrite(boAll); break;
+                case qaCancel: OperationProgress->SetCancel(csCancel); // continue on next case
+                case qaNoToAll: OperationProgress->SetBatchOverwrite(boNone);
+                case qaNo: Result = false; break;
+              }
             }
           }
+          __finally
+          {
+            OperationProgress->UnlockUserSelections();
+          }
         }
         else
         {
@@ -4569,6 +4815,17 @@ void __fastcall TTerminal::CalculateLocalFileSize(const UnicodeString FileName,
 
       if (AllowTransfer)
       {
+        if (AParams->Files != NULL)
+        {
+          UnicodeString FullFileName = ::ExpandFileName(FileName);
+          if (Dir)
+          {
+            FullFileName += L"\\";
+          }
+
+          AParams->Files->Add(FullFileName);
+        }
+
         if (!Dir)
         {
           AParams->Size += Size;
@@ -4587,7 +4844,7 @@ void __fastcall TTerminal::CalculateLocalFileSize(const UnicodeString FileName,
 }
 //---------------------------------------------------------------------------
 bool __fastcall TTerminal::CalculateLocalFilesSize(TStrings * FileList,
-  __int64 & Size, const TCopyParamType * CopyParam, bool AllowDirs)
+  __int64 & Size, const TCopyParamType * CopyParam, bool AllowDirs, TStrings * Files)
 {
   bool Result = false;
   TFileOperationProgressType OperationProgress(&DoProgress, &DoFinished);
@@ -4599,23 +4856,39 @@ bool __fastcall TTerminal::CalculateLocalFilesSize(TStrings * FileList,
     Params.Size = 0;
     Params.Params = 0;
     Params.CopyParam = CopyParam;
+    Params.Files = NULL;
     Params.Result = true;
 
     DebugAssert(!FOperationProgress);
     FOperationProgress = &OperationProgress;
+    UnicodeString LastDirPath;
     for (int Index = 0; Params.Result && (Index < FileList->Count); Index++)
     {
       UnicodeString FileName = FileList->Strings[Index];
       TSearchRec Rec;
       if (FileSearchRec(FileName, Rec))
       {
-        if (FLAGSET(Rec.Attr, faDirectory) && !AllowDirs)
+        bool Dir = FLAGSET(Rec.Attr, faDirectory);
+        if (Dir && !AllowDirs)
         {
           Params.Result = false;
         }
         else
         {
+          if (Files != NULL)
+          {
+            UnicodeString FullFileName = ::ExpandFileName(FileName);
+            UnicodeString DirPath = ExtractFilePath(FullFileName);
+            if (DirPath != LastDirPath)
+            {
+              Params.Files = new TStringList();
+              LastDirPath = DirPath;
+              Files->AddObject(LastDirPath, Params.Files);
+            }
+          }
+
           CalculateLocalFileSize(FileName, Rec, &Params);
+
           OperationProgress.Finish(FileName, true, OnceDoneOperation);
         }
       }
@@ -5335,7 +5608,7 @@ void __fastcall TTerminal::SynchronizeApply(TSynchronizeChecklist * Checklist,
           }
 
           if ((UploadList->Count > 0) &&
-              !CopyToRemote(UploadList, Data.RemoteDirectory, &SyncCopyParam, CopyParams))
+              !CopyToRemote(UploadList, Data.RemoteDirectory, &SyncCopyParam, CopyParams, NULL))
           {
             Abort();
           }
@@ -5674,8 +5947,46 @@ bool __fastcall TTerminal::GetStoredCredentialsTried()
   return Result;
 }
 //---------------------------------------------------------------------------
+int __fastcall TTerminal::CopyToRemoteParallel(TParallelOperation * ParallelOperation, TFileOperationProgressType * OperationProgress)
+{
+  UnicodeString FileName;
+  UnicodeString TargetDir;
+  bool Dir;
+
+  int Result = ParallelOperation->GetNext(this, FileName, TargetDir, Dir);
+  if (Result > 0)
+  {
+    std::unique_ptr<TStrings> FilesToCopy(new TStringList());
+    FilesToCopy->Add(FileName);
+
+    UnicodeString UnlockedTargetDir = TranslateLockedPath(TargetDir, false);
+    // OnceDoneOperation is not supported
+    TOnceDoneOperation OnceDoneOperation = odoIdle;
+
+    int Params = ParallelOperation->Params;
+    if (Dir)
+    {
+      Params = Params | cpNoRecurse;
+    }
+
+    int Prev = OperationProgress->FilesFinishedSuccessfully;
+    try
+    {
+      FFileSystem->CopyToRemote(
+        FilesToCopy.get(), UnlockedTargetDir, ParallelOperation->CopyParam, Params, OperationProgress, OnceDoneOperation);
+    }
+    __finally
+    {
+      bool Success = (Prev < OperationProgress->FilesFinishedSuccessfully);
+      ParallelOperation->Done(FileName, Dir, Success);
+    }
+  }
+
+  return Result;
+}
+//---------------------------------------------------------------------------
 bool __fastcall TTerminal::CopyToRemote(TStrings * FilesToCopy,
-  const UnicodeString TargetDir, const TCopyParamType * CopyParam, int Params)
+  const UnicodeString TargetDir, const TCopyParamType * CopyParam, int Params, TParallelOperation * ParallelOperation)
 {
   DebugAssert(FFileSystem);
   DebugAssert(FilesToCopy);
@@ -5687,12 +5998,24 @@ bool __fastcall TTerminal::CopyToRemote(TStrings * FilesToCopy,
   try
   {
     __int64 Size;
+    std::unique_ptr<TStringList> Files;
+    if ((ParallelOperation != NULL) &&
+        FFileSystem->IsCapable(fsParallelTransfers) &&
+        // parallel upload is not implemented for operations needed to be done on a folder
+        // after all its files are processed
+        FLAGCLEAR(Params, cpDelete) &&
+        !CopyParam->ClearArchive &&
+        (!CopyParam->PreserveTime || !CopyParam->PreserveTimeDirs))
+    {
+      Files.reset(new TStringList());
+      Files->OwnsObjects = true;
+    }
     // dirty trick: when moving, do not pass copy param to avoid exclude mask
     bool CalculatedSize =
       CalculateLocalFilesSize(
         FilesToCopy, Size,
         (FLAGCLEAR(Params, cpDelete) ? CopyParam : NULL),
-        CopyParam->CalculateSize);
+        CopyParam->CalculateSize, Files.get());
 
     TFileOperationProgressType OperationProgress(&DoProgress, &DoFinished);
     OperationProgress.Start((Params & cpDelete ? foMove : foCopy), osLocal,
@@ -5720,15 +6043,49 @@ bool __fastcall TTerminal::CopyToRemote(TStrings * FilesToCopy,
       BeginTransaction();
       try
       {
+        bool Parallel = CalculatedSize && (Files.get() != NULL);
         if (Log->Logging)
         {
           LogEvent(FORMAT(L"Copying %d files/directories to remote directory "
-            "\"%s\"", (FilesToCopy->Count, TargetDir)));
+            "\"%s\"%s", (FilesToCopy->Count, TargetDir, (Parallel ? L" in parallel" : L""))));
           LogEvent(CopyParam->LogStr);
         }
 
-        FFileSystem->CopyToRemote(FilesToCopy, UnlockedTargetDir,
-          CopyParam, Params, &OperationProgress, OnceDoneOperation);
+        if (Parallel)
+        {
+          // OnceDoneOperation is not supported
+          ParallelOperation->Init(Files.release(), UnlockedTargetDir, CopyParam, Params, &OperationProgress);
+
+          try
+          {
+            bool Continue = true;
+            do
+            {
+              int GotNext = CopyToRemoteParallel(ParallelOperation, &OperationProgress);
+              if (GotNext < 0)
+              {
+                Continue = false;
+              }
+              else if (GotNext == 0)
+              {
+                Sleep(100);
+              }
+            }
+            while (Continue);
+
+            Result = true;
+          }
+          __finally
+          {
+            OperationProgress.SetDone();
+            ParallelOperation->WaitFor();
+          }
+        }
+        else
+        {
+          FFileSystem->CopyToRemote(FilesToCopy, UnlockedTargetDir,
+            CopyParam, Params, &OperationProgress, OnceDoneOperation);
+        }
       }
       __finally
       {

+ 47 - 2
source/core/Terminal.h

@@ -28,6 +28,7 @@ struct TSpaceAvailable;
 struct TFilesFindParams;
 class TTunnelUI;
 class TCallbackGuard;
+class TParallelOperation;
 //---------------------------------------------------------------------------
 typedef void __fastcall (__closure *TQueryUserEvent)
   (TObject * Sender, const UnicodeString Query, TStrings * MoreMessages, unsigned int Answers,
@@ -107,6 +108,7 @@ const int cpTemporary = 0x04;
 const int cpNoConfirmation = 0x08;
 const int cpAppend = 0x20;
 const int cpResume = 0x40;
+const int cpNoRecurse = 0x80;
 //---------------------------------------------------------------------------
 const int ccApplyToDirectories = 0x01;
 const int ccRecursive = 0x02;
@@ -297,7 +299,7 @@ protected:
   void __fastcall CalculateLocalFileSize(const UnicodeString FileName,
     const TSearchRec Rec, /*__int64*/ void * Size);
   bool __fastcall CalculateLocalFilesSize(TStrings * FileList, __int64 & Size,
-    const TCopyParamType * CopyParam, bool AllowDirs);
+    const TCopyParamType * CopyParam, bool AllowDirs, TStrings * Files);
   TBatchOverwrite __fastcall EffectiveBatchOverwrite(
     const UnicodeString & SourceFullFileName, const TCopyParamType * CopyParam, int Params,
     TFileOperationProgressType * OperationProgress, bool Special);
@@ -427,7 +429,8 @@ public:
   bool __fastcall CopyToLocal(TStrings * FilesToCopy,
     const UnicodeString TargetDir, const TCopyParamType * CopyParam, int Params);
   bool __fastcall CopyToRemote(TStrings * FilesToCopy,
-    const UnicodeString TargetDir, const TCopyParamType * CopyParam, int Params);
+    const UnicodeString TargetDir, const TCopyParamType * CopyParam, int Params, TParallelOperation * ParallelOperation);
+  int __fastcall CopyToRemoteParallel(TParallelOperation * ParallelOperation, TFileOperationProgressType * OperationProgress);
   void __fastcall CreateDirectory(const UnicodeString DirName,
     const TRemoteProperties * Properties = NULL);
   void __fastcall CreateLink(const UnicodeString FileName, const UnicodeString PointTo, bool Symbolic);
@@ -621,6 +624,7 @@ struct TCalculateSizeParams
   const TCopyParamType * CopyParam;
   TCalculateSizeStats * Stats;
   bool AllowDirs;
+  TStrings * Files;
   bool Result;
 };
 //---------------------------------------------------------------------------
@@ -751,4 +755,45 @@ private:
   TDateTime FStart;
 };
 //---------------------------------------------------------------------------
+class TParallelOperation
+{
+public:
+  TParallelOperation();
+  ~TParallelOperation();
+
+  void Init(
+    TStrings * AFiles, const UnicodeString & TargetDir, const TCopyParamType * CopyParam, int Params,
+    TFileOperationProgressType * MainOperationProgress);
+
+  void WaitFor();
+  bool ShouldAddClient();
+  void AddClient();
+  void RemoveClient();
+  int GetNext(TTerminal * Terminal, UnicodeString & FileName, UnicodeString & TargetDir, bool & Dir);
+  void Done(const UnicodeString & FileName, bool Dir, bool Success);
+
+  __property const TCopyParamType * CopyParam = { read = FCopyParam };
+  __property int Params = { read = FParams };
+  __property UnicodeString TargetDir = { read = FTargetDir };
+  __property TFileOperationProgressType * MainOperationProgress = { read = FMainOperationProgress };
+
+private:
+  struct TDirectoryData
+  {
+    UnicodeString RemotePath;
+    bool Exists;
+  };
+
+  std::unique_ptr<TStrings> FFileList;
+  typedef std::map<UnicodeString, TDirectoryData> TDirectories;
+  TDirectories FDirectories;
+  UnicodeString FTargetDir;
+  const TCopyParamType * FCopyParam;
+  int FParams;
+  bool FProbablyEmpty;
+  int FClients;
+  std::unique_ptr<TCriticalSection> FSection;
+  TFileOperationProgressType * FMainOperationProgress;
+};
+//---------------------------------------------------------------------------
 #endif

+ 71 - 63
source/core/WebDAVFileSystem.cpp

@@ -679,6 +679,7 @@ bool __fastcall TWebDAVFileSystem::IsCapable(int Capability) const
     // instead of trying to open it as directory
     case fcResolveSymlink:
     case fsSkipTransfer:
+    case fsParallelTransfers:
       return true;
 
     case fcUserGroupListing:
@@ -1642,70 +1643,73 @@ void __fastcall TWebDAVFileSystem::DirectorySource(const UnicodeString Directory
 
   OperationProgress->SetFile(DirectoryName);
 
-  int FindAttrs = faReadOnly | faHidden | faSysFile | faDirectory | faArchive;
-  TSearchRecChecked SearchRec;
-  bool FindOK;
-
-  FILE_OPERATION_LOOP_BEGIN
+  if (FLAGCLEAR(Params, cpNoRecurse))
   {
-    FindOK =
-      (FindFirstChecked(DirectoryName + L"*.*", FindAttrs, SearchRec) == 0);
-  }
-  FILE_OPERATION_LOOP_END(FMTLOAD(LIST_DIR_ERROR, (DirectoryName)));
+    int FindAttrs = faReadOnly | faHidden | faSysFile | faDirectory | faArchive;
+    TSearchRecChecked SearchRec;
+    bool FindOK;
 
-  try
-  {
-    while (FindOK && !OperationProgress->Cancel)
+    FILE_OPERATION_LOOP_BEGIN
     {
-      UnicodeString FileName = DirectoryName + SearchRec.Name;
-      try
+      FindOK =
+        (FindFirstChecked(DirectoryName + L"*.*", FindAttrs, SearchRec) == 0);
+    }
+    FILE_OPERATION_LOOP_END(FMTLOAD(LIST_DIR_ERROR, (DirectoryName)));
+
+    try
+    {
+      while (FindOK && !OperationProgress->Cancel)
       {
-        if ((SearchRec.Name != L".") && (SearchRec.Name != L".."))
+        UnicodeString FileName = DirectoryName + SearchRec.Name;
+        try
         {
-          SourceRobust(FileName, DestFullName, CopyParam, Params, OperationProgress,
-            Flags & ~(tfFirstLevel));
+          if ((SearchRec.Name != L".") && (SearchRec.Name != L".."))
+          {
+            SourceRobust(FileName, DestFullName, CopyParam, Params, OperationProgress,
+              Flags & ~(tfFirstLevel));
+          }
         }
-      }
-      catch (EScpSkipFile & E)
-      {
-        // If ESkipFile occurs, just log it and continue with next file
-        TSuspendFileOperationProgress Suspend(OperationProgress);
-        // here a message to user was displayed, which was not appropriate
-        // when user refused to overwrite the file in subdirectory.
-        // hopefully it won't be missing in other situations.
-        if (!FTerminal->HandleException(&E))
+        catch (EScpSkipFile & E)
         {
-          throw;
+          // If ESkipFile occurs, just log it and continue with next file
+          TSuspendFileOperationProgress Suspend(OperationProgress);
+          // here a message to user was displayed, which was not appropriate
+          // when user refused to overwrite the file in subdirectory.
+          // hopefully it won't be missing in other situations.
+          if (!FTerminal->HandleException(&E))
+          {
+            throw;
+          }
         }
-      }
 
-      FILE_OPERATION_LOOP_BEGIN
-      {
-        FindOK = (FindNextChecked(SearchRec) == 0);
+        FILE_OPERATION_LOOP_BEGIN
+        {
+          FindOK = (FindNextChecked(SearchRec) == 0);
+        }
+        FILE_OPERATION_LOOP_END(FMTLOAD(LIST_DIR_ERROR, (DirectoryName)));
       }
-      FILE_OPERATION_LOOP_END(FMTLOAD(LIST_DIR_ERROR, (DirectoryName)));
     }
-  }
-  __finally
-  {
-    FindClose(SearchRec);
-  }
-
-  // TODO : Delete also read-only directories.
-  // TODO : Show error message on failure.
-  if (!OperationProgress->Cancel)
-  {
-    if (FLAGSET(Params, cpDelete))
+    __finally
     {
-      RemoveDir(ApiPath(DirectoryName));
+      FindClose(SearchRec);
     }
-    else if (CopyParam->ClearArchive && FLAGSET(Attrs, faArchive))
+
+    // TODO : Delete also read-only directories.
+    // TODO : Show error message on failure.
+    if (!OperationProgress->Cancel)
     {
-      FILE_OPERATION_LOOP_BEGIN
+      if (FLAGSET(Params, cpDelete))
+      {
+        RemoveDir(ApiPath(DirectoryName));
+      }
+      else if (CopyParam->ClearArchive && FLAGSET(Attrs, faArchive))
       {
-        THROWOSIFFALSE(FileSetAttr(ApiPath(DirectoryName), Attrs & ~faArchive) == 0);
+        FILE_OPERATION_LOOP_BEGIN
+        {
+          THROWOSIFFALSE(FileSetAttr(ApiPath(DirectoryName), Attrs & ~faArchive) == 0);
+        }
+        FILE_OPERATION_LOOP_END(FMTLOAD(CANT_SET_ATTRS, (DirectoryName)));
       }
-      FILE_OPERATION_LOOP_END(FMTLOAD(CANT_SET_ATTRS, (DirectoryName)));
     }
   }
 }
@@ -2116,22 +2120,25 @@ void __fastcall TWebDAVFileSystem::Sink(const UnicodeString FileName,
       }
       FILE_OPERATION_LOOP_END(FMTLOAD(CREATE_DIR_ERROR, (DestFullName)));
 
-      TSinkFileParams SinkFileParams;
-      SinkFileParams.TargetDir = IncludeTrailingBackslash(DestFullName);
-      SinkFileParams.CopyParam = CopyParam;
-      SinkFileParams.Params = Params;
-      SinkFileParams.OperationProgress = OperationProgress;
-      SinkFileParams.Skipped = false;
-      SinkFileParams.Flags = Flags & ~tfFirstLevel;
-
-      FTerminal->ProcessDirectory(FileName, SinkFile, &SinkFileParams);
-
-      // Do not delete directory if some of its files were skip.
-      // Throw "skip file" for the directory to avoid attempt to deletion
-      // of any parent directory
-      if (FLAGSET(Params, cpDelete) && SinkFileParams.Skipped)
+      if (FLAGCLEAR(Params, cpNoRecurse))
       {
-        THROW_SKIP_FILE_NULL;
+        TSinkFileParams SinkFileParams;
+        SinkFileParams.TargetDir = IncludeTrailingBackslash(DestFullName);
+        SinkFileParams.CopyParam = CopyParam;
+        SinkFileParams.Params = Params;
+        SinkFileParams.OperationProgress = OperationProgress;
+        SinkFileParams.Skipped = false;
+        SinkFileParams.Flags = Flags & ~tfFirstLevel;
+
+        FTerminal->ProcessDirectory(FileName, SinkFile, &SinkFileParams);
+
+        // Do not delete directory if some of its files were skip.
+        // Throw "skip file" for the directory to avoid attempt to deletion
+        // of any parent directory
+        if (FLAGSET(Params, cpDelete) && SinkFileParams.Skipped)
+        {
+          THROW_SKIP_FILE_NULL;
+        }
       }
     }
     else
@@ -2260,6 +2267,7 @@ void __fastcall TWebDAVFileSystem::Sink(const UnicodeString FileName,
 
   if (FLAGSET(Params, cpDelete))
   {
+    DebugAssert(FLAGCLEAR(Params, cpNoRecurse));
     ChildError = true;
     // If file is directory, do not delete it recursively, because it should be
     // empty already. If not, it should not be deleted (some files were

+ 0 - 6
source/forms/Copy.cpp

@@ -268,7 +268,6 @@ void __fastcall TCopyDialog::SetParams(const TGUICopyParamType & value)
   FCopyParams = value;
   DirectoryEdit->Text = Directory + FParams.FileMask;
   QueueCheck2->Checked = FParams.Queue;
-  QueueIndividuallyCheck->Checked = FParams.QueueIndividually;
   UpdateControls();
 }
 //---------------------------------------------------------------------------
@@ -278,7 +277,6 @@ TGUICopyParamType __fastcall TCopyDialog::GetParams()
   FParams = FCopyParams;
   FParams.FileMask = GetFileMask();
   FParams.Queue = QueueCheck2->Checked;
-  FParams.QueueIndividually = QueueIndividuallyCheck->Checked;
   return FParams;
 }
 //---------------------------------------------------------------------------
@@ -337,10 +335,6 @@ void __fastcall TCopyDialog::UpdateControls()
   bool RemoteTransfer = FLAGSET(FOutputOptions, cooRemoteTransfer);
   EnableControl(QueueCheck2,
     ((FOptions & (coDisableQueue | coTemp)) == 0) && !RemoteTransfer);
-  QueueIndividuallyCheck->Visible =
-    FLAGCLEAR(FOptions, coNoQueueIndividually) &&
-    QueueCheck2->Enabled && QueueCheck2->Checked &&
-    (FFileList != NULL) && (FFileList->Count > 1);
 
   TransferSettingsButton->Style =
     FLAGCLEAR(FOptions, coDoNotUsePresets) ?

+ 0 - 9
source/forms/Copy.dfm

@@ -96,15 +96,6 @@ object CopyDialog: TCopyDialog
     TabOrder = 4
     OnClick = ControlChange
   end
-  object QueueIndividuallyCheck: TCheckBox
-    Left = 312
-    Top = 112
-    Width = 193
-    Height = 17
-    Caption = '&Transfer each file individually'
-    TabOrder = 5
-    OnClick = ControlChange
-  end
   object HelpButton: TButton
     Left = 427
     Top = 136

+ 0 - 1
source/forms/Copy.h

@@ -25,7 +25,6 @@ __published:
   TButton *CancelButton;
   TButton *LocalDirectoryBrowseButton;
   TCheckBox *QueueCheck2;
-  TCheckBox *QueueIndividuallyCheck;
   TButton *HelpButton;
   TCheckBox *NeverShowAgainCheck;
   TButton *TransferSettingsButton;

+ 7 - 26
source/forms/CustomScpExplorer.cpp

@@ -1109,26 +1109,7 @@ bool __fastcall TCustomScpExplorerForm::CopyParamDialog(
     Params |=
       (CopyParam.QueueNoConfirmation ? cpNoConfirmation : 0);
 
-    if (CopyParam.QueueIndividually)
-    {
-      for (int Index = 0; Index < FileList->Count; Index++)
-      {
-        TStrings * FileList1 = new TStringList();
-        try
-        {
-          FileList1->AddObject(FileList->Strings[Index], FileList->Objects[Index]);
-          AddQueueItem(Queue, Direction, FileList1, TargetDirectory, CopyParam, Params);
-        }
-        __finally
-        {
-          delete FileList1;
-        }
-      }
-    }
-    else
-    {
-      AddQueueItem(Queue, Direction, FileList, TargetDirectory, CopyParam, Params);
-    }
+    AddQueueItem(Queue, Direction, FileList, TargetDirectory, CopyParam, Params);
     Result = false;
 
     ClearTransferSourceSelection(Direction);
@@ -1155,7 +1136,7 @@ void __fastcall TCustomScpExplorerForm::ClearTransferSourceSelection(TTransferDi
 //---------------------------------------------------------------------------
 void __fastcall TCustomScpExplorerForm::AddQueueItem(
   TTerminalQueue * Queue, TTransferDirection Direction, TStrings * FileList,
-  const UnicodeString TargetDirectory, const TCopyParamType & CopyParam,
+  const UnicodeString TargetDirectory, const TGUICopyParamType & CopyParam,
   int Params)
 {
   DebugAssert(Queue != NULL);
@@ -1179,7 +1160,7 @@ void __fastcall TCustomScpExplorerForm::AddQueueItem(
   if (Direction == tdToRemote)
   {
     QueueItem = new TUploadQueueItem(Terminal, FileList, TargetDirectory,
-      &CopyParam, Params, SingleFile);
+      &CopyParam, Params, SingleFile, CopyParam.QueueParallel);
   }
   else
   {
@@ -1996,7 +1977,7 @@ void __fastcall TCustomScpExplorerForm::CustomCommand(TStrings * FileList,
                   std::unique_ptr<TStrings> TemporaryFilesList(new TStringList());
                   TemporaryFilesList->Add(FileName);
 
-                  FTerminal->CopyToRemote(TemporaryFilesList.get(), RemoteDir, &CopyParam, cpTemporary);
+                  FTerminal->CopyToRemote(TemporaryFilesList.get(), RemoteDir, &CopyParam, cpTemporary, NULL);
                 }
               }
             }
@@ -2368,7 +2349,7 @@ bool __fastcall TCustomScpExplorerForm::ExecuteFileOperation(TFileOperation Oper
               PermanentFileList = FileList;
 
               Params |= FLAGMASK(Temp, cpTemporary);
-              Terminal->CopyToRemote(FileList, TargetDirectory, &CopyParam, Params);
+              Terminal->CopyToRemote(FileList, TargetDirectory, &CopyParam, Params, NULL);
               if (Operation == foMove)
               {
                 ReloadLocalDirectory();
@@ -3427,7 +3408,7 @@ void __fastcall TCustomScpExplorerForm::ExecutedFileChanged(const UnicodeString
 
     int Params = cpNoConfirmation | cpTemporary;
     TQueueItem * QueueItem = new TUploadQueueItem(Data->Terminal, FileList,
-      Data->RemoteDirectory, &CopyParam, Params, true);
+      Data->RemoteDirectory, &CopyParam, Params, true, false);
     QueueItem->CompleteEvent = UploadCompleteEvent;
     AddQueueItem(Data->Queue, QueueItem, Data->Terminal);
   }
@@ -3829,7 +3810,7 @@ bool __fastcall TCustomScpExplorerForm::RemoteTransferFiles(
 
           DebugAssert(!FAutoOperation);
           FAutoOperation = true;
-          FTerminal->CopyToRemote(TemporaryFilesList, Target, &CopyParam, cpTemporary);
+          FTerminal->CopyToRemote(TemporaryFilesList, Target, &CopyParam, cpTemporary, NULL);
         }
       }
       __finally

+ 1 - 1
source/forms/CustomScpExplorer.h

@@ -274,7 +274,7 @@ private:
   void __fastcall QueueSplitterDblClick(TObject * Sender);
   void __fastcall AddQueueItem(TTerminalQueue * Queue, TTransferDirection Direction,
     TStrings * FileList, const UnicodeString TargetDirectory,
-    const TCopyParamType & CopyParam, int Params);
+    const TGUICopyParamType & CopyParam, int Params);
   void __fastcall AddQueueItem(TTerminalQueue * Queue, TQueueItem * QueueItem, TTerminal * Terminal);
   void __fastcall ClearTransferSourceSelection(TTransferDirection Direction);
   void __fastcall SessionsDDDragOver(int KeyState, const TPoint & Point, int & Effect);

+ 2 - 2
source/forms/Preferences.cpp

@@ -394,7 +394,7 @@ void __fastcall TPreferencesDialog::LoadConfiguration()
     EnableQueueByDefaultCheck->Checked = WinConfiguration->EnableQueueByDefault;
     QueueAutoPopupCheck->Checked = GUIConfiguration->QueueAutoPopup;
     QueueCheck->Checked = GUIConfiguration->DefaultCopyParam.Queue;
-    QueueIndividuallyCheck->Checked = GUIConfiguration->DefaultCopyParam.QueueIndividually;
+    QueueParallelCheck->Checked = GUIConfiguration->DefaultCopyParam.QueueParallel;
     QueueNoConfirmationCheck->Checked = GUIConfiguration->DefaultCopyParam.QueueNoConfirmation;
     if (!GUIConfiguration->QueueKeepDoneItems)
     {
@@ -746,7 +746,7 @@ void __fastcall TPreferencesDialog::SaveConfiguration()
     WinConfiguration->EnableQueueByDefault = EnableQueueByDefaultCheck->Checked;
     GUIConfiguration->QueueAutoPopup = QueueAutoPopupCheck->Checked;
     CopyParam.Queue = QueueCheck->Checked;
-    CopyParam.QueueIndividually = QueueIndividuallyCheck->Checked;
+    CopyParam.QueueParallel = QueueParallelCheck->Checked;
     CopyParam.QueueNoConfirmation = QueueNoConfirmationCheck->Checked;
     GUIConfiguration->QueueKeepDoneItems = (QueueKeepDoneItemsForCombo->ItemIndex != 0);
     switch (QueueKeepDoneItemsForCombo->ItemIndex)

+ 2 - 2
source/forms/Preferences.dfm

@@ -1468,13 +1468,13 @@ object PreferencesDialog: TPreferencesDialog
             Caption = '&No confirmations for background transfers'
             TabOrder = 4
           end
-          object QueueIndividuallyCheck: TCheckBox
+          object QueueParallelCheck: TCheckBox
             Left = 16
             Top = 98
             Width = 369
             Height = 17
             Anchors = [akLeft, akTop, akRight]
-            Caption = '&Transfer each file individually on background by default'
+            Caption = '&Use multiple connections for single transfer'
             TabOrder = 3
           end
           object EnableQueueByDefaultCheck: TCheckBox

+ 1 - 1
source/forms/Preferences.h

@@ -192,7 +192,7 @@ __published:
   TRadioButton *UpdatesAutoCheck;
   TCheckBox *ConfirmTransferringCheck;
   TGroupBox *UpdatesOptionsGroup;
-  TCheckBox *QueueIndividuallyCheck;
+  TCheckBox *QueueParallelCheck;
   TCheckBox *SessionReopenAutoIdleCheck;
   TCheckBox *RenameWholeNameCheck;
   TCheckBox *TreeOnLeftCheck;

+ 4 - 4
source/windows/GUIConfiguration.cpp

@@ -60,7 +60,7 @@ void __fastcall TGUICopyParamType::GUIAssign(const TGUICopyParamType * Source)
 {
   Queue = Source->Queue;
   QueueNoConfirmation = Source->QueueNoConfirmation;
-  QueueIndividually = Source->QueueIndividually;
+  QueueParallel = Source->QueueParallel;
 }
 //---------------------------------------------------------------------------
 void __fastcall TGUICopyParamType::Default()
@@ -74,7 +74,7 @@ void __fastcall TGUICopyParamType::GUIDefault()
 {
   Queue = false;
   QueueNoConfirmation = true;
-  QueueIndividually = false;
+  QueueParallel = true;
 }
 //---------------------------------------------------------------------------
 void __fastcall TGUICopyParamType::Load(THierarchicalStorage * Storage)
@@ -83,7 +83,7 @@ void __fastcall TGUICopyParamType::Load(THierarchicalStorage * Storage)
 
   Queue = Storage->ReadBool(L"Queue", Queue);
   QueueNoConfirmation = Storage->ReadBool(L"QueueNoConfirmation", QueueNoConfirmation);
-  QueueIndividually = Storage->ReadBool(L"QueueIndividually", QueueIndividually);
+  QueueParallel = Storage->ReadBool(L"QueueParallel", QueueParallel);
 }
 //---------------------------------------------------------------------------
 void __fastcall TGUICopyParamType::Save(THierarchicalStorage * Storage)
@@ -92,7 +92,7 @@ void __fastcall TGUICopyParamType::Save(THierarchicalStorage * Storage)
 
   Storage->WriteBool(L"Queue", Queue);
   Storage->WriteBool(L"QueueNoConfirmation", QueueNoConfirmation);
-  Storage->WriteBool(L"QueueIndividually", QueueIndividually);
+  Storage->WriteBool(L"QueueParallel", QueueParallel);
 }
 //---------------------------------------------------------------------------
 TGUICopyParamType & __fastcall TGUICopyParamType::operator =(const TCopyParamType & rhp)

+ 2 - 2
source/windows/GUIConfiguration.h

@@ -39,7 +39,7 @@ public:
 
   __property bool Queue = { read = FQueue, write = FQueue };
   __property bool QueueNoConfirmation = { read = FQueueNoConfirmation, write = FQueueNoConfirmation };
-  __property bool QueueIndividually = { read = FQueueIndividually, write = FQueueIndividually };
+  __property bool QueueParallel = { read = FQueueParallel, write = FQueueParallel };
 
 protected:
   void __fastcall GUIDefault();
@@ -48,7 +48,7 @@ protected:
 private:
   bool FQueue;
   bool FQueueNoConfirmation;
-  bool FQueueIndividually;
+  bool FQueueParallel;
 };
 //---------------------------------------------------------------------------
 struct TCopyParamRuleData

+ 25 - 10
source/windows/QueueController.cpp

@@ -291,7 +291,7 @@ void __fastcall TQueueController::ExecuteOperation(TQueueOperation Operation,
 }
 //---------------------------------------------------------------------------
 void __fastcall TQueueController::FillQueueViewItem(TListItem * Item,
-  TQueueItemProxy * QueueItem, bool Detail)
+  TQueueItemProxy * QueueItem, bool Detail, bool OnlyLine)
 {
   DebugAssert(!Detail || (QueueItem->Status != TQueueItem::qsPending));
 
@@ -345,7 +345,7 @@ void __fastcall TQueueController::FillQueueViewItem(TListItem * Item,
   TFileOperationProgressType * ProgressData = QueueItem->ProgressData;
   TQueueItem::TInfo * Info = QueueItem->Info;
 
-  if (!Detail)
+  if (!Detail && Info->Primary)
   {
     switch (Info->Operation)
     {
@@ -358,6 +358,11 @@ void __fastcall TQueueController::FillQueueViewItem(TListItem * Item,
         break;
     }
 
+    if (!OnlyLine)
+    {
+      Image = -1;
+    }
+
     // cannot use ProgressData->Temp as it is set only after the transfer actually starts
     Values[0] = Info->Source.IsEmpty() ? LoadStr(PROGRESS_TEMP_DIR) : Info->Source;
     Values[1] = Info->Destination.IsEmpty() ? LoadStr(PROGRESS_TEMP_DIR) : Info->Destination;
@@ -401,7 +406,6 @@ void __fastcall TQueueController::FillQueueViewItem(TListItem * Item,
   }
   else
   {
-    Image = -1;
     if (ProgressData != NULL)
     {
       if ((Info->Side == osRemote) || !ProgressData->Temp)
@@ -491,16 +495,17 @@ void __fastcall TQueueController::UpdateQueueStatus(
       }
 
       Item = InsertItemFor(QueueItem, Index);
-      FillQueueViewItem(Item, QueueItem, false);
+      bool HasDetailsLine = UseDetailsLine(ItemIndex, QueueItem);
+      FillQueueViewItem(Item, QueueItem, false, !HasDetailsLine);
       Index++;
 
       DebugAssert((QueueItem->Status != TQueueItem::qsPending) ==
         (ItemIndex < FQueueStatus->DoneAndActiveCount));
 
-      if (UseDetailsLine(ItemIndex, QueueItem))
+      if (HasDetailsLine)
       {
         Item = InsertItemFor(QueueItem, Index);
-        FillQueueViewItem(Item, QueueItem, true);
+        FillQueueViewItem(Item, QueueItem, true, false);
         Index++;
       }
     }
@@ -523,7 +528,9 @@ bool __fastcall TQueueController::UseDetailsLine(int ItemIndex, TQueueItemProxy
   return
     (ItemIndex >= FQueueStatus->DoneCount) &&
     (ItemIndex < FQueueStatus->DoneAndActiveCount) &&
-    !QueueItem->Info->SingleFile;
+    QueueItem->Info->Primary &&
+    !QueueItem->Info->SingleFile &&
+    ((QueueItem->ProgressData == NULL) || !QueueItem->ProgressData->Done);
 }
 //---------------------------------------------------------------------------
 void __fastcall TQueueController::RefreshQueueItem(TQueueItemProxy * QueueItem)
@@ -544,15 +551,23 @@ void __fastcall TQueueController::RefreshQueueItem(TQueueItemProxy * QueueItem)
     }
   }
 
-  FillQueueViewItem(ListItem, QueueItem, false);
+  bool HasDetailsLine = UseDetailsLine(QueueItem->Index, QueueItem);
+  FillQueueViewItem(ListItem, QueueItem, false, !HasDetailsLine);
 
-  if (UseDetailsLine(QueueItem->Index, QueueItem))
+  if (HasDetailsLine)
   {
     if (NextListItem == NULL)
     {
       NextListItem = FListView->Items->Insert(Index + 1);
     }
-    FillQueueViewItem(NextListItem, QueueItem, true);
+    FillQueueViewItem(NextListItem, QueueItem, true, false);
+  }
+  else
+  {
+    if (NextListItem != NULL)
+    {
+      NextListItem->Delete();
+    }
   }
 
   DoChange();

+ 1 - 1
source/windows/QueueController.h

@@ -47,7 +47,7 @@ private:
   void __fastcall RememberConfiguration();
 
   static void __fastcall FillQueueViewItem(TListItem * Item,
-    TQueueItemProxy * QueueItem, bool Detail);
+    TQueueItemProxy * QueueItem, bool Detail, bool OnlyLine);
   TListItem * __fastcall InsertItemFor(TQueueItemProxy * QueueItem, int Index);
   bool __fastcall UseDetailsLine(int ItemIndex, TQueueItemProxy * QueueItem);
 };

+ 0 - 1
source/windows/WinInterface.h

@@ -193,7 +193,6 @@ const coDisableSaveSettings = 0x040; // not used anymore
 const coDoNotUsePresets     = 0x080;
 const coAllowRemoteTransfer = 0x100;
 const coNoQueue             = 0x200;
-const coNoQueueIndividually = 0x400;
 const coShortCutHint        = 0x800;
 const coAllFiles            = 0x1000;
 const cooDoNotShowAgain     = 0x01;

+ 1 - 1
source/windows/WinMain.cpp

@@ -105,7 +105,7 @@ void __fastcall Upload(TTerminal * Terminal, TStrings * FileList, bool UseDefaul
       DoCopyDialog(true, false, FileList, TargetDirectory, &CopyParam, Options,
         CopyParamAttrs, NULL, NULL))
   {
-    Terminal->CopyToRemote(FileList, TargetDirectory, &CopyParam, 0);
+    Terminal->CopyToRemote(FileList, TargetDirectory, &CopyParam, 0, NULL);
   }
 }
 //---------------------------------------------------------------------------