main pico / pkg / rsync-receiver / rsyncreceiver / client.go
Eric Bower  ·  2026-05-31
  1package rsyncreceiver
  2
  3import (
  4	"fmt"
  5	"io"
  6	"log/slog"
  7	"os"
  8
  9	"github.com/picosh/pico/pkg/rsync-receiver/rsync"
 10	"github.com/picosh/pico/pkg/rsync-receiver/rsyncopts"
 11	"github.com/picosh/pico/pkg/rsync-receiver/rsyncsender"
 12	"github.com/picosh/pico/pkg/rsync-receiver/rsyncwire"
 13	"github.com/picosh/pico/pkg/rsync-receiver/utils"
 14)
 15
 16func ClientRun(logger *slog.Logger, opts *rsyncopts.Options, conn io.ReadWriter, filesystem utils.FS, paths []string, negotiate bool) error {
 17	var err error
 18
 19	crd, cwr := rsyncwire.CounterPair(conn, conn)
 20
 21	const sessionChecksumSeed = 666
 22
 23	c := &rsyncwire.Conn{
 24		Reader: crd,
 25		Writer: cwr,
 26	}
 27
 28	if negotiate {
 29		remoteProtocol, err := c.ReadInt32()
 30		if err != nil {
 31			return err
 32		}
 33		logger.Debug("remote protocol", "protocol", remoteProtocol)
 34		if err := c.WriteInt32(rsync.ProtocolVersion); err != nil {
 35			return err
 36		}
 37	}
 38
 39	if err := c.WriteInt32(sessionChecksumSeed); err != nil {
 40		return err
 41	}
 42
 43	// Switch to multiplexing protocol, but only for server-side transmissions.
 44	// Transmissions received from the client are not multiplexed.
 45	mpx := &rsyncwire.MultiplexWriter{Writer: c.Writer}
 46	c.Writer = mpx
 47
 48	defer func() {
 49		if err != nil {
 50			_, _ = mpx.WriteMsg(rsyncwire.MsgError, fmt.Appendf(nil, "gokr-rsync [receiver]: %v\n", err))
 51		}
 52	}()
 53
 54	rt := &Transfer{
 55		Opts: &TransferOpts{
 56			DryRun: opts.DryRun(),
 57
 58			DeleteMode:       opts.DeleteMode(),
 59			PreserveGid:      opts.PreserveGid(),
 60			PreserveUid:      opts.PreserveUid(),
 61			PreserveLinks:    opts.PreserveLinks(),
 62			PreservePerms:    opts.PreservePerms(),
 63			PreserveDevices:  opts.PreserveDevices(),
 64			PreserveSpecials: opts.PreserveSpecials(),
 65			PreserveTimes:    opts.PreserveMTimes(),
 66			IgnoreTimes:      opts.IgnoreTimes(),
 67			SizeOnly:         opts.SizeOnly(),
 68			AlwaysChecksum:   opts.AlwaysChecksum(),
 69			// TODO: PreserveHardlinks: opts.PreserveHardlinks,
 70		},
 71		Dest: "/",
 72		// TODO: what is Env used for and can we get rid of it?
 73		Env: Osenv{
 74			Stdout: os.Stdout,
 75			Stderr: os.Stderr,
 76			Stdin:  os.Stdin,
 77		},
 78		Conn: c,
 79		Seed: sessionChecksumSeed,
 80
 81		Files: filesystem,
 82
 83		Logger: logger,
 84	}
 85
 86	if opts.DeleteMode() {
 87		// receive the exclusion list (openrsync’s is always empty)
 88		exclusionList, err := rsyncsender.RecvFilterList(c)
 89		if err != nil {
 90			return err
 91		}
 92		logger.Debug("exclusion list read", "filters", exclusionList.Filters)
 93	}
 94
 95	// receive file list
 96	logger.Debug("receiving file list")
 97	fileList, err := rt.ReceiveFileList()
 98	if err != nil {
 99		return err
100	}
101	logger.Debug("received names", "files", fileList)
102	stats, err := rt.Do(c, fileList, true)
103	if err != nil {
104		return err
105	}
106
107	logger.Debug("stats", "stats", stats)
108	return nil
109}