main pico / pkg / apps / prose / router.go
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}