diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index 8a29dca07..73b8c1fd4 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -552,7 +552,7 @@ export class ContestFilesHandler extends ContestDetailBaseHandler { if (filename.includes('/') || filename.includes('..')) throw new ValidationError('filename', null, 'Bad filename'); await storage.put(`contest/${domainId}/${tid}/${filename}`, file.filepath, this.user._id); const meta = await storage.getMeta(`contest/${domainId}/${tid}/${filename}`); - const payload = { name: filename, ...pick(meta, ['size', 'lastModified', 'etag']) }; + const payload = { _id: filename, name: filename, ...pick(meta, ['size', 'lastModified', 'etag']) }; if (!meta) throw new FileUploadError(); await contest.edit(domainId, tid, { files: [...(this.tdoc.files || []), payload] }); this.back(); diff --git a/packages/hydrooj/src/handler/training.ts b/packages/hydrooj/src/handler/training.ts index 0fdb60ce9..7339348b7 100644 --- a/packages/hydrooj/src/handler/training.ts +++ b/packages/hydrooj/src/handler/training.ts @@ -1,15 +1,24 @@ import assert from 'assert'; +import { statSync } from 'fs-extra'; +import { pick } from 'lodash'; import { FilterQuery, ObjectID } from 'mongodb'; -import { ProblemNotFoundError, ValidationError } from '../error'; +import { sortFiles } from '@hydrooj/utils/lib/utils'; +import { + FileLimitExceededError, FileUploadError, ProblemNotFoundError, ValidationError, +} from '../error'; import { Tdoc, TrainingDoc } from '../interface'; import paginate from '../lib/paginate'; import { PERM, PRIV, STATUS } from '../model/builtin'; +import * as oplog from '../model/oplog'; import problem from '../model/problem'; +import storage from '../model/storage'; import * as system from '../model/system'; import * as training from '../model/training'; import user from '../model/user'; import * as bus from '../service/bus'; -import { Handler, param, Types } from '../service/server'; +import { + Handler, param, post, Types, +} from '../service/server'; async function _parseDagJson(domainId: string, _dag: string): Promise { const parsed = []; @@ -149,6 +158,9 @@ class TrainingDetailHandler extends Handler { this.response.body = { tdoc, tsdoc, pids, pdict, psdict, ndict, nsdict, udoc, udict, selfPsdict, }; + this.response.body.tdoc.description = this.response.body.tdoc.description + .replace(/\(file:\/\//g, `(./${tdoc.docId}/file/`) + .replace(/="file:\/\//g, `="./${tdoc.docId}/file/`); this.response.pjax = 'partials/training_detail.html'; this.response.template = 'training_detail.html'; } @@ -219,9 +231,86 @@ class TrainingEditHandler extends Handler { } } +export class TrainingFilesHandler extends Handler { + tdoc: TrainingDoc; + + @param('tid', Types.ObjectID) + async prepare(domainId: string, tid: ObjectID) { + this.tdoc = await training.get(domainId, tid); + if (!this.user.own(this.tdoc)) this.checkPerm(PERM.PERM_EDIT_TRAINING); + else this.checkPerm(PERM.PERM_EDIT_TRAINING_SELF); + } + + @param('tid', Types.ObjectID) + async get(domainId: string, tid: ObjectID) { + if (!this.user.own(this.tdoc)) this.checkPerm(PERM.PERM_EDIT_TRAINING); + this.response.body = { + tdoc: this.tdoc, + tsdoc: await training.getStatus(domainId, this.tdoc.docId, this.user._id), + udoc: await user.getById(domainId, this.tdoc.owner), + files: sortFiles(this.tdoc.files || []), + urlForFile: (filename: string) => this.url('training_file_download', { tid, filename }), + }; + this.response.pjax = 'partials/files.html'; + this.response.template = 'training_files.html'; + } + + @param('tid', Types.ObjectID) + @post('filename', Types.Name, true) + async postUploadFile(domainId: string, tid: ObjectID, filename: string) { + if ((this.tdoc.files?.length || 0) >= system.get('limit.training_files')) { + throw new FileLimitExceededError('count'); + } + const file = this.request.files?.file; + if (!file) throw new ValidationError('file'); + const f = statSync(file.filepath); + const size = Math.sum((this.tdoc.files || []).map((i) => i.size)) + f.size; + if (size >= system.get('limit.training_files_size')) { + throw new FileLimitExceededError('size'); + } + if (!filename) filename = file.originalFilename || String.random(16); + if (filename.includes('/') || filename.includes('..')) throw new ValidationError('filename', null, 'Bad filename'); + await storage.put(`training/${domainId}/${tid}/${filename}`, file.filepath, this.user._id); + const meta = await storage.getMeta(`training/${domainId}/${tid}/${filename}`); + const payload = { _id: filename, name: filename, ...pick(meta, ['size', 'lastModified', 'etag']) }; + if (!meta) throw new FileUploadError(); + await training.edit(domainId, tid, { files: [...(this.tdoc.files || []), payload] }); + this.back(); + } + + @param('tid', Types.ObjectID) + @post('files', Types.Array) + async postDeleteFiles(domainId: string, tid: ObjectID, files: string[]) { + await Promise.all([ + storage.del(files.map((t) => `contest/${domainId}/${tid}/${t}`), this.user._id), + training.edit(domainId, tid, { files: this.tdoc.files.filter((i) => !files.includes(i.name)) }), + ]); + this.back(); + } +} +export class TrainingFileDownloadHandler extends Handler { + @param('tid', Types.ObjectID) + @param('filename', Types.Name) + @param('noDisposition', Types.Boolean) + async get(domainId: string, tid: ObjectID, filename: string, noDisposition = false) { + this.response.addHeader('Cache-Control', 'public'); + const target = `training/${domainId}/${tid}/${filename}`; + const file = await storage.getMeta(target); + await oplog.log(this, 'download.file.training', { + target, + size: file?.size || 0, + }); + this.response.redirect = await storage.signDownloadLink( + target, noDisposition ? undefined : filename, false, 'user', + ); + } +} + export async function apply(ctx) { ctx.Route('training_main', '/training', TrainingMainHandler, PERM.PERM_VIEW_TRAINING); ctx.Route('training_create', '/training/create', TrainingEditHandler); ctx.Route('training_detail', '/training/:tid', TrainingDetailHandler, PERM.PERM_VIEW_TRAINING); ctx.Route('training_edit', '/training/:tid/edit', TrainingEditHandler); + ctx.Route('training_files', '/training/:tid/file', TrainingFilesHandler, PERM.PERM_VIEW_TRAINING); + ctx.Route('training_file_download', '/training/:tid/file/:filename', TrainingFileDownloadHandler, PERM.PERM_VIEW_TRAINING); } diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index ce5982d87..b4ab16245 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -367,6 +367,8 @@ export interface Tdoc { export interface ContestStat extends Record { detail: Record>, + unrank?: boolean, } export interface ContestRule { diff --git a/packages/hydrooj/src/model/contest.ts b/packages/hydrooj/src/model/contest.ts index e57bc7075..32442f95c 100644 --- a/packages/hydrooj/src/model/contest.ts +++ b/packages/hydrooj/src/model/contest.ts @@ -540,7 +540,7 @@ export async function add( return res; } -export async function edit(domainId: string, tid: ObjectID, $set: any) { +export async function edit(domainId: string, tid: ObjectID, $set: Partial) { if ($set.rule && !RULES[$set.rule]) throw new ValidationError('rule'); const tdoc = await document.get(domainId, document.TYPE_CONTEST, tid); if (!tdoc) throw new ContestNotFoundError(domainId, tid); diff --git a/packages/hydrooj/src/model/setting.ts b/packages/hydrooj/src/model/setting.ts index 0a3a72341..054f47ecd 100644 --- a/packages/hydrooj/src/model/setting.ts +++ b/packages/hydrooj/src/model/setting.ts @@ -215,6 +215,8 @@ SystemSetting( Setting('setting_limits', 'limit.user_files_size', 128 * 1024 * 1024, 'number', 'limit.user_files_size', 'Max total file size for user'), Setting('setting_limits', 'limit.contest_files', 100, 'number', 'limit.contest_files', 'Max files for contest'), Setting('setting_limits', 'limit.contest_files_size', 128 * 1024 * 1024, 'number', 'limit.contest_files_size', 'Max total file size for contest'), + Setting('setting_limits', 'limit.training_files', 100, 'number', 'limit.training_files', 'Max files for training'), + Setting('setting_limits', 'limit.training_files_size', 128 * 1024 * 1024, 'number', 'limit.training_files_size', 'Max total file size for training'), Setting('setting_limits', 'limit.submission', 60, 'number', 'limit.submission', 'Max submission count per minute'), Setting('setting_limits', 'limit.submission_user', 15, 'number', 'limit.submission_user', 'Max submission count per user per minute'), Setting('setting_limits', 'limit.pretest', 60, 'number', 'limit.pretest', 'Max pretest count per minute'), diff --git a/packages/ui-default/locales/zh.yaml b/packages/ui-default/locales/zh.yaml index 74d4e2d45..0c746a4ac 100644 --- a/packages/ui-default/locales/zh.yaml +++ b/packages/ui-default/locales/zh.yaml @@ -927,6 +927,7 @@ View problem solutions: 查看题解 View Problem: 查看题目 View problems: 查看题目 View this domain: 查看此域 +View Training: 查看训练计划 View training plans: 查看训练计划 View: 查看 Visible to registered users: 对注册用户可见 diff --git a/packages/ui-default/pages/files.page.js b/packages/ui-default/pages/files.page.js index dd16ab053..5eeed6270 100644 --- a/packages/ui-default/pages/files.page.js +++ b/packages/ui-default/pages/files.page.js @@ -92,7 +92,7 @@ function handleDrop(ev) { handleClickUpload(files); } -const page = new NamedPage(['home_files', 'contest_files'], () => { +const page = new NamedPage(['home_files', 'contest_files', 'training_files'], () => { $(document).on('click', '[name="upload_file"]', () => handleClickUpload()); $(document).on('click', '[name="remove_selected"]', () => handleClickRemoveSelected()); $(document).on('dragover', '.files', (ev) => handleDragOver(ev)); diff --git a/packages/ui-default/pages/files.page.styl b/packages/ui-default/pages/files.page.styl index 435a0938d..9108c57c9 100644 --- a/packages/ui-default/pages/files.page.styl +++ b/packages/ui-default/pages/files.page.styl @@ -1,4 +1,4 @@ -.page--home_files,.page--contest_files +.page--home_files,.page--contest_files,.page--training_files .col--checkbox width: 60px diff --git a/packages/ui-default/templates/contest_files.html b/packages/ui-default/templates/contest_files.html index 2e2e515bf..4a0d233e9 100644 --- a/packages/ui-default/templates/contest_files.html +++ b/packages/ui-default/templates/contest_files.html @@ -1,7 +1,4 @@ {% extends "layout/basic.html" %} -{% import "components/contest.html" as contest with context %} -{% import "components/record.html" as record with context %} -{% import "components/problem.html" as problem with context %} {% block content %} {{ set(UiContext, 'tdoc', tdoc) }}
diff --git a/packages/ui-default/templates/training_detail.html b/packages/ui-default/templates/training_detail.html index 93738b524..c56c56ee5 100644 --- a/packages/ui-default/templates/training_detail.html +++ b/packages/ui-default/templates/training_detail.html @@ -75,6 +75,9 @@ + {% endif %}