repos / pico

pico services mono repo
git clone https://github.com/picosh/pico.git

pico / pkg / send / protocols / scp
Antonio Mika  ·  2025-03-12

copy_from_client.go

  1package scp
  2
  3import (
  4	"bufio"
  5	"errors"
  6	"fmt"
  7	"io"
  8	"io/fs"
  9	"path/filepath"
 10	"regexp"
 11	"strconv"
 12
 13	"github.com/picosh/pico/pkg/pssh"
 14	"github.com/picosh/pico/pkg/send/utils"
 15)
 16
 17var (
 18	reTimestamp = regexp.MustCompile(`^T(\d{10}) 0 (\d{10}) 0$`)
 19	reNewFolder = regexp.MustCompile(`^D(\d{4}) 0 (.*)$`)
 20	reNewFile   = regexp.MustCompile(`^C(\d{4}) (\d+) (.*)$`)
 21)
 22
 23type parseError struct {
 24	subject string
 25}
 26
 27func (e parseError) Error() string {
 28	return fmt.Sprintf("failed to parse: %q", e.subject)
 29}
 30
 31func copyFromClient(session *pssh.SSHServerConnSession, info Info, handler utils.CopyFromClientHandler) error {
 32	// accepts the request
 33	_, _ = session.Write(utils.NULL)
 34
 35	writeErrors := []error{}
 36	writeSuccess := []string{}
 37
 38	var (
 39		path  = info.Path
 40		r     = bufio.NewReader(session)
 41		mtime int64
 42		atime int64
 43	)
 44
 45	for {
 46		line, _, err := r.ReadLine()
 47		if err != nil {
 48			if errors.Is(err, io.EOF) {
 49				break
 50			}
 51			return fmt.Errorf("failed to read line: %w", err)
 52		}
 53
 54		if matches := reTimestamp.FindAllStringSubmatch(string(line), 2); matches != nil {
 55			mtime, err = strconv.ParseInt(matches[0][1], 10, 64)
 56			if err != nil {
 57				return parseError{string(line)}
 58			}
 59			atime, err = strconv.ParseInt(matches[0][2], 10, 64)
 60			if err != nil {
 61				return parseError{string(line)}
 62			}
 63
 64			// accepts the header
 65			_, _ = session.Write(utils.NULL)
 66			continue
 67		}
 68
 69		if matches := reNewFile.FindAllStringSubmatch(string(line), 3); matches != nil {
 70			if len(matches) != 1 || len(matches[0]) != 4 {
 71				return parseError{string(line)}
 72			}
 73
 74			mode, err := strconv.ParseUint(matches[0][1], 8, 32)
 75			if err != nil {
 76				return parseError{string(line)}
 77			}
 78
 79			size, err := strconv.ParseInt(matches[0][2], 10, 64)
 80			if err != nil {
 81				return parseError{string(line)}
 82			}
 83			name := matches[0][3]
 84
 85			// accepts the header
 86			_, _ = session.Write(utils.NULL)
 87
 88			result, err := handler.Write(session, &utils.FileEntry{
 89				Filepath: filepath.Join(path, name),
 90				Mode:     fs.FileMode(mode),
 91				Size:     size,
 92				Mtime:    mtime,
 93				Atime:    atime,
 94				Reader:   utils.NewLimitReader(r, int(size)),
 95			})
 96
 97			if err == nil {
 98				writeSuccess = append(writeSuccess, result)
 99			} else {
100				writeErrors = append(writeErrors, err)
101				fmt.Printf("failed to write file: %q: %v\n", name, err)
102			}
103
104			// read the trailing nil char
105			_, _ = r.ReadByte() // TODO: check if it is indeed a utils.NULL?
106
107			mtime = 0
108			atime = 0
109			// says 'hey im done'
110			_, _ = session.Write(utils.NULL)
111			continue
112		}
113
114		if matches := reNewFolder.FindAllStringSubmatch(string(line), 2); matches != nil {
115			if len(matches) != 1 || len(matches[0]) != 3 {
116				return parseError{string(line)}
117			}
118
119			name := matches[0][2]
120			path = filepath.Join(path, name)
121			// says 'hey im done'
122			_, _ = session.Write(utils.NULL)
123			continue
124		}
125
126		if string(line) == "E" {
127			path = filepath.Dir(path)
128
129			// says 'hey im done'
130			_, _ = session.Write(utils.NULL)
131			continue
132		}
133
134		return fmt.Errorf("unhandled input: %q", string(line))
135	}
136
137	utils.PrintMsg(session, writeSuccess, writeErrors)
138
139	_, _ = session.Write(utils.NULL)
140	return nil
141}