新增: 文件操作增强与预览扩展
- 文件剪贴板复制剪切粘贴,重名自动副本 - SFTP下载到本机与外部文件拖入,目录递归 - 传输进度浮动面板,transfer-progress事件链 - Drawio预览,Excel工作线程,冻结看门狗 - cmd与bat自写batch语法高亮,psm1等映射补齐
This commit is contained in:
+206
-30
@@ -5,6 +5,8 @@ import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -143,11 +145,10 @@ func (s *FileSystemService) ReadFile(path string) (string, error) {
|
||||
}
|
||||
|
||||
s.logRead(path, int64(len(data)), nil)
|
||||
return BytesToString(data), nil
|
||||
return common.BytesToString(data), nil
|
||||
}
|
||||
|
||||
// Write 写入文件内容(实现 FileService 接口)
|
||||
// writeFile 内部写入实现(路径验证+大小检查+写入+日志)
|
||||
// writeFileWithLog 内部写入实现(路径验证+大小检查+写入+日志)
|
||||
func (s *FileSystemService) writeFileWithLog(path string, data []byte) error {
|
||||
if err := s.validatePath(path); err != nil {
|
||||
return err
|
||||
@@ -229,28 +230,20 @@ func (s *FileSystemService) DeletePathWithContext(ctx context.Context, path stri
|
||||
}
|
||||
}
|
||||
|
||||
// 执行删除
|
||||
var deleteErr error
|
||||
if info.IsDir() {
|
||||
deleteErr = os.RemoveAll(path)
|
||||
} else {
|
||||
deleteErr = os.Remove(path)
|
||||
}
|
||||
|
||||
s.logDelete(path, info.IsDir(), info.Size(), deleteErr)
|
||||
|
||||
if deleteErr != nil {
|
||||
return nil, fmt.Errorf("删除失败: %v", deleteErr)
|
||||
}
|
||||
|
||||
// 如果启用回收站,移动到回收站而非永久删除
|
||||
if s.recycleBin != nil {
|
||||
// 检查是否已在回收站中
|
||||
if !isInRecycleBin(path) {
|
||||
if err := s.recycleBin.MoveToRecycleBin(path); err != nil {
|
||||
// 回收站失败,记录但继续
|
||||
slog.Warn("移动到回收站失败", "error", err)
|
||||
// 回收站优先:先尝试移动,失败再永久删除
|
||||
if s.recycleBin != nil && !isInRecycleBin(path) {
|
||||
if err := s.recycleBin.MoveToRecycleBin(path); err != nil {
|
||||
slog.Warn("回收站移动失败,改为永久删除", "error", err)
|
||||
if err := s.permanentDelete(path, info); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
s.logDelete(path, info.IsDir(), info.Size(), nil)
|
||||
}
|
||||
} else {
|
||||
// 无回收站或已在回收站中,永久删除
|
||||
if err := s.permanentDelete(path, info); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,7 +252,7 @@ func (s *FileSystemService) DeletePathWithContext(ctx context.Context, path stri
|
||||
Path: filepath.ToSlash(path), // 统一使用正斜杠
|
||||
Name: info.Name(),
|
||||
Size: info.Size(),
|
||||
SizeStr: FormatBytes(info.Size()),
|
||||
SizeStr: common.FormatBytesInt(info.Size()),
|
||||
IsDir: info.IsDir(),
|
||||
ModTime: info.ModTime().Format("2006-01-02 15:04:05"),
|
||||
Mode: info.Mode().String(),
|
||||
@@ -366,7 +359,7 @@ func (s *FileSystemService) CreateDir(path string) (*FileOperationResult, error)
|
||||
Path: filepath.ToSlash(path), // 统一使用正斜杠
|
||||
Name: info.Name(),
|
||||
Size: info.Size(),
|
||||
SizeStr: FormatBytes(info.Size()),
|
||||
SizeStr: common.FormatBytesInt(info.Size()),
|
||||
IsDir: true,
|
||||
ModTime: info.ModTime().Format("2006-01-02 15:04:05"),
|
||||
Mode: info.Mode().String(),
|
||||
@@ -414,7 +407,7 @@ func (s *FileSystemService) CreateFile(path string) (*FileOperationResult, error
|
||||
Path: filepath.ToSlash(path), // 统一使用正斜杠
|
||||
Name: info.Name(),
|
||||
Size: info.Size(),
|
||||
SizeStr: FormatBytes(info.Size()),
|
||||
SizeStr: common.FormatBytesInt(info.Size()),
|
||||
IsDir: false,
|
||||
ModTime: info.ModTime().Format("2006-01-02 15:04:05"),
|
||||
Mode: info.Mode().String(),
|
||||
@@ -439,7 +432,7 @@ func (s *FileSystemService) GetFileInfo(path string) (map[string]interface{}, er
|
||||
"name": info.Name(),
|
||||
"path": filepath.ToSlash(path), // 统一使用正斜杠
|
||||
"size": info.Size(),
|
||||
"size_str": FormatBytes(info.Size()),
|
||||
"size_str": common.FormatBytesInt(info.Size()),
|
||||
"is_dir": info.IsDir(),
|
||||
"mod_time": info.ModTime().Format("2006-01-02 15:04:05"),
|
||||
"mode": info.Mode().String(),
|
||||
@@ -495,7 +488,7 @@ func (s *FileSystemService) RenamePath(oldPath, newPath string) (*FileOperationR
|
||||
Path: filepath.ToSlash(newPath), // 统一使用正斜杠
|
||||
Name: info.Name(),
|
||||
Size: info.Size(),
|
||||
SizeStr: FormatBytes(info.Size()),
|
||||
SizeStr: common.FormatBytesInt(info.Size()),
|
||||
IsDir: info.IsDir(),
|
||||
ModTime: info.ModTime().Format("2006-01-02 15:04:05"),
|
||||
Mode: info.Mode().String(),
|
||||
@@ -503,6 +496,174 @@ func (s *FileSystemService) RenamePath(oldPath, newPath string) (*FileOperationR
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ========== 复制与移动 ==========
|
||||
|
||||
// checkCopyMovePaths 复制/移动共用的前置校验:路径合法、源存在、目标不存在
|
||||
func (s *FileSystemService) checkCopyMovePaths(src, dst string) (os.FileInfo, error) {
|
||||
if err := s.validatePath(src); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.validatePath(dst); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info, err := os.Lstat(src)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("源路径不存在: %v", err)
|
||||
}
|
||||
if _, err := os.Lstat(dst); err == nil {
|
||||
return nil, fmt.Errorf("目标已存在: %s", dst)
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// logCopyMoveAudit 记录复制/移动成功审计日志
|
||||
func (s *FileSystemService) logCopyMoveAudit(op AuditOperation, src, dst string, isDir bool) {
|
||||
s.logAudit(AuditLogEntry{
|
||||
Timestamp: getCurrentTimestamp(),
|
||||
Operation: op,
|
||||
Path: dst,
|
||||
OldPath: src,
|
||||
IsDirectory: isDir,
|
||||
Success: true,
|
||||
})
|
||||
}
|
||||
|
||||
// CopyPath 递归复制文件或目录到目标路径(dst 必须不存在,唯一性由调用方保证)
|
||||
// onProgress 每读取一块回调一次(可为 nil),copied/total 单位字节
|
||||
func (s *FileSystemService) CopyPath(src, dst string, onProgress func(copied, total int64)) error {
|
||||
info, err := s.checkCopyMovePaths(src, dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
total, err := pathSize(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("统计源大小失败: %v", err)
|
||||
}
|
||||
|
||||
var copied int64
|
||||
if err := copyPathRecursive(src, dst, info, &copied, total, onProgress); err != nil {
|
||||
return fmt.Errorf("复制失败: %w", err)
|
||||
}
|
||||
|
||||
s.logCopyMoveAudit(OperationCopy, src, dst, info.IsDir())
|
||||
return nil
|
||||
}
|
||||
|
||||
// MovePath 移动文件或目录:同盘直接重命名,跨盘回退为复制后永久删除源(不走回收站)
|
||||
func (s *FileSystemService) MovePath(src, dst string) error {
|
||||
info, err := s.checkCopyMovePaths(src, dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 快路径:同盘 rename(不发进度)
|
||||
if err := os.Rename(src, dst); err == nil {
|
||||
s.logCopyMoveAudit(OperationMove, src, dst, info.IsDir())
|
||||
return nil
|
||||
}
|
||||
|
||||
// 跨盘:复制 + 删除源
|
||||
if err := s.CopyPath(src, dst, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.RemoveAll(src); err != nil {
|
||||
return fmt.Errorf("移动后删除源失败: %v", err)
|
||||
}
|
||||
|
||||
s.logCopyMoveAudit(OperationMove, src, dst, info.IsDir())
|
||||
return nil
|
||||
}
|
||||
|
||||
// UniquePath 返回不冲突的目标路径:重名时追加「 - 副本」「 - 副本 (2)」后缀(扩展名前插入)
|
||||
// 命名循环复用 common.UniqueName(与 SFTP 远端重名逻辑同源)
|
||||
func UniquePath(path string) string {
|
||||
dir, name := filepath.Split(path)
|
||||
unique, err := common.UniqueName(name, func(candidate string) (bool, error) {
|
||||
_, err := os.Lstat(filepath.Join(dir, candidate))
|
||||
return err == nil, nil
|
||||
})
|
||||
if err != nil {
|
||||
return path // exists 恒不返回错误,防御
|
||||
}
|
||||
return filepath.Join(dir, unique)
|
||||
}
|
||||
|
||||
// copyPathRecursive 递归复制单个路径(文件/目录/符号链接)
|
||||
func copyPathRecursive(src, dst string, info os.FileInfo, copied *int64, total int64, onProgress func(copied, total int64)) error {
|
||||
switch {
|
||||
case info.IsDir():
|
||||
if err := os.MkdirAll(dst, info.Mode().Perm()); err != nil {
|
||||
return err
|
||||
}
|
||||
entries, err := os.ReadDir(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, entry := range entries {
|
||||
entryInfo, err := entry.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := copyPathRecursive(filepath.Join(src, entry.Name()), filepath.Join(dst, entry.Name()), entryInfo, copied, total, onProgress); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
case info.Mode()&os.ModeSymlink != 0:
|
||||
target, err := os.Readlink(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Symlink(target, dst)
|
||||
default:
|
||||
return copyFileWithProgress(src, dst, info, copied, total, onProgress)
|
||||
}
|
||||
}
|
||||
|
||||
// copyFileWithProgress 复制单个普通文件,按读取量累计回调进度
|
||||
func copyFileWithProgress(src, dst string, info os.FileInfo, copied *int64, total int64, onProgress func(copied, total int64)) error {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer in.Close()
|
||||
|
||||
out, err := os.OpenFile(dst, os.O_CREATE|os.O_EXCL|os.O_WRONLY, info.Mode().Perm())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
var reader io.Reader = in
|
||||
if onProgress != nil {
|
||||
reader = &common.CountingReader{R: in, OnN: func(n int64) {
|
||||
*copied += n
|
||||
onProgress(*copied, total)
|
||||
}}
|
||||
}
|
||||
_, err = io.Copy(out, reader)
|
||||
return err
|
||||
}
|
||||
|
||||
// pathSize 统计路径总大小(文件为自身大小,目录递归求和,跳过符号链接)
|
||||
func pathSize(path string) (int64, error) {
|
||||
var total int64
|
||||
err := filepath.WalkDir(path, func(_ string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.Type().IsRegular() {
|
||||
if info, e := d.Info(); e == nil {
|
||||
total += info.Size()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return total, err
|
||||
}
|
||||
|
||||
// ========== ZIP操作接口 ==========
|
||||
|
||||
// ListZipContents 列出ZIP文件内容(别名,保持向后兼容)
|
||||
@@ -532,12 +693,27 @@ func getCurrentTimestamp() time.Time {
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
// permanentDelete 永久删除路径(目录 RemoveAll / 文件 Remove)并记录删除日志
|
||||
func (s *FileSystemService) permanentDelete(path string, info os.FileInfo) error {
|
||||
var deleteErr error
|
||||
if info.IsDir() {
|
||||
deleteErr = os.RemoveAll(path)
|
||||
} else {
|
||||
deleteErr = os.Remove(path)
|
||||
}
|
||||
s.logDelete(path, info.IsDir(), info.Size(), deleteErr)
|
||||
if deleteErr != nil {
|
||||
return fmt.Errorf("删除失败: %v", deleteErr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isInRecycleBin 检查路径是否在回收站中
|
||||
func isInRecycleBin(path string) bool {
|
||||
recycleBinPath := filepath.Join(common.GetUserDataDir(), "recycle_bin")
|
||||
cleanPath := filepath.Clean(path)
|
||||
cleanBinPath := filepath.Clean(recycleBinPath)
|
||||
return len(cleanPath) >= len(cleanBinPath) && cleanPath[:len(cleanBinPath)] == cleanBinPath
|
||||
return strings.HasPrefix(cleanPath, cleanBinPath)
|
||||
}
|
||||
|
||||
// ========== 辅助方法 ==========
|
||||
|
||||
Reference in New Issue
Block a user