Init V4 community edition (#2265)
* Init V4 community edition * Init V4 community edition
This commit is contained in:
325
pkg/filemanager/fs/dbfs/lock.go
Normal file
325
pkg/filemanager/fs/dbfs/lock.go
Normal file
@@ -0,0 +1,325 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user