Repository: AlistGo/alist (language: go)

Issue:
通过先复制后删除移动文件
### Please make sure of the following things

- [X] I have read the [documentation](https://alist.nn.ci).
- [X] I'm sure there are no duplicate issues or discussions.
- [X] I'm sure this feature is not implemented.
- [X] I'm sure it's a reasonable and popular requirement.

### Description of the feature / 需求描述

SFTP等存储，无法在跨越文件系统的路径之间rename

### Suggested solution / 实现思路

可以通过先复制后删除移动文件。

### Additional context / 附件

_No response_


Discussion / hints:
### See
1. 57% #5292

<!-- Created by actions-cool/issues-similarity-analysis. Do not remove. -->
rclone可以实现
在同一个文件系统内移动文件是原子操作，能瞬间完成并返回的，跨文件系统移动需要完整复制文件内容，如果文件太大的话客户端就会超时，所以你这个需求只能是把移动操作添加为复制任务，但是复制任务是不支持完成后删除源文件的，不然跨网盘移动也不会提示“使用复制操作”了。因此我建议在这样的场景中使用复制，复制完成后手动删除源文件
但是复制也需要花费很长时间，而复制后删除源文件应该也是一瞬间吧？只需要在复制任务上加个flag是不是就可以了。
> 但是复制也需要花费很长时间，而复制后删除源文件应该也是一瞬间吧？只需要在复制任务上加个flag是不是就可以了。

不太行，因为复制文件夹的任务的执行逻辑是，首先在目标位置新建文件夹，然后对于源文件夹的每个子文件/文件夹，创建一个单独的复制任务，最后之间结束。它不会检查这些子项复制任务全部完成的时机然后去删除源文件夹，最后能做到的最好的效果就是在原位置留下目录结构。

我直接把跨文件系统的移动指令当成复制处理算了，本来开源项目就不能保证复制过程是绝对没有问题的，万一复制出来的文件是有问题的，结果它还自动把源文件删了就不好了

Fix patch:
diff --git a/server/ftp/fsmanage.go b/server/ftp/fsmanage.go
index 5199a473b5b..fb03c1b95cb 100644
--- a/server/ftp/fsmanage.go
+++ b/server/ftp/fsmanage.go
@@ -2,6 +2,7 @@ package ftp
 
 import (
 	"context"
+	"fmt"
 	"github.com/alist-org/alist/v3/internal/errs"
 	"github.com/alist-org/alist/v3/internal/fs"
 	"github.com/alist-org/alist/v3/internal/model"
@@ -64,8 +65,14 @@ func Rename(ctx context.Context, oldPath, newPath string) error {
 		if !user.CanFTPManage() || !user.CanMove() || (srcBase != dstBase && !user.CanRename()) {
 			return errs.PermissionDenied
 		}
-		if err := fs.Move(ctx, srcPath, dstDir); err != nil {
-			return err
+		if err = fs.Move(ctx, srcPath, dstDir); err != nil {
+			if srcBase != dstBase {
+				return err
+			}
+			if _, err1 := fs.Copy(ctx, srcPath, dstDir); err1 != nil {
+				return fmt.Errorf("failed move for %+v, and failed try copying for %+v", err, err1)
+			}
+			return nil
 		}
 		if srcBase != dstBase {
 			return fs.Rename(ctx, stdpath.Join(dstDir, srcBase), dstBase)


Test patch: