main pico / pkg / rsync / fs.go
Antonio Mika  ·  2026-10-10
  1package rsync
  2
  3import (
  4	"errors"
  5	"io"
  6	"time"
  7)
  8
  9// ErrFileIgnored lets Put skip a file without failing the transfer or
 10// acknowledging it for --remove-source-files.
 11var ErrFileIgnored = errors.New("file ignored")
 12
 13// FileInfo describes an entry in an FS.
 14type FileInfo struct {
 15	// Name is slash-separated and relative to the directory being listed.
 16	Name    string
 17	Size    int64
 18	ModTime time.Time
 19	IsDir   bool
 20}
 21
 22// File is a file opened for reading.
 23type File interface {
 24	io.ReaderAt
 25	io.Closer
 26}
 27
 28// FS is the storage the server reads from and writes to. Paths are
 29// slash-separated, relative to the root of the storage, and never start
 30// with a slash. The root itself is "".
 31type FS interface {
 32	// Stat returns the entry at name, or an error wrapping fs.ErrNotExist.
 33	Stat(name string) (FileInfo, error)
 34	// ReadDir returns the entries below dir with names relative to dir,
 35	// descending into subdirectories when recursive is set.
 36	ReadDir(dir string, recursive bool) ([]FileInfo, error)
 37	// Open opens a regular file for reading.
 38	Open(name string) (File, FileInfo, error)
 39	// Put stores a regular file with the data read from r. If the transfer
 40	// fails, r returns an error instead of io.EOF and Put must not keep
 41	// what it read. The returned message, if any, is shown to the user.
 42	// Return an error wrapping ErrFileIgnored for an intentional skip.
 43	Put(name string, info FileInfo, r io.Reader) (string, error)
 44	// Remove deletes a file or an empty directory.
 45	Remove(name string, isDir bool) error
 46}
 47
 48// mapFile gives windowed access to a file being sent, like rsync's
 49// map_ptr. Returned slices are valid until the next call. After a read error
 50// it returns zeroes and records the error, which the caller checks once the
 51// file is done.
 52type mapFile struct {
 53	r       io.ReaderAt
 54	size    int64
 55	window  []byte
 56	start   int64
 57	defSize int64
 58	err     error
 59}
 60
 61func newMapFile(r io.ReaderAt, size int64, readSize int64, blength int32) *mapFile {
 62	if blength > 0 && readSize%int64(blength) != 0 {
 63		readSize += int64(blength) - readSize%int64(blength)
 64	}
 65	return &mapFile{r: r, size: size, defSize: alignUp(readSize)}
 66}
 67
 68func alignUp(n int64) int64 { return (n + 1023) &^ 1023 }
 69
 70func (m *mapFile) ptr(offset int64, n int) []byte {
 71	if n == 0 {
 72		return nil
 73	}
 74	if m.err != nil {
 75		return make([]byte, n)
 76	}
 77	end := offset + int64(n)
 78	if offset >= m.start && end <= m.start+int64(len(m.window)) {
 79		return m.window[offset-m.start : end-m.start]
 80	}
 81	start := offset &^ 1023
 82	size := max(m.defSize, alignUp(end-start))
 83	if start+size > m.size {
 84		size = m.size - start
 85	}
 86	buf := make([]byte, size)
 87	// Reuse the overlap with the current window.
 88	read := buf
 89	readAt := start
 90	if start >= m.start && start < m.start+int64(len(m.window)) {
 91		k := copy(buf, m.window[start-m.start:])
 92		read = buf[k:]
 93		readAt = start + int64(k)
 94	}
 95	if len(read) > 0 {
 96		if err := readFullAt(m.r, read, readAt); err != nil {
 97			m.err = err
 98			return make([]byte, n)
 99		}
100	}
101	m.window = buf
102	m.start = start
103	return m.window[offset-m.start : end-m.start]
104}