mirror of
https://github.com/filebrowser/filebrowser.git
synced 2026-07-17 16:36:49 +00:00
DeleteWithPathPrefix queried the share index with the raw path, so deleting a directory through a trailing-slash path (e.g. DELETE /api/resources/a/) only matched descendants like /a/child and missed the exact /a share, leaving it in storage. If the same path was later recreated, the stale public share re-exposed the new content. Normalize the path before the prefix query so the exact share and its descendants are both removed. Adds a regression test.
135 lines
3.6 KiB
Go
135 lines
3.6 KiB
Go
package bolt
|
|
|
|
import (
|
|
"os"
|
|
"sort"
|
|
"testing"
|
|
|
|
"github.com/asdine/storm/v3"
|
|
|
|
"github.com/filebrowser/filebrowser/v2/share"
|
|
)
|
|
|
|
func newTestShareBackend(t *testing.T) shareBackend {
|
|
t.Helper()
|
|
|
|
f, err := os.CreateTemp(t.TempDir(), "shares-*.db")
|
|
if err != nil {
|
|
t.Fatalf("failed to create temp db: %v", err)
|
|
}
|
|
_ = f.Close()
|
|
|
|
db, err := storm.Open(f.Name())
|
|
if err != nil {
|
|
t.Fatalf("failed to open db: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
|
|
return shareBackend{db: db}
|
|
}
|
|
|
|
func remainingHashes(t *testing.T, s shareBackend) []string {
|
|
t.Helper()
|
|
|
|
links, err := s.All()
|
|
if err != nil {
|
|
t.Fatalf("All returned error: %v", err)
|
|
}
|
|
|
|
hashes := make([]string, 0, len(links))
|
|
for _, link := range links {
|
|
hashes = append(hashes, link.Hash)
|
|
}
|
|
sort.Strings(hashes)
|
|
return hashes
|
|
}
|
|
|
|
func TestDeleteWithPathPrefix(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
s := newTestShareBackend(t)
|
|
|
|
links := []*share.Link{
|
|
// user 1's links
|
|
{Hash: "u1-a", Path: "/a", UserID: 1},
|
|
{Hash: "u1-a-child", Path: "/a/child.txt", UserID: 1},
|
|
{Hash: "u1-abc", Path: "/abc", UserID: 1}, // not a descendant of /a
|
|
{Hash: "u1-other", Path: "/other", UserID: 1},
|
|
// user 2's links — must never be touched when user 1 deletes
|
|
{Hash: "u2-a", Path: "/a", UserID: 2},
|
|
{Hash: "u2-a-child", Path: "/a/child.txt", UserID: 2},
|
|
}
|
|
for _, l := range links {
|
|
if err := s.Save(l); err != nil {
|
|
t.Fatalf("failed to save link %s: %v", l.Hash, err)
|
|
}
|
|
}
|
|
|
|
// User 1 deletes their directory /a. Only user 1's /a and its descendants
|
|
// should be removed; /abc (sibling sharing a byte prefix) and all of user
|
|
// 2's links must remain.
|
|
if err := s.DeleteWithPathPrefix("/a", 1); err != nil {
|
|
t.Fatalf("DeleteWithPathPrefix returned error: %v", err)
|
|
}
|
|
|
|
got := remainingHashes(t, s)
|
|
want := []string{"u1-abc", "u1-other", "u2-a", "u2-a-child"}
|
|
if len(got) != len(want) {
|
|
t.Fatalf("remaining hashes = %v, want %v", got, want)
|
|
}
|
|
for i := range want {
|
|
if got[i] != want[i] {
|
|
t.Fatalf("remaining hashes = %v, want %v", got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Regression for the trailing-slash delete leaving a stale share
|
|
// (GHSA-pp88-jhwj-5qh5): deleting "/a/" must remove the exact "/a" share and its
|
|
// descendants, not just the descendants. Siblings and other users are untouched.
|
|
func TestDeleteWithPathPrefixTrailingSlash(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
s := newTestShareBackend(t)
|
|
|
|
links := []*share.Link{
|
|
{Hash: "u1-a", Path: "/a", UserID: 1},
|
|
{Hash: "u1-a-child", Path: "/a/child.txt", UserID: 1},
|
|
{Hash: "u1-abc", Path: "/abc", UserID: 1}, // sibling sharing a byte prefix
|
|
{Hash: "u2-a", Path: "/a", UserID: 2}, // other user, must remain
|
|
}
|
|
for _, l := range links {
|
|
if err := s.Save(l); err != nil {
|
|
t.Fatalf("failed to save link %s: %v", l.Hash, err)
|
|
}
|
|
}
|
|
|
|
// Delete with a trailing slash, as the resource delete handler does for a
|
|
// directory request like DELETE /api/resources/a/.
|
|
if err := s.DeleteWithPathPrefix("/a/", 1); err != nil {
|
|
t.Fatalf("DeleteWithPathPrefix returned error: %v", err)
|
|
}
|
|
|
|
got := remainingHashes(t, s)
|
|
want := []string{"u1-abc", "u2-a"}
|
|
if len(got) != len(want) {
|
|
t.Fatalf("remaining hashes = %v, want %v", got, want)
|
|
}
|
|
for i := range want {
|
|
if got[i] != want[i] {
|
|
t.Fatalf("remaining hashes = %v, want %v", got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDeleteWithPathPrefixNoMatch(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
s := newTestShareBackend(t)
|
|
|
|
// No links exist at all: the storm Prefix query returns ErrNotFound, which
|
|
// must be treated as a no-op rather than surfaced as an error.
|
|
if err := s.DeleteWithPathPrefix("/a", 1); err != nil {
|
|
t.Fatalf("DeleteWithPathPrefix on empty store returned error: %v", err)
|
|
}
|
|
}
|