main pico / pkg / send / utils / utils.go
Antonio Mika  ·  2026-10-10
  1package utils
  2
  3import (
  4	"encoding/base64"
  5	"errors"
  6	"fmt"
  7	"io"
  8	"io/fs"
  9	"log/slog"
 10	"os"
 11	"path/filepath"
 12	"strconv"
 13
 14	"github.com/picosh/pico/pkg/pssh"
 15)
 16
 17// NULL is an array with a single NULL byte.
 18var NULL = []byte{'\x00'}
 19
 20// ErrFileIgnored marks a file deliberately excluded by upload policy.
 21var ErrFileIgnored = errors.New("file ignored")
 22
 23// FileEntry is an Entry that reads from a Reader, defining a file and
 24// its contents.
 25type FileEntry struct {
 26	Filepath string
 27	Mode     fs.FileMode
 28	Size     int64
 29	Reader   io.Reader
 30	Atime    int64
 31	Mtime    int64
 32	Metadata map[string]string
 33}
 34
 35// Write a file to the given writer.
 36func (e *FileEntry) Write(w io.Writer) error {
 37	if e.Mtime > 0 && e.Atime > 0 {
 38		if _, err := fmt.Fprintf(w, "T%d 0 %d 0\n", e.Mtime, e.Atime); err != nil {
 39			return fmt.Errorf("failed to write file: %q: %w", e.Filepath, err)
 40		}
 41	}
 42	fname := filepath.Base(e.Filepath)
 43	if _, err := fmt.Fprintf(w, "C%s %d %s\n", octalPerms(e.Mode), e.Size, fname); err != nil {
 44		return fmt.Errorf("failed to write file: %q: %w", e.Filepath, err)
 45	}
 46
 47	if _, err := io.Copy(w, e.Reader); err != nil {
 48		return fmt.Errorf("failed to read file: %q: %w", e.Filepath, err)
 49	}
 50
 51	if _, err := w.Write(NULL); err != nil {
 52		return fmt.Errorf("failed to write file: %q: %w", e.Filepath, err)
 53	}
 54	return nil
 55}
 56
 57func octalPerms(info fs.FileMode) string {
 58	return "0" + strconv.FormatUint(uint64(info.Perm()), 8)
 59}
 60
 61// CopyFromClientHandler is a handler that can be implemented to handle files
 62// being copied from the client to the server.
 63type CopyFromClientHandler interface {
 64	// Write should write the given file.
 65	Delete(*pssh.SSHServerConnSession, *FileEntry) error
 66	Write(*pssh.SSHServerConnSession, *FileEntry) (string, error)
 67	Read(*pssh.SSHServerConnSession, *FileEntry) (os.FileInfo, ReadAndReaderAtCloser, error)
 68	List(*pssh.SSHServerConnSession, string, bool, bool) ([]os.FileInfo, error)
 69	GetLogger(*pssh.SSHServerConnSession) *slog.Logger
 70	Validate(*pssh.SSHServerConnSession) error
 71}
 72
 73// WriteValidator is implemented by handlers that can check a write before
 74// its contents arrive, so SFTP can refuse a file when it is opened.
 75type WriteValidator interface {
 76	ValidateWrite(*pssh.SSHServerConnSession, *FileEntry) error
 77}
 78
 79// FileRenamer is implemented by handlers that can move a stored file in
 80// place, replacing any file at the destination. It returns
 81// errors.ErrUnsupported for a move it can't make, and SFTP copies the file
 82// instead.
 83type FileRenamer interface {
 84	Rename(s *pssh.SSHServerConnSession, from, to *FileEntry) error
 85}
 86
 87// ImplicitDirs is implemented by handlers with directories that exist
 88// before anything is stored in them. A stat of one reports a directory, so
 89// a client copying a single file there puts the file inside it.
 90type ImplicitDirs interface {
 91	IsImplicitDir(s *pssh.SSHServerConnSession, path string) bool
 92}
 93
 94func KeyText(session *pssh.SSHServerConnSession) (string, error) {
 95	if session.PublicKey() == nil {
 96		return "", fmt.Errorf("session doesn't have public key")
 97	}
 98	kb := base64.StdEncoding.EncodeToString(session.PublicKey().Marshal())
 99	return fmt.Sprintf("%s %s", session.PublicKey().Type(), kb), nil
100}
101
102func ErrorHandler(session *pssh.SSHServerConnSession, err error) {
103	_, _ = fmt.Fprint(session.Stderr(), err, "\r\n")
104	_ = session.Exit(1)
105	_ = session.Close()
106}
107
108func PrintMsg(session *pssh.SSHServerConnSession, stdout []string, stderr []error) {
109	output := ""
110	if len(stdout) > 0 {
111		for _, msg := range stdout {
112			if msg != "" {
113				output += fmt.Sprintf("%s\r\n", msg)
114			}
115		}
116		_, _ = fmt.Fprintln(session.Stderr(), output)
117	}
118
119	outputErr := ""
120	if len(stderr) > 0 {
121		for _, err := range stderr {
122			outputErr += fmt.Sprintf("%v\r\n", err)
123		}
124		_, _ = fmt.Fprintln(session.Stderr(), outputErr)
125	}
126}