diff --git a/backend/app/dto/setting.go b/backend/app/dto/setting.go index 65da34309..e7f66d2a2 100644 --- a/backend/app/dto/setting.go +++ b/backend/app/dto/setting.go @@ -165,6 +165,7 @@ type CleanData struct { UploadClean []CleanTree `json:"uploadClean"` DownloadClean []CleanTree `json:"downloadClean"` SystemLogClean []CleanTree `json:"systemLogClean"` + ContainerClean []CleanTree `json:"containerClean"` } type CleanTree struct { diff --git a/backend/app/service/container.go b/backend/app/service/container.go index 7225345e5..24299824d 100644 --- a/backend/app/service/container.go +++ b/backend/app/service/container.go @@ -337,7 +337,9 @@ func (u *ContainerService) Prune(req dto.ContainerPrune) (dto.ContainerPruneRepo report.DeletedNumber = len(rep.VolumesDeleted) report.SpaceReclaimed = int(rep.SpaceReclaimed) case "buildcache": - rep, err := client.BuildCachePrune(context.Background(), types.BuildCachePruneOptions{}) + opts := types.BuildCachePruneOptions{} + opts.All = true + rep, err := client.BuildCachePrune(context.Background(), opts) if err != nil { return report, err } diff --git a/backend/app/service/device_clean.go b/backend/app/service/device_clean.go index c6fe82830..ffe42cb20 100644 --- a/backend/app/service/device_clean.go +++ b/backend/app/service/device_clean.go @@ -1,7 +1,11 @@ package service import ( + "context" "fmt" + "github.com/1Panel-dev/1Panel/backend/utils/docker" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" "os" "path" "sort" @@ -130,6 +134,9 @@ func (u *DeviceService) Scan() dto.CleanData { logTree := loadLogTree(fileOp) SystemClean.SystemLogClean = append(SystemClean.SystemLogClean, logTree...) + containerTree := loadContainerTree() + SystemClean.ContainerClean = append(SystemClean.ContainerClean, containerTree...) + return SystemClean } @@ -259,6 +266,14 @@ func (u *DeviceService) Clean(req []dto.Clean) { } else { _ = cronjobRepo.DeleteRecord(cronjobRepo.WithByRecordFile(pathItem)) } + case "images": + dropImages() + case "containers": + dropContainers() + case "volumes": + dropVolumes() + case "build_cache": + dropBuildCache() } } @@ -496,6 +511,82 @@ func loadLogTree(fileOp fileUtils.FileOp) []dto.CleanTree { return treeData } +func loadContainerTree() []dto.CleanTree { + var treeData []dto.CleanTree + client, err := docker.NewDockerClient() + diskUsage, err := client.DiskUsage(context.Background(), types.DiskUsageOptions{}) + if err != nil { + return treeData + } + var listImage []dto.CleanTree + imageSize := uint64(0) + for _, file := range diskUsage.Images { + if file.Containers == 0 { + name := "none" + if file.RepoTags != nil { + name = file.RepoTags[0] + } + item := dto.CleanTree{ + ID: file.ID, + Label: name, + Type: "images", + Size: uint64(file.Size), + Name: name, + IsCheck: false, + IsRecommend: true, + } + imageSize += item.Size + listImage = append(listImage, item) + } + } + treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "container_images", Size: imageSize, Children: listImage, Type: "images", IsRecommend: true}) + + var listContainer []dto.CleanTree + containerSize := uint64(0) + for _, file := range diskUsage.Containers { + if file.State != "running" { + item := dto.CleanTree{ + ID: file.ID, + Label: file.Names[0], + Type: "containers", + Size: uint64(file.SizeRw), + Name: file.Names[0], + IsCheck: false, + IsRecommend: true, + } + containerSize += item.Size + listContainer = append(listContainer, item) + } + } + treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "container_containers", Size: containerSize, Children: listContainer, Type: "containers", IsRecommend: true}) + + var listVolume []dto.CleanTree + volumeSize := uint64(0) + for _, file := range diskUsage.Volumes { + if file.UsageData.RefCount <= 0 { + item := dto.CleanTree{ + ID: uuid.NewString(), + Label: file.Name, + Type: "volumes", + Size: uint64(file.UsageData.Size), + Name: file.Name, + IsCheck: false, + IsRecommend: true, + } + volumeSize += item.Size + listVolume = append(listVolume, item) + } + } + treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "container_volumes", Size: volumeSize, Children: listVolume, Type: "volumes", IsRecommend: true}) + + var buildCacheTotalSize int64 + for _, cache := range diskUsage.BuildCache { + buildCacheTotalSize += cache.Size + } + treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "build_cache", Size: uint64(buildCacheTotalSize), Type: "build_cache", 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) @@ -586,6 +677,63 @@ func dropFileOrDir(itemPath string) { } } +func dropBuildCache() { + client, err := docker.NewDockerClient() + if err != nil { + global.LOG.Errorf("do not get docker client") + } + opts := types.BuildCachePruneOptions{} + opts.All = true + _, err = client.BuildCachePrune(context.Background(), opts) + if err != nil { + global.LOG.Errorf("drop build cache failed, err %v", err) + } +} + +func dropImages() { + client, err := docker.NewDockerClient() + if err != nil { + global.LOG.Errorf("do not get docker client") + } + pruneFilters := filters.NewArgs() + pruneFilters.Add("dangling", "false") + _, err = client.ImagesPrune(context.Background(), pruneFilters) + if err != nil { + global.LOG.Errorf("drop images failed, err %v", err) + } +} + +func dropContainers() { + client, err := docker.NewDockerClient() + if err != nil { + global.LOG.Errorf("do not get docker client") + } + pruneFilters := filters.NewArgs() + _, err = client.ContainersPrune(context.Background(), pruneFilters) + if err != nil { + global.LOG.Errorf("drop containers failed, err %v", err) + } +} + +func dropVolumes() { + client, err := docker.NewDockerClient() + if err != nil { + global.LOG.Errorf("do not get docker client") + } + pruneFilters := filters.NewArgs() + versions, err := client.ServerVersion(context.Background()) + if err != nil { + global.LOG.Errorf("do not get docker api versions") + } + if common.ComparePanelVersion(versions.APIVersion, "1.42") { + pruneFilters.Add("all", "true") + } + _, err = client.VolumesPrune(context.Background(), pruneFilters) + if err != nil { + global.LOG.Errorf("drop volumes failed, err %v", err) + } +} + func dropFileOrDirWithLog(itemPath string, log *string, size *int64, count *int) { itemSize := int64(0) itemCount := 0 diff --git a/frontend/src/api/interface/toolbox.ts b/frontend/src/api/interface/toolbox.ts index 4ad344d35..ee0da2d4e 100644 --- a/frontend/src/api/interface/toolbox.ts +++ b/frontend/src/api/interface/toolbox.ts @@ -36,6 +36,7 @@ export namespace Toolbox { uploadClean: Array; downloadClean: Array; systemLogClean: Array; + containerClean: Array; } export interface CleanTree { id: string; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index a63e32c89..4a44ef4f1 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -796,6 +796,11 @@ const message = { sockPathErr: 'Please select or enter the correct Docker sock file path', related: 'Related resources', includeAppstore: 'Show app store container', + + cleanDockerDiskZone: 'Clean up disk space used by Docker', + cleanImagesHelper: '( Clean up all images that are not used by any containers )', + cleanContainersHelper: '( Clean up all stopped containers )', + cleanVolumesHelper: '( Clean up all unused local volumes )', }, cronjob: { create: 'Create Cronjob', @@ -1601,6 +1606,12 @@ const message = { shell: 'Shell script scheduled tasks', containerShell: 'Container internal Shell script scheduled tasks', curl: 'CURL scheduled tasks', + + containerTrash: 'Container Trash', + images: 'Images', + containers: 'Containers', + volumes: 'Volumes', + buildCache: 'Container Build Cache', }, app: { app: 'Application', diff --git a/frontend/src/lang/modules/tw.ts b/frontend/src/lang/modules/tw.ts index eb64a8ff7..e31c9600a 100644 --- a/frontend/src/lang/modules/tw.ts +++ b/frontend/src/lang/modules/tw.ts @@ -763,6 +763,11 @@ const message = { sockPathErr: '請選擇或輸入正確的 Docker sock 文件路徑', related: '相關資源', includeAppstore: '顯示應用程式商店容器', + + cleanDockerDiskZone: '清理 Docker 使用的磁碟空間', + cleanImagesHelper: '( 清理所有未被任何容器使用的鏡像 )', + cleanContainersHelper: '( 清理所有處於停止狀態的容器 )', + cleanVolumesHelper: '( 清理所有未被使用的本地存儲卷 )', }, cronjob: { create: '創建計劃任務', @@ -1493,6 +1498,12 @@ const message = { shell: 'Shell 腳本計劃任務', containerShell: '容器內執行 Shell 腳本計劃任務', curl: 'CURL 計劃任務', + + containerTrash: '容器垃圾', + images: '鏡像', + containers: '容器', + volumes: '存儲卷', + buildCache: '容器建置快取', }, app: { app: '應用', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index fba8d6098..ff49bb5b7 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -764,6 +764,11 @@ const message = { sockPathErr: '请选择或输入正确的 Docker sock 文件路径', related: '关联资源', includeAppstore: '显示应用商店容器', + + cleanDockerDiskZone: '清理 Docker 使用的磁盘空间', + cleanImagesHelper: '( 清理所有未被任何容器使用的镜像 )', + cleanContainersHelper: '( 清理所有处于停止状态的容器 )', + cleanVolumesHelper: '( 清理所有未被使用的本地存储卷 )', }, cronjob: { create: '创建计划任务', @@ -1493,6 +1498,12 @@ const message = { shell: 'Shell 脚本计划任务', containerShell: '容器内执行 Shell 脚本计划任务', curl: 'CURL 计划任务', + + containerTrash: '容器垃圾', + images: '镜像', + containers: '容器', + volumes: '存储卷', + buildCache: '构建缓存', }, app: { app: '应用', diff --git a/frontend/src/views/toolbox/clean/index.vue b/frontend/src/views/toolbox/clean/index.vue index ca7454589..b7e95b650 100644 --- a/frontend/src/views/toolbox/clean/index.vue +++ b/frontend/src/views/toolbox/clean/index.vue @@ -33,7 +33,9 @@
- + + + {{ $t('clean.scanHelper') }}
@@ -53,6 +55,24 @@ + + + + + + + +
+ + {{ $t('clean.containerTrash') }} + +
+ + {{ $t('container.cleanDockerDiskZone') }} + +
+
+
@@ -156,6 +176,30 @@ + + + + + + { for (const item of cleanData.systemLogClean) { totalSize.value += item.size; } + cleanData.containerClean = res.data.containerClean || []; + for (const item of cleanData.containerClean) { + totalSize.value += item.size; + } loadCheck(cleanData.systemClean, systemDefaultCheck.value); loadCheck(cleanData.uploadClean, uploadDefaultCheck.value); loadCheck(cleanData.downloadClean, downloadDefaultCheck.value); loadCheck(cleanData.systemLogClean, systemLogDefaultCheck.value); + loadCheck(cleanData.containerClean, containerDefaultCheck.value); scanStatus.value = 'scanned'; }) .catch(() => { @@ -324,6 +377,7 @@ const onSubmitClean = async () => { loadSubmitCheck(cleanData.uploadClean); loadSubmitCheck(cleanData.downloadClean); loadSubmitCheck(cleanData.systemLogClean); + loadSubmitCheck(cleanData.containerClean); for (const item of submitCleans.value) { if (item.treeType === 'cache') { restart = true; @@ -416,6 +470,12 @@ function onChange(data: any, isCheck: boolean) { selectSize.value = selectSize.value + Number(item.size); } } + let containerSelects = containerRef.value.getCheckedNodes(false, true); + for (const item of containerSelects) { + if (item.children === null) { + selectSize.value = selectSize.value + Number(item.size); + } + } } function loadCheck(data: any, checkList: any) { @@ -444,6 +504,15 @@ function loadTag(node: any, data: any) { if (data.size === 0) { return i18n.global.t('clean.statusClean'); } + if (data.label === 'container_images') { + return i18n.global.t('container.cleanImagesHelper'); + } + if (data.label === 'container_containers') { + return i18n.global.t('container.cleanContainersHelper'); + } + if (data.label === 'container_volumes') { + return i18n.global.t('container.cleanVolumesHelper'); + } if (data.label === 'upgrade') { return i18n.global.t('clean.upgradeHelper'); } @@ -509,6 +578,14 @@ function load18n(label: string) { return i18n.global.t('clean.containerShell'); case 'curl': return i18n.global.t('clean.curl'); + case 'container_images': + return i18n.global.t('clean.images'); + case 'container_containers': + return i18n.global.t('clean.containers'); + case 'container_volumes': + return i18n.global.t('clean.volumes'); + case 'build_cache': + return i18n.global.t('clean.buildCache'); default: return label; } @@ -524,32 +601,45 @@ onMounted(() => { .app-card { cursor: pointer; width: 100%; + &:hover .app-icon { transform: scale(1.2); } + .e-card { margin-top: 20px; cursor: pointer; border: var(--panel-border) !important; + &:hover { cursor: pointer; border: 1px solid var(--el-color-primary) !important; } } } + .card_icon { font-size: 36px; float: right; margin-right: 15px; } + .card_title { font-size: 18px; } + .clean_title { font-size: 22px; } + .large_button { float: right; margin-top: -40px; } + +.svg-icon { + font-size: 14px; + float: right; + margin-right: 15px; +}