diff --git a/packages/hydrojudge/package.json b/packages/hydrojudge/package.json index 00676638d..ecd00b0a8 100644 --- a/packages/hydrojudge/package.json +++ b/packages/hydrojudge/package.json @@ -1,7 +1,7 @@ { "name": "@hydrooj/hydrojudge", "bin": "bin/hydrojudge.js", - "version": "2.15.5", + "version": "2.16.0", "main": "package.json", "author": "undefined ", "repository": "https://github.com/hydro-dev/Hydro.git", diff --git a/packages/hydrojudge/src/check.ts b/packages/hydrojudge/src/check.ts index 8223d7d9f..8155c878c 100644 --- a/packages/hydrojudge/src/check.ts +++ b/packages/hydrojudge/src/check.ts @@ -1,4 +1,3 @@ -import fs from 'fs-extra'; import { findFileSync } from '@hydrooj/utils/lib/utils'; import checkers from './checkers'; import compile from './compile'; @@ -19,7 +18,6 @@ export async function check(config: CheckConfig): Promise<[number, number, strin export async function compileChecker(getLang: Function, checkerType: string, checker: string, copyIn: any): Promise { if (!checkers[checkerType]) throw new SystemError('Unknown checker type {0}.', [checkerType]); if (checkerType === 'testlib') copyIn['testlib.h'] = { src: testlibSrc }; - const file = await fs.readFile(checker); const s = checker.replace('@', '.').split('.'); let lang; let langId = s.pop(); @@ -30,5 +28,5 @@ export async function compileChecker(getLang: Function, checkerType: string, che } if (!lang) throw new SystemError('Unknown checker language.'); // TODO cache compiled checker - return await compile(lang, file.toString(), copyIn); + return await compile(lang, { src: checker }, copyIn); } diff --git a/packages/hydrojudge/src/compile.ts b/packages/hydrojudge/src/compile.ts index 79b73816b..eb767152b 100644 --- a/packages/hydrojudge/src/compile.ts +++ b/packages/hydrojudge/src/compile.ts @@ -7,7 +7,7 @@ import { CopyInFile } from './sandbox/interface'; import { compilerText } from './utils'; export = async function compile( - lang: LangConfig, code: string, copyIn: Record = {}, next?: Function, + lang: LangConfig, code: CopyInFile, copyIn: Record = {}, next?: Function, ): Promise { const target = lang.target || 'foo'; const execute = copyIn['execute.sh'] ? '/bin/bash execute.sh' : lang.execute; @@ -18,7 +18,7 @@ export = async function compile( } = await run( copyIn['compile.sh'] ? '/bin/bash compile.sh' : lang.compile, { - copyIn: { ...copyIn, [lang.code_file]: { content: code } }, + copyIn: { ...copyIn, [lang.code_file]: code }, copyOutCached: [target], env: { HYDRO_LANG: lang.key }, }, @@ -35,7 +35,7 @@ export = async function compile( } return { execute, - copyIn: { ...copyIn, [lang.code_file]: { content: code } }, + copyIn: { ...copyIn, [lang.code_file]: code }, clean: () => Promise.resolve(null), time, }; diff --git a/packages/hydrojudge/src/hosts/hydro.ts b/packages/hydrojudge/src/hosts/hydro.ts index e2534c333..f90156f0a 100644 --- a/packages/hydrojudge/src/hosts/hydro.ts +++ b/packages/hydrojudge/src/hosts/hydro.ts @@ -14,6 +14,7 @@ import { getConfig } from '../config'; import { CompileError, FormatError, SystemError } from '../error'; import judge from '../judge'; import log from '../log'; +import { CopyInFile } from '../sandbox/interface'; import * as sysinfo from '../sysinfo'; import * as tmpfs from '../tmpfs'; import { @@ -31,10 +32,10 @@ class JudgeTask { source: string; rid: string; lang: string; - code: string; + code: CopyInFile; tmpdir: string; input?: string; - clean: Function[]; + clean: (() => Promise)[]; data: any[]; folder: string; config: any; @@ -57,7 +58,7 @@ class JudgeTask { this.stat.handle = new Date(); this.rid = this.request.rid; this.lang = this.request.lang; - this.code = this.request.code; + this.code = { content: this.request.code }; this.config = this.request.config; this.input = this.request.input; this.data = this.request.data; @@ -100,7 +101,7 @@ class JudgeTask { } } finally { Lock.release(`${this.host}/${this.source}/${this.rid}`); - for (const clean of this.clean) await clean().catch(noop); + for (const clean of this.clean) await clean()?.catch(noop); tmpfs.umount(this.tmpdir); fs.removeSync(this.tmpdir); } @@ -109,6 +110,12 @@ class JudgeTask { async doSubmission(startPromise = Promise.resolve()) { this.stat.cache_start = new Date(); this.folder = await this.session.cacheOpen(this.source, this.data, this.next); + if ((this.code as any).content.startsWith('@@hydro_submission_file@@')) { + const id = (this.code as any).content.split('@@hydro_submission_file@@')[1]?.split('#')?.[0]; + const target = await this.session.fetchCodeFile(id); + this.code = { src: target }; + this.clean.push(() => fs.remove(target)); + } this.stat.read_cases = new Date(); this.config = await readCases( this.folder, @@ -247,6 +254,21 @@ export default class Hydro { return filePath; } + async fetchCodeFile(name: string) { + const res = await this.axios.post('judge/code', { id: name }); + const f = await this.axios.get(res.data.url, { responseType: 'stream' }) + .catch((e) => new Error(`DownloadFail(${name}): ${e.message}`)); + if (f instanceof Error) throw f; + const target = path.join('/tmp/hydro/judge', name.replace(/\//g, '_')); + const w = fs.createWriteStream(target); + f.data.pipe(w); + await new Promise((resolve, reject) => { + w.on('finish', resolve); + w.on('error', (e) => reject(new Error(`DownloadFail(${name}): ${e.message}`))); + }); + return target; + } + getLang(name: string, doThrow = true) { if (this.language[name]) return this.language[name]; if (name === 'cpp' && this.language.cc) return this.language.cc; diff --git a/packages/hydrojudge/src/judge/default.ts b/packages/hydrojudge/src/judge/default.ts index eaffb8d09..ad740cfdf 100644 --- a/packages/hydrojudge/src/judge/default.ts +++ b/packages/hydrojudge/src/judge/default.ts @@ -4,6 +4,7 @@ import { check, compileChecker } from '../check'; import compile from '../compile'; import { getConfig } from '../config'; import { CompileError, FormatError } from '../error'; +import { Logger } from '../log'; import { del, run } from '../sandbox'; import signals from '../signals'; import { parseFilename } from '../utils'; @@ -17,6 +18,8 @@ const Score = { min: Math.min, }; +const logger = new Logger('judge/default'); + function judgeCase(c: Case, sid: string) { return async (ctx: Context, ctxSubtask: ContextSubTask, runner?: Function) => { if (ctx.errored || (ctx.failed[sid] && ctxSubtask.subtask.type === 'min') @@ -91,28 +94,6 @@ function judgeCase(c: Case, sid: string) { await runner(ctx, ctxSubtask); return; } - if ([STATUS.STATUS_WRONG_ANSWER, STATUS.STATUS_RUNTIME_ERROR].includes(status)) { - const langConfig = ctx.getLang(ctx.lang); - if (langConfig.analysis && !ctx.analysis) { - ctx.analysis = true; - run(langConfig.analysis, { - copyIn: { - ...copyIn, - input: stdin ? { src: stdin } : { content: '' }, - [langConfig.code_file || 'foo']: { content: ctx.code }, - compile: { content: langConfig.compile || '' }, - execute: { content: langConfig.execute || '' }, - }, - time: 5000, - memory: 256, - env: ctx.env, - }).then((r) => { - const out = r.stdout.toString(); - if (out.length) ctx.next({ compiler_text: out.substring(0, 1024) }); - if (process.env.DEV) console.log(r); - }); - } - } ctxSubtask.score = Score[ctxSubtask.subtask.type](ctxSubtask.score, score); ctxSubtask.status = Math.max(ctxSubtask.status, status); if (ctxSubtask.status > STATUS.STATUS_ACCEPTED) ctx.failed[sid] = true; @@ -128,6 +109,32 @@ function judgeCase(c: Case, sid: string) { }, addProgress: 100 / ctx.config.count, }, c.id); + if ([STATUS.STATUS_WRONG_ANSWER, STATUS.STATUS_RUNTIME_ERROR].includes(status)) { + const langConfig = ctx.getLang(ctx.lang); + if (langConfig.analysis && !ctx.analysis) { + ctx.analysis = true; + try { + const r = await run(langConfig.analysis, { + copyIn: { + ...copyIn, + input: stdin ? { src: stdin } : { content: '' }, + [langConfig.code_file || 'foo']: ctx.code, + compile: { content: langConfig.compile || '' }, + execute: { content: langConfig.execute || '' }, + }, + time: 5000, + memory: 256, + env: ctx.env, + }); + const out = r.stdout.toString(); + if (out.length) ctx.next({ compiler_text: out.substring(0, 1024) }); + if (process.env.DEV) console.log(r); + } catch (e) { + logger.info('Failed to run analysis'); + logger.error(e); + } + } + } }; } @@ -158,10 +165,10 @@ function judgeSubtask(subtask: SubTask, sid: string) { export const judge = async (ctx: Context, startPromise = Promise.resolve()) => { if (!ctx.config.subtasks.length) throw new FormatError('Problem data not found.'); startPromise.then(() => ctx.next({ status: STATUS.STATUS_COMPILING })); - if (ctx.config.template) { + if (ctx.config.template && 'content' in ctx.code) { if (ctx.config.template[ctx.lang]) { const tpl = ctx.config.template[ctx.lang]; - ctx.code = tpl[0] + ctx.code + tpl[1]; + ctx.code.content = tpl[0] + ctx.code.content + tpl[1]; } else throw new CompileError('Language not supported by provided templates'); } [ctx.execute, ctx.checker] = await Promise.all([ @@ -176,7 +183,7 @@ export const judge = async (ctx: Context, startPromise = Promise.resolve()) => { if (['default', 'strict'].includes(ctx.config.checker_type || 'default')) { return { execute: '', copyIn: {}, clean: () => Promise.resolve(null) }; } - const copyIn = { user_code: { content: ctx.code } }; + const copyIn = { user_code: ctx.code }; for (const file of ctx.config.judge_extra_files) { copyIn[parseFilename(file)] = { src: file }; } diff --git a/packages/hydrojudge/src/judge/interactive.ts b/packages/hydrojudge/src/judge/interactive.ts index ac5391fb9..8ea0638e0 100644 --- a/packages/hydrojudge/src/judge/interactive.ts +++ b/packages/hydrojudge/src/judge/interactive.ts @@ -108,14 +108,14 @@ export const judge = async (ctx: Context, startPromise = Promise.resolve()) => { (() => { const copyIn = { 'testlib.h': { src: testlibSrc }, - user_code: { content: ctx.code }, + user_code: ctx.code, }; for (const file of ctx.config.judge_extra_files) { copyIn[parseFilename(file)] = { src: file }; } return compile( ctx.getLang(parseFilename(ctx.config.interactor).split('.')[1].replace('@', '.')), - fs.readFileSync(ctx.config.interactor).toString(), + { src: ctx.config.interactor }, copyIn, ); })(), diff --git a/packages/hydrojudge/src/judge/interface.ts b/packages/hydrojudge/src/judge/interface.ts index 88fe0518f..9b11c0351 100644 --- a/packages/hydrojudge/src/judge/interface.ts +++ b/packages/hydrojudge/src/judge/interface.ts @@ -1,6 +1,7 @@ import PQueue from 'p-queue'; import { LangConfig } from '@hydrooj/utils/lib/lang'; import { Execute } from '../interface'; +import { CopyInFile } from '../sandbox/interface'; export type Context = JudgeTaskInterface & RuntimeContext; @@ -29,7 +30,7 @@ export interface JudgeTaskInterface { stat: Record; lang: string; - code: string; + code: CopyInFile; tmpdir: string; input?: string; clean: Function[]; diff --git a/packages/hydrojudge/src/judge/run.ts b/packages/hydrojudge/src/judge/run.ts index 4de66244a..c746ea2d8 100644 --- a/packages/hydrojudge/src/judge/run.ts +++ b/packages/hydrojudge/src/judge/run.ts @@ -3,6 +3,7 @@ import fs from 'fs-extra'; import { STATUS } from '@hydrooj/utils/lib/status'; import compile from '../compile'; import { CompileError } from '../error'; +import { Logger } from '../log'; import { run } from '../sandbox'; import signals from '../signals'; import { compilerText, parseMemoryMB, parseTimeMS } from '../utils'; @@ -16,6 +17,8 @@ const failure = (status: number, message?: string) => ({ message, }); +const logger = new Logger('judge/run'); + export const judge = async (ctx: Context) => { ctx.stat.judge = new Date(); ctx.next({ status: STATUS.STATUS_COMPILING }); @@ -80,31 +83,6 @@ export const judge = async (ctx: Context) => { if (code < 32) message.push(`ExitCode: ${code} (${signals[code]})`); else message.push(`ExitCode: ${code}`); } - if ([STATUS.STATUS_WRONG_ANSWER, STATUS.STATUS_RUNTIME_ERROR].includes(status)) { - const langConfig = ctx.getLang(ctx.lang); - if (langConfig.analysis) { - ctx.analysis = true; - run(langConfig.analysis, { - copyIn: { - ...copyIn, - input: { src: stdin }, - [langConfig.code_file || 'foo']: { content: ctx.code }, - compile: { content: langConfig.compile || '' }, - execute: { content: langConfig.execute || '' }, - }, - env: { - ...ctx.env, - HYDRO_PRETEST: 'true', - }, - time: 5000, - memory: 256, - }).then((r) => { - const out = r.stdout.toString(); - if (out.length) ctx.next({ compiler_text: out.substring(0, 1024) }); - if (process.env.DEV) console.log(r); - }); - } - } message.push(fs.readFileSync(stdout).toString()); message.push(fs.readFileSync(stderr).toString()); ctx.next({ @@ -116,6 +94,35 @@ export const judge = async (ctx: Context) => { message: message.join('\n').substring(0, 102400), }, }); + if ([STATUS.STATUS_WRONG_ANSWER, STATUS.STATUS_RUNTIME_ERROR].includes(status)) { + const langConfig = ctx.getLang(ctx.lang); + if (langConfig.analysis) { + try { + ctx.analysis = true; + const r = await run(langConfig.analysis, { + copyIn: { + ...copyIn, + input: { src: stdin }, + [langConfig.code_file || 'foo']: ctx.code, + compile: { content: langConfig.compile || '' }, + execute: { content: langConfig.execute || '' }, + }, + env: { + ...ctx.env, + HYDRO_PRETEST: 'true', + }, + time: 5000, + memory: 256, + }); + const out = r.stdout.toString(); + if (out.length) ctx.next({ compiler_text: out.substring(0, 1024) }); + if (process.env.DEV) console.log(r); + } catch (e) { + logger.info('Failed to run analysis'); + logger.error(e); + } + } + } ctx.stat.done = new Date(); if (process.env.DEV) ctx.next({ message: JSON.stringify(ctx.stat) }); ctx.end({ diff --git a/packages/hydrojudge/src/judge/submit_answer.ts b/packages/hydrojudge/src/judge/submit_answer.ts index 1e87800fd..4399788fc 100644 --- a/packages/hydrojudge/src/judge/submit_answer.ts +++ b/packages/hydrojudge/src/judge/submit_answer.ts @@ -1,12 +1,19 @@ +import { readFile } from 'fs-extra'; import { STATUS } from '@hydrooj/utils/lib/status'; import { Context } from './interface'; +// TODO drop this format in next major version +// NOT RECOMMENDED TO USE export async function judge({ next, end, config, code, }: Context) { next({ status: STATUS.STATUS_JUDGING, progress: 0 }); - code = code.replace(/\r/g, ''); - const outputs = code.split('\n'); + const answer = ('src' in code) + ? await readFile(code.src, 'utf-8') + : ('content' in code) + ? code.content.toString().replace(/\r/g, '') + : ''; + const outputs = answer.split('\n'); let totalScore = 0; let totalStatus = 0; for (const i in config.outputs) { diff --git a/packages/hydrojudge/src/service.ts b/packages/hydrojudge/src/service.ts index 23fbc2b4f..a7869b501 100644 --- a/packages/hydrojudge/src/service.ts +++ b/packages/hydrojudge/src/service.ts @@ -12,6 +12,7 @@ import * as monitor from 'hydrooj/src/service/monitor'; import readCases from './cases'; import { getConfig } from './config'; import { CompileError, FormatError, SystemError } from './error'; +import { CopyInFile } from './sandbox/interface'; import * as sysinfo from './sysinfo'; import * as tmpfs from './tmpfs'; import { compilerText, md5 } from './utils'; @@ -74,6 +75,21 @@ async function postInit() { } } + async function fetchCodeFile(name: string) { + const res = await this.axios.post('judge/code', { id: name }); + const f = await this.axios.get(res.body.link, { responseType: 'stream' }) + .catch((e) => new Error(`DownloadFail(${name}): ${e.message}`)); + if (f instanceof Error) throw f; + const target = path.join('/tmp/hydro/judge', name.replace(/\//g, '_')); + const w = fs.createWriteStream(target); + f.data.pipe(w); + await new Promise((resolve, reject) => { + w.on('finish', resolve); + w.on('error', (e) => reject(new Error(`DownloadFail(${name}): ${e.message}`))); + }); + return target; + } + async function cacheOpen(source: string, files: any[]) { const filePath = path.join(getConfig('cache_dir'), source); await fs.ensureDir(filePath); @@ -148,7 +164,7 @@ async function postInit() { source: string; rid: string; lang: string; - code: string; + code: CopyInFile; data: any[]; config: any; env: any; @@ -156,7 +172,7 @@ async function postInit() { next: (data: any, id?: number) => void; end: (data: any) => void; tmpdir: string; - clean: any[]; + clean: (() => Promise)[]; folder: string; getLang: (lang: string) => any; @@ -173,7 +189,7 @@ async function postInit() { this.stat.handle = new Date(); this.rid = this.request.rid.toString(); this.lang = this.request.lang; - this.code = this.request.code; + this.code = { content: this.request.code }; this.data = this.request.data; this.source = this.request.source; this.config = this.request.config; @@ -223,6 +239,12 @@ async function postInit() { async submission() { this.stat.cache_start = new Date(); this.folder = await cacheOpen(this.source, this.data); + if ((this.code as any).content.startsWith('@@hydro_submission_file@@')) { + const id = (this.code as any).content.split('@@hydro_submission_file@@')[1]?.split('#')?.[0]; + const target = await fetchCodeFile(id); + this.code = { src: target }; + this.clean.push(() => fs.remove(target)); + } this.stat.read_cases = new Date(); this.config = await readCases( this.folder, diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json index e4e4bcc87..46fadeafd 100644 --- a/packages/hydrooj/package.json +++ b/packages/hydrooj/package.json @@ -1,6 +1,6 @@ { "name": "hydrooj", - "version": "3.11.4", + "version": "3.11.5", "bin": "bin/hydrooj.js", "main": "src/loader", "module": "src/loader", diff --git a/packages/hydrooj/setting.yaml b/packages/hydrooj/setting.yaml index 5f54b5ebb..d356e3785 100644 --- a/packages/hydrooj/setting.yaml +++ b/packages/hydrooj/setting.yaml @@ -12,6 +12,7 @@ homepage: - width: 3 hitokoto: true starred_problems: 50 + problem_search: true discussion_nodes: true suggestion: true langs: diff --git a/packages/hydrooj/src/handler/judge.ts b/packages/hydrooj/src/handler/judge.ts index 72d80b405..e1fb46ee9 100644 --- a/packages/hydrooj/src/handler/judge.ts +++ b/packages/hydrooj/src/handler/judge.ts @@ -117,13 +117,20 @@ export class JudgeFilesDownloadHandler extends Handler { // eslint-disable-next-line no-await-in-loop links[file] = await storage.signDownloadLink( `problem/${pdoc.domainId}/${pdoc.docId}/testdata/${file}`, - file, false, 'judge', + file, true, 'judge', ); } this.response.body.links = links; } } +export class SubmissionDataDownloadHandler extends Handler { + @post('id', Types.String) + async post(domainId: string, id: string) { + this.response.body = { url: await storage.signDownloadLink(`submission/${id}`, 'code', true, 'judge') }; + } +} + class JudgeConnectionHandler extends ConnectionHandler { processing: any = null; closed = false; @@ -190,6 +197,7 @@ class JudgeConnectionHandler extends ConnectionHandler { export async function apply() { Route('judge_files_download', '/judge/files', JudgeFilesDownloadHandler, builtin.PRIV.PRIV_JUDGE); + Route('judge_submission_download', '/judge/code', SubmissionDataDownloadHandler, builtin.PRIV.PRIV_JUDGE); Connection('judge_conn', '/judge/conn', JudgeConnectionHandler, builtin.PRIV.PRIV_JUDGE); } diff --git a/packages/hydrooj/src/handler/problem.ts b/packages/hydrooj/src/handler/problem.ts index 90a85c7a2..5300c96f9 100644 --- a/packages/hydrooj/src/handler/problem.ts +++ b/packages/hydrooj/src/handler/problem.ts @@ -2,6 +2,7 @@ import AdmZip from 'adm-zip'; import { statSync } from 'fs-extra'; import { intersection, isSafeInteger } from 'lodash'; import { FilterQuery, ObjectID } from 'mongodb'; +import { nanoid } from 'nanoid'; import { sortFiles } from '@hydrooj/utils/lib/utils'; import { BadRequestError, ContestNotAttendedError, ContestNotEndedError, @@ -462,7 +463,7 @@ export class ProblemSubmitHandler extends ProblemDetailHandler { } @param('lang', Types.Name) - @param('code', Types.Content) + @param('code', Types.Content, true) @param('pretest', Types.Boolean) @param('input', Types.String, true) @param('tid', Types.ObjectID, true) @@ -470,11 +471,21 @@ export class ProblemSubmitHandler extends ProblemDetailHandler { if (this.response.body.pdoc.config?.langs && !this.response.body.pdoc.config.langs.includes(lang)) { throw new BadRequestError('Language not allowed.'); } - if (pretest && setting.langs[lang]?.pretest) lang = setting.langs[lang].pretest; - if (pretest && setting.langs[lang]?.pretest === false) throw new BadRequestError('Cannot run pretest for this language.'); - if (pretest && !['default', 'fileio', 'remote_judge'].includes(this.response.body.pdoc.config?.type)) { - throw new BadRequestError('unable to run pretest'); + if (pretest) { + if (setting.langs[lang]?.pretest) lang = setting.langs[lang].pretest; + if (setting.langs[lang]?.pretest === false) throw new BadRequestError('Cannot run pretest for this language.'); + if (!['default', 'fileio', 'remote_judge'].includes(this.response.body.pdoc.config?.type)) { + throw new BadRequestError('unable to run pretest'); + } } + if (!code) { + const file = this.request.files?.file; + if (!file) throw new ValidationError('code'); + if (file.size > 128 * 1024 * 1024) throw new ValidationError('file'); + const id = nanoid(); + await storage.put(`submission/${this.user._id}/${id}`, file.path); + code = `@@hydro_submission_file@@${this.user._id}/${id}#${file.name}`; + } else if (code.startsWith('@@hydro_submission_file@@')) throw new ValidationError('code'); await this.limitRate('add_record', 60, system.get('limit.submission')); const rid = await record.add(domainId, this.pdoc.docId, this.user._id, lang, code, true, pretest ? input : tid, tid && !pretest); const rdoc = await record.get(domainId, rid); diff --git a/packages/hydrooj/src/handler/record.ts b/packages/hydrooj/src/handler/record.ts index 89470e887..ee4a00d97 100644 --- a/packages/hydrooj/src/handler/record.ts +++ b/packages/hydrooj/src/handler/record.ts @@ -8,6 +8,8 @@ import { PERM, PRIV, STATUS } from '../model/builtin'; import * as contest from '../model/contest'; import problem from '../model/problem'; import record from '../model/record'; +import { langs } from '../model/setting'; +import storage from '../model/storage'; import * as system from '../model/system'; import TaskModel from '../model/task'; import user from '../model/user'; @@ -101,7 +103,8 @@ class RecordListHandler extends Handler { class RecordDetailHandler extends Handler { @param('rid', Types.ObjectID) - async get(domainId: string, rid: ObjectID) { + @param('download', Types.Boolean) + async get(domainId: string, rid: ObjectID, download = false) { this.response.template = 'record_detail.html'; const rdoc = await record.get(domainId, rid); if (!rdoc) throw new RecordNotFoundError(rid); @@ -132,6 +135,17 @@ class RecordDetailHandler extends Handler { rdoc.compilerTexts = []; } + if (download && rdoc.code.startsWith('@@hydro_submission_file@@')) { + const [id, filename] = rdoc.code.split('@@hydro_submission_file@@')[1].split('#'); + this.response.redirect = await storage.signDownloadLink(`submission/${id}`, filename || 'code', true, 'judge'); + return; + } + if (download) { + const lang = langs[rdoc.lang]?.pretest || rdoc.lang; + this.response.body = rdoc.code; + this.response.disposition = `attachment; filename="${langs[lang].code_file}` || `foo.${rdoc.lang}"`; + return; + } if (pdoc && !(rdoc.contest && this.user._id === rdoc.uid)) { if (!problem.canViewBy(pdoc, this.user)) throw new PermissionError(PERM.PERM_VIEW_PROBLEM_HIDDEN); } diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index d9f0c29c5..a7633c073 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/ui-default", - "version": "4.34.31", + "version": "4.34.32", "author": "undefined ", "license": "AGPL-3.0", "main": "hydro.js", diff --git a/packages/ui-default/templates/problem_submit.html b/packages/ui-default/templates/problem_submit.html index f44af1dc0..6c37ca92c 100644 --- a/packages/ui-default/templates/problem_submit.html +++ b/packages/ui-default/templates/problem_submit.html @@ -8,7 +8,7 @@

{{ _('Submit to Judge') }}

-
+
{{ form.form_select({ label:'Code language', @@ -36,6 +36,12 @@
{{ form.form_textarea({label:'Code', columns:12, name:'code', extra_class:'monospace', autofocus:true}) }} +
+
+

{{ _('Or upload a file:') }}

+
+
+
diff --git a/packages/ui-default/templates/record_detail.html b/packages/ui-default/templates/record_detail.html index 310706f9c..d494d8421 100644 --- a/packages/ui-default/templates/record_detail.html +++ b/packages/ui-default/templates/record_detail.html @@ -9,10 +9,15 @@

{{ _('Code') }}

+
-
-
{{ rdoc['code'] }}
-
+ {% if not rdoc.code.startsWith('@@hydro_submission_file@@') %} +
+
{{ rdoc['code'] }}
+
+ {% endif %}
{% endif %}