You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1138 lines
35 KiB
Go
1138 lines
35 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
|
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
|
"github.com/1Panel-dev/1Panel/agent/app/task"
|
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
|
"github.com/1Panel-dev/1Panel/agent/global"
|
|
"github.com/1Panel-dev/1Panel/agent/i18n"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
|
fileUtils "github.com/1Panel-dev/1Panel/agent/utils/files"
|
|
"github.com/docker/docker/api/types"
|
|
"github.com/docker/docker/api/types/build"
|
|
"github.com/docker/docker/api/types/filters"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
const (
|
|
rollbackPath = "1panel/tmp"
|
|
upgradePath = "1panel/tmp/upgrade"
|
|
uploadPath = "1panel/uploads"
|
|
downloadPath = "1panel/download"
|
|
)
|
|
|
|
func (u *DeviceService) Scan() dto.CleanData {
|
|
var (
|
|
SystemClean dto.CleanData
|
|
treeData []dto.CleanTree
|
|
)
|
|
fileOp := fileUtils.NewFileOp()
|
|
|
|
originalPath := path.Join(global.Dir.BaseDir, "1panel_original")
|
|
originalSize, _ := fileOp.GetDirSize(originalPath)
|
|
treeData = append(treeData, dto.CleanTree{
|
|
ID: uuid.NewString(),
|
|
Label: "1panel_original",
|
|
Size: uint64(originalSize),
|
|
IsCheck: originalSize > 0,
|
|
IsRecommend: true,
|
|
Type: "1panel_original",
|
|
Children: loadTreeWithDir(true, "1panel_original", originalPath, fileOp),
|
|
})
|
|
treeData = append(treeData, loadUpgradeTree(fileOp))
|
|
treeData = append(treeData, loadAgentPackage(fileOp))
|
|
|
|
SystemClean.BackupClean = loadBackupTree(fileOp)
|
|
|
|
rollBackTree := loadRollBackTree(fileOp)
|
|
rollbackSize := uint64(0)
|
|
for _, rollback := range rollBackTree {
|
|
rollbackSize += rollback.Size
|
|
}
|
|
treeData = append(treeData, dto.CleanTree{
|
|
ID: uuid.NewString(),
|
|
Label: "rollback",
|
|
Size: rollbackSize,
|
|
IsCheck: rollbackSize > 0,
|
|
IsRecommend: true,
|
|
Type: "rollback",
|
|
Children: rollBackTree,
|
|
})
|
|
SystemClean.SystemClean = treeData
|
|
|
|
uploadTreeData := loadUploadTree(fileOp)
|
|
SystemClean.UploadClean = append(SystemClean.UploadClean, uploadTreeData...)
|
|
|
|
downloadTreeData := loadDownloadTree(fileOp)
|
|
SystemClean.DownloadClean = append(SystemClean.DownloadClean, downloadTreeData...)
|
|
|
|
logTree := loadLogTree(fileOp)
|
|
SystemClean.SystemLogClean = append(SystemClean.SystemLogClean, logTree...)
|
|
|
|
containerTree := loadContainerTree()
|
|
SystemClean.ContainerClean = append(SystemClean.ContainerClean, containerTree...)
|
|
|
|
return SystemClean
|
|
}
|
|
|
|
func (u *DeviceService) Clean(req []dto.Clean) {
|
|
size := uint64(0)
|
|
for _, item := range req {
|
|
size += item.Size
|
|
switch item.TreeType {
|
|
case "1panel_original":
|
|
dropFileOrDir(path.Join(global.Dir.BaseDir, "1panel_original", item.Name))
|
|
|
|
case "upgrade":
|
|
dropFileOrDir(path.Join(global.Dir.BaseDir, upgradePath, item.Name))
|
|
|
|
case "agent":
|
|
dropFileOrDir(path.Join(global.Dir.BaseDir, "1panel/agent/package", item.Name))
|
|
|
|
case "tmp_backup":
|
|
dropFileOrDir(path.Join(global.Dir.LocalBackupDir, "tmp"))
|
|
case "unknown_backup":
|
|
if strings.HasPrefix(item.Name, path.Join(global.Dir.LocalBackupDir, "log/website")) {
|
|
dropFileOrDir(item.Name)
|
|
} else {
|
|
dropFile(item.Name)
|
|
}
|
|
|
|
case "rollback":
|
|
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "app"))
|
|
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "database"))
|
|
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "website"))
|
|
case "rollback_app":
|
|
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "app", item.Name))
|
|
case "rollback_database":
|
|
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "database", item.Name))
|
|
case "rollback_website":
|
|
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "website", item.Name))
|
|
|
|
case "upload":
|
|
dropFileOrDir(path.Join(global.Dir.BaseDir, uploadPath, item.Name))
|
|
case "upload_app":
|
|
dropFileOrDir(path.Join(global.Dir.BaseDir, uploadPath, "app", item.Name))
|
|
case "upload_database":
|
|
dropFileOrDir(path.Join(global.Dir.BaseDir, uploadPath, "database", item.Name))
|
|
case "upload_website":
|
|
dropFileOrDir(path.Join(global.Dir.BaseDir, uploadPath, "website", item.Name))
|
|
case "download":
|
|
dropFileOrDir(path.Join(global.Dir.BaseDir, downloadPath, item.Name))
|
|
case "download_app":
|
|
dropFileOrDir(path.Join(global.Dir.BaseDir, downloadPath, "app", item.Name))
|
|
case "download_database":
|
|
dropFileOrDir(path.Join(global.Dir.BaseDir, downloadPath, "database", item.Name))
|
|
case "download_website":
|
|
dropFileOrDir(path.Join(global.Dir.BaseDir, downloadPath, "website", item.Name))
|
|
|
|
case "system_log":
|
|
if len(item.Name) == 0 {
|
|
files, _ := os.ReadDir(global.Dir.LogDir)
|
|
if len(files) == 0 {
|
|
continue
|
|
}
|
|
for _, file := range files {
|
|
if file.Name() == "1Panel-Core.log" || file.Name() == "1Panel.log" || file.IsDir() {
|
|
continue
|
|
}
|
|
dropFileOrDir(path.Join(global.Dir.LogDir, file.Name()))
|
|
}
|
|
} else {
|
|
dropFileOrDir(path.Join(global.Dir.LogDir, item.Name))
|
|
}
|
|
case "task_log":
|
|
if len(item.Name) == 0 {
|
|
files, _ := os.ReadDir(global.Dir.TaskDir)
|
|
if len(files) == 0 {
|
|
continue
|
|
}
|
|
for _, file := range files {
|
|
if file.Name() == "ssl" || !file.IsDir() {
|
|
continue
|
|
}
|
|
dropTaskLog(path.Join(global.Dir.TaskDir, file.Name()))
|
|
}
|
|
} else {
|
|
dropTaskLog(path.Join(global.Dir.TaskDir, item.Name))
|
|
}
|
|
case "website_log":
|
|
dropWebsiteLog(item.Name)
|
|
case "script":
|
|
dropFileOrDir(path.Join(global.Dir.TmpDir, "script", item.Name))
|
|
case "images":
|
|
_, _ = dropImages()
|
|
case "containers":
|
|
_, _ = dropContainers()
|
|
case "volumes":
|
|
_, _ = dropVolumes()
|
|
case "build_cache":
|
|
_, _ = dropBuildCache()
|
|
case "app_tmp_download_version":
|
|
dropFileOrDir(path.Join(global.Dir.RemoteAppResourceDir, item.Name))
|
|
}
|
|
}
|
|
|
|
_ = cleanEmptyDirs(global.Dir.LocalBackupDir)
|
|
_ = settingRepo.Update("LastCleanTime", time.Now().Format(constant.DateTimeLayout))
|
|
_ = settingRepo.Update("LastCleanSize", fmt.Sprintf("%v", size))
|
|
_ = settingRepo.Update("LastCleanData", fmt.Sprintf("%v", len(req)))
|
|
}
|
|
|
|
func doSystemClean(taskItem *task.Task) func(t *task.Task) error {
|
|
return func(t *task.Task) error {
|
|
size := int64(0)
|
|
fileCount := 0
|
|
dropWithTask(path.Join(global.Dir.BaseDir, "1panel_original"), taskItem, &size, &fileCount)
|
|
|
|
upgradePath := path.Join(global.Dir.BaseDir, upgradePath)
|
|
upgradeFiles, _ := os.ReadDir(upgradePath)
|
|
if len(upgradeFiles) != 0 {
|
|
sort.Slice(upgradeFiles, func(i, j int) bool {
|
|
return upgradeFiles[i].Name() > upgradeFiles[j].Name()
|
|
})
|
|
for i := 0; i < len(upgradeFiles); i++ {
|
|
if i != 0 {
|
|
dropWithTask(path.Join(upgradePath, upgradeFiles[i].Name()), taskItem, &size, &fileCount)
|
|
}
|
|
}
|
|
}
|
|
|
|
dropWithTask(path.Join(global.Dir.LocalBackupDir, "tmp/system"), taskItem, &size, &fileCount)
|
|
|
|
dropWithTask(path.Join(global.Dir.BaseDir, rollbackPath, "app"), taskItem, &size, &fileCount)
|
|
dropWithTask(path.Join(global.Dir.BaseDir, rollbackPath, "website"), taskItem, &size, &fileCount)
|
|
dropWithTask(path.Join(global.Dir.BaseDir, rollbackPath, "database"), taskItem, &size, &fileCount)
|
|
|
|
upgrades := path.Join(global.Dir.BaseDir, upgradePath)
|
|
oldUpgradeFiles, _ := os.ReadDir(upgrades)
|
|
if len(oldUpgradeFiles) != 0 {
|
|
for i := 0; i < len(oldUpgradeFiles); i++ {
|
|
dropWithTask(path.Join(upgrades, oldUpgradeFiles[i].Name()), taskItem, &size, &fileCount)
|
|
}
|
|
}
|
|
|
|
dropWithExclude(path.Join(global.Dir.BaseDir, uploadPath), []string{"theme"}, taskItem, &size, &fileCount)
|
|
dropWithTask(path.Join(global.Dir.BaseDir, downloadPath), taskItem, &size, &fileCount)
|
|
|
|
logFiles, _ := os.ReadDir(global.Dir.LogDir)
|
|
if len(logFiles) != 0 {
|
|
for i := 0; i < len(logFiles); i++ {
|
|
if logFiles[i].IsDir() {
|
|
continue
|
|
}
|
|
if logFiles[i].Name() != "1Panel.log" && logFiles[i].Name() != "1Panel-Core.log" {
|
|
dropWithTask(path.Join(global.Dir.LogDir, logFiles[i].Name()), taskItem, &size, &fileCount)
|
|
}
|
|
}
|
|
}
|
|
|
|
count1, size1 := dropVolumes()
|
|
size += int64(size1)
|
|
fileCount += count1
|
|
count2, size2 := dropBuildCache()
|
|
size += int64(size2)
|
|
fileCount += count2
|
|
|
|
timeNow := time.Now().Format(constant.DateTimeLayout)
|
|
if fileCount != 0 {
|
|
taskItem.Log(i18n.GetMsgWithMap("FileDropSum", map[string]interface{}{"size": common.LoadSizeUnit2F(float64(size)), "count": fileCount}))
|
|
}
|
|
|
|
_ = settingRepo.Update("LastCleanTime", timeNow)
|
|
_ = settingRepo.Update("LastCleanSize", fmt.Sprintf("%v", size))
|
|
_ = settingRepo.Update("LastCleanData", fmt.Sprintf("%v", fileCount))
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func loadUpgradeTree(fileOp fileUtils.FileOp) dto.CleanTree {
|
|
upgradePath := path.Join(global.Dir.BaseDir, upgradePath)
|
|
upgradeSize, _ := fileOp.GetDirSize(upgradePath)
|
|
upgradeTree := dto.CleanTree{
|
|
ID: uuid.NewString(),
|
|
Label: "upgrade",
|
|
Size: uint64(upgradeSize),
|
|
IsCheck: false,
|
|
IsRecommend: true,
|
|
Type: "upgrade",
|
|
Children: loadTreeWithDir(true, "upgrade", upgradePath, fileOp),
|
|
}
|
|
if len(upgradeTree.Children) != 0 {
|
|
sort.Slice(upgradeTree.Children, func(i, j int) bool {
|
|
return common.CompareVersion(upgradeTree.Children[i].Label, upgradeTree.Children[j].Label)
|
|
})
|
|
if global.IsMaster {
|
|
var copiesSetting model.Setting
|
|
_ = global.CoreDB.Where("key = ?", "UpgradeBackupCopies").First(&copiesSetting).Error
|
|
copies, _ := strconv.Atoi(copiesSetting.Value)
|
|
if copies == 0 || copies > len(upgradeTree.Children) {
|
|
copies = len(upgradeTree.Children)
|
|
}
|
|
for i := 0; i < copies; i++ {
|
|
upgradeTree.Children[i].IsCheck = false
|
|
upgradeTree.Children[i].IsRecommend = false
|
|
}
|
|
} else {
|
|
upgradeTree.Children[0].IsCheck = false
|
|
upgradeTree.Children[0].IsRecommend = false
|
|
}
|
|
}
|
|
return upgradeTree
|
|
}
|
|
|
|
func loadAgentPackage(fileOp fileUtils.FileOp) dto.CleanTree {
|
|
pathItem := path.Join(global.Dir.BaseDir, "1panel/agent/package")
|
|
itemTree := dto.CleanTree{
|
|
ID: uuid.NewString(),
|
|
Label: "agent_packages",
|
|
IsCheck: false,
|
|
IsRecommend: true,
|
|
Type: "agent",
|
|
}
|
|
files, _ := os.ReadDir(pathItem)
|
|
for _, file := range files {
|
|
if file.IsDir() {
|
|
itemSize, _ := fileOp.GetDirSize(path.Join(pathItem, file.Name()))
|
|
itemTree.Size += uint64(itemSize)
|
|
itemTree.Children = append(itemTree.Children, dto.CleanTree{
|
|
ID: uuid.NewString(),
|
|
Label: file.Name(),
|
|
Name: file.Name(),
|
|
Size: uint64(itemSize),
|
|
IsCheck: true,
|
|
IsRecommend: true,
|
|
Type: "agent",
|
|
})
|
|
} else {
|
|
itemSize, _ := file.Info()
|
|
itemName := file.Name()
|
|
isCurrentVersion := strings.HasPrefix(itemName, fmt.Sprintf("1panel-agent_%s_", global.CONF.Base.Version))
|
|
if isCurrentVersion {
|
|
continue
|
|
}
|
|
itemTree.Size += uint64(itemSize.Size())
|
|
itemTree.Children = append(itemTree.Children, dto.CleanTree{
|
|
ID: uuid.NewString(),
|
|
Label: itemName,
|
|
Name: itemName,
|
|
Size: uint64(itemSize.Size()),
|
|
IsCheck: !isCurrentVersion,
|
|
IsRecommend: true,
|
|
Type: "agent",
|
|
})
|
|
}
|
|
}
|
|
if itemTree.Size == 0 {
|
|
itemTree.IsCheck = false
|
|
}
|
|
return itemTree
|
|
}
|
|
|
|
func loadBackupTree(fileOp fileUtils.FileOp) []dto.CleanTree {
|
|
var treeData []dto.CleanTree
|
|
|
|
tmpSize, _ := fileOp.GetDirSize(path.Join(global.Dir.LocalBackupDir, "tmp"))
|
|
treeData = append(treeData, dto.CleanTree{
|
|
ID: uuid.NewString(),
|
|
Label: "tmp_backup",
|
|
Size: uint64(tmpSize),
|
|
IsCheck: tmpSize != 0,
|
|
IsRecommend: true,
|
|
Type: "tmp_backup",
|
|
})
|
|
backupRecords, _ := backupRepo.ListRecord()
|
|
var recordMap = make(map[string][]string)
|
|
for _, record := range backupRecords {
|
|
if val, ok := recordMap[record.FileDir]; ok {
|
|
val = append(val, record.FileName)
|
|
recordMap[record.FileDir] = val
|
|
} else {
|
|
recordMap[record.FileDir] = []string{record.FileName}
|
|
}
|
|
}
|
|
|
|
treeData = append(treeData, loadUnknownApps(fileOp, recordMap))
|
|
treeData = append(treeData, loadUnknownDbs(fileOp, recordMap))
|
|
treeData = append(treeData, loadUnknownWebsites(fileOp, recordMap))
|
|
treeData = append(treeData, loadUnknownSnapshot(fileOp))
|
|
treeData = append(treeData, loadUnknownWebsiteLog(fileOp))
|
|
return treeData
|
|
}
|
|
|
|
func loadUnknownApps(fileOp fileUtils.FileOp, recordMap map[string][]string) dto.CleanTree {
|
|
apps, _ := appInstallRepo.ListBy(context.Background())
|
|
var excludePaths []string
|
|
for _, app := range apps {
|
|
itemName := fmt.Sprintf("app/%s/%s", app.App.Key, app.Name)
|
|
if val, ok := recordMap[itemName]; ok {
|
|
for _, item := range val {
|
|
excludePaths = append(excludePaths, path.Join(global.Dir.LocalBackupDir, itemName, item))
|
|
}
|
|
}
|
|
}
|
|
backupPath := path.Join(global.Dir.LocalBackupDir, "app")
|
|
treeData := dto.CleanTree{
|
|
ID: uuid.NewString(),
|
|
Label: "unknown_app",
|
|
IsCheck: false,
|
|
IsRecommend: false,
|
|
Name: backupPath,
|
|
Type: "unknown_backup",
|
|
}
|
|
_ = loadFileOrDirWithExclude(fileOp, 0, backupPath, &treeData, excludePaths)
|
|
return treeData
|
|
}
|
|
func loadUnknownDbs(fileOp fileUtils.FileOp, recordMap map[string][]string) dto.CleanTree {
|
|
dbs, _ := databaseRepo.GetList()
|
|
var excludePaths []string
|
|
dbMap := make(map[string]struct{})
|
|
for _, db := range dbs {
|
|
dbMap[fmt.Sprintf("database/%s/%s", db.Type, db.Name)] = struct{}{}
|
|
}
|
|
for key, val := range recordMap {
|
|
itemName := path.Dir(key)
|
|
if _, ok := dbMap[itemName]; ok {
|
|
for _, item := range val {
|
|
excludePaths = append(excludePaths, path.Join(global.Dir.LocalBackupDir, key, item))
|
|
}
|
|
}
|
|
}
|
|
backupPath := path.Join(global.Dir.LocalBackupDir, "database")
|
|
treeData := dto.CleanTree{
|
|
ID: uuid.NewString(),
|
|
Label: "unknown_database",
|
|
Name: backupPath,
|
|
IsCheck: false,
|
|
IsRecommend: false,
|
|
Type: "unknown_backup",
|
|
}
|
|
_ = loadFileOrDirWithExclude(fileOp, 0, backupPath, &treeData, excludePaths)
|
|
return treeData
|
|
}
|
|
func loadUnknownWebsites(fileOp fileUtils.FileOp, recordMap map[string][]string) dto.CleanTree {
|
|
websites, _ := websiteRepo.List()
|
|
var excludePaths []string
|
|
for _, website := range websites {
|
|
itemName := fmt.Sprintf("website/%s", website.Alias)
|
|
if val, ok := recordMap[itemName]; ok {
|
|
for _, item := range val {
|
|
excludePaths = append(excludePaths, path.Join(global.Dir.LocalBackupDir, itemName, item))
|
|
}
|
|
}
|
|
}
|
|
backupPath := path.Join(global.Dir.LocalBackupDir, "website")
|
|
treeData := dto.CleanTree{
|
|
ID: uuid.NewString(),
|
|
Label: "unknown_website",
|
|
Name: backupPath,
|
|
IsCheck: false,
|
|
IsRecommend: false,
|
|
Type: "unknown_backup",
|
|
}
|
|
_ = loadFileOrDirWithExclude(fileOp, 0, backupPath, &treeData, excludePaths)
|
|
return treeData
|
|
}
|
|
func loadUnknownSnapshot(fileOp fileUtils.FileOp) dto.CleanTree {
|
|
snaps, _ := snapshotRepo.GetList()
|
|
var excludePaths []string
|
|
for _, item := range snaps {
|
|
excludePaths = append(excludePaths, path.Join(global.Dir.LocalBackupDir, "system_snapshot", item.Name+".tar.gz"))
|
|
}
|
|
backupPath := path.Join(global.Dir.LocalBackupDir, "system_snapshot")
|
|
treeData := dto.CleanTree{
|
|
ID: uuid.NewString(),
|
|
Label: "unknown_snapshot",
|
|
Name: backupPath,
|
|
IsCheck: false,
|
|
IsRecommend: false,
|
|
Type: "unknown_backup",
|
|
}
|
|
entries, _ := os.ReadDir(backupPath)
|
|
for _, entry := range entries {
|
|
childPath := filepath.Join(backupPath, entry.Name())
|
|
if isExactPathMatch(childPath, excludePaths) {
|
|
continue
|
|
}
|
|
childNode := dto.CleanTree{
|
|
ID: uuid.NewString(),
|
|
Label: entry.Name(),
|
|
IsCheck: false,
|
|
IsRecommend: false,
|
|
Name: childPath,
|
|
Type: "unknown_backup",
|
|
}
|
|
if entry.IsDir() {
|
|
itemSize, _ := fileOp.GetDirSize(childPath)
|
|
childNode.Size = uint64(itemSize)
|
|
childNode.IsCheck = true
|
|
childNode.IsRecommend = true
|
|
treeData.Size += childNode.Size
|
|
} else {
|
|
info, _ := entry.Info()
|
|
childNode.Size = uint64(info.Size())
|
|
treeData.Size += childNode.Size
|
|
}
|
|
|
|
treeData.Children = append(treeData.Children, childNode)
|
|
}
|
|
return treeData
|
|
}
|
|
|
|
func loadUnknownWebsiteLog(fileOp fileUtils.FileOp) dto.CleanTree {
|
|
treeData := dto.CleanTree{
|
|
ID: uuid.NewString(),
|
|
Label: "unknown_website_log",
|
|
IsCheck: false,
|
|
IsRecommend: true,
|
|
Type: "unknown_backup",
|
|
}
|
|
dir := path.Join(global.Dir.LocalBackupDir, "log/website")
|
|
websites, _ := websiteRepo.List()
|
|
websiteMap := make(map[string]struct{})
|
|
for _, website := range websites {
|
|
websiteMap[website.Alias] = struct{}{}
|
|
}
|
|
|
|
entries, _ := os.ReadDir(dir)
|
|
for _, entry := range entries {
|
|
if !entry.IsDir() {
|
|
continue
|
|
}
|
|
dirName := entry.Name()
|
|
if _, ok := websiteMap[dirName]; !ok {
|
|
dirPath := path.Join(dir, dirName)
|
|
itemSize, _ := fileOp.GetDirSize(dirPath)
|
|
childData := dto.CleanTree{
|
|
ID: uuid.NewString(),
|
|
Label: dirName,
|
|
IsCheck: true,
|
|
IsRecommend: true,
|
|
Name: dirPath,
|
|
Type: "unknown_backup",
|
|
Size: uint64(itemSize),
|
|
}
|
|
treeData.Size += uint64(itemSize)
|
|
treeData.Children = append(treeData.Children, childData)
|
|
}
|
|
}
|
|
if treeData.Size > 0 {
|
|
treeData.IsCheck = true
|
|
}
|
|
return treeData
|
|
}
|
|
|
|
func loadFileOrDirWithExclude(fileOp fileUtils.FileOp, index uint, dir string, rootTree *dto.CleanTree, excludes []string) error {
|
|
index++
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, entry := range entries {
|
|
childPath := filepath.Join(dir, entry.Name())
|
|
if isExactPathMatch(childPath, excludes) {
|
|
continue
|
|
}
|
|
childNode := dto.CleanTree{
|
|
ID: uuid.NewString(),
|
|
Label: entry.Name(),
|
|
IsCheck: false,
|
|
IsRecommend: false,
|
|
Name: childPath,
|
|
Type: "unknown_backup",
|
|
}
|
|
if entry.IsDir() {
|
|
if index < 4 {
|
|
if err = loadFileOrDirWithExclude(fileOp, index, childPath, &childNode, excludes); err != nil {
|
|
return err
|
|
}
|
|
childNode.Size = 0
|
|
for _, child := range childNode.Children {
|
|
childNode.Size += child.Size
|
|
}
|
|
rootTree.Size += childNode.Size
|
|
} else {
|
|
itemSize, _ := fileOp.GetDirSize(childPath)
|
|
childNode.Size = uint64(itemSize)
|
|
rootTree.Size += childNode.Size
|
|
}
|
|
} else {
|
|
info, _ := entry.Info()
|
|
childNode.Size = uint64(info.Size())
|
|
rootTree.Size += childNode.Size
|
|
}
|
|
|
|
rootTree.Children = append(rootTree.Children, childNode)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isExactPathMatch(path string, excludePaths []string) bool {
|
|
cleanPath := filepath.Clean(path)
|
|
|
|
for _, excludePath := range excludePaths {
|
|
cleanExclude := filepath.Clean(excludePath)
|
|
if cleanPath == cleanExclude {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func loadRollBackTree(fileOp fileUtils.FileOp) []dto.CleanTree {
|
|
var treeData []dto.CleanTree
|
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, rollbackPath, "app"), "rollback_app", fileOp)
|
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, rollbackPath, "website"), "rollback_website", fileOp)
|
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, rollbackPath, "database"), "rollback_database", fileOp)
|
|
|
|
return treeData
|
|
}
|
|
|
|
func loadUploadTree(fileOp fileUtils.FileOp) []dto.CleanTree {
|
|
var treeData []dto.CleanTree
|
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, uploadPath, "app"), "upload_app", fileOp)
|
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, uploadPath, "website"), "upload_website", fileOp)
|
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, uploadPath, "database"), "upload_database", fileOp)
|
|
|
|
path5 := path.Join(global.Dir.BaseDir, uploadPath)
|
|
uploadTreeData := loadTreeWithAllFile(true, path5, "upload", path5, fileOp)
|
|
treeData = append(treeData, uploadTreeData...)
|
|
|
|
return treeData
|
|
}
|
|
|
|
func loadDownloadTree(fileOp fileUtils.FileOp) []dto.CleanTree {
|
|
var treeData []dto.CleanTree
|
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, downloadPath, "app"), "download_app", fileOp)
|
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, downloadPath, "website"), "download_website", fileOp)
|
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, downloadPath, "database"), "download_database", fileOp)
|
|
|
|
path5 := path.Join(global.Dir.BaseDir, downloadPath)
|
|
uploadTreeData := loadTreeWithAllFile(true, path5, "download", path5, fileOp)
|
|
treeData = append(treeData, uploadTreeData...)
|
|
|
|
appTmpDownloadTree := loadAppTmpDownloadTree(fileOp)
|
|
if len(appTmpDownloadTree) > 0 {
|
|
parentTree := dto.CleanTree{
|
|
ID: uuid.NewString(),
|
|
Label: "app_tmp_download",
|
|
IsCheck: true,
|
|
IsRecommend: true,
|
|
Type: "app_tmp_download",
|
|
Name: "apps",
|
|
}
|
|
for _, child := range appTmpDownloadTree {
|
|
parentTree.Size += child.Size
|
|
}
|
|
parentTree.Children = appTmpDownloadTree
|
|
treeData = append(treeData, parentTree)
|
|
}
|
|
return treeData
|
|
}
|
|
|
|
func loadLogTree(fileOp fileUtils.FileOp) []dto.CleanTree {
|
|
var treeData []dto.CleanTree
|
|
path1 := path.Join(global.Dir.LogDir)
|
|
list1 := loadTreeWithAllFile(true, path1, "system_log", path1, fileOp)
|
|
size := uint64(0)
|
|
for _, file := range list1 {
|
|
size += file.Size
|
|
}
|
|
treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "system_log", Size: size, Children: list1, Type: "system_log", IsRecommend: true})
|
|
|
|
path2 := path.Join(global.Dir.TaskDir)
|
|
list2 := loadTreeWithDir(false, "task_log", path2, fileOp)
|
|
size2, _ := fileOp.GetDirSize(path2)
|
|
treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "task_log", Size: uint64(size2), Children: list2, Type: "task_log"})
|
|
|
|
websiteLogList := loadWebsiteLogTree(fileOp)
|
|
logTotalSize := uint64(0)
|
|
for _, websiteLog := range websiteLogList {
|
|
logTotalSize += websiteLog.Size
|
|
}
|
|
treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "website_log", Size: logTotalSize, Children: websiteLogList, Type: "website_log", IsRecommend: false})
|
|
|
|
return treeData
|
|
}
|
|
|
|
func loadWebsiteLogTree(fileOp fileUtils.FileOp) []dto.CleanTree {
|
|
websites, _ := websiteRepo.List()
|
|
if len(websites) == 0 {
|
|
return nil
|
|
}
|
|
var res []dto.CleanTree
|
|
for _, website := range websites {
|
|
size3, _ := fileOp.GetDirSize(path.Join(GetSiteDir(website.Alias), "log"))
|
|
res = append(res, dto.CleanTree{
|
|
ID: uuid.NewString(),
|
|
Label: website.PrimaryDomain,
|
|
Size: uint64(size3),
|
|
Type: "website_log",
|
|
Name: website.Alias,
|
|
})
|
|
}
|
|
return res
|
|
}
|
|
|
|
func loadAppTmpDownloadTree(fileOp fileUtils.FileOp) []dto.CleanTree {
|
|
appDirs, err := os.ReadDir(global.Dir.RemoteAppResourceDir)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
var res []dto.CleanTree
|
|
for _, appDir := range appDirs {
|
|
if !appDir.IsDir() {
|
|
continue
|
|
}
|
|
appKey := appDir.Name()
|
|
app, _ := appRepo.GetFirst(appRepo.WithKey(appKey))
|
|
if app.ID == 0 {
|
|
continue
|
|
}
|
|
appPath := filepath.Join(global.Dir.RemoteAppResourceDir, appKey)
|
|
versionDirs, err := os.ReadDir(appPath)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
appDetails, _ := appDetailRepo.GetBy(appDetailRepo.WithAppId(app.ID))
|
|
existingVersions := make(map[string]bool)
|
|
for _, appDetail := range appDetails {
|
|
existingVersions[appDetail.Version] = true
|
|
}
|
|
var missingVersions []string
|
|
for _, versionDir := range versionDirs {
|
|
if !versionDir.IsDir() {
|
|
continue
|
|
}
|
|
|
|
version := versionDir.Name()
|
|
if !existingVersions[version] {
|
|
missingVersions = append(missingVersions, version)
|
|
}
|
|
}
|
|
if len(missingVersions) > 0 {
|
|
var appTree dto.CleanTree
|
|
appTree.ID = uuid.NewString()
|
|
appTree.Label = app.Name
|
|
appTree.Type = "app_tmp_download"
|
|
appTree.Name = appKey
|
|
appTree.IsRecommend = true
|
|
appTree.IsCheck = true
|
|
for _, version := range missingVersions {
|
|
versionPath := filepath.Join(appPath, version)
|
|
size, _ := fileOp.GetDirSize(versionPath)
|
|
appTree.Size += uint64(size)
|
|
appTree.Children = append(appTree.Children, dto.CleanTree{
|
|
ID: uuid.NewString(),
|
|
Label: version,
|
|
Size: uint64(size),
|
|
IsCheck: true,
|
|
IsRecommend: true,
|
|
Type: "app_tmp_download_version",
|
|
Name: path.Join(appKey, version),
|
|
})
|
|
}
|
|
res = append(res, appTree)
|
|
}
|
|
}
|
|
return res
|
|
}
|
|
|
|
func loadContainerTree() []dto.CleanTree {
|
|
var treeData []dto.CleanTree
|
|
client, err := docker.NewDockerClient()
|
|
if err != nil {
|
|
return treeData
|
|
}
|
|
defer client.Close()
|
|
diskUsage, err := client.DiskUsage(context.Background(), types.DiskUsageOptions{})
|
|
if err != nil {
|
|
return treeData
|
|
}
|
|
imageSize := uint64(0)
|
|
for _, file := range diskUsage.Images {
|
|
if file.Containers == 0 {
|
|
imageSize += uint64(file.Size)
|
|
}
|
|
}
|
|
treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "container_images", Size: imageSize, Children: nil, Type: "images", IsRecommend: true})
|
|
|
|
containerSize := uint64(0)
|
|
for _, file := range diskUsage.Containers {
|
|
if file.State != "running" {
|
|
containerSize += uint64(file.SizeRw)
|
|
}
|
|
}
|
|
treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "container_containers", Size: containerSize, Children: nil, Type: "containers", IsRecommend: true})
|
|
|
|
volumeSize := uint64(0)
|
|
for _, file := range diskUsage.Volumes {
|
|
if file.UsageData.RefCount <= 0 {
|
|
volumeSize += uint64(file.UsageData.Size)
|
|
}
|
|
}
|
|
treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "container_volumes", Size: volumeSize, IsCheck: volumeSize > 0, Children: nil, Type: "volumes", IsRecommend: true})
|
|
|
|
var buildCacheTotalSize int64
|
|
for _, cache := range diskUsage.BuildCache {
|
|
if cache.Type == "source.local" {
|
|
buildCacheTotalSize += cache.Size
|
|
}
|
|
}
|
|
treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "build_cache", Size: uint64(buildCacheTotalSize), IsCheck: buildCacheTotalSize > 0, Type: "build_cache", IsRecommend: true})
|
|
return treeData
|
|
}
|
|
|
|
func loadTreeWithCheck(treeData []dto.CleanTree, pathItem, treeType string, fileOp fileUtils.FileOp) []dto.CleanTree {
|
|
size, _ := fileOp.GetDirSize(pathItem)
|
|
if size == 0 {
|
|
return treeData
|
|
}
|
|
list := loadTreeWithAllFile(true, pathItem, treeType, pathItem, fileOp)
|
|
treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: treeType, Size: uint64(size), IsCheck: size > 0, Children: list, Type: treeType, IsRecommend: true})
|
|
return treeData
|
|
}
|
|
|
|
func loadTreeWithDir(isCheck bool, treeType, pathItem string, fileOp fileUtils.FileOp) []dto.CleanTree {
|
|
var lists []dto.CleanTree
|
|
files, err := os.ReadDir(pathItem)
|
|
if err != nil {
|
|
return lists
|
|
}
|
|
for _, file := range files {
|
|
if file.Name() == "ssl" {
|
|
continue
|
|
}
|
|
if file.IsDir() {
|
|
size, err := fileOp.GetDirSize(path.Join(pathItem, file.Name()))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
item := dto.CleanTree{
|
|
ID: uuid.NewString(),
|
|
Label: file.Name(),
|
|
Type: treeType,
|
|
Size: uint64(size),
|
|
Name: strings.TrimPrefix(file.Name(), "/"),
|
|
IsCheck: isCheck,
|
|
IsRecommend: isCheck,
|
|
}
|
|
lists = append(lists, item)
|
|
}
|
|
}
|
|
return lists
|
|
}
|
|
|
|
func loadTreeWithAllFile(isCheck bool, originalPath, treeType, pathItem string, fileOp fileUtils.FileOp) []dto.CleanTree {
|
|
var lists []dto.CleanTree
|
|
|
|
files, err := os.ReadDir(pathItem)
|
|
if err != nil {
|
|
return lists
|
|
}
|
|
for _, file := range files {
|
|
if treeType == "upload" && (file.Name() == "theme" && file.IsDir()) {
|
|
continue
|
|
}
|
|
if treeType == "system_log" && (file.Name() == "1Panel-Core.log" || file.Name() == "1Panel.log" || file.IsDir()) {
|
|
continue
|
|
}
|
|
if (treeType == "upload" || treeType == "download") && file.IsDir() && (file.Name() == "app" || file.Name() == "database" || file.Name() == "website" || file.Name() == "directory") {
|
|
continue
|
|
}
|
|
size := uint64(0)
|
|
name := strings.TrimPrefix(path.Join(pathItem, file.Name()), originalPath+"/")
|
|
if file.IsDir() {
|
|
sizeItem, err := fileOp.GetDirSize(path.Join(pathItem, file.Name()))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
size = uint64(sizeItem)
|
|
} else {
|
|
fileInfo, err := file.Info()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
size = uint64(fileInfo.Size())
|
|
}
|
|
item := dto.CleanTree{
|
|
ID: uuid.NewString(),
|
|
Label: file.Name(),
|
|
Type: treeType,
|
|
Size: size,
|
|
Name: name,
|
|
IsCheck: isCheck,
|
|
IsRecommend: isCheck,
|
|
}
|
|
if file.IsDir() {
|
|
item.Children = loadTreeWithAllFile(isCheck, originalPath, treeType, path.Join(pathItem, file.Name()), fileOp)
|
|
}
|
|
lists = append(lists, item)
|
|
}
|
|
return lists
|
|
}
|
|
|
|
func dropFileOrDir(itemPath string) {
|
|
if err := os.RemoveAll(itemPath); err != nil {
|
|
global.LOG.Errorf("drop file %s failed, err %v", itemPath, err)
|
|
}
|
|
}
|
|
func dropFile(itemPath string) {
|
|
info, err := os.Stat(itemPath)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if info.IsDir() {
|
|
return
|
|
}
|
|
if err := os.Remove(itemPath); err != nil {
|
|
global.LOG.Errorf("drop file %s failed, err %v", itemPath, err)
|
|
}
|
|
}
|
|
|
|
func dropBuildCache() (int, int) {
|
|
client, err := docker.NewDockerClient()
|
|
if err != nil {
|
|
global.LOG.Errorf("do not get docker client")
|
|
return 0, 0
|
|
}
|
|
defer client.Close()
|
|
opts := build.CachePruneOptions{}
|
|
opts.All = true
|
|
res, err := client.BuildCachePrune(context.Background(), opts)
|
|
if err != nil {
|
|
global.LOG.Errorf("drop build cache failed, err %v", err)
|
|
return 0, 0
|
|
}
|
|
return len(res.CachesDeleted), int(res.SpaceReclaimed)
|
|
}
|
|
|
|
func dropImages() (int, int) {
|
|
client, err := docker.NewDockerClient()
|
|
if err != nil {
|
|
global.LOG.Errorf("do not get docker client")
|
|
return 0, 0
|
|
}
|
|
defer client.Close()
|
|
pruneFilters := filters.NewArgs()
|
|
pruneFilters.Add("dangling", "false")
|
|
res, err := client.ImagesPrune(context.Background(), pruneFilters)
|
|
if err != nil {
|
|
global.LOG.Errorf("drop images failed, err %v", err)
|
|
return 0, 0
|
|
}
|
|
return len(res.ImagesDeleted), int(res.SpaceReclaimed)
|
|
}
|
|
|
|
func dropContainers() (int, int) {
|
|
client, err := docker.NewDockerClient()
|
|
if err != nil {
|
|
global.LOG.Errorf("do not get docker client")
|
|
return 0, 0
|
|
}
|
|
defer client.Close()
|
|
pruneFilters := filters.NewArgs()
|
|
res, err := client.ContainersPrune(context.Background(), pruneFilters)
|
|
if err != nil {
|
|
global.LOG.Errorf("drop containers failed, err %v", err)
|
|
return 0, 0
|
|
}
|
|
return len(res.ContainersDeleted), int(res.SpaceReclaimed)
|
|
}
|
|
|
|
func dropVolumes() (int, int) {
|
|
client, err := docker.NewDockerClient()
|
|
if err != nil {
|
|
global.LOG.Errorf("do not get docker client")
|
|
return 0, 0
|
|
}
|
|
defer client.Close()
|
|
pruneFilters := filters.NewArgs()
|
|
versions, err := client.ServerVersion(context.Background())
|
|
if err != nil {
|
|
global.LOG.Errorf("do not get docker api versions")
|
|
return 0, 0
|
|
}
|
|
if common.ComparePanelVersion(versions.APIVersion, "1.42") {
|
|
pruneFilters.Add("all", "true")
|
|
}
|
|
res, err := client.VolumesPrune(context.Background(), pruneFilters)
|
|
if err != nil {
|
|
global.LOG.Errorf("drop volumes failed, err %v", err)
|
|
return 0, 0
|
|
}
|
|
return len(res.VolumesDeleted), int(res.SpaceReclaimed)
|
|
}
|
|
|
|
func dropWebsiteLog(alias string) {
|
|
accessLogPath := path.Join(GetSiteDir(alias), "log", "access.log")
|
|
errorLogPath := path.Join(GetSiteDir(alias), "log", "error.log")
|
|
if err := os.Truncate(accessLogPath, 0); err != nil {
|
|
global.LOG.Errorf("truncate access log %s failed, err %v", accessLogPath, err)
|
|
}
|
|
|
|
if err := os.Truncate(errorLogPath, 0); err != nil {
|
|
global.LOG.Errorf("truncate error log %s failed, err %v", errorLogPath, err)
|
|
}
|
|
}
|
|
|
|
func dropTaskLog(logDir string) {
|
|
files, err := os.ReadDir(logDir)
|
|
if err != nil {
|
|
return
|
|
}
|
|
taskType := path.Base(logDir)
|
|
var usedTasks []string
|
|
switch taskType {
|
|
case "Cronjob":
|
|
_ = global.DB.Model(&model.JobRecords{}).Where("task_id != ?", "").Select("task_id").Find(&usedTasks).Error
|
|
case "Snapshot":
|
|
var (
|
|
snapIDs []string
|
|
recoverIDs []string
|
|
rollbackIDs []string
|
|
)
|
|
_ = global.DB.Model(&model.Snapshot{}).Where("task_id != ?", "").Select("task_id").Find(&snapIDs).Error
|
|
_ = global.DB.Model(&model.Snapshot{}).Where("task_recover_id != ", "").Select("task_id").Find(&recoverIDs).Error
|
|
_ = global.DB.Model(&model.Snapshot{}).Where("task_rollback_id != ?", "").Select("task_id").Find(&rollbackIDs).Error
|
|
usedTasks = append(usedTasks, snapIDs...)
|
|
usedTasks = append(usedTasks, recoverIDs...)
|
|
usedTasks = append(usedTasks, rollbackIDs...)
|
|
case "Backup":
|
|
_ = global.DB.Model(&model.BackupRecord{}).Where("task_id != ?", "").Select("task_id").Find(&usedTasks).Error
|
|
case "Clam":
|
|
_ = global.DB.Model(&model.ClamRecord{}).Where("task_id != ?", "").Select("task_id").Find(&usedTasks).Error
|
|
case "Tamper":
|
|
xpackDB, err := common.LoadDBConnByPathWithErr(path.Join(global.CONF.Base.InstallDir, "1panel/db/xpack.db"), "xpack.db")
|
|
if err == nil {
|
|
_ = xpackDB.Table("tampers").Where("task_id != ?", "").Select("task_id").Find(&usedTasks).Error
|
|
}
|
|
defer common.CloseDB(xpackDB)
|
|
case "System":
|
|
xpackDB, err := common.LoadDBConnByPathWithErr(path.Join(global.CONF.Base.InstallDir, "1panel/db/xpack.db"), "xpack.db")
|
|
if err == nil {
|
|
_ = xpackDB.Model("nodes").Where("task_id != ?", "").Select("task_id").Find(&usedTasks).Error
|
|
}
|
|
defer common.CloseDB(xpackDB)
|
|
default:
|
|
dropFileOrDir(logDir)
|
|
_ = taskRepo.Delete(repo.WithByType(taskType))
|
|
return
|
|
}
|
|
usedMap := make(map[string]struct{})
|
|
for _, item := range usedTasks {
|
|
if _, ok := usedMap[item]; !ok {
|
|
usedMap[item] = struct{}{}
|
|
}
|
|
}
|
|
for _, item := range files {
|
|
if _, ok := usedMap[strings.TrimSuffix(item.Name(), ".log")]; ok {
|
|
continue
|
|
}
|
|
_ = os.Remove(logDir + "/" + item.Name())
|
|
}
|
|
_ = taskRepo.Delete(repo.WithByType(taskType), taskRepo.WithByIDNotIn(usedTasks))
|
|
}
|
|
|
|
func dropWithExclude(pathToDelete string, excludeSubDirs []string, taskItem *task.Task, size *int64, count *int) {
|
|
entries, err := os.ReadDir(pathToDelete)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
name := entry.Name()
|
|
fullPath := filepath.Join(pathToDelete, name)
|
|
excluded := false
|
|
for _, ex := range excludeSubDirs {
|
|
if name == ex {
|
|
excluded = true
|
|
break
|
|
}
|
|
}
|
|
if excluded {
|
|
continue
|
|
}
|
|
dropWithTask(fullPath, taskItem, size, count)
|
|
}
|
|
}
|
|
|
|
func dropWithTask(itemPath string, taskItem *task.Task, size *int64, count *int) {
|
|
itemSize := int64(0)
|
|
itemCount := 0
|
|
scanFile(itemPath, &itemSize, &itemCount)
|
|
*size += itemSize
|
|
*count += itemCount
|
|
if err := os.RemoveAll(itemPath); err != nil {
|
|
taskItem.Log(i18n.GetWithNameAndErr("FileDropFailed", itemPath, err))
|
|
return
|
|
}
|
|
if itemCount != 0 {
|
|
taskItem.Log(i18n.GetMsgWithMap("FileDropSuccess", map[string]interface{}{"name": itemPath, "count": itemCount, "size": common.LoadSizeUnit2F(float64(itemSize))}))
|
|
}
|
|
}
|
|
|
|
func scanFile(pathItem string, size *int64, count *int) {
|
|
files, _ := os.ReadDir(pathItem)
|
|
for _, f := range files {
|
|
if f.IsDir() {
|
|
scanFile(path.Join(pathItem, f.Name()), size, count)
|
|
} else {
|
|
fileInfo, err := f.Info()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
*count++
|
|
*size += fileInfo.Size()
|
|
}
|
|
}
|
|
}
|
|
|
|
func cleanEmptyDirs(root string) error {
|
|
dirsToCheck := make([]string, 0)
|
|
err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
if d.IsDir() {
|
|
dirsToCheck = append(dirsToCheck, path)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for i := len(dirsToCheck) - 1; i >= 0; i-- {
|
|
dir := dirsToCheck[i]
|
|
if dir == root {
|
|
continue
|
|
}
|
|
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
if len(entries) == 0 {
|
|
_ = os.Remove(dir)
|
|
}
|
|
}
|
|
return nil
|
|
}
|