Private
Public Access
1
0
Files
u-desk/internal/filesystem/service.go
绝尘 edd5b7c869 优化:文件操作精确更新,避免占用问题
后端改进:
- API 返回 FileOperationResult 结构体(类型安全)
- 所有操作返回文件信息,支持精确更新
- 删除过度抽象的接口和全局函数包装器(桌面程序不需要)

前端改进:
- 精确更新文件列表(避免整目录刷新)
- 分离 add/remove/update 三个独立函数
- 重命名前智能关闭文件/文件夹,解决占用问题
- 优化错误提示,用户友好提示

技术细节:
- 定义 FileOperationResult 结构体替代 map[string]interface{}
- 前端 API 返回类型从 void 改为 any
- 保留运行时状态(如 is_favorite)
- 智能识别文件占用错误并给出解决建议
2026-02-04 12:13:12 +08:00

776 lines
20 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package filesystem
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"sync"
"time"
"u-desk/internal/common"
)
// FileOperationResult 文件操作结果
type FileOperationResult struct {
Path string `json:"path"`
Name string `json:"name"`
Size int64 `json:"size"`
SizeStr string `json:"size_str,omitempty"`
IsDir bool `json:"is_dir"`
ModTime string `json:"mod_time,omitempty"`
Mode string `json:"mode,omitempty"`
OldPath string `json:"old_path,omitempty"` // 仅重命名操作时有值
Deleted bool `json:"deleted,omitempty"` // 仅删除操作时有值
}
// FileSystemService 文件系统服务
// 统一管理所有文件系统相关的功能,使用依赖注入而非全局变量
type FileSystemService struct {
// 核心组件
config *Config
pathValidator PathValidator
fileTypeManager FileTypeManager
// 基础设施组件
auditLogger *AuditLogger
recycleBin *RecycleBin
lockChecker *FileLockChecker
// 状态管理
mu sync.RWMutex
initialized bool
}
// NewFileSystemService 创建新的文件系统服务
// 使用依赖注入,所有组件通过参数传入,便于测试和替换
func NewFileSystemService(config *Config) (*FileSystemService, error) {
if config == nil {
config = DefaultConfig()
}
service := &FileSystemService{
config: config,
pathValidator: NewPathValidator(config),
fileTypeManager: NewFileTypeManager(config),
}
// 初始化基础设施组件
if err := service.initializeComponents(); err != nil {
return nil, fmt.Errorf("初始化文件系统服务失败: %w", err)
}
service.initialized = true
return service, nil
}
// initializeComponents 初始化各个组件
func (s *FileSystemService) initializeComponents() error {
// 1. 初始化审计日志
if s.config.Features.AuditLog {
if err := s.initAuditLogger(); err != nil {
return fmt.Errorf("初始化审计日志失败: %w", err)
}
}
// 2. 初始化回收站
if s.config.Features.RecycleBin {
if err := s.initRecycleBin(); err != nil {
return fmt.Errorf("初始化回收站失败: %w", err)
}
}
// 3. 初始化文件锁检查器
if s.config.Features.FileLockCheck {
s.lockChecker = NewFileLockChecker()
}
return nil
}
// initAuditLogger 初始化审计日志
func (s *FileSystemService) initAuditLogger() error {
logDir := filepath.Join(common.GetUserDataDir(), "logs")
logger, err := NewAuditLogger(logDir)
if err != nil {
return err
}
s.auditLogger = logger
return nil
}
// initRecycleBin 初始化回收站
func (s *FileSystemService) initRecycleBin() error {
recycleBinPath := filepath.Join(common.GetUserDataDir(), "recycle_bin")
bin, err := NewRecycleBin(recycleBinPath)
if err != nil {
return err
}
s.recycleBin = bin
return nil
}
// ========== 核心文件操作 ==========
// Read 读取文件内容(实现 FileService 接口)
func (s *FileSystemService) Read(path string) (string, error) {
return s.ReadFile(path)
}
// ReadFile 读取文件内容
func (s *FileSystemService) ReadFile(path string) (string, error) {
// 路径验证
if err := s.validatePath(path); err != nil {
return "", err
}
// 读取文件
data, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("读取文件失败: %v", err)
}
s.logRead(path, int64(len(data)), nil)
return string(data), nil
}
// Write 写入文件内容(实现 FileService 接口)
func (s *FileSystemService) Write(path, content string) error {
return s.WriteFile(path, content)
}
// WriteFile 写入文件
func (s *FileSystemService) WriteFile(path, content string) error {
// 路径验证
if err := s.validatePath(path); err != nil {
return err
}
// 创建目录
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, DefaultDirPermissions); err != nil {
return fmt.Errorf("创建目录失败: %v", err)
}
// 写入文件
data := []byte(content)
if err := os.WriteFile(path, data, DefaultFilePermissions); err != nil {
s.logWrite(path, int64(len(data)), err)
return fmt.Errorf("写入文件失败: %v", err)
}
s.logWrite(path, int64(len(data)), nil)
return nil
}
// List 列出目录内容(实现 FileService 接口)
func (s *FileSystemService) List(path string) ([]map[string]interface{}, error) {
return s.ListDir(path)
}
// Open 打开文件(实现 FileService 接口)
func (s *FileSystemService) Open(path string) error {
// 使用系统默认程序打开文件
var cmd *exec.Cmd
switch runtime.GOOS {
case "windows":
cmd = exec.Command("cmd", "/c", "start", "", path)
case "darwin":
cmd = exec.Command("open", path)
default:
cmd = exec.Command("xdg-open", path)
}
return cmd.Start()
}
// Delete 删除文件或目录(实现 FileService 接口)
func (s *FileSystemService) Delete(path string) (*FileOperationResult, error) {
return s.DeletePathWithContext(context.Background(), path)
}
// DeletePath 删除文件或目录
func (s *FileSystemService) DeletePath(path string) (*FileOperationResult, error) {
return s.DeletePathWithContext(context.Background(), path)
}
// DeletePathWithContext 带上下文的删除操作,返回被删除文件的信息
func (s *FileSystemService) DeletePathWithContext(ctx context.Context, path string) (*FileOperationResult, error) {
// 路径验证
if err := s.validatePath(path); err != nil {
return nil, err
}
// 获取文件信息(在删除前保存)
info, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return nil, fmt.Errorf("文件或目录不存在")
}
return nil, fmt.Errorf("获取文件信息失败: %v", err)
}
// 检查删除限制
exceeds, details, checkErr := CheckDeleteRestrictions(path, info, s.config)
if checkErr != nil {
return nil, checkErr
}
if exceeds {
if s.config.Security.DeleteRestrictions.RequireConfirm {
return nil, &DeleteRestrictionWarning{
Path: path,
Details: details,
Info: info,
}
}
return nil, fmt.Errorf("删除限制: %s", details)
}
// 文件锁检查(可选)
if s.lockChecker != nil {
if err := s.lockChecker.SafeDeleteWithLockCheck(path); err != nil {
return nil, err
}
}
// 执行删除
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 {
// 回收站失败,记录但继续
fmt.Printf("[警告] 移动到回收站失败: %v\n", err)
}
}
}
// 返回被删除的文件信息,用于前端更新
return &FileOperationResult{
Path: path,
Name: info.Name(),
Size: info.Size(),
SizeStr: formatBytes(info.Size()),
IsDir: info.IsDir(),
ModTime: info.ModTime().Format("2006-01-02 15:04:05"),
Mode: info.Mode().String(),
Deleted: true,
}, nil
}
// ListDir 列出目录内容
func (s *FileSystemService) ListDir(path string) ([]map[string]interface{}, error) {
// 路径验证
if err := s.validatePath(path); err != nil {
return nil, err
}
// 读取目录
entries, err := os.ReadDir(path)
if err != nil {
return nil, fmt.Errorf("读取目录失败: %v", err)
}
// 转换为结果格式
result := make([]map[string]interface{}, 0, len(entries))
for _, entry := range entries {
info, err := entry.Info()
if err != nil {
continue
}
fullPath := filepath.Join(path, entry.Name())
result = append(result, map[string]interface{}{
"name": entry.Name(),
"path": fullPath,
"is_dir": entry.IsDir(),
"size": info.Size(),
"mod_time": info.ModTime().Format("2006-01-02 15:04:05"),
})
}
s.logAudit(AuditLogEntry{
Timestamp: getCurrentTimestamp(),
Operation: OperationList,
Path: path,
IsDirectory: true,
Success: true,
})
return result, nil
}
// CreateDir 创建目录,返回创建的目录信息
func (s *FileSystemService) CreateDir(path string) (*FileOperationResult, error) {
if err := s.validatePath(path); err != nil {
return nil, err
}
if err := os.MkdirAll(path, DefaultDirPermissions); err != nil {
return nil, fmt.Errorf("创建目录失败: %v", err)
}
s.logAudit(AuditLogEntry{
Timestamp: getCurrentTimestamp(),
Operation: OperationCreate,
Path: path,
IsDirectory: true,
Success: true,
})
// 获取创建的目录信息
info, err := os.Stat(path)
if err != nil {
// 创建成功但获取信息失败,返回基本信息
return &FileOperationResult{
Path: path,
Name: filepath.Base(path),
IsDir: true,
}, nil
}
return &FileOperationResult{
Path: path,
Name: info.Name(),
Size: info.Size(),
SizeStr: formatBytes(info.Size()),
IsDir: true,
ModTime: info.ModTime().Format("2006-01-02 15:04:05"),
Mode: info.Mode().String(),
}, nil
}
// CreateFile 创建空文件,返回创建的文件信息
func (s *FileSystemService) CreateFile(path string) (*FileOperationResult, error) {
if err := s.validatePath(path); err != nil {
return nil, err
}
// 检查文件是否已存在
if _, err := os.Stat(path); err == nil {
return nil, fmt.Errorf("文件已存在")
}
file, err := os.Create(path)
if err != nil {
return nil, fmt.Errorf("创建文件失败: %v", err)
}
file.Close()
s.logAudit(AuditLogEntry{
Timestamp: getCurrentTimestamp(),
Operation: OperationCreate,
Path: path,
IsDirectory: false,
Success: true,
})
// 获取创建的文件信息
info, err := os.Stat(path)
if err != nil {
// 创建成功但获取信息失败,返回基本信息
return &FileOperationResult{
Path: path,
Name: filepath.Base(path),
IsDir: false,
Size: 0,
}, nil
}
return &FileOperationResult{
Path: path,
Name: info.Name(),
Size: info.Size(),
SizeStr: formatBytes(info.Size()),
IsDir: false,
ModTime: info.ModTime().Format("2006-01-02 15:04:05"),
Mode: info.Mode().String(),
}, nil
}
// GetInfo 获取文件信息(实现 FileService 接口)
func (s *FileSystemService) GetInfo(path string) (map[string]interface{}, error) {
return s.GetFileInfo(path)
}
// GetFileInfo 获取文件信息
func (s *FileSystemService) GetFileInfo(path string) (map[string]interface{}, error) {
if err := s.validatePath(path); err != nil {
return nil, err
}
info, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return nil, fmt.Errorf("文件或目录不存在")
}
return nil, fmt.Errorf("获取文件信息失败: %v", err)
}
return map[string]interface{}{
"name": info.Name(),
"path": path,
"size": info.Size(),
"size_str": formatBytes(info.Size()),
"is_dir": info.IsDir(),
"mod_time": info.ModTime().Format("2006-01-02 15:04:05"),
"mode": info.Mode().String(),
}, nil
}
// OpenPath 打开文件或目录(使用系统默认程序)
func (s *FileSystemService) OpenPath(path string) error {
if err := s.validatePath(path); err != nil {
return err
}
return OpenPath(path)
}
// RenamePath 重命名文件或目录,返回新文件信息
func (s *FileSystemService) RenamePath(oldPath, newPath string) (*FileOperationResult, error) {
// 验证旧路径
if err := s.validatePath(oldPath); err != nil {
return nil, err
}
// 验证新路径
if err := s.validatePath(newPath); err != nil {
return nil, err
}
// 执行重命名
if err := os.Rename(oldPath, newPath); err != nil {
return nil, fmt.Errorf("重命名失败: %v", err)
}
s.logAudit(AuditLogEntry{
Timestamp: getCurrentTimestamp(),
Operation: OperationRename,
Path: newPath,
OldPath: oldPath,
Success: true,
})
// 获取新文件信息
info, err := os.Stat(newPath)
if err != nil {
// 重命名成功但获取信息失败,返回基本信息
return &FileOperationResult{
Path: newPath,
Name: filepath.Base(newPath),
OldPath: oldPath,
}, nil
}
return &FileOperationResult{
Path: newPath,
Name: info.Name(),
Size: info.Size(),
SizeStr: formatBytes(info.Size()),
IsDir: info.IsDir(),
ModTime: info.ModTime().Format("2006-01-02 15:04:05"),
Mode: info.Mode().String(),
OldPath: oldPath,
}, nil
}
// ========== ZIP操作接口 ==========
// ListZip 列出ZIP文件内容
func (s *FileSystemService) ListZip(zipPath string) ([]map[string]interface{}, error) {
return ListZipContents(zipPath)
}
// ListZipContents 列出ZIP文件内容别名保持向后兼容
func (s *FileSystemService) ListZipContents(zipPath string) ([]map[string]interface{}, error) {
return ListZipContents(zipPath)
}
// ExtractZipFile 从ZIP提取文件内容
func (s *FileSystemService) ExtractZipFile(zipPath, filePath string) (string, error) {
return ExtractFileFromZip(zipPath, filePath)
}
// ExtractFileFromZip 从ZIP提取文件内容别名保持向后兼容
func (s *FileSystemService) ExtractFileFromZip(zipPath, filePath string) (string, error) {
return ExtractFileFromZip(zipPath, filePath)
}
// ExtractZipFileToTemp 从ZIP提取文件到临时目录
func (s *FileSystemService) ExtractZipFileToTemp(zipPath, filePath string) (string, error) {
return ExtractFileFromZipToTemp(zipPath, filePath)
}
// ExtractFileFromZipToTemp 从ZIP提取文件到临时目录别名保持向后兼容
func (s *FileSystemService) ExtractFileFromZipToTemp(zipPath, filePath string) (string, error) {
return ExtractFileFromZipToTemp(zipPath, filePath)
}
// GetZipFileInfo 获取ZIP文件信息
func (s *FileSystemService) GetZipFileInfo(zipPath, filePath string) (map[string]interface{}, error) {
return GetZipFileInfo(zipPath, filePath)
}
// ========== 辅助函数 ==========
// getCurrentTimestamp 获取当前时间戳
func getCurrentTimestamp() time.Time {
return time.Now()
}
// isInRecycleBin 检查路径是否在回收站中
func isInRecycleBin(path string) bool {
recycleBinPath := filepath.Join(common.GetUserDataDir(), "recycle_bin")
return filepath.HasPrefix(filepath.Clean(path), filepath.Clean(recycleBinPath))
}
// ========== 辅助方法 ==========
// validatePath 验证路径
func (s *FileSystemService) validatePath(path string) error {
err := s.pathValidator.Validate(path)
if err != nil && err.IsError {
return err
}
return nil
}
// GetConfig 获取配置
func (s *FileSystemService) GetConfig() *Config {
return s.config
}
// GetAuditLogger 获取审计日志记录器
func (s *FileSystemService) GetAuditLogger() *AuditLogger {
return s.auditLogger
}
// GetRecycleBin 获取回收站
func (s *FileSystemService) GetRecycleBin() *RecycleBin {
return s.recycleBin
}
// ========== 审计日志接口 ==========
// logAudit 安全记录审计日志(自动处理 nil 检查)
func (s *FileSystemService) logAudit(entry AuditLogEntry) {
if s.auditLogger != nil {
s.auditLogger.Log(entry)
}
}
// logRead 记录读取操作审计日志
func (s *FileSystemService) logRead(path string, size int64, err error) {
if s.auditLogger != nil {
s.auditLogger.LogRead(path, size, err)
}
}
// logWrite 记录写入操作审计日志
func (s *FileSystemService) logWrite(path string, size int64, err error) {
if s.auditLogger != nil {
s.auditLogger.LogWrite(path, size, err)
}
}
// logDelete 记录删除操作审计日志
func (s *FileSystemService) logDelete(path string, isDir bool, size int64, err error) {
if s.auditLogger != nil {
s.auditLogger.LogDelete(path, isDir, size, err)
}
}
// GetAuditLogs 获取审计日志
func (s *FileSystemService) GetAuditLogs(limit int) ([]map[string]interface{}, error) {
if s.auditLogger == nil {
return []map[string]interface{}{}, nil
}
logDir := filepath.Join(common.GetUserDataDir(), "logs")
entries, err := GetRecentLogs(logDir, limit)
if err != nil {
return nil, err
}
result := make([]map[string]interface{}, len(entries))
for i, entry := range entries {
result[i] = map[string]interface{}{
"timestamp": entry.Timestamp.Format("2006-01-02 15:04:05"),
"operation": entry.Operation,
"path": entry.Path,
"size": entry.Size,
"is_directory": entry.IsDirectory,
"success": entry.Success,
"error": entry.Error,
}
}
return result, nil
}
// ========== 回收站接口 ==========
// GetRecycleBinEntries 获取回收站条目
func (s *FileSystemService) GetRecycleBinEntries() ([]map[string]interface{}, error) {
if s.recycleBin == nil {
return []map[string]interface{}{}, nil
}
entries := s.recycleBin.ListEntries()
result := make([]map[string]interface{}, len(entries))
for i, entry := range entries {
result[i] = map[string]interface{}{
"original_path": entry.OriginalPath,
"deleted_path": entry.DeletedPath,
"deleted_time": entry.DeletedTime.Format("2006-01-02 15:04:05"),
"size": entry.Size,
"is_directory": entry.IsDirectory,
}
}
return result, nil
}
// RestoreFromRecycleBin 从回收站恢复文件
func (s *FileSystemService) RestoreFromRecycleBin(recyclePath string) error {
if s.recycleBin == nil {
return fmt.Errorf("回收站未初始化")
}
return s.recycleBin.RestoreFromRecycleBin(recyclePath)
}
// DeletePermanently 永久删除回收站中的文件
func (s *FileSystemService) DeletePermanently(recyclePath string) error {
if s.recycleBin == nil {
return fmt.Errorf("回收站未初始化")
}
return s.recycleBin.DeletePermanently(recyclePath)
}
// EmptyRecycleBin 清空回收站
func (s *FileSystemService) EmptyRecycleBin() error {
if s.recycleBin == nil {
return fmt.Errorf("回收站未初始化")
}
return s.recycleBin.Empty()
}
// ResolveShortcut 解析快捷方式(.lnk文件返回目标路径
func (s *FileSystemService) ResolveShortcut(lnkPath string) (targetPath string, err error) {
// 验证路径
if err := s.validatePath(lnkPath); err != nil {
return "", fmt.Errorf("路径验证失败: %w", err)
}
// 检查文件扩展名
if filepath.Ext(lnkPath) != ".lnk" {
return "", fmt.Errorf("不是快捷方式文件")
}
// 检查文件是否存在
if _, err := os.Stat(lnkPath); os.IsNotExist(err) {
return "", fmt.Errorf("快捷方式文件不存在")
}
// 使用 Windows PowerShell 解析 lnk 文件
// 这种方法更可靠,不需要依赖第三方库
if runtime.GOOS == "windows" {
// 创建 PowerShell 脚本
psScript := fmt.Sprintf(
"$shell = New-Object -ComObject WScript.Shell; "+
"$shortcut = $shell.CreateShortcut('%s'); "+
"$shortcut.TargetPath",
lnkPath,
)
// 执行 PowerShell 命令
cmd := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", psScript)
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("解析快捷方式失败: %w", err)
}
// 去除空白字符
targetPath = string(output)
targetPath = filepath.Clean(targetPath)
// 如果目标路径为空,返回错误
if targetPath == "" || targetPath == "." {
return "", fmt.Errorf("快捷方式目标路径为空")
}
return targetPath, nil
}
// 非 Windows 系统暂不支持
return "", fmt.Errorf("当前系统不支持快捷方式解析")
}
// Close 关闭服务,释放资源
func (s *FileSystemService) Close(ctx context.Context) error {
s.mu.Lock()
defer s.mu.Unlock()
if !s.initialized {
return nil
}
// 关闭审计日志
if s.auditLogger != nil {
if err := s.auditLogger.Close(); err != nil {
return fmt.Errorf("关闭审计日志失败: %w", err)
}
}
s.initialized = false
return nil
}
// ========== 全局服务实例(向后兼容)==========
var (
globalService *FileSystemService
globalServiceOnce sync.Once
)
// GetGlobalService 获取全局文件系统服务实例(单例)
// 保持向后兼容,但推荐使用依赖注入
func GetGlobalService() (*FileSystemService, error) {
var initErr error
globalServiceOnce.Do(func() {
globalService, initErr = NewFileSystemService(DefaultConfig())
})
return globalService, initErr
}
// InitGlobalFileSystem 初始化全局文件系统(兼容旧代码)
func InitGlobalFileSystem() error {
_, err := GetGlobalService()
return err
}
// CloseGlobalFileSystem 关闭全局文件系统
func CloseGlobalFileSystem(ctx context.Context) error {
if globalService != nil {
return globalService.Close(ctx)
}
return nil
}