397 lines
9.2 KiB
Go
397 lines
9.2 KiB
Go
package filesystem
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"math/big"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// RecycleBinEntry 回收站条目
|
|
type RecycleBinEntry struct {
|
|
OriginalPath string `json:"original_path"` // 原始路径
|
|
DeletedPath string `json:"deleted_path"` // 回收站中的路径
|
|
DeletedTime time.Time `json:"deleted_time"` // 删除时间
|
|
Size int64 `json:"size"` // 文件大小
|
|
IsDirectory bool `json:"is_directory"` // 是否为目录
|
|
OriginalDevice string `json:"original_device"` // 原始设备(盘符)
|
|
}
|
|
|
|
// RecycleBin 回收站管理器
|
|
type RecycleBin struct {
|
|
mu sync.Mutex
|
|
binPath string
|
|
metadataFile string
|
|
entries []RecycleBinEntry
|
|
}
|
|
|
|
// NewRecycleBin 创建回收站管理器
|
|
func NewRecycleBin(binPath string) (*RecycleBin, error) {
|
|
// 创建回收站目录
|
|
if err := os.MkdirAll(binPath, 0755); err != nil {
|
|
return nil, fmt.Errorf("创建回收站目录失败: %v", err)
|
|
}
|
|
|
|
bin := &RecycleBin{
|
|
binPath: binPath,
|
|
metadataFile: filepath.Join(binPath, "metadata.json"),
|
|
entries: make([]RecycleBinEntry, 0),
|
|
}
|
|
|
|
// 加载元数据
|
|
if err := bin.loadMetadata(); err != nil {
|
|
// 如果文件不存在,这是正常的,忽略错误
|
|
if !os.IsNotExist(err) {
|
|
return nil, fmt.Errorf("加载回收站元数据失败: %v", err)
|
|
}
|
|
}
|
|
|
|
// 启动自动清理协程
|
|
go bin.autoCleanup()
|
|
|
|
return bin, nil
|
|
}
|
|
|
|
// MoveToRecycleBin 移动文件到回收站
|
|
func (rb *RecycleBin) MoveToRecycleBin(path string) error {
|
|
rb.mu.Lock()
|
|
defer rb.mu.Unlock()
|
|
// 获取文件信息
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
return fmt.Errorf("获取文件信息失败: %v", err)
|
|
}
|
|
|
|
// 生成唯一的回收站文件名
|
|
timestamp := time.Now().Format("20060102_150405")
|
|
randomSuffix := generateRandomString(6)
|
|
baseName := filepath.Base(path)
|
|
var recycleName string
|
|
|
|
if info.IsDir() {
|
|
recycleName = fmt.Sprintf("%s_%s_%s", timestamp, randomSuffix, baseName)
|
|
} else {
|
|
ext := filepath.Ext(baseName)
|
|
nameWithoutExt := baseName[:len(baseName)-len(ext)]
|
|
recycleName = fmt.Sprintf("%s_%s_%s%s", timestamp, randomSuffix, nameWithoutExt, ext)
|
|
}
|
|
|
|
recyclePath := filepath.Join(rb.binPath, recycleName)
|
|
|
|
// 移动文件到回收站
|
|
if err := os.Rename(path, recyclePath); err != nil {
|
|
// 如果跨设备移动失败,尝试复制后删除
|
|
if err := copyRecursively(path, recyclePath); err != nil {
|
|
return fmt.Errorf("移动到回收站失败: %v", err)
|
|
}
|
|
os.RemoveAll(path)
|
|
}
|
|
|
|
// 创建元数据条目
|
|
entry := RecycleBinEntry{
|
|
OriginalPath: path,
|
|
DeletedPath: recyclePath,
|
|
DeletedTime: time.Now(),
|
|
Size: info.Size(),
|
|
IsDirectory: info.IsDir(),
|
|
OriginalDevice: getDevice(path),
|
|
}
|
|
|
|
// 添加到元数据
|
|
rb.entries = append(rb.entries, entry)
|
|
|
|
// 保存元数据
|
|
if err := rb.saveMetadata(); err != nil {
|
|
return fmt.Errorf("保存回收站元数据失败: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RestoreFromRecycleBin 从回收站恢复文件
|
|
func (rb *RecycleBin) RestoreFromRecycleBin(recyclePath string) error {
|
|
rb.mu.Lock()
|
|
defer rb.mu.Unlock()
|
|
// 查找对应的元数据条目
|
|
var entry *RecycleBinEntry
|
|
for i := range rb.entries {
|
|
if rb.entries[i].DeletedPath == recyclePath {
|
|
entry = &rb.entries[i]
|
|
// 从列表中移除
|
|
rb.entries = append(rb.entries[:i], rb.entries[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
|
|
if entry == nil {
|
|
return fmt.Errorf("回收站中未找到该文件")
|
|
}
|
|
|
|
// 检查原始路径的父目录是否存在
|
|
parentDir := filepath.Dir(entry.OriginalPath)
|
|
if err := os.MkdirAll(parentDir, 0755); err != nil {
|
|
return fmt.Errorf("创建父目录失败: %v", err)
|
|
}
|
|
|
|
// 检查原始位置是否已有文件
|
|
if _, err := os.Stat(entry.OriginalPath); err == nil {
|
|
return fmt.Errorf("原始位置已存在同名文件,请先删除或重命名")
|
|
}
|
|
|
|
// 移回文件
|
|
if err := os.Rename(recyclePath, entry.OriginalPath); err != nil {
|
|
// 如果跨设备移动失败,尝试复制后删除
|
|
if err := copyRecursively(recyclePath, entry.OriginalPath); err != nil {
|
|
return fmt.Errorf("恢复文件失败: %v", err)
|
|
}
|
|
os.RemoveAll(recyclePath)
|
|
}
|
|
|
|
// 保存元数据
|
|
if err := rb.saveMetadata(); err != nil {
|
|
return fmt.Errorf("保存回收站元数据失败: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// DeletePermanently 永久删除回收站中的文件
|
|
func (rb *RecycleBin) DeletePermanently(recyclePath string) error {
|
|
rb.mu.Lock()
|
|
defer rb.mu.Unlock()
|
|
// 查找元数据条目
|
|
for i, entry := range rb.entries {
|
|
if entry.DeletedPath == recyclePath {
|
|
// 从列表中移除
|
|
rb.entries = append(rb.entries[:i], rb.entries[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
|
|
// 删除文件
|
|
if err := os.RemoveAll(recyclePath); err != nil {
|
|
return fmt.Errorf("永久删除失败: %v", err)
|
|
}
|
|
|
|
// 保存元数据
|
|
if err := rb.saveMetadata(); err != nil {
|
|
return fmt.Errorf("保存回收站元数据失败: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ListEntries 列出回收站中的所有条目
|
|
func (rb *RecycleBin) ListEntries() []RecycleBinEntry {
|
|
rb.mu.Lock()
|
|
defer rb.mu.Unlock()
|
|
return append([]RecycleBinEntry(nil), rb.entries...)
|
|
}
|
|
|
|
// Empty 清空回收站
|
|
func (rb *RecycleBin) Empty() error {
|
|
rb.mu.Lock()
|
|
defer rb.mu.Unlock()
|
|
// 删除所有文件
|
|
for _, entry := range rb.entries {
|
|
if err := os.RemoveAll(entry.DeletedPath); err != nil {
|
|
return fmt.Errorf("删除文件失败: %s", err)
|
|
}
|
|
}
|
|
|
|
// 清空元数据
|
|
rb.entries = make([]RecycleBinEntry, 0)
|
|
|
|
// 保存元数据
|
|
if err := rb.saveMetadata(); err != nil {
|
|
return fmt.Errorf("保存回收站元数据失败: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// autoCleanup 自动清理超过30天的文件
|
|
func (rb *RecycleBin) autoCleanup() {
|
|
ticker := time.NewTicker(24 * time.Hour) // 每天检查一次
|
|
defer ticker.Stop()
|
|
|
|
for range ticker.C {
|
|
rb.cleanupExpiredEntries()
|
|
}
|
|
}
|
|
|
|
// cleanupExpiredEntries 清理过期的条目
|
|
func (rb *RecycleBin) cleanupExpiredEntries() {
|
|
rb.mu.Lock()
|
|
defer rb.mu.Unlock()
|
|
now := time.Now()
|
|
expiredEntries := make([]int, 0)
|
|
|
|
// 找出所有过期的条目(超过30天)
|
|
for i, entry := range rb.entries {
|
|
if now.Sub(entry.DeletedTime) > 30*24*time.Hour {
|
|
expiredEntries = append(expiredEntries, i)
|
|
}
|
|
}
|
|
|
|
// 从后往前删除(避免索引问题)
|
|
for i := len(expiredEntries) - 1; i >= 0; i-- {
|
|
idx := expiredEntries[i]
|
|
entry := rb.entries[idx]
|
|
|
|
// 删除文件
|
|
_ = os.RemoveAll(entry.DeletedPath)
|
|
|
|
// 从列表中移除
|
|
rb.entries = append(rb.entries[:idx], rb.entries[idx+1:]...)
|
|
}
|
|
|
|
// 保存元数据
|
|
if len(expiredEntries) > 0 {
|
|
_ = rb.saveMetadata()
|
|
}
|
|
}
|
|
|
|
// loadMetadata 加载元数据
|
|
func (rb *RecycleBin) loadMetadata() error {
|
|
data, err := os.ReadFile(rb.metadataFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return json.Unmarshal(data, &rb.entries)
|
|
}
|
|
|
|
// saveMetadata 保存元数据
|
|
func (rb *RecycleBin) saveMetadata() error {
|
|
data, err := json.MarshalIndent(rb.entries, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return os.WriteFile(rb.metadataFile, data, 0644)
|
|
}
|
|
|
|
// copyRecursively 递归复制文件或目录
|
|
func copyRecursively(src, dst string) error {
|
|
info, err := os.Stat(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if info.IsDir() {
|
|
return copyDirectory(src, dst)
|
|
}
|
|
|
|
return copyFile(src, dst)
|
|
}
|
|
|
|
// copyDirectory 复制目录
|
|
func copyDirectory(src, dst string) error {
|
|
// 创建目标目录
|
|
if err := os.MkdirAll(dst, 0755); err != nil {
|
|
return err
|
|
}
|
|
|
|
// 读取源目录
|
|
entries, err := os.ReadDir(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 复制每个条目
|
|
for _, entry := range entries {
|
|
srcPath := filepath.Join(src, entry.Name())
|
|
dstPath := filepath.Join(dst, entry.Name())
|
|
|
|
if entry.IsDir() {
|
|
if err := copyDirectory(srcPath, dstPath); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if err := copyFile(srcPath, dstPath); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// copyFile 复制文件
|
|
func copyFile(src, dst string) error {
|
|
// 打开源文件
|
|
srcFile, err := os.Open(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer srcFile.Close()
|
|
|
|
// 创建目标文件
|
|
dstFile, err := os.Create(dst)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer dstFile.Close()
|
|
|
|
// 复制内容
|
|
if _, err := io.Copy(dstFile, srcFile); err != nil {
|
|
return err
|
|
}
|
|
|
|
// 复制文件权限
|
|
srcInfo, err := os.Stat(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return os.Chmod(dst, srcInfo.Mode())
|
|
}
|
|
|
|
// getDevice 获取文件所在设备(盘符)
|
|
func getDevice(path string) string {
|
|
absPath, err := filepath.Abs(path)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
if len(absPath) >= 2 {
|
|
return absPath[:2] // 返回 "C:" 这样的盘符
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// generateRandomString 生成随机字符串
|
|
// 使用加密安全的随机数生成器,保证随机性和性能
|
|
func generateRandomString(length int) string {
|
|
const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
|
|
b := make([]byte, length)
|
|
|
|
// 使用 crypto/rand 生成安全的随机数
|
|
for i := range b {
|
|
n, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
|
|
if err != nil {
|
|
// 如果加密随机数生成失败,回退到时间戳(极低概率)
|
|
b[i] = charset[time.Now().UnixNano()%int64(len(charset))]
|
|
continue
|
|
}
|
|
b[i] = charset[n.Int64()]
|
|
}
|
|
|
|
return string(b)
|
|
}
|
|
|
|
// 全局回收站实例
|
|
var globalRecycleBin *RecycleBin
|
|
|
|
// GetRecycleBin 获取全局回收站实例
|
|
func GetRecycleBin() *RecycleBin {
|
|
return globalRecycleBin
|
|
}
|