mirror of
https://github.com/whyour/qinglong.git
synced 2026-10-18 18:33:06 +08:00
Download updates asynchronously with persistent progress and validate matching source and build artifacts before an explicit restart. Keep upgrade workers alive across service restarts, verify the new runtime, and recover the browser with a one-time success notice. Publish immutable archive sizes and checksums for accurate download progress. Align the update dialog, hide unavailable actions during transfers, and show the sidebar notice only for active, ready or failed jobs. Constrain long account names with ellipsis while preserving avatar and collapse controls.
537 lines
14 KiB
TypeScript
537 lines
14 KiB
TypeScript
import { Router, Request, Response, NextFunction } from 'express';
|
|
import { Container } from 'typedi';
|
|
import { Logger } from 'winston';
|
|
import * as fs from 'fs/promises';
|
|
import config from '../config';
|
|
import SystemService from '../services/system';
|
|
import { celebrate, Joi } from 'celebrate';
|
|
import UserService from '../services/user';
|
|
import { t } from '../shared/i18n';
|
|
import { isDefaultAuthInfo } from '../shared/auth';
|
|
import { getUniqPath, handleLogPath, parseVersion } from '../config/util';
|
|
import { findCronId } from '../shared/cronCommand';
|
|
import dayjs from 'dayjs';
|
|
import multer from 'multer';
|
|
import { logStreamManager } from '../shared/logStreamManager';
|
|
import { rateLimit } from 'express-rate-limit';
|
|
|
|
const route = Router();
|
|
const storage = multer.diskStorage({
|
|
destination: function (req, file, cb) {
|
|
cb(null, config.tmpPath);
|
|
},
|
|
filename: function (req, file, cb) {
|
|
cb(null, 'data.tgz');
|
|
},
|
|
});
|
|
const upload = multer({ storage: storage });
|
|
|
|
export default (app: Router) => {
|
|
app.use('/system', route);
|
|
|
|
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
|
const logger: Logger = Container.get('logger');
|
|
try {
|
|
const userService = Container.get(UserService);
|
|
const authInfo = await userService.getAuthInfo();
|
|
const { version, changeLog, changeLogLink, publishTime } =
|
|
await parseVersion(config.versionFile);
|
|
|
|
const isInitialized = !isDefaultAuthInfo(authInfo);
|
|
res.send({
|
|
code: 200,
|
|
data: {
|
|
isInitialized,
|
|
version,
|
|
publishTime: dayjs(publishTime).unix(),
|
|
branch: process.env.QL_BRANCH || 'master',
|
|
changeLog,
|
|
changeLogLink,
|
|
},
|
|
});
|
|
} catch (e) {
|
|
logger.error('🔥 error: %o', e);
|
|
return next(e);
|
|
}
|
|
});
|
|
|
|
route.get(
|
|
'/config',
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
const logger: Logger = Container.get('logger');
|
|
try {
|
|
const systemService = Container.get(SystemService);
|
|
const data = await systemService.getSystemConfig();
|
|
res.send({ code: 200, data });
|
|
} catch (e) {
|
|
return next(e);
|
|
}
|
|
},
|
|
);
|
|
|
|
route.put(
|
|
'/config/log-remove-frequency',
|
|
celebrate({
|
|
body: Joi.object({
|
|
logRemoveFrequency: Joi.number().allow(null),
|
|
}),
|
|
}),
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const systemService = Container.get(SystemService);
|
|
const result = await systemService.updateLogRemoveFrequency(req.body);
|
|
res.send(result);
|
|
} catch (e) {
|
|
return next(e);
|
|
}
|
|
},
|
|
);
|
|
|
|
route.put(
|
|
'/config/cron-concurrency',
|
|
celebrate({
|
|
body: Joi.object({
|
|
cronConcurrency: Joi.number().allow(null),
|
|
}),
|
|
}),
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const systemService = Container.get(SystemService);
|
|
const result = await systemService.updateCronConcurrency(req.body);
|
|
res.send(result);
|
|
} catch (e) {
|
|
return next(e);
|
|
}
|
|
},
|
|
);
|
|
|
|
route.put(
|
|
'/config/dependence-proxy',
|
|
celebrate({
|
|
body: Joi.object({
|
|
dependenceProxy: Joi.string().allow('').allow(null),
|
|
}),
|
|
}),
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const systemService = Container.get(SystemService);
|
|
const result = await systemService.updateDependenceProxy(req.body);
|
|
res.send(result);
|
|
} catch (e) {
|
|
return next(e);
|
|
}
|
|
},
|
|
);
|
|
|
|
route.put(
|
|
'/config/node-mirror',
|
|
celebrate({
|
|
body: Joi.object({
|
|
nodeMirror: Joi.string().allow('').allow(null),
|
|
}),
|
|
}),
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const systemService = Container.get(SystemService);
|
|
res.setHeader('Content-type', 'application/octet-stream');
|
|
await systemService.updateNodeMirror(req.body, res);
|
|
} catch (e) {
|
|
return next(e);
|
|
}
|
|
},
|
|
);
|
|
|
|
route.put(
|
|
'/config/python-mirror',
|
|
celebrate({
|
|
body: Joi.object({
|
|
pythonMirror: Joi.string().allow('').allow(null),
|
|
}),
|
|
}),
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const systemService = Container.get(SystemService);
|
|
const result = await systemService.updatePythonMirror(req.body);
|
|
res.send(result);
|
|
} catch (e) {
|
|
return next(e);
|
|
}
|
|
},
|
|
);
|
|
|
|
route.put(
|
|
'/config/linux-mirror',
|
|
celebrate({
|
|
body: Joi.object({
|
|
linuxMirror: Joi.string().allow('').allow(null),
|
|
}),
|
|
}),
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const systemService = Container.get(SystemService);
|
|
res.setHeader('Content-type', 'application/octet-stream');
|
|
await systemService.updateLinuxMirror(req.body, res);
|
|
} catch (e) {
|
|
return next(e);
|
|
}
|
|
},
|
|
);
|
|
|
|
route.put(
|
|
'/update-check',
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
const logger: Logger = Container.get('logger');
|
|
try {
|
|
const systemService = Container.get(SystemService);
|
|
const result = await systemService.checkUpdate();
|
|
res.send(result);
|
|
} catch (e) {
|
|
return next(e);
|
|
}
|
|
},
|
|
);
|
|
|
|
route.put(
|
|
'/update',
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
const logger: Logger = Container.get('logger');
|
|
try {
|
|
const systemService = Container.get(SystemService);
|
|
const result = await systemService.updateSystem();
|
|
res.send(result);
|
|
} catch (e) {
|
|
return next(e);
|
|
}
|
|
},
|
|
);
|
|
|
|
route.get(
|
|
'/update-status',
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
res.send(await Container.get(SystemService).getUpgradeStatus());
|
|
} catch (e) {
|
|
return next(e);
|
|
}
|
|
},
|
|
);
|
|
|
|
route.put(
|
|
'/reload',
|
|
celebrate({
|
|
body: Joi.object({
|
|
type: Joi.string().optional().allow('').allow(null),
|
|
}),
|
|
}),
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
const logger: Logger = Container.get('logger');
|
|
try {
|
|
const systemService = Container.get(SystemService);
|
|
const result = await systemService.reloadSystem(req.body.type);
|
|
res.send(result);
|
|
} catch (e) {
|
|
return next(e);
|
|
}
|
|
},
|
|
);
|
|
|
|
route.put(
|
|
'/notify',
|
|
celebrate({
|
|
body: Joi.object({
|
|
title: Joi.string().required(),
|
|
content: Joi.string().required(),
|
|
}),
|
|
}),
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
const logger: Logger = Container.get('logger');
|
|
try {
|
|
const systemService = Container.get(SystemService);
|
|
const result = await systemService.notify(req.body);
|
|
res.send(result);
|
|
} catch (e) {
|
|
return next(e);
|
|
}
|
|
},
|
|
);
|
|
|
|
route.put(
|
|
'/command-run',
|
|
rateLimit({ windowMs: 60 * 1000, limit: 60 }),
|
|
celebrate({
|
|
body: Joi.object({
|
|
command: Joi.string().required(),
|
|
}),
|
|
}),
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const systemService = Container.get(SystemService);
|
|
const command = req.body.command;
|
|
const crontab = await fs
|
|
.readFile(config.crontabFile, 'utf8')
|
|
.catch((e) => {
|
|
if (e.code === 'ENOENT') return '';
|
|
throw e;
|
|
});
|
|
const id = findCronId(crontab, command);
|
|
const uniqPath = await getUniqPath(command, id);
|
|
const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS');
|
|
const logPath = `${uniqPath}/${logTime}.log`;
|
|
res.setHeader('Content-type', 'application/octet-stream');
|
|
await systemService.run(
|
|
{ ...req.body, logPath },
|
|
{
|
|
onStart: async (cp, startTime) => {
|
|
res.setHeader('QL-Task-Pid', `${cp.pid}`);
|
|
res.setHeader('QL-Task-Log', `${logPath}`);
|
|
},
|
|
onEnd: async (cp, endTime, diff) => {
|
|
// Close the stream after task completion
|
|
try {
|
|
await logStreamManager.closeStream(
|
|
await handleLogPath(logPath),
|
|
);
|
|
} finally {
|
|
res.end();
|
|
}
|
|
},
|
|
onError: async (message: string) => {
|
|
res.write(message);
|
|
const absolutePath = await handleLogPath(logPath);
|
|
await logStreamManager.write(absolutePath, message);
|
|
},
|
|
onLog: async (message: string) => {
|
|
res.write(message);
|
|
const absolutePath = await handleLogPath(logPath);
|
|
await logStreamManager.write(absolutePath, message);
|
|
},
|
|
},
|
|
);
|
|
} catch (e) {
|
|
return next(e);
|
|
}
|
|
},
|
|
);
|
|
|
|
route.put(
|
|
'/command-stop',
|
|
celebrate({
|
|
body: Joi.object({
|
|
command: Joi.string().optional(),
|
|
pid: Joi.number().optional(),
|
|
}),
|
|
}),
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const systemService = Container.get(SystemService);
|
|
const result = await systemService.stop(req.body);
|
|
res.send(result);
|
|
} catch (e) {
|
|
return next(e);
|
|
}
|
|
},
|
|
);
|
|
|
|
route.put(
|
|
'/data/export',
|
|
celebrate({
|
|
body: Joi.object({
|
|
type: Joi.array().items(Joi.string()).optional(),
|
|
}),
|
|
}),
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const systemService = Container.get(SystemService);
|
|
await systemService.exportData(res, req.body.type);
|
|
} catch (e) {
|
|
return next(e);
|
|
}
|
|
},
|
|
);
|
|
|
|
route.put(
|
|
'/data/import',
|
|
upload.single('data'),
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const systemService = Container.get(SystemService);
|
|
const result = await systemService.importData();
|
|
res.send(result);
|
|
} catch (e) {
|
|
return next(e);
|
|
}
|
|
},
|
|
);
|
|
|
|
route.get(
|
|
'/log',
|
|
celebrate({
|
|
query: {
|
|
startTime: Joi.string().allow('').optional(),
|
|
endTime: Joi.string().allow('').optional(),
|
|
limit: Joi.number()
|
|
.integer()
|
|
.min(1)
|
|
.max(1024 * 1024)
|
|
.optional(),
|
|
t: Joi.string().optional(),
|
|
},
|
|
}),
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const systemService = Container.get(SystemService);
|
|
await systemService.getSystemLog(
|
|
res,
|
|
req.query as {
|
|
startTime?: string;
|
|
endTime?: string;
|
|
limit?: number;
|
|
},
|
|
);
|
|
} catch (e) {
|
|
return next(e);
|
|
}
|
|
},
|
|
);
|
|
|
|
route.get(
|
|
'/notify-log',
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const systemService = Container.get(SystemService);
|
|
const data = await systemService.getNotifyLog();
|
|
res.send({ code: 200, data });
|
|
} catch (e) {
|
|
return next(e);
|
|
}
|
|
},
|
|
);
|
|
|
|
route.delete(
|
|
'/log',
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const systemService = Container.get(SystemService);
|
|
await systemService.deleteSystemLog();
|
|
res.send({ code: 200 });
|
|
} catch (e) {
|
|
return next(e);
|
|
}
|
|
},
|
|
);
|
|
|
|
route.put(
|
|
'/auth/reset',
|
|
celebrate({
|
|
body: Joi.object({
|
|
retries: Joi.number().optional(),
|
|
twoFactorActivated: Joi.boolean().optional(),
|
|
password: Joi.string().optional(),
|
|
username: Joi.string().optional(),
|
|
}),
|
|
}),
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const userService = Container.get(UserService);
|
|
const result = await userService.resetAuthInfo(req.body);
|
|
if (result) {
|
|
return res.send(result);
|
|
}
|
|
res.send({ code: 200, message: t('更新成功') });
|
|
} catch (e) {
|
|
return next(e);
|
|
}
|
|
},
|
|
);
|
|
|
|
route.put(
|
|
'/config/timezone',
|
|
celebrate({
|
|
body: Joi.object({
|
|
timezone: Joi.string().allow('').allow(null),
|
|
}),
|
|
}),
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const systemService = Container.get(SystemService);
|
|
const result = await systemService.updateTimezone(req.body);
|
|
res.send(result);
|
|
} catch (e) {
|
|
return next(e);
|
|
}
|
|
},
|
|
);
|
|
|
|
route.put(
|
|
'/config/lang',
|
|
celebrate({
|
|
body: Joi.object({
|
|
lang: Joi.string().allow('').allow(null),
|
|
}),
|
|
}),
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const systemService = Container.get(SystemService);
|
|
const result = await systemService.updateLanguage(req.body);
|
|
res.send(result);
|
|
} catch (e) {
|
|
return next(e);
|
|
}
|
|
},
|
|
);
|
|
|
|
route.put(
|
|
'/config/panel-title',
|
|
celebrate({
|
|
body: Joi.object({
|
|
panelTitle: Joi.string().max(100).allow('').allow(null),
|
|
}),
|
|
}),
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const systemService = Container.get(SystemService);
|
|
const result = await systemService.updatePanelTitle(req.body);
|
|
res.send(result);
|
|
} catch (e) {
|
|
return next(e);
|
|
}
|
|
},
|
|
);
|
|
|
|
route.put(
|
|
'/config/global-ssh-key',
|
|
celebrate({
|
|
body: Joi.object({
|
|
globalSshKey: Joi.string().allow('').allow(null),
|
|
}),
|
|
}),
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const systemService = Container.get(SystemService);
|
|
const result = await systemService.updateGlobalSshKey(req.body);
|
|
res.send(result);
|
|
} catch (e) {
|
|
return next(e);
|
|
}
|
|
},
|
|
);
|
|
|
|
route.put(
|
|
'/config/dependence-clean',
|
|
celebrate({
|
|
body: Joi.object({
|
|
type: Joi.string().allow(''),
|
|
}),
|
|
}),
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const systemService = Container.get(SystemService);
|
|
const result = await systemService.cleanDependence(req.body.type);
|
|
res.send(result);
|
|
} catch (e) {
|
|
return next(e);
|
|
}
|
|
},
|
|
);
|
|
};
|