@@ -158,6 +158,17 @@ func UseFstat(value bool) ClientOption {
158158 }
159159}
160160
161+ // UseStderr is used to indicate that you intend to read from the standard error of the remote sftp-server command.
162+ // This does not actually get or set the standard error,
163+ // instead this simply prevents the standard error from being discarded.
164+ // You will still need to call [Client.StderrPipe] to get the reader.
165+ func UseStderr () ClientOption {
166+ return func (c * Client ) error {
167+ c .useStderr = true
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,9 @@ func UseFstat(value bool) ClientOption {
166177type Client struct {
167178 clientConn
168179
180+ stderr io.Reader
181+ useStderr bool
182+
169183 ext map [string ]string // Extensions (name -> data).
170184
171185 maxPacket int // max packet size read or written.
@@ -186,9 +200,7 @@ func NewClient(conn *ssh.Client, opts ...ClientOption) (*Client, error) {
186200 if err != nil {
187201 return nil , err
188202 }
189- if err := s .RequestSubsystem ("sftp" ); err != nil {
190- return nil , err
191- }
203+
192204 pw , err := s .StdinPipe ()
193205 if err != nil {
194206 return nil , err
@@ -197,22 +209,35 @@ func NewClient(conn *ssh.Client, opts ...ClientOption) (*Client, error) {
197209 if err != nil {
198210 return nil , err
199211 }
212+ perr , err := s .StderrPipe ()
213+ if err != nil {
214+ return nil , err
215+ }
200216
201- return NewClientPipe (pr , pw , opts ... )
217+ if err := s .RequestSubsystem ("sftp" ); err != nil {
218+ return nil , err
219+ }
220+
221+ return newClientPipe (pr , pw , perr , s .Wait , opts ... )
202222}
203223
204224// NewClientPipe creates a new SFTP client given a Reader and a WriteCloser.
205225// This can be used for connecting to an SFTP server over TCP/TLS or by using
206226// the system's ssh client program (e.g. via exec.Command).
207227func NewClientPipe (rd io.Reader , wr io.WriteCloser , opts ... ClientOption ) (* Client , error ) {
208- sftp := & Client {
228+ return newClientPipe (rd , wr , nil , nil , opts ... )
229+ }
230+
231+ func newClientPipe (rd io.Reader , wr io.WriteCloser , stderr io.Reader , wait func () error , opts ... ClientOption ) (* Client , error ) {
232+ c := & Client {
209233 clientConn : clientConn {
210234 conn : conn {
211235 Reader : rd ,
212236 WriteCloser : wr ,
213237 },
214238 inflight : make (map [uint32 ]chan <- result ),
215239 closed : make (chan struct {}),
240+ wait : wait ,
216241 },
217242
218243 ext : make (map [string ]string ),
@@ -222,32 +247,59 @@ func NewClientPipe(rd io.Reader, wr io.WriteCloser, opts ...ClientOption) (*Clie
222247 }
223248
224249 for _ , opt := range opts {
225- if err := opt (sftp ); err != nil {
250+ if err := opt (c ); err != nil {
226251 wr .Close ()
227252 return nil , err
228253 }
229254 }
230255
231- if err := sftp .sendInit (); err != nil {
256+ if stderr != nil {
257+ if ! c .useStderr {
258+ go func () {
259+ _ , err := io .Copy (io .Discard , stderr )
260+ if err != nil {
261+ debug ("error discarding stderr: %v" , err )
262+ }
263+ }()
264+
265+ } else {
266+ // Only set c.stderr when we're not discarding it.
267+ c .stderr = stderr
268+ }
269+ }
270+
271+ if err := c .sendInit (); err != nil {
232272 wr .Close ()
233273 return nil , fmt .Errorf ("error sending init packet to server: %w" , err )
234274 }
235275
236- if err := sftp .recvVersion (); err != nil {
276+ if err := c .recvVersion (); err != nil {
237277 wr .Close ()
238278 return nil , fmt .Errorf ("error receiving version packet from server: %w" , err )
239279 }
240280
241- sftp .clientConn .wg .Add (1 )
281+ c .clientConn .wg .Add (1 )
242282 go func () {
243- defer sftp .clientConn .wg .Done ()
283+ defer c .clientConn .wg .Done ()
244284
245- if err := sftp .clientConn .recv (); err != nil {
246- sftp .clientConn .broadcastErr (err )
285+ if err := c .clientConn .recv (); err != nil {
286+ c .clientConn .broadcastErr (err )
247287 }
248288 }()
249289
250- return sftp , nil
290+ return c , nil
291+ }
292+
293+ // StderrPipe returns a reader for the standard error of the remote sftp-server command.
294+ // You must have passed in the `UseStderr` client option or the standard error will already be set up to be discarded.
295+ // An error returned here does not mean that the client is no longer useable,
296+ // it only means that you won't be able to read the standard error output from the remote command.
297+ func (c * Client ) StderrPipe () (io.Reader , error ) {
298+ if c .stderr == nil {
299+ return nil , fmt .Errorf ("stderr not available" )
300+ }
301+
302+ return c .stderr , nil
251303}
252304
253305// Create creates the named file mode 0666 (before umask), truncating it if it
0 commit comments