package dbfs import ( "context" "errors" "fmt" "strconv" "strings" "time" "github.com/cloudreve/Cloudreve/v4/ent" "github.com/cloudreve/Cloudreve/v4/inventory/types" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/lock" "github.com/cloudreve/Cloudreve/v4/pkg/hashid" "github.com/samber/lo" ) type ( LockSession struct { Tokens map[string]string TokenStack [][]string } LockByPath struct { Uri *fs.URI ClosestAncestor *File Type types.FileType Token string } AlwaysIncludeTokenCtx struct{} ) func (f *DBFS) ConfirmLock(ctx context.Context, ancestor fs.File, uri *fs.URI, token ...string) (func(), fs.LockSession, error) { session := LockSessionFromCtx(ctx) lockUri := ancestor.RootUri().JoinRaw(uri.PathTrimmed()) ns, root, lKey := lockTupleFromUri(lockUri, f.user, f.hasher) lc := lock.LockInfo{ Ns: ns, Root: root, Token: token, } // Skip if already locked in current session if _, ok := session.Tokens[lKey]; ok { return func() {}, session, nil } release, tokenHit, err := f.ls.Confirm(time.Now(), lc) if err != nil { return nil, nil, err } session.Tokens[lKey] = tokenHit stackIndex := len(session.TokenStack) - 1 session.TokenStack[stackIndex] = append(session.TokenStack[stackIndex], lKey) return release, session, nil } func (f *DBFS) Lock(ctx context.Context, d time.Duration, requester *ent.User, zeroDepth bool, application lock.Application, uri *fs.URI, token string) (fs.LockSession, error) { // Get navigator navigator, err := f.getNavigator(ctx, uri, NavigatorCapabilityLockFile) if err != nil { return nil, err } ancestor, err := f.getFileByPath(ctx, navigator, uri) if err != nil && !ent.IsNotFound(err) { return nil, fmt.Errorf("failed to get ancestor: %w", err) } if ancestor.IsRootFolder() && ancestor.Uri(false).IsSame(uri, hashid.EncodeUserID(f.hasher, f.user.ID)) { return nil, fs.ErrNotSupportedAction.WithError(fmt.Errorf("cannot lock root folder")) } // Lock require create or update permission if _, ok := ctx.Value(ByPassOwnerCheckCtxKey{}).(bool); !ok && ancestor.Owner().ID != requester.ID { return nil, fs.ErrOwnerOnly } t := types.FileTypeFile if ancestor.Uri(false).IsSame(uri, hashid.EncodeUserID(f.hasher, f.user.ID)) { t = ancestor.Type() } lr := &LockByPath{ Uri: ancestor.RootUri().JoinRaw(uri.PathTrimmed()), ClosestAncestor: ancestor, Type: t, Token: token, } ls, err := f.acquireByPath(ctx, d, requester, zeroDepth, application, lr) if err != nil { return nil, err } return ls, nil } func (f *DBFS) Unlock(ctx context.Context, tokens ...string) error { return f.ls.Unlock(time.Now(), tokens...) } func (f *DBFS) Refresh(ctx context.Context, d time.Duration, token string) (lock.LockDetails, error) { return f.ls.Refresh(time.Now(), d, token) } func (f *DBFS) acquireByPath(ctx context.Context, duration time.Duration, requester *ent.User, zeroDepth bool, application lock.Application, locks ...*LockByPath) (*LockSession, error) { session := LockSessionFromCtx(ctx) // Prepare lock details for each file lockDetails := make([]lock.LockDetails, 0, len(locks)) lockedRequest := make([]*LockByPath, 0, len(locks)) for _, l := range locks { ns, root, lKey := lockTupleFromUri(l.Uri, f.user, f.hasher) ld := lock.LockDetails{ Owner: lock.Owner{ Application: application, }, Ns: ns, Root: root, ZeroDepth: zeroDepth, Duration: duration, Type: l.Type, Token: l.Token, } // Skip if already locked in current session if _, ok := session.Tokens[lKey]; ok { continue } lockDetails = append(lockDetails, ld) lockedRequest = append(lockedRequest, l) } // Acquire lock tokens, err := f.ls.Create(time.Now(), lockDetails...) if len(tokens) > 0 { for i, token := range tokens { key := lockDetails[i].Key() session.Tokens[key] = token stackIndex := len(session.TokenStack) - 1 session.TokenStack[stackIndex] = append(session.TokenStack[stackIndex], key) } } if err != nil { var conflicts lock.ConflictError if errors.As(err, &conflicts) { // Conflict with existing lock, generate user-friendly error message conflicts = lo.Map(conflicts, func(c *lock.ConflictDetail, index int) *lock.ConflictDetail { lr := lockedRequest[c.Index] if lr.ClosestAncestor.Root().Model.OwnerID == requester.ID { // Add absolute path for owner issued lock request c.Path = newMyUri().JoinRaw(c.Path).String() return c } // Hide token for non-owner requester if v, ok := ctx.Value(AlwaysIncludeTokenCtx{}).(bool); !ok || !v { c.Token = "" } // If conflicted resources still under user root, expose the relative path userRoot := lr.ClosestAncestor.UserRoot() userRootPath := userRoot.Uri(true).Path() if strings.HasPrefix(c.Path, userRootPath) { c.Path = userRoot. Uri(false). Join(strings.Split(strings.TrimPrefix(c.Path, userRootPath), fs.Separator)...).String() return c } // Hide sensitive information for non-owner issued lock request c.Path = "" return c }) return session, fs.ErrLockConflict.WithError(conflicts) } return session, fmt.Errorf("faield to create lock: %w", err) } // Check if any ancestor is modified during `getFileByPath` and `lock`. if err := f.ensureConsistency( ctx, lo.Map(lockedRequest, func(item *LockByPath, index int) *File { return item.ClosestAncestor })..., ); err != nil { return session, err } return session, nil } func (f *DBFS) Release(ctx context.Context, session *LockSession) error { if session == nil { return nil } stackIndex := len(session.TokenStack) - 1 err := f.ls.Unlock(time.Now(), lo.Map(session.TokenStack[stackIndex], func(key string, index int) string { return session.Tokens[key] })...) if err == nil { for _, key := range session.TokenStack[stackIndex] { delete(session.Tokens, key) } session.TokenStack = session.TokenStack[:len(session.TokenStack)-1] } return err } // ensureConsistency queries database for all given files and its ancestors, make sure there's no modification in // between. This is to make sure there's no modification between navigator's first query and lock acquisition. func (f *DBFS) ensureConsistency(ctx context.Context, files ...*File) error { if len(files) == 0 { return nil } // Generate a list of unique files (include ancestors) to check uniqueFiles := make(map[int]*File) for _, file := range files { for root := file; root != nil; root = root.Parent { if _, ok := uniqueFiles[root.Model.ID]; ok { // This file and its ancestors are already included break } uniqueFiles[root.Model.ID] = root } } page := 0 fileIds := lo.Keys(uniqueFiles) for page >= 0 { files, next, err := f.fileClient.GetByIDs(ctx, fileIds, page) if err != nil { return fmt.Errorf("failed to check file consistency: %w", err) } for _, file := range files { latest := uniqueFiles[file.ID].Model if file.Name != latest.Name || file.FileChildren != latest.FileChildren || file.OwnerID != latest.OwnerID || file.Type != latest.Type { return fs.ErrModified. WithError(fmt.Errorf("file %s has been modified before lock acquisition", file.Name)) } } page = next } return nil } // LockSessionFromCtx retrieves lock session from context. If no lock session // found, a new empty lock session will be returned. func LockSessionFromCtx(ctx context.Context) *LockSession { l, _ := ctx.Value(fs.LockSessionCtxKey{}).(*LockSession) if l == nil { ls := &LockSession{ Tokens: make(map[string]string), TokenStack: make([][]string, 0), } l = ls } l.TokenStack = append(l.TokenStack, make([]string, 0)) return l } // Exclude removes lock from session, so that it won't be released. func (l *LockSession) Exclude(lock *LockByPath, u *ent.User, hasher hashid.Encoder) string { _, _, lKey := lockTupleFromUri(lock.Uri, u, hasher) foundInCurrentStack := false token, found := l.Tokens[lKey] if found { stackIndex := len(l.TokenStack) - 1 l.TokenStack[stackIndex] = lo.Filter(l.TokenStack[stackIndex], func(t string, index int) bool { if t == lKey { foundInCurrentStack = true } return t != lKey }) if foundInCurrentStack { delete(l.Tokens, lKey) return token } } return "" } func (l *LockSession) LastToken() string { stackIndex := len(l.TokenStack) - 1 if len(l.TokenStack[stackIndex]) == 0 { return "" } return l.Tokens[l.TokenStack[stackIndex][len(l.TokenStack[stackIndex])-1]] } // WithAlwaysIncludeToken returns a new context with a flag to always include token in conflic response. func WithAlwaysIncludeToken(ctx context.Context) context.Context { return context.WithValue(ctx, AlwaysIncludeTokenCtx{}, true) } func lockTupleFromUri(uri *fs.URI, u *ent.User, hasher hashid.Encoder) (string, string, string) { id := uri.ID(hashid.EncodeUserID(hasher, u.ID)) if id == "" { id = strconv.Itoa(u.ID) } ns := fmt.Sprintf(id + "/" + string(uri.FileSystem())) root := uri.Path() return ns, root, ns + "/" + root }