Eric Bower
·
2026-05-31
1package rsyncsender
2
3import (
4 "os"
5 "os/user"
6 "strconv"
7 "sync"
8
9 "github.com/picosh/pico/pkg/rsync-receiver/rsync"
10 "github.com/picosh/pico/pkg/rsync-receiver/rsyncchecksum"
11 "github.com/picosh/pico/pkg/rsync-receiver/rsyncopts"
12 "github.com/picosh/pico/pkg/rsync-receiver/rsyncwire"
13 "github.com/picosh/pico/pkg/rsync-receiver/utils"
14)
15
16type fileList struct {
17 TotalSize int64
18 Files []utils.SenderFile
19}
20
21// rsync/rsync.h defines chunkSize as 32 * 1024, but increasing it to 256K
22// increases throughput with “tridge” rsync as client by 50 Mbit/s.
23const chunkSize = 256 * 1024
24
25var (
26 lookupOnce sync.Once
27 lookupGroupOnce sync.Once
28)
29
30// rsync/flist.c:send_file_list.
31func (st *Transfer) SendFileList(opts *rsyncopts.Options, paths []string, excl *filterRuleList) (*fileList, error) {
32 var fileList fileList
33 fec := &rsyncwire.Buffer{}
34
35 uidMap := make(map[int32]string)
36 gidMap := make(map[int32]string)
37
38 // TODO: flush in between to keep the pipes filled when traversal takes long
39
40 // TODO: handle info == nil case (permission denied?): should set an i/o
41 // error flag, but traversal should continue
42
43 st.Logger.Debug("sendFileList()")
44 // TODO: handle |root| referring to an individual file, symlink or special (skip)
45 for _, requested := range paths {
46 files, err := st.Files.List(requested)
47 if err != nil {
48 return nil, err
49 }
50
51 for _, info := range files {
52 // Only ever transmit long names, like openrsync
53 flags := byte(rsync.XMIT_LONG_NAME)
54
55 // log.Printf("Trim(path=%q, %q) = %q", path, strip, name)
56 name := info.Name()
57 path := name
58 if name == "/" {
59 name = "."
60 flags |= rsync.XMIT_TOP_DIR
61 }
62 // log.Printf("flags for %q: %v", name, flags)
63
64 if excl.matches(name) {
65 continue
66 }
67
68 fileList.Files = append(fileList.Files, utils.SenderFile{
69 Path: "/",
70 Regular: info.Mode().IsRegular(),
71 WPath: name,
72 })
73
74 // 1. status byte (integer)
75 _ = fec.WriteByte(flags)
76
77 // 2. inherited filename length (optional, byte)
78 // 3. filename length (integer or byte)
79 fec.WriteInt32(int32(len(name)))
80
81 // 4. file (byte array)
82 fec.WriteString(name)
83
84 // 5. file length (long)
85 size := info.Size()
86 if info.Mode().IsDir() {
87 // tmpfs returns non-4K sizes for directories. Override with
88 // 4096 to make the tests succeed regardless of the /tmp file
89 // system type.
90 size = 4096
91 }
92 fec.WriteInt64(size)
93
94 fileList.TotalSize += size
95
96 // 6. file modification time (optional, integer)
97 // TODO: this will overflow in 2038! :(
98 fec.WriteInt32(int32(info.ModTime().Unix()))
99
100 // 7. file mode (optional, mode_t, integer)
101 mode := int32(info.Mode() & os.ModePerm)
102 isDev := false
103 isSpecial := false
104 if info.Mode().IsDir() {
105 mode |= rsync.S_IFDIR
106 } else if info.Mode().IsRegular() {
107 mode |= rsync.S_IFREG
108 } else if info.Mode().Type()&os.ModeSymlink != 0 {
109 mode |= rsync.S_IFLNK
110 // TODO: skip symlink if PreserveSymlinks is not set
111 }
112
113 if info.Mode().Type()&os.ModeCharDevice != 0 {
114 mode |= rsync.S_IFCHR
115 isDev = true
116 } else if info.Mode().Type()&os.ModeDevice != 0 {
117 mode |= rsync.S_IFBLK
118 isDev = true
119 }
120
121 if info.Mode().Type()&os.ModeNamedPipe != 0 {
122 mode |= rsync.S_IFIFO
123 isSpecial = true
124 }
125
126 if info.Mode().Type()&os.ModeSocket != 0 {
127 mode |= rsync.S_IFSOCK
128 isSpecial = true
129 }
130
131 fec.WriteInt32(mode)
132
133 if opts.PreserveUid() {
134 uid, ok := uidFromFileInfo(info)
135 if ok {
136 if _, ok := uidMap[uid]; !ok && uid != 0 {
137 u, err := user.LookupId(strconv.Itoa(int(uid)))
138 if err != nil {
139 lookupOnce.Do(func() {
140 st.Logger.Error("lookup", "uid", uid, "err", err)
141 })
142 } else {
143 uidMap[uid] = u.Username
144 }
145 }
146 }
147 // 8. if -o, the user id (integer)
148 fec.WriteInt32(uid)
149 }
150
151 if opts.PreserveGid() {
152 gid, ok := gidFromFileInfo(info)
153 if ok {
154 if _, ok := gidMap[gid]; !ok && gid != 0 {
155 g, err := user.LookupGroupId(strconv.Itoa(int(gid)))
156 if err != nil {
157 lookupGroupOnce.Do(func() {
158 st.Logger.Error("lookupgroup", "gid", gid, "err", err)
159 })
160 } else {
161 gidMap[gid] = g.Name
162 }
163 }
164 }
165 // 9. if -g, the group id (integer)
166 fec.WriteInt32(gid)
167 }
168
169 if (opts.PreserveDevices() && isDev) ||
170 (opts.PreserveSpecials() && isSpecial) {
171 // 10. if a special file and -D, the device “rdev” type (integer)
172 rdev, _ := rdevFromFileInfo(info)
173 fec.WriteInt32(rdev)
174 }
175
176 if opts.PreserveLinks() && info.Mode().Type()&os.ModeSymlink != 0 {
177 // 11. if a symbolic link and -l, the link target's length (integer)
178 // 12. if a symbolic link and -l, the link target (byte array)
179 target, err := os.Readlink(path)
180 if err != nil {
181 continue
182 }
183 fec.WriteInt32(int32(len(target)))
184 fec.WriteString(target)
185 }
186
187 if opts.AlwaysChecksum() {
188 var emptyChecksum [rsyncchecksum.Size]byte
189 checksum := emptyChecksum[:]
190 if info.Mode().IsRegular() {
191 // TODO: send md4 checksum of this file
192 checksum, err = rsyncchecksum.FileChecksum(path)
193 if err != nil {
194 continue
195 }
196 }
197 // For non-regular files, send empty md4 checksum
198 fec.WriteString(string(checksum))
199 }
200 }
201 if err != nil {
202 return nil, err
203 }
204 }
205
206 const endOfFileList = 0
207 _ = fec.WriteByte(endOfFileList)
208
209 const endOfSet = 0
210 if opts.PreserveUid() {
211 for uid, name := range uidMap {
212 fec.WriteInt32(uid)
213 _ = fec.WriteByte(byte(len(name)))
214 fec.WriteString(name)
215 }
216 fec.WriteInt32(endOfSet)
217 }
218
219 if opts.PreserveGid() {
220 for gid, name := range gidMap {
221 fec.WriteInt32(gid)
222 _ = fec.WriteByte(byte(len(name)))
223 fec.WriteString(name)
224 }
225 fec.WriteInt32(endOfSet)
226 }
227
228 const ioErrors = 0
229 fec.WriteInt32(ioErrors)
230
231 if err := st.Conn.WriteString(fec.String()); err != nil {
232 return nil, err
233 }
234
235 return &fileList, nil
236}