Compare commits
2 Commits
340370a43b
...
2ec031c9dc
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ec031c9dc | |||
| b2172145e7 |
@@ -1,5 +1,5 @@
|
|||||||
BINARY=syncserver
|
BINARY=syncserver
|
||||||
VERSION?=1.0.47
|
VERSION?=1.0.48
|
||||||
GO?=go
|
GO?=go
|
||||||
LDFLAGS=-s -w -X main.version=$(VERSION) -X main.commit=$(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
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
|
BUILD_FLAGS=CGO_ENABLED=0
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ import (
|
|||||||
"github.com/syncserver/internal/syncengine"
|
"github.com/syncserver/internal/syncengine"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "1.0.47"
|
var version = "1.0.48"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
cfgPath := flag.String("config", "", "Path to config.yaml")
|
cfgPath := flag.String("config", "", "Path to config.yaml")
|
||||||
|
|||||||
@@ -69,15 +69,28 @@ func (r *RsyncRunner) buildArgs(pair *SyncPairConfig) []string {
|
|||||||
args = append(args, "--delete")
|
args = append(args, "--delete")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
src := ensureDirSlash(pair.Source)
|
||||||
if pair.Direction == "pull" {
|
if pair.Direction == "pull" {
|
||||||
args = append(args, pair.Dest, pair.Source)
|
args = append(args, pair.Dest, src)
|
||||||
} else {
|
} else {
|
||||||
args = append(args, pair.Source, pair.Dest)
|
args = append(args, src, pair.Dest)
|
||||||
}
|
}
|
||||||
|
|
||||||
return args
|
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 {
|
type MachineKeys struct {
|
||||||
Host string
|
Host string
|
||||||
Port int
|
Port int
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ func TestBuildArgs_Push(t *testing.T) {
|
|||||||
if args[0] != "-aP" {
|
if args[0] != "-aP" {
|
||||||
t.Errorf("first flag = %q, want %q", args[0], "-aP")
|
t.Errorf("first flag = %q, want %q", args[0], "-aP")
|
||||||
}
|
}
|
||||||
if args[len(args)-2] != "/local/src" {
|
if args[len(args)-2] != "/local/src/" {
|
||||||
t.Errorf("source = %q, want %q", 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" {
|
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")
|
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" {
|
if args[len(args)-2] != "/local/dst" {
|
||||||
t.Errorf("pull: second-to-last (dest) = %q, want %q", 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" {
|
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")
|
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()
|
srcArg, dstArg := tc.build()
|
||||||
|
|
||||||
if srcArg != "/share/homes/admin/media" {
|
if srcArg != "/share/homes/admin/media/" {
|
||||||
t.Errorf("push src = %q, want raw path '/share/homes/admin/media' (no user@host: prefix added)", srcArg)
|
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" {
|
if dstArg != "/share/media/peliculas" {
|
||||||
t.Errorf("push dst = %q, want raw path '/share/media/peliculas'", dstArg)
|
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 {
|
func countOccurrences(s, substr string) int {
|
||||||
return strings.Count(s, substr)
|
return strings.Count(s, substr)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -375,6 +375,9 @@ export default function SyncPairs() {
|
|||||||
setForm({ ...form, source_path: e.target.value })
|
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>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="sp-dest-path" required>
|
<Label htmlFor="sp-dest-path" required>
|
||||||
@@ -388,6 +391,9 @@ export default function SyncPairs() {
|
|||||||
setForm({ ...form, dest_path: e.target.value })
|
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>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
|||||||
Reference in New Issue
Block a user