mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-01-19 08:19:15 +08:00
feat: 工具箱的缓存清理添加容器垃圾清理功能 (#5024)
This commit is contained in:
parent
2e47afef3b
commit
c31b3e9ddc
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -36,6 +36,7 @@ export namespace Toolbox {
|
||||
uploadClean: Array<CleanTree>;
|
||||
downloadClean: Array<CleanTree>;
|
||||
systemLogClean: Array<CleanTree>;
|
||||
containerClean: Array<CleanTree>;
|
||||
}
|
||||
export interface CleanTree {
|
||||
id: string;
|
||||
|
@ -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',
|
||||
|
@ -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: '應用',
|
||||
|
@ -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: '应用',
|
||||
|
@ -33,7 +33,9 @@
|
||||
</div>
|
||||
<div else>
|
||||
<el-text class="clean_title">
|
||||
<el-icon><MagicStick /></el-icon>
|
||||
<el-icon>
|
||||
<MagicStick />
|
||||
</el-icon>
|
||||
{{ $t('clean.scanHelper') }}
|
||||
</el-text>
|
||||
</div>
|
||||
@ -53,6 +55,24 @@
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
<el-card class="e-card">
|
||||
<el-row>
|
||||
<el-col :span="4">
|
||||
<svg-icon iconName="p-docker" class="svg-icon"></svg-icon>
|
||||
<el-button link class="card_icon" />
|
||||
</el-col>
|
||||
<el-col :span="20">
|
||||
<div>
|
||||
<el-text class="mx-1 card_title" type="primary">
|
||||
{{ $t('clean.containerTrash') }}
|
||||
</el-text>
|
||||
</div>
|
||||
<span class="input-help">
|
||||
{{ $t('container.cleanDockerDiskZone') }}
|
||||
</span>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
<el-card class="e-card">
|
||||
<el-row>
|
||||
<el-col :span="4">
|
||||
@ -156,6 +176,30 @@
|
||||
</template>
|
||||
</el-tree>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item :title="$t('clean.containerTrash')" name="container_trash">
|
||||
<el-tree
|
||||
ref="containerRef"
|
||||
:data="cleanData.containerClean"
|
||||
node-key="id"
|
||||
:default-checked-keys="containerDefaultCheck"
|
||||
show-checkbox
|
||||
:props="defaultProps"
|
||||
@check-change="onChange"
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<div class="float-left">
|
||||
<span>{{ load18n(data.label) }}</span>
|
||||
</div>
|
||||
<div class="ml-4 float-left">
|
||||
<span v-if="data.size">{{ computeSize(data.size) }}</span>
|
||||
</div>
|
||||
<div class="ml-4 float-left">
|
||||
<span>{{ loadTag(node, data) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item :title="$t('clean.upload')" name="upload">
|
||||
<el-tree
|
||||
ref="uploadRef"
|
||||
@ -241,6 +285,7 @@ import { clean, scan } from '@/api/modules/toolbox';
|
||||
import i18n from '@/lang';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import { GlobalStore } from '@/store';
|
||||
|
||||
const globalStore = GlobalStore();
|
||||
|
||||
const loading = ref();
|
||||
@ -258,6 +303,7 @@ const cleanData = reactive({
|
||||
uploadClean: [],
|
||||
downloadClean: [],
|
||||
systemLogClean: [],
|
||||
containerClean: [],
|
||||
});
|
||||
const systemRef = ref();
|
||||
const systemDefaultCheck = ref([]);
|
||||
@ -267,7 +313,9 @@ const downloadRef = ref();
|
||||
const downloadDefaultCheck = ref([]);
|
||||
const systemLogRef = ref();
|
||||
const systemLogDefaultCheck = ref([]);
|
||||
const activeNames = ref(['system', 'upload', 'download', 'system_log']);
|
||||
const containerRef = ref();
|
||||
const containerDefaultCheck = ref([]);
|
||||
const activeNames = ref(['system', 'upload', 'download', 'system_log', 'container_trash']);
|
||||
|
||||
const submitCleans = ref();
|
||||
|
||||
@ -300,10 +348,15 @@ const scanData = async () => {
|
||||
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;
|
||||
}
|
||||
</style>
|
||||
|
Loading…
x
Reference in New Issue
Block a user