mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-10-07 22:16:16 +08:00
feat: Support downloading scheduled task website log rotation files (#8597)
This commit is contained in:
parent
746882bc73
commit
a0dfbde0d3
17 changed files with 104 additions and 71 deletions
|
@ -61,14 +61,16 @@ type CronjobDownload struct {
|
|||
}
|
||||
|
||||
type CronjobClean struct {
|
||||
IsDelete bool `json:"isDelete"`
|
||||
CleanData bool `json:"cleanData"`
|
||||
CronjobID uint `json:"cronjobID" validate:"required"`
|
||||
IsDelete bool `json:"isDelete"`
|
||||
CleanData bool `json:"cleanData"`
|
||||
CronjobID uint `json:"cronjobID" validate:"required"`
|
||||
CleanRemoteData bool `json:"cleanRemoteData"`
|
||||
}
|
||||
|
||||
type CronjobBatchDelete struct {
|
||||
CleanData bool `json:"cleanData"`
|
||||
IDs []uint `json:"ids" validate:"required"`
|
||||
CleanData bool `json:"cleanData"`
|
||||
CleanRemoteData bool `json:"cleanRemoteData"`
|
||||
IDs []uint `json:"ids" validate:"required"`
|
||||
}
|
||||
|
||||
type CronjobInfo struct {
|
||||
|
|
|
@ -202,10 +202,15 @@ func (u *CronjobService) CleanRecord(req dto.CronjobClean) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !req.CleanRemoteData {
|
||||
for key := range accountMap {
|
||||
if key != constant.Local {
|
||||
delete(accountMap, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
cronjob.RetainCopies = 0
|
||||
u.removeExpiredBackup(cronjob, accountMap, model.BackupRecord{})
|
||||
} else {
|
||||
u.removeExpiredLog(cronjob)
|
||||
}
|
||||
}
|
||||
if req.IsDelete {
|
||||
|
@ -272,6 +277,13 @@ func (u *CronjobService) Create(req dto.CronjobOperate) error {
|
|||
if err := copier.Copy(&cronjob, &req); err != nil {
|
||||
return buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
||||
}
|
||||
if cronjob.Type == "cutWebsiteLog" {
|
||||
backupAccount, err := backupRepo.Get(repo.WithByType(constant.Local))
|
||||
if backupAccount.ID == 0 {
|
||||
return fmt.Errorf("load local backup dir failed, err: %v", err)
|
||||
}
|
||||
cronjob.DownloadAccountID, cronjob.SourceAccountIDs = backupAccount.ID, fmt.Sprintf("%v", backupAccount.ID)
|
||||
}
|
||||
cronjob.Status = constant.StatusEnable
|
||||
|
||||
global.LOG.Infof("create cronjob %s successful, spec: %s", cronjob.Name, cronjob.Spec)
|
||||
|
@ -334,7 +346,7 @@ func (u *CronjobService) Delete(req dto.CronjobBatchDelete) error {
|
|||
global.Cron.Remove(cron.EntryID(idItem))
|
||||
}
|
||||
global.LOG.Infof("stop cronjob entryID: %s", cronjob.EntryIDs)
|
||||
if err := u.CleanRecord(dto.CronjobClean{CronjobID: id, CleanData: req.CleanData, IsDelete: true}); err != nil {
|
||||
if err := u.CleanRecord(dto.CronjobClean{CronjobID: id, CleanData: req.CleanData, CleanRemoteData: req.CleanRemoteData, IsDelete: true}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cronjobRepo.Delete(repo.WithByID(id)); err != nil {
|
||||
|
|
|
@ -86,7 +86,7 @@ func (u *CronjobService) loadTask(cronjob *model.Cronjob, record *model.JobRecor
|
|||
case "ntp":
|
||||
u.handleNtpSync(*cronjob, taskItem)
|
||||
case "cutWebsiteLog":
|
||||
err = u.handleCutWebsiteLog(cronjob, record, taskItem)
|
||||
err = u.handleCutWebsiteLog(cronjob, record.StartTime, taskItem)
|
||||
case "clean":
|
||||
u.handleSystemClean(*cronjob, taskItem)
|
||||
case "website":
|
||||
|
@ -174,26 +174,40 @@ func (u *CronjobService) handleNtpSync(cronjob model.Cronjob, taskItem *task.Tas
|
|||
}, nil, int(cronjob.RetryTimes), time.Duration(cronjob.Timeout)*time.Second)
|
||||
}
|
||||
|
||||
func (u *CronjobService) handleCutWebsiteLog(cronjob *model.Cronjob, record *model.JobRecords, taskItem *task.Task) error {
|
||||
func (u *CronjobService) handleCutWebsiteLog(cronjob *model.Cronjob, startTime time.Time, taskItem *task.Task) error {
|
||||
taskItem.AddSubTaskWithOps(i18n.GetWithName("CutWebsiteLog", cronjob.Name), func(t *task.Task) error {
|
||||
var filePaths []string
|
||||
websites := loadWebsForJob(*cronjob)
|
||||
fileOp := files.NewFileOp()
|
||||
baseDir := GetOpenrestyDir(SitesRootDir)
|
||||
clientMap, err := NewBackupClientMap([]string{fmt.Sprintf("%v", cronjob.DownloadAccountID)})
|
||||
if err != nil {
|
||||
return fmt.Errorf("load local backup client failed, err: %v", err)
|
||||
}
|
||||
for _, website := range websites {
|
||||
taskItem.Log(website.Alias)
|
||||
var record model.BackupRecord
|
||||
record.From = "cronjob"
|
||||
record.Type = "cut-website-log"
|
||||
record.CronjobID = cronjob.ID
|
||||
record.Name = website.Alias
|
||||
record.DetailName = website.Alias
|
||||
record.DownloadAccountID, record.SourceAccountIDs = cronjob.DownloadAccountID, cronjob.SourceAccountIDs
|
||||
backupDir := pathUtils.Join(global.Dir.LocalBackupDir, "log", "website", website.Alias)
|
||||
if !fileOp.Stat(backupDir) {
|
||||
_ = os.MkdirAll(backupDir, constant.DirPerm)
|
||||
}
|
||||
record.FileDir = strings.TrimPrefix(backupDir, global.Dir.LocalBackupDir+"/")
|
||||
record.FileName = fmt.Sprintf("%s_log_%s.gz", website.PrimaryDomain, startTime.Format(constant.DateTimeSlimLayout))
|
||||
if err := backupRepo.CreateRecord(&record); err != nil {
|
||||
global.LOG.Errorf("save backup record failed, err: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
websiteLogDir := pathUtils.Join(baseDir, website.Alias, "log")
|
||||
srcAccessLogPath := pathUtils.Join(websiteLogDir, "access.log")
|
||||
srcErrorLogPath := pathUtils.Join(websiteLogDir, "error.log")
|
||||
dstLogDir := pathUtils.Join(global.Dir.LocalBackupDir, "log", "website", website.Alias)
|
||||
if !fileOp.Stat(dstLogDir) {
|
||||
_ = os.MkdirAll(dstLogDir, constant.DirPerm)
|
||||
}
|
||||
|
||||
dstName := fmt.Sprintf("%s_log_%s.gz", website.PrimaryDomain, record.StartTime.Format(constant.DateTimeSlimLayout))
|
||||
dstFilePath := pathUtils.Join(dstLogDir, dstName)
|
||||
filePaths = append(filePaths, dstFilePath)
|
||||
|
||||
dstFilePath := pathUtils.Join(backupDir, record.FileName)
|
||||
if err := backupLogFile(dstFilePath, websiteLogDir, fileOp); err != nil {
|
||||
taskItem.LogFailedWithErr("CutWebsiteLog", err)
|
||||
continue
|
||||
|
@ -202,9 +216,8 @@ func (u *CronjobService) handleCutWebsiteLog(cronjob *model.Cronjob, record *mod
|
|||
_ = fileOp.WriteFile(srcErrorLogPath, strings.NewReader(""), constant.DirPerm)
|
||||
}
|
||||
taskItem.Log(i18n.GetMsgWithMap("CutWebsiteLogSuccess", map[string]interface{}{"name": website.PrimaryDomain, "path": dstFilePath}))
|
||||
u.removeExpiredBackup(*cronjob, clientMap, record)
|
||||
}
|
||||
u.removeExpiredLog(*cronjob)
|
||||
record.File = strings.Join(filePaths, ",")
|
||||
return nil
|
||||
}, nil, int(cronjob.RetryTimes), time.Duration(cronjob.Timeout)*time.Second)
|
||||
return nil
|
||||
|
@ -287,25 +300,8 @@ func (u *CronjobService) removeExpiredBackup(cronjob model.Cronjob, accountMap m
|
|||
}
|
||||
}
|
||||
|
||||
func (u *CronjobService) removeExpiredLog(cronjob model.Cronjob) {
|
||||
records, _ := cronjobRepo.ListRecord(cronjobRepo.WithByJobID(int(cronjob.ID)), repo.WithOrderBy("created_at desc"))
|
||||
if len(records) <= int(cronjob.RetainCopies) {
|
||||
return
|
||||
}
|
||||
for i := int(cronjob.RetainCopies); i < len(records); i++ {
|
||||
if len(records[i].File) != 0 {
|
||||
files := strings.Split(records[i].File, ",")
|
||||
for _, file := range files {
|
||||
_ = os.Remove(file)
|
||||
}
|
||||
}
|
||||
_ = cronjobRepo.DeleteRecord(repo.WithByID(records[i].ID))
|
||||
_ = os.Remove(records[i].Records)
|
||||
}
|
||||
}
|
||||
|
||||
func hasBackup(cronjobType string) bool {
|
||||
return cronjobType == "app" || cronjobType == "database" || cronjobType == "website" || cronjobType == "directory" || cronjobType == "snapshot" || cronjobType == "log"
|
||||
return cronjobType == "app" || cronjobType == "database" || cronjobType == "website" || cronjobType == "directory" || cronjobType == "snapshot" || cronjobType == "log" || cronjobType == "cutWebsiteLog"
|
||||
}
|
||||
|
||||
func handleCronJobAlert(cronjob *model.Cronjob) {
|
||||
|
|
|
@ -98,6 +98,7 @@ export namespace Cronjob {
|
|||
export interface CronjobDelete {
|
||||
ids: Array<number>;
|
||||
cleanData: boolean;
|
||||
cleanRemoteData: boolean;
|
||||
}
|
||||
export interface UpdateStatus {
|
||||
id: number;
|
||||
|
|
|
@ -39,8 +39,8 @@ export const searchRecords = (params: Cronjob.SearchRecord) => {
|
|||
return http.post<ResPage<Cronjob.Record>>(`cronjobs/search/records`, params);
|
||||
};
|
||||
|
||||
export const cleanRecords = (id: number, cleanData: boolean) => {
|
||||
return http.post(`cronjobs/records/clean`, { cronjobID: id, cleanData: cleanData });
|
||||
export const cleanRecords = (id: number, cleanData: boolean, cleanRemoteData: boolean) => {
|
||||
return http.post(`cronjobs/records/clean`, { cronjobID: id, cleanData: cleanData, cleanRemoteData });
|
||||
};
|
||||
|
||||
export const getRecordDetail = (params: string) => {
|
||||
|
|
|
@ -1060,6 +1060,7 @@ const message = {
|
|||
errHandle: 'Cronjob execution failure',
|
||||
noRecord: 'The execution did not generate any logs',
|
||||
cleanData: 'Clean data',
|
||||
cleanRemoteData: 'Delete remote data',
|
||||
cleanDataHelper: 'Delete the backup file generated during this task.',
|
||||
noLogs: 'No task output yet...',
|
||||
errPath: 'Backup path [{0}] error, cannot download!',
|
||||
|
|
|
@ -1019,6 +1019,7 @@ const message = {
|
|||
errHandle: 'cronjob実行障害',
|
||||
noRecord: 'Cronジョブをトリガーすると、ここにレコードが表示されます。',
|
||||
cleanData: 'クリーンデータ',
|
||||
cleanRemoteData: 'リモートデータを削除',
|
||||
cleanDataHelper: 'このタスク中に生成されたバックアップファイルを削除します。',
|
||||
noLogs: 'タスク出力はまだありません...',
|
||||
errPath: 'バックアップパス[{0}]エラー、ダウンロードできません!',
|
||||
|
|
|
@ -1014,6 +1014,7 @@ const message = {
|
|||
errHandle: '크론 작업 실행 실패',
|
||||
noRecord: '크론 작업을 트리거하고 나면 여기에 레코드가 표시됩니다.',
|
||||
cleanData: '데이터 정리',
|
||||
cleanRemoteData: '원격 데이터 삭제',
|
||||
cleanDataHelper: '이 작업에서 생성된 백업 파일을 삭제합니다.',
|
||||
noLogs: '작업 출력이 아직 없습니다...',
|
||||
errPath: '백업 경로 [{0}] 오류, 다운로드할 수 없습니다!',
|
||||
|
|
|
@ -1049,6 +1049,7 @@ const message = {
|
|||
errHandle: 'Kegagalan pelaksanaan tugas cron',
|
||||
noRecord: 'Picu Tugas Cron, dan anda akan melihat rekod di sini.',
|
||||
cleanData: 'Bersihkan data',
|
||||
cleanRemoteData: 'Padam data jarak jauh',
|
||||
cleanDataHelper: 'Padam fail sandaran yang dijana semasa tugas ini.',
|
||||
noLogs: 'Tiada keluaran tugas lagi...',
|
||||
errPath: 'Laluan sandaran [{0}] salah, tidak boleh dimuat turun!',
|
||||
|
|
|
@ -1038,6 +1038,7 @@ const message = {
|
|||
errHandle: 'Falha na execução do Cronjob',
|
||||
noRecord: 'Acione a tarefa Cron e você verá os registros aqui.',
|
||||
cleanData: 'Limpar dados',
|
||||
cleanRemoteData: 'Excluir dados remotos',
|
||||
cleanDataHelper: 'Excluir o arquivo de backup gerado durante esta tarefa.',
|
||||
noLogs: 'Ainda não há saída de tarefa...',
|
||||
errPath: 'Caminho de backup [{0}] com erro, não é possível fazer o download!',
|
||||
|
|
|
@ -1043,6 +1043,7 @@ const message = {
|
|||
errHandle: 'Сбой выполнения задачи Cron',
|
||||
noRecord: 'Запустите задачу Cron, и вы увидите записи здесь.',
|
||||
cleanData: 'Очистить данные',
|
||||
cleanRemoteData: 'Удалить удалённые данные',
|
||||
cleanDataHelper: 'Удалить файл резервной копии, созданный во время этой задачи.',
|
||||
noLogs: 'Пока нет вывода задачи...',
|
||||
errPath: 'Ошибка пути резервной копии [{0}], невозможно скачать!',
|
||||
|
|
|
@ -1007,6 +1007,7 @@ const message = {
|
|||
errHandle: '任務執行失敗',
|
||||
noRecord: '當前計劃任務暫未產生記錄',
|
||||
cleanData: '刪除備份文件',
|
||||
cleanRemoteData: '刪除遠端備份檔案',
|
||||
cleanDataHelper: '刪除該任務執行過程中產生的備份文件',
|
||||
noLogs: '暫無任務輸出...',
|
||||
errPath: '備份路徑 [{0}] 錯誤,無法下載!',
|
||||
|
|
|
@ -1005,6 +1005,7 @@ const message = {
|
|||
errHandle: '任务执行失败',
|
||||
noRecord: '当前计划任务暂未产生记录',
|
||||
cleanData: '删除备份文件',
|
||||
cleanRemoteData: '删除远程备份文件',
|
||||
cleanDataHelper: '删除该任务执行过程中产生的备份文件',
|
||||
noLogs: '暂无任务输出...',
|
||||
errPath: '备份路径 [{0}] 错误,无法下载!',
|
||||
|
|
|
@ -189,3 +189,15 @@ export function transSpecToStr(spec: string): string {
|
|||
return i18n.global.t('cronjob.perNSecondHelper', [loadZero(specObj.second)]);
|
||||
}
|
||||
}
|
||||
|
||||
export function hasBackup(type: string) {
|
||||
return (
|
||||
type === 'app' ||
|
||||
type === 'website' ||
|
||||
type === 'database' ||
|
||||
type === 'directory' ||
|
||||
type === 'snapshot' ||
|
||||
type === 'log' ||
|
||||
type === 'cutWebsiteLog'
|
||||
);
|
||||
}
|
||||
|
|
|
@ -143,6 +143,11 @@
|
|||
<el-form class="mt-4 mb-1" v-if="showClean" ref="deleteForm" label-position="left">
|
||||
<el-form-item>
|
||||
<el-checkbox v-model="cleanData" :label="$t('cronjob.cleanData')" />
|
||||
<el-checkbox
|
||||
v-if="cleanData"
|
||||
v-model="cleanRemoteData"
|
||||
:label="$t('cronjob.cleanRemoteData')"
|
||||
/>
|
||||
<span class="input-help">
|
||||
{{ $t('cronjob.cleanDataHelper') }}
|
||||
</span>
|
||||
|
@ -164,7 +169,7 @@ import i18n from '@/lang';
|
|||
import { Cronjob } from '@/api/interface/cronjob';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import { transSpecToStr } from './helper';
|
||||
import { hasBackup, transSpecToStr } from './helper';
|
||||
import { GlobalStore } from '@/store';
|
||||
import router from '@/routers';
|
||||
|
||||
|
@ -181,6 +186,7 @@ const operateIDs = ref();
|
|||
const opRef = ref();
|
||||
const showClean = ref();
|
||||
const cleanData = ref();
|
||||
const cleanRemoteData = ref();
|
||||
|
||||
const data = ref();
|
||||
const paginationConfig = reactive({
|
||||
|
@ -257,7 +263,7 @@ const onDelete = async (row: Cronjob.CronjobInfo | null) => {
|
|||
|
||||
const onSubmitDelete = async () => {
|
||||
loading.value = true;
|
||||
await deleteCronjob({ ids: operateIDs.value, cleanData: cleanData.value })
|
||||
await deleteCronjob({ ids: operateIDs.value, cleanData: cleanData.value, cleanRemoteData: cleanRemoteData.value })
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.deleteSuccess'));
|
||||
|
@ -311,17 +317,6 @@ const onHandle = async (row: Cronjob.CronjobInfo) => {
|
|||
});
|
||||
};
|
||||
|
||||
const hasBackup = (type: string) => {
|
||||
return (
|
||||
type === 'app' ||
|
||||
type === 'website' ||
|
||||
type === 'database' ||
|
||||
type === 'directory' ||
|
||||
type === 'snapshot' ||
|
||||
type === 'log'
|
||||
);
|
||||
};
|
||||
|
||||
const loadDetail = (row: any) => {
|
||||
isRecordShow.value = true;
|
||||
let params = {
|
||||
|
|
|
@ -200,6 +200,7 @@
|
|||
<el-form ref="deleteForm" label-position="left" v-loading="delLoading">
|
||||
<el-form-item>
|
||||
<el-checkbox v-model="cleanData" :label="$t('cronjob.cleanData')" />
|
||||
<el-checkbox v-if="cleanData" v-model="cleanRemoteData" :label="$t('cronjob.cleanRemoteData')" />
|
||||
<span class="input-help">
|
||||
{{ $t('cronjob.cleanDataHelper') }}
|
||||
</span>
|
||||
|
@ -231,6 +232,7 @@ import { MsgSuccess } from '@/utils/message';
|
|||
import { listDbItems } from '@/api/modules/database';
|
||||
import { listAppInstalled } from '@/api/modules/app';
|
||||
import { shortcuts } from '@/utils/shortcuts';
|
||||
import { hasBackup } from '../helper';
|
||||
|
||||
const loading = ref();
|
||||
const refresh = ref(false);
|
||||
|
@ -253,6 +255,7 @@ const currentRecordDetail = ref<string>('');
|
|||
const open = ref();
|
||||
const delLoading = ref();
|
||||
const cleanData = ref();
|
||||
const cleanRemoteData = ref();
|
||||
|
||||
const acceptParams = async (params: DialogProps): Promise<void> => {
|
||||
let itemSize = Number(localStorage.getItem(searchInfo.cacheSizeKey));
|
||||
|
@ -402,26 +405,30 @@ const loadRecord = async (row: Cronjob.Record) => {
|
|||
};
|
||||
|
||||
const onClean = async () => {
|
||||
ElMessageBox.confirm(i18n.global.t('commons.msg.clean'), i18n.global.t('commons.button.delete'), {
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
type: 'warning',
|
||||
}).then(async () => {
|
||||
await cleanRecords(dialogData.value.rowData.id, cleanData.value)
|
||||
.then(() => {
|
||||
delLoading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
search();
|
||||
})
|
||||
.catch(() => {
|
||||
delLoading.value = false;
|
||||
});
|
||||
});
|
||||
if (!hasBackup(dialogData.value.rowData.type)) {
|
||||
ElMessageBox.confirm(i18n.global.t('commons.msg.clean'), i18n.global.t('commons.msg.deleteTitle'), {
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
type: 'warning',
|
||||
}).then(async () => {
|
||||
await cleanRecords(dialogData.value.rowData.id, cleanData.value, cleanRemoteData.value)
|
||||
.then(() => {
|
||||
delLoading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
search();
|
||||
})
|
||||
.catch(() => {
|
||||
delLoading.value = false;
|
||||
});
|
||||
});
|
||||
} else {
|
||||
open.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
const cleanRecord = async () => {
|
||||
delLoading.value = true;
|
||||
await cleanRecords(dialogData.value.rowData.id, cleanData.value)
|
||||
await cleanRecords(dialogData.value.rowData.id, cleanData.value, false)
|
||||
.then(() => {
|
||||
delLoading.value = false;
|
||||
open.value = false;
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
</el-table-column>
|
||||
<el-table-column :label="$t('setting.bindNode')" :min-width="120">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.status !== 'Free'">
|
||||
<div v-if="row.status && row.status !== 'Free'">
|
||||
<div>
|
||||
{{ $t('license.pro') }}:
|
||||
{{ row.bindNode === '127.0.0.1' ? $t('xpack.node.master') : row.bindNode }}
|
||||
|
|
Loading…
Add table
Reference in a new issue