mirror of https://github.com/cloudreve/Cloudreve
433 lines
13 KiB
Go
433 lines
13 KiB
Go
package dbfs
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math"
|
|
"path"
|
|
"time"
|
|
|
|
"github.com/cloudreve/Cloudreve/v4/ent"
|
|
"github.com/cloudreve/Cloudreve/v4/inventory"
|
|
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
|
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
|
|
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
|
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
|
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
|
)
|
|
|
|
func (f *DBFS) PreValidateUpload(ctx context.Context, dst *fs.URI, files ...fs.PreValidateFile) error {
|
|
// Get navigator
|
|
navigator, err := f.getNavigator(ctx, dst, NavigatorCapabilityUploadFile, NavigatorCapabilityLockFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
dstFile, err := f.getFileByPath(ctx, navigator, dst)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get destination folder: %w", err)
|
|
}
|
|
|
|
if dstFile.Type() != types.FileTypeFolder {
|
|
return fmt.Errorf("destination is not a folder")
|
|
}
|
|
|
|
// check ownership
|
|
if f.user.ID != dstFile.OwnerID() {
|
|
return fmt.Errorf("failed to evaluate permission: %w", err)
|
|
}
|
|
|
|
total := int64(0)
|
|
for _, file := range files {
|
|
total += file.Size
|
|
}
|
|
|
|
// Get parent folder storage policy and performs validation
|
|
policy, err := f.getPreferredPolicy(ctx, dstFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// validate upload request
|
|
for _, file := range files {
|
|
if file.OmitName {
|
|
if err := validateFileSize(file.Size, policy); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if err := validateNewFile(path.Base(file.Name), file.Size, policy); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate available capacity
|
|
if err := f.validateUserCapacity(ctx, total, dstFile.Owner()); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (f *DBFS) PrepareUpload(ctx context.Context, req *fs.UploadRequest, opts ...fs.Option) (*fs.UploadSession, error) {
|
|
// Get navigator
|
|
navigator, err := f.getNavigator(ctx, req.Props.Uri, NavigatorCapabilityUploadFile, NavigatorCapabilityLockFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get most recent ancestor or target file
|
|
ctx = context.WithValue(ctx, inventory.LoadFileEntity{}, true)
|
|
ancestor, err := f.getFileByPath(ctx, navigator, req.Props.Uri)
|
|
if err != nil && !ent.IsNotFound(err) {
|
|
return nil, fmt.Errorf("failed to get ancestor: %w", err)
|
|
}
|
|
|
|
if ancestor.IsSymbolic() {
|
|
return nil, ErrSymbolicFolderFound
|
|
}
|
|
|
|
fileExisted := false
|
|
if ancestor.Uri(false).IsSame(req.Props.Uri, hashid.EncodeUserID(f.hasher, f.user.ID)) {
|
|
fileExisted = true
|
|
}
|
|
|
|
// If file already exist, and update operation is suspended or existing file is not a file
|
|
if fileExisted && (req.Props.EntityType == nil || ancestor.Type() != types.FileTypeFile) {
|
|
return nil, fs.ErrFileExisted
|
|
}
|
|
|
|
// If file not exist, only empty entity / version entity is allowed
|
|
if !fileExisted && (req.Props.EntityType != nil && *req.Props.EntityType != types.EntityTypeVersion) {
|
|
return nil, fs.ErrPathNotExist
|
|
}
|
|
|
|
if _, ok := ctx.Value(ByPassOwnerCheckCtxKey{}).(bool); !ok && ancestor.OwnerID() != f.user.ID {
|
|
return nil, fs.ErrOwnerOnly
|
|
}
|
|
|
|
// Lock target
|
|
lockedPath := ancestor.RootUri().JoinRaw(req.Props.Uri.PathTrimmed())
|
|
lr := &LockByPath{lockedPath, ancestor, types.FileTypeFile, ""}
|
|
ls, err := f.acquireByPath(ctx, time.Until(req.Props.ExpireAt), f.user, false, fs.LockApp(fs.ApplicationUpload), lr)
|
|
defer func() { _ = f.Release(ctx, ls) }()
|
|
ctx = fs.LockSessionToContext(ctx, ls)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get parent folder storage policy and performs validation
|
|
var (
|
|
policy *ent.StoragePolicy
|
|
)
|
|
if req.ImportFrom == nil {
|
|
policy, err = f.getPreferredPolicy(ctx, ancestor)
|
|
} else {
|
|
policy, err = f.storagePolicyClient.GetPolicyByID(ctx, req.Props.PreferredStoragePolicy)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// validate upload request
|
|
if err := validateNewFile(req.Props.Uri.Name(), req.Props.Size, policy); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Validate available capacity
|
|
if err := f.validateUserCapacity(ctx, req.Props.Size, ancestor.Owner()); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Generate save path by storage policy
|
|
isThumbnailAndPolicyNotAvailable := policy.ID != ancestor.Model.StoragePolicyFiles &&
|
|
(req.Props.EntityType != nil && *req.Props.EntityType == types.EntityTypeThumbnail) &&
|
|
req.ImportFrom == nil
|
|
if req.Props.SavePath == "" || isThumbnailAndPolicyNotAvailable {
|
|
req.Props.SavePath = generateSavePath(policy, req, f.user)
|
|
if isThumbnailAndPolicyNotAvailable {
|
|
req.Props.SavePath = fmt.Sprintf(
|
|
"%s.%s%s",
|
|
req.Props.SavePath,
|
|
util.RandStringRunes(16),
|
|
f.settingClient.ThumbEntitySuffix(ctx))
|
|
}
|
|
}
|
|
|
|
// Create upload placeholder
|
|
var (
|
|
fileId int
|
|
entityId int
|
|
lockToken string
|
|
targetFile *ent.File
|
|
)
|
|
fc, dbTx, ctx, err := inventory.WithTx(ctx, f.fileClient)
|
|
if err != nil {
|
|
return nil, serializer.NewError(serializer.CodeDBError, "Failed to start transaction", err)
|
|
}
|
|
|
|
if fileExisted {
|
|
entityType := types.EntityTypeVersion
|
|
if req.Props.EntityType != nil {
|
|
entityType = *req.Props.EntityType
|
|
}
|
|
entity, err := f.CreateEntity(ctx, ancestor, policy, entityType, req,
|
|
WithPreviousVersion(req.Props.PreviousVersion),
|
|
fs.WithUploadRequest(req),
|
|
WithRemoveStaleEntities(),
|
|
)
|
|
if err != nil {
|
|
_ = inventory.Rollback(dbTx)
|
|
return nil, fmt.Errorf("failed to create new entity: %w", err)
|
|
}
|
|
fileId = ancestor.ID()
|
|
entityId = entity.ID()
|
|
targetFile = ancestor.Model
|
|
} else {
|
|
uploadPlaceholder, err := f.Create(ctx, req.Props.Uri, types.FileTypeFile,
|
|
fs.WithUploadRequest(req),
|
|
WithPreferredStoragePolicy(policy),
|
|
WithErrorOnConflict(),
|
|
WithAncestor(ancestor),
|
|
)
|
|
if err != nil {
|
|
_ = inventory.Rollback(dbTx)
|
|
return nil, fmt.Errorf("failed to create upload placeholder: %w", err)
|
|
}
|
|
|
|
fileId = uploadPlaceholder.ID()
|
|
entityId = uploadPlaceholder.Entities()[0].ID()
|
|
targetFile = uploadPlaceholder.(*File).Model
|
|
}
|
|
|
|
if req.ImportFrom == nil {
|
|
// If not importing, we can keep the lock
|
|
lockToken = ls.Exclude(lr, f.user, f.hasher)
|
|
|
|
// create metadata to record uploading entity id
|
|
if err := fc.UpsertMetadata(ctx, targetFile, map[string]string{
|
|
MetadataUploadSessionID: req.Props.UploadSessionID,
|
|
}, nil); err != nil {
|
|
_ = inventory.Rollback(dbTx)
|
|
return nil, serializer.NewError(serializer.CodeDBError, "Failed to update upload session metadata", err)
|
|
}
|
|
}
|
|
|
|
if err := inventory.CommitWithStorageDiff(ctx, dbTx, f.l, f.userClient); err != nil {
|
|
return nil, serializer.NewError(serializer.CodeDBError, "Failed to commit file upload preparation", err)
|
|
}
|
|
|
|
session := &fs.UploadSession{
|
|
Props: &fs.UploadProps{
|
|
Uri: req.Props.Uri,
|
|
Size: req.Props.Size,
|
|
SavePath: req.Props.SavePath,
|
|
LastModified: req.Props.LastModified,
|
|
UploadSessionID: req.Props.UploadSessionID,
|
|
ExpireAt: req.Props.ExpireAt,
|
|
EntityType: req.Props.EntityType,
|
|
Metadata: req.Props.Metadata,
|
|
},
|
|
FileID: fileId,
|
|
NewFileCreated: !fileExisted,
|
|
Importing: req.ImportFrom != nil,
|
|
EntityID: entityId,
|
|
UID: f.user.ID,
|
|
Policy: policy,
|
|
CallbackSecret: util.RandStringRunes(32),
|
|
LockToken: lockToken, // Prevent lock being released.
|
|
}
|
|
|
|
// TODO: frontend should create new upload session if resumed session does not exist.
|
|
return session, nil
|
|
}
|
|
|
|
func (f *DBFS) CompleteUpload(ctx context.Context, session *fs.UploadSession) (fs.File, error) {
|
|
// Get placeholder file
|
|
file, err := f.Get(ctx, session.Props.Uri, WithFileEntities(), WithNotRoot())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get placeholder file: %w", err)
|
|
}
|
|
|
|
filePrivate := file.(*File)
|
|
|
|
// Confirm locks on placeholder file
|
|
if session.LockToken != "" {
|
|
release, ls, err := f.ConfirmLock(ctx, file, file.Uri(false), session.LockToken)
|
|
if err != nil {
|
|
return nil, fs.ErrLockExpired.WithError(err)
|
|
}
|
|
|
|
release()
|
|
ctx = fs.LockSessionToContext(ctx, ls)
|
|
}
|
|
|
|
// Update placeholder entity to actual desired entity
|
|
entityType := types.EntityTypeVersion
|
|
if session.Props.EntityType != nil {
|
|
entityType = *session.Props.EntityType
|
|
}
|
|
|
|
// Check version retention policy
|
|
owner := filePrivate.Owner()
|
|
// Max allowed versions
|
|
maxVersions := 1
|
|
if entityType == types.EntityTypeVersion &&
|
|
owner.Settings.VersionRetention &&
|
|
(len(owner.Settings.VersionRetentionExt) == 0 || util.IsInExtensionList(owner.Settings.VersionRetentionExt, file.Name())) {
|
|
// Retention is enabled for this file
|
|
maxVersions = owner.Settings.VersionRetentionMax
|
|
if maxVersions == 0 {
|
|
// Unlimited versions
|
|
maxVersions = math.MaxInt32
|
|
}
|
|
}
|
|
|
|
// Start transaction to update file
|
|
fc, tx, ctx, err := inventory.WithTx(ctx, f.fileClient)
|
|
if err != nil {
|
|
return nil, serializer.NewError(serializer.CodeDBError, "Failed to start transaction", err)
|
|
}
|
|
|
|
err = fc.UpgradePlaceholder(ctx, filePrivate.Model, session.Props.LastModified, session.EntityID, entityType)
|
|
if err != nil {
|
|
_ = inventory.Rollback(tx)
|
|
return nil, serializer.NewError(serializer.CodeDBError, "Failed to update placeholder file", err)
|
|
}
|
|
|
|
// Remove metadata that are defined in upload session
|
|
err = fc.RemoveMetadata(ctx, filePrivate.Model, MetadataUploadSessionID, ThumbDisabledKey)
|
|
if err != nil {
|
|
_ = inventory.Rollback(tx)
|
|
return nil, serializer.NewError(serializer.CodeDBError, "Failed to update placeholder metadata", err)
|
|
}
|
|
|
|
if len(session.Props.Metadata) > 0 {
|
|
if err := fc.UpsertMetadata(ctx, filePrivate.Model, session.Props.Metadata, nil); err != nil {
|
|
_ = inventory.Rollback(tx)
|
|
return nil, serializer.NewError(serializer.CodeDBError, "Failed to upsert placeholder metadata", err)
|
|
}
|
|
}
|
|
|
|
diff, err := fc.CapEntities(ctx, filePrivate.Model, owner, maxVersions, entityType)
|
|
if err != nil {
|
|
_ = inventory.Rollback(tx)
|
|
return nil, serializer.NewError(serializer.CodeDBError, "Failed to cap version entities", err)
|
|
}
|
|
tx.AppendStorageDiff(diff)
|
|
|
|
if entityType == types.EntityTypeVersion {
|
|
// If updating version entity, we need to cap all existing thumbnail entity to let it re-generate.
|
|
diff, err = fc.CapEntities(ctx, filePrivate.Model, owner, 0, types.EntityTypeThumbnail)
|
|
if err != nil {
|
|
_ = inventory.Rollback(tx)
|
|
return nil, serializer.NewError(serializer.CodeDBError, "Failed to cap thumbnail entities", err)
|
|
}
|
|
|
|
tx.AppendStorageDiff(diff)
|
|
}
|
|
|
|
if err := inventory.CommitWithStorageDiff(ctx, tx, f.l, f.userClient); err != nil {
|
|
return nil, serializer.NewError(serializer.CodeDBError, "Failed to commit file change", err)
|
|
}
|
|
|
|
// Unlock file
|
|
if session.LockToken != "" {
|
|
if err := f.ls.Unlock(time.Now(), session.LockToken); err != nil {
|
|
return nil, serializer.NewError(serializer.CodeLockConflict, "Failed to unlock file", err)
|
|
}
|
|
}
|
|
|
|
file, err = f.Get(ctx, session.Props.Uri, WithFileEntities(), WithNotRoot())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get updated file: %w", err)
|
|
}
|
|
|
|
return file, nil
|
|
}
|
|
|
|
// This function will be used:
|
|
// - File still locked by uplaod session
|
|
// - File unlocked, upload session valid
|
|
// - File unlocked, upload session not valid
|
|
func (f *DBFS) CancelUploadSession(ctx context.Context, path *fs.URI, sessionID string, session *fs.UploadSession) ([]fs.Entity, error) {
|
|
// Get placeholder file
|
|
file, err := f.Get(ctx, path, WithFileEntities(), WithNotRoot())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get placeholder file: %w", err)
|
|
}
|
|
|
|
filePrivate := file.(*File)
|
|
|
|
// Make sure presented upload session is valid
|
|
if session != nil && (session.UID != f.user.ID || session.FileID != file.ID()) {
|
|
return nil, serializer.NewError(serializer.CodeNotFound, "Upload session not found", nil)
|
|
}
|
|
|
|
// Confirm locks on placeholder file
|
|
if session != nil && session.LockToken != "" {
|
|
release, ls, err := f.ConfirmLock(ctx, file, file.Uri(false), session.LockToken)
|
|
if err == nil {
|
|
release()
|
|
ctx = fs.LockSessionToContext(ctx, ls)
|
|
}
|
|
}
|
|
|
|
if _, ok := ctx.Value(ByPassOwnerCheckCtxKey{}).(bool); !ok && filePrivate.OwnerID() != f.user.ID {
|
|
return nil, fs.ErrOwnerOnly
|
|
}
|
|
|
|
// Lock file
|
|
ls, err := f.acquireByPath(ctx, -1, f.user, true, fs.LockApp(fs.ApplicationUpload),
|
|
&LockByPath{filePrivate.Uri(true), filePrivate, filePrivate.Type(), ""})
|
|
defer func() { _ = f.Release(ctx, ls) }()
|
|
ctx = fs.LockSessionToContext(ctx, ls)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Find placeholder entity
|
|
var entity fs.Entity
|
|
for _, e := range filePrivate.Entities() {
|
|
if sid := e.UploadSessionID(); sid != nil && sid.String() == sessionID {
|
|
entity = e
|
|
break
|
|
}
|
|
}
|
|
|
|
// Remove upload session metadata
|
|
if err := f.fileClient.RemoveMetadata(ctx, filePrivate.Model, MetadataUploadSessionID, ThumbDisabledKey); err != nil {
|
|
return nil, serializer.NewError(serializer.CodeDBError, "Failed to remove upload session metadata", err)
|
|
}
|
|
|
|
if entity == nil {
|
|
// Given upload session does not exist
|
|
return nil, nil
|
|
}
|
|
|
|
if session != nil && session.LockToken != "" {
|
|
defer func() {
|
|
if err := f.ls.Unlock(time.Now(), session.LockToken); err != nil {
|
|
f.l.Warning("Failed to unlock file %q: %s", filePrivate.Uri(true).String(), err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
if len(filePrivate.Entities()) == 1 {
|
|
// Only one placeholder entity, just delete this file
|
|
return f.Delete(ctx, []*fs.URI{path})
|
|
}
|
|
|
|
// Delete place holder entity
|
|
storageDiff, err := f.deleteEntity(ctx, filePrivate, entity.ID())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to delete placeholder entity: %w", err)
|
|
}
|
|
|
|
if err := f.userClient.ApplyStorageDiff(ctx, storageDiff); err != nil {
|
|
return nil, fmt.Errorf("failed to apply storage diff: %w", err)
|
|
}
|
|
|
|
return nil, nil
|
|
}
|