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}