Skip to content

Commit 7d8ceee

Browse files
committed
first draft
1 parent 36ce9cf commit 7d8ceee

2 files changed

Lines changed: 84 additions & 13 deletions

File tree

client.go

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -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 {
166177
type 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).
207227
func 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

conn.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ type clientConn struct {
4343
conn
4444
wg sync.WaitGroup
4545

46+
wait func() error // if non-nil, call this during Wait() to get a possible remote status error.
47+
4648
sync.Mutex // protects inflight
4749
inflight map[uint32]chan<- result // outstanding requests
4850

@@ -55,6 +57,23 @@ type clientConn struct {
5557
// goroutines.
5658
func (c *clientConn) Wait() error {
5759
<-c.closed
60+
if c.wait != nil {
61+
if err := c.wait(); err != nil {
62+
63+
// TODO: when https://github.com/golang/go/issues/35025 is fixed,
64+
// we can remove this if block entirely.
65+
// Right now, it’s always going to return this, so it is not useful.
66+
// But we have this code here so that as soon as the ssh library is updated,
67+
// we can return a possibly more useful error.
68+
if err.Error() == "ssh: session not started" {
69+
return c.err
70+
}
71+
72+
// We intentionally override the c.err error here,
73+
// it will probably be io.UnexpectedEOF in this case anyways.
74+
return err
75+
}
76+
}
5877
return c.err
5978
}
6079

0 commit comments

Comments
 (0)