main pico / pkg / rsync-receiver / rsyncsender / flist.go
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}