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}