Private
Public Access
1
0
Files
u-desk/internal/filesystem/service.go
绝尘 fb12ec48e8 修复:大文件点击卡死 + Dockerfile高亮支持
- useFileEdit: 新增 KNOWN_BINARY_EXTS 集合,exe/dll/zip 等 28 种二进制扩展名直接判定,不再读取文件内容
- index.vue: loadFileContent 增加大文件预检,基于 fileSize 超过阈值直接拦截
- service.go: ReadFile 增加 10MB 读取上限,超限返回错误
- Dockerfile 支持:CODE 分类、🐳图标、CodeMirror shell 模式高亮、languageMap 映射
2026-04-07 11:39:50 +08:00

786 lines
21 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 读取文件内容(限制最大 10MB
func (s *FileSystemService) ReadFile(path string) (string, error) {
// 路径验证
if err := s.validatePath(path); err != nil {
return "", err
}
// 检查文件大小,避免读取超大文件导致内存问题
info, err := os.Stat(path)
if err != nil {
return "", fmt.Errorf("获取文件信息失败: %v", err)
}
const maxReadSize = 10 * 1024 * 1024 // 10MB
if info.Size() > maxReadSize {
return "", fmt.Errorf("文件过大 (%.1f MB),超过读取上限 (%d MB)", float64(info.Size())/1024/1024, maxReadSize/1024/1024)
}
// 读取文件
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: filepath.ToSlash(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": filepath.ToSlash(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: filepath.ToSlash(path), // 统一使用正斜杠
Name: filepath.Base(path),
IsDir: true,
}, nil
}
return &FileOperationResult{
Path: filepath.ToSlash(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: filepath.ToSlash(path), // 统一使用正斜杠
Name: filepath.Base(path),
IsDir: false,
Size: 0,
}, nil
}
return &FileOperationResult{
Path: filepath.ToSlash(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": filepath.ToSlash(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: filepath.ToSlash(newPath), // 统一使用正斜杠
Name: filepath.Base(newPath),
OldPath: filepath.ToSlash(oldPath),
}, nil
}
return &FileOperationResult{
Path: filepath.ToSlash(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: filepath.ToSlash(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
}