mirror of https://github.com/1Panel-dev/1Panel
ssongliu
1 year ago
committed by
GitHub
19 changed files with 936 additions and 180 deletions
@ -0,0 +1,224 @@
|
||||
package service |
||||
|
||||
import ( |
||||
"context" |
||||
"encoding/json" |
||||
"fmt" |
||||
"os" |
||||
"path" |
||||
"regexp" |
||||
"strings" |
||||
"sync" |
||||
"time" |
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/constant" |
||||
"github.com/1Panel-dev/1Panel/backend/global" |
||||
"github.com/1Panel-dev/1Panel/backend/utils/cmd" |
||||
"github.com/1Panel-dev/1Panel/backend/utils/files" |
||||
) |
||||
|
||||
type snapHelper struct { |
||||
SnapID uint |
||||
Ctx context.Context |
||||
FileOp files.FileOp |
||||
Wg *sync.WaitGroup |
||||
} |
||||
|
||||
func snapJson(snap snapHelper, statusID uint, snapJson SnapshotJson, targetDir string) { |
||||
defer snap.Wg.Done() |
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"panel_info": constant.Running}) |
||||
status := constant.StatusDone |
||||
remarkInfo, _ := json.MarshalIndent(snapJson, "", "\t") |
||||
if err := os.WriteFile(fmt.Sprintf("%s/snapshot.json", targetDir), remarkInfo, 0640); err != nil { |
||||
status = err.Error() |
||||
} |
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"panel_info": status}) |
||||
} |
||||
|
||||
func snapPanel(snap snapHelper, statusID uint, targetDir string) { |
||||
defer snap.Wg.Done() |
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"panel": constant.Running}) |
||||
status := constant.StatusDone |
||||
if err := cpBinary("/usr/local/bin/1panel", path.Join(targetDir, "1panel")); err != nil { |
||||
status = err.Error() |
||||
} |
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"panel": status}) |
||||
} |
||||
|
||||
func snapPanelCtl(snap snapHelper, statusID uint, targetDir string) { |
||||
defer snap.Wg.Done() |
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"panel_ctl": constant.Running}) |
||||
status := constant.StatusDone |
||||
if err := cpBinary("/usr/local/bin/1pctl", path.Join(targetDir, "1pctl")); err != nil { |
||||
status = err.Error() |
||||
} |
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"panel_ctl": status}) |
||||
} |
||||
|
||||
func snapPanelService(snap snapHelper, statusID uint, targetDir string) { |
||||
defer snap.Wg.Done() |
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"panel_service": constant.Running}) |
||||
status := constant.StatusDone |
||||
if err := cpBinary("/etc/systemd/system/1panel.service", path.Join(targetDir, "1panel.service")); err != nil { |
||||
status = err.Error() |
||||
} |
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"panel_service": status}) |
||||
} |
||||
|
||||
func snapDaemonJson(snap snapHelper, statusID uint, targetDir string) { |
||||
defer snap.Wg.Done() |
||||
if !snap.FileOp.Stat("/etc/docker/daemon.json") { |
||||
return |
||||
} |
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"daemon_json": constant.Running}) |
||||
status := constant.StatusDone |
||||
if err := cpBinary("/etc/docker/daemon.json", path.Join(targetDir, "daemon.json")); err != nil { |
||||
status = err.Error() |
||||
} |
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"daemon_json": status}) |
||||
} |
||||
|
||||
func snapAppData(snap snapHelper, statusID uint, targetDir string) { |
||||
defer snap.Wg.Done() |
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"app_data": constant.Running}) |
||||
appInstalls, err := appInstallRepo.ListBy() |
||||
if err != nil { |
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"app_data": err.Error()}) |
||||
return |
||||
} |
||||
runtimes, err := runtimeRepo.List() |
||||
if err != nil { |
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"app_data": err.Error()}) |
||||
return |
||||
} |
||||
imageRegex := regexp.MustCompile(`image:\s*(.*)`) |
||||
var imageSaveList []string |
||||
existStr, _ := cmd.Exec("docker images | awk '{print $1\":\"$2}' | grep -v REPOSITORY:TAG") |
||||
existImages := strings.Split(existStr, "\n") |
||||
duplicateMap := make(map[string]bool) |
||||
for _, app := range appInstalls { |
||||
matches := imageRegex.FindAllStringSubmatch(app.DockerCompose, -1) |
||||
for _, match := range matches { |
||||
for _, existImage := range existImages { |
||||
if match[1] == existImage && !duplicateMap[match[1]] { |
||||
imageSaveList = append(imageSaveList, match[1]) |
||||
duplicateMap[match[1]] = true |
||||
} |
||||
} |
||||
} |
||||
} |
||||
for _, rumtime := range runtimes { |
||||
for _, existImage := range existImages { |
||||
if rumtime.Image == existImage && !duplicateMap[rumtime.Image] { |
||||
imageSaveList = append(imageSaveList, rumtime.Image) |
||||
duplicateMap[rumtime.Image] = true |
||||
} |
||||
} |
||||
} |
||||
|
||||
global.LOG.Debugf("docker save %s | gzip -c > %s", strings.Join(imageSaveList, " "), path.Join(targetDir, "docker_image.tar")) |
||||
std, err := cmd.Execf("docker save %s | gzip -c > %s", strings.Join(imageSaveList, " "), path.Join(targetDir, "docker_image.tar")) |
||||
if err != nil { |
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"app_data": std}) |
||||
return |
||||
} |
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"app_data": constant.StatusDone}) |
||||
} |
||||
|
||||
func snapBackup(snap snapHelper, statusID uint, localDir, targetDir string) { |
||||
defer snap.Wg.Done() |
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"backup_data": constant.Running}) |
||||
status := constant.StatusDone |
||||
if err := handleSnapTar(localDir, targetDir, "1panel_backup.tar.gz", "./system;"); err != nil { |
||||
status = err.Error() |
||||
} |
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"backup_data": status}) |
||||
} |
||||
|
||||
func snapPanelData(snap snapHelper, statusID uint, localDir, targetDir string) { |
||||
defer snap.Wg.Done() |
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"panel_data": constant.Running}) |
||||
status := constant.StatusDone |
||||
dataDir := path.Join(global.CONF.System.BaseDir, "1panel") |
||||
exclusionRules := "./tmp;./log;./cache;./db/1Panel.db-*;" |
||||
if strings.Contains(localDir, dataDir) { |
||||
exclusionRules += ("." + strings.ReplaceAll(localDir, dataDir, "") + ";") |
||||
} |
||||
|
||||
_ = snapshotRepo.Update(snap.SnapID, map[string]interface{}{"status": "OnSaveData"}) |
||||
if err := handleSnapTar(dataDir, targetDir, "1panel_data.tar.gz", exclusionRules); err != nil { |
||||
status = err.Error() |
||||
} |
||||
_ = snapshotRepo.Update(snap.SnapID, map[string]interface{}{"status": constant.StatusWaiting}) |
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"panel_data": status}) |
||||
} |
||||
|
||||
func snapCompress(snap snapHelper, statusID uint, rootDir string) { |
||||
defer func() { |
||||
global.LOG.Debugf("remove snapshot file %s", rootDir) |
||||
_ = os.RemoveAll(rootDir) |
||||
}() |
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"compress": constant.StatusRunning}) |
||||
tmpDir := path.Join(global.CONF.System.TmpDir, "system") |
||||
fileName := fmt.Sprintf("%s.tar.gz", path.Base(rootDir)) |
||||
if err := snap.FileOp.Compress([]string{rootDir}, tmpDir, fileName, files.TarGz); err != nil { |
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"compress": err.Error()}) |
||||
return |
||||
} |
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"compress": constant.StatusDone}) |
||||
} |
||||
|
||||
func snapUpload(account string, statusID uint, file string) { |
||||
source := path.Join(global.CONF.System.TmpDir, "system", path.Base(file)) |
||||
defer func() { |
||||
global.LOG.Debugf("remove snapshot file %s", source) |
||||
_ = os.Remove(source) |
||||
}() |
||||
|
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"upload": constant.StatusUploading}) |
||||
backup, err := backupRepo.Get(commonRepo.WithByType(account)) |
||||
if err != nil { |
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"upload": err.Error()}) |
||||
return |
||||
} |
||||
client, err := NewIBackupService().NewClient(&backup) |
||||
if err != nil { |
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"upload": err.Error()}) |
||||
return |
||||
} |
||||
target := path.Join(backup.BackupPath, "system_snapshot", path.Base(file)) |
||||
if _, err := client.Upload(source, target); err != nil { |
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"upload": err.Error()}) |
||||
return |
||||
} |
||||
_ = snapshotRepo.UpdateStatus(statusID, map[string]interface{}{"upload": constant.StatusDone}) |
||||
} |
||||
|
||||
func handleSnapTar(sourceDir, targetDir, name, exclusionRules string) error { |
||||
if _, err := os.Stat(targetDir); err != nil && os.IsNotExist(err) { |
||||
if err = os.MkdirAll(targetDir, os.ModePerm); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
exStr := "" |
||||
excludes := strings.Split(exclusionRules, ";") |
||||
for _, exclude := range excludes { |
||||
if len(exclude) == 0 { |
||||
continue |
||||
} |
||||
exStr += " --exclude " |
||||
exStr += exclude |
||||
} |
||||
|
||||
commands := fmt.Sprintf("tar --warning=no-file-changed -zcf %s %s -C %s .", targetDir+"/"+name, exStr, sourceDir) |
||||
global.LOG.Debug(commands) |
||||
stdout, err := cmd.ExecWithTimeOut(commands, 30*time.Minute) |
||||
if err != nil { |
||||
if len(stdout) != 0 { |
||||
global.LOG.Errorf("do handle tar failed, stdout: %s, err: %v", stdout, err) |
||||
return fmt.Errorf("do handle tar failed, stdout: %s, err: %v", stdout, err) |
||||
} |
||||
} |
||||
return nil |
||||
} |
@ -0,0 +1,338 @@
|
||||
<template> |
||||
<el-dialog |
||||
v-model="dialogVisiable" |
||||
@close="onClose" |
||||
:destroy-on-close="true" |
||||
:close-on-click-modal="false" |
||||
width="50%" |
||||
> |
||||
<template #header> |
||||
<div class="card-header"> |
||||
<span>{{ $t('setting.status') }}</span> |
||||
</div> |
||||
</template> |
||||
<div v-loading="loading"> |
||||
<el-alert :type="loadStatus(status.panel)" :closable="false"> |
||||
<template #title> |
||||
<el-button :icon="loadIcon(status.panel)" link>{{ $t('setting.panelBin') }}</el-button> |
||||
<div v-if="showErrorMsg(status.panel)" class="top-margin"> |
||||
<span class="err-message">{{ status.panel }}</span> |
||||
</div> |
||||
</template> |
||||
</el-alert> |
||||
<el-alert :type="loadStatus(status.panelCtl)" :closable="false"> |
||||
<template #title> |
||||
<el-button :icon="loadIcon(status.panelCtl)" link>{{ $t('setting.panelCtl') }}</el-button> |
||||
<div v-if="showErrorMsg(status.panelCtl)" class="top-margin"> |
||||
<span class="err-message">{{ status.panelCtl }}</span> |
||||
</div> |
||||
</template> |
||||
</el-alert> |
||||
<el-alert :type="loadStatus(status.panelService)" :closable="false"> |
||||
<template #title> |
||||
<el-button :icon="loadIcon(status.panelService)" link>{{ $t('setting.panelService') }}</el-button> |
||||
<div v-if="showErrorMsg(status.panelService)" class="top-margin"> |
||||
<span class="err-message">{{ status.panelService }}</span> |
||||
</div> |
||||
</template> |
||||
</el-alert> |
||||
<el-alert :type="loadStatus(status.panelInfo)" :closable="false"> |
||||
<template #title> |
||||
<el-button :icon="loadIcon(status.panelInfo)" link>{{ $t('setting.panelInfo') }}</el-button> |
||||
<div v-if="showErrorMsg(status.panelInfo)" class="top-margin"> |
||||
<span class="err-message">{{ status.panelInfo }}</span> |
||||
</div> |
||||
</template> |
||||
</el-alert> |
||||
<el-alert :type="loadStatus(status.daemonJson)" :closable="false"> |
||||
<template #title> |
||||
<el-button :icon="loadIcon(status.daemonJson)" link>{{ $t('setting.daemonJson') }}</el-button> |
||||
<div v-if="showErrorMsg(status.daemonJson)" class="top-margin"> |
||||
<span class="err-message">{{ status.daemonJson }}</span> |
||||
</div> |
||||
</template> |
||||
</el-alert> |
||||
<el-alert :type="loadStatus(status.appData)" :closable="false"> |
||||
<template #title> |
||||
<el-button :icon="loadIcon(status.appData)" link>{{ $t('setting.appData') }}</el-button> |
||||
<div v-if="showErrorMsg(status.appData)" class="top-margin"> |
||||
<span class="err-message">{{ status.appData }}</span> |
||||
</div> |
||||
</template> |
||||
</el-alert> |
||||
<el-alert :type="loadStatus(status.panelData)" :closable="false"> |
||||
<template #title> |
||||
<el-button :icon="loadIcon(status.panelData)" link>{{ $t('setting.panelData') }}</el-button> |
||||
<div v-if="showErrorMsg(status.panelData)" class="top-margin"> |
||||
<span class="err-message">{{ status.panelData }}</span> |
||||
</div> |
||||
</template> |
||||
</el-alert> |
||||
<el-alert :type="loadStatus(status.backupData)" :closable="false"> |
||||
<template #title> |
||||
<el-button :icon="loadIcon(status.backupData)" link>{{ $t('setting.backupData') }}</el-button> |
||||
<div v-if="showErrorMsg(status.backupData)" class="top-margin"> |
||||
<span class="err-message">{{ status.backupData }}</span> |
||||
</div> |
||||
</template> |
||||
</el-alert> |
||||
<el-alert :type="loadStatus(status.compress)" :closable="false"> |
||||
<template #title> |
||||
<el-button :icon="loadIcon(status.compress)" link>{{ $t('setting.compress') }}</el-button> |
||||
<div v-if="showErrorMsg(status.compress)" class="top-margin"> |
||||
<span class="err-message">{{ status.compress }}</span> |
||||
</div> |
||||
</template> |
||||
</el-alert> |
||||
<el-alert :type="loadStatus(status.upload)" :closable="false"> |
||||
<template #title> |
||||
<el-button :icon="loadIcon(status.upload)" link>{{ $t('setting.upload') }}</el-button> |
||||
<div v-if="showErrorMsg(status.upload)" class="top-margin"> |
||||
<span class="err-message">{{ status.upload }}</span> |
||||
</div> |
||||
</template> |
||||
</el-alert> |
||||
</div> |
||||
<template #footer> |
||||
<span class="dialog-footer"> |
||||
<el-button @click="onClose"> |
||||
{{ $t('commons.button.cancel') }} |
||||
</el-button> |
||||
<el-button v-if="showRetry()" @click="onRetry"> |
||||
{{ $t('commons.button.retry') }} |
||||
</el-button> |
||||
</span> |
||||
</template> |
||||
</el-dialog> |
||||
</template> |
||||
|
||||
<script lang="ts" setup> |
||||
import { Setting } from '@/api/interface/setting'; |
||||
import { loadSnapStatus, snapshotCreate } from '@/api/modules/setting'; |
||||
import { nextTick, onBeforeUnmount, reactive, ref } from 'vue'; |
||||
|
||||
const status = reactive<Setting.SnapshotStatus>({ |
||||
panel: '', |
||||
panelCtl: '', |
||||
panelService: '', |
||||
panelInfo: '', |
||||
daemonJson: '', |
||||
appData: '', |
||||
panelData: '', |
||||
backupData: '', |
||||
|
||||
compress: '', |
||||
upload: '', |
||||
}); |
||||
|
||||
const dialogVisiable = ref(false); |
||||
|
||||
const loading = ref(); |
||||
const snapID = ref(); |
||||
const snapFrom = ref(); |
||||
const snapDescription = ref(); |
||||
|
||||
let timer: NodeJS.Timer | null = null; |
||||
|
||||
interface DialogProps { |
||||
id: number; |
||||
from: string; |
||||
description: string; |
||||
} |
||||
|
||||
const acceptParams = (props: DialogProps): void => { |
||||
dialogVisiable.value = true; |
||||
snapID.value = props.id; |
||||
snapFrom.value = props.from; |
||||
snapDescription.value = props.description; |
||||
onWatch(); |
||||
nextTick(() => { |
||||
loadCurrentStatus(); |
||||
}); |
||||
}; |
||||
const emit = defineEmits(['search']); |
||||
|
||||
const loadCurrentStatus = async () => { |
||||
loading.value = true; |
||||
await loadSnapStatus(snapID.value) |
||||
.then((res) => { |
||||
loading.value = false; |
||||
status.panel = res.data.panel; |
||||
status.panelCtl = res.data.panelCtl; |
||||
status.panelService = res.data.panelService; |
||||
status.panelInfo = res.data.panelInfo; |
||||
status.daemonJson = res.data.daemonJson; |
||||
status.appData = res.data.appData; |
||||
status.panelData = res.data.panelData; |
||||
status.backupData = res.data.backupData; |
||||
|
||||
status.compress = res.data.compress; |
||||
status.upload = res.data.upload; |
||||
}) |
||||
.catch(() => { |
||||
loading.value = false; |
||||
}); |
||||
}; |
||||
|
||||
const onClose = async () => { |
||||
emit('search'); |
||||
dialogVisiable.value = false; |
||||
}; |
||||
|
||||
const onRetry = async () => { |
||||
loading.value = true; |
||||
await snapshotCreate({ id: snapID.value, from: snapFrom.value, description: snapDescription.value }) |
||||
.then(() => { |
||||
loading.value = false; |
||||
loadCurrentStatus(); |
||||
}) |
||||
.catch(() => { |
||||
loading.value = false; |
||||
}); |
||||
}; |
||||
|
||||
const onWatch = () => { |
||||
timer = setInterval(async () => { |
||||
if (keepLoadStatus()) { |
||||
const res = await loadSnapStatus(snapID.value); |
||||
status.panel = res.data.panel; |
||||
status.panelCtl = res.data.panelCtl; |
||||
status.panelService = res.data.panelService; |
||||
status.panelInfo = res.data.panelInfo; |
||||
status.daemonJson = res.data.daemonJson; |
||||
status.appData = res.data.appData; |
||||
status.panelData = res.data.panelData; |
||||
status.backupData = res.data.backupData; |
||||
|
||||
status.compress = res.data.compress; |
||||
status.upload = res.data.upload; |
||||
} |
||||
}, 1000 * 3); |
||||
}; |
||||
|
||||
const keepLoadStatus = () => { |
||||
if (status.panel === 'Running') { |
||||
return true; |
||||
} |
||||
if (status.panelCtl === 'Running') { |
||||
return true; |
||||
} |
||||
if (status.panelService === 'Running') { |
||||
return true; |
||||
} |
||||
if (status.panelInfo === 'Running') { |
||||
return true; |
||||
} |
||||
if (status.daemonJson === 'Running') { |
||||
return true; |
||||
} |
||||
if (status.appData === 'Running') { |
||||
return true; |
||||
} |
||||
if (status.panelData === 'Running') { |
||||
return true; |
||||
} |
||||
if (status.backupData === 'Running') { |
||||
return true; |
||||
} |
||||
if (status.compress === 'Running') { |
||||
return true; |
||||
} |
||||
if (status.upload === 'Uploading') { |
||||
return true; |
||||
} |
||||
return false; |
||||
}; |
||||
|
||||
const showErrorMsg = (status: string) => { |
||||
return status !== 'Running' && status !== 'Done' && status !== 'Uploading' && status !== 'Waiting'; |
||||
}; |
||||
|
||||
const showRetry = () => { |
||||
if (keepLoadStatus()) { |
||||
return false; |
||||
} |
||||
if (status.panel !== 'Running' && status.panel !== 'Done') { |
||||
return true; |
||||
} |
||||
if (status.panelCtl !== 'Running' && status.panelCtl !== 'Done') { |
||||
return true; |
||||
} |
||||
if (status.panelService !== 'Running' && status.panelService !== 'Done') { |
||||
return true; |
||||
} |
||||
if (status.panelInfo !== 'Running' && status.panelInfo !== 'Done') { |
||||
return true; |
||||
} |
||||
if (status.daemonJson !== 'Running' && status.daemonJson !== 'Done') { |
||||
return true; |
||||
} |
||||
if (status.appData !== 'Running' && status.appData !== 'Done') { |
||||
return true; |
||||
} |
||||
if (status.panelData !== 'Running' && status.panelData !== 'Done') { |
||||
return true; |
||||
} |
||||
if (status.backupData !== 'Running' && status.backupData !== 'Done') { |
||||
return true; |
||||
} |
||||
if (status.compress !== 'Running' && status.compress !== 'Done' && status.compress !== 'Waiting') { |
||||
return true; |
||||
} |
||||
if (status.upload !== 'Uploading' && status.upload !== 'Done' && status.upload !== 'Waiting') { |
||||
return true; |
||||
} |
||||
return false; |
||||
}; |
||||
|
||||
const loadStatus = (status: string) => { |
||||
switch (status) { |
||||
case 'Running': |
||||
case 'Waiting': |
||||
case 'Uploading': |
||||
return 'info'; |
||||
case 'Done': |
||||
return 'success'; |
||||
default: |
||||
return 'error'; |
||||
} |
||||
}; |
||||
|
||||
const loadIcon = (status: string) => { |
||||
switch (status) { |
||||
case 'Running': |
||||
case 'Waiting': |
||||
case 'Uploading': |
||||
return 'Loading'; |
||||
case 'Done': |
||||
return 'Check'; |
||||
default: |
||||
return 'Close'; |
||||
} |
||||
}; |
||||
|
||||
onBeforeUnmount(() => { |
||||
clearInterval(Number(timer)); |
||||
timer = null; |
||||
}); |
||||
defineExpose({ |
||||
acceptParams, |
||||
}); |
||||
</script> |
||||
<style scoped lang="scss"> |
||||
.el-alert { |
||||
margin: 10px 0 0; |
||||
} |
||||
.el-alert:first-child { |
||||
margin: 0; |
||||
} |
||||
.top-margin { |
||||
margin-top: 10px; |
||||
} |
||||
.err-message { |
||||
margin-left: 23px; |
||||
line-height: 20px; |
||||
word-break: break-all; |
||||
word-wrap: break-word; |
||||
} |
||||
</style> |
Loading…
Reference in new issue