diff --git a/controller/internal/stacks/kept.go b/controller/internal/stacks/kept.go index 1122fc0..bc0d6a4 100644 --- a/controller/internal/stacks/kept.go +++ b/controller/internal/stacks/kept.go @@ -244,11 +244,19 @@ func overlaps(a, b string) bool { func (m *Manager) ownerOf(drive, abs string) (string, string) { var cands []Stack for _, st := range m.GetStacks() { - if st.ComposePath == "" { + if st.ComposePath == "" || st.Protected { + continue + } + // v0.274.0 (found live on 9202): only an app whose definition binds its drive folder THROUGH + // ${HDD_PATH} can own one. The file browser's own compose binds every kept folder by its absolute + // path (the read-only view), and matched first — the list named romm's and paperless's leftovers + // "Filebrowser". Pinned by TestKept_OwnerIsNeverTheFileBrowser. + if b, err := os.ReadFile(st.ComposePath); err != nil || !strings.Contains(string(b), "${HDD_PATH}") { continue } for _, p := range ParseComposeHDDMounts(st.ComposePath, drive) { - if filepath.Clean(p) == abs { + // the folder itself, or one inside it (paperless binds appdata/paperless/media and …/export) + if c := filepath.Clean(p); c == abs || strings.HasPrefix(c, abs+string(filepath.Separator)) { cands = append(cands, st) break } diff --git a/controller/internal/stacks/kept_test.go b/controller/internal/stacks/kept_test.go index 66cbded..b6e7856 100644 --- a/controller/internal/stacks/kept_test.go +++ b/controller/internal/stacks/kept_test.go @@ -268,3 +268,30 @@ func TestKept_AfterLoadRunsTheDeclaredCommand(t *testing.T) { t.Fatalf("after_load ran %q, want %q", strings.Join(got, " "), want) } } + +// TestKept_OwnerIsNeverTheFileBrowser — found live on 9202 (0.274.0-rc1): the file browser's compose binds +// every kept folder by its absolute path (the read-only view), and the list named two leftovers +// "Filebrowser". An owner is an app that binds the folder THROUGH ${HDD_PATH} — the folder or one inside it. +// COMPANION RED-PROOF (REPORT.md): drop the ${HDD_PATH} filter — this fails at "named Filebrowser". +func TestKept_OwnerIsNeverTheFileBrowser(t *testing.T) { + m, drive := keptManager(t) + fb := filepath.Join(m.cfg.Paths.StacksDir, "filebrowser") + must(t, os.MkdirAll(fb, 0o755)) + must(t, os.WriteFile(filepath.Join(fb, "docker-compose.yml"), []byte("services:\n filebrowser:\n image: fb\n volumes:\n - "+ + filepath.Join(drive, "appdata/cloudapp")+":/srv/kept:ro\n - "+filepath.Join(drive, "appdata/paperless")+":/srv/kept2:ro\n"), 0o644)) + must(t, os.WriteFile(filepath.Join(fb, ".felhom.yml"), []byte("display_name: Filebrowser\n"), 0o644)) + pl := filepath.Join(m.cfg.Paths.StacksDir, "paperless-ngx") + must(t, os.MkdirAll(pl, 0o755)) + must(t, os.WriteFile(filepath.Join(pl, "docker-compose.yml"), []byte("services:\n web:\n image: p\n volumes:\n - ${HDD_PATH}/appdata/paperless/media:/m\n"), 0o644)) + must(t, os.WriteFile(filepath.Join(pl, ".felhom.yml"), []byte("display_name: Paperless-ngx\n"), 0o644)) + must(t, m.ScanStacks()) + must(t, os.MkdirAll(filepath.Join(drive, "appdata/paperless/media"), 0o755)) + must(t, os.WriteFile(filepath.Join(drive, "appdata/paperless/media/doc.pdf"), []byte("x"), 0o644)) + got := map[string]string{} + for _, it := range m.ListKept([]string{drive}) { + got[filepath.Base(it.Path)] = it.DisplayName + } + if got["cloudapp"] != "Cloud App" || got["paperless"] != "Paperless-ngx" { + t.Fatalf("the leftovers must be named by the app that binds them through HDD_PATH, never the file browser: %v", got) + } +}