main pico / pkg / send / protocols / rsync / rsync.go
Antonio Mika  ·  2026-10-10
  1package rsync
  2
  3import (
  4	"errors"
  5	"fmt"
  6	"io"
  7	"io/fs"
  8	"os"
  9	"path"
 10	"strings"
 11
 12	"github.com/picosh/pico/pkg/pssh"
 13	"github.com/picosh/pico/pkg/rsync"
 14	"github.com/picosh/pico/pkg/send/utils"
 15)
 16
 17// storageFS exposes a CopyFromClientHandler as an rsync.FS. Handlers take
 18// absolute paths and differ in how they name listing results, so this
 19// normalizes both directions.
 20type storageFS struct {
 21	session *pssh.SSHServerConnSession
 22	handler utils.CopyFromClientHandler
 23}
 24
 25func abs(name string) string { return "/" + strings.TrimPrefix(name, "/") }
 26
 27// isSelf reports whether a listing entry stands for the listed path itself
 28// rather than something below it.
 29func isSelf(info os.FileInfo) bool {
 30	switch info.Name() {
 31	case "", ".", "/":
 32		return info.IsDir()
 33	}
 34	return false
 35}
 36
 37func (s *storageFS) Stat(name string) (rsync.FileInfo, error) {
 38	if name == "" {
 39		return rsync.FileInfo{IsDir: true}, nil
 40	}
 41	entries, err := s.handler.List(s.session, abs(name), false, false)
 42	if err != nil {
 43		return rsync.FileInfo{}, err
 44	}
 45	for _, e := range entries {
 46		if isSelf(e) {
 47			return rsync.FileInfo{Name: name, ModTime: e.ModTime(), IsDir: true}, nil
 48		}
 49	}
 50	d, implicit := s.handler.(utils.ImplicitDirs)
 51	switch {
 52	case len(entries) == 0 && implicit && d.IsImplicitDir(s.session, abs(name)):
 53		return rsync.FileInfo{Name: name, IsDir: true}, nil
 54	case len(entries) == 0:
 55		return rsync.FileInfo{}, fmt.Errorf("%s: %w", name, fs.ErrNotExist)
 56	case len(entries) == 1 && !entries[0].IsDir() && path.Base(entries[0].Name()) == path.Base(name):
 57		e := entries[0]
 58		return rsync.FileInfo{Name: name, Size: e.Size(), ModTime: e.ModTime()}, nil
 59	default:
 60		// The handler listed the contents of a directory.
 61		return rsync.FileInfo{Name: name, IsDir: true}, nil
 62	}
 63}
 64
 65func (s *storageFS) ReadDir(dir string, recursive bool) ([]rsync.FileInfo, error) {
 66	entries, err := s.handler.List(s.session, abs(dir), true, recursive)
 67	if err != nil {
 68		return nil, err
 69	}
 70	out := make([]rsync.FileInfo, 0, len(entries))
 71	for _, e := range entries {
 72		if isSelf(e) {
 73			continue
 74		}
 75		name := strings.Trim(e.Name(), "/")
 76		if name == "" {
 77			continue
 78		}
 79		out = append(out, rsync.FileInfo{Name: name, Size: e.Size(), ModTime: e.ModTime(), IsDir: e.IsDir()})
 80	}
 81	return out, nil
 82}
 83
 84func (s *storageFS) Open(name string) (rsync.File, rsync.FileInfo, error) {
 85	info, r, err := s.handler.Read(s.session, &utils.FileEntry{Filepath: abs(name)})
 86	if err != nil {
 87		return nil, rsync.FileInfo{}, err
 88	}
 89	if r == nil {
 90		return nil, rsync.FileInfo{}, fmt.Errorf("%s: %w", name, fs.ErrNotExist)
 91	}
 92	return r, rsync.FileInfo{Name: name, Size: info.Size(), ModTime: info.ModTime()}, nil
 93}
 94
 95func (s *storageFS) Put(name string, info rsync.FileInfo, r io.Reader) (string, error) {
 96	msg, err := s.handler.Write(s.session, &utils.FileEntry{
 97		Filepath: abs(name),
 98		Mode:     fs.FileMode(0600),
 99		Size:     info.Size,
100		Mtime:    info.ModTime.Unix(),
101		Atime:    info.ModTime.Unix(),
102		Reader:   r,
103	})
104	if errors.Is(err, utils.ErrFileIgnored) {
105		return msg, ignoredError{err}
106	}
107	return msg, err
108}
109
110// ignoredError marks a handler's ignored file for rsync, keeping the
111// handler's message.
112type ignoredError struct{ error }
113
114func (e ignoredError) Is(target error) bool { return target == rsync.ErrFileIgnored }
115
116func (e ignoredError) Unwrap() error { return e.error }
117
118func (s *storageFS) Remove(name string, isDir bool) error {
119	return s.handler.Delete(s.session, &utils.FileEntry{Filepath: abs(name)})
120}
121
122// Middleware serves rsync clients ("rsync --server ...") from the handler's
123// storage.
124func Middleware(writeHandler utils.CopyFromClientHandler) pssh.SSHServerMiddleware {
125	return func(sshHandler pssh.SSHServerHandler) pssh.SSHServerHandler {
126		return func(session *pssh.SSHServerConnSession) error {
127			cmd := session.Command()
128			if len(cmd) == 0 || cmd[0] != "rsync" {
129				return sshHandler(session)
130			}
131
132			logger := writeHandler.GetLogger(session).With(
133				"rsync", true,
134				"cmd", cmd,
135			)
136
137			defer func() {
138				if r := recover(); r != nil {
139					logger.Error("error running rsync middleware", "err", r)
140					_, _ = session.Stderr().Write([]byte("ERROR: error running rsync middleware, check the flags you are using\r\n"))
141				}
142			}()
143
144			err := rsync.Serve(rsync.Config{
145				FS:     &storageFS{session: session, handler: writeHandler},
146				Logger: logger,
147				Stderr: session.Stderr(),
148			}, session, cmd[1:])
149			if err == nil {
150				return nil
151			}
152
153			// The client has already been told what went wrong; what is
154			// left is to hand it rsync's exit code.
155			code := 1
156			var exit *rsync.ExitError
157			if errors.As(err, &exit) {
158				code = exit.Code
159			}
160			logger.Error("rsync transfer failed", "err", err, "code", code)
161			_ = session.Exit(code)
162			_ = session.Close()
163			return nil
164		}
165	}
166}