From 65a503b3bdac968fe55b5b79173e6d23a97249c3 Mon Sep 17 00:00:00 2001 From: panda Date: Fri, 3 Nov 2023 10:09:43 +0800 Subject: [PATCH] import-hoj: add import support for HOJ (#680) --- packages/import-hoj/index.ts | 142 +++++++++++++++++++++++++++++++ packages/import-hoj/package.json | 10 +++ 2 files changed, 152 insertions(+) create mode 100644 packages/import-hoj/index.ts create mode 100644 packages/import-hoj/package.json diff --git a/packages/import-hoj/index.ts b/packages/import-hoj/index.ts new file mode 100644 index 00000000..0e8a725a --- /dev/null +++ b/packages/import-hoj/index.ts @@ -0,0 +1,142 @@ +/* eslint-disable no-await-in-loop */ +import os from 'os'; +import path from 'path'; +import { + AdmZip, buildContent, Context, fs, Handler, PERM, + ProblemConfigFile, ProblemModel, ValidationError, yaml, +} from 'hydrooj'; + +const tmpdir = path.join(os.tmpdir(), 'hydro', 'import-hoj'); +fs.ensureDirSync(tmpdir); + +class ImportHojHandler extends Handler { + async fromFile(domainId: string, zipfile: string) { + let zip: AdmZip; + try { + zip = new AdmZip(zipfile); + } catch (e) { + throw new ValidationError('zip', null, e.message); + } + const tmp = path.resolve(tmpdir, String.random(32)); + await new Promise((resolve, reject) => { + zip.extractAllToAsync(tmp, true, (err) => (err ? reject(err) : resolve(null))); + }); + let cnt = 0; + try { + const folders = await fs.readdir(tmp, { withFileTypes: true }); + for (const { name: folder } of folders.filter((i) => i.isDirectory())) { + if (!fs.existsSync(path.join(tmp, `${folder}.json`))) continue; + const buf = await fs.readFile(path.join(tmp, `${folder}.json`)); + const doc = JSON.parse(buf.toString()); + const pdoc = doc.problem; + const content = { + description: pdoc.description, + input: pdoc.input, + output: pdoc.output, + samples: [], + hint: pdoc.hint, + source: pdoc.source, + }; + if (pdoc.examples) { + const re = /([\s\S]*?)<\/input>([\s\S]*?)<\/output>/g; + const examples = pdoc.examples.match(re).map((i) => { + const m = i.match(/([\s\S]*?)<\/input>([\s\S]*?)<\/output>/); + return { input: m[1], output: m[2] }; + }); + content.samples = examples.map((sample) => ([sample.input, sample.output])); + } + const isValidPid = async (id: string) => { + if (!(/^[A-Za-z]+[0-9A-Za-z]*$/.test(id))) return false; + if (await ProblemModel.get(domainId, id)) return false; + return true; + }; + if (!await isValidPid(pdoc.problemId)) pdoc.display_id = null; + const pid = await ProblemModel.add( + domainId, pdoc.display_id, pdoc.title, buildContent(content, 'markdown'), + this.user._id, doc.tags || [], + ); + const config: ProblemConfigFile = { + time: `${pdoc.timeLimit}ms`, + memory: `${pdoc.memoryLimit}m`, + subtasks: [], + }; + if (pdoc.isFileIO && pdoc.ioReadFileName && pdoc.ioWriteFileName) { + config.filename = pdoc.ioReadFileName.split('.')[0].trim(); + } + const tasks = []; + for (const tc of doc.samples) { + tasks.push(ProblemModel.addTestdata( + domainId, pid, tc.input, + path.join(tmp, folder, tc.input), + )); + tasks.push(ProblemModel.addTestdata( + domainId, pid, tc.output, + path.join(tmp, folder, tc.output), + )); + config.subtasks.push({ + ...(tc.score ? { score: tc.score } : {}), + cases: [{ + input: tc.input, + output: tc.output, + }], + }); + } + if (pdoc.spjLanguage === 'C++') { + tasks.push(ProblemModel.addTestdata( + domainId, pid, 'checker.cc', + Buffer.from(pdoc.spjCode), + )); + config.checker = 'checker.cc'; + config.checker_type = 'testlib'; + } + if (pdoc.userExtraFile) { + for (const file of Object.keys(pdoc.userExtraFile)) { + if (file === 'testlib.h') continue; + tasks.push(ProblemModel.addTestdata( + domainId, pid, file, Buffer.from(pdoc.userExtraFile[file]), + )); + config.user_extra_files ||= []; + config.user_extra_files.push(file); + } + } + if (pdoc.judgeExtraFile) { + for (const file of Object.keys(pdoc.judgeExtraFile)) { + tasks.push(ProblemModel.addTestdata( + domainId, pid, file, Buffer.from(pdoc.judgeExtraFile[file]), + )); + config.judge_extra_files ||= []; + config.judge_extra_files.push(file); + } + } + tasks.push(ProblemModel.addTestdata(domainId, pid, 'config.yaml', Buffer.from(yaml.dump(config)))); + await Promise.all(tasks); + cnt++; + } + } finally { + await fs.remove(tmp); + } + if (!cnt) throw new ValidationError('zip', 'No problemset imported'); + } + + async get() { + this.response.body = { type: 'HOJ' }; + this.response.template = 'problem_import.html'; + } + + async post({ domainId }) { + if (!this.request.files.file) throw new ValidationError('file'); + const stat = await fs.stat(this.request.files.file.filepath); + if (stat.size > 128 * 1024 * 1024) throw new ValidationError('file', 'File too large'); + await this.fromFile(domainId, this.request.files.file.filepath); + this.response.redirect = this.url('problem_main'); + } +} + +export const name = 'import-hoj'; +export async function apply(ctx: Context) { + ctx.Route('problem_import_hoj', '/problem/import/hoj', ImportHojHandler, PERM.PERM_CREATE_PROBLEM); + ctx.inject('ProblemAdd', 'problem_import_hoj', { icon: 'copy', text: 'From HOJ Export' }); + ctx.i18n.load('zh', { + 'From HOJ Export': '从 HOJ 导入', + }); +} diff --git a/packages/import-hoj/package.json b/packages/import-hoj/package.json new file mode 100644 index 00000000..ea36d3ea --- /dev/null +++ b/packages/import-hoj/package.json @@ -0,0 +1,10 @@ +{ + "name": "@hydrooj/import-hoj", + "version": "0.0.1", + "description": "Import HOJ problem export", + "main": "index.ts", + "repository": "https://github.com/hydro-dev/Hydro.git", + "author": "panda ", + "license": "AGPL-3.0-or-later", + "preferUnplugged": true +}