mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 17:36:08 +08:00
151 lines
6.3 KiB
TypeScript
151 lines
6.3 KiB
TypeScript
/* eslint-disable no-await-in-loop */
|
|
import os from 'os';
|
|
import path from 'path';
|
|
import { Readable } from 'stream';
|
|
import {
|
|
buildContent, Context, extractZip, FileTooLargeError, fs, Handler, PERM,
|
|
ProblemConfigFile, ProblemModel, randomstring, Schema, ValidationError, yaml, Zip,
|
|
} from 'hydrooj';
|
|
|
|
const tmpdir = path.join(os.tmpdir(), 'hydro', 'import-qduoj');
|
|
fs.ensureDirSync(tmpdir);
|
|
|
|
const StringValue = Schema.object({
|
|
format: Schema.union(['html', 'markdown']).default('html'),
|
|
value: Schema.string(),
|
|
});
|
|
const ProblemSchema = Schema.object({
|
|
tags: Schema.array(Schema.string()),
|
|
title: Schema.string().required(),
|
|
description: StringValue,
|
|
input_description: StringValue,
|
|
output_description: StringValue,
|
|
samples: Schema.array(Schema.object({
|
|
input: Schema.string(),
|
|
output: Schema.string(),
|
|
})),
|
|
hint: StringValue,
|
|
source: Schema.union([StringValue, Schema.string()]),
|
|
display_id: Schema.transform(
|
|
Schema.union([Schema.number(), Schema.string()]),
|
|
(value) => (typeof value === 'number' ? `P${value.toString()}` : value),
|
|
),
|
|
time_limit: Schema.union([Schema.number(), Schema.string()]).required(),
|
|
memory_limit: Schema.union([Schema.number(), Schema.string()]).required(),
|
|
spj: Schema.union([
|
|
Schema.never(),
|
|
Schema.object({
|
|
language: Schema.string().required(),
|
|
code: Schema.string().required(),
|
|
}),
|
|
]),
|
|
test_case_score: Schema.array(Schema.object({
|
|
input_name: Schema.string().required(),
|
|
output_name: Schema.string().required(),
|
|
score: Schema.number().required(),
|
|
})),
|
|
});
|
|
|
|
class ImportQduojHandler extends Handler {
|
|
async fromFile(domainId: string, zipfile: string) {
|
|
const zip = new Zip.ZipReader(Readable.toWeb(fs.createReadStream(zipfile)));
|
|
const tmp = path.resolve(tmpdir, randomstring(32));
|
|
await extractZip(zip, tmp, {
|
|
strip: true,
|
|
parseError: (e) => new ValidationError('zip', null, e.message),
|
|
});
|
|
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, 'problem.json'))) continue;
|
|
const buf = await fs.readFile(path.join(tmp, folder, 'problem.json'));
|
|
const pdoc = ProblemSchema(JSON.parse(buf.toString()));
|
|
const content = buildContent({
|
|
description: pdoc.description?.value,
|
|
input: pdoc.input_description?.value,
|
|
output: pdoc.output_description?.value,
|
|
samples: pdoc.samples.map((sample) => [sample.input, sample.output]),
|
|
hint: pdoc.hint?.value,
|
|
source: typeof pdoc.source === 'string' ? pdoc.source : pdoc.source?.value || '',
|
|
}, 'html');
|
|
if (+pdoc.display_id) pdoc.display_id = `P${pdoc.display_id}`;
|
|
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.display_id)) pdoc.display_id = null;
|
|
const pid = await ProblemModel.add(
|
|
domainId, pdoc.display_id, pdoc.title, content,
|
|
this.user._id, pdoc.tags || [],
|
|
);
|
|
const config: ProblemConfigFile = {
|
|
time: `${pdoc.time_limit}ms`,
|
|
memory: `${pdoc.memory_limit}m`,
|
|
subtasks: [],
|
|
};
|
|
const tasks = [];
|
|
for (const tc of pdoc.test_case_score) {
|
|
tasks.push(ProblemModel.addTestdata(
|
|
domainId, pid, tc.input_name,
|
|
path.join(tmp, folder, 'testcase', tc.input_name),
|
|
));
|
|
if (tc.output_name !== '-') {
|
|
tasks.push(ProblemModel.addTestdata(
|
|
domainId, pid, tc.output_name,
|
|
path.join(tmp, folder, 'testcase', tc.output_name),
|
|
));
|
|
}
|
|
config.subtasks.push({
|
|
score: tc.score,
|
|
cases: [{
|
|
input: tc.input_name,
|
|
output: tc.output_name === '-' ? '/dev/null' : tc.output_name,
|
|
}],
|
|
});
|
|
}
|
|
if (pdoc.spj?.language === 'C++') {
|
|
tasks.push(ProblemModel.addTestdata(
|
|
domainId, pid, 'checker.cc',
|
|
Buffer.from(pdoc.spj.code),
|
|
));
|
|
config.checker = 'checker.cc';
|
|
config.checker_type = 'qduoj';
|
|
}
|
|
tasks.push(
|
|
ProblemModel.addTestdata(domainId, pid, 'config.yaml', Buffer.from(yaml.dump(config))),
|
|
ProblemModel.edit(domainId, pid, { html: true }),
|
|
);
|
|
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: 'QDUOJ' };
|
|
this.response.template = 'problem_import.html';
|
|
}
|
|
|
|
async post({ domainId }) {
|
|
const file = this.request.files.file;
|
|
if (!file) throw new ValidationError('file');
|
|
if (file.size > 256 * 1024 * 1024) throw new FileTooLargeError('256m');
|
|
await this.fromFile(domainId, file.filepath);
|
|
this.response.redirect = this.url('problem_main');
|
|
}
|
|
}
|
|
|
|
export const name = 'import-qduoj';
|
|
export async function apply(ctx: Context) {
|
|
ctx.Route('problem_import_qduoj', '/problem/import/qduoj', ImportQduojHandler, PERM.PERM_CREATE_PROBLEM);
|
|
ctx.injectUI('ProblemAdd', 'problem_import_qduoj', { icon: 'copy', text: 'From QDUOJ Export' });
|
|
ctx.i18n.load('zh', {
|
|
'From QDUOJ Export': '从 QDUOJ 导入',
|
|
});
|
|
}
|