diff --git a/hydro/development.js b/hydro/development.js index 2dc7aa875..0a9571903 100644 --- a/hydro/development.js +++ b/hydro/development.js @@ -35,6 +35,7 @@ async function run() { require('./handler/user'); require('./handler/contest'); require('./handler/training'); + require('./handler/import'); server.start(); } process.on('restart', async () => { diff --git a/hydro/error.js b/hydro/error.js index 8d8ee1dde..43fec6cfd 100644 --- a/hydro/error.js +++ b/hydro/error.js @@ -32,14 +32,18 @@ class NotFoundError extends UserFacingError { this.code = 404; } } - +class RemoteOnlineJudgeError extends UserFacingError { + constructor(message) { + super('RemoteOnlineJudgeError'); + this.params = [message]; + } +} class AlreadyVotedError extends BadRequestError { constructor(psid, uid) { super('You\'ve already voted.'); this.params = [psid, uid]; } } - class LoginError extends ForbiddenError { constructor(uname) { super('LoginError'); @@ -160,6 +164,7 @@ class ProblemDataNotFoundError extends NotFoundError { this.params = [pid]; } } + module.exports = { BadRequestError, ForbiddenError, @@ -187,6 +192,7 @@ module.exports = { UserFacingError, SystemError, TrainingAlreadyEnrollError, + RemoteOnlineJudgeError, }; /* diff --git a/hydro/handler/import.js b/hydro/handler/import.js new file mode 100644 index 000000000..f829ef9ed --- /dev/null +++ b/hydro/handler/import.js @@ -0,0 +1,85 @@ +const assert = require('assert'); +const download = require('../lib/download'); +const problem = require('../model/problem'); +const { Route, Handler } = require('../service/server'); +const { PERM_CREATE_PROBLEM } = require('../permission'); +const axios = require('../lib/axios'); +const { ValidationError, RemoteOnlineJudgeError } = require('../error'); + +class ProblemImportHandler extends Handler { + async syzoj(url) { + const RE_SYZOJ = /https?:\/\/([a-zA-Z0-9\.]+)\/problem\/([0-9]+)\/?/i; + assert(url.match(RE_SYZOJ), new ValidationError('url')); + if (!url.endsWith('/')) url += '/'; + const [, host, pid] = RE_SYZOJ.exec(url); + const res = await axios.get(`${url}export`); + assert(res.status === 200, new RemoteOnlineJudgeError('Cannot connect to target server')); + assert(res.data.success, new RemoteOnlineJudgeError((res.data.error || {}).message)); + const p = res.data.obj; + const content = [ + this.translate('problem.import.problem_description'), + p.description, + this.translate('problem.import.input_format'), + p.input_format, + this.translate('problem.import.output_format'), + p.output_format, + this.translate('problem.import.hint'), + p.hint, + this.translate('problem.import.limit_and_hint'), + p.limit_and_hint, + ]; + if (p.have_additional_file) { + content.push( + this.translate('problem.import.additional_file'), + `${url}download/additional_file`, + ); + } + const pdoc = { + title: p.title, + content: content.join(' \n'), + owner: this.user._id, + from: url, + pid: `${host}_${pid}`, + config: { + time: p.time_limit, + memory: p.memory_limit * 1024, + filename: p.file_io_input_name, + type: p.type, + tags: p.tags, + }, + }; + const r = await download(`${url}testdata/download`); + return [pdoc, r]; + } + + async prepare() { + this.checkPerm(PERM_CREATE_PROBLEM); + } + + async get() { + this.response.template = 'problem_import.html'; + this.response.body = { + path: [ + ['Hydro', '/'], + ['problem_main', '/p'], + ['problem_import', null], + ], + }; + } + + async post({ + url, pid, hidden, remoteType, + }) { + if (remoteType !== 'syzoj') throw new ValidationError('remoteType'); + const [pdoc, testdata] = await this.syzoj(url); + if (pid) pdoc.pid = pid; + if (hidden) pdoc.hidden = true; + const _id = await problem.add(pdoc); + console.log(_id); + await problem.setTestdata(_id, testdata); + this.response.body = { pid: pid || _id }; + this.response.redirect = `/p/${pid || _id}/settings`; + } +} + +Route('/problem/import', ProblemImportHandler); diff --git a/hydro/handler/judge.js b/hydro/handler/judge.js index 09785d36d..879fa1f6a 100644 --- a/hydro/handler/judge.js +++ b/hydro/handler/judge.js @@ -13,8 +13,8 @@ async function next(body) { let rdoc = await record.get(body.rid); const $set = {}; if (body.case) { - rdoc.cases.push(body.case); - $set.cases = rdoc.cases; + rdoc.testCases.push(body.case); + $set.testCases = rdoc.testCases; } if (body.judge_text) { rdoc.judgeTexts.push(body.judge_text); @@ -35,8 +35,8 @@ async function end(body) { let rdoc = await record.get(body.rid); const $set = {}; if (body.case) { - rdoc.cases.push(body.case); - $set.cases = rdoc.cases; + rdoc.testCases.push(body.case); + $set.testCases = rdoc.testCases; } if (body.judge_text) { rdoc.judgeTexts.push(body.judge_text); diff --git a/hydro/handler/problem.js b/hydro/handler/problem.js index bdb33361d..9e2e22e69 100644 --- a/hydro/handler/problem.js +++ b/hydro/handler/problem.js @@ -142,12 +142,12 @@ class ProblemSettingsHandler extends ProblemManageHandler { } class ProblemEditHandler extends ProblemManageHandler { - async get() { + async get({ pid }) { this.response.template = 'problem_edit.html'; this.response.body.path = [ ['Hydro', '/'], ['problem_main', '/p'], - [this.pdoc.title, `/p/${this.request.params.pid}`, true], + [this.pdoc.title, `/p/${pid}`, true], ['problem_edit', null], ]; this.response.body.page_name = 'problem_edit'; @@ -177,14 +177,7 @@ class ProblemDataUploadHandler extends ProblemManageHandler { async post() { if (!this.request.files.file) throw new BadRequestError(); const r = fs.createReadStream(this.request.files.file.path); - const f = gridfs.openUploadStream('data.zip'); - await new Promise((resolve, reject) => { - r.pipe(f); - f.once('finish', resolve); - f.once('error', reject); - }); - if (this.pdoc.data && typeof this.pdoc.data === 'object') { gridfs.delete(this.pdoc.data); } - this.pdoc = await problem.edit(this.pdoc._id, { data: f.id }); + await problem.setTestdata(this.pdoc._id, r); if (this.pdoc.data && typeof this.pdoc.data === 'object') { const files = await gridfs.find({ _id: this.pdoc.data }).toArray(); this.md5 = files[0].md5; diff --git a/hydro/lib/axios.js b/hydro/lib/axios.js new file mode 100644 index 000000000..fa4a548e6 --- /dev/null +++ b/hydro/lib/axios.js @@ -0,0 +1,23 @@ +const axios = require('axios'); + +async function get(url, options) { + let res; + try { + res = await axios.get(url, options); + } catch (e) { + res = e; + } + return res; +} + +async function post(url, data, options) { + let res; + try { + res = await axios.post(url, data, options); + } catch (e) { + res = e; + } + return res; +} + +module.exports = { get, post }; diff --git a/hydro/lib/download.js b/hydro/lib/download.js new file mode 100644 index 000000000..b0c4d5b45 --- /dev/null +++ b/hydro/lib/download.js @@ -0,0 +1,22 @@ +const axios = require('axios'); +const fs = require('fs'); + +async function download(url, path, retry = 3) { + let r; + try { + const res = await axios.get(url, { responseType: 'stream' }); + if (path) { + const w = await fs.createWriteStream(path); + res.data.pipe(w); + await new Promise((resolve, reject) => { + w.on('finish', resolve); + w.on('error', reject); + }); + } else r = res.data; + } catch (e) { + if (retry) r = await download(url, path, retry - 1); + else throw e; + } + return r; +} +module.exports = download; diff --git a/hydro/model/problem.js b/hydro/model/problem.js index cab26d3dc..ce3458777 100644 --- a/hydro/model/problem.js +++ b/hydro/model/problem.js @@ -1,4 +1,5 @@ const { ObjectID } = require('bson'); +const gridfs = require('../service/gridfs'); const { ProblemNotFoundError } = require('../error'); const validator = require('../lib/validator'); const db = require('../service/db.js'); @@ -34,7 +35,7 @@ async function add({ }) { validator.checkTitle(title); validator.checkContent(content); - await coll.insertOne({ + const res = await coll.insertOne({ content, owner, pid, @@ -46,7 +47,7 @@ async function add({ nSubmit: 0, nAccept: 0, }); - return pid; + return res.insertedId; } /** * @param {string|ObjectID} pid @@ -133,6 +134,17 @@ async function getListStatus(uid, pids) { for (const psdoc of psdocs) r[psdoc.pid] = psdoc; return r; } +async function setTestdata(_id, readStream) { + const pdoc = await getById(_id); + const f = gridfs.openUploadStream('data.zip'); + await new Promise((resolve, reject) => { + readStream.pipe(f); + f.once('finish', resolve); + f.once('error', reject); + }); + if (pdoc.data && typeof pdoc.data === 'object') gridfs.delete(this.pdoc.data); + return await edit(_id, { data: f.id }); +} module.exports = { add, @@ -146,4 +158,5 @@ module.exports = { getList, getListStatus, getMultiStatus, + setTestdata, }; diff --git a/hydro/model/record.js b/hydro/model/record.js index 014d88f67..a3773f3c7 100644 --- a/hydro/model/record.js +++ b/hydro/model/record.js @@ -52,7 +52,7 @@ function reset(rid) { status: STATUS_WAITING, time: 0, memory: 0, - cases: [], + testCases: [], judgeTexts: [], compilerTexts: [], judgeAt: null, diff --git a/hydro/service/server.js b/hydro/service/server.js index bc33d418d..0566f454b 100644 --- a/hydro/service/server.js +++ b/hydro/service/server.js @@ -115,6 +115,10 @@ class Handler { this.response.redirect = this.request.headers.referer || '/'; } + translate(str) { + return str ? str.toString().translate(this.user.language) : ''; + } + binary(data, name) { this.response.body = data; this.response.template = null; diff --git a/templates/problem_import.html b/templates/problem_import.html index ef4d8b7fd..c47701f0c 100644 --- a/templates/problem_import.html +++ b/templates/problem_import.html @@ -1,3 +1,4 @@ +{% set page_name = "problem_import" %} {% extends "layout/basic.html" %} {% block content %}