import-hoj: add import support for HOJ (#680)
parent
c008639242
commit
65a503b3bd
@ -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 = /<input>([\s\S]*?)<\/input><output>([\s\S]*?)<\/output>/g;
|
||||
const examples = pdoc.examples.match(re).map((i) => {
|
||||
const m = i.match(/<input>([\s\S]*?)<\/input><output>([\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 导入',
|
||||
});
|
||||
}
|
@ -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 <panda_dtdyy@outlook.com>",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"preferUnplugged": true
|
||||
}
|
Loading…
Reference in New Issue