增加数据备份功能

This commit is contained in:
whyour 2023-07-16 00:23:29 +08:00
parent 8affff96f3
commit 88b87de391
7 changed files with 110 additions and 13 deletions

View File

@ -209,4 +209,16 @@ export default (app: Router) => {
} }
}, },
); );
route.put(
'/data/export',
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
await systemService.exportData(res);
} catch (e) {
return next(e);
}
},
);
}; };

View File

@ -20,6 +20,7 @@ const rootPath = process.env.QL_DIR as string;
const envFound = dotenv.config({ path: path.join(rootPath, '.env') }); const envFound = dotenv.config({ path: path.join(rootPath, '.env') });
const dataPath = path.join(rootPath, 'data/'); const dataPath = path.join(rootPath, 'data/');
const tmpPath = path.join(rootPath, '.tmp/');
const samplePath = path.join(rootPath, 'sample/'); const samplePath = path.join(rootPath, 'sample/');
const configPath = path.join(dataPath, 'config/'); const configPath = path.join(dataPath, 'config/');
const scriptPath = path.join(dataPath, 'scripts/'); const scriptPath = path.join(dataPath, 'scripts/');
@ -42,6 +43,7 @@ const authError = '错误的用户名密码,请重试';
const loginFaild = '请先登录!'; const loginFaild = '请先登录!';
const configString = 'config sample crontab shareCode diy'; const configString = 'config sample crontab shareCode diy';
const versionFile = path.join(rootPath, 'version.yaml'); const versionFile = path.join(rootPath, 'version.yaml');
const dataTgzFile = path.join(tmpPath, 'data.tgz');
if (envFound.error) { if (envFound.error) {
throw new Error("⚠️ Couldn't find .env file ⚠️"); throw new Error("⚠️ Couldn't find .env file ⚠️");
@ -59,6 +61,9 @@ export default {
prefix: '/api', prefix: '/api',
}, },
rootPath, rootPath,
tmpPath,
dataPath,
dataTgzFile,
configString, configString,
loginFaild, loginFaild,
authError, authError,

View File

@ -1,7 +1,7 @@
import { Response } from 'express';
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import config from '../config'; import config from '../config';
import * as fs from 'fs';
import { import {
AuthDataType, AuthDataType,
AuthInfo, AuthInfo,
@ -23,6 +23,8 @@ import {
} from '../config/util'; } from '../config/util';
import { TASK_COMMAND } from '../config/const'; import { TASK_COMMAND } from '../config/const';
import taskLimit from '../shared/pLimit'; import taskLimit from '../shared/pLimit';
import tar from 'tar';
import fs from 'fs';
@Service() @Service()
export default class SystemService { export default class SystemService {
@ -33,7 +35,7 @@ export default class SystemService {
@Inject('logger') private logger: winston.Logger, @Inject('logger') private logger: winston.Logger,
private scheduleService: ScheduleService, private scheduleService: ScheduleService,
private sockService: SockService, private sockService: SockService,
) {} ) { }
public async getSystemConfig() { public async getSystemConfig() {
const doc = await this.getDb({ type: AuthDataType.systemConfig }); const doc = await this.getDb({ type: AuthDataType.systemConfig });
@ -108,7 +110,7 @@ export default class SystemService {
}, },
); );
lastVersionContent = await parseContentVersion(result.body); lastVersionContent = await parseContentVersion(result.body);
} catch (error) {} } catch (error) { }
if (!lastVersionContent) { if (!lastVersionContent) {
lastVersionContent = currentVersionContent; lastVersionContent = currentVersionContent;
@ -250,4 +252,18 @@ export default class SystemService {
return { code: 400, message: '任务未找到' }; return { code: 400, message: '任务未找到' };
} }
} }
public async exportData(res: Response) {
try {
await tar.create({ gzip: true, file: config.dataTgzFile, cwd: config.rootPath }, ['data'])
const dataFile = fs.createReadStream(config.dataTgzFile);
res.writeHead(200, {
'Content-Type': 'application/force-download',
'Content-Disposition': 'attachment; filename=data.tgz'
});
dataFile.pipe(res);
} catch (error: any) {
return res.send({ code: 400, message: error.message });
}
}
} }

View File

@ -91,6 +91,7 @@
"serve-handler": "^6.1.3", "serve-handler": "^6.1.3",
"sockjs": "^0.3.24", "sockjs": "^0.3.24",
"sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.3", "sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.3",
"tar": "^6.1.15",
"toad-scheduler": "^1.6.0", "toad-scheduler": "^1.6.0",
"typedi": "^0.10.0", "typedi": "^0.10.0",
"uuid": "^8.3.2", "uuid": "^8.3.2",
@ -108,6 +109,7 @@
"@types/cross-spawn": "^6.0.2", "@types/cross-spawn": "^6.0.2",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/express-jwt": "^6.0.4", "@types/express-jwt": "^6.0.4",
"@types/file-saver": "^2.0.5",
"@types/js-yaml": "^4.0.5", "@types/js-yaml": "^4.0.5",
"@types/jsonwebtoken": "^8.5.8", "@types/jsonwebtoken": "^8.5.8",
"@types/lodash": "^4.14.185", "@types/lodash": "^4.14.185",
@ -123,6 +125,7 @@
"@types/serve-handler": "^6.1.1", "@types/serve-handler": "^6.1.1",
"@types/sockjs": "^0.3.33", "@types/sockjs": "^0.3.33",
"@types/sockjs-client": "^1.5.1", "@types/sockjs-client": "^1.5.1",
"@types/tar": "^6.1.5",
"@types/uuid": "^8.3.4", "@types/uuid": "^8.3.4",
"@umijs/max": "^4.0.55", "@umijs/max": "^4.0.55",
"@umijs/ssr-darkreader": "^4.9.45", "@umijs/ssr-darkreader": "^4.9.45",
@ -132,6 +135,7 @@
"codemirror": "^5.65.2", "codemirror": "^5.65.2",
"compression-webpack-plugin": "9.2.0", "compression-webpack-plugin": "9.2.0",
"concurrently": "^7.0.0", "concurrently": "^7.0.0",
"file-saver": "^2.0.5",
"lint-staged": "^13.0.3", "lint-staged": "^13.0.3",
"monaco-editor": "0.33.0", "monaco-editor": "0.33.0",
"nodemon": "^2.0.15", "nodemon": "^2.0.15",

View File

@ -109,6 +109,9 @@ dependencies:
sqlite3: sqlite3:
specifier: git+https://github.com/whyour/node-sqlite3.git#v1.0.3 specifier: git+https://github.com/whyour/node-sqlite3.git#v1.0.3
version: github.com/whyour/node-sqlite3/3a00af0b5d7603b7f1b290032507320b18a6b741 version: github.com/whyour/node-sqlite3/3a00af0b5d7603b7f1b290032507320b18a6b741
tar:
specifier: ^6.1.15
version: 6.1.15
toad-scheduler: toad-scheduler:
specifier: ^1.6.0 specifier: ^1.6.0
version: 1.6.1 version: 1.6.1
@ -156,6 +159,9 @@ devDependencies:
'@types/express-jwt': '@types/express-jwt':
specifier: ^6.0.4 specifier: ^6.0.4
version: 6.0.4 version: 6.0.4
'@types/file-saver':
specifier: ^2.0.5
version: 2.0.5
'@types/js-yaml': '@types/js-yaml':
specifier: ^4.0.5 specifier: ^4.0.5
version: 4.0.5 version: 4.0.5
@ -201,6 +207,9 @@ devDependencies:
'@types/sockjs-client': '@types/sockjs-client':
specifier: ^1.5.1 specifier: ^1.5.1
version: 1.5.1 version: 1.5.1
'@types/tar':
specifier: ^6.1.5
version: 6.1.5
'@types/uuid': '@types/uuid':
specifier: ^8.3.4 specifier: ^8.3.4
version: 8.3.4 version: 8.3.4
@ -228,6 +237,9 @@ devDependencies:
concurrently: concurrently:
specifier: ^7.0.0 specifier: ^7.0.0
version: 7.6.0 version: 7.6.0
file-saver:
specifier: ^2.0.5
version: 2.0.5
lint-staged: lint-staged:
specifier: ^13.0.3 specifier: ^13.0.3
version: 13.2.2 version: 13.2.2
@ -4248,6 +4260,10 @@ packages:
'@types/qs': 6.9.7 '@types/qs': 6.9.7
'@types/serve-static': 1.15.1 '@types/serve-static': 1.15.1
/@types/file-saver@2.0.5:
resolution: {integrity: sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ==}
dev: true
/@types/graceful-fs@4.1.6: /@types/graceful-fs@4.1.6:
resolution: {integrity: sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==} resolution: {integrity: sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==}
dependencies: dependencies:
@ -4457,6 +4473,13 @@ packages:
'@types/node': 17.0.45 '@types/node': 17.0.45
dev: true dev: true
/@types/tar@6.1.5:
resolution: {integrity: sha512-qm2I/RlZij5RofuY7vohTpYNaYcrSQlN2MyjucQc7ZweDwaEWkdN/EeNh6e9zjK6uEm6PwjdMXkcj05BxZdX1Q==}
dependencies:
'@types/node': 17.0.45
minipass: 4.2.8
dev: true
/@types/triple-beam@1.3.2: /@types/triple-beam@1.3.2:
resolution: {integrity: sha512-txGIh+0eDFzKGC25zORnswy+br1Ha7hj5cMVwKIU7+s0U2AxxJru/jZSMU6OC9MJWP6+pc/hc6ZjyZShpsyY2g==} resolution: {integrity: sha512-txGIh+0eDFzKGC25zORnswy+br1Ha7hj5cMVwKIU7+s0U2AxxJru/jZSMU6OC9MJWP6+pc/hc6ZjyZShpsyY2g==}
dev: false dev: false
@ -4850,6 +4873,7 @@ packages:
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
@ -4859,6 +4883,7 @@ packages:
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
@ -4868,6 +4893,7 @@ packages:
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
@ -4877,6 +4903,7 @@ packages:
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
@ -8085,6 +8112,10 @@ packages:
flat-cache: 3.0.4 flat-cache: 3.0.4
dev: true dev: true
/file-saver@2.0.5:
resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==}
dev: true
/file-uri-to-path@2.0.0: /file-uri-to-path@2.0.0:
resolution: {integrity: sha512-hjPFI8oE/2iQPVe4gbrJ73Pp+Xfub2+WI2LlXDbsaJBwT5wuMh35WNWVYYTpnz895shtwfyutMFLFywpQAFdLg==} resolution: {integrity: sha512-hjPFI8oE/2iQPVe4gbrJ73Pp+Xfub2+WI2LlXDbsaJBwT5wuMh35WNWVYYTpnz895shtwfyutMFLFywpQAFdLg==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@ -9645,6 +9676,7 @@ packages:
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
@ -9654,6 +9686,7 @@ packages:
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
@ -9663,6 +9696,7 @@ packages:
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
@ -9672,6 +9706,7 @@ packages:
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
@ -10212,6 +10247,11 @@ packages:
yallist: 4.0.0 yallist: 4.0.0
dev: false dev: false
/minipass@4.2.8:
resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==}
engines: {node: '>=8'}
dev: true
/minipass@5.0.0: /minipass@5.0.0:
resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==}
engines: {node: '>=8'} engines: {node: '>=8'}

View File

@ -5,6 +5,7 @@ import config from '@/utils/config';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import CheckUpdate from './checkUpdate'; import CheckUpdate from './checkUpdate';
import { SharedContext } from '@/layouts'; import { SharedContext } from '@/layouts';
import { saveAs } from 'file-saver';
import './index.less'; import './index.less';
const optionsWithDisabled = [ const optionsWithDisabled = [
@ -76,6 +77,17 @@ const Other = ({
}); });
}; };
const exportData = () => {
request
.put(`${config.apiPrefix}system/data/export`, { responseType: 'blob' })
.then((res) => {
saveAs(res, 'data.tgz');
})
.catch((error: any) => {
console.log(error);
});
};
useEffect(() => { useEffect(() => {
getSystemConfig(); getSystemConfig();
}, []); }, []);
@ -127,6 +139,11 @@ const Other = ({
</Button> </Button>
</Input.Group> </Input.Group>
</Form.Item> </Form.Item>
<Form.Item label="数据备份还原" name="frequency">
<Button type="primary" onClick={exportData}>
</Button>
</Form.Item>
<Form.Item label="检查更新" name="update"> <Form.Item label="检查更新" name="update">
<CheckUpdate systemInfo={systemInfo} socketMessage={socketMessage} /> <CheckUpdate systemInfo={systemInfo} socketMessage={socketMessage} />
</Form.Item> </Form.Item>

View File

@ -66,16 +66,19 @@ _request.interceptors.response.use(async (response) => {
history.push('/login'); history.push('/login');
} }
} else { } else {
const res = await response.clone().json(); try {
if (res.code !== 200) { const res = await response.clone().json();
const msg = res.message || res.data; if (res.code !== 200) {
msg && const msg = res.message || res.data;
message.error({ msg &&
content: msg, message.error({
style: { maxWidth: 500, margin: '0 auto' }, content: msg,
}); style: { maxWidth: 500, margin: '0 auto' },
} });
return res; }
return res;
} catch (error) { }
return response;
} }
return response; return response;
}); });