diff --git a/hydro/handler/contest.js b/hydro/handler/contest.js index c4271a510..cff10ee4a 100644 --- a/hydro/handler/contest.js +++ b/hydro/handler/contest.js @@ -1,9 +1,14 @@ const moment = require('moment-timezone'); +const { ObjectID } = require('bson'); +const AdmZip = require('adm-zip'); const { ContestNotLiveError, ValidationError, ProblemNotFoundError, ContestNotAttendedError, } = require('../error'); -const { PERM_CREATE_CONTEST, PERM_EDIT_CONTEST } = require('../permission'); +const { + PERM_CREATE_CONTEST, PERM_EDIT_CONTEST, PERM_READ_RECORD_CODE, + PERM_VIEW_CONTEST, +} = require('../permission'); const paginate = require('../lib/paginate'); const contest = require('../model/contest'); const document = require('../model/document'); @@ -259,6 +264,30 @@ class ContestProblemSubmitHandler extends ContestProblemHandler { } } +class ContestCodeHandler extends ContestDetailHandler { + async get({ domainId, tid }) { + this.checkPerm(PERM_READ_RECORD_CODE); + this.limitRate('homework_code', 3600, 60); + const [tdoc, tsdocs] = await contest.getAndListStatus(domainId, tid); + const rnames = {}; + for (const tsdoc of tsdocs) { + for (const pdetail of tsdoc.detail || []) { + rnames[pdetail.rid] = `U${tsdoc.uid}_P${pdetail.pid}_R${pdetail.rid}`; + } + } + const zip = new AdmZip(); + const rdocs = await record.getMulti(domainId, { + _id: { + $in: Array.from(Object.keys(rnames)).map((id) => new ObjectID(id)), + }, + }).toArray(); + for (const rdoc of rdocs) { + zip.addFile(`${rnames[rdoc._id]}.${rdoc.lang}`, rdoc.code); + } + await this.binary(zip.toBuffer(), `${tdoc.title}.zip`); + } +} + class ContestCreateHandler extends ContestHandler { async get() { this.response.template = 'contest_edit.html'; @@ -302,13 +331,14 @@ class ContestCreateHandler extends ContestHandler { } async function apply() { - Route('contest_main', '/contest', ContestListHandler); - Route('contest_detail', '/contest/:tid', ContestDetailHandler); - Route('contest_edit', '/contest/:tid/edit', ContestEditHandler); - Route('contest_scoreboard', '/contest/:tid/scoreboard', ContestScoreboardHandler); - Route('contest_scoreboard_download', '/contest/:tid/export/:ext', ContestScoreboardDownloadHandler); - Route('contest_detail_problem', '/contest/:tid/p/:pid', ContestProblemHandler); - Route('contest_detail_problem_submit', '/contest/:tid/p/:pid/submit', ContestProblemSubmitHandler); + Route('contest_main', '/contest', ContestListHandler, PERM_VIEW_CONTEST); + Route('contest_detail', '/contest/:tid', ContestDetailHandler, PERM_VIEW_CONTEST); + Route('contest_edit', '/contest/:tid/edit', ContestEditHandler, PERM_VIEW_CONTEST); + Route('contest_scoreboard', '/contest/:tid/scoreboard', ContestScoreboardHandler, PERM_VIEW_CONTEST); + Route('contest_scoreboard_download', '/contest/:tid/export/:ext', ContestScoreboardDownloadHandler, PERM_VIEW_CONTEST); + Route('contest_detail_problem', '/contest/:tid/p/:pid', ContestProblemHandler, PERM_VIEW_CONTEST); + Route('contest_detail_problem_submit', '/contest/:tid/p/:pid/submit', ContestProblemSubmitHandler, PERM_VIEW_CONTEST); + Route('contest_code', '/contest/:tid/code', ContestCodeHandler, PERM_VIEW_CONTEST); Route('contest_create', '/contest/create', ContestCreateHandler, PERM_CREATE_CONTEST); } diff --git a/hydro/handler/homework.js b/hydro/handler/homework.js index b698c6b47..91a3ac759 100644 --- a/hydro/handler/homework.js +++ b/hydro/handler/homework.js @@ -1,5 +1,7 @@ const yaml = require('js-yaml'); const moment = require('moment-timezone'); +const { ObjectID } = require('bson'); +const AdmZip = require('adm-zip'); const { ValidationError, HomeworkNotLiveError, ProblemNotFoundError, HomeworkNotAttendedError, @@ -7,7 +9,7 @@ const { const { PERM_VIEW_HOMEWORK, PERM_ATTEND_HOMEWORK, PERM_VIEW_PROBLEM, PERM_SUBMIT_PROBLEM, PERM_CREATE_HOMEWORK, PERM_EDIT_HOMEWORK, - PERM_VIEW_HOMEWORK_SCOREBOARD, + PERM_VIEW_HOMEWORK_SCOREBOARD, PERM_READ_RECORD_CODE, } = require('../permission'); const { Route, Handler } = require('../service/server'); const system = require('../model/system'); @@ -221,7 +223,7 @@ class HomeworkCreateHandler extends HomeworkHandler { 'homework', beginAt.toDate(), endAt.toDate(), pids, rated, { penaltySince, penaltyRules }, document.TYPE_HOMEWORK); this.response.body = { tid }; - this.response.redirect = `/homework/${tid}`; + this.response.redirect = this.url('homework_detail', { tid }); } } @@ -289,7 +291,7 @@ class HomeworkEditHandler extends HomeworkHandler { await contest.recalcStatus(domainId, document.TYPE_HOMEWORK, tdoc.docId); } this.response.body = { tid }; - this.response.redirect = `/homework/${tid}`; + this.response.redirect = this.url('homework_detail', { tid }); } } @@ -323,6 +325,32 @@ class HomeworkScoreboardDownloadHandler extends HomeworkHandler { } } +class HomeworkCodeHandler extends HomeworkHandler { + async get({ domainId, tid }) { + this.checkPerm(PERM_READ_RECORD_CODE); + this.limitRate('homework_code', 3600, 60); + const [tdoc, tsdocs] = await contest.getAndListStatus( + domainId, tid, document.TYPE_HOMEWORK, + ); + const rnames = {}; + for (const tsdoc of tsdocs) { + for (const pdetail of tsdoc.detail || []) { + rnames[pdetail.rid] = `U${tsdoc.uid}_P${pdetail.pid}_R${pdetail.rid}`; + } + } + const zip = new AdmZip(); + const rdocs = await record.getMulti(domainId, { + _id: { + $in: Array.from(Object.keys(rnames)).map((id) => new ObjectID(id)), + }, + }).toArray(); + for (const rdoc of rdocs) { + zip.addFile(`${rnames[rdoc._id]}.${rdoc.lang}`, rdoc.code); + } + await this.binary(zip.toBuffer(), `${tdoc.title}.zip`); + } +} + async function apply() { contest.isHomeworkExtended = (tdoc) => { const now = new Date().getTime(); @@ -471,35 +499,8 @@ async function apply() { Route('homework_scoreboard', '/homework/:tid/scoreboard/download/:ext', HomeworkScoreboardDownloadHandler, PERM_VIEW_HOMEWORK_SCOREBOARD); Route('homework_detail_problem', '/homework/:tid/p/:pid', HomeworkDetailProblemHandler, PERM_VIEW_HOMEWORK); Route('homework_detail_problem_submit', '/homework/:tid/p/:pid/submit', HomeworkDetailProblemSubmitHandler, PERM_SUBMIT_PROBLEM); + Route('homework_code', '/homework/:tid/code', HomeworkCodeHandler, PERM_VIEW_HOMEWORK); Route('homework_edit', '/homework/:tid/edit', HomeworkEditHandler); } global.Hydro.handler.homework = module.exports = apply; - -/* -@app.route('/homework/{tid:\w{24}}/code', 'homework_code') -class HomeworkCodeHandler(base.OperationHandler): - @base.limit_rate('homework_code', 3600, 60) - @base.route_argument - @base.require_perm(builtin.PERM_VIEW_HOMEWORK) - @base.require_perm(builtin.PERM_READ_RECORD_CODE) - @base.sanitize - async def get(this, *, tid: objectid.ObjectId): - tdoc, tsdocs = await contest.get_and_list_status(domainId, document.TYPE_HOMEWORK, tid) - rnames = {} - for tsdoc in tsdocs: - for pdetail in tsdoc.get('detail', []): - rnames[pdetail['rid']] = 'U{}_P{}_R{}'.format(tsdoc['uid'], pdetail['pid'], pdetail['rid']) - output_buffer = io.BytesIO() - zip_file = zipfile.ZipFile(output_buffer, 'a', zipfile.ZIP_DEFLATED) - rdocs = record.get_multi(get_hidden=True, _id={'$in': list(rnames.keys())}) - async for rdoc in rdocs: - zip_file.writestr(rnames[rdoc['_id']] + '.' + rdoc['lang'], rdoc['code']) - # mark all files as created in Windows :p - for zfile in zip_file.filelist: - zfile.create_system = 0 - zip_file.close() - - await this.binary(output_buffer.getvalue(), 'application/zip', - file_name='{}.zip'.format(tdoc['title'])) -*/ diff --git a/hydro/handler/record.js b/hydro/handler/record.js index ae8c18ec0..88a080b7b 100644 --- a/hydro/handler/record.js +++ b/hydro/handler/record.js @@ -1,3 +1,4 @@ +const { ObjectID } = require('bson'); const { PERM_READ_RECORD_CODE, PERM_REJUDGE, PERM_VIEW_PROBLEM_HIDDEN } = require('../permission'); const { PermissionError } = require('../error'); const problem = require('../model/problem'); @@ -108,7 +109,7 @@ class RecordMainConnectionHandler extends RecordConnectionHandler { async message(msg) { if (msg.rids instanceof Array) { const rdocs = await record.getMulti( - this.domainId, { _id: { $in: msg.rids } }, + this.domainId, { _id: { $in: msg.rids.map((id) => new ObjectID(id)) } }, ).toArray(); for (const rdoc of rdocs) { this.onRecordChange({ value: rdoc }); @@ -150,7 +151,7 @@ class RecordDetailConnectionHandler extends contest.ContestHandlerMixin(Connecti async onRecordChange(data) { const rdoc = data.value; - if (rdoc._id.toString() !== this.rid) return; + if (rdoc._id.toString() !== this.rid.toString()) return; this.send({ status_html: await this.renderHTML('record_detail_status.html', { rdoc }), summary_html: await this.renderHTML('record_detail_summary.html', { rdoc }), diff --git a/hydro/loader.js b/hydro/loader.js index 647da6129..f4a6554df 100644 --- a/hydro/loader.js +++ b/hydro/loader.js @@ -24,7 +24,6 @@ async function fork(args) { async function entry(config) { if (config.entry) { - // TODO newProcess if (config.newProcess) { const process = await fork([`--entry=${config.entry}`]); await new Promise((resolve) => { @@ -43,7 +42,8 @@ async function stopWorker() { } async function startWorker(cnt) { - for (let i = 0; i < cnt; i++) await fork(); + await fork(['--firstWorker']); + for (let i = 1; i < cnt; i++) await fork(); } async function executeCommand(input) { @@ -120,6 +120,7 @@ async function load() { await entry({ entry: argv.entry }); console.log(`Worker ${process.pid} Started as ${argv.entry}`); } else { + if (argv.firstWorker) cluster.isFirstWorker = true; console.log(`Worker ${process.pid} Starting`); await entry({ entry: 'worker' }); console.log(`Worker ${process.pid} Started`); diff --git a/hydro/model/contest.js b/hydro/model/contest.js index 01924f21b..c0bac69dd 100644 --- a/hydro/model/contest.js +++ b/hydro/model/contest.js @@ -369,6 +369,14 @@ function _getStatusJournal(tsdoc) { return tsdoc.journal.sort((a, b) => (a.rid.generationTime - b.rid.generationTime)); } +async function getAndListStatus(domainId, tid, docType = document.TYPE_CONTEST) { + // TODO(iceboy): projection, pagination. + const tdoc = await get(domainId, tid, docType); + const tsdocs = await document.getMultiStatus(domainId, docType, { docId: tid }) + .sort(RULES[tdoc.rule].statusSort).toArray(); + return [tdoc, tsdocs]; +} + async function recalcStatus(domainId, tid, type) { const [tdoc, tsdocs] = await Promise.all([ document.get(domainId, type, tid), @@ -404,6 +412,7 @@ global.Hydro.model.contest = module.exports = { count, getMulti, setStatus, + getAndListStatus, recalcStatus, isNew, isUpcoming, diff --git a/hydro/service/server.js b/hydro/service/server.js index 0fa2dbeb6..f44b41456 100644 --- a/hydro/service/server.js +++ b/hydro/service/server.js @@ -281,10 +281,15 @@ class Handler { async putResponse() { if (this.response.disposition) this.ctx.set('Content-Disposition', this.response.disposition); - if (this.response.redirect && !this.preferJson) { - this.ctx.response.type = 'application/octet-stream'; - this.ctx.response.status = 302; - this.ctx.redirect(this.response.redirect); + if (this.response.redirect) { + if (!this.preferJson) { + this.ctx.response.type = 'application/octet-stream'; + this.ctx.response.status = 302; + this.ctx.redirect(this.response.redirect); + } else { + this.response.body = this.response.body || {}; + this.response.body.url = this.response.redirect; + } } else { if (this.response.body != null) { this.ctx.response.body = this.response.body; @@ -439,19 +444,26 @@ class ConnectionHandler { } } -function Connection(name, prefix, RouteConnHandler) { +function Connection(name, prefix, RouteConnHandler, permission) { const sock = sockjs.createServer({ prefix }); sock.on('connection', async (conn) => { const h = new RouteConnHandler(conn); try { const args = { domainId: 'system', ...h.request.params }; - - if (args.uid) args.uid = parseInt(validator.checkUid(args.uid)); - if (args.page) args.page = parseInt(args.page); - if (args.rid) args.rid = new ObjectID(args.rid); - if (args.tid) args.tid = new ObjectID(args.tid); - if (h.___prepare) await h.___prepare(args); + if (permission) h.checkPerm(permission); + let checking = ''; + try { + for (const key in validate) { + checking = key; + if (args[key]) { + args[key] = validate[key](args[key], key); + } + } + } catch (e) { + if (e instanceof ValidationError) throw e; + throw new ValidationError(`Argument ${checking} check failed`); + } if (h.__prepare) await h.__prepare(args); if (h._prepare) await h._prepare(args); if (h.prepare) await h.prepare(args); diff --git a/module/module-pastebin/handler.js b/module/module-pastebin/handler.js index ae1797ef3..119827b11 100644 --- a/module/module-pastebin/handler.js +++ b/module/module-pastebin/handler.js @@ -5,7 +5,7 @@ const { nav } = global.Hydro.lib; class PasteMainHandler extends Handler { async get() { - this.response.redirect = this.url('paste_creaate'); + this.response.redirect = this.url('paste_create'); } } diff --git a/templates/partials/nav.html b/templates/partials/nav.html index 2fd846374..c6773eb25 100644 --- a/templates/partials/nav.html +++ b/templates/partials/nav.html @@ -1,10 +1,10 @@ -{% macro nav_item(target_page_name, target_page_args, target_page_prefix) %} +{%- macro nav_item(target_page_name, target_page_args, target_page_prefix) -%}