Commit d39b458

Antonio Mika  ·  2026-10-14 17:38:53 -0400 EDT
parent 40c2ba8
fix(rsync): accept --relative on uploads
4 files changed,  +85, -6
+24, -0
......@@ -72,11 +72,18 @@ type flistCodec struct {
7272 preserveAtime bool
7373 alwaysSum bool
7474 sumLen int
75+ recurse bool
76+ relative bool
7577
7678 last fileEntry
7779 lastName string
7880 lastAtime int64
7981 rdevMaj uint32
82+
83+ // Before protocol 30 the receiver works out which directories are
84+ // content dirs from the order of the list (flist.c:in_del_hier).
85+ inDelHier bool
86+ delHierLen int
8087 }
8188
8289 func (fc *flistCodec) sendEntry(c *conn, f *fileEntry) error {
......@@ -493,7 +500,24 @@ func (fc *flistCodec) recvEntry(c *conn, xflags int) (*fileEntry, error) {
493500 f.flags |= flagImpliedDir
494501 }
495502 } else if xflags&xmitTopDir != 0 {
503+ fc.inDelHier = fc.recurse
504+ fc.delHierLen = 0
505+ if name != "." {
506+ fc.delHierLen = len(rawName)
507+ }
508+ if fc.relative && strings.HasSuffix(rawName[:fc.delHierLen], "/.") {
509+ fc.delHierLen -= 2
510+ }
496511 f.flags |= flagTopDir | flagContentDir
512+ } else if fc.inDelHier {
513+ // Under -R only the top dir's own subtree is content; implied
514+ // parents of other paths end the hierarchy.
515+ if !fc.relative || fc.delHierLen == 0 ||
516+ (l1 >= fc.delHierLen && len(rawName) > fc.delHierLen && rawName[fc.delHierLen] == '/') {
517+ f.flags |= flagContentDir
518+ } else {
519+ fc.inDelHier = false
520+ }
497521 }
498522 }
499523
+55, -1
......@@ -923,7 +923,7 @@ func TestRsyncMergeFilterErrors(t *testing.T) {
923923 func TestRsyncUnsupportedOptions(t *testing.T) {
924924 e := newRsyncEnv(t)
925925 populate(t, e.local)
926- for _, opt := range []string{"-H", "-A", "-X", "-R"} {
926+ for _, opt := range []string{"-H", "-A", "-X"} {
927927 out, err := e.run("-r", opt, e.local+"/", "host:site")
928928 if err == nil {
929929 t.Errorf("%s: want an error\n%s", opt, out)
......@@ -932,4 +932,58 @@ func TestRsyncUnsupportedOptions(t *testing.T) {
932932 t.Errorf("%s: missing explanation\n%s", opt, out)
933933 }
934934 }
935+ out, err := e.run("-rR", "host:site/./css", t.TempDir())
936+ if err == nil {
937+ t.Errorf("-R download: want an error\n%s", out)
938+ }
939+ if !strings.Contains(out, "not supported when downloading") {
940+ t.Errorf("-R download: missing explanation\n%s", out)
941+ }
942+}
943+
944+func TestRsyncUploadRelative(t *testing.T) {
945+ cases := []struct {
946+ name string
947+ args func(local, list string) []string
948+ }{
949+ {"files-from", func(local, list string) []string {
950+ return []string{"-t", "--files-from=" + list, local + "/"}
951+ }},
952+ {"files-from-delete", func(local, list string) []string {
953+ return []string{"-rt", "--delete", "--files-from=" + list, local + "/"}
954+ }},
955+ {"dot-marker", func(local, list string) []string {
956+ return []string{"-tR", local + "/./a/b/c/deep.txt"}
957+ }},
958+ {"dot-marker-dir-delete", func(local, list string) []string {
959+ return []string{"-rtR", "--delete", local + "/./a/b"}
960+ }},
961+ }
962+ for _, proto := range protocols {
963+ for _, tc := range cases {
964+ t.Run(fmt.Sprintf("proto=%s/%s", proto, tc.name), func(t *testing.T) {
965+ e := newRsyncEnv(t)
966+ populate(t, e.local)
967+ list := filepath.Join(t.TempDir(), "files")
968+ writeFile(t, list, []byte("index.html\na/b/c/deep.txt\n"))
969+
970+ // Seed both sides with files outside the list so a
971+ // deletion that reaches too far shows up.
972+ ref := t.TempDir()
973+ populate(t, ref)
974+ writeFile(t, filepath.Join(ref, "a/b/stale.txt"), []byte("stale"))
975+ writeFile(t, filepath.Join(ref, "a/b/c/stale.txt"), []byte("stale"))
976+ writeFile(t, filepath.Join(ref, "a/other.txt"), []byte("other"))
977+ e.mustRun("-rt", ref+"/", "host:site")
978+
979+ args := append(protoArgs(proto), tc.args(e.local, list)...)
980+ refCmd := exec.Command("rsync", append(slices.Clone(args), ref)...)
981+ if out, err := refCmd.CombinedOutput(); err != nil {
982+ t.Fatalf("reference rsync: %v\n%s", err, out)
983+ }
984+ e.mustRun(append(args, "host:site")...)
985+ assertSameTree(t, ref, filepath.Join(e.remote, "site"))
986+ })
987+ }
988+ }
935989 }
+0, -3
......@@ -502,9 +502,6 @@ func (g *generator) deletePass() error {
502502 if !ok || !f.isDir() {
503503 return false
504504 }
505- if g.s.c.protocol < 30 && s.opts.recurse {
506- return true
507- }
508505 return f.flags&flagContentDir != 0
509506 }
510507
+6, -2
......@@ -42,6 +42,7 @@ func (e *ExitError) Unwrap() error { return e.Err }
4242 type options struct {
4343 sender bool
4444 recurse bool
45+ relative bool
4546 xferDirs bool
4647 dryRun bool
4748 preserveUID bool
......@@ -155,6 +156,7 @@ func Serve(cfg Config, rw io.ReadWriter, args []string) error {
155156 s.opts = options{
156157 sender: o.Sender(),
157158 recurse: o.Recurse(),
159+ relative: o.RelativePaths(),
158160 xferDirs: o.XferDirs(),
159161 dryRun: o.DryRun(),
160162 preserveUID: o.PreserveUid(),
......@@ -249,8 +251,8 @@ func checkSupported(o *rsyncopts.Options) error {
249251 return errors.New("--xattrs is not supported")
250252 case o.PreserveCrtimes():
251253 return errors.New("--crtimes is not supported")
252- case o.RelativePaths():
253- return errors.New("--relative is not supported")
254+ case o.Sender() && o.RelativePaths():
255+ return errors.New("--relative is not supported when downloading")
254256 case o.AppendMode() != 0:
255257 return errors.New("--append is not supported")
256258 case len(o.BasisDirs()) > 0:
......@@ -641,6 +643,8 @@ func (s *session) newFlistCodec() *flistCodec {
641643 preserveAtime: s.opts.preserveAtimes,
642644 alwaysSum: s.opts.alwaysChecksum,
643645 sumLen: s.cs.file.size(),
646+ recurse: s.opts.recurse,
647+ relative: s.opts.relative,
644648 }
645649 }
646650