Antonio Mika
·
2026-10-11
1package prose
2
3import (
4 "errors"
5 "fmt"
6 "log/slog"
7 "os"
8 "path/filepath"
9
10 "github.com/picosh/pico/pkg/pssh"
11 sendutils "github.com/picosh/pico/pkg/send/utils"
12)
13
14// ReadWriteHandler serves the files with the extensions it is routed.
15type ReadWriteHandler interface {
16 List(s *pssh.SSHServerConnSession, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error)
17 Write(*pssh.SSHServerConnSession, *sendutils.FileEntry) (string, error)
18 Read(*pssh.SSHServerConnSession, *sendutils.FileEntry) (os.FileInfo, sendutils.ReadAndReaderAtCloser, error)
19 Delete(*pssh.SSHServerConnSession, *sendutils.FileEntry) error
20}
21
22// fileRouter sends each file to the handler for its extension.
23type fileRouter struct {
24 FileMap map[string]ReadWriteHandler
25 Space string
26}
27
28var _ sendutils.CopyFromClientHandler = (*fileRouter)(nil)
29
30func newFileRouter(space string, mapper map[string]ReadWriteHandler) *fileRouter {
31 return &fileRouter{
32 Space: space,
33 FileMap: mapper,
34 }
35}
36
37func (r *fileRouter) findHandler(fp string) (ReadWriteHandler, error) {
38 fext := filepath.Ext(fp)
39 handler, ok := r.FileMap[fext]
40 if !ok {
41 hand, hasFallback := r.FileMap["fallback"]
42 if !hasFallback {
43 return nil, fmt.Errorf("no corresponding handler for file extension: %s", fext)
44 }
45 handler = hand
46 }
47 return handler, nil
48}
49
50func (r *fileRouter) Write(s *pssh.SSHServerConnSession, entry *sendutils.FileEntry) (string, error) {
51 if entry.Mode.IsDir() {
52 return "", os.ErrInvalid
53 }
54
55 handler, err := r.findHandler(entry.Filepath)
56 if err != nil {
57 return "", err
58 }
59 return handler.Write(s, entry)
60}
61
62func (r *fileRouter) ValidateWrite(s *pssh.SSHServerConnSession, entry *sendutils.FileEntry) error {
63 handler, err := r.findHandler(entry.Filepath)
64 if err != nil {
65 return err
66 }
67 if v, ok := handler.(sendutils.WriteValidator); ok {
68 return v.ValidateWrite(s, entry)
69 }
70 return nil
71}
72
73func (r *fileRouter) Delete(s *pssh.SSHServerConnSession, entry *sendutils.FileEntry) error {
74 handler, err := r.findHandler(entry.Filepath)
75 if err != nil {
76 return err
77 }
78 return handler.Delete(s, entry)
79}
80
81// Rename moves a file in place when one handler serves both names.
82func (r *fileRouter) Rename(s *pssh.SSHServerConnSession, from, to *sendutils.FileEntry) error {
83 handler, err := r.findHandler(from.Filepath)
84 if err != nil {
85 return err
86 }
87 target, err := r.findHandler(to.Filepath)
88 if err != nil {
89 return err
90 }
91 renamer, ok := handler.(sendutils.FileRenamer)
92 if !ok || handler != target {
93 return errors.ErrUnsupported
94 }
95 return renamer.Rename(s, from, to)
96}
97
98func (r *fileRouter) Read(s *pssh.SSHServerConnSession, entry *sendutils.FileEntry) (os.FileInfo, sendutils.ReadAndReaderAtCloser, error) {
99 handler, err := r.findHandler(entry.Filepath)
100 if err != nil {
101 return nil, nil, err
102 }
103 return handler.Read(s, entry)
104}
105
106func (r *fileRouter) List(s *pssh.SSHServerConnSession, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error) {
107 files := []os.FileInfo{}
108 // Several extensions can share a handler, and handlers can list the
109 // same entries, such as the root directory.
110 listed := map[ReadWriteHandler]bool{}
111 seen := map[string]bool{}
112 for _, handler := range r.FileMap {
113 if listed[handler] {
114 continue
115 }
116 listed[handler] = true
117
118 ff, err := handler.List(s, fpath, isDir, recursive)
119 if err != nil {
120 r.GetLogger(s).Error("handler list", "err", err)
121 continue
122 }
123 for _, f := range ff {
124 key := fmt.Sprintf("%s:%v", f.Name(), f.IsDir())
125 if !seen[key] {
126 seen[key] = true
127 files = append(files, f)
128 }
129 }
130 }
131 return files, nil
132}
133
134func (r *fileRouter) GetLogger(s *pssh.SSHServerConnSession) *slog.Logger {
135 return pssh.GetLogger(s)
136}
137
138func (r *fileRouter) Validate(s *pssh.SSHServerConnSession) error {
139 logger := pssh.GetLogger(s)
140 user := pssh.GetUser(s)
141
142 if user == nil {
143 err := fmt.Errorf("could not get user from ctx")
144 logger.Error("error getting user from ctx", "err", err)
145 return err
146 }
147
148 logger.Info(
149 "attempting to upload files",
150 "user", user.Name,
151 "space", r.Space,
152 )
153 return nil
154}