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

383 lines
8.9 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 (
"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
// GetRecycleBin 获取全局回收站实例
func GetRecycleBin() *RecycleBin {
return globalRecycleBin
}