完善订阅接口

This commit is contained in:
whyour 2022-05-14 21:38:26 +08:00
parent 5a21247cbb
commit 5523d537dc
5 changed files with 90 additions and 23 deletions

View File

@ -28,7 +28,8 @@ export default (app: Router) => {
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
type: Joi.string().required(), type: Joi.string().required(),
schedule: Joi.string().required(), schedule: Joi.string().optional(),
intervalSchedule: Joi.object().optional(),
name: Joi.string().optional(), name: Joi.string().optional(),
url: Joi.string().required(), url: Joi.string().required(),
whitelist: Joi.string().optional(), whitelist: Joi.string().optional(),
@ -38,8 +39,8 @@ export default (app: Router) => {
status: Joi.number().optional(), status: Joi.number().optional(),
pull_type: Joi.string().optional(), pull_type: Joi.string().optional(),
pull_option: Joi.object().optional(), pull_option: Joi.object().optional(),
schedule_type: Joi.number().optional(), schedule_type: Joi.string().required(),
alias: Joi.number().required(), alias: Joi.string().required(),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
@ -156,7 +157,8 @@ export default (app: Router) => {
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
type: Joi.string().required(), type: Joi.string().required(),
schedule: Joi.string().required(), schedule: Joi.string().optional(),
intervalSchedule: Joi.object().optional(),
name: Joi.string().optional(), name: Joi.string().optional(),
url: Joi.string().required(), url: Joi.string().required(),
whitelist: Joi.string().optional(), whitelist: Joi.string().optional(),
@ -166,8 +168,8 @@ export default (app: Router) => {
status: Joi.number().optional(), status: Joi.number().optional(),
pull_type: Joi.string().optional(), pull_type: Joi.string().optional(),
pull_option: Joi.object().optional(), pull_option: Joi.object().optional(),
schedule_type: Joi.number().optional(), schedule_type: Joi.string().optional(),
alias: Joi.number().required(), alias: Joi.string().required(),
id: Joi.number().required(), id: Joi.number().required(),
}), }),
}), }),
@ -176,6 +178,7 @@ export default (app: Router) => {
try { try {
if ( if (
!req.body.schedule || !req.body.schedule ||
typeof req.body.schedule === 'object' ||
cron_parser.parseExpression(req.body.schedule).hasNext() cron_parser.parseExpression(req.body.schedule).hasNext()
) { ) {
const subscriptionService = Container.get(SubscriptionService); const subscriptionService = Container.get(SubscriptionService);

View File

@ -7,7 +7,8 @@ export class Subscription {
name?: string; name?: string;
type?: 'public-repo' | 'private-repo' | 'file'; type?: 'public-repo' | 'private-repo' | 'file';
schedule_type?: 'crontab' | 'interval'; schedule_type?: 'crontab' | 'interval';
schedule?: string | SimpleIntervalSchedule; schedule?: string;
intervalSchedule?: SimpleIntervalSchedule;
url?: string; url?: string;
whitelist?: string; whitelist?: string;
blacklist?: string; blacklist?: string;
@ -22,6 +23,7 @@ export class Subscription {
isDisabled?: 1 | 0; isDisabled?: 1 | 0;
log_path?: string; log_path?: string;
alias: string; alias: string;
command?: string;
constructor(options: Subscription) { constructor(options: Subscription) {
this.id = options.id; this.id = options.id;
@ -41,6 +43,7 @@ export class Subscription {
this.log_path = options.log_path; this.log_path = options.log_path;
this.schedule_type = options.schedule_type; this.schedule_type = options.schedule_type;
this.alias = options.alias; this.alias = options.alias;
this.intervalSchedule = options.intervalSchedule;
} }
} }
@ -69,6 +72,11 @@ export const SubscriptionModel = sequelize.define<SubscriptionInstance>(
unique: 'compositeIndex', unique: 'compositeIndex',
type: DataTypes.STRING, type: DataTypes.STRING,
}, },
intervalSchedule: {
unique: 'compositeIndex',
type: DataTypes.JSON,
},
type: DataTypes.STRING,
whitelist: DataTypes.STRING, whitelist: DataTypes.STRING,
blacklist: DataTypes.STRING, blacklist: DataTypes.STRING,
status: DataTypes.NUMBER, status: DataTypes.NUMBER,

View File

@ -1,7 +1,6 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import nodeSchedule from 'node-schedule'; import nodeSchedule from 'node-schedule';
import { Crontab } from '../data/cron';
import { exec } from 'child_process'; import { exec } from 'child_process';
import { import {
ToadScheduler, ToadScheduler,
@ -10,6 +9,13 @@ import {
SimpleIntervalSchedule, SimpleIntervalSchedule,
} from 'toad-scheduler'; } from 'toad-scheduler';
interface ScheduleTaskType {
id: number;
command: string;
name?: string;
schedule?: string;
}
@Service() @Service()
export default class ScheduleService { export default class ScheduleService {
private scheduleStacks = new Map<string, nodeSchedule.Job>(); private scheduleStacks = new Map<string, nodeSchedule.Job>();
@ -20,7 +26,12 @@ export default class ScheduleService {
constructor(@Inject('logger') private logger: winston.Logger) {} constructor(@Inject('logger') private logger: winston.Logger) {}
async createCronTask({ id = 0, command, name, schedule = '' }: Crontab) { async createCronTask({
id = 0,
command,
name,
schedule = '',
}: ScheduleTaskType) {
const _id = this.formatId(id); const _id = this.formatId(id);
this.logger.info( this.logger.info(
'[创建cron任务]任务ID: %scron: %s任务名: %s执行命令: %s', '[创建cron任务]任务ID: %scron: %s任务名: %s执行命令: %s',
@ -32,7 +43,7 @@ export default class ScheduleService {
this.scheduleStacks.set( this.scheduleStacks.set(
_id, _id,
nodeSchedule.scheduleJob(id + '', schedule, async () => { nodeSchedule.scheduleJob(_id, schedule, async () => {
try { try {
exec( exec(
command, command,
@ -70,15 +81,16 @@ export default class ScheduleService {
); );
} }
async cancelCronTask({ id = 0, name }: Crontab) { async cancelCronTask({ id = 0, name }: ScheduleTaskType) {
const _id = this.formatId(id); const _id = this.formatId(id);
this.logger.info('[取消定时任务],任务名:%s', name); this.logger.info('[取消定时任务],任务名:%s', name);
this.scheduleStacks.has(_id) && this.scheduleStacks.get(_id)?.cancel(); this.scheduleStacks.has(_id) && this.scheduleStacks.get(_id)?.cancel();
} }
async createIntervalTask( async createIntervalTask(
{ id = 0, command, name = '' }: Crontab, { id = 0, command, name = '' }: ScheduleTaskType,
schedule: SimpleIntervalSchedule, schedule: SimpleIntervalSchedule,
runImmediately = true,
) { ) {
const _id = this.formatId(id); const _id = this.formatId(id);
this.logger.info( this.logger.info(
@ -131,12 +143,12 @@ export default class ScheduleService {
}, },
); );
const job = new LongIntervalJob({ ...schedule }, task, _id); const job = new LongIntervalJob({ ...schedule, runImmediately }, task, _id);
this.intervalSchedule.addIntervalJob(job); this.intervalSchedule.addIntervalJob(job);
} }
async cancelIntervalTask({ id = 0, name }: Crontab) { async cancelIntervalTask({ id = 0, name }: ScheduleTaskType) {
const _id = this.formatId(id); const _id = this.formatId(id);
this.logger.info('[取消interval任务]任务ID: %s任务名%s', _id, name); this.logger.info('[取消interval任务]任务ID: %s任务名%s', _id, name);
this.intervalSchedule.removeById(_id); this.intervalSchedule.removeById(_id);

View File

@ -14,10 +14,15 @@ import { promises, existsSync } from 'fs';
import { promisify } from 'util'; import { promisify } from 'util';
import { Op } from 'sequelize'; import { Op } from 'sequelize';
import path from 'path'; import path from 'path';
import ScheduleService from './schedule';
import { SimpleIntervalSchedule } from 'toad-scheduler';
@Service() @Service()
export default class SubscriptionService { export default class SubscriptionService {
constructor(@Inject('logger') private logger: winston.Logger) {} constructor(
@Inject('logger') private logger: winston.Logger,
private scheduleService: ScheduleService,
) {}
public async list(searchText?: string): Promise<Subscription[]> { public async list(searchText?: string): Promise<Subscription[]> {
let query = {}; let query = {};
@ -75,9 +80,38 @@ export default class SubscriptionService {
} }
} }
private formatCommand(doc: Subscription) {
let command = 'ql ';
const { type, url, whitelist, blacklist, dependences, branch } = doc;
if (type === 'file') {
command += `raw ${url}`;
} else {
command += `repo ${url} ${whitelist || ''} ${blacklist || ''} ${
dependences || ''
} ${branch || ''}`;
}
return command;
}
private handleTask(doc: Subscription, needCreate = true) {
doc.command = this.formatCommand(doc);
if (doc.schedule_type === 'crontab') {
this.scheduleService.cancelCronTask(doc as any);
needCreate && this.scheduleService.createCronTask(doc as any);
} else {
this.scheduleService.cancelIntervalTask(doc as any);
needCreate &&
this.scheduleService.createIntervalTask(
doc as any,
doc.intervalSchedule as SimpleIntervalSchedule,
);
}
}
public async create(payload: Subscription): Promise<Subscription> { public async create(payload: Subscription): Promise<Subscription> {
const tab = new Subscription(payload); const tab = new Subscription(payload);
const doc = await this.insert(tab); const doc = await this.insert(tab);
this.handleTask(doc);
return doc; return doc;
} }
@ -87,6 +121,7 @@ export default class SubscriptionService {
public async update(payload: Subscription): Promise<Subscription> { public async update(payload: Subscription): Promise<Subscription> {
const newDoc = await this.updateDb(payload); const newDoc = await this.updateDb(payload);
this.handleTask(newDoc);
return newDoc; return newDoc;
} }
@ -156,7 +191,9 @@ export default class SubscriptionService {
this.logger.silly(error); this.logger.silly(error);
} }
} }
const err = await this.killTask(''); this.handleTask(doc, false);
const command = this.formatCommand(doc);
const err = await this.killTask(command);
const absolutePath = path.resolve(config.logPath, `${doc.log_path}`); const absolutePath = path.resolve(config.logPath, `${doc.log_path}`);
const logFileExist = doc.log_path && (await fileExist(absolutePath)); const logFileExist = doc.log_path && (await fileExist(absolutePath));
if (logFileExist) { if (logFileExist) {
@ -219,17 +256,16 @@ export default class SubscriptionService {
return; return;
} }
let { id, log_path } = cron; let { id, log_path, name } = cron;
const command = this.formatCommand(cron);
const absolutePath = path.resolve(config.logPath, `${log_path}`); const absolutePath = path.resolve(config.logPath, `${log_path}`);
const logFileExist = log_path && (await fileExist(absolutePath)); const logFileExist = log_path && (await fileExist(absolutePath));
this.logger.silly('Running job'); this.logger.silly('Running job' + name);
this.logger.silly('ID: ' + id); this.logger.silly('ID: ' + id);
this.logger.silly('Original command: '); this.logger.silly('Original command: ' + command);
let cmdStr = ''; const cp = spawn(command, { shell: '/bin/bash' });
const cp = spawn(cmdStr, { shell: '/bin/bash' });
await SubscriptionModel.update( await SubscriptionModel.update(
{ status: SubscriptionStatus.running, pid: cp.pid }, { status: SubscriptionStatus.running, pid: cp.pid },
@ -266,10 +302,18 @@ export default class SubscriptionService {
} }
public async disabled(ids: number[]) { public async disabled(ids: number[]) {
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
for (const doc of docs) {
this.handleTask(doc, false);
}
await SubscriptionModel.update({ isDisabled: 1 }, { where: { id: ids } }); await SubscriptionModel.update({ isDisabled: 1 }, { where: { id: ids } });
} }
public async enabled(ids: number[]) { public async enabled(ids: number[]) {
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
for (const doc of docs) {
this.handleTask(doc);
}
await SubscriptionModel.update({ isDisabled: 0 }, { where: { id: ids } }); await SubscriptionModel.update({ isDisabled: 0 }, { where: { id: ids } });
} }

View File

@ -281,7 +281,7 @@ const SubscriptionModal = ({
</Radio.Group> </Radio.Group>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="schedule" name={scheduleType === 'crontab' ? 'schedule' : 'intervalSchedule'}
label="定时规则" label="定时规则"
rules={[ rules={[
{ required: true }, { required: true },