From a5004a7c1bb9be732961d71cfcabb6f79cdcaa51 Mon Sep 17 00:00:00 2001 From: undefined Date: Sat, 7 Nov 2020 17:58:55 +0800 Subject: [PATCH] feat: import problems from qduoj --- packages/hydrooj/package.json | 2 +- packages/import-qduoj/handler.ts | 127 ++++++++++++++++++ packages/import-qduoj/package.json | 24 ++++ .../template/problem_import_qduoj.html | 28 ++++ 4 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 packages/import-qduoj/handler.ts create mode 100644 packages/import-qduoj/package.json create mode 100644 packages/import-qduoj/template/problem_import_qduoj.html diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json index ebaeb3905..c177bd5f9 100644 --- a/packages/hydrooj/package.json +++ b/packages/hydrooj/package.json @@ -1,6 +1,6 @@ { "name": "hydrooj", - "version": "2.13.39", + "version": "2.13.40", "bin": "bin/hydrooj.js", "main": "dist/loader.js", "typings": "dist/loader.d.ts", diff --git a/packages/import-qduoj/handler.ts b/packages/import-qduoj/handler.ts new file mode 100644 index 000000000..bd4ded6fb --- /dev/null +++ b/packages/import-qduoj/handler.ts @@ -0,0 +1,127 @@ +/* eslint-disable no-await-in-loop */ +import path from 'path'; +import os from 'os'; +import fs from 'fs-extra'; +import AdmZip from 'adm-zip'; +import yaml from 'js-yaml'; +import { ObjectID } from 'mongodb'; +import { LocalProblemConfig } from 'hydrooj'; +import { + Route, Handler, param, Types, +} from 'hydrooj/dist/service/server'; +import { BadRequestError } from 'hydrooj/dist/error'; +import { streamToBuffer } from 'hydrooj/dist/utils'; +import { ProblemAdd } from 'hydrooj/dist/lib/ui'; +import * as file from 'hydrooj/dist/model/file'; +import * as problem from 'hydrooj/dist/model/problem'; +import { PERM } from 'hydrooj/dist/model/builtin'; + +const processing = {}; +fs.ensureDirSync('/tmp/hydro/import-qduoj'); + +class ImportQduojHandler extends Handler { + async ImportFromFile(domainId: string, id: string, zipfile: Buffer) { + const zip = new AdmZip(zipfile); + const tmp = path.resolve(os.tmpdir(), 'hydro', 'import-qduoj', id); + await new Promise((resolve, reject) => { + zip.extractAllToAsync(tmp, true, (err) => (err ? resolve() : reject(err))); + }); + try { + const folders = await fs.readdir(tmp); + for (const folder of folders) { + const buf = await fs.readFile(path.join(tmp, folder, 'problem.json')); + const pdoc = JSON.parse(buf.toString()); + const content = [ + ...pdoc.description?.value + ? [ + '

Description

', + pdoc.description.value, + ] : [], + ...pdoc.input_description?.value + ? [ + '

Input Description

', + pdoc.input_description.value, + ] : [], + ...pdoc.output_description?.value + ? [ + '

Output Description

', + pdoc.output_description.value, + ] : [], + ...(pdoc.samples as any[]).map((sample, i) => [ + `

Sample Input ${i + 1}

`,
+                        sample.input,
+                        `

Sample Output ${i + 1}

`,
+                        sample.output,
+                        '
', + ].join('\n')), + ...pdoc.hint?.value + ? [ + '

Hint

', + pdoc.hint.value, + ] : [], + ...pdoc.source ? ['

Source

', pdoc.source] : [], + ].join('\n'); + const pid = await problem.add(domainId, pdoc.display_id, pdoc.title, content, this.user._id, pdoc.tags); + const testdata = new AdmZip(); + const config: LocalProblemConfig = { + time: `${pdoc.time_limit}ms`, + memory: `${pdoc.memory_limit}m`, + subtasks: [], + }; + for (const tc of pdoc.test_case_score) { + testdata.addLocalFile(path.join(tmp, 'testcase', tc.input_name)); + testdata.addLocalFile(path.join(tmp, 'testcase', tc.output_name)); + config.subtasks.push({ + score: tc.score, + cases: [{ + input: tc.input_name, + output: tc.output_name, + }], + }); + } + testdata.addFile('config.yaml', Buffer.from(yaml.safeDump(config))); + const f = path.resolve(os.tmpdir(), 'hydro', `${Math.random()}.zip`); + await new Promise((resolve, reject) => { + testdata.writeZip(f, (err) => (err ? resolve() : reject(err))); + }); + try { + await problem.setTestdata(domainId, pid, f); + await problem.edit(domainId, pid, { html: true }); + } finally { + await fs.unlink(f); + } + } + } finally { + await fs.remove(tmp); + } + } + + @param('ufid', Types.ObjectID, true) + async get(domainId: string, ufid?: ObjectID) { + if (ufid) { + if (processing[ufid.toHexString()]) { + this.response.body = 'Processing'; + return; + } + const stat = await file.getMeta(ufid); + if (stat.size > 128 * 1024 * 1024) throw new BadRequestError('File too large'); + const stream = await file.get(ufid); + processing[ufid.toHexString()] = true; + try { + const buf = await streamToBuffer(stream); + await this.ImportFromFile(domainId, ufid.toHexString(), buf); + await file.del(ufid); + } catch (e) { + processing[ufid.toHexString()] = false; + throw e; + } + } else this.response.template = 'problem_import_qduoj.html'; + } +} + +export async function apply() { + Route('problem_import_qduoj', '/problem/import/qduoj', ImportQduojHandler, PERM.PERM_CREATE_PROBLEM); + ProblemAdd('problem_import_qduoj', {}, 'copy', 'From QDUOJ Export'); +} + +global.Hydro.handler.qduImport = apply; diff --git a/packages/import-qduoj/package.json b/packages/import-qduoj/package.json new file mode 100644 index 000000000..7fa16abad --- /dev/null +++ b/packages/import-qduoj/package.json @@ -0,0 +1,24 @@ +{ + "name": "@hydrooj/import-qduoj", + "version": "1.0.0", + "description": "Import QDUOJ problem export", + "main": "package.json", + "repository": "https://github.com/hydro-dev/Hydro.git", + "author": "undefined ", + "license": "AGPL-3.0-only", + "dependencies": { + "adm-zip": "^0.4.16", + "decode-html": "^2.0.0", + "fs-extra": "^9.0.1" + }, + "devDependencies": { + "bson": "^4.1.0" + }, + "peerDependencies": { + "hydrooj": "^2.13.0" + }, + "scripts": { + "build": "tsc", + "lint": "eslint . --ext ts --fix" + } +} diff --git a/packages/import-qduoj/template/problem_import_qduoj.html b/packages/import-qduoj/template/problem_import_qduoj.html new file mode 100644 index 000000000..a23a85bf6 --- /dev/null +++ b/packages/import-qduoj/template/problem_import_qduoj.html @@ -0,0 +1,28 @@ +{% extends "layout/basic.html" %} +{% block content %} +
+
+
+
+
+ +

{{ _('Upload zipfile') }}: + + + + +

+
+
+
+
+
+
+
+

{{ _('What is this?') }}

+

{{ _('With this feature, you can import problems that you can view from a site to here. Their title, content, tags and categories will be imported.') }}

+
+
+
+
+{% endblock %}