Browse Source

Bug 572: Amazon S3 protocol (upload)

https://winscp.net/tracker/572

- FILE_OPERATION_LOOP_END reimplemented using FileOperationLoopEnd
- ReadFile refactored - as a side effect, it now reports a correct file path in error message

Source commit: fc577c6da1425794ae49674de27205962735e85a
Martin Prikryl 7 years ago
parent
commit
3259d040b4

+ 3 - 1
libs/libs3/inc/libs3.h

@@ -2506,7 +2506,9 @@ void S3_list_parts(S3BucketContext *bucketContext, const char *key,
 void S3_abort_multipart_upload(S3BucketContext *bucketContext, const char *key,
                                const char *uploadId,
                                int timeoutMs,
-                               S3AbortMultipartUploadHandler *handler);
+                               S3AbortMultipartUploadHandler *handler,
+                               S3RequestContext *requestContext, // WINSCP
+                               void *callbackData); // WINSCP
 
 
 /**

+ 2 - 0
libs/libs3/inc/request.h

@@ -164,6 +164,8 @@ typedef struct Request
 
     // Parser of errors
     ErrorParser errorParser;
+
+    string_buffer(statusMessage, 1024); // WINSCP
 } Request;
 
 

+ 8 - 4
libs/libs3/src/multipart.c

@@ -67,6 +67,7 @@ static void InitialMultipartCompleteCallback
     free(mdata);
 }
 
+#ifndef WINSCP
 static void AbortMultipartUploadCompleteCallback
     (S3Status requestStatus, const S3ErrorDetails *s3ErrorDetails,
      void *callbackData)
@@ -76,6 +77,7 @@ static void AbortMultipartUploadCompleteCallback
     fprintf(stderr, "\nERROR: %s\n", S3_get_status_name(requestStatus));
 
 }
+#endif
 
 static S3Status initialMultipartXmlCallback(const char *elementPath,
                                             const char *data,
@@ -146,7 +148,9 @@ void S3_initiate_multipart(S3BucketContext *bucketContext, const char *key,
 void S3_abort_multipart_upload(S3BucketContext *bucketContext, const char *key,
                                const char *uploadId,
                                int timeoutMs,
-                               S3AbortMultipartUploadHandler *handler)
+                               S3AbortMultipartUploadHandler *handler,
+                               S3RequestContext *requestContext, // WINSCP
+                               void *callbackData) // WINSCP
 {
     char subResource[512];
     snprintf(subResource, 512, "uploadId=%s", uploadId);
@@ -175,13 +179,13 @@ void S3_abort_multipart_upload(S3BucketContext *bucketContext, const char *key,
         0,                                            // toS3Callback
         0,                                            // toS3CallbackTotalSize
         0,                                            // fromS3Callback
-        AbortMultipartUploadCompleteCallback,         // completeCallback
-        0,                                            // callbackData
+        handler->responseHandler.completeCallback,    // completeCallback WINSCP
+        callbackData,                                 // callbackData WINSCP
         timeoutMs                                     // timeoutMs
     };
 
     // Perform the request
-    request_perform(&params, 0);
+    request_perform(&params, requestContext); // WINSCP
 }
 
 

+ 9 - 4
libs/libs3/src/request.c

@@ -226,7 +226,7 @@ static int neon_read_func(void * userdata, char * buf, size_t len)
         (len, (char *) ptr, request->callbackData);
     if (ret < 0) {
         request->status = S3StatusAbortedByCallback;
-        return 0;
+        return -1;
     }
     else {
         if (ret > request->toS3CallbackBytesRemaining) {
@@ -1322,6 +1322,8 @@ static S3Status request_get(const RequestParams *params,
     request->next = 0;
     #else
     request->requestContext = context;
+
+    string_buffer_initialize(request->statusMessage);
     #endif
 
     // Request status is initialized to no error, will be updated whenever
@@ -1580,6 +1582,10 @@ void request_perform(const RequestParams *params, S3RequestContext *context)
         NeonCode code = ne_request_dispatch(request->NeonRequest);
         if ((code != NE_OK) && (request->status == S3StatusOK)) {
             request->status = request_neon_code_to_status(code);
+            const char * neonError = ne_get_error(request->NeonSession);
+            int allFit;
+            string_buffer_append(request->statusMessage, neonError, strlen(neonError), allFit);
+            request->errorParser.s3ErrorDetails.message = request->statusMessage;
         }
 
         // Finish the request, ensuring that all callbacks have been made, and
@@ -1677,9 +1683,8 @@ S3Status request_neon_code_to_status(NeonCode code)
     case NE_CONNECT:
         return S3StatusFailedToConnect;
     case NE_TIMEOUT:
-        return S3StatusErrorRequestTimeout;
-    case NE_REDIRECT:
-        return S3StatusErrorTemporaryRedirect;
+    case NE_SOCKET:
+        return S3StatusConnectionFailed;
     default:
         return S3StatusInternalError;
     }

+ 1 - 1
libs/neon/src/ne_request.c

@@ -188,7 +188,7 @@ static int aborted(ne_request *req, const char *doing, ssize_t code)
 {
     ne_session *sess = req->session;
     NE_DEBUG_WINSCP_CONTEXT(sess);
-    int ret = NE_ERROR;
+    int ret = NE_SOCKET; // WINSCP
 
     NE_DEBUG(NE_DBG_HTTP, "Aborted request (%" NE_FMT_SSIZE_T "): %s\n",
 	     code, doing);

+ 1 - 0
libs/neon/src/ne_request.h

@@ -38,6 +38,7 @@ NE_BEGIN_DECLS
 #define NE_FAILED (7) /* The precondition failed */
 #define NE_RETRY (8) /* Retry request (ne_end_request ONLY) */
 #define NE_REDIRECT (9) /* See ne_redirect.h */
+#define NE_SOCKET (10) /* Socket error - WINSCP */
 
 /* Opaque object representing a single HTTP request. */
 typedef struct ne_request_s ne_request;

+ 4 - 0
source/core/Exceptions.h

@@ -112,6 +112,10 @@ private:
     { \
       return new NAME(this, L""); \
     } \
+    virtual void __fastcall Rethrow() \
+    { \
+      throw NAME(this, L""); \
+    } \
   };
 //---------------------------------------------------------------------------
 DERIVE_EXT_EXCEPTION(ESsh, ExtException);

+ 17 - 10
source/core/FileOperationProgress.cpp

@@ -628,19 +628,26 @@ 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))
+  if (ASize >= 0)
   {
-    FTicks.push_back(Ticks);
-    FTotalTransferredThen.push_back(FTotalTransferred);
-  }
+    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)
+    if (FTicks.size() > 10)
+    {
+      FTicks.erase(FTicks.begin());
+      FTotalTransferredThen.erase(FTotalTransferredThen.begin());
+    }
+  }
+  else
   {
-    FTicks.erase(FTicks.begin());
-    FTotalTransferredThen.erase(FTotalTransferredThen.begin());
+    FTicks.clear();
   }
 
   if (FParent != NULL)

+ 1 - 0
source/core/NeonIntf.cpp

@@ -153,6 +153,7 @@ void CheckNeonStatus(ne_session * Session, int NeonStatus,
       switch (NeonStatus)
       {
         case NE_ERROR:
+        case NE_SOCKET:
           // noop
           DebugAssert(!NeonError.IsEmpty());
           Error = NeonError;

+ 394 - 20
source/core/S3FileSystem.cpp

@@ -17,6 +17,7 @@
 #include "HelpCore.h"
 #include "NeonIntf.h"
 #include <ne_request.h>
+#include <StrUtils.hpp>
 //---------------------------------------------------------------------------
 #pragma package(smart_init)
 //---------------------------------------------------------------------------
@@ -45,6 +46,8 @@ UnicodeString __fastcall S3LibDefaultRegion()
 }
 //---------------------------------------------------------------------------
 //---------------------------------------------------------------------------
+const int TS3FileSystem::S3MultiPartChunkSize = 5 * 1024 * 1024;
+//---------------------------------------------------------------------------
 TS3FileSystem::TS3FileSystem(TTerminal * ATerminal) :
   TCustomFileSystem(ATerminal),
   FActive(false)
@@ -314,13 +317,18 @@ void TS3FileSystem::RequestInit(TLibS3CallbackData & Data)
   FResponse = L"";
 }
 //---------------------------------------------------------------------------
-void TS3FileSystem::CheckLibS3Error(const TLibS3CallbackData & Data)
+void TS3FileSystem::CheckLibS3Error(const TLibS3CallbackData & Data, bool FatalOnConnectError)
 {
   if (Data.Status != S3StatusOK)
   {
     UnicodeString Error, Details;
+    bool FatalCandidate = false;
     switch (Data.Status)
     {
+      case S3StatusAbortedByCallback:
+        Error = LoadStr(USER_TERMINATED);
+        break;
+
       case S3StatusErrorAccessDenied:
         Error = LoadStr(S3_STATUS_ACCESS_DENIED);
         break;
@@ -329,6 +337,20 @@ void TS3FileSystem::CheckLibS3Error(const TLibS3CallbackData & Data)
       case S3StatusErrorInvalidAccessKeyId:
         Error = LoadStr(AUTHENTICATION_FAILED);
         break;
+
+      case S3StatusNameLookupError:
+        Error = ReplaceStr(LoadStr(NET_TRANSL_HOST_NOT_EXIST2), L"%HOST%", FTerminal->SessionData->HostNameExpanded);
+        FatalCandidate = true;
+        break;
+
+      case S3StatusFailedToConnect:
+        Error = LoadStr(CONNECTION_FAILED);
+        FatalCandidate = true;
+        break;
+
+      case S3StatusConnectionFailed:
+        FatalCandidate = true;
+        break;
     }
 
     if (!Error.IsEmpty())
@@ -351,7 +373,16 @@ void TS3FileSystem::CheckLibS3Error(const TLibS3CallbackData & Data)
       Details = Data.ErrorDetails;
     }
 
-    throw ExtException(MainInstructions(Error), Details);
+    Error = MainInstructions(Error);
+
+    if (FatalCandidate && FatalOnConnectError)
+    {
+      throw EFatal(NULL, Error, Details);
+    }
+    else
+    {
+      throw ExtException(Error, Details);
+    }
   }
 }
 //---------------------------------------------------------------------------
@@ -490,7 +521,8 @@ TLibS3BucketContext TS3FileSystem::GetBucketContext(const UnicodeString & Bucket
   return Result;
 }
 //---------------------------------------------------------------------------
-#define CreateResponseHandler() { &LibS3ResponsePropertiesCallback, &LibS3ResponseCompleteCallback }
+#define CreateResponseHandlerCustom(PropertiesCallback) { &PropertiesCallback, &LibS3ResponseCompleteCallback }
+#define CreateResponseHandler() CreateResponseHandlerCustom(LibS3ResponsePropertiesCallback)
 //---------------------------------------------------------------------------
 void __fastcall TS3FileSystem::Close()
 {
@@ -831,7 +863,8 @@ void TS3FileSystem::ReadDirectoryInternal(
     {
       // We do not get any error, when the "prefix" does not exist. But when prefix does exist, there's at least
       // prefix/ entry (translated to ..). If there's none, it means that the path does not exist.
-      if (FileList->Count == 0)
+      // When called from DoReadFile (FileName is set), leaving error handling to the caller.
+      if ((FileList->Count == 0) && FileName.IsEmpty())
       {
         throw Exception(FMTLOAD(FILE_NOT_EXISTS, (APath)));
       }
@@ -852,20 +885,31 @@ void __fastcall TS3FileSystem::ReadSymlink(TRemoteFile * /*SymlinkFile*/,
   DebugFail();
 }
 //---------------------------------------------------------------------------
-void __fastcall TS3FileSystem::ReadFile(const UnicodeString FileName,
-  TRemoteFile *& File)
+void TS3FileSystem::DoReadFile(const UnicodeString & FileName, TRemoteFile *& File)
 {
-  TOperationVisualizer Visualizer(FTerminal->UseBusyCursor);
-
   UnicodeString FileNameOnly = UnixExtractFileName(FileName);
   std::unique_ptr<TRemoteFileList> FileList(new TRemoteFileList());
   ReadDirectoryInternal(UnixExtractFileDir(FileName), FileList.get(), 1, FileNameOnly);
   TRemoteFile * AFile = FileList->FindFile(FileNameOnly);
-  if (AFile == NULL)
+  if (AFile != NULL)
+  {
+    File = AFile->Duplicate();
+  }
+  else
+  {
+    File = NULL;
+  }
+}
+//---------------------------------------------------------------------------
+void __fastcall TS3FileSystem::ReadFile(const UnicodeString FileName,
+  TRemoteFile *& File)
+{
+  TOperationVisualizer Visualizer(FTerminal->UseBusyCursor);
+  DoReadFile(FileName, File);
+  if (File == NULL)
   {
     throw Exception(FMTLOAD(FILE_NOT_EXISTS, (FileName)));
   }
-  File = AFile->Duplicate();
 }
 //---------------------------------------------------------------------------
 void __fastcall TS3FileSystem::DeleteFile(const UnicodeString AFileName,
@@ -1036,21 +1080,351 @@ void __fastcall TS3FileSystem::SpaceAvailable(const UnicodeString Path,
   DebugFail();
 }
 //---------------------------------------------------------------------------
-void __fastcall TS3FileSystem::CopyToRemote(TStrings * /*FilesToCopy*/,
-  const UnicodeString ATargetDir, const TCopyParamType * /*CopyParam*/,
-  int /*Params*/, TFileOperationProgressType * /*OperationProgress*/,
-  TOnceDoneOperation & /*OnceDoneOperation*/)
+void __fastcall TS3FileSystem::CopyToRemote(
+  TStrings * FilesToCopy, const UnicodeString TargetDir, const TCopyParamType * CopyParam,
+  int Params, TFileOperationProgressType * OperationProgress, TOnceDoneOperation & OnceDoneOperation)
 {
-  throw Exception(L"Not implemented");
+  Params &= ~cpAppend;
+
+  FTerminal->DoCopyToRemote(FilesToCopy, TargetDir, CopyParam, Params, OperationProgress, tfPreCreateDir, OnceDoneOperation);
+}
+//---------------------------------------------------------------------------
+void TS3FileSystem::ConfirmOverwrite(
+  const UnicodeString & SourceFullFileName, UnicodeString & TargetFileName,
+  TFileOperationProgressType * OperationProgress, const TOverwriteFileParams * FileParams,
+  const TCopyParamType * CopyParam, int Params)
+{
+  int Answers = qaYes | qaNo | qaCancel | qaYesToAll | qaNoToAll;
+  std::vector<TQueryButtonAlias> Aliases;
+  Aliases.push_back(TQueryButtonAlias::CreateYesToAllGrouppedWithYes());
+  Aliases.push_back(TQueryButtonAlias::CreateNoToAllGrouppedWithNo());
+
+  TQueryParams QueryParams(qpNeverAskAgainCheck);
+  QueryParams.Aliases = &Aliases[0];
+  QueryParams.AliasesCount = Aliases.size();
+
+  unsigned int Answer;
+
+  {
+    TSuspendFileOperationProgress Suspend(OperationProgress);
+    Answer =
+      FTerminal->ConfirmFileOverwrite(
+        SourceFullFileName, TargetFileName, FileParams, Answers, &QueryParams,
+        ReverseOperationSide(OperationProgress->Side),
+        CopyParam, Params, OperationProgress);
+  }
+
+  switch (Answer)
+  {
+    case qaYes:
+      // noop
+      break;
+
+    case qaNo:
+      THROW_SKIP_FILE_NULL;
+
+    default:
+      DebugFail();
+    case qaCancel:
+      OperationProgress->SetCancelAtLeast(csCancel);
+      Abort();
+      break;
+  }
+}
+//---------------------------------------------------------------------------
+struct TLibS3PutObjectDataCallbackData : TLibS3CallbackData
+{
+  UnicodeString FileName;
+  TStream * Stream;
+  TFileOperationProgressType * OperationProgress;
+  std::auto_ptr<Exception> Exception;
+  RawByteString ETag;
+};
+//---------------------------------------------------------------------------
+int TS3FileSystem::LibS3PutObjectDataCallback(int BufferSize, char * Buffer, void * CallbackData)
+{
+  TLibS3PutObjectDataCallbackData & Data = *static_cast<TLibS3PutObjectDataCallbackData *>(CallbackData);
+
+  return Data.FileSystem->PutObjectData(BufferSize, Buffer, Data);
+}
+//---------------------------------------------------------------------------
+int TS3FileSystem::PutObjectData(int BufferSize, char * Buffer, TLibS3PutObjectDataCallbackData & Data)
+{
+  int Result;
+
+  TFileOperationProgressType * OperationProgress = Data.OperationProgress;
+  if (OperationProgress->Cancel != csContinue)
+  {
+    Data.Exception.reset(new EAbort(L""));
+    Result = -1;
+  }
+  else
+  {
+    try
+    {
+      FILE_OPERATION_LOOP_BEGIN
+      {
+        Result = Data.Stream->Read(Buffer, BufferSize);
+      }
+      FILE_OPERATION_LOOP_END(FMTLOAD(READ_ERROR, (Data.FileName)));
+
+      OperationProgress->AddTransferred(Result);
+    }
+    catch (Exception & E)
+    {
+      Data.Exception.reset(CloneException(&E));
+      Result = -1;
+    }
+  }
+
+  return Result;
+}
+//---------------------------------------------------------------------------
+struct TLibS3MultipartInitialCallbackData : TLibS3CallbackData
+{
+  RawByteString UploadId;
+};
+//---------------------------------------------------------------------------
+S3Status TS3FileSystem::LibS3MultipartInitialCallback(const char * UploadId, void * CallbackData)
+{
+  TLibS3MultipartInitialCallbackData & Data = *static_cast<TLibS3MultipartInitialCallbackData *>(CallbackData);
+
+  Data.UploadId = UploadId;
+
+  return S3StatusOK;
+}
+//---------------------------------------------------------------------------
+struct TLibS3MultipartCommitPutObjectDataCallbackData : TLibS3CallbackData
+{
+  RawByteString Message;
+  int Remaining;
+};
+//---------------------------------------------------------------------------
+S3Status TS3FileSystem::LibS3MultipartResponsePropertiesCallback(
+  const S3ResponseProperties * Properties, void * CallbackData)
+{
+  S3Status Result = LibS3ResponsePropertiesCallback(Properties, CallbackData);
+
+  TLibS3PutObjectDataCallbackData & Data = *static_cast<TLibS3PutObjectDataCallbackData *>(CallbackData);
+
+  Data.ETag = Properties->eTag;
+
+  return Result;
+}
+//---------------------------------------------------------------------------
+int TS3FileSystem::LibS3MultipartCommitPutObjectDataCallback(int BufferSize, char * Buffer, void * CallbackData)
+{
+  TLibS3MultipartCommitPutObjectDataCallbackData & Data =
+    *static_cast<TLibS3MultipartCommitPutObjectDataCallbackData *>(CallbackData);
+  int Result = 0;
+  if (Data.Remaining > 0)
+  {
+    Result = std::min(BufferSize, Data.Remaining);
+    memcpy(Buffer, Data.Message.c_str() + Data.Message.Length() - Data.Remaining, Result);
+    Data.Remaining -= Result;
+  }
+  return Result;
 }
 //---------------------------------------------------------------------------
 void __fastcall TS3FileSystem::Source(
-  TLocalFileHandle & /*Handle*/, const UnicodeString & /*TargetDir*/, UnicodeString & /*DestFileName*/,
-  const TCopyParamType * /*CopyParam*/, int /*Params*/,
-  TFileOperationProgressType * /*OperationProgress*/, unsigned int /*Flags*/,
-  TUploadSessionAction & /*Action*/, bool & /*ChildError*/)
+  TLocalFileHandle & Handle, const UnicodeString & TargetDir, UnicodeString & DestFileName,
+  const TCopyParamType * CopyParam, int Params,
+  TFileOperationProgressType * OperationProgress, unsigned int /*Flags*/,
+  TUploadSessionAction & Action, bool & /*ChildError*/)
 {
-  throw Exception(L"Not implemented");
+  UnicodeString DestFullName = TargetDir + DestFileName;
+
+  TRemoteFile * RemoteFile = NULL;
+  try
+  {
+    // Should not throw on non-existing file by purpose (mainly not to get an exception while debugging)
+    DoReadFile(DestFullName, RemoteFile);
+  }
+  catch (...)
+  {
+    // Pointless, as there's no persistent connection.
+    if (!FTerminal->Active)
+    {
+      throw;
+    }
+  }
+
+  if (RemoteFile != NULL)
+  {
+    TOverwriteFileParams FileParams;
+
+    FileParams.SourceSize = Handle.Size;
+    FileParams.SourceTimestamp = Handle.Modification;
+    FileParams.DestSize = RemoteFile->Size;
+    FileParams.DestTimestamp = TDateTime();
+    FileParams.DestPrecision = mfNone;
+    delete RemoteFile;
+
+    ConfirmOverwrite(Handle.FileName, DestFileName, OperationProgress, &FileParams, CopyParam, Params);
+  }
+
+  DestFullName = TargetDir + DestFileName;
+  // only now, we know the final destination
+  // (not really true as we do not support changing file name on overwrite dialog)
+  Action.Destination(DestFullName);
+
+  UnicodeString BucketName, Key;
+  ParsePath(DestFullName, BucketName, Key);
+
+  TLibS3BucketContext BucketContext = GetBucketContext(BucketName);
+
+  int Parts = std::max(1, static_cast<int>((Handle.Size + S3MultiPartChunkSize - 1) / S3MultiPartChunkSize));
+  bool Multipart = (Parts > 1);
+
+  RawByteString MultipartUploadId;
+  TLibS3MultipartCommitPutObjectDataCallbackData MultipartCommitPutObjectDataCallbackData;
+
+  if (Multipart)
+  {
+    FTerminal->LogEvent(FORMAT(L"Initiating multipart upload (%d parts)", (Parts)));
+
+    FILE_OPERATION_LOOP_BEGIN
+    {
+      TLibS3MultipartInitialCallbackData Data;
+      RequestInit(Data);
+
+      S3MultipartInitialHandler Handler = { CreateResponseHandler(), &LibS3MultipartInitialCallback };
+
+      S3_initiate_multipart(&BucketContext, StrToS3(Key), 0, &Handler, FRequestContext, FTimeout, &Data);
+
+      CheckLibS3Error(Data, true);
+
+      MultipartUploadId = Data.UploadId;
+    }
+    FILE_OPERATION_LOOP_END_EX(FMTLOAD(TRANSFER_ERROR, (Handle.FileName)), (folAllowSkip | folRetryOnFatal));
+
+    FTerminal->LogEvent(FORMAT(L"Initiated multipart upload (%s - %d parts)", (UnicodeString(MultipartUploadId), Parts)));
+
+    MultipartCommitPutObjectDataCallbackData.Message += "<CompleteMultipartUpload>\n";
+  }
+
+  try
+  {
+    TLibS3PutObjectDataCallbackData Data;
+
+    __int64 Position = 0;
+
+    std::unique_ptr<TStream> Stream(new TSafeHandleStream((THandle)Handle.Handle));
+
+    for (int Part = 1; Part <= Parts; Part++)
+    {
+      FILE_OPERATION_LOOP_BEGIN
+      {
+        DebugAssert(Stream->Position == OperationProgress->TransferredSize);
+
+        // If not, it's chunk retry and we have to undo the unsuccessful chunk upload
+        if (Position < Stream->Position)
+        {
+          Stream->Position = Position;
+          OperationProgress->AddTransferred(Position - OperationProgress->TransferredSize);
+        }
+
+        RequestInit(Data);
+        Data.FileName = Handle.FileName;
+        Data.Stream = Stream.get();
+        Data.OperationProgress = OperationProgress;
+        Data.Exception.reset(NULL);
+
+        if (Multipart)
+        {
+          S3PutObjectHandler UploadPartHandler =
+            { CreateResponseHandlerCustom(LibS3MultipartResponsePropertiesCallback), LibS3PutObjectDataCallback };
+          int PartLength = std::min(S3MultiPartChunkSize, static_cast<int>(Stream->Size - Stream->Position));
+          FTerminal->LogEvent(FORMAT(L"Uploading part %d [%s]", (Part, IntToStr(PartLength))));
+          S3_upload_part(
+            &BucketContext, StrToS3(Key), NULL, &UploadPartHandler, Part, MultipartUploadId.c_str(),
+            PartLength, FRequestContext, FTimeout, &Data);
+        }
+        else
+        {
+          S3PutObjectHandler PutObjectHandler = { CreateResponseHandler(), LibS3PutObjectDataCallback };
+          S3_put_object(&BucketContext, StrToS3(Key), Handle.Size, NULL, FRequestContext, FTimeout, &PutObjectHandler, &Data);
+        }
+
+        // The "exception" was already seen by the user, its presence mean an accepted abort of the operation.
+        if (Data.Exception.get() == NULL)
+        {
+          CheckLibS3Error(Data, true);
+        }
+
+        Position = Stream->Position;
+
+        if (Multipart)
+        {
+          RawByteString PartCommitTag =
+            RawByteString::Format("  <Part><PartNumber>%d</PartNumber><ETag>%s</ETag></Part>\n", ARRAYOFCONST((Part, Data.ETag)));
+          MultipartCommitPutObjectDataCallbackData.Message += PartCommitTag;
+        }
+      }
+      FILE_OPERATION_LOOP_END_EX(FMTLOAD(TRANSFER_ERROR, (Handle.FileName)), (folAllowSkip | folRetryOnFatal));
+
+      if (Data.Exception.get() != NULL)
+      {
+        RethrowException(Data.Exception.get());
+      }
+    }
+
+    Stream.reset(NULL);
+
+    if (Multipart)
+    {
+      MultipartCommitPutObjectDataCallbackData.Message += "</CompleteMultipartUpload>\n";
+
+      FTerminal->LogEvent(FORMAT(L"Committing multipart upload (%s - %d parts)", (UnicodeString(MultipartUploadId), Parts)));
+      FTerminal->LogEvent(UnicodeString(MultipartCommitPutObjectDataCallbackData.Message));
+
+      FILE_OPERATION_LOOP_BEGIN
+      {
+        RequestInit(MultipartCommitPutObjectDataCallbackData);
+
+        MultipartCommitPutObjectDataCallbackData.Remaining = MultipartCommitPutObjectDataCallbackData.Message.Length();
+
+        S3MultipartCommitHandler MultipartCommitHandler =
+          { CreateResponseHandler(), &LibS3MultipartCommitPutObjectDataCallback, NULL };
+
+        S3_complete_multipart_upload(
+          &BucketContext, StrToS3(Key), &MultipartCommitHandler, MultipartUploadId.c_str(),
+          MultipartCommitPutObjectDataCallbackData.Remaining,
+          FRequestContext, FTimeout, &MultipartCommitPutObjectDataCallbackData);
+
+        CheckLibS3Error(MultipartCommitPutObjectDataCallbackData, true);
+      }
+      FILE_OPERATION_LOOP_END_EX(FMTLOAD(TRANSFER_ERROR, (Handle.FileName)), (folAllowSkip | folRetryOnFatal));
+
+      // to skip abort, in case we ever add any code before the catch, that can throw
+      MultipartUploadId = RawByteString();
+    }
+  }
+  catch (Exception & E)
+  {
+    if (!MultipartUploadId.IsEmpty())
+    {
+      FTerminal->LogEvent(FORMAT(L"Aborting multipart upload (%s - %d parts)", (UnicodeString(MultipartUploadId), Parts)));
+
+      try
+      {
+        TLibS3CallbackData Data;
+        RequestInit(Data);
+
+        S3AbortMultipartUploadHandler AbortMultipartUploadHandler = { CreateResponseHandler() };
+
+        S3_abort_multipart_upload(
+          &BucketContext, StrToS3(Key), MultipartUploadId.c_str(),
+          FTimeout, &AbortMultipartUploadHandler, FRequestContext, &Data);
+      }
+      catch (...)
+      {
+        // swallow
+      }
+    }
+
+    throw;
+  }
 }
 //---------------------------------------------------------------------------
 void __fastcall TS3FileSystem::CopyToLocal(TStrings * /*FilesToCopy*/,

+ 15 - 1
source/core/S3FileSystem.h

@@ -5,9 +5,11 @@
 #include <FileSystems.h>
 //------------------------------------------------------------------------------
 struct TNeonCertificateData;
+struct TOverwriteFileParams;
 struct TLibS3CallbackData;
 struct TLibS3BucketContext;
 struct TLibS3ListBucketCallbackData;
+struct TLibS3PutObjectDataCallbackData;
 struct ssl_st;
 #ifdef NEED_LIBS3
 // resolve clash
@@ -122,7 +124,7 @@ protected:
   void LibS3Deinitialize();
   bool VerifyCertificate(TNeonCertificateData Data);
   void CollectTLSSessionInfo();
-  void CheckLibS3Error(const TLibS3CallbackData & Data);
+  void CheckLibS3Error(const TLibS3CallbackData & Data, bool FatalOnConnectError = false);
   void InitSslSession(ssl_st * Ssl, ne_session_s * Session);
   void RequestInit(TLibS3CallbackData & Data);
   void TryOpenDirectory(const UnicodeString & Directory);
@@ -134,6 +136,12 @@ protected:
     const UnicodeString & Prefix, TRemoteFileList * FileList, int MaxKeys, const TLibS3BucketContext & BucketContext,
     TLibS3ListBucketCallbackData & Data);
   UnicodeString GetFolderKey(const UnicodeString & Key);
+  void DoReadFile(const UnicodeString & FileName, TRemoteFile *& File);
+  void ConfirmOverwrite(
+    const UnicodeString & SourceFullFileName, UnicodeString & TargetFileName,
+    TFileOperationProgressType * OperationProgress, const TOverwriteFileParams * FileParams,
+    const TCopyParamType * CopyParam, int Params);
+  int PutObjectData(int BufferSize, char * Buffer, TLibS3PutObjectDataCallbackData & Data);
 
   static TS3FileSystem * GetFileSystem(void * CallbackData);
   static void LibS3SessionCallback(ne_session_s * Session, void * CallbackData);
@@ -147,6 +155,12 @@ protected:
   static S3Status LibS3ListBucketCallback(
     int IsTruncated, const char * NextMarker, int ContentsCount, const S3ListBucketContent * Contents,
     int CommonPrefixesCount, const char ** CommonPrefixes, void * CallbackData);
+  static int LibS3PutObjectDataCallback(int BufferSize, char * Buffer, void * CallbackData);
+  static S3Status LibS3MultipartInitialCallback(const char * UploadId, void * CallbackData);
+  static int LibS3MultipartCommitPutObjectDataCallback(int BufferSize, char * Buffer, void * CallbackData);
+  static S3Status LibS3MultipartResponsePropertiesCallback(const S3ResponseProperties * Properties, void * CallbackData);
+
+  static const int S3MultiPartChunkSize;
 };
 //------------------------------------------------------------------------------
 UnicodeString __fastcall S3LibVersion();

+ 2 - 3
source/core/ScpFileSystem.cpp

@@ -1770,7 +1770,7 @@ void __fastcall TSCPFileSystem::SCPSource(const UnicodeString FileName,
         }
         FILE_OPERATION_LOOP_END_EX(
           FMTLOAD(READ_ERROR, (FileName)),
-          !OperationProgress->TransferringFile);
+          FLAGMASK(!OperationProgress->TransferringFile, folAllowSkip));
 
         OperationProgress->AddLocallyUsed(BlockBuf.Size);
 
@@ -2549,8 +2549,7 @@ void __fastcall TSCPFileSystem::SCPSink(const UnicodeString TargetDir,
                   {
                     BlockBuf.WriteToStream(FileStream, BlockBuf.Size);
                   }
-                  FILE_OPERATION_LOOP_END_EX(
-                    FMTLOAD(WRITE_ERROR, (DestFileName)), false);
+                  FILE_OPERATION_LOOP_END_EX(FMTLOAD(WRITE_ERROR, (DestFileName)), folNone);
 
                   OperationProgress->AddLocallyUsed(BlockBuf.Size);
 

+ 4 - 4
source/core/SftpFileSystem.cpp

@@ -4592,7 +4592,7 @@ void __fastcall TSFTPFileSystem::Source(
   OpenParams.Confirmed = false;
 
   FTerminal->LogEvent(L"Opening remote file.");
-  FTerminal->FileOperationLoop(SFTPOpenRemote, OperationProgress, true,
+  FTerminal->FileOperationLoop(SFTPOpenRemote, OperationProgress, folAllowSkip,
     FMTLOAD(SFTP_CREATE_FILE_ERROR, (OpenParams.RemoteFileName)),
     &OpenParams);
   OperationProgress->Progress();
@@ -4765,7 +4765,7 @@ void __fastcall TSFTPFileSystem::Source(
     FILE_OPERATION_LOOP_END_CUSTOM(
       FMTLOAD(RENAME_AFTER_RESUME_ERROR,
         (UnixExtractFileName(OpenParams.RemoteFileName), DestFileName)),
-      true, HELP_RENAME_AFTER_RESUME_ERROR);
+      folAllowSkip, HELP_RENAME_AFTER_RESUME_ERROR);
   }
 
   if (SetProperties)
@@ -4829,7 +4829,7 @@ void __fastcall TSFTPFileSystem::Source(
       }
       FILE_OPERATION_LOOP_END_CUSTOM(
         FMTLOAD(PRESERVE_TIME_PERM_ERROR3, (DestFileName)),
-        true, HELP_PRESERVE_TIME_PERM_ERROR);
+        folAllowSkip, HELP_PRESERVE_TIME_PERM_ERROR);
     }
     catch(Exception & E)
     {
@@ -5059,7 +5059,7 @@ int __fastcall TSFTPFileSystem::SFTPOpenRemote(void * AOpenParams, void * /*Para
 
         if (FTerminal->FileOperationLoopQuery(E, OperationProgress,
               FMTLOAD(SFTP_OVERWRITE_FILE_ERROR2, (OpenParams->RemoteFileName)),
-              true, LoadStr(SFTP_OVERWRITE_DELETE_BUTTON)))
+              folAllowSkip, LoadStr(SFTP_OVERWRITE_DELETE_BUTTON)))
         {
           OperationProgress->Progress();
           int Params = dfNoRecursive;

+ 38 - 5
source/core/Terminal.cpp

@@ -2180,11 +2180,12 @@ bool __fastcall TTerminal::QueryReopen(Exception * E, int Params,
 //---------------------------------------------------------------------------
 bool __fastcall TTerminal::FileOperationLoopQuery(Exception & E,
   TFileOperationProgressType * OperationProgress, const UnicodeString Message,
-  bool AllowSkip, UnicodeString SpecialRetry, UnicodeString HelpKeyword)
+  unsigned int Flags, UnicodeString SpecialRetry, UnicodeString HelpKeyword)
 {
   bool Result = false;
   Log->AddException(&E);
   unsigned int Answer;
+  bool AllowSkip = FLAGSET(Flags, folAllowSkip);
   bool SkipToAllPossible = AllowSkip && (OperationProgress != NULL);
 
   if (SkipToAllPossible && OperationProgress->SkipToAll)
@@ -2260,8 +2261,40 @@ bool __fastcall TTerminal::FileOperationLoopQuery(Exception & E,
   return Result;
 }
 //---------------------------------------------------------------------------
+void __fastcall TTerminal::FileOperationLoopEnd(Exception & E,
+  TFileOperationProgressType * OperationProgress, const UnicodeString & Message,
+  unsigned int Flags, const UnicodeString & SpecialRetry, const UnicodeString & HelpKeyword)
+{
+  if ((dynamic_cast<EAbort *>(&E) != NULL) ||
+      (dynamic_cast<EScpSkipFile *>(&E) != NULL) ||
+      (dynamic_cast<EScpSkipFile *>(&E) != NULL))
+  {
+    RethrowException(&E);
+  }
+  else if (dynamic_cast<EFatal *>(&E) != NULL)
+  {
+    if (FLAGCLEAR(Flags, folRetryOnFatal))
+    {
+      RethrowException(&E);
+    }
+    else
+    {
+      DebugAssert(SpecialRetry.IsEmpty());
+      std::unique_ptr<Exception> E2(new EFatal(&E, Message, HelpKeyword));
+      if (!DoQueryReopen(E2.get()))
+      {
+        RethrowException(E2.get());
+      }
+    }
+  }
+  else
+  {
+    FileOperationLoopQuery(E, OperationProgress, Message, Flags, SpecialRetry, HelpKeyword);
+  }
+}
+//---------------------------------------------------------------------------
 int __fastcall TTerminal::FileOperationLoop(TFileOperationEvent CallBackFunc,
-  TFileOperationProgressType * OperationProgress, bool AllowSkip,
+  TFileOperationProgressType * OperationProgress, unsigned int Flags,
   const UnicodeString Message, void * Param1, void * Param2)
 {
   DebugAssert(CallBackFunc);
@@ -2270,7 +2303,7 @@ int __fastcall TTerminal::FileOperationLoop(TFileOperationEvent CallBackFunc,
   {
     Result = CallBackFunc(Param1, Param2);
   }
-  FILE_OPERATION_LOOP_END_EX(Message, AllowSkip);
+  FILE_OPERATION_LOOP_END_EX(Message, Flags);
 
   return Result;
 }
@@ -2896,9 +2929,9 @@ unsigned int __fastcall TTerminal::ConfirmFileOverwrite(
         {
           Message = FMTLOAD(FILE_OVERWRITE_DETAILS, (Message,
             FormatSize(FileParams->SourceSize),
-            UserModificationStr(FileParams->SourceTimestamp, FileParams->SourcePrecision),
+            DefaultStr(UserModificationStr(FileParams->SourceTimestamp, FileParams->SourcePrecision), LoadStr(TIME_UNKNOWN)),
             FormatSize(FileParams->DestSize),
-            UserModificationStr(FileParams->DestTimestamp, FileParams->DestPrecision)));
+            DefaultStr(UserModificationStr(FileParams->DestTimestamp, FileParams->DestPrecision), LoadStr(TIME_UNKNOWN))));
         }
         if (DebugAlwaysTrue(QueryParams->HelpKeyword.IsEmpty()))
         {

+ 14 - 20
source/core/Terminal.h

@@ -67,6 +67,10 @@ typedef void __fastcall (__closure *TCustomCommandEvent)
 #define THROW_SKIP_FILE(EXCEPTION, MESSAGE) \
   throw EScpSkipFile(EXCEPTION, MESSAGE)
 #define THROW_SKIP_FILE_NULL THROW_SKIP_FILE(NULL, L"")
+//---------------------------------------------------------------------------
+const unsigned int folNone = 0x00;
+const unsigned int folAllowSkip = 0x01;
+const unsigned int folRetryOnFatal = 0x02;
 
 /* TODO : Better user interface (query to user) */
 #define FILE_OPERATION_LOOP_BEGIN \
@@ -77,32 +81,19 @@ typedef void __fastcall (__closure *TCustomCommandEvent)
       DoRepeat = false; \
       try \
 
-#define FILE_OPERATION_LOOP_END_CUSTOM(MESSAGE, ALLOW_SKIP, HELPKEYWORD) \
-      catch (EAbort & E) \
-      { \
-        throw; \
-      } \
-      catch (EScpSkipFile & E) \
-      { \
-        throw; \
-      } \
-      catch (EFatal & E) \
-      { \
-        throw; \
-      } \
+#define FILE_OPERATION_LOOP_END_CUSTOM(MESSAGE, FLAGS, HELPKEYWORD) \
       catch (Exception & E) \
       { \
-        FILE_OPERATION_LOOP_TERMINAL->FileOperationLoopQuery( \
-          E, OperationProgress, MESSAGE, ALLOW_SKIP, L"", HELPKEYWORD); \
+        FILE_OPERATION_LOOP_TERMINAL->FileOperationLoopEnd(E, OperationProgress, MESSAGE, FLAGS, L"", HELPKEYWORD); \
         DoRepeat = true; \
       } \
     } while (DoRepeat); \
   }
 
-#define FILE_OPERATION_LOOP_END_EX(MESSAGE, ALLOW_SKIP) \
-  FILE_OPERATION_LOOP_END_CUSTOM(MESSAGE, ALLOW_SKIP, L"")
+#define FILE_OPERATION_LOOP_END_EX(MESSAGE, FLAGS) \
+  FILE_OPERATION_LOOP_END_CUSTOM(MESSAGE, FLAGS, L"")
 #define FILE_OPERATION_LOOP_END(MESSAGE) \
-  FILE_OPERATION_LOOP_END_EX(MESSAGE, true)
+  FILE_OPERATION_LOOP_END_EX(MESSAGE, folAllowSkip)
 //---------------------------------------------------------------------------
 enum TCurrentFSProtocol { cfsUnknown, cfsSCP, cfsSFTP, cfsFTP, cfsWebDAV, cfsS3 };
 //---------------------------------------------------------------------------
@@ -282,7 +273,7 @@ protected:
   void __fastcall FileModified(const TRemoteFile * File,
     const UnicodeString FileName, bool ClearDirectoryChange = false);
   int __fastcall FileOperationLoop(TFileOperationEvent CallBackFunc,
-    TFileOperationProgressType * OperationProgress, bool AllowSkip,
+    TFileOperationProgressType * OperationProgress, unsigned int Flags,
     const UnicodeString Message, void * Param1 = NULL, void * Param2 = NULL);
   bool __fastcall GetIsCapable(TFSCapability Capability) const;
   bool __fastcall ProcessFiles(TStrings * FileList, TFileOperation Operation,
@@ -535,7 +526,10 @@ public:
     const TSearchRec Rec, void * Param);
   bool __fastcall FileOperationLoopQuery(Exception & E,
     TFileOperationProgressType * OperationProgress, const UnicodeString Message,
-    bool AllowSkip, UnicodeString SpecialRetry = L"", UnicodeString HelpKeyword = L"");
+    unsigned int Flags, UnicodeString SpecialRetry = L"", UnicodeString HelpKeyword = L"");
+  void __fastcall FileOperationLoopEnd(Exception & E,
+    TFileOperationProgressType * OperationProgress, const UnicodeString & Message,
+    unsigned int Flags, const UnicodeString & SpecialRetry, const UnicodeString & HelpKeyword);
   TUsableCopyParamAttrs __fastcall UsableCopyParamAttrs(int Params);
   bool __fastcall ContinueReopen(TDateTime Start);
   bool __fastcall QueryReopen(Exception * E, int Params,

+ 1 - 0
source/resource/TextsCore.h

@@ -472,6 +472,7 @@
 #define AUTH_CHANGING_PASSWORD  557
 #define PASTE_KEY_BUTTON        558
 #define SCRIPT_CP_DESC          559
+#define TIME_UNKNOWN            560
 
 #define CORE_VARIABLE_STRINGS   600
 #define PUTTY_BASED_ON          601

+ 1 - 0
source/resource/TextsCore1.rc

@@ -441,6 +441,7 @@ BEGIN
   AUTH_CHANGING_PASSWORD, "Changing password."
   PASTE_KEY_BUTTON, "&Paste key"
   SCRIPT_CP_DESC, "Duplicates remote file"
+  TIME_UNKNOWN, "Unknown"
 
   CORE_VARIABLE_STRINGS, "CORE_VARIABLE"
   PUTTY_BASED_ON, "SSH and SCP code based on PuTTY %s"