Files
1Panel/agent/app/service/recycle_bin.go

515 lines
13 KiB
Go

package service
import (
"bytes"
"fmt"
"math"
"net/url"
"os"
"path"
"strconv"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/1Panel-dev/1Panel/agent/app/dto/response"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/utils/files"
"github.com/1Panel-dev/1Panel/agent/utils/re"
"github.com/shirou/gopsutil/v4/disk"
"gopkg.in/ini.v1"
)
const (
recycleBinClashDir = ".1panel_clash"
recycleBinFilesSubdir = "files"
recycleBinInfoSubdir = "info"
trashInfoSuffix = ".trashinfo"
trashInfoSection = "Trash Info"
trashInfoTimeLayout = "2006-01-02T15:04:05"
)
type RecycleBinService struct {
}
type IRecycleBinService interface {
Page(search dto.PageInfo) (int64, []response.RecycleBinDTO, error)
Create(create request.RecycleBinCreate) error
Reduce(reduce request.RecycleBinReduce) error
Clear() error
}
func NewIRecycleBinService() IRecycleBinService {
return &RecycleBinService{}
}
func (r RecycleBinService) Page(search dto.PageInfo) (int64, []response.RecycleBinDTO, error) {
var result []response.RecycleBinDTO
partitions, err := disk.Partitions(false)
if err != nil {
return 0, nil, err
}
op := files.NewFileOp()
for _, p := range partitions {
clashRoot := path.Join(p.Mountpoint, recycleBinClashDir)
if !op.Stat(clashRoot) {
continue
}
result = append(result, collectTrashEntries(clashRoot)...)
result = append(result, collectLegacyEntries(clashRoot)...)
}
if search.PageSize <= 0 {
return int64(len(result)), []response.RecycleBinDTO{}, nil
}
page := search.Page
if page < 1 {
page = 1
}
startIndex := (page - 1) * search.PageSize
endIndex := startIndex + search.PageSize
if startIndex >= len(result) {
return int64(len(result)), []response.RecycleBinDTO{}, nil
}
if endIndex > len(result) {
endIndex = len(result)
}
return int64(len(result)), result[startIndex:endIndex], nil
}
func (r RecycleBinService) Create(create request.RecycleBinCreate) error {
if files.IsProtected(create.SourcePath) {
return buserr.New("ErrPathNotDelete")
}
op := files.NewFileOp()
if !op.Stat(create.SourcePath) {
return buserr.New("ErrLinkPathNotFound")
}
clashRoot, err := getClashDir(create.SourcePath)
if err != nil {
return err
}
filesDir, infoDir, err := ensureTrashDirs(clashRoot)
if err != nil {
return err
}
deleteTime := time.Now()
openFile, err := op.OpenFile(create.SourcePath)
if err != nil {
return err
}
fileInfo, err := openFile.Stat()
_ = openFile.Close()
if err != nil {
return err
}
size := int64(0)
if fileInfo.IsDir() {
sizeF, err := op.GetDirSize(create.SourcePath)
if err != nil {
return err
}
size = int64(sizeF)
} else {
size = fileInfo.Size()
}
rName := allocateTrashEntryName(filesDir, infoDir, create.SourcePath)
info := trashInfo{
Path: create.SourcePath,
DeletionDate: deleteTime,
Size: size,
IsDir: fileInfo.IsDir(),
}
infoPath := path.Join(infoDir, rName+trashInfoSuffix)
if err := writeTrashInfo(infoPath, info); err != nil {
return err
}
targetPath := path.Join(filesDir, rName)
if err := op.Mv(create.SourcePath, targetPath); err != nil {
if rmErr := os.Remove(infoPath); rmErr != nil && !os.IsNotExist(rmErr) {
global.LOG.Warnf("rollback trashinfo failed for %s: %v", infoPath, rmErr)
}
return err
}
return nil
}
func (r RecycleBinService) Reduce(reduce request.RecycleBinReduce) error {
filePath := path.Join(reduce.From, reduce.RName)
op := files.NewFileOp()
if !op.Stat(filePath) {
return buserr.New("ErrLinkPathNotFound")
}
recycleBinDTO, err := loadRecycleBinDTO(reduce.From, reduce.RName)
if err != nil {
return err
}
if recycleBinDTO.SourcePath == "" {
return buserr.New("ErrSourcePathNotFound")
}
if !op.Stat(path.Dir(recycleBinDTO.SourcePath)) {
return buserr.New("ErrSourcePathNotFound")
}
if op.Stat(recycleBinDTO.SourcePath) {
if err = op.RmRf(recycleBinDTO.SourcePath); err != nil {
return err
}
}
if err := op.Mv(filePath, recycleBinDTO.SourcePath); err != nil {
return err
}
if err := cleanupTrashInfoByEntryPath(filePath); err != nil {
global.LOG.Warnf("cleanup trashinfo failed for %s: %v", filePath, err)
}
return nil
}
func (r RecycleBinService) Clear() error {
partitions, err := disk.Partitions(false)
if err != nil {
return err
}
op := files.NewFileOp()
for _, p := range partitions {
dir := path.Join(p.Mountpoint, recycleBinClashDir)
if !op.Stat(dir) {
continue
}
newDir := path.Join(p.Mountpoint, recycleBinClashDir+"-"+strings.ReplaceAll(common.GetUuid(), "-", ""))
if err := op.Mv(dir, newDir); err != nil {
return err
}
go func(target string) {
defer func() {
if r := recover(); r != nil {
global.LOG.Warnf("clear recycle bin panic on %s: %v", target, r)
}
}()
_ = op.DeleteDir(target)
}(newDir)
}
return nil
}
func getClashDir(realPath string) (string, error) {
partitions, err := disk.Partitions(false)
if err != nil {
return "", err
}
for _, p := range partitions {
if p.Mountpoint == "/" {
continue
}
if strings.HasPrefix(realPath, p.Mountpoint) {
clashDir := path.Join(p.Mountpoint, recycleBinClashDir)
if err = createClashDir(clashDir); err != nil {
return "", err
}
return clashDir, nil
}
}
return global.Dir.RecycleBinDir, createClashDir(global.Dir.RecycleBinDir)
}
func createClashDir(clashDir string) error {
op := files.NewFileOp()
if !op.Stat(clashDir) {
if err := op.CreateDir(clashDir, constant.DirPerm); err != nil {
return err
}
}
return nil
}
func ensureTrashDirs(clashRoot string) (string, string, error) {
filesDir := path.Join(clashRoot, recycleBinFilesSubdir)
infoDir := path.Join(clashRoot, recycleBinInfoSubdir)
op := files.NewFileOp()
if !op.Stat(filesDir) {
if err := op.CreateDir(filesDir, constant.DirPerm); err != nil {
return "", "", err
}
}
if !op.Stat(infoDir) {
if err := op.CreateDir(infoDir, constant.DirPerm); err != nil {
return "", "", err
}
}
return filesDir, infoDir, nil
}
func trashFilesDir(clashRoot string) string {
return path.Join(clashRoot, recycleBinFilesSubdir)
}
func trashInfoDir(clashRoot string) string {
return path.Join(clashRoot, recycleBinInfoSubdir)
}
func trashInfoPath(infoDir, entryName string) string {
return path.Join(infoDir, entryName+trashInfoSuffix)
}
type trashInfo struct {
Path string
DeletionDate time.Time
Size int64
IsDir bool
}
func writeTrashInfo(dstPath string, info trashInfo) error {
cfg := ini.Empty()
section, err := cfg.NewSection(trashInfoSection)
if err != nil {
return err
}
if _, err := section.NewKey("Path", url.PathEscape(info.Path)); err != nil {
return err
}
if _, err := section.NewKey("DeletionDate", info.DeletionDate.Format(trashInfoTimeLayout)); err != nil {
return err
}
if _, err := section.NewKey("Size", strconv.FormatInt(info.Size, 10)); err != nil {
return err
}
if _, err := section.NewKey("IsDir", strconv.FormatBool(info.IsDir)); err != nil {
return err
}
var buf bytes.Buffer
if _, err := cfg.WriteTo(&buf); err != nil {
return err
}
tmp := dstPath + ".tmp"
if err := os.WriteFile(tmp, buf.Bytes(), constant.FilePerm); err != nil {
return err
}
return os.Rename(tmp, dstPath)
}
func readTrashInfo(srcPath string) (*trashInfo, error) {
cfg, err := ini.Load(srcPath)
if err != nil {
return nil, err
}
section, err := cfg.GetSection(trashInfoSection)
if err != nil {
return nil, err
}
rawPath := section.Key("Path").Value()
decodedPath, err := url.PathUnescape(rawPath)
if err != nil {
decodedPath = rawPath
}
info := &trashInfo{
Path: decodedPath,
IsDir: section.Key("IsDir").MustBool(false),
}
if deletionStr := section.Key("DeletionDate").Value(); deletionStr != "" {
if t, parseErr := time.ParseInLocation(trashInfoTimeLayout, deletionStr, time.Local); parseErr == nil {
info.DeletionDate = t
}
}
if sizeStr := section.Key("Size").Value(); sizeStr != "" {
if sz, sizeErr := strconv.ParseInt(sizeStr, 10, 64); sizeErr == nil {
info.Size = sz
}
}
return info, nil
}
func collectTrashEntries(clashRoot string) []response.RecycleBinDTO {
filesDir := trashFilesDir(clashRoot)
infoDir := trashInfoDir(clashRoot)
op := files.NewFileOp()
if !op.Stat(filesDir) {
return nil
}
entries, err := os.ReadDir(filesDir)
if err != nil {
global.LOG.Warnf("read recycle files dir %s failed: %v", filesDir, err)
return nil
}
var result []response.RecycleBinDTO
for _, entry := range entries {
dto, err := buildTrashDTO(filesDir, infoDir, entry)
if err != nil {
global.LOG.Warnf("build recycle dto for %s failed: %v", entry.Name(), err)
continue
}
result = append(result, *dto)
}
return result
}
func collectLegacyEntries(clashRoot string) []response.RecycleBinDTO {
op := files.NewFileOp()
if !op.Stat(clashRoot) {
return nil
}
entries, err := os.ReadDir(clashRoot)
if err != nil {
return nil
}
var result []response.RecycleBinDTO
for _, entry := range entries {
name := entry.Name()
if name == recycleBinFilesSubdir || name == recycleBinInfoSubdir {
continue
}
if !strings.HasPrefix(name, "_1p_") {
continue
}
dto, err := getRecycleBinDTOFromName(name)
if err != nil {
continue
}
dto.IsDir = entry.IsDir()
dto.From = clashRoot
result = append(result, *dto)
}
return result
}
func buildTrashDTO(filesDir, infoDir string, entry os.DirEntry) (*response.RecycleBinDTO, error) {
entryName := entry.Name()
infoPath := trashInfoPath(infoDir, entryName)
if info, err := readTrashInfo(infoPath); err == nil {
return trashInfoToDTO(entryName, filesDir, info), nil
} else if !os.IsNotExist(err) {
global.LOG.Warnf("read trashinfo %s failed: %v", infoPath, err)
}
entryPath := path.Join(filesDir, entryName)
fi, err := os.Stat(entryPath)
if err != nil {
return nil, err
}
size := fi.Size()
if fi.IsDir() {
if sz, sizeErr := files.NewFileOp().GetDirSize(entryPath); sizeErr == nil {
size = int64(sz)
}
}
return &response.RecycleBinDTO{
Name: entryName,
Size: clampToInt(size),
Type: "file",
DeleteTime: fi.ModTime(),
RName: entryName,
SourcePath: "",
IsDir: fi.IsDir(),
From: filesDir,
}, nil
}
func trashInfoToDTO(entryName, filesDir string, info *trashInfo) *response.RecycleBinDTO {
return &response.RecycleBinDTO{
Name: path.Base(info.Path),
Size: clampToInt(info.Size),
Type: "file",
DeleteTime: info.DeletionDate,
RName: entryName,
SourcePath: info.Path,
IsDir: info.IsDir,
From: filesDir,
}
}
func loadRecycleBinDTO(from, name string) (*response.RecycleBinDTO, error) {
if path.Base(from) == recycleBinFilesSubdir {
infoDir := trashInfoDir(path.Dir(from))
info, err := readTrashInfo(trashInfoPath(infoDir, name))
if err == nil {
return trashInfoToDTO(name, from, info), nil
}
if !os.IsNotExist(err) {
global.LOG.Warnf("read trashinfo for %s failed: %v", name, err)
}
}
dto, err := getRecycleBinDTOFromName(name)
if err != nil {
return nil, err
}
dto.From = from
return dto, nil
}
func cleanupTrashInfoByEntryPath(filePath string) error {
cleaned := path.Clean(filePath)
parent := path.Dir(cleaned)
if path.Base(parent) != recycleBinFilesSubdir {
return nil
}
clashRoot := path.Dir(parent)
if path.Base(clashRoot) != recycleBinClashDir {
return nil
}
infoPath := trashInfoPath(trashInfoDir(clashRoot), path.Base(cleaned))
if err := os.Remove(infoPath); err != nil && !os.IsNotExist(err) {
return err
}
return nil
}
func allocateTrashEntryName(filesDir, infoDir, sourcePath string) string {
ext := path.Ext(path.Base(sourcePath))
op := files.NewFileOp()
for i := 0; i < 5; i++ {
name := strings.ReplaceAll(common.GetUuid(), "-", "") + ext
if op.Stat(path.Join(filesDir, name)) {
continue
}
if op.Stat(trashInfoPath(infoDir, name)) {
continue
}
return name
}
return fmt.Sprintf("%s-%d%s", strings.ReplaceAll(common.GetUuid(), "-", ""), time.Now().UnixNano(), ext)
}
func clampToInt(v int64) int {
if v > math.MaxInt {
return math.MaxInt
}
if v < math.MinInt {
return math.MinInt
}
return int(v)
}
func getRecycleBinDTOFromName(filename string) (*response.RecycleBinDTO, error) {
matches := re.GetRegex(re.RecycleBinFilePattern).FindStringSubmatch(filename)
if len(matches) != 4 {
return nil, fmt.Errorf("invalid filename format")
}
sourcePath := "/" + strings.ReplaceAll(matches[1], "_1p_", "/")
size, err := strconv.ParseInt(matches[2], 10, 64)
if err != nil {
return nil, err
}
if size < math.MinInt || size > math.MaxInt {
return nil, fmt.Errorf("size out of int range")
}
deleteTime, err := strconv.ParseInt(matches[3], 10, 64)
if err != nil {
return nil, err
}
return &response.RecycleBinDTO{
Name: path.Base(sourcePath),
Size: int(size),
Type: "file",
DeleteTime: time.Unix(deleteTime, 0),
SourcePath: sourcePath,
RName: filename,
}, nil
}