From 4de3390d492d4473c34ad2ac3f0601da581b6772 Mon Sep 17 00:00:00 2001 From: undefined Date: Wed, 5 Apr 2023 16:18:38 +0800 Subject: [PATCH] core: addon create: use symlink --- packages/hydrooj/bin/commands.ts | 185 +---------------------- packages/hydrooj/src/commands/addon.ts | 53 +++++++ packages/hydrooj/src/commands/db.ts | 76 ++++++++++ packages/hydrooj/src/commands/install.ts | 89 +++++++++++ 4 files changed, 221 insertions(+), 182 deletions(-) create mode 100644 packages/hydrooj/src/commands/addon.ts create mode 100644 packages/hydrooj/src/commands/db.ts create mode 100644 packages/hydrooj/src/commands/install.ts diff --git a/packages/hydrooj/bin/commands.ts b/packages/hydrooj/bin/commands.ts index 7ce2fe44..6ae17d97 100644 --- a/packages/hydrooj/bin/commands.ts +++ b/packages/hydrooj/bin/commands.ts @@ -1,38 +1,10 @@ /* eslint-disable import/no-dynamic-require */ -import child from 'child_process'; import os from 'os'; import path from 'path'; -import readline from 'readline/promises'; import cac from 'cac'; import fs from 'fs-extra'; -import superagent from 'superagent'; -import tar from 'tar'; -import { size } from '@hydrooj/utils'; const argv = cac().parse(); -let yarnVersion = 0; -try { - // eslint-disable-next-line no-unsafe-optional-chaining - yarnVersion = +child.execSync('yarn --version', { cwd: os.tmpdir() }).toString().split('v').pop()!.split('.')[0]; -} catch (e) { - // yarn 2 does not support global dir -} - -const exec = (...args) => { - console.log('Executing: ', args[0], args[1].join(' ')); - const res = child.spawnSync(...args); - if (res.error) throw res.error; - if (res.status) throw new Error(`Error: Exited with code ${res.status}`); - return res; -}; -function buildUrl(opts) { - let mongourl = `${opts.protocol || 'mongodb'}://`; - if (opts.username) mongourl += `${opts.username}:${opts.password}@`; - mongourl += `${opts.host}:${opts.port}/${opts.name}`; - if (opts.url) mongourl = opts.url; - if (opts.uri) mongourl = opts.uri; - return mongourl; -} const hydroPath = path.resolve(os.homedir(), '.hydro'); fs.ensureDirSync(hydroPath); @@ -50,160 +22,9 @@ if (!argv.args[0] || argv.args[0] === 'cli') { }); } else { const cli = cac(); - cli.command('db').action(() => { - const dbConfig = fs.readFileSync(path.resolve(hydroPath, 'config.json'), 'utf-8'); - const url = buildUrl(JSON.parse(dbConfig)); - try { - console.log('Detecting mongosh...'); - const mongosh = child.execSync('mongosh --version').toString(); - if (/\d+\.\d+\.\d+/.test(mongosh)) child.spawn('mongosh', [url], { stdio: 'inherit' }); - } catch (e) { - console.log('Cannot run mongosh. Trying legacy mongo client...'); - child.spawn('mongo', [url], { stdio: 'inherit' }); - } - }); - cli.command('backup').action(() => { - const dbConfig = fs.readFileSync(path.resolve(hydroPath, 'config.json'), 'utf-8'); - const url = buildUrl(JSON.parse(dbConfig)); - const dir = `${os.tmpdir()}/${Math.random().toString(36).substring(2)}`; - exec('mongodump', [url, `--out=${dir}/dump`], { stdio: 'inherit' }); - const target = `${process.cwd()}/backup-${new Date().toISOString().replace(':', '-').split(':')[0]}.zip`; - exec('zip', ['-r', target, 'dump'], { cwd: dir, stdio: 'inherit' }); - if (!argv.options.dbOnly) { - exec('zip', ['-r', target, 'file'], { cwd: '/data', stdio: 'inherit' }); - } - exec('rm', ['-rf', dir]); - const stat = fs.statSync(target); - console.log(`Database backup saved at ${target} , size: ${size(stat.size)}`); - }); - cli.command('restore ').action(async (filename) => { - const dbConfig = fs.readFileSync(path.resolve(hydroPath, 'config.json'), 'utf-8'); - const url = buildUrl(JSON.parse(dbConfig)); - const dir = `${os.tmpdir()}/${Math.random().toString(36).substring(2)}`; - if (!fs.existsSync(filename)) { - console.error('Cannot find file'); - return; - } - const rl = readline.createInterface(process.stdin, process.stdout); - const answer = await rl.question(`Overwrite current database with backup file ${filename}? [y/N]`); - rl.close(); - if (answer.toLowerCase() !== 'y') { - console.log('Abort.'); - return; - } - exec('unzip', [filename, '-d', dir], { stdio: 'inherit' }); - exec('mongorestore', [`--uri=${url}`, `--dir=${dir}/dump/hydro`, '--drop'], { stdio: 'inherit' }); - if (fs.existsSync(`${dir}/file`)) { - exec('rm', ['-rf', '/data/file/hydro'], { stdio: 'inherit' }); - exec('bash', ['-c', `mv ${dir}/file/* /data/file`], { stdio: 'inherit' }); - } - fs.removeSync(dir); - console.log('Successfully restored.'); - }); - cli.command('addon [operation] [name]').action((operation, name) => { - if (operation && !['add', 'remove', 'create', 'list'].includes(operation)) { - console.log('Unknown operation.'); - return; - } - if (operation === 'create') { - name ||= `${os.homedir()}/addon`; - fs.mkdirSync(name); - child.execSync('yarn init -y', { cwd: name }); - fs.mkdirSync(`${name}/templates`); - fs.mkdirSync(`${name}/locales`); - fs.mkdirSync(`${name}/public`); - addons.push(name); - console.log(`Addon created at ${name}`); - } else if (operation && name) { - for (let i = 0; i < addons.length; i++) { - if (addons[i] === name) { - addons.splice(i, 1); - break; - } - } - } - - if (operation === 'add' && name) { - try { - require.resolve(`${name}/package.json`); - } catch (e) { - console.error(`Addon not found or not available: ${name}`); - return; - } - addons.push(name); - } - addons = Array.from(new Set(addons)); - console.log('Current Addons: ', addons); - fs.writeFileSync(addonPath, JSON.stringify(addons, null, 2)); - }); - cli.command('install [package]').action(async (_src) => { - if (!_src) { - cli.outputHelp(); - return; - } - if (yarnVersion !== 1) throw new Error('Yarn 1 is required.'); - const addonDir = path.join(hydroPath, 'addons'); - let newAddonPath: string = ''; - fs.ensureDirSync(addonDir); - let src = _src; - if (!src.startsWith('http')) { - try { - src = child.execSync(`yarn info ${src} dist.tarball`, { cwd: os.tmpdir() }) - .toString().trim().split('\n')[1]; - } catch (e) { - throw new Error('Cannot fetch package info.'); - } - } - if (src.startsWith('@hydrooj/')) { - src = child.execSync(`npm info ${src} dist.tarball`, { cwd: os.tmpdir() }).toString().trim(); - if (!src.startsWith('http')) throw new Error('Cannot fetch package info.'); - } - if (src.startsWith('http')) { - const url = new URL(src); - const filename = url.pathname.split('/').pop()!; - if (['.tar.gz', '.tgz', '.zip'].find((i) => filename.endsWith(i))) { - const name = filename.replace(/(-?\d+\.\d+\.\d+)?(\.tar\.gz|\.zip|\.tgz)$/g, ''); - newAddonPath = path.join(addonDir, name); - console.log(`Downloading ${src} to ${newAddonPath}`); - fs.ensureDirSync(newAddonPath); - await new Promise((resolve, reject) => { - superagent.get(src) - .pipe(tar.x({ - C: newAddonPath, - strip: 1, - })) - .on('finish', resolve) - .on('error', reject); - }); - } else throw new Error('Unsupported file type'); - } else throw new Error(`Unsupported install source: ${src}`); - if (!newAddonPath) throw new Error('Addon download failed'); - console.log('Installing depedencies'); - if (!fs.existsSync(path.join(newAddonPath, 'package.json'))) throw new Error('Invalid plugin file'); - child.execSync('yarn --production', { stdio: 'inherit', cwd: newAddonPath }); - child.execSync(`hydrooj addon add '${newAddonPath}'`); - fs.writeFileSync(path.join(newAddonPath, '__metadata__'), JSON.stringify({ - src: _src, - lastUpdate: Date.now(), - })); - }); - cli.command('uninstall [package]').action(async (name) => { - if (!name) { - cli.outputHelp(); - return; - } - if (yarnVersion !== 1) throw new Error('Yarn 1 is required.'); - const addonDir = path.join(hydroPath, 'addons'); - fs.ensureDirSync(addonDir); - const plugins = fs.readdirSync(addonDir); - if (!plugins.includes(name)) { - throw new Error(`Plugin ${name} not found or not installed with \`hydrooj install\`.`); - } - const newAddonPath = path.join(addonDir, name); - child.execSync(`hydrooj addon remove '${newAddonPath}'`, { stdio: 'inherit' }); - fs.removeSync(newAddonPath); - console.log(`Successfully uninstalled ${name}.`); - }); + require('../src/commands/install').register(cli); + require('../src/commands/addon').register(cli); + require('../src/commands/db').register(cli); require('../src/commands/patch').register(cli); cli.help(); cli.parse(); diff --git a/packages/hydrooj/src/commands/addon.ts b/packages/hydrooj/src/commands/addon.ts new file mode 100644 index 00000000..aee4c2ef --- /dev/null +++ b/packages/hydrooj/src/commands/addon.ts @@ -0,0 +1,53 @@ +import child from 'child_process'; +import os from 'os'; +import path from 'path'; +import { CAC } from 'cac'; +import fs from 'fs-extra'; +import { Logger } from '@hydrooj/utils'; + +const logger = new Logger('addon'); +const addonPath = path.resolve(os.homedir(), '.hydro', 'addon.json'); +const addonDir = path.resolve(os.homedir(), '.hydro', 'addons'); + +export function register(cli: CAC) { + cli.command('addon [operation] [name]').action((operation, name) => { + if (operation && !['add', 'remove', 'create', 'list'].includes(operation)) { + console.log('Unknown operation.'); + return; + } + let addons = JSON.parse(fs.readFileSync(addonPath).toString()); + if (operation === 'create') { + const dir = `${addonDir}/${name || 'addon'}`; + fs.mkdirSync(dir); + child.execSync('yarn init -y', { cwd: dir }); + fs.mkdirSync(`${dir}/templates`); + fs.mkdirSync(`${dir}/locales`); + fs.mkdirSync(`${dir}/public`); + fs.mkdirSync(`${dir}/frontend`); + fs.symlinkSync(dir, path.resolve(os.homedir(), name), 'dir'); + addons.push(dir); + logger.success(`Addon created at ${dir}`); + } else if (operation && name) { + for (let i = 0; i < addons.length; i++) { + if (addons[i] === name) { + addons.splice(i, 1); + break; + } + } + } + + if (operation === 'add' && name) { + try { + require.resolve(`${name}/package.json`); + } catch (e) { + logger.error(`Addon not found or not available: ${name}`); + return; + } + addons.push(name); + } + addons = Array.from(new Set(addons)); + logger.info('Current Addons: '); + console.log(addons); + fs.writeFileSync(addonPath, JSON.stringify(addons, null, 2)); + }); +} diff --git a/packages/hydrooj/src/commands/db.ts b/packages/hydrooj/src/commands/db.ts new file mode 100644 index 00000000..7a9c5a20 --- /dev/null +++ b/packages/hydrooj/src/commands/db.ts @@ -0,0 +1,76 @@ +import child from 'child_process'; +import os from 'os'; +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'; +const argv = cac().parse(); + +const logger = new Logger('db'); +const exec = (...args: Parameters) => { + logger.info('Executing: ', args[0], args[1].join(' ')); + const res = child.spawnSync(...args); + if (res.error) throw res.error; + if (res.status) throw new Error(`Error: Exited with code ${res.status}`); + return res; +}; +const hydroPath = path.resolve(os.homedir(), '.hydro'); +const dir = `${os.tmpdir()}/${Math.random().toString(36).substring(2)}`; +function getUrl() { + const dbConfig = fs.readFileSync(path.resolve(hydroPath, 'config.json'), 'utf-8'); + const opts = JSON.parse(dbConfig); + if (opts.url || opts.uri) return opts.url || opts.uri; + let mongourl = `${opts.protocol || 'mongodb'}://`; + if (opts.username) mongourl += `${opts.username}:${opts.password}@`; + mongourl += `${opts.host}:${opts.port}/${opts.name}`; + return mongourl; +} + +export function register(cli: CAC) { + cli.command('db').action(() => { + const url = getUrl(); + try { + logger.info('Detecting mongosh...'); + const mongosh = child.execSync('mongosh --version').toString(); + if (/\d+\.\d+\.\d+/.test(mongosh)) child.spawn('mongosh', [url], { stdio: 'inherit' }); + } catch (e) { + logger.warn('Cannot run mongosh. Trying legacy mongo client...'); + child.spawn('mongo', [url], { stdio: 'inherit' }); + } + }); + cli.command('backup').action(() => { + const url = getUrl(); + exec('mongodump', [url, `--out=${dir}/dump`], { stdio: 'inherit' }); + const target = `${process.cwd()}/backup-${new Date().toISOString().replace(':', '-').split(':')[0]}.zip`; + exec('zip', ['-r', target, 'dump'], { cwd: dir, stdio: 'inherit' }); + if (!argv.options.dbOnly) { + exec('zip', ['-r', target, 'file'], { cwd: '/data', stdio: 'inherit' }); + } + exec('rm', ['-rf', dir]); + const stat = fs.statSync(target); + logger.success(`Database backup saved at ${target} , size: ${size(stat.size)}`); + }); + cli.command('restore ').action(async (filename) => { + const url = getUrl(); + if (!fs.existsSync(filename)) { + logger.error('Cannot find file'); + return; + } + const rl = readline.createInterface(process.stdin, process.stdout); + const answer = await rl.question(`Overwrite current database with backup file ${filename}? [y/N]`); + rl.close(); + if (answer.toLowerCase() !== 'y') { + logger.warn('Abort.'); + return; + } + exec('unzip', [filename, '-d', dir], { stdio: 'inherit' }); + exec('mongorestore', [`--uri=${url}`, `--dir=${dir}/dump/hydro`, '--drop'], { stdio: 'inherit' }); + if (fs.existsSync(`${dir}/file`)) { + exec('rm', ['-rf', '/data/file/hydro'], { stdio: 'inherit' }); + exec('bash', ['-c', `mv ${dir}/file/* /data/file`], { stdio: 'inherit' }); + } + fs.removeSync(dir); + logger.success('Successfully restored.'); + }); +} diff --git a/packages/hydrooj/src/commands/install.ts b/packages/hydrooj/src/commands/install.ts new file mode 100644 index 00000000..f1c9e384 --- /dev/null +++ b/packages/hydrooj/src/commands/install.ts @@ -0,0 +1,89 @@ +import child from 'child_process'; +import os from 'os'; +import path from 'path'; +import { CAC } from 'cac'; +import fs from 'fs-extra'; +import superagent from 'superagent'; +import tar from 'tar'; +import { Logger } from '@hydrooj/utils'; + +const logger = new Logger('install'); +let yarnVersion = 0; +try { + // eslint-disable-next-line no-unsafe-optional-chaining + yarnVersion = +child.execSync('yarn --version', { cwd: os.tmpdir() }).toString().split('v').pop()!.split('.')[0]; +} catch (e) { + // yarn 2 does not support global dir +} + +const hydroPath = path.resolve(os.homedir(), '.hydro'); +const addonDir = path.join(hydroPath, 'addons'); + +export function register(cli: CAC) { + cli.command('install [package]').action(async (_src) => { + if (!_src) { + cli.outputHelp(); + return; + } + if (yarnVersion !== 1) throw new Error('Yarn 1 is required.'); + let newAddonPath: string = ''; + fs.ensureDirSync(addonDir); + let src = _src; + if (!src.startsWith('http')) { + try { + src = child.execSync(`yarn info ${src} dist.tarball`, { cwd: os.tmpdir() }) + .toString().trim().split('\n')[1]; + } catch (e) { + throw new Error('Cannot fetch package info.'); + } + } + if (src.startsWith('@hydrooj/')) { + src = child.execSync(`npm info ${src} dist.tarball`, { cwd: os.tmpdir() }).toString().trim(); + if (!src.startsWith('http')) throw new Error('Cannot fetch package info.'); + } + if (src.startsWith('http')) { + const url = new URL(src); + const filename = url.pathname.split('/').pop()!; + if (['.tar.gz', '.tgz', '.zip'].find((i) => filename.endsWith(i))) { + const name = filename.replace(/(-?\d+\.\d+\.\d+)?(\.tar\.gz|\.zip|\.tgz)$/g, ''); + newAddonPath = path.join(addonDir, name); + logger.info(`Downloading ${src} to ${newAddonPath}`); + fs.ensureDirSync(newAddonPath); + await new Promise((resolve, reject) => { + superagent.get(src) + .pipe(tar.x({ + C: newAddonPath, + strip: 1, + })) + .on('finish', resolve) + .on('error', reject); + }); + } else throw new Error('Unsupported file type'); + } else throw new Error(`Unsupported install source: ${src}`); + if (!newAddonPath) throw new Error('Addon download failed'); + logger.info('Installing depedencies'); + if (!fs.existsSync(path.join(newAddonPath, 'package.json'))) throw new Error('Invalid plugin file'); + child.execSync('yarn --production', { stdio: 'inherit', cwd: newAddonPath }); + child.execSync(`hydrooj addon add '${newAddonPath}'`); + fs.writeFileSync(path.join(newAddonPath, '__metadata__'), JSON.stringify({ + src: _src, + lastUpdate: Date.now(), + })); + }); + cli.command('uninstall [package]').action(async (name) => { + if (!name) { + cli.outputHelp(); + return; + } + if (yarnVersion !== 1) throw new Error('Yarn 1 is required.'); + fs.ensureDirSync(addonDir); + const plugins = fs.readdirSync(addonDir); + if (!plugins.includes(name)) { + throw new Error(`Plugin ${name} not found or not installed with \`hydrooj install\`.`); + } + const newAddonPath = path.join(addonDir, name); + child.execSync(`hydrooj addon remove '${newAddonPath}'`, { stdio: 'inherit' }); + fs.removeSync(newAddonPath); + logger.success(`Successfully uninstalled ${name}.`); + }); +}