mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 09:36:08 +08:00
core: command: add restic support for backup/restore
This commit is contained in:
1 file changed
+82
-19
@@ -4,7 +4,7 @@ import path from 'path';
|
||||
import readline from 'readline/promises';
|
||||
import cac, { CAC } from 'cac';
|
||||
import fs from 'fs-extra';
|
||||
import { Logger, size } from '@hydrooj/utils';
|
||||
import { Logger, size, sleep } from '@hydrooj/utils';
|
||||
import { hydroPath } from '../options';
|
||||
const argv = cac().parse();
|
||||
|
||||
@@ -42,30 +42,68 @@ export function register(cli: CAC) {
|
||||
cli.command('backup')
|
||||
.option('--dbOnly', 'Only dump database', { default: false })
|
||||
.option('--withAddons', 'Include addons', { default: false })
|
||||
.option('--withLogs', 'Include logs', { default: false })
|
||||
.option('-r <repo>', 'Restic repository', { default: '' })
|
||||
.option('-p <password>', 'Restic password', { default: '' })
|
||||
.action(() => {
|
||||
const url = getUrl();
|
||||
exec('mongodump', [url, `--out=${dir}/dump`], { stdio: 'inherit' });
|
||||
exec('mongodump', [
|
||||
url, `--out=${dir}/dump`,
|
||||
'--excludeCollection=opcount', '--excludeCollection=event',
|
||||
...(argv.options.withLogs ? [] : ['--excludeCollection=oplog']),
|
||||
], { stdio: 'inherit' });
|
||||
const target = `${process.cwd()}/backup-${new Date().toISOString().replace(':', '-').split(':')[0]}.zip`;
|
||||
const addToZip = (cwd: string, item: string) => exec('zip', ['-r', target, item], { cwd, stdio: 'inherit' });
|
||||
addToZip(dir, 'dump');
|
||||
if (!argv.options.dbOnly) addToZip('/data', 'file');
|
||||
const filesToAdd = [];
|
||||
const filesToRemove = [];
|
||||
const addFile = argv.options.r
|
||||
? (cwd: string, item: string, keepSource = true) => {
|
||||
filesToAdd.push(item);
|
||||
if (cwd === '/data') return;
|
||||
if (keepSource) fs.copySync(path.join(cwd, item), path.join('/data', item), { overwrite: true });
|
||||
else fs.moveSync(path.join(cwd, item), path.join('/data', item), { overwrite: true });
|
||||
filesToRemove.push(path.join(cwd, item));
|
||||
}
|
||||
: (cwd: string, item: string, keepSource = true) => {
|
||||
exec('zip', ['-r', target, item], { cwd, stdio: 'inherit' });
|
||||
if (!keepSource) fs.removeSync(path.join(cwd, item));
|
||||
};
|
||||
addFile(dir, 'dump', false);
|
||||
if (!argv.options.dbOnly) addFile('/data', 'file');
|
||||
if (argv.options.withAddons) {
|
||||
if (fs.existsSync(path.join(hydroPath, 'addons'))) addToZip(hydroPath, 'addons');
|
||||
if (fs.existsSync(path.join(hydroPath, 'addon.json'))) addToZip(hydroPath, 'addon.json');
|
||||
if (fs.existsSync(path.join(hydroPath, 'addons'))) addFile(hydroPath, 'addons');
|
||||
if (fs.existsSync(path.join(hydroPath, 'addon.json'))) addFile(hydroPath, 'addon.json');
|
||||
}
|
||||
if (argv.options.withLogs && fs.existsSync('/data/access.log')) addFile('/data', 'access.log');
|
||||
if (argv.options.r) {
|
||||
if (!argv.options.p) throw new Error('Restic password is required');
|
||||
const passwordFile = path.join(os.tmpdir(), 'hydrooj-restic-password');
|
||||
fs.writeFileSync(passwordFile, argv.options.p.toString());
|
||||
exec('restic', ['backup', '-r', argv.options.r.toString(), '-p', passwordFile, ...filesToAdd], { stdio: 'inherit', cwd: '/data' });
|
||||
fs.removeSync(passwordFile);
|
||||
for (const file of filesToRemove) fs.removeSync(file);
|
||||
} else {
|
||||
const stat = fs.statSync(target);
|
||||
logger.success(`Database backup saved at ${target} , size: ${size(stat.size)}`);
|
||||
}
|
||||
fs.removeSync(dir);
|
||||
const stat = fs.statSync(target);
|
||||
logger.success(`Database backup saved at ${target} , size: ${size(stat.size)}`);
|
||||
});
|
||||
cli.command('restore <filename>')
|
||||
cli.command('restore [filename]')
|
||||
.option('-y', 'Assume yes', { default: false })
|
||||
.option('--withAddons', 'Include addons', { default: false })
|
||||
.option('-r <repo>', 'Restic repository', { default: '' })
|
||||
.option('-p <password>', 'Restic password', { default: '' })
|
||||
.action(async (filename) => {
|
||||
const url = getUrl();
|
||||
if (!fs.existsSync(filename)) {
|
||||
let restic: child.ChildProcess;
|
||||
let dataDir = dir;
|
||||
if (filename && !fs.existsSync(filename)) {
|
||||
logger.error('Cannot find file');
|
||||
return;
|
||||
}
|
||||
if (!filename && !argv.options.r) {
|
||||
logger.error('No backup file or restic repository specified');
|
||||
return;
|
||||
}
|
||||
if (!argv.options.y) {
|
||||
const rl = readline.createInterface(process.stdin, process.stdout);
|
||||
const answer = await rl.question(`Overwrite current database with backup file ${filename}? [y/N]`);
|
||||
@@ -75,21 +113,46 @@ export function register(cli: CAC) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
exec('unzip', [filename, '-d', dir], { stdio: 'inherit' });
|
||||
exec('mongorestore', [`--uri=${url}`, `--dir=${dir}/dump/hydro`, '--drop'], { stdio: 'inherit' });
|
||||
if (fs.existsSync(`${dir}/file`)) {
|
||||
if (filename && filename.endsWith('.zip')) {
|
||||
exec('unzip', [filename, '-d', dir], { stdio: 'inherit' });
|
||||
} else if (filename) {
|
||||
dataDir = filename;
|
||||
} else {
|
||||
if (!argv.options.p) throw new Error('Restic password is required');
|
||||
const passwordFile = path.join(os.tmpdir(), 'hydrooj-restic-password');
|
||||
fs.writeFileSync(passwordFile, argv.options.p.toString());
|
||||
fs.ensureDirSync(dir);
|
||||
restic = child.spawn('restic', ['mount', '-r', argv.options.r, '-p', passwordFile, dir], { stdio: 'inherit' });
|
||||
await sleep(1000);
|
||||
fs.removeSync(passwordFile);
|
||||
dataDir = path.join(dir, 'snapshots/latest');
|
||||
}
|
||||
let isReadOnly = false;
|
||||
try {
|
||||
await fs.access(dataDir, fs.constants.W_OK);
|
||||
} catch (e) {
|
||||
isReadOnly = true;
|
||||
}
|
||||
exec('mongorestore', [`--uri=${url}`, `--dir=${dataDir}/dump/hydro`, '--drop'], { stdio: 'inherit' });
|
||||
if (fs.existsSync(`${dataDir}/file`)) {
|
||||
exec('rm', ['-rf', '/data/file/hydro'], { stdio: 'inherit' });
|
||||
exec('bash', ['-c', `mv ${dir}/file/* /data/file`], { stdio: 'inherit' });
|
||||
exec('bash', ['-c', `${isReadOnly ? 'cp -r' : 'mv'} ${dataDir}/file/* /data/file`], { stdio: 'inherit' });
|
||||
}
|
||||
if (argv.options.withAddons) {
|
||||
if (fs.existsSync(`${dir}/addons.json`)) {
|
||||
fs.copyFileSync(`${dir}/addons.json`, path.join(hydroPath, 'addons.json'));
|
||||
if (fs.existsSync(`${dataDir}/addons.json`)) {
|
||||
fs.copyFileSync(`${dataDir}/addons.json`, path.join(hydroPath, 'addons.json'));
|
||||
}
|
||||
if (fs.existsSync(`${dir}/addons`)) {
|
||||
if (fs.existsSync(`${dataDir}/addons`)) {
|
||||
fs.removeSync(path.join(hydroPath, 'addons'));
|
||||
fs.copySync(`${dir}/addons`, path.join(hydroPath, 'addons'));
|
||||
fs.copySync(`${dataDir}/addons`, path.join(hydroPath, 'addons'));
|
||||
}
|
||||
}
|
||||
if (restic) {
|
||||
await new Promise((resolve) => {
|
||||
restic.on('exit', () => resolve(null));
|
||||
restic.kill('SIGINT');
|
||||
});
|
||||
}
|
||||
fs.removeSync(dir);
|
||||
logger.success('Successfully restored.');
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user