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 { | |
| 72 | 72 | preserveAtime bool | |
| 73 | 73 | alwaysSum bool | |
| 74 | 74 | sumLen int | |
| 75 | + | recurse bool | |
| 76 | + | relative bool | |
| 75 | 77 | ||
| 76 | 78 | last fileEntry | |
| 77 | 79 | lastName string | |
| 78 | 80 | lastAtime int64 | |
| 79 | 81 | 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 | |
| 80 | 87 | } | |
| 81 | 88 | ||
| 82 | 89 | func (fc *flistCodec) sendEntry(c *conn, f *fileEntry) error { |
| ... | ... | @@ -493,7 +500,24 @@ func (fc *flistCodec) recvEntry(c *conn, xflags int) (*fileEntry, error) { | |
| 493 | 500 | f.flags |= flagImpliedDir | |
| 494 | 501 | } | |
| 495 | 502 | } 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 | + | } | |
| 496 | 511 | 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 | + | } | |
| 497 | 521 | } | |
| 498 | 522 | } | |
| 499 | 523 |
+55,
-1
| ... | ... | @@ -923,7 +923,7 @@ func TestRsyncMergeFilterErrors(t *testing.T) { | |
| 923 | 923 | func TestRsyncUnsupportedOptions(t *testing.T) { | |
| 924 | 924 | e := newRsyncEnv(t) | |
| 925 | 925 | populate(t, e.local) | |
| 926 | - | for _, opt := range []string{"-H", "-A", "-X", "-R"} { | |
| 926 | + | for _, opt := range []string{"-H", "-A", "-X"} { | |
| 927 | 927 | out, err := e.run("-r", opt, e.local+"/", "host:site") | |
| 928 | 928 | if err == nil { | |
| 929 | 929 | t.Errorf("%s: want an error\n%s", opt, out) |
| ... | ... | @@ -932,4 +932,58 @@ func TestRsyncUnsupportedOptions(t *testing.T) { | |
| 932 | 932 | t.Errorf("%s: missing explanation\n%s", opt, out) | |
| 933 | 933 | } | |
| 934 | 934 | } | |
| 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 | + | } | |
| 935 | 989 | } |
+0,
-3
| ... | ... | @@ -502,9 +502,6 @@ func (g *generator) deletePass() error { | |
| 502 | 502 | if !ok || !f.isDir() { | |
| 503 | 503 | return false | |
| 504 | 504 | } | |
| 505 | - | if g.s.c.protocol < 30 && s.opts.recurse { | |
| 506 | - | return true | |
| 507 | - | } | |
| 508 | 505 | return f.flags&flagContentDir != 0 | |
| 509 | 506 | } | |
| 510 | 507 |
+6,
-2
| ... | ... | @@ -42,6 +42,7 @@ func (e *ExitError) Unwrap() error { return e.Err } | |
| 42 | 42 | type options struct { | |
| 43 | 43 | sender bool | |
| 44 | 44 | recurse bool | |
| 45 | + | relative bool | |
| 45 | 46 | xferDirs bool | |
| 46 | 47 | dryRun bool | |
| 47 | 48 | preserveUID bool |
| ... | ... | @@ -155,6 +156,7 @@ func Serve(cfg Config, rw io.ReadWriter, args []string) error { | |
| 155 | 156 | s.opts = options{ | |
| 156 | 157 | sender: o.Sender(), | |
| 157 | 158 | recurse: o.Recurse(), | |
| 159 | + | relative: o.RelativePaths(), | |
| 158 | 160 | xferDirs: o.XferDirs(), | |
| 159 | 161 | dryRun: o.DryRun(), | |
| 160 | 162 | preserveUID: o.PreserveUid(), |
| ... | ... | @@ -249,8 +251,8 @@ func checkSupported(o *rsyncopts.Options) error { | |
| 249 | 251 | return errors.New("--xattrs is not supported") | |
| 250 | 252 | case o.PreserveCrtimes(): | |
| 251 | 253 | 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") | |
| 254 | 256 | case o.AppendMode() != 0: | |
| 255 | 257 | return errors.New("--append is not supported") | |
| 256 | 258 | case len(o.BasisDirs()) > 0: |