From 9e62f034898e842c51e2018d73a1a89b6ad469d2 Mon Sep 17 00:00:00 2001 From: Tomas Dvorak Date: Fri, 18 Sep 2026 20:30:05 +0200 Subject: [PATCH] fix(dav): repoint account mount URIs on rename and move (#2878) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WebDAV accounts bind a mount to a URI string; moving or renaming the bound folder left the account pointing at a dead path — the mount 404'd and users had to re-create it manually. DavAccountClient.UpdateURIPrefix rewrites the owner's dav account URIs that match the old URI exactly or sit under it (nested mounts follow moved subtrees). manager.Rename and manager.MoveOrCopy (move only — copies don't change the source's URI) invoke it after a successful operation, scoped to the file's owner. Rewrite failures are logged, not fatal to the file operation. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- inventory/dav_account.go | 35 +++++++++++++++ inventory/dav_account_test.go | 67 ++++++++++++++++++++++++++++ pkg/filemanager/manager/operation.go | 29 +++++++++++- 3 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 inventory/dav_account_test.go diff --git a/inventory/dav_account.go b/inventory/dav_account.go index d2f7e3cc..3c069531 100644 --- a/inventory/dav_account.go +++ b/inventory/dav_account.go @@ -10,6 +10,7 @@ import ( "github.com/cloudreve/Cloudreve/v4/pkg/conf" "github.com/cloudreve/Cloudreve/v4/pkg/hashid" "github.com/samber/lo" + "strings" ) type ( @@ -25,6 +26,11 @@ type ( GetByIDAndUserID(ctx context.Context, id, userID int) (*ent.DavAccount, error) // Delete deletes the dav account. Delete(ctx context.Context, id int) error + // UpdateURIPrefix rewrites the mount URIs of a user's dav accounts after + // a file rename or move: an account mounting exactly oldURI, or any path + // under it, is repointed to newURI. This keeps mounts working when the + // bound folder changes location. + UpdateURIPrefix(ctx context.Context, userID int, oldURI, newURI string) error } ListDavAccountArgs struct { @@ -96,6 +102,35 @@ func (c *davAccountClient) Delete(ctx context.Context, id int) error { return c.client.DavAccount.DeleteOneID(id).Exec(ctx) } +func (c *davAccountClient) UpdateURIPrefix(ctx context.Context, userID int, oldURI, newURI string) error { + oldURI = strings.TrimSuffix(oldURI, "/") + newURI = strings.TrimSuffix(newURI, "/") + if oldURI == newURI || userID == 0 { + return nil + } + + accounts, err := c.client.DavAccount.Query(). + Where( + davaccount.OwnerIDEQ(userID), + davaccount.Or( + davaccount.URIEQ(oldURI), + davaccount.URIHasPrefix(oldURI+"/"), + ), + ). + All(ctx) + if err != nil { + return fmt.Errorf("failed to query dav accounts for URI rewrite: %w", err) + } + + for _, account := range accounts { + updated := strings.TrimSuffix(newURI+strings.TrimPrefix(account.URI, oldURI), "/") + if err := c.client.DavAccount.UpdateOneID(account.ID).SetURI(updated).Exec(ctx); err != nil { + return fmt.Errorf("failed to rewrite dav account URI: %w", err) + } + } + return nil +} + func (c *davAccountClient) List(ctx context.Context, args *ListDavAccountArgs) (*ListDavAccountResult, error) { query := c.listQuery(args) diff --git a/inventory/dav_account_test.go b/inventory/dav_account_test.go new file mode 100644 index 00000000..204e94f2 --- /dev/null +++ b/inventory/dav_account_test.go @@ -0,0 +1,67 @@ +package inventory + +import ( + "context" + "fmt" + "testing" + + "github.com/cloudreve/Cloudreve/v4/ent/enttest" + "github.com/cloudreve/Cloudreve/v4/pkg/boolset" + "github.com/stretchr/testify/require" +) + +func TestUpdateURIPrefix(t *testing.T) { + client := enttest.Open(t, "sqlite3", "file:"+t.Name()+"?mode=memory&cache=shared") + t.Cleanup(func() { require.NoError(t, client.Close()) }) + ctx := context.Background() + + group := client.Group.Create().SetName("g").SetPermissions(&boolset.BooleanSet{}).SaveX(ctx) + owner := client.User.Create().SetEmail("dav@example.com").SetNick("dav").SetGroup(group).SaveX(ctx) + other := client.User.Create().SetEmail("other@example.com").SetNick("other").SetGroup(group).SaveX(ctx) + + seq := 0 + mk := func(userID int, uri string) { + seq++ + client.DavAccount.Create(). + SetName("mount"). + SetURI(uri). + SetPassword(fmt.Sprintf("pw-%d", seq)). + SetOptions(&boolset.BooleanSet{}). + SetOwnerID(userID). + SaveX(ctx) + } + getURIs := func(userID int) []string { + accounts, err := client.DavAccount.Query().All(ctx) + require.NoError(t, err) + var uris []string + for _, a := range accounts { + if a.OwnerID == userID { + uris = append(uris, a.URI) + } + } + return uris + } + + mk(owner.ID, "my://hash/dir") + mk(owner.ID, "my://hash/dir/sub") + mk(owner.ID, "my://hash/dir-other") + mk(other.ID, "my://hash/dir") + + c := NewDavAccountClient(client, "sqlite3", nil) + require.NoError(t, c.UpdateURIPrefix(ctx, owner.ID, "my://hash/dir", "my://hash/moved/dir")) + + got := getURIs(owner.ID) + require.ElementsMatch(t, []string{"my://hash/moved/dir", "my://hash/moved/dir/sub", "my://hash/dir-other"}, got) + // Other user's account untouched. + require.Equal(t, []string{"my://hash/dir"}, getURIs(other.ID)) + + // Trailing-slash stored URI is normalized and rewritten. + mk(owner.ID, "my://hash/base/") + require.NoError(t, c.UpdateURIPrefix(ctx, owner.ID, "my://hash/base/", "my://hash/base2")) + require.Contains(t, getURIs(owner.ID), "my://hash/base2") + + // Same-URI no-op and empty results are clean. + require.NoError(t, c.UpdateURIPrefix(ctx, owner.ID, "my://hash/x", "my://hash/x")) + require.NoError(t, c.UpdateURIPrefix(ctx, owner.ID, "my://hash/missing", "my://hash/y")) + require.NoError(t, c.UpdateURIPrefix(ctx, 0, "my://hash/dir", "my://hash/z")) +} diff --git a/pkg/filemanager/manager/operation.go b/pkg/filemanager/manager/operation.go index ca87f280..5097f5b5 100644 --- a/pkg/filemanager/manager/operation.go +++ b/pkg/filemanager/manager/operation.go @@ -140,7 +140,15 @@ func (m *manager) Create(ctx context.Context, path *fs.URI, fileType types.FileT func (m *manager) Rename(ctx context.Context, path *fs.URI, newName string) (fs.File, error) { file, indexDiff, err := m.fs.Rename(ctx, path, newName) m.processIndexDiff(ctx, indexDiff) - return file, err + if err != nil { + return file, err + } + + // Keep WebDAV mounts bound under the renamed path pointing at its new URI. + // Scope to the file's owner — the URI authority carries the owner's hash, + // which may differ from the requester for admin-on-behalf operations. + m.rewriteDavMounts(ctx, file.OwnerID(), path, file.Uri(false)) + return file, nil } func (m *manager) MoveOrCopy(ctx context.Context, src []*fs.URI, dst *fs.URI, isCopy bool) error { @@ -150,9 +158,28 @@ func (m *manager) MoveOrCopy(ctx context.Context, src []*fs.URI, dst *fs.URI, is } m.processIndexDiff(ctx, indexDiff) + + // Keep WebDAV mounts bound under moved paths pointing at their new URIs. + if !isCopy && m.user != nil { + for _, srcURI := range src { + m.rewriteDavMounts(ctx, m.user.ID, srcURI, dst.JoinRaw(srcURI.Name())) + } + } return nil } +// rewriteDavMounts repoints the given user's dav account mount URIs that +// reference oldURI (or any path under it) to newURI. Failures are logged but +// never fail the file operation itself. +func (m *manager) rewriteDavMounts(ctx context.Context, ownerID int, oldURI, newURI *fs.URI) { + if ownerID == 0 || oldURI == nil || newURI == nil { + return + } + if err := m.dep.DavAccountClient().UpdateURIPrefix(ctx, ownerID, oldURI.String(), newURI.String()); err != nil { + m.l.Warning("failed to update dav account URIs after file operation: %s", err) + } +} + func (m *manager) SoftDelete(ctx context.Context, path ...*fs.URI) error { return m.fs.SoftDelete(ctx, path...) }