Private
Public Access
1
0
Files
u-desk/internal/filesystem/service.go

738 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"
"encoding/base64"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
"u-desk/internal/common"
)
const maxReadWriteSize = 10 * 1024 * 1024 // 10MB 读写上限
// 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
}
// ========== 核心文件操作 ==========
// 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)
}
if info.Size() > maxReadWriteSize {
return "", fmt.Errorf("文件过大 (%.1f MB),超过读取上限 (%d MB)", float64(info.Size())/1024/1024, maxReadWriteSize/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 接口)
// writeFile 内部写入实现(路径验证+大小检查+写入+日志)
func (s *FileSystemService) writeFileWithLog(path string, data []byte) 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)
}
if len(data) > maxReadWriteSize {
return fmt.Errorf("文件过大 (%.1f MB),超过写入上限 (%d MB)", float64(len(data))/1024/1024, maxReadWriteSize/1024/1024)
}
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
}
// WriteFile 写入文件
func (s *FileSystemService) WriteFile(path, content string) error {
return s.writeFileWithLog(path, []byte(content))
}
// SaveBase64File 将 base64 编码内容解码后写入二进制文件
func (s *FileSystemService) SaveBase64File(path, base64Content string) error {
if strings.TrimSpace(base64Content) == "" {
return errors.New("base64 内容不能为空")
}
data, err := base64.StdEncoding.DecodeString(base64Content)
if err != nil {
return fmt.Errorf("base64 解码失败: %v", err)
}
return s.writeFileWithLog(path, data)
}
// 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
}
// 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操作接口 ==========
// ListZipContents 列出ZIP文件内容别名保持向后兼容
func (s *FileSystemService) ListZipContents(zipPath string) ([]map[string]interface{}, error) {
return ListZipContents(zipPath)
}
// ExtractFileFromZip 从ZIP提取文件内容别名保持向后兼容
func (s *FileSystemService) ExtractFileFromZip(zipPath, filePath string) (string, error) {
return ExtractFileFromZip(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")
cleanPath := filepath.Clean(path)
cleanBinPath := filepath.Clean(recycleBinPath)
return len(cleanPath) >= len(cleanBinPath) && cleanPath[:len(cleanBinPath)] == cleanBinPath
}
// ========== 辅助方法 ==========
// 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
}