mirror of https://github.com/cloudreve/Cloudreve
326 lines
9.0 KiB
Go
326 lines
9.0 KiB
Go
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
|
|
}
|