@@ -158,6 +158,17 @@ func UseFstat(value bool) ClientOption {
158158 }
159159}
160160
161+ // CopyStderrTo specifies a writer to which the standard error of the remote sftp-server command should be written.
162+ //
163+ // The writer passed in will not be automatically closed.
164+ // It is the responsibility of the caller to coordinate closure of any writers.
165+ func CopyStderrTo (wr io.Writer ) ClientOption {
166+ return func (c * Client ) error {
167+ c .stderrTo = wr
168+ return nil
169+ }
170+ }
171+
161172// Client represents an SFTP session on a *ssh.ClientConn SSH connection.
162173// Multiple Clients can be active on a single SSH connection, and a Client
163174// may be called concurrently from multiple Goroutines.
@@ -166,6 +177,8 @@ func UseFstat(value bool) ClientOption {
166177type Client struct {
167178 clientConn
168179
180+ stderrTo io.Writer
181+
169182 ext map [string ]string // Extensions (name -> data).
170183
171184 maxPacket int // max packet size read or written.
@@ -186,9 +199,7 @@ func NewClient(conn *ssh.Client, opts ...ClientOption) (*Client, error) {
186199 if err != nil {
187200 return nil , err
188201 }
189- if err := s .RequestSubsystem ("sftp" ); err != nil {
190- return nil , err
191- }
202+
192203 pw , err := s .StdinPipe ()
193204 if err != nil {
194205 return nil , err
@@ -197,22 +208,35 @@ func NewClient(conn *ssh.Client, opts ...ClientOption) (*Client, error) {
197208 if err != nil {
198209 return nil , err
199210 }
211+ perr , err := s .StderrPipe ()
212+ if err != nil {
213+ return nil , err
214+ }
200215
201- return NewClientPipe (pr , pw , opts ... )
216+ if err := s .RequestSubsystem ("sftp" ); err != nil {
217+ return nil , err
218+ }
219+
220+ return newClientPipe (pr , perr , pw , s .Wait , opts ... )
202221}
203222
204223// NewClientPipe creates a new SFTP client given a Reader and a WriteCloser.
205224// This can be used for connecting to an SFTP server over TCP/TLS or by using
206225// the system's ssh client program (e.g. via exec.Command).
207226func NewClientPipe (rd io.Reader , wr io.WriteCloser , opts ... ClientOption ) (* Client , error ) {
208- sftp := & Client {
227+ return newClientPipe (rd , nil , wr , nil , opts ... )
228+ }
229+
230+ func newClientPipe (rd , stderr io.Reader , wr io.WriteCloser , wait func () error , opts ... ClientOption ) (* Client , error ) {
231+ c := & Client {
209232 clientConn : clientConn {
210233 conn : conn {
211234 Reader : rd ,
212235 WriteCloser : wr ,
213236 },
214237 inflight : make (map [uint32 ]chan <- result ),
215238 closed : make (chan struct {}),
239+ wait : wait ,
216240 },
217241
218242 ext : make (map [string ]string ),
@@ -222,32 +246,50 @@ func NewClientPipe(rd io.Reader, wr io.WriteCloser, opts ...ClientOption) (*Clie
222246 }
223247
224248 for _ , opt := range opts {
225- if err := opt (sftp ); err != nil {
249+ if err := opt (c ); err != nil {
226250 wr .Close ()
227251 return nil , err
228252 }
229253 }
230254
231- if err := sftp .sendInit (); err != nil {
255+ if stderr != nil {
256+ wr := io .Discard
257+ if c .stderrTo != nil {
258+ wr = c .stderrTo
259+ }
260+
261+ go func () {
262+ // DO NOT close the writer!
263+ // Programs may pass in `os.Stderr` to write the remote stderr to,
264+ // and the program may continue after disconnect by reconnecting.
265+ // But if we've closed their stderr, then we just messed everything up.
266+
267+ if _ , err := io .Copy (wr , stderr ); err != nil {
268+ debug ("error copying stderr: %v" , err )
269+ }
270+ }()
271+ }
272+
273+ if err := c .sendInit (); err != nil {
232274 wr .Close ()
233275 return nil , fmt .Errorf ("error sending init packet to server: %w" , err )
234276 }
235277
236- if err := sftp .recvVersion (); err != nil {
278+ if err := c .recvVersion (); err != nil {
237279 wr .Close ()
238280 return nil , fmt .Errorf ("error receiving version packet from server: %w" , err )
239281 }
240282
241- sftp .clientConn .wg .Add (1 )
283+ c .clientConn .wg .Add (1 )
242284 go func () {
243- defer sftp .clientConn .wg .Done ()
285+ defer c .clientConn .wg .Done ()
244286
245- if err := sftp .clientConn .recv (); err != nil {
246- sftp .clientConn .broadcastErr (err )
287+ if err := c .clientConn .recv (); err != nil {
288+ c .clientConn .broadcastErr (err )
247289 }
248290 }()
249291
250- return sftp , nil
292+ return c , nil
251293}
252294
253295// Create creates the named file mode 0666 (before umask), truncating it if it
0 commit comments