Merge pull request #457 from Silo-Server/t3code/review-discord-issue
fix(autoscan): match rewrite rules against UNC webhook paths
This commit is contained in:
@@ -10,7 +10,7 @@ func normalizeSeparators(path string) string {
|
||||
}
|
||||
|
||||
// applyRewrites returns path with the MOST-SPECIFIC matching prefix rewrite
|
||||
// applied, or path unchanged when none match.
|
||||
// applied, or the normalized path unchanged when none match.
|
||||
//
|
||||
// "Most-specific" means the longest matching From wins, not the first one in the
|
||||
// slice. A first-match strategy lets a broad rewrite (From="/data") shadow a
|
||||
@@ -18,6 +18,25 @@ func normalizeSeparators(path string) string {
|
||||
// be listed first; the arr plugin review flagged exactly this. Selecting the
|
||||
// longest matching prefix makes the result independent of rule ordering.
|
||||
func applyRewrites(path string, rewrites []PathRewrite) string {
|
||||
// Normalize the incoming path the SAME way the stored From is normalized
|
||||
// below. Separator swapping alone is not enough: a Windows UNC path like
|
||||
// `\\NAS\Media\TV\...` becomes `//NAS/Media/TV/...`, while normalizePath
|
||||
// collapses the From's leading `//` to `/NAS/Media/TV` — an asymmetry that
|
||||
// made UNC roots unmatchable by any rewrite rule.
|
||||
//
|
||||
// A trailing separator is semantic downstream — filepath.Dir("/x/Show/")
|
||||
// is the directory itself while filepath.Dir("/x/Show") is its parent, so
|
||||
// legacy-scope changes rely on it to scan the notified directory rather
|
||||
// than collapsing to a broader parent scan. normalizePath strips it for
|
||||
// matching; restore it on whatever we return.
|
||||
trailing := strings.HasSuffix(normalizeSeparators(strings.TrimSpace(path)), "/")
|
||||
path = normalizePath(path)
|
||||
restoreTrailing := func(p string) string {
|
||||
if trailing && p != "/" {
|
||||
return p + "/"
|
||||
}
|
||||
return p
|
||||
}
|
||||
bestIdx := -1
|
||||
bestLen := -1
|
||||
var bestTrimmed string
|
||||
@@ -42,7 +61,9 @@ func applyRewrites(path string, rewrites []PathRewrite) string {
|
||||
}
|
||||
}
|
||||
if bestIdx < 0 {
|
||||
return path
|
||||
return restoreTrailing(path)
|
||||
}
|
||||
return strings.TrimSpace(rewrites[bestIdx].To) + strings.TrimPrefix(path, bestTrimmed)
|
||||
// Normalize the joined result too: a To stored with a trailing slash would
|
||||
// otherwise yield a doubled separator at the join point.
|
||||
return restoreTrailing(normalizePath(strings.TrimSpace(rewrites[bestIdx].To) + strings.TrimPrefix(path, bestTrimmed)))
|
||||
}
|
||||
|
||||
@@ -33,8 +33,7 @@ func TestApplyRewrites(t *testing.T) {
|
||||
|
||||
// TestApplyRewritesNormalizesStoredFrom verifies that a Windows-style / dup-slash
|
||||
// stored From is normalized the same way coveredBy/normalizePath does, so a
|
||||
// rewrite the suggester reports as "covered" actually matches at poll time. The
|
||||
// incoming path is already separator-normalized by PollOnce before applyRewrites.
|
||||
// rewrite the suggester reports as "covered" actually matches at poll time.
|
||||
func TestApplyRewritesNormalizesStoredFrom(t *testing.T) {
|
||||
// Backslash From: a Windows-hosted arr root stored verbatim.
|
||||
winFrom := []PathRewrite{{From: `D:\data\tv`, To: "/mnt/media/tv"}}
|
||||
@@ -53,6 +52,61 @@ func TestApplyRewritesNormalizesStoredFrom(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplyRewritesUNCPath verifies a Windows UNC root (\\NAS\Media\TV) from a
|
||||
// Windows-hosted arr matches its rewrite rule. Separator swapping alone turns
|
||||
// the incoming path into //NAS/... while the From normalizes to /NAS/..., so
|
||||
// the prefix never matched — both sides must go through normalizePath.
|
||||
func TestApplyRewritesUNCPath(t *testing.T) {
|
||||
incoming := `\\NAS\Media\TV\Show\S01\E01.mkv`
|
||||
for _, from := range []string{`\\NAS\Media\TV`, "//NAS/Media/TV", "/NAS/Media/TV"} {
|
||||
rw := []PathRewrite{{From: from, To: "/mnt/media/tv"}}
|
||||
if got := applyRewrites(incoming, rw); got != "/mnt/media/tv/Show/S01/E01.mkv" {
|
||||
t.Fatalf("UNC path with From=%q: got %q", from, got)
|
||||
}
|
||||
}
|
||||
|
||||
// An unmatched UNC path still comes back normalized (collapsed slashes),
|
||||
// consistent with what the resolver sees for matched paths.
|
||||
if got := applyRewrites(incoming, nil); got != "/NAS/Media/TV/Show/S01/E01.mkv" {
|
||||
t.Fatalf("unmatched UNC path: got %q", got)
|
||||
}
|
||||
|
||||
// A trailing-slash To must not produce a doubled separator at the join.
|
||||
slashTo := []PathRewrite{{From: `\\NAS\Media\TV`, To: "/mnt/media/tv/"}}
|
||||
if got := applyRewrites(incoming, slashTo); got != "/mnt/media/tv/Show/S01/E01.mkv" {
|
||||
t.Fatalf("trailing-slash To: got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplyRewritesPreservesTrailingSlash verifies a trailing separator on the
|
||||
// incoming path survives normalization and rewriting. It is semantic for
|
||||
// legacy-scope changes: filepath.Dir("/x/Show/") is the directory itself while
|
||||
// filepath.Dir("/x/Show") is its parent, so dropping it would widen a targeted
|
||||
// directory notification into a parent/library scan.
|
||||
func TestApplyRewritesPreservesTrailingSlash(t *testing.T) {
|
||||
rw := []PathRewrite{{From: "/data/tv", To: "/mnt/media/tv"}}
|
||||
if got := applyRewrites("/data/tv/Show/", rw); got != "/mnt/media/tv/Show/" {
|
||||
t.Fatalf("rewritten dir: got %q", got)
|
||||
}
|
||||
// Unmatched paths keep it too.
|
||||
if got := applyRewrites("/other/Show/", rw); got != "/other/Show/" {
|
||||
t.Fatalf("unmatched dir: got %q", got)
|
||||
}
|
||||
// Windows separator form: trailing backslash counts as a trailing separator.
|
||||
unc := []PathRewrite{{From: `\\NAS\Media\TV`, To: "/mnt/media/tv"}}
|
||||
if got := applyRewrites(`\\NAS\Media\TV\Show\`, unc); got != "/mnt/media/tv/Show/" {
|
||||
t.Fatalf("UNC dir: got %q", got)
|
||||
}
|
||||
// Files without a trailing separator stay without one.
|
||||
if got := applyRewrites("/data/tv/Show/E01.mkv", rw); got != "/mnt/media/tv/Show/E01.mkv" {
|
||||
t.Fatalf("file: got %q", got)
|
||||
}
|
||||
// Bare root never doubles.
|
||||
if got := applyRewrites("/", nil); got != "/" {
|
||||
t.Fatalf("root: got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplyRewritesMostSpecificWins verifies the longest matching From wins
|
||||
// regardless of slice ordering: a broad rule must not shadow a nested one.
|
||||
func TestApplyRewritesMostSpecificWins(t *testing.T) {
|
||||
|
||||
@@ -629,7 +629,7 @@ func (s *Service) resolveConnection(ctx context.Context, connectionID string) (R
|
||||
func rewriteChanges(changes []Change, rewrites []PathRewrite) []Change {
|
||||
rewritten := make([]Change, 0, len(changes))
|
||||
for _, change := range changes {
|
||||
path := applyRewrites(normalizeSeparators(change.SourcePath), rewrites)
|
||||
path := applyRewrites(change.SourcePath, rewrites)
|
||||
rewritten = append(rewritten, Change{SourcePath: path, Scope: change.Scope})
|
||||
}
|
||||
return rewritten
|
||||
|
||||
Reference in New Issue
Block a user