Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions internal/conf/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,4 +196,5 @@ const (
PathKey
SharingIDKey
SkipHookKey
SkipNoOverwriteKey
)
2 changes: 1 addition & 1 deletion internal/db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ var db *gorm.DB

func Init(d *gorm.DB) {
db = d
err := AutoMigrate(new(model.Storage), new(model.User), new(model.Meta), new(model.SettingItem), new(model.SearchNode), new(model.TaskItem), new(model.SSHPublicKey), new(model.SharingDB))
err := AutoMigrate(new(model.Storage), new(model.User), new(model.Meta), new(model.SettingItem), new(model.SearchNode), new(model.TaskItem), new(model.SSHPublicKey), new(model.SharingDB), new(model.WebDAVProperty))
if err != nil {
log.Fatalf("failed migrate database: %s", err.Error())
}
Expand Down
106 changes: 97 additions & 9 deletions internal/fs/copy_move.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"time"

"github.com/OpenListTeam/OpenList/v4/internal/conf"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/errs"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/op"
Expand All @@ -16,7 +17,9 @@ import (
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/OpenList/v4/server/common"
"github.com/OpenListTeam/tache"
"github.com/google/uuid"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)

type taskType uint8
Expand Down Expand Up @@ -100,7 +103,72 @@ func (t *FileTransferTask) SetRetry(retry int, maxRetry int) {
}
}

func transfer(ctx context.Context, taskType taskType, srcObjPath, dstDirPath string, skipHook ...bool) (task.TaskExtensionInfo, error) {
func supportsNamedNativeCopy(storage driver.Driver) bool {
_, hasMkdir := storage.(driver.Mkdir)
if !hasMkdir {
_, hasMkdir = storage.(driver.MkdirResult)
}
_, hasCopy := storage.(driver.Copy)
if !hasCopy {
_, hasCopy = storage.(driver.CopyResult)
}
_, hasMove := storage.(driver.Move)
if !hasMove {
_, hasMove = storage.(driver.MoveResult)
}
_, hasRename := storage.(driver.Rename)
if !hasRename {
_, hasRename = storage.(driver.RenameResult)
}
_, hasRemove := storage.(driver.Remove)
return hasMkdir && hasCopy && hasMove && hasRename && hasRemove
}

func namedCopyStageObjectPath(stageDirPath, srcPath, dstName string) string {
name := stdpath.Base(srcPath)
if dstName != "" {
name = dstName
}
return stdpath.Join(stageDirPath, name)
}

func copyNamedInStorage(ctx context.Context, storage driver.Driver, srcPath, dstDirPath, dstName string) error {
stageDirPath := stdpath.Join(dstDirPath, ".openlist-copy-"+uuid.NewString())
stageCtx := context.WithValue(ctx, conf.SkipHookKey, struct{}{})
if err := op.MakeDir(stageCtx, storage, stageDirPath); err != nil {
return errors.WithMessage(err, "failed create copy staging directory")
}
stageCreated := true
defer func() {
if stageCreated {
if err := op.Remove(stageCtx, storage, stageDirPath); err != nil {
log.Warnf("failed remove copy staging directory %s: %v", stageDirPath, err)
}
}
}()

if err := op.Copy(stageCtx, storage, srcPath, stageDirPath); err != nil {
return err
}
srcName := stdpath.Base(srcPath)
stageObjPath := namedCopyStageObjectPath(stageDirPath, srcPath, "")
if srcName != dstName {
if err := op.Rename(stageCtx, storage, stageObjPath, dstName); err != nil {
return err
}
stageObjPath = namedCopyStageObjectPath(stageDirPath, srcPath, dstName)
}
if err := op.Move(ctx, storage, stageObjPath, dstDirPath); err != nil {
return err
}
if err := op.Remove(stageCtx, storage, stageDirPath); err != nil {
return errors.WithMessage(err, "failed remove copy staging directory")
}
stageCreated = false
return nil
}

func transfer(ctx context.Context, taskType taskType, srcObjPath, dstDirPath, dstName string, skipHook ...bool) (task.TaskExtensionInfo, error) {
srcStorage, srcObjActualPath, err := op.GetStorageAndActualPath(srcObjPath)
if err != nil {
return nil, errors.WithMessage(err, "failed get src storage")
Expand All @@ -115,9 +183,18 @@ func transfer(ctx context.Context, taskType taskType, srcObjPath, dstDirPath str
ctx = context.WithValue(ctx, conf.SkipHookKey, struct{}{})
}
if taskType == copy || taskType == merge {
err = op.Copy(ctx, srcStorage, srcObjActualPath, dstDirActualPath)
if !errors.Is(err, errs.NotImplement) && !errors.Is(err, errs.NotSupport) {
return nil, err
if dstName != "" {
if supportsNamedNativeCopy(srcStorage) {
err = copyNamedInStorage(ctx, srcStorage, srcObjActualPath, dstDirActualPath, dstName)
if !errors.Is(err, errs.NotImplement) && !errors.Is(err, errs.NotSupport) {
return nil, err
}
}
} else {
err = op.Copy(ctx, srcStorage, srcObjActualPath, dstDirActualPath)
if !errors.Is(err, errs.NotImplement) && !errors.Is(err, errs.NotSupport) {
return nil, err
}
}
} else {
err = op.Move(ctx, srcStorage, srcObjActualPath, dstDirActualPath)
Expand All @@ -127,13 +204,15 @@ func transfer(ctx context.Context, taskType taskType, srcObjPath, dstDirPath str
}
}

// not in the same storage
// Use the transfer path when native copy cannot preserve the destination
// name or the source and destination storages differ.
t := &FileTransferTask{
TaskData: TaskData{
SrcStorage: srcStorage,
DstStorage: dstStorage,
SrcActualPath: srcObjActualPath,
DstActualPath: dstDirActualPath,
DstName: dstName,
SrcStorageMp: srcStorage.GetStorage().MountPath,
DstStorageMp: dstStorage.GetStorage().MountPath,
},
Expand Down Expand Up @@ -189,9 +268,14 @@ func (t *FileTransferTask) RunWithNextTaskCallback(f func(nextTask *FileTransfer
if err != nil {
return errors.WithMessagef(err, "failed list src [%s] objs", t.SrcActualPath)
}
dstActualPath := stdpath.Join(t.DstActualPath, srcObj.GetName())
task_group.TransferCoordinator.AppendPayload(t.groupID, task_group.DstPathToHook(dstActualPath))

dstName := srcObj.GetName()
if t.DstName != "" {
dstName = t.DstName
}
dstActualPath := stdpath.Join(t.DstActualPath, dstName)
if err := op.MakeDir(t.Ctx(), t.DstStorage, dstActualPath); err != nil {
return errors.WithMessagef(err, "failed create dst dir [%s]", dstActualPath)
}
existedObjs := make(map[string]bool)
if t.TaskType == merge {
dstObjs, err := op.List(t.Ctx(), t.DstStorage, dstActualPath, model.ListArgs{})
Expand Down Expand Up @@ -250,8 +334,12 @@ func (t *FileTransferTask) RunWithNextTaskCallback(f func(nextTask *FileTransfer
return errors.WithMessagef(err, "failed get [%s] link", t.SrcActualPath)
}
// any link provided is seekable
streamObj := srcObj
if t.DstName != "" {
streamObj = &model.ObjWrapName{Name: t.DstName, Obj: srcObj}
}
ss, err := stream.NewSeekableStream(&stream.FileStream{
Obj: srcObj,
Obj: streamObj,
Ctx: t.Ctx(),
}, link)
if err != nil {
Expand Down
23 changes: 23 additions & 0 deletions internal/fs/copy_move_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package fs

import "testing"

func TestNamedCopyStageObjectPath(t *testing.T) {
tests := []struct {
name string
stageDir string
srcPath string
dstName string
want string
}{
{name: "source basename", stageDir: "/dst/.stage", srcPath: "/src/foo", want: "/dst/.stage/foo"},
{name: "requested name", stageDir: "/dst/.stage", srcPath: "/src/foo", dstName: "bar", want: "/dst/.stage/bar"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := namedCopyStageObjectPath(tt.stageDir, tt.srcPath, tt.dstName); got != tt.want {
t.Fatalf("namedCopyStageObjectPath(%q, %q, %q) = %q, want %q", tt.stageDir, tt.srcPath, tt.dstName, got, tt.want)
}
})
}
}
15 changes: 12 additions & 3 deletions internal/fs/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,23 +69,32 @@ func MakeDir(ctx context.Context, path string) error {
}

func Move(ctx context.Context, srcPath, dstDirPath string, skipHook ...bool) (task.TaskExtensionInfo, error) {
req, err := transfer(ctx, move, srcPath, dstDirPath, skipHook...)
req, err := transfer(ctx, move, srcPath, dstDirPath, "", skipHook...)
if err != nil {
log.Errorf("failed move %s to %s: %+v", srcPath, dstDirPath, err)
}
return req, err
}

func Copy(ctx context.Context, srcObjPath, dstDirPath string, skipHook ...bool) (task.TaskExtensionInfo, error) {
res, err := transfer(ctx, copy, srcObjPath, dstDirPath, skipHook...)
res, err := transfer(ctx, copy, srcObjPath, dstDirPath, "", skipHook...)
if err != nil {
log.Errorf("failed copy %s to %s: %+v", srcObjPath, dstDirPath, err)
}
return res, err
}

// CopyTo copies a file or directory to dstDirPath using dstName as its name.
func CopyTo(ctx context.Context, srcObjPath, dstDirPath, dstName string, skipHook ...bool) (task.TaskExtensionInfo, error) {
res, err := transfer(ctx, copy, srcObjPath, dstDirPath, dstName, skipHook...)
if err != nil {
log.Errorf("failed copy %s to %s as %s: %+v", srcObjPath, dstDirPath, dstName, err)
}
return res, err
}

func Merge(ctx context.Context, srcObjPath, dstDirPath string, skipHook ...bool) (task.TaskExtensionInfo, error) {
res, err := transfer(ctx, merge, srcObjPath, dstDirPath, skipHook...)
res, err := transfer(ctx, merge, srcObjPath, dstDirPath, "", skipHook...)
if err != nil {
log.Errorf("failed merge %s to %s: %+v", srcObjPath, dstDirPath, err)
}
Expand Down
1 change: 1 addition & 0 deletions internal/fs/other.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ type TaskData struct {
Status string `json:"-"` //don't save status to save space
SrcActualPath string `json:"src_path"`
DstActualPath string `json:"dst_path"`
DstName string `json:"dst_name,omitempty"`
SrcStorage driver.Driver `json:"-"`
DstStorage driver.Driver `json:"-"`
SrcStorageMp string `json:"src_storage_mp"`
Expand Down
11 changes: 11 additions & 0 deletions internal/model/webdav_property.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package model

// WebDAVProperty stores a dead WebDAV property for a resource.
type WebDAVProperty struct {
ID uint `json:"id" gorm:"primaryKey"`
Path string `json:"path" gorm:"uniqueIndex:idx_webdav_property"`
Namespace string `json:"namespace" gorm:"uniqueIndex:idx_webdav_property"`
Name string `json:"name" gorm:"uniqueIndex:idx_webdav_property"`
Lang string `json:"lang"`
InnerXML []byte `json:"inner_xml"`
}
4 changes: 2 additions & 2 deletions internal/op/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,7 @@ func Copy(ctx context.Context, storage driver.Driver, srcPath, dstDirPath string
srcPath = utils.FixAndCleanPath(srcPath)
dstDirPath = utils.FixAndCleanPath(dstDirPath)
if dstDirPath == stdpath.Dir(srcPath) {
return errors.New("copy in place")
return errors.WithStack(errs.NotImplement)
}
srcRawObj, err := Get(ctx, storage, srcPath, true)
if err != nil {
Expand Down Expand Up @@ -632,7 +632,7 @@ func Put(ctx context.Context, storage driver.Driver, dstDirPath string, file mod
if err != nil {
return errors.WithMessagef(err, "while uploading, failed remove existing file which size = 0")
}
} else if storage.Config().NoOverwriteUpload {
} else if storage.Config().NoOverwriteUpload && ctx.Value(conf.SkipNoOverwriteKey) == nil {
// try to rename old obj
err = Rename(ctx, storage, dstPath, tempName)
if err != nil {
Expand Down
Loading