184 lines
4.4 KiB
Go
184 lines
4.4 KiB
Go
package commands
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/goravel/framework/contracts/console"
|
|
"github.com/goravel/framework/contracts/console/command"
|
|
"github.com/goravel/framework/facades"
|
|
"github.com/goravel/framework/support/path"
|
|
|
|
"goravel/app/utils"
|
|
)
|
|
|
|
type ClearChunks struct {
|
|
}
|
|
|
|
// Signature The name and signature of the console command.
|
|
func (r *ClearChunks) Signature() string {
|
|
return "app:clear-chunks"
|
|
}
|
|
|
|
// Description The console command description.
|
|
func (r *ClearChunks) Description() string {
|
|
return "清理3天前的分片文件"
|
|
}
|
|
|
|
// Extend The console command extend.
|
|
func (r *ClearChunks) Extend() command.Extend {
|
|
return command.Extend{Category: "app"}
|
|
}
|
|
|
|
// Handle Execute the console command.
|
|
func (r *ClearChunks) Handle(ctx console.Context) error {
|
|
// 从数据库读取文件存储配置
|
|
disk := utils.GetConfigValue("storage", "file_disk", "")
|
|
if disk == "" {
|
|
disk = "local"
|
|
}
|
|
|
|
// 检查存储驱动是否为本地存储
|
|
if disk != "local" && disk != "public" {
|
|
ctx.Info(fmt.Sprintf("当前存储驱动为 %s,清理分片文件功能仅支持本地存储,跳过清理", disk))
|
|
return nil
|
|
}
|
|
|
|
storage := facades.Storage().Disk(disk)
|
|
|
|
// 计算3天前的时间
|
|
threeDaysAgo := time.Now().AddDate(0, 0, -3)
|
|
ctx.Info(fmt.Sprintf("开始清理3天前的分片文件(%s之前创建的文件)...", threeDaysAgo.Format(utils.DateTimeFormat)))
|
|
|
|
// 获取存储根目录
|
|
var storageRoot string
|
|
if disk == "public" {
|
|
storageRoot = path.Storage("app/public")
|
|
} else {
|
|
storageRoot = path.Storage("app")
|
|
}
|
|
|
|
chunksDir := filepath.Join(storageRoot, "chunks")
|
|
|
|
// 检查目录是否存在
|
|
if _, err := os.Stat(chunksDir); os.IsNotExist(err) {
|
|
ctx.Info("分片目录不存在,无需清理")
|
|
return nil
|
|
}
|
|
|
|
cleanedCount := 0
|
|
cleanedSize := int64(0)
|
|
errorCount := 0
|
|
|
|
// 遍历 chunks 目录下的所有文件
|
|
err := filepath.Walk(chunksDir, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
// 如果无法访问某个文件/目录,记录错误但继续
|
|
ctx.Info(fmt.Sprintf("无法访问路径 %s: %v", path, err))
|
|
errorCount++
|
|
return nil
|
|
}
|
|
|
|
// 跳过根目录本身
|
|
if path == chunksDir {
|
|
return nil
|
|
}
|
|
|
|
// 只处理文件(分片文件)
|
|
if !info.IsDir() {
|
|
// 跳过 .gitignore 文件,避免误删
|
|
if info.Name() == ".gitignore" {
|
|
return nil
|
|
}
|
|
|
|
// 检查文件修改时间
|
|
if info.ModTime().Before(threeDaysAgo) {
|
|
// 使用 Storage 接口删除文件(保持一致性)
|
|
relativePath := strings.TrimPrefix(path, storageRoot+string(filepath.Separator))
|
|
relativePath = strings.ReplaceAll(relativePath, string(filepath.Separator), "/")
|
|
|
|
if err := storage.Delete(relativePath); err != nil {
|
|
ctx.Info(fmt.Sprintf("删除分片文件失败 %s: %v", relativePath, err))
|
|
errorCount++
|
|
} else {
|
|
cleanedCount++
|
|
cleanedSize += info.Size()
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
ctx.Error(fmt.Sprintf("遍历分片目录失败: %v", err))
|
|
return err
|
|
}
|
|
|
|
// 清理空目录(避免留下大量空目录)
|
|
emptyDirCount := r.cleanupEmptyDirs(chunksDir, ctx)
|
|
|
|
ctx.Info(fmt.Sprintf("分片文件清理完成!已清理 %d 个文件,释放空间 %s,删除 %d 个空目录,错误数: %d",
|
|
cleanedCount,
|
|
r.formatFileSize(cleanedSize),
|
|
emptyDirCount,
|
|
errorCount))
|
|
|
|
return nil
|
|
}
|
|
|
|
// cleanupEmptyDirs 清理空目录
|
|
func (r *ClearChunks) cleanupEmptyDirs(rootDir string, _ console.Context) int {
|
|
var dirs []string
|
|
|
|
err := filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
if info.IsDir() && path != rootDir {
|
|
dirs = append(dirs, path)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
|
|
// 反向遍历目录(从最深层的开始)
|
|
removedCount := 0
|
|
for i := len(dirs) - 1; i >= 0; i-- {
|
|
dir := dirs[i]
|
|
// 检查目录是否为空
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if len(entries) == 0 {
|
|
// 目录为空,删除它
|
|
if err := os.Remove(dir); err == nil {
|
|
removedCount++
|
|
}
|
|
}
|
|
}
|
|
|
|
return removedCount
|
|
}
|
|
|
|
// formatFileSize 格式化文件大小
|
|
func (r *ClearChunks) formatFileSize(size int64) string {
|
|
const unit = 1024
|
|
if size < unit {
|
|
return fmt.Sprintf("%d B", size)
|
|
}
|
|
div, exp := int64(unit), 0
|
|
for n := size / unit; n >= unit; n /= unit {
|
|
div *= unit
|
|
exp++
|
|
}
|
|
return fmt.Sprintf("%.1f %cB", float64(size)/float64(div), "KMGTPE"[exp])
|
|
}
|