Compare commits
2 Commits
340370a43b
...
2ec031c9dc
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ec031c9dc | |||
| b2172145e7 |
@@ -1,5 +1,5 @@
|
||||
BINARY=syncserver
|
||||
VERSION?=1.0.47
|
||||
VERSION?=1.0.48
|
||||
GO?=go
|
||||
LDFLAGS=-s -w -X main.version=$(VERSION) -X main.commit=$(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
||||
BUILD_FLAGS=CGO_ENABLED=0
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ import (
|
||||
"github.com/syncserver/internal/syncengine"
|
||||
)
|
||||
|
||||
var version = "1.0.47"
|
||||
var version = "1.0.48"
|
||||
|
||||
func main() {
|
||||
cfgPath := flag.String("config", "", "Path to config.yaml")
|
||||
|
||||
@@ -69,15 +69,28 @@ func (r *RsyncRunner) buildArgs(pair *SyncPairConfig) []string {
|
||||
args = append(args, "--delete")
|
||||
}
|
||||
|
||||
src := ensureDirSlash(pair.Source)
|
||||
if pair.Direction == "pull" {
|
||||
args = append(args, pair.Dest, pair.Source)
|
||||
args = append(args, pair.Dest, src)
|
||||
} else {
|
||||
args = append(args, pair.Source, pair.Dest)
|
||||
args = append(args, src, pair.Dest)
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
// ensureDirSlash guarantees the source path is treated by rsync as a
|
||||
// directory whose contents are copied, regardless of whether the user
|
||||
// supplied a trailing slash. This avoids the common foot-gun where
|
||||
// "rsync host:/path/series /dest/" creates /dest/series/<contents> nested
|
||||
// inside an extra "series" subdirectory.
|
||||
func ensureDirSlash(p string) string {
|
||||
if strings.HasSuffix(p, "/") {
|
||||
return p
|
||||
}
|
||||
return p + "/"
|
||||
}
|
||||
|
||||
type MachineKeys struct {
|
||||
Host string
|
||||
Port int
|
||||
|
||||
@@ -40,8 +40,8 @@ func TestBuildArgs_Push(t *testing.T) {
|
||||
if args[0] != "-aP" {
|
||||
t.Errorf("first flag = %q, want %q", args[0], "-aP")
|
||||
}
|
||||
if args[len(args)-2] != "/local/src" {
|
||||
t.Errorf("source = %q, want %q", args[len(args)-2], "/local/src")
|
||||
if args[len(args)-2] != "/local/src/" {
|
||||
t.Errorf("source = %q, want %q (auto-appended trailing slash)", args[len(args)-2], "/local/src/")
|
||||
}
|
||||
if args[len(args)-1] != "admin@10.5.0.144:/remote/dst" {
|
||||
t.Errorf("dest = %q, want %q", args[len(args)-1], "admin@10.5.0.144:/remote/dst")
|
||||
@@ -61,8 +61,8 @@ func TestBuildArgs_Pull(t *testing.T) {
|
||||
if args[len(args)-2] != "/local/dst" {
|
||||
t.Errorf("pull: second-to-last (dest) = %q, want %q", args[len(args)-2], "/local/dst")
|
||||
}
|
||||
if args[len(args)-1] != "admin@10.5.0.144:/remote/src" {
|
||||
t.Errorf("pull: last (source) = %q, want %q", args[len(args)-1], "admin@10.5.0.144:/remote/src")
|
||||
if args[len(args)-1] != "admin@10.5.0.144:/remote/src/" {
|
||||
t.Errorf("pull: last (source) = %q, want %q (auto-appended trailing slash)", args[len(args)-1], "admin@10.5.0.144:/remote/src/")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,8 +162,8 @@ func TestRunRemote_PushSrcAndDestStayAsIs(t *testing.T) {
|
||||
}
|
||||
srcArg, dstArg := tc.build()
|
||||
|
||||
if srcArg != "/share/homes/admin/media" {
|
||||
t.Errorf("push src = %q, want raw path '/share/homes/admin/media' (no user@host: prefix added)", srcArg)
|
||||
if srcArg != "/share/homes/admin/media/" {
|
||||
t.Errorf("push src = %q, want '/share/homes/admin/media/' (auto-appended trailing slash, no user@host: prefix added)", srcArg)
|
||||
}
|
||||
if dstArg != "/share/media/peliculas" {
|
||||
t.Errorf("push dst = %q, want raw path '/share/media/peliculas'", dstArg)
|
||||
@@ -193,6 +193,80 @@ func TestRunRemote_PullSrcAndDestStayAsIs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildArgs_AutoAppendsTrailingSlashToSource(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
source string
|
||||
dest string
|
||||
direction string
|
||||
wantSrc string
|
||||
}{
|
||||
{
|
||||
name: "push, source without trailing slash",
|
||||
source: "/mnt/storage/multimedia/series",
|
||||
dest: "/share/media/series",
|
||||
direction: "push",
|
||||
wantSrc: "/mnt/storage/multimedia/series/",
|
||||
},
|
||||
{
|
||||
name: "push, source already has trailing slash (idempotent)",
|
||||
source: "/mnt/storage/multimedia/series/",
|
||||
dest: "/share/media/series",
|
||||
direction: "push",
|
||||
wantSrc: "/mnt/storage/multimedia/series/",
|
||||
},
|
||||
{
|
||||
name: "push, remote source without trailing slash",
|
||||
source: "admin@baby-nas:/mnt/storage/multimedia/series",
|
||||
dest: "/share/media/series",
|
||||
direction: "push",
|
||||
wantSrc: "admin@baby-nas:/mnt/storage/multimedia/series/",
|
||||
},
|
||||
{
|
||||
name: "pull, source without trailing slash",
|
||||
source: "admin@baby-nas:/mnt/storage/multimedia/series",
|
||||
dest: "/share/media/series",
|
||||
direction: "pull",
|
||||
wantSrc: "admin@baby-nas:/mnt/storage/multimedia/series/",
|
||||
},
|
||||
{
|
||||
name: "mirror, source without trailing slash",
|
||||
source: "/mnt/storage/multimedia/series",
|
||||
dest: "admin@10.5.0.144:/share/media/series",
|
||||
direction: "mirror",
|
||||
wantSrc: "/mnt/storage/multimedia/series/",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
runner := NewRsyncRunner("/tmp/ssh", "")
|
||||
pair := &SyncPairConfig{
|
||||
Source: tc.source,
|
||||
Dest: tc.dest,
|
||||
Direction: tc.direction,
|
||||
}
|
||||
args := runner.buildArgs(pair)
|
||||
|
||||
var gotSrc, gotDest string
|
||||
if tc.direction == "pull" {
|
||||
gotDest = args[len(args)-2]
|
||||
gotSrc = args[len(args)-1]
|
||||
} else {
|
||||
gotSrc = args[len(args)-2]
|
||||
gotDest = args[len(args)-1]
|
||||
}
|
||||
|
||||
if gotSrc != tc.wantSrc {
|
||||
t.Errorf("source = %q, want %q (dest must never be touched)", gotSrc, tc.wantSrc)
|
||||
}
|
||||
if gotDest != tc.dest {
|
||||
t.Errorf("dest = %q, want %q (dest must never be normalized)", gotDest, tc.dest)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func countOccurrences(s, substr string) int {
|
||||
return strings.Count(s, substr)
|
||||
}
|
||||
|
||||
@@ -375,6 +375,9 @@ export default function SyncPairs() {
|
||||
setForm({ ...form, source_path: e.target.value })
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-fg-subtle">
|
||||
Directory on the source machine. Its <span className="font-medium">contents</span> will be copied into the destination. Trailing <code className="font-mono">/</code> is optional.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="sp-dest-path" required>
|
||||
@@ -388,6 +391,9 @@ export default function SyncPairs() {
|
||||
setForm({ ...form, dest_path: e.target.value })
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-fg-subtle">
|
||||
Directory on the destination machine where the source contents will land.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
|
||||
Reference in New Issue
Block a user