1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-01-31 14:08:06 +08:00

feat: 计划任务脚本支持选择用户及解释器 (#6721)

Refs #3933 #2738
This commit is contained in:
ssongliu 2024-10-15 16:05:21 +08:00 committed by GitHub
parent f3fd02fcb3
commit 86bf7ea73a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 392 additions and 180 deletions

View File

@ -86,6 +86,21 @@ func (b *BaseApi) UpdateDeviceByFile(c *gin.Context) {
helper.SuccessWithData(c, nil)
}
// @Tags Device
// @Summary Load user list
// @Description 获取服务器用户列表
// @Success 200
// @Security ApiKeyAuth
// @Router /toolbox/device/users [get]
func (b *BaseApi) LoadUsers(c *gin.Context) {
users, err := deviceService.LoadUsers()
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, users)
}
// @Tags Device
// @Summary Update device
// @Description 修改系统参数

View File

@ -21,9 +21,13 @@ type CronjobCreate struct {
SpecCustom bool `json:"specCustom"`
Spec string `json:"spec" validate:"required"`
Script string `json:"script"`
Command string `json:"command"`
ContainerName string `json:"containerName"`
Executor string `json:"executor"`
ScriptMode string `json:"scriptMode"`
Script string `json:"script"`
Command string `json:"command"`
ContainerName string `json:"containerName"`
User string `json:"user"`
AppID string `json:"appID"`
Website string `json:"website"`
ExclusionRules string `json:"exclusionRules"`
@ -44,9 +48,13 @@ type CronjobUpdate struct {
SpecCustom bool `json:"specCustom"`
Spec string `json:"spec" validate:"required"`
Script string `json:"script"`
Command string `json:"command"`
ContainerName string `json:"containerName"`
Executor string `json:"executor"`
ScriptMode string `json:"scriptMode"`
Script string `json:"script"`
Command string `json:"command"`
ContainerName string `json:"containerName"`
User string `json:"user"`
AppID string `json:"appID"`
Website string `json:"website"`
ExclusionRules string `json:"exclusionRules"`
@ -89,9 +97,13 @@ type CronjobInfo struct {
SpecCustom bool `json:"specCustom"`
Spec string `json:"spec"`
Script string `json:"script"`
Command string `json:"command"`
ContainerName string `json:"containerName"`
Executor string `json:"executor"`
ScriptMode string `json:"scriptMode"`
Script string `json:"script"`
Command string `json:"command"`
ContainerName string `json:"containerName"`
User string `json:"user"`
AppID string `json:"appID"`
Website string `json:"website"`
ExclusionRules string `json:"exclusionRules"`

View File

@ -14,9 +14,13 @@ type Cronjob struct {
SpecCustom bool `json:"specCustom"`
Spec string `gorm:"not null" json:"spec"`
Command string `json:"command"`
ContainerName string `json:"containerName"`
Script string `json:"script"`
Executor string `json:"executor"`
Command string `json:"command"`
ContainerName string `json:"containerName"`
ScriptMode string `json:"scriptMode"`
Script string `json:"script"`
User string `json:"user"`
Website string `json:"website"`
AppID string `json:"appID"`
DBType string `json:"dbType"`
@ -39,6 +43,7 @@ type JobRecords struct {
BaseModel
CronjobID uint `json:"cronjobID"`
TaskID string `json:"taskID"`
StartTime time.Time `json:"startTime"`
Interval float64 `json:"interval"`
Records string `json:"records"`

View File

@ -182,13 +182,13 @@ func (u *CronjobService) HandleOnce(id uint) error {
return nil
}
func (u *CronjobService) Create(cronjobDto dto.CronjobCreate) error {
cronjob, _ := cronjobRepo.Get(commonRepo.WithByName(cronjobDto.Name))
func (u *CronjobService) Create(req dto.CronjobCreate) error {
cronjob, _ := cronjobRepo.Get(commonRepo.WithByName(req.Name))
if cronjob.ID != 0 {
return constant.ErrRecordExist
}
cronjob.Secret = cronjobDto.Secret
if err := copier.Copy(&cronjob, &cronjobDto); err != nil {
cronjob.Secret = req.Secret
if err := copier.Copy(&cronjob, &req); err != nil {
return errors.WithMessage(constant.ErrStructTransform, err.Error())
}
cronjob.Status = constant.StatusEnable
@ -282,8 +282,11 @@ func (u *CronjobService) Update(id uint, req dto.CronjobUpdate) error {
upMap["spec_custom"] = req.SpecCustom
upMap["spec"] = spec
upMap["script"] = req.Script
upMap["script_mode"] = req.ScriptMode
upMap["command"] = req.Command
upMap["container_name"] = req.ContainerName
upMap["executor"] = req.Executor
upMap["user"] = req.User
upMap["app_id"] = req.AppID
upMap["website"] = req.Website
upMap["exclusion_rules"] = req.ExclusionRules

View File

@ -8,13 +8,12 @@ import (
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/buserr"
"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/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/files"
"github.com/1Panel-dev/1Panel/agent/utils/ntp"
@ -35,15 +34,7 @@ func (u *CronjobService) HandleJob(cronjob *model.Cronjob) {
}
record.Records = u.generateLogsPath(*cronjob, record.StartTime)
_ = cronjobRepo.UpdateRecords(record.ID, map[string]interface{}{"records": record.Records})
script := cronjob.Script
if len(cronjob.ContainerName) != 0 {
command := "sh"
if len(cronjob.Command) != 0 {
command = cronjob.Command
}
script = fmt.Sprintf("docker exec %s %s -c \"%s\"", cronjob.ContainerName, command, strings.ReplaceAll(cronjob.Script, "\"", "\\\""))
}
err = u.handleShell(cronjob.Type, cronjob.Name, script, record.Records)
err = u.handleShell(*cronjob, record.Records)
u.removeExpiredLog(*cronjob)
case "curl":
if len(cronjob.URL) == 0 {
@ -51,7 +42,7 @@ func (u *CronjobService) HandleJob(cronjob *model.Cronjob) {
}
record.Records = u.generateLogsPath(*cronjob, record.StartTime)
_ = cronjobRepo.UpdateRecords(record.ID, map[string]interface{}{"records": record.Records})
err = u.handleShell(cronjob.Type, cronjob.Name, fmt.Sprintf("curl '%s'", cronjob.URL), record.Records)
err = cmd.ExecShell(record.Records, 24*time.Hour, "bash", "-c", "curl", cronjob.URL)
u.removeExpiredLog(*cronjob)
case "ntp":
err = u.handleNtpSync()
@ -100,17 +91,29 @@ func (u *CronjobService) HandleJob(cronjob *model.Cronjob) {
}()
}
func (u *CronjobService) handleShell(cronType, cornName, script, logPath string) error {
handleDir := fmt.Sprintf("%s/task/%s/%s", constant.DataDir, cronType, cornName)
if _, err := os.Stat(handleDir); err != nil && os.IsNotExist(err) {
if err = os.MkdirAll(handleDir, os.ModePerm); err != nil {
func (u *CronjobService) handleShell(cronjob model.Cronjob, logPath string) error {
if len(cronjob.ContainerName) != 0 {
command := "sh"
if len(cronjob.Command) != 0 {
command = cronjob.Command
}
scriptFile, _ := os.ReadFile(cronjob.Script)
return cmd.ExecShell(logPath, 24*time.Hour, "docker", "exec", cronjob.ContainerName, command, "-c", strings.ReplaceAll(string(scriptFile), "\"", "\\\""))
}
if cronjob.ScriptMode == "input" {
fileItem := path.Join(global.CONF.System.BaseDir, "1panel", "task", "shell", cronjob.Name, cronjob.Name+".sh")
_ = os.MkdirAll(path.Dir(fileItem), os.ModePerm)
shellFile, err := os.OpenFile(fileItem, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
if err != nil {
return err
}
defer shellFile.Close()
if _, err := shellFile.WriteString(cronjob.Script); err != nil {
return err
}
return cmd.ExecShell(logPath, 24*time.Hour, "sudo", "-u", cronjob.User, cronjob.Executor, fileItem)
}
if err := cmd.ExecCronjobWithTimeOut(script, handleDir, logPath, 24*time.Hour); err != nil {
return err
}
return nil
return cmd.ExecShell(logPath, 24*time.Hour, "sudo", "-u", cronjob.User, cronjob.Executor, cronjob.Script)
}
func (u *CronjobService) handleNtpSync() error {

View File

@ -37,6 +37,7 @@ type IDeviceService interface {
LoadTimeZone() ([]string, error)
CheckDNS(key, value string) (bool, error)
LoadConf(name string) (string, error)
LoadUsers() ([]string, error)
Scan() dto.CleanData
Clean(req []dto.Clean)
@ -168,6 +169,21 @@ func (u *DeviceService) Update(key, value string) error {
return nil
}
func (u *DeviceService) LoadUsers() ([]string, error) {
file, err := os.ReadFile("/etc/passwd")
if err != nil {
return nil, err
}
var users []string
lines := strings.Split(string(file), "\n")
for _, line := range lines {
if strings.Contains(line, ":") {
users = append(users, strings.Split(line, ":")[0])
}
}
return users, nil
}
func (u *DeviceService) UpdateHosts(req []dto.HostHelper) error {
conf, err := os.ReadFile(defaultHostPath)
if err != nil {

View File

@ -58,6 +58,7 @@ const (
TaskBuild = "TaskBuild"
TaskPull = "TaskPull"
TaskPush = "TaskPush"
TaskHandle = "TaskHandle"
)
const (
@ -65,6 +66,7 @@ const (
TaskScopeApp = "App"
TaskScopeRuntime = "Runtime"
TaskScopeDatabase = "Database"
TaskScopeCronjob = "Cronjob"
TaskScopeAppStore = "AppStore"
TaskScopeSnapshot = "Snapshot"
TaskScopeContainer = "Container"

View File

@ -227,6 +227,7 @@ TaskRollback: "Rollback"
TaskPull: "Pull"
TaskBuild: "Build"
TaskPush: "Push"
TaskHandle: "Execute"
Website: "Website"
App: "App"
Runtime: "Runtime"

View File

@ -229,6 +229,7 @@ TaskRollback: "回滚"
TaskPull: "拉取"
TaskBuild: "建構"
TaskPush: "推送"
TaskHandle: "執行"
Website: "網站"
App: "應用"
Runtime: "運行環境"

View File

@ -231,6 +231,7 @@ TaskRollback: "回滚"
TaskPull: "拉取"
TaskBuild: "构建"
TaskPush: "推送"
TaskHandle: "执行"
Website: "网站"
App: "应用"
Runtime: "运行环境"

View File

@ -22,6 +22,7 @@ func Init() {
migrations.AddTaskDB,
migrations.UpdateAppInstall,
migrations.UpdateSnapshot,
migrations.UpdateCronjob,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)

View File

@ -266,3 +266,10 @@ var UpdateSnapshot = &gormigrate.Migration{
return tx.AutoMigrate(&model.Snapshot{})
},
}
var UpdateCronjob = &gormigrate.Migration{
ID: "20241011-update-cronjob",
Migrate: func(tx *gorm.DB) error {
return tx.AutoMigrate(&model.Cronjob{}, &model.JobRecords{})
},
}

View File

@ -12,6 +12,7 @@ func (s *ToolboxRouter) InitRouter(Router *gin.RouterGroup) {
baseApi := v2.ApiGroupApp.BaseApi
{
toolboxRouter.POST("/device/base", baseApi.LoadDeviceBaseInfo)
toolboxRouter.GET("/device/users", baseApi.LoadUsers)
toolboxRouter.GET("/device/zone/options", baseApi.LoadTimeOption)
toolboxRouter.POST("/device/update/conf", baseApi.UpdateDeviceConf)
toolboxRouter.POST("/device/update/host", baseApi.UpdateDeviceHost)

View File

@ -107,15 +107,14 @@ func ExecContainerScript(containerName, cmdStr string, timeout time.Duration) er
return nil
}
func ExecCronjobWithTimeOut(cmdStr, workdir, outPath string, timeout time.Duration) error {
func ExecShell(outPath string, timeout time.Duration, name string, arg ...string) error {
file, err := os.OpenFile(outPath, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
return err
}
defer file.Close()
cmd := exec.Command("bash", "-c", cmdStr)
cmd.Dir = workdir
cmd := exec.Command(name, arg...)
cmd.Stdout = file
cmd.Stderr = file
if err := cmd.Start(); err != nil {

View File

@ -10,11 +10,15 @@ export namespace Cronjob {
specs: Array<string>;
specObjs: Array<SpecObj>;
executor: string;
isExecutorCustom: boolean;
script: string;
scriptMode: string;
isCustom: boolean;
command: string;
inContainer: boolean;
containerName: string;
user: string;
appID: string;
website: string;
exclusionRules: string;

View File

@ -12,6 +12,9 @@ export const getDeviceBase = () => {
export const loadTimeZoneOptions = () => {
return http.get<Array<string>>(`/toolbox/device/zone/options`);
};
export const loadUsers = () => {
return http.get<Array<string>>(`/toolbox/device/users`);
};
export const updateDevice = (key: string, value: string) => {
return http.post(`/toolbox/device/update/conf`, { key: key, value: value }, TimeoutEnum.T_60S);
};

View File

@ -13,6 +13,7 @@ import { oneDark } from '@codemirror/theme-one-dark';
import { StreamLanguage } from '@codemirror/language';
import { nginx } from './nginx';
import { yaml } from '@codemirror/legacy-modes/mode/yaml';
import { shell } from '@codemirror/legacy-modes/mode/shell';
import { dockerFile } from '@codemirror/legacy-modes/mode/dockerfile';
import { placeholder } from '@codemirror/view';
import { json } from '@codemirror/lang-json';
@ -93,6 +94,9 @@ const initCodeMirror = () => {
case 'json':
extensions.push(json());
break;
case 'shell':
extensions.push(StreamLanguage.define(shell));
break;
}
let startState = EditorState.create({
doc: content.value,

View File

@ -923,6 +923,7 @@ const message = {
saturday: 'Saturday',
sunday: 'Sunday',
shellContent: 'Script',
executor: 'Executor',
errRecord: 'Incorrect logging',
errHandle: 'Cronjob execution failure',
noRecord: 'The execution did not generate any logs',

View File

@ -878,6 +878,7 @@ const message = {
saturday: '周六',
sunday: '周日',
shellContent: '腳本內容',
executor: '解釋器',
errRecord: '錯誤的日誌記錄',
errHandle: '任務執行失敗',
noRecord: '當前計劃任務暫未產生記錄',

View File

@ -879,6 +879,7 @@ const message = {
saturday: '周六',
sunday: '周日',
shellContent: '脚本内容',
executor: '解释器',
errRecord: '错误的日志记录',
errHandle: '任务执行失败',
noRecord: '当前计划任务暂未产生记录',

View File

@ -67,149 +67,251 @@
<el-form-item :label="$t('cronjob.taskName')" prop="name">
<el-input :disabled="dialogData.title === 'edit'" clearable v-model.trim="dialogData.rowData!.name" />
</el-form-item>
<el-form-item :label="$t('cronjob.cronSpec')" prop="specCustom">
<el-checkbox :label="$t('container.custom')" v-model="dialogData.rowData!.specCustom" />
</el-form-item>
<div v-if="!dialogData.rowData!.specCustom">
<el-form-item prop="spec">
<div v-for="(specObj, index) of dialogData.rowData.specObjs" :key="index" style="width: 100%">
<el-select class="specTypeClass" v-model="specObj.specType" @change="changeSpecType(index)">
<el-option
v-for="item in specOptions"
:key="item.label"
:value="item.value"
:label="item.label"
/>
</el-select>
<el-select v-if="specObj.specType === 'perWeek'" class="specClass" v-model="specObj.week">
<el-option
v-for="item in weekOptions"
:key="item.label"
:value="item.value"
:label="item.label"
/>
</el-select>
<el-input v-if="hasDay(specObj)" class="specClass" v-model.number="specObj.day">
<template #append>
<div class="append">{{ $t('cronjob.day') }}</div>
</template>
</el-input>
<el-input v-if="hasHour(specObj)" class="specClass" v-model.number="specObj.hour">
<template #append>
<div class="append">{{ $t('commons.units.hour') }}</div>
</template>
</el-input>
<el-input
v-if="specObj.specType !== 'perNSecond'"
class="specClass"
v-model.number="specObj.minute"
>
<template #append>
<div class="append">{{ $t('commons.units.minute') }}</div>
</template>
</el-input>
<el-input
v-if="specObj.specType === 'perNSecond'"
class="specClass"
v-model.number="specObj.second"
>
<template #append>
<div class="append">{{ $t('commons.units.second') }}</div>
</template>
</el-input>
<el-popover placement="top-start" :title="$t('cronjob.nextTime')" width="200" trigger="click">
<div v-for="(time, index_t) of nextTimes" :key="index_t">
<el-tag class="mt-2">{{ time }}</el-tag>
</div>
<template #reference>
<el-button class="ml-2.5" @click="loadNext(specObj)" link type="primary">
{{ $t('commons.button.preview') }}
</el-button>
</template>
</el-popover>
<el-button
class="ml-2.5"
link
type="primary"
@click="handleSpecDelete(index)"
v-if="dialogData.rowData.specObjs.length > 1"
>
{{ $t('commons.button.delete') }}
</el-button>
<el-divider v-if="dialogData.rowData.specObjs.length > 1" class="divider" />
</div>
<el-card>
<el-form-item :label="$t('cronjob.cronSpec')" prop="specCustom">
<el-checkbox :label="$t('container.custom')" v-model="dialogData.rowData!.specCustom" />
</el-form-item>
<el-button class="mb-3" @click="handleSpecAdd()">
{{ $t('commons.button.add') }}
</el-button>
</div>
<div v-if="!dialogData.rowData!.specCustom">
<el-form-item prop="spec">
<div v-for="(specObj, index) of dialogData.rowData.specObjs" :key="index" style="width: 100%">
<el-select class="specTypeClass" v-model="specObj.specType" @change="changeSpecType(index)">
<el-option
v-for="item in specOptions"
:key="item.label"
:value="item.value"
:label="item.label"
/>
</el-select>
<el-select v-if="specObj.specType === 'perWeek'" class="specClass" v-model="specObj.week">
<el-option
v-for="item in weekOptions"
:key="item.label"
:value="item.value"
:label="item.label"
/>
</el-select>
<el-input v-if="hasDay(specObj)" class="specClass" v-model.number="specObj.day">
<template #append>
<div class="append">{{ $t('cronjob.day') }}</div>
</template>
</el-input>
<el-input v-if="hasHour(specObj)" class="specClass" v-model.number="specObj.hour">
<template #append>
<div class="append">{{ $t('commons.units.hour') }}</div>
</template>
</el-input>
<el-input
v-if="specObj.specType !== 'perNSecond'"
class="specClass"
v-model.number="specObj.minute"
>
<template #append>
<div class="append">{{ $t('commons.units.minute') }}</div>
</template>
</el-input>
<el-input
v-if="specObj.specType === 'perNSecond'"
class="specClass"
v-model.number="specObj.second"
>
<template #append>
<div class="append">{{ $t('commons.units.second') }}</div>
</template>
</el-input>
<el-popover
placement="top-start"
:title="$t('cronjob.nextTime')"
width="200"
trigger="click"
>
<div v-for="(time, index_t) of nextTimes" :key="index_t">
<el-tag class="mt-2">{{ time }}</el-tag>
</div>
<template #reference>
<el-button class="ml-2.5" @click="loadNext(specObj)" link type="primary">
{{ $t('commons.button.preview') }}
</el-button>
</template>
</el-popover>
<el-button
class="ml-2.5"
link
type="primary"
@click="handleSpecDelete(index)"
v-if="dialogData.rowData.specObjs.length > 1"
>
{{ $t('commons.button.delete') }}
</el-button>
<el-divider v-if="dialogData.rowData.specObjs.length > 1" class="divider" />
</div>
</el-form-item>
<el-button class="mb-3" @click="handleSpecAdd()">
{{ $t('commons.button.add') }}
</el-button>
</div>
<div v-if="dialogData.rowData!.specCustom">
<el-form-item prop="spec">
<div v-for="(spec, index) of dialogData.rowData.specs" :key="index" style="width: 100%">
<el-input style="width: 80%" v-model="dialogData.rowData.specs[index]" />
<el-popover placement="top-start" :title="$t('cronjob.nextTime')" width="200" trigger="click">
<div v-for="(time, index_t) of nextTimes" :key="index_t">
<el-tag class="mt-2">{{ time }}</el-tag>
</div>
<template #reference>
<el-button class="ml-2.5" @click="loadNext(spec)" link type="primary">
{{ $t('commons.button.preview') }}
</el-button>
</template>
</el-popover>
<el-button
class="ml-2.5"
link
type="primary"
@click="handleSpecCustomDelete(index)"
v-if="dialogData.rowData.specs.length > 1"
>
{{ $t('commons.button.delete') }}
</el-button>
<el-divider v-if="dialogData.rowData.specs.length > 1" class="divider" />
</div>
<div v-if="dialogData.rowData!.specCustom">
<el-form-item prop="spec">
<div v-for="(spec, index) of dialogData.rowData.specs" :key="index">
<el-input style="width: 80%" v-model="dialogData.rowData.specs[index]" />
<el-popover
placement="top-start"
:title="$t('cronjob.nextTime')"
width="200"
trigger="click"
>
<div v-for="(time, index_t) of nextTimes" :key="index_t">
<el-tag class="mt-2">{{ time }}</el-tag>
</div>
<template #reference>
<el-button class="ml-2.5" @click="loadNext(spec)" link type="primary">
{{ $t('commons.button.preview') }}
</el-button>
</template>
</el-popover>
<el-button
class="ml-2.5"
link
type="primary"
@click="handleSpecCustomDelete(index)"
v-if="dialogData.rowData.specs.length > 1"
>
{{ $t('commons.button.delete') }}
</el-button>
<el-divider v-if="dialogData.rowData.specs.length > 1" class="divider" />
</div>
</el-form-item>
<el-button class="mb-3" @click="handleSpecCustomAdd()">
{{ $t('commons.button.add') }}
</el-button>
</div>
</el-card>
<div v-if="hasScript()">
<el-form-item class="mt-5">
<el-checkbox v-model="dialogData.rowData!.inContainer">
{{ $t('cronjob.containerCheckBox') }}
</el-checkbox>
</el-form-item>
<el-card v-if="dialogData.rowData!.inContainer">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item :label="$t('cronjob.containerName')" prop="containerName">
<el-select class="selectClass" v-model="dialogData.rowData!.containerName">
<el-option
v-for="item in containerOptions"
:key="item"
:value="item"
:label="item"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="$t('container.command')" prop="command" :rules="Rules.requiredInput">
<el-checkbox border v-model="dialogData.rowData!.isCustom">
{{ $t('container.custom') }}
</el-checkbox>
<el-select
v-if="!dialogData.rowData!.isCustom"
style="width: calc(100% - 100px)"
filterable
clearable
v-model="dialogData.rowData!.command"
>
<el-option value="ash" label="/bin/ash" />
<el-option value="bash" label="/bin/bash" />
<el-option value="sh" label="/bin/sh" />
</el-select>
<el-input
clearable
v-else
style="width: calc(100% - 100px)"
v-model="dialogData.rowData!.command"
/>
</el-form-item>
</el-col>
</el-row>
</el-card>
<div v-if="!dialogData.rowData!.inContainer">
<el-card>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item
:label="$t('commons.table.user')"
prop="user"
:rules="Rules.requiredSelect"
>
<el-select filterable v-model="dialogData.rowData!.user">
<div v-for="item in userOptions" :key="item">
<el-option :value="item" :label="item" />
</div>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item
:label="$t('cronjob.executor')"
prop="executor"
:rules="Rules.requiredSelect"
>
<el-checkbox border v-model="dialogData.rowData!.isExecutorCustom">
{{ $t('container.custom') }}
</el-checkbox>
<el-select
v-if="!dialogData.rowData!.isExecutorCustom"
style="width: calc(100% - 100px)"
v-model="dialogData.rowData!.executor"
>
<el-option value="bash" label="bash" />
<el-option value="python" label="python" />
<el-option value="sh" label="sh" />
</el-select>
<el-input
clearable
v-else
style="width: calc(100% - 100px)"
v-model="dialogData.rowData!.executor"
/>
</el-form-item>
</el-col>
</el-row>
</el-card>
<el-form-item :label="$t('cronjob.shellContent')" prop="script" class="mt-5">
<el-radio-group v-model="dialogData.rowData!.scriptMode">
<el-radio value="input">{{ $t('commons.button.edit') }}</el-radio>
<el-radio value="select">{{ $t('container.pathSelect') }}</el-radio>
</el-radio-group>
<CodemirrorPro
v-if="dialogData.rowData!.scriptMode=== 'input'"
v-model="dialogData.rowData!.script"
placeholder="#Define or paste the content of your shell file here"
mode="javascript"
:heightDiff="400"
/>
<el-input
v-if="dialogData.rowData!.scriptMode=== 'select'"
:placeholder="$t('commons.example') + '/tmp/test.sh'"
v-model="dialogData.rowData!.script"
>
<template #prepend>
<FileList @choose="loadScriptDir" :dir="false"></FileList>
</template>
</el-input>
</el-form-item>
</div>
<el-form-item :label="$t('cronjob.shellContent')" v-else prop="script" class="mt-5">
<CodemirrorPro
v-if="dialogData.rowData!.scriptMode=== 'input'"
v-model="dialogData.rowData!.script"
placeholder="#Define or paste the content of your shell file here"
mode="javascript"
:heightDiff="400"
/>
</el-form-item>
<el-button class="mb-3" @click="handleSpecCustomAdd()">
{{ $t('commons.button.add') }}
</el-button>
</div>
<el-form-item v-if="hasScript()">
<el-checkbox v-model="dialogData.rowData!.inContainer">
{{ $t('cronjob.containerCheckBox') }}
</el-checkbox>
</el-form-item>
<el-form-item
v-if="hasScript() && dialogData.rowData!.inContainer"
:label="$t('cronjob.containerName')"
prop="containerName"
>
<el-select class="selectClass" v-model="dialogData.rowData!.containerName">
<el-option v-for="item in containerOptions" :key="item" :value="item" :label="item" />
</el-select>
</el-form-item>
<el-form-item
v-if="hasScript() && dialogData.rowData!.inContainer"
:label="$t('container.command')"
prop="command"
:rules="Rules.requiredInput"
>
<el-checkbox class="w-full" border v-model="dialogData.rowData!.isCustom">
{{ $t('container.custom') }}
</el-checkbox>
<el-select style="width: calc(100% - 100px)" filterable clearable v-model="dialogData.rowData!.command">
<el-option value="ash" label="/bin/ash" />
<el-option value="bash" label="/bin/bash" />
<el-option value="sh" label="/bin/sh" />
</el-select>
</el-form-item>
<el-form-item v-if="hasScript()" :label="$t('cronjob.shellContent')" prop="script">
<el-input clearable type="textarea" :rows="5" v-model="dialogData.rowData!.script" />
</el-form-item>
<el-form-item
v-if="dialogData.rowData!.type === 'website' || dialogData.rowData!.type === 'cutWebsiteLog'"
:label="dialogData.rowData!.type === 'website' ? $t('cronjob.website'):$t('website.website')"
@ -383,6 +485,7 @@ import i18n from '@/lang';
import { ElForm } from 'element-plus';
import { Cronjob } from '@/api/interface/cronjob';
import { addCronjob, editCronjob, loadNextHandle } from '@/api/modules/cronjob';
import CodemirrorPro from '@/components/codemirror-pro/index.vue';
import { listDbItems } from '@/api/modules/database';
import { GetWebsiteOptions } from '@/api/modules/website';
import { MsgError, MsgSuccess } from '@/utils/message';
@ -398,6 +501,7 @@ import {
transSpecToObj,
weekOptions,
} from './../helper';
import { loadUsers } from '@/api/modules/toolbox';
const router = useRouter();
interface DialogProps {
@ -428,6 +532,7 @@ const acceptParams = (params: DialogProps): void => {
dialogData.value.rowData.specs = dialogData.value.rowData.specs || [];
if (dialogData.value.title === 'create') {
changeType();
dialogData.value.rowData.scriptMode = 'input';
dialogData.value.rowData.dbType = 'mysql';
dialogData.value.rowData.downloadAccountID = 1;
}
@ -442,7 +547,14 @@ const acceptParams = (params: DialogProps): void => {
dialogData.value.rowData!.isCustom =
dialogData.value.rowData!.command !== 'sh' &&
dialogData.value.rowData!.command !== 'bash' &&
dialogData.value.rowData!.command === 'ash';
dialogData.value.rowData!.command !== 'ash';
dialogData.value.rowData!.executor = dialogData.value.rowData!.executor || 'bash';
dialogData.value.rowData!.isCustom =
dialogData.value.rowData!.executor !== 'sh' &&
dialogData.value.rowData!.executor !== 'bash' &&
dialogData.value.rowData!.executor !== 'python' &&
dialogData.value.rowData!.executor !== 'python3';
title.value = i18n.global.t('cronjob.' + dialogData.value.title);
if (dialogData.value?.rowData?.exclusionRules) {
@ -454,6 +566,7 @@ const acceptParams = (params: DialogProps): void => {
drawerVisible.value = true;
loadBackups();
loadAppInstalls();
loadShellUsers();
loadWebsites();
loadContainers();
if (dialogData.value.rowData?.dbType) {
@ -477,6 +590,7 @@ const websiteOptions = ref([]);
const backupOptions = ref([]);
const accountOptions = ref([]);
const appOptions = ref([]);
const userOptions = ref([]);
const dbInfo = reactive({
isExist: false,
@ -485,6 +599,14 @@ const dbInfo = reactive({
dbs: [] as Array<Database.DbItem>,
});
const verifyScript = (rule: any, value: any, callback: any) => {
if (!dialogData.value.rowData!.script || dialogData.value.rowData!.script.length === 0) {
callback(new Error(i18n.global.t('commons.rule.requiredInput')));
return;
}
callback();
};
const verifySpec = (rule: any, value: any, callback: any) => {
if (dialogData.value.rowData!.specObjs.length === 0) {
callback(new Error(i18n.global.t('commons.rule.requiredInput')));
@ -583,7 +705,7 @@ const rules = reactive({
{ validator: verifySpec, trigger: 'change', required: true },
],
script: [Rules.requiredInput],
script: [{ validator: verifyScript, trigger: 'blur', required: true }],
website: [Rules.requiredSelect],
dbName: [Rules.requiredSelect],
url: [Rules.requiredInput],
@ -600,6 +722,10 @@ const loadDir = async (path: string) => {
dialogData.value.rowData!.sourceDir = path;
};
const loadScriptDir = async (path: string) => {
dialogData.value.rowData!.script = path;
};
const hasDay = (item: any) => {
return item.specType === 'perMonth' || item.specType === 'perNDay';
};
@ -724,6 +850,11 @@ const changeAccount = async () => {
}
};
const loadShellUsers = async () => {
const res = await loadUsers();
userOptions.value = res.data || [];
};
const loadAppInstalls = async () => {
const res = await ListAppInstalled();
appOptions.value = res.data || [];