393 lines
9.1 KiB
Go
393 lines
9.1 KiB
Go
package filesystem
|
||
|
||
import (
|
||
"crypto/rand"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"math/big"
|
||
"os"
|
||
"path/filepath"
|
||
"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 {
|
||
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 {
|
||
// 获取文件信息
|
||
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 {
|
||
// 查找对应的元数据条目
|
||
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 {
|
||
// 查找元数据条目
|
||
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 {
|
||
return rb.entries
|
||
}
|
||
|
||
// Empty 清空回收站
|
||
func (rb *RecycleBin) Empty() error {
|
||
// 删除所有文件
|
||
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() {
|
||
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
|
||
|
||
// InitRecycleBin 初始化全局回收站
|
||
func InitRecycleBin(binPath string) error {
|
||
bin, err := NewRecycleBin(binPath)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
globalRecycleBin = bin
|
||
return nil
|
||
}
|
||
|
||
// GetRecycleBin 获取全局回收站实例
|
||
func GetRecycleBin() *RecycleBin {
|
||
return globalRecycleBin
|
||
}
|