fix(dav): repoint account mount URIs on rename and move (#2878)

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>
pull/3582/head
Tomas Dvorak 2 weeks ago
parent a82c9f5277
commit 9e62f03489

@ -10,6 +10,7 @@ import (
"github.com/cloudreve/Cloudreve/v4/pkg/conf" "github.com/cloudreve/Cloudreve/v4/pkg/conf"
"github.com/cloudreve/Cloudreve/v4/pkg/hashid" "github.com/cloudreve/Cloudreve/v4/pkg/hashid"
"github.com/samber/lo" "github.com/samber/lo"
"strings"
) )
type ( type (
@ -25,6 +26,11 @@ type (
GetByIDAndUserID(ctx context.Context, id, userID int) (*ent.DavAccount, error) GetByIDAndUserID(ctx context.Context, id, userID int) (*ent.DavAccount, error)
// Delete deletes the dav account. // Delete deletes the dav account.
Delete(ctx context.Context, id int) error 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 { ListDavAccountArgs struct {
@ -96,6 +102,35 @@ func (c *davAccountClient) Delete(ctx context.Context, id int) error {
return c.client.DavAccount.DeleteOneID(id).Exec(ctx) 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) { func (c *davAccountClient) List(ctx context.Context, args *ListDavAccountArgs) (*ListDavAccountResult, error) {
query := c.listQuery(args) query := c.listQuery(args)

@ -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"))
}

@ -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) { func (m *manager) Rename(ctx context.Context, path *fs.URI, newName string) (fs.File, error) {
file, indexDiff, err := m.fs.Rename(ctx, path, newName) file, indexDiff, err := m.fs.Rename(ctx, path, newName)
m.processIndexDiff(ctx, indexDiff) 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 { 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) 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 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 { func (m *manager) SoftDelete(ctx context.Context, path ...*fs.URI) error {
return m.fs.SoftDelete(ctx, path...) return m.fs.SoftDelete(ctx, path...)
} }

Loading…
Cancel
Save