浏览代码

With SFTP protocol files can be streamed to stdout in scripting (part of Bug 1738)

https://winscp.net/tracker/1738
(cherry picked from commit 891e603d7638ce9530b769c47a4895d26e1b5778)

Source commit: 3f802e465066617a548a7b73d3b4cab654553b69
Martin Prikryl 5 年之前
父节点
当前提交
cebb4e2a67

+ 2 - 1
dotnet/internal/ConsoleCommStruct.cs

@@ -12,6 +12,7 @@ namespace WinSCP
         public uint InputType;
         public uint OutputType;
         public bool WantsProgress; // since version 6
+        public bool UseStdErr; // since version 10
     }
 
     [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
@@ -95,7 +96,7 @@ namespace WinSCP
 
     internal class ConsoleCommStruct : IDisposable
     {
-        public const int CurrentVersion = 0x0009;
+        public const int CurrentVersion = 0x000A;
 
         public ConsoleCommStruct(Session session, SafeFileHandle fileMapping)
         {

+ 3 - 3
source/WinSCP.cbproj

@@ -49,7 +49,7 @@
 	</PropertyGroup>
 	<PropertyGroup Condition="'$(Base)'!=''">
 		<_TCHARMapping>wchar_t</_TCHARMapping>
-		<AllPackageLibs>vcl.lib;rtl.lib;vclx.lib;ws2_32.lib;secur32.lib;My.lib;DriveDir.lib;DragDropP.lib;tb2k.lib;tbxp.lib;bcbie.lib;Crypt32.lib;PngComponents.lib;xmlrtl.lib;vclactnband.lib;vclimg.lib;winhttp.lib;jcl.lib;vclie.lib;urlmon.lib;shlwapi.lib;powrprof.lib;soaprtl.lib;fmx.lib;dbrtl.lib;inet.lib</AllPackageLibs>
+		<AllPackageLibs>vcl.lib;rtl.lib;vclx.lib;ws2_32.lib;secur32.lib;My.lib;DriveDir.lib;DragDropP.lib;tb2k.lib;tbxp.lib;bcbie.lib;Crypt32.lib;PngComponents.lib;xmlrtl.lib;vclactnband.lib;vclimg.lib;winhttp.lib;jcl.lib;vclie.lib;urlmon.lib;shlwapi.lib;powrprof.lib;soaprtl.lib;fmx.lib;dbrtl.lib;inet.lib;vclFireDAC.lib;FireDAC.lib;FireDACSqliteDriver.lib;FireDACCommonDriver.lib;FireDACCommon.lib;vcldb.lib</AllPackageLibs>
 		<BCC_AllWarnings>true</BCC_AllWarnings>
 		<BCC_ExtendedErrorInfo>true</BCC_ExtendedErrorInfo>
 		<BCC_OptimizeForSpeed>true</BCC_OptimizeForSpeed>
@@ -105,8 +105,8 @@
 	</PropertyGroup>
 	<PropertyGroup Condition="'$(Cfg_1_Win32)'!=''">
 		<ILINK_DisableIncrementalLinking>true</ILINK_DisableIncrementalLinking>
-		<ILINK_LibraryPath>$(INTERM_PATH)\Win32\Debug\;$(BDS)\lib\Win32\Debug\;$(BDS)\lib\Win32\Release\;$(BDS)\lib\Win32\Release\psdk\;$(ILINK_LibraryPath)</ILINK_LibraryPath>
-		<LinkPackageStatics>vcl.lib;rtl.lib;vclx.lib;ws2_32.lib;secur32.lib;My.lib;DriveDir.lib;DragDropP.lib;tb2k.lib;tbxp.lib;bcbie.lib;Crypt32.lib;PngComponents.lib;xmlrtl.lib;vclactnband.lib;vclimg.lib;winhttp.lib;jcl.lib;vclie.lib;urlmon.lib;shlwapi.lib;powrprof.lib;soaprtl.lib;fmx.lib;dbrtl.lib;inet.lib</LinkPackageStatics>
+		<ILINK_LibraryPath>C:\Misto\bin\interm\Win32\Debug\;$(INTERM_PATH)\Win32\Debug\;$(BDS)\lib\Win32\Debug\;$(BDS)\lib\Win32\Release\;$(BDS)\lib\Win32\Release\psdk\;$(ILINK_LibraryPath)</ILINK_LibraryPath>
+		<LinkPackageStatics>vcl.lib;rtl.lib;vclx.lib;ws2_32.lib;secur32.lib;My.lib;DriveDir.lib;DragDropP.lib;tb2k.lib;tbxp.lib;bcbie.lib;Crypt32.lib;PngComponents.lib;xmlrtl.lib;vclactnband.lib;vclimg.lib;winhttp.lib;jcl.lib;vclie.lib;urlmon.lib;shlwapi.lib;powrprof.lib;soaprtl.lib;fmx.lib;dbrtl.lib;inet.lib;vclFireDAC.lib;FireDAC.lib;FireDACSqliteDriver.lib;FireDACCommonDriver.lib;FireDACCommon.lib;vcldb.lib</LinkPackageStatics>
 	</PropertyGroup>
 	<PropertyGroup Condition="'$(Cfg_1_Win64)'!=''">
 		<ILINK_LibraryPath>$(INTERM_PATH)\Win64\Debug\;$(BDS)\lib\Win64\Debug\;$(BDS)\lib\Win64\Release\;$(BDS)\lib\Win64\Release\psdk\;$(ILINK_LibraryPath)</ILINK_LibraryPath>

+ 12 - 3
source/console/Console.h

@@ -12,8 +12,8 @@ struct TConsoleCommStruct
 {
   enum TVersion
   {
-    CurrentVersion =          0x0009,
-    CurrentVersionConfirmed = 0x0109
+    CurrentVersion =          0x000A,
+    CurrentVersionConfirmed = 0x010A
   };
 
   struct TInitEvent
@@ -21,6 +21,7 @@ struct TConsoleCommStruct
     unsigned int InputType;
     unsigned int OutputType;
     bool WantsProgress; // since version 6
+    bool UseStdErr; // since version 10
   };
 
   struct TPrintEvent
@@ -69,9 +70,16 @@ struct TConsoleCommStruct
     bool Cancel; // since version 8
   };
 
+  // Since version 10
+  struct TTransferEvent
+  {
+    unsigned char Data[20480];
+    size_t Len;
+  };
+
   size_t Size;
   int Version;
-  enum { NONE, PRINT, INPUT, CHOICE, TITLE, INIT, PROGRESS } Event;
+  enum { NONE, PRINT, INPUT, CHOICE, TITLE, INIT, PROGRESS, TRANSFEROUT } Event;
 
   union
   {
@@ -81,6 +89,7 @@ struct TConsoleCommStruct
     TTitleEvent TitleEvent;
     TInitEvent InitEvent;
     TProgressEvent ProgressEvent;
+    TTransferEvent TransferEvent;
   };
 };
 //---------------------------------------------------------------------------

+ 53 - 16
source/console/Main.cpp

@@ -3,6 +3,8 @@
 #include <stdio.h>
 #include <stdlib.h>
 #include <windows.h>
+#include <io.h>
+#include <fcntl.h>
 
 #include "Console.h"
 #define MAX_ATTEMPTS 10
@@ -12,9 +14,12 @@
 using namespace std;
 HANDLE ConsoleInput = NULL;
 HANDLE ConsoleOutput = NULL;
+HANDLE ConsoleStandardOutput = NULL;
+HANDLE ConsoleErrorOutput = NULL;
 HANDLE Child = NULL;
 HANDLE CancelEvent = NULL;
 HANDLE InputTimerEvent = NULL;
+bool SupportsUtf8ConsoleOutput = false;
 unsigned int OutputType = FILE_TYPE_UNKNOWN;
 unsigned int InputType = FILE_TYPE_UNKNOWN;
 enum { RESULT_GLOBAL_ERROR = 1, RESULT_INIT_ERROR = 2, RESULT_PROCESSING_ERROR = 3,
@@ -410,6 +415,19 @@ inline void Flush()
   }
 }
 //---------------------------------------------------------------------------
+void PrintException(const exception& e)
+{
+  if (ConsoleOutput != NULL)
+  {
+    unsigned long Written;
+    WriteFile(ConsoleOutput, e.what(), strlen(e.what()), &Written, NULL);
+  }
+  else
+  {
+    puts(e.what());
+  }
+}
+//---------------------------------------------------------------------------
 void Print(const wchar_t* Message)
 {
   char* Buffer = WideStringToString(Message);
@@ -767,12 +785,36 @@ inline void ProcessTitleEvent(TConsoleCommStruct::TTitleEvent& Event)
 //---------------------------------------------------------------------------
 inline void ProcessInitEvent(TConsoleCommStruct::TInitEvent& Event)
 {
+  if (Event.UseStdErr)
+  {
+    ConsoleOutput = ConsoleErrorOutput;
+    setmode(fileno(stdout), O_BINARY);
+  }
+
+  OutputType = GetFileType(ConsoleOutput);
+  // Until now we should not have printed anything.
+  // Only in case of a fatal failure, we might have printed a pure ASCII error messages (and never got here).
+  if ((OutputType == FILE_TYPE_DISK) || (OutputType == FILE_TYPE_PIPE) ||
+      SupportsUtf8ConsoleOutput)
+  {
+    SetConsoleOutputCP(CP_UTF8);
+  }
+  else
+  {
+    SetConsoleOutputCP(CP_ACP);
+  }
+
   Event.InputType = InputType;
   Event.OutputType = OutputType;
   // default anyway
   Event.WantsProgress = false;
 }
 //---------------------------------------------------------------------------
+inline void ProcessTransferOutEvent(TConsoleCommStruct::TTransferEvent& Event)
+{
+  fwrite(Event.Data, 1, Event.Len, stdout);
+}
+//---------------------------------------------------------------------------
 void ProcessEvent(HANDLE ResponseEvent, HANDLE FileMapping)
 {
   TConsoleCommStruct* CommStruct = GetCommStruct(FileMapping);
@@ -805,6 +847,10 @@ void ProcessEvent(HANDLE ResponseEvent, HANDLE FileMapping)
         ProcessInitEvent(CommStruct->InitEvent);
         break;
 
+      case TConsoleCommStruct::TRANSFEROUT:
+        ProcessTransferOutEvent(CommStruct->TransferEvent);
+        break;
+
       default:
         throw runtime_error("Unknown event");
     }
@@ -855,11 +901,12 @@ int wmain(int /*argc*/, wchar_t* /*argv*/[])
     unsigned int SavedConsoleCP = GetConsoleCP();
     unsigned int SavedConsoleOutputCP = GetConsoleOutputCP();
 
-    ConsoleOutput = GetStdHandle(STD_OUTPUT_HANDLE);
-    OutputType = GetFileType(ConsoleOutput);
+    ConsoleStandardOutput = GetStdHandle(STD_OUTPUT_HANDLE);
+    ConsoleOutput = ConsoleStandardOutput;
+    ConsoleErrorOutput = GetStdHandle(STD_ERROR_HANDLE);
 
 
-    bool SupportsUtf8ConsoleOutput =
+    SupportsUtf8ConsoleOutput =
       ((VersionInfo.dwMajorVersion == 6) && (VersionInfo.dwMinorVersion >= 1)) ||
       (VersionInfo.dwMajorVersion > 6);
 
@@ -873,16 +920,6 @@ int wmain(int /*argc*/, wchar_t* /*argv*/[])
       SetConsoleCP(CP_ACP);
     }
 
-    if ((OutputType == FILE_TYPE_DISK) || (OutputType == FILE_TYPE_PIPE) ||
-        SupportsUtf8ConsoleOutput)
-    {
-      SetConsoleOutputCP(CP_UTF8);
-    }
-    else
-    {
-      SetConsoleOutputCP(CP_ACP);
-    }
-
 
     wchar_t InstanceName[MAX_PATH];
     HANDLE RequestEvent, ResponseEvent, FileMapping, Job;
@@ -939,7 +976,7 @@ int wmain(int /*argc*/, wchar_t* /*argv*/[])
       }
       catch(const exception& e)
       {
-        puts(e.what());
+        PrintException(e);
         Result = RESULT_PROCESSING_ERROR;
       }
 
@@ -954,7 +991,7 @@ int wmain(int /*argc*/, wchar_t* /*argv*/[])
     }
     catch(const exception& e)
     {
-      puts(e.what());
+      PrintException(e);
       Result = RESULT_INIT_ERROR;
     }
 
@@ -963,7 +1000,7 @@ int wmain(int /*argc*/, wchar_t* /*argv*/[])
   }
   catch(const exception& e)
   {
-    puts(e.what());
+    PrintException(e);
     Result = RESULT_GLOBAL_ERROR;
   }
 

+ 6 - 0
source/core/CopyParam.cpp

@@ -59,6 +59,7 @@ void __fastcall TCopyParamType::Default()
   ExcludeEmptyDirectories = false;
   Size = -1;
   OnceDoneOperation = odoIdle;
+  OnTransferOut = NULL;
 }
 //---------------------------------------------------------------------------
 UnicodeString __fastcall TCopyParamType::GetInfoStr(
@@ -575,6 +576,7 @@ void __fastcall TCopyParamType::Assign(const TCopyParamType * Source)
   COPY(ExcludeEmptyDirectories);
   COPY(Size);
   COPY(OnceDoneOperation);
+  COPY(OnTransferOut);
   #undef COPY
 }
 //---------------------------------------------------------------------------
@@ -900,6 +902,7 @@ void __fastcall TCopyParamType::Load(THierarchicalStorage * Storage)
   ExcludeEmptyDirectories = Storage->ReadBool(L"ExcludeEmptyDirectories", ExcludeEmptyDirectories);
   Size = -1;
   OnceDoneOperation = odoIdle;
+  OnTransferOut = NULL;
 }
 //---------------------------------------------------------------------------
 void __fastcall TCopyParamType::Save(THierarchicalStorage * Storage, const TCopyParamType * Defaults) const
@@ -948,6 +951,7 @@ void __fastcall TCopyParamType::Save(THierarchicalStorage * Storage, const TCopy
   WRITE_DATA(Bool, ExcludeEmptyDirectories);
   DebugAssert(Size < 0);
   DebugAssert(OnceDoneOperation == odoIdle);
+  DebugAssert(OnTransferOut == NULL);
 }
 //---------------------------------------------------------------------------
 #define C(Property) (Property == rhp.Property)
@@ -955,8 +959,10 @@ bool __fastcall TCopyParamType::operator==(const TCopyParamType & rhp) const
 {
   DebugAssert(FTransferSkipList.get() == NULL);
   DebugAssert(FTransferResumeFile.IsEmpty());
+  DebugAssert(OnTransferOut == NULL);
   DebugAssert(rhp.FTransferSkipList.get() == NULL);
   DebugAssert(rhp.FTransferResumeFile.IsEmpty());
+  DebugAssert(rhp.OnTransferOut == NULL);
   return
     C(AddXToDirectories) &&
     C(AsciiFileMask) &&

+ 3 - 0
source/core/CopyParam.h

@@ -3,6 +3,7 @@
 #define CopyParamH
 
 #include "FileMasks.h"
+#include "FileBuffer.h"
 #include "RemoteFiles.h"
 #include "Exceptions.h"
 //---------------------------------------------------------------------------
@@ -70,6 +71,7 @@ private:
   bool FExcludeEmptyDirectories;
   __int64 FSize;
   TOnceDoneOperation FOnceDoneOperation;
+  TTransferOutEvent FOnTransferOut;
   static const wchar_t TokenPrefix = L'%';
   static const wchar_t NoReplacement = wchar_t(false);
   static const wchar_t TokenReplacement = wchar_t(true);
@@ -146,6 +148,7 @@ public:
   __property bool ExcludeEmptyDirectories = { read = FExcludeEmptyDirectories, write = FExcludeEmptyDirectories };
   __property __int64 Size = { read = FSize, write = FSize };
   __property TOnceDoneOperation OnceDoneOperation = { read = FOnceDoneOperation, write = FOnceDoneOperation };
+  __property TTransferOutEvent OnTransferOut = { read = FOnTransferOut, write = FOnTransferOut };
 };
 //---------------------------------------------------------------------------
 unsigned long __fastcall GetSpeedLimit(const UnicodeString & Text);

+ 6 - 0
source/core/FileBuffer.cpp

@@ -234,6 +234,12 @@ void __fastcall TFileBuffer::WriteToStream(TStream * Stream, const DWORD Len)
   }
 }
 //---------------------------------------------------------------------------
+void __fastcall TFileBuffer::WriteToOut(TTransferOutEvent OnTransferOut, TObject * Sender, const DWORD Len)
+{
+  OnTransferOut(Sender, Data + Position, Len);
+  FMemory->Seek(Len, soCurrent);
+}
+//---------------------------------------------------------------------------
 //---------------------------------------------------------------------------
 __fastcall TSafeHandleStream::TSafeHandleStream(int AHandle) :
   THandleStream(AHandle)

+ 3 - 0
source/core/FileBuffer.h

@@ -9,6 +9,8 @@ enum TEOLType { eolLF /* \n */, eolCRLF /* \r\n */, eolCR /* \r */ };
 const int cpRemoveCtrlZ = 0x01;
 const int cpRemoveBOM =   0x02;
 //---------------------------------------------------------------------------
+typedef void __fastcall (__closure *TTransferOutEvent)(TObject * Sender, const unsigned char * Data, size_t Len);
+//---------------------------------------------------------------------------
 class TFileBuffer
 {
 public:
@@ -23,6 +25,7 @@ public:
   DWORD __fastcall LoadStream(TStream * Stream, const DWORD Len, bool ForceLen);
   DWORD __fastcall ReadStream(TStream * Stream, const DWORD Len, bool ForceLen);
   void __fastcall WriteToStream(TStream * Stream, const DWORD Len);
+  void __fastcall WriteToOut(TTransferOutEvent OnTransferOut, TObject * Sender, const DWORD Len);
   __property TMemoryStream * Memory  = { read=FMemory, write=SetMemory };
   __property char * Data = { read=GetData };
   __property int Size = { read=FSize, write=SetSize };

+ 1 - 0
source/core/FtpFileSystem.cpp

@@ -1849,6 +1849,7 @@ bool __fastcall TFTPFileSystem::IsCapable(int Capability) const
     case fcPreservingTimestampDirs:
     case fcResumeSupport:
     case fcChangePassword:
+    case fcTransferOut:
       return false;
 
     default:

+ 1 - 0
source/core/S3FileSystem.cpp

@@ -679,6 +679,7 @@ bool __fastcall TS3FileSystem::IsCapable(int Capability) const
     case fcResumeSupport:
     case fcChangePassword:
     case fcLocking:
+    case fcTransferOut:
       return false;
 
     default:

+ 1 - 0
source/core/ScpFileSystem.cpp

@@ -462,6 +462,7 @@ bool __fastcall TSCPFileSystem::IsCapable(int Capability) const
     case fcResumeSupport:
     case fcSkipTransfer:
     case fcParallelTransfers: // does not implement cpNoRecurse
+    case fcTransferOut:
       return false;
 
     case fcChangePassword:

+ 23 - 13
source/core/Script.cpp

@@ -15,6 +15,7 @@
 #pragma package(smart_init)
 //---------------------------------------------------------------------------
 const wchar_t * ToggleNames[] = { L"off", L"on" };
+const UnicodeString InOutParam(TraceInitStr(L"-"));
 //---------------------------------------------------------------------------
 __fastcall TScriptProcParams::TScriptProcParams(const UnicodeString & FullCommand, const UnicodeString & ParamsStr)
 {
@@ -306,6 +307,7 @@ __fastcall TScript::TScript(bool LimitedOutput)
   FGroups = false;
   FWantsProgress = false;
   FInteractive = false;
+  FOnTransferOut = NULL;
   FIncludeFileMaskOptionUsed = false;
   FPendingLogLines = new TStringList();
 
@@ -711,7 +713,6 @@ TStrings * __fastcall TScript::CreateFileList(TScriptProcParams * Parameters, in
 
   if (FLAGSET(ListType, fltOnlyFile))
   {
-    // For internal use by .NET assembly
     for (int Index = 0; Index < Result->Count; Index++)
     {
       TRemoteFile * File = dynamic_cast<TRemoteFile *>(Result->Objects[Index]);
@@ -1461,27 +1462,36 @@ void __fastcall TScript::GetProc(TScriptProcParams * Parameters)
 
   RequireParams(Parameters, 1);
   int LastFileParam = (Parameters->ParamCount == 1 ? 1 : Parameters->ParamCount - 1);
+  DebugAssert(CopyParam.OnTransferOut == NULL);
+  if ((OnTransferOut != NULL) && (Parameters->ParamCount > 1) && SameText(Parameters->Param[Parameters->ParamCount], InOutParam))
+  {
+    CopyParam.OnTransferOut = OnTransferOut;
+    OnlyFile = true;
+  }
   TStrings * FileList = CreateFileList(Parameters, 1, LastFileParam,
     (TFileListType)(fltQueryServer | fltMask | FLAGMASK(Latest, fltLatest) | FLAGMASK(OnlyFile, fltOnlyFile)));
   try
   {
     UnicodeString TargetDirectory;
-    if (Parameters->ParamCount == 1)
-    {
-      TargetDirectory = GetCurrentDir();
-      CopyParam.FileMask = L"";
-    }
-    else
+    if (CopyParam.OnTransferOut == NULL)
     {
-      UnicodeString Target = Parameters->Param[Parameters->ParamCount];
-      TargetDirectory = ExtractFilePath(Target);
-      if (TargetDirectory.IsEmpty())
+      if (Parameters->ParamCount == 1)
       {
         TargetDirectory = GetCurrentDir();
+        CopyParam.FileMask = L"";
+      }
+      else
+      {
+        UnicodeString Target = Parameters->Param[Parameters->ParamCount];
+        TargetDirectory = ExtractFilePath(Target);
+        if (TargetDirectory.IsEmpty())
+        {
+          TargetDirectory = GetCurrentDir();
+        }
+        CopyParam.FileMask = ExtractFileName(Target);
+        Target = IncludeTrailingBackslash(TargetDirectory) + CopyParam.FileMask;
+        CheckMultiFilesToOne(FileList, Target, false);
       }
-      CopyParam.FileMask = ExtractFileName(Target);
-      Target = IncludeTrailingBackslash(TargetDirectory) + CopyParam.FileMask;
-      CheckMultiFilesToOne(FileList, Target, false);
     }
 
     CheckParams(Parameters);

+ 2 - 0
source/core/Script.h

@@ -76,6 +76,7 @@ public:
   __property bool Groups = { read = FGroups, write = FGroups };
   __property bool WantsProgress = { read = FWantsProgress, write = FWantsProgress };
   __property bool Interactive = { read = FInteractive, write = FInteractive };
+  __property TTransferOutEvent OnTransferOut = { read = FOnTransferOut, write = FOnTransferOut };
 
 protected:
   TTerminal * FTerminal;
@@ -103,6 +104,7 @@ protected:
   bool FGroups;
   bool FWantsProgress;
   bool FInteractive;
+  TTransferOutEvent FOnTransferOut;
   TStrings * FPendingLogLines;
   bool FWarnNonDefaultCopyParam;
   bool FWarnNonDefaultSynchronizeParams;

+ 1 - 0
source/core/SessionInfo.h

@@ -46,6 +46,7 @@ enum TFSCapability { fcUserGroupListing, fcModeChanging, fcGroupChanging,
   fcSecondaryShell, fcRemoveCtrlZUpload, fcRemoveBOMUpload, fcMoveToQueue,
   fcLocking, fcPreservingTimestampDirs, fcResumeSupport,
   fcChangePassword, fcSkipTransfer, fcParallelTransfers, fcBackgroundTransfers,
+  fcTransferOut,
   fcCount };
 //---------------------------------------------------------------------------
 struct TFileSystemInfo

+ 47 - 34
source/core/SftpFileSystem.cpp

@@ -2053,6 +2053,7 @@ bool __fastcall TSFTPFileSystem::IsCapable(int Capability) const
   switch (Capability) {
     case fcAnyCommand:
     case fcShellAnyCommand:
+    case fcLocking:
       return false;
 
     case fcNewerOnlyUpload:
@@ -2063,6 +2064,7 @@ bool __fastcall TSFTPFileSystem::IsCapable(int Capability) const
     case fcRemoveCtrlZUpload:
     case fcRemoveBOMUpload:
     case fcPreservingTimestampDirs:
+    case fcTransferOut:
       return true;
 
     case fcMoveToQueue:
@@ -2155,9 +2157,6 @@ bool __fastcall TSFTPFileSystem::IsCapable(int Capability) const
         (FVersion >= 6) ||
         FSupportsHardlink;
 
-    case fcLocking:
-      return false;
-
     case fcChangePassword:
       return FSecureShell->CanChangePassword();
 
@@ -5210,14 +5209,21 @@ void __fastcall TSFTPFileSystem::DirectorySunk(
 }
 //---------------------------------------------------------------------------
 void __fastcall TSFTPFileSystem::WriteLocalFile(
-  TStream * FileStream, TFileBuffer & BlockBuf, const UnicodeString & LocalFileName,
+  const TCopyParamType * CopyParam, TStream * FileStream, TFileBuffer & BlockBuf, const UnicodeString & LocalFileName,
   TFileOperationProgressType * OperationProgress)
 {
-  FILE_OPERATION_LOOP_BEGIN
+  if (CopyParam->OnTransferOut != NULL)
+  {
+    BlockBuf.WriteToOut(CopyParam->OnTransferOut, FTerminal, BlockBuf.Size);
+  }
+  else
   {
-    BlockBuf.WriteToStream(FileStream, BlockBuf.Size);
+    FILE_OPERATION_LOOP_BEGIN
+    {
+      BlockBuf.WriteToStream(FileStream, BlockBuf.Size);
+    }
+    FILE_OPERATION_LOOP_END(FMTLOAD(WRITE_ERROR, (LocalFileName)));
   }
-  FILE_OPERATION_LOOP_END(FMTLOAD(WRITE_ERROR, (LocalFileName)));
 
   OperationProgress->AddLocallyUsed(BlockBuf.Size);
 }
@@ -5233,7 +5239,8 @@ void __fastcall TSFTPFileSystem::Sink(
     FLAGCLEAR(Params, cpTemporary) &&
     !OperationProgress->AsciiTransfer &&
     CopyParam->AllowResume(OperationProgress->TransferSize) &&
-    !FTerminal->IsEncryptingFiles();
+    !FTerminal->IsEncryptingFiles() &&
+    (CopyParam->OnTransferOut == NULL);
 
   HANDLE LocalHandle = NULL;
   TStream * FileStream = NULL;
@@ -5402,20 +5409,23 @@ void __fastcall TSFTPFileSystem::Sink(
 
     Action.Destination(ExpandUNCFileName(DestFullName));
 
-    // if not already opened (resume, append...), create new empty file
-    if (!LocalHandle)
+    if (CopyParam->OnTransferOut == NULL)
     {
-      if (!FTerminal->CreateLocalFile(LocalFileName, OperationProgress,
-             &LocalHandle, FLAGSET(Params, cpNoConfirmation)))
+      // if not already opened (resume, append...), create new empty file
+      if (!LocalHandle)
       {
-        throw ESkipFile();
+        if (!FTerminal->CreateLocalFile(LocalFileName, OperationProgress,
+               &LocalHandle, FLAGSET(Params, cpNoConfirmation)))
+        {
+          throw ESkipFile();
+        }
       }
-    }
-    DebugAssert(LocalHandle);
+      DebugAssert(LocalHandle);
 
-    DeleteLocalFile = true;
+      DeleteLocalFile = true;
 
-    FileStream = new TSafeHandleStream((THandle)LocalHandle);
+      FileStream = new TSafeHandleStream((THandle)LocalHandle);
+    }
 
     // at end of this block queue is discarded
     {
@@ -5539,7 +5549,7 @@ void __fastcall TSFTPFileSystem::Sink(
               Encryption.Decrypt(BlockBuf);
             }
 
-            WriteLocalFile(FileStream, BlockBuf, LocalFileName, OperationProgress);
+            WriteLocalFile(CopyParam, FileStream, BlockBuf, LocalFileName, OperationProgress);
           }
 
           if (OperationProgress->Cancel != csContinue)
@@ -5565,7 +5575,7 @@ void __fastcall TSFTPFileSystem::Sink(
           TFileBuffer BlockBuf;
           if (Encryption.DecryptEnd(BlockBuf))
           {
-            WriteLocalFile(FileStream, BlockBuf, LocalFileName, OperationProgress);
+            WriteLocalFile(CopyParam, FileStream, BlockBuf, LocalFileName, OperationProgress);
           }
         }
       }
@@ -5576,30 +5586,33 @@ void __fastcall TSFTPFileSystem::Sink(
       // queue is discarded here
     }
 
-    if (CopyParam->PreserveTime)
+    if (LocalHandle)
     {
-      FTerminal->UpdateTargetTime(LocalHandle, Modification, FTerminal->SessionData->DSTMode);
-    }
+      if (CopyParam->PreserveTime)
+      {
+        FTerminal->UpdateTargetTime(LocalHandle, Modification, FTerminal->SessionData->DSTMode);
+      }
 
-    CloseHandle(LocalHandle);
-    LocalHandle = NULL;
+      CloseHandle(LocalHandle);
+      LocalHandle = NULL;
 
-    if (ResumeAllowed)
-    {
-      FILE_OPERATION_LOOP_BEGIN
+      if (ResumeAllowed)
       {
-        if (FileExists(ApiPath(DestFullName)))
+        FILE_OPERATION_LOOP_BEGIN
         {
-          DeleteFileChecked(DestFullName);
+          if (FileExists(ApiPath(DestFullName)))
+          {
+            DeleteFileChecked(DestFullName);
+          }
+          THROWOSIFFALSE(Sysutils::RenameFile(ApiPath(DestPartialFullName), ApiPath(DestFullName)));
         }
-        THROWOSIFFALSE(Sysutils::RenameFile(ApiPath(DestPartialFullName), ApiPath(DestFullName)));
+        FILE_OPERATION_LOOP_END(FMTLOAD(RENAME_AFTER_RESUME_ERROR, (ExtractFileName(DestPartialFullName), DestFileName)));
       }
-      FILE_OPERATION_LOOP_END(FMTLOAD(RENAME_AFTER_RESUME_ERROR, (ExtractFileName(DestPartialFullName), DestFileName)));
-    }
 
-    DeleteLocalFile = false;
+      DeleteLocalFile = false;
 
-    FTerminal->UpdateTargetAttrs(DestFullName, File, CopyParam, Attrs);
+      FTerminal->UpdateTargetAttrs(DestFullName, File, CopyParam, Attrs);
+    }
 
   }
   __finally

+ 1 - 1
source/core/SftpFileSystem.h

@@ -196,7 +196,7 @@ protected:
   void __fastcall Progress(TFileOperationProgressType * OperationProgress);
   void AddPathString(TSFTPPacket & Packet, const UnicodeString & Value, bool EncryptNewFiles = false);
   void __fastcall WriteLocalFile(
-    TStream * FileStream, TFileBuffer & BlockBuf, const UnicodeString & LocalFileName,
+    const TCopyParamType * CopyParam, TStream * FileStream, TFileBuffer & BlockBuf, const UnicodeString & LocalFileName,
     TFileOperationProgressType * OperationProgress);
   bool __fastcall DoesFileLookLikeSymLink(TRemoteFile * File);
 };

+ 29 - 7
source/core/Terminal.cpp

@@ -7169,6 +7169,11 @@ bool __fastcall TTerminal::CopyToLocal(
   DebugAssert(FilesToCopy != NULL);
   TOnceDoneOperation OnceDoneOperation = odoIdle;
 
+  if ((CopyParam->OnTransferOut != NULL) && !FFileSystem->IsCapable(fcTransferOut))
+  {
+    throw Exception(LoadStr(NOTSUPPORTED));
+  }
+
   FDestFileName = L"";
   FMultipleDestinationFiles = false;
 
@@ -7491,7 +7496,14 @@ void __fastcall TTerminal::Sink(
   }
   else
   {
-    LogEvent(FORMAT(L"Copying \"%s\" to local directory started.", (FileName)));
+    if (CopyParam->OnTransferOut != NULL)
+    {
+      LogEvent(FORMAT(L"Streaming \"%s\" to local machine started.", (FileName)));
+    }
+    else
+    {
+      LogEvent(FORMAT(L"Copying \"%s\" to local directory started.", (FileName)));
+    }
 
     // Will we use ASCII or BINARY file transfer?
     UnicodeString BaseFileName = GetBaseFileName(FileName);
@@ -7512,20 +7524,30 @@ void __fastcall TTerminal::Sink(
     OperationProgress->SetTransferSize(TransferSize);
 
     int Attrs;
-    FILE_OPERATION_LOOP_BEGIN
+    UnicodeString LogFileName;
+    if (CopyParam->OnTransferOut != NULL)
+    {
+      Attrs = -1;
+      LogFileName = L"-";
+    }
+    else
     {
-      Attrs = FileGetAttrFix(ApiPath(DestFullName));
-      if ((Attrs >= 0) && FLAGSET(Attrs, faDirectory))
+      FILE_OPERATION_LOOP_BEGIN
       {
-        EXCEPTION;
+        Attrs = FileGetAttrFix(ApiPath(DestFullName));
+        if ((Attrs >= 0) && FLAGSET(Attrs, faDirectory))
+        {
+          EXCEPTION;
+        }
       }
+      FILE_OPERATION_LOOP_END(FMTLOAD(NOT_FILE_ERROR, (DestFullName)));
+      LogFileName = ExpandUNCFileName(DestFullName);
     }
-    FILE_OPERATION_LOOP_END(FMTLOAD(NOT_FILE_ERROR, (DestFullName)));
 
     FFileSystem->Sink(
       FileName, File, TargetDir, DestFileName, Attrs, CopyParam, Params, OperationProgress, Flags, Action);
 
-    LogFileDone(OperationProgress, ExpandUNCFileName(DestFullName));
+    LogFileDone(OperationProgress, LogFileName);
     OperationProgress->Succeeded();
   }
 }

+ 1 - 0
source/core/WebDAVFileSystem.cpp

@@ -581,6 +581,7 @@ bool __fastcall TWebDAVFileSystem::IsCapable(int Capability) const
     case fcPreservingTimestampDirs:
     case fcResumeSupport:
     case fcChangePassword:
+    case fcTransferOut:
       return false;
 
     case fcLocking:

+ 1 - 0
source/resource/TextsWin.h

@@ -300,6 +300,7 @@
 #define USAGE_USERNAME          1589
 #define USAGE_PASSWORD          1590
 #define USAGE_INTERACTIVEINPUT  1591
+#define USAGE_STDOUT            1592
 
 #define WIN_FORMS_STRINGS       1600
 #define COPY_FILE               1605

+ 1 - 0
source/resource/TextsWin1.rc

@@ -305,6 +305,7 @@ BEGIN
         USAGE_USERNAME, "An alternative way to provide a username"
         USAGE_PASSWORD, "An alternative way to provide a password"
         USAGE_INTERACTIVEINPUT, "All prompts are automatically cancelled"
+        USAGE_STDOUT, "Allows streaming files to stdout (and redirects status output to stderr)"
 
         WIN_FORMS_STRINGS, "WIN_FORMS_STRINGS"
         COPY_FILE, "%s file '%s' to %s:"

+ 65 - 3
source/windows/ConsoleRunner.cpp

@@ -60,6 +60,7 @@ public:
   virtual void __fastcall SetTitle(UnicodeString Title);
   virtual void __fastcall WaitBeforeExit();
   virtual void __fastcall Progress(TScriptProgress & Progress);
+  virtual void __fastcall TransferOut(const unsigned char * Data, size_t Len);
   virtual UnicodeString __fastcall FinalLogMessage();
 
 protected:
@@ -471,6 +472,7 @@ bool __fastcall TOwnConsole::HasFlag(TConsoleFlag Flag) const
     case cfNoInteractiveInput:
     case cfCommandLineOnly:
     case cfWantsProgress:
+    case cfStdOut:
       return false;
 
     default:
@@ -503,6 +505,11 @@ void __fastcall TOwnConsole::Progress(TScriptProgress & /*Progress*/)
   DebugFail();
 }
 //---------------------------------------------------------------------------
+void __fastcall TOwnConsole::TransferOut(const unsigned char * DebugUsedArg(Data), size_t DebugUsedArg(Len))
+{
+  DebugFail();
+}
+//---------------------------------------------------------------------------
 UnicodeString __fastcall TOwnConsole::FinalLogMessage()
 {
   return UnicodeString();
@@ -511,7 +518,7 @@ UnicodeString __fastcall TOwnConsole::FinalLogMessage()
 class TExternalConsole : public TConsole
 {
 public:
-  __fastcall TExternalConsole(const UnicodeString Instance, bool NoInteractiveInput);
+  __fastcall TExternalConsole(const UnicodeString Instance, bool NoInteractiveInput, bool StdOut);
   virtual __fastcall ~TExternalConsole();
 
   virtual void __fastcall Print(UnicodeString Str, bool FromBeginning = false, bool Error = false);
@@ -524,6 +531,7 @@ public:
   virtual void __fastcall SetTitle(UnicodeString Title);
   virtual void __fastcall WaitBeforeExit();
   virtual void __fastcall Progress(TScriptProgress & Progress);
+  virtual void __fastcall TransferOut(const unsigned char * Data, size_t Len);
   virtual UnicodeString __fastcall FinalLogMessage();
 
 private:
@@ -536,6 +544,7 @@ private:
   bool FLiveOutput;
   bool FPipeOutput;
   bool FNoInteractiveInput;
+  bool FStdOut;
   bool FWantsProgress;
   bool FInteractive;
   unsigned int FMaxSend;
@@ -548,7 +557,7 @@ private:
 };
 //---------------------------------------------------------------------------
 __fastcall TExternalConsole::TExternalConsole(
-  const UnicodeString Instance, bool NoInteractiveInput)
+  const UnicodeString Instance, bool NoInteractiveInput, bool StdOut)
 {
   UnicodeString Name;
   Name = FORMAT(L"%s%s", (CONSOLE_EVENT_REQUEST, (Instance)));
@@ -591,6 +600,7 @@ __fastcall TExternalConsole::TExternalConsole(
   SetTimer(Application->Handle, 1, 500, NULL);
 
   FNoInteractiveInput = NoInteractiveInput;
+  FStdOut = StdOut;
   FMaxSend = 0;
 
   Init();
@@ -804,6 +814,7 @@ void __fastcall TExternalConsole::Init()
   {
     CommStruct->Event = TConsoleCommStruct::INIT;
     CommStruct->InitEvent.WantsProgress = false;
+    CommStruct->InitEvent.UseStdErr = FStdOut;
   }
   __finally
   {
@@ -852,6 +863,9 @@ bool __fastcall TExternalConsole::HasFlag(TConsoleFlag Flag) const
     case cfWantsProgress:
       return FWantsProgress;
 
+    case cfStdOut:
+      return FStdOut;
+
     default:
       DebugFail();
       return false;
@@ -929,6 +943,28 @@ void __fastcall TExternalConsole::Progress(TScriptProgress & Progress)
   }
 }
 //---------------------------------------------------------------------------
+void __fastcall TExternalConsole::TransferOut(const unsigned char * Data, size_t Len)
+{
+  size_t Offset = 0;
+  while (Offset < Len)
+  {
+    TConsoleCommStruct * CommStruct = GetCommStruct();
+    try
+    {
+      CommStruct->Event = TConsoleCommStruct::TRANSFEROUT;
+      size_t BlockLen = std::min(Len - Offset, sizeof(CommStruct->TransferEvent.Data));
+      memcpy(CommStruct->TransferEvent.Data, Data + Offset, BlockLen);
+      CommStruct->TransferEvent.Len = BlockLen;
+      Offset += BlockLen;
+    }
+    __finally
+    {
+      FreeCommStruct(CommStruct);
+    }
+    SendEvent(INFINITE);
+  }
+}
+//---------------------------------------------------------------------------
 class TNullConsole : public TConsole
 {
 public:
@@ -945,6 +981,7 @@ public:
   virtual void __fastcall WaitBeforeExit();
 
   virtual void __fastcall Progress(TScriptProgress & Progress);
+  virtual void __fastcall TransferOut(const unsigned char * Data, size_t Len);
   virtual UnicodeString __fastcall FinalLogMessage();
 };
 //---------------------------------------------------------------------------
@@ -1004,6 +1041,7 @@ bool __fastcall TNullConsole::HasFlag(TConsoleFlag Flag) const
     case cfInteractive:
     case cfCommandLineOnly:
     case cfWantsProgress:
+    case cfStdOut:
       return false;
 
     default:
@@ -1024,6 +1062,11 @@ void __fastcall TNullConsole::Progress(TScriptProgress & /*Progress*/)
   DebugFail();
 }
 //---------------------------------------------------------------------------
+void __fastcall TNullConsole::TransferOut(const unsigned char * DebugUsedArg(Data), size_t DebugUsedArg(Len))
+{
+  DebugFail();
+}
+//---------------------------------------------------------------------------
 UnicodeString __fastcall TNullConsole::FinalLogMessage()
 {
   return UnicodeString();
@@ -1097,6 +1140,7 @@ private:
   UnicodeString ExpandCommand(UnicodeString Command, TStrings * ScriptParameters);
   void __fastcall Failed(bool & AnyError);
   void __fastcall ScriptProgress(TScript * Script, TScriptProgress & Progress);
+  void __fastcall ScriptTransferOut(TObject *, const unsigned char * Data, size_t Len);
   void __fastcall ConfigurationChange(TObject * Sender);
 };
 //---------------------------------------------------------------------------
@@ -1733,6 +1777,11 @@ void __fastcall TConsoleRunner::ScriptProgress(TScript * /*Script*/, TScriptProg
   FConsole->Progress(Progress);
 }
 //---------------------------------------------------------------------------
+void __fastcall TConsoleRunner::ScriptTransferOut(TObject *, const unsigned char * Data, size_t Len)
+{
+  FConsole->TransferOut(Data, Len);
+}
+//---------------------------------------------------------------------------
 void __fastcall TConsoleRunner::SynchronizeControllerLog(
   TSynchronizeController * /*Controller*/, TSynchronizeLogEntry /*Entry*/,
   const UnicodeString Message)
@@ -1980,6 +2029,10 @@ int __fastcall TConsoleRunner::Run(const UnicodeString Session, TOptions * Optio
       FScript->OnSynchronizeStartStop = ScriptSynchronizeStartStop;
       FScript->OnProgress = ScriptProgress;
       FScript->Interactive = (ScriptCommands == NULL) && FConsole->HasFlag(cfInteractive);
+      if (FConsole->HasFlag(cfStdOut))
+      {
+        FScript->OnTransferOut = ScriptTransferOut;
+      }
 
       UpdateTitle();
 
@@ -2170,6 +2223,10 @@ void __fastcall Usage(TConsole * Console)
   PrintUsageSyntax(Console,
     UnicodeString(!CommandLineOnly ? L"[/console] " : L"") +
     FORMAT(L"[/script=<file>] [/%s cmd1...] [/parameter // param1...]", (LowerCase(COMMAND_SWITCH))));
+  if (CommandLineOnly)
+  {
+    PrintUsageSyntax(Console, FORMAT(L"[/%s]", (LowerCase(STDOUT_SWITCH))));
+  }
   PrintUsageSyntax(Console,
     FORMAT(L"[/%s=<logfile> [/loglevel=<level>]] [/%s=[<count>%s]<size>]", (LowerCase(LOG_SWITCH), LowerCase(LOGSIZE_SWITCH), LOGSIZE_SEPARATOR)));
   if (!CommandLineOnly)
@@ -2225,6 +2282,10 @@ void __fastcall Usage(TConsole * Console)
   RegisterSwitch(SwitchesUsage, L"/script=", USAGE_SCRIPT);
   RegisterSwitch(SwitchesUsage, TProgramParams::FormatSwitch(COMMAND_SWITCH), USAGE_COMMAND);
   RegisterSwitch(SwitchesUsage, L"/parameter", USAGE_PARAMETER);
+  if (CommandLineOnly)
+  {
+    RegisterSwitch(SwitchesUsage, TProgramParams::FormatSwitch(STDOUT_SWITCH), USAGE_STDOUT);
+  }
   RegisterSwitch(SwitchesUsage, TProgramParams::FormatSwitch(LOG_SWITCH) + L"=", USAGE_LOG);
   RegisterSwitch(SwitchesUsage, L"/loglevel=", USAGE_LOGLEVEL);
   RegisterSwitch(SwitchesUsage, TProgramParams::FormatSwitch(LOGSIZE_SWITCH) + L"=", USAGE_LOGSIZE);
@@ -2686,7 +2747,8 @@ int __fastcall Console(TConsoleMode Mode)
     {
       Configuration->Usage->Inc(L"ConsoleExternal");
       bool NoInteractiveInput = Params->FindSwitch(NOINTERACTIVEINPUT_SWITCH);
-      Console = new TExternalConsole(ConsoleInstance, NoInteractiveInput);
+      bool StdOut = Params->FindSwitch(STDOUT_SWITCH);
+      Console = new TExternalConsole(ConsoleInstance, NoInteractiveInput, StdOut);
     }
     else if (Params->FindSwitch(L"Console") || (Mode != cmScripting))
     {

+ 4 - 1
source/windows/WinInterface.h

@@ -50,6 +50,7 @@ const int mpAllowContinueOnError = 0x02;
 #define COMREGISTRATION_SWITCH L"ComRegistration"
 #define BROWSE_SWITCH L"Browse"
 #define NOINTERACTIVEINPUT_SWITCH L"NoInteractiveInput"
+#define STDOUT_SWITCH L"StdOut"
 
 #define DUMPCALLSTACK_EVENT L"WinSCPCallstack%d"
 
@@ -621,7 +622,8 @@ enum TConsoleFlag
   cfNoInteractiveInput,
   cfInteractive,
   cfCommandLineOnly,
-  cfWantsProgress
+  cfWantsProgress,
+  cfStdOut
 };
 //---------------------------------------------------------------------------
 class TConsole
@@ -639,6 +641,7 @@ public:
   virtual void __fastcall SetTitle(UnicodeString Title) = 0;
   virtual void __fastcall WaitBeforeExit() = 0;
   virtual void __fastcall Progress(TScriptProgress & Progress) = 0;
+  virtual void __fastcall TransferOut(const unsigned char * Data, size_t Len) = 0;
   virtual UnicodeString __fastcall FinalLogMessage() = 0;
 };
 //---------------------------------------------------------------------------