From bdf2d972cd19e2653abc140c1a15b1266e77a725 Mon Sep 17 00:00:00 2001 From: undefined Date: Fri, 21 May 2021 13:49:36 +0800 Subject: [PATCH] ui: download multiple problems --- packages/hydrooj/package.json | 2 +- packages/hydrooj/src/handler/problem.ts | 18 +---- packages/hydrooj/src/model/setting.ts | 2 + packages/ui-default/package.json | 2 +- .../ui-default/pages/problem_main.page.js | 76 ++++++++++++++++++- .../ui-default/templates/problem_main.html | 4 + 6 files changed, 87 insertions(+), 17 deletions(-) diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json index 2355afd32..6c38dc340 100644 --- a/packages/hydrooj/package.json +++ b/packages/hydrooj/package.json @@ -1,6 +1,6 @@ { "name": "hydrooj", - "version": "2.26.19", + "version": "2.26.20", "bin": "bin/hydrooj.js", "main": "dist/loader.js", "typings": "dist/loader.d.ts", diff --git a/packages/hydrooj/src/handler/problem.ts b/packages/hydrooj/src/handler/problem.ts index ee813cf89..2be141491 100644 --- a/packages/hydrooj/src/handler/problem.ts +++ b/packages/hydrooj/src/handler/problem.ts @@ -339,19 +339,6 @@ export class ProblemPretestConnectionHandler extends ConnectionHandler { } } -export class ProblemStatisticsHandler extends ProblemDetailHandler { - async get(domainId: string) { - const udoc = await user.getById(domainId, this.pdoc.owner); - const path = [ - ['problem_main', 'problem_main'], - [this.pdoc.title, 'problem_detail', { pid: this.pdoc.pid || this.pdoc.docId }, true], - ['problem_statistics', null], - ]; - this.response.template = 'problem_statistics.html'; - this.response.body = { pdoc: this.pdoc, udoc, path }; - } -} - export class ProblemManageHandler extends ProblemDetailHandler { async prepare() { if (!this.user.own(this.pdoc, PERM.PERM_EDIT_PROBLEM_SELF)) this.checkPerm(PERM.PERM_EDIT_PROBLEM); @@ -435,6 +422,10 @@ export class ProblemFilesHandler extends ProblemDetailHandler { throw new ForbiddenError('File limit exceeded.'); } if (!this.request.files.file) throw new ValidationError('file'); + const size = Math.sum((this.pdoc.data || []).map((i) => i.size), (this.pdoc.additional_file || []).map((i) => i.size)); + if (size >= system.get('limit.problem_files_max_size')) { + throw new ForbiddenError('File size limit exceeded.'); + } if (!filename) filename = this.request.files.file.name || String.random(16); if (filename.includes('/') || filename.includes('..')) throw new ValidationError('filename', 'Bad filename'); if (!this.user.own(this.pdoc, PERM.PERM_EDIT_PROBLEM_SELF)) this.checkPerm(PERM.PERM_EDIT_PROBLEM); @@ -648,7 +639,6 @@ export async function apply() { Route('problem_detail', '/p/:pid', ProblemDetailHandler, PERM.PERM_VIEW_PROBLEM); Route('problem_submit', '/p/:pid/submit', ProblemSubmitHandler, PERM.PERM_SUBMIT_PROBLEM); Route('problem_pretest', '/p/:pid/pretest', ProblemPretestHandler, PERM.PERM_SUBMIT_PROBLEM); - Route('problem_statistics', '/p/:pid/statistics', ProblemStatisticsHandler, PERM.PERM_VIEW_PROBLEM); Route('problem_edit', '/p/:pid/edit', ProblemEditHandler); Route('problem_files', '/p/:pid/files', ProblemFilesHandler, PERM.PERM_VIEW_PROBLEM); Route('problem_file_download', '/p/:pid/file/:filename', ProblemFileDownloadHandler, PERM.PERM_VIEW_PROBLEM); diff --git a/packages/hydrooj/src/model/setting.ts b/packages/hydrooj/src/model/setting.ts index 92e3ed316..f0fa5d186 100644 --- a/packages/hydrooj/src/model/setting.ts +++ b/packages/hydrooj/src/model/setting.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-len */ /* eslint-disable no-await-in-loop */ import moment from 'moment-timezone'; import { Dictionary } from 'lodash'; @@ -182,6 +183,7 @@ SystemSetting( Setting('setting_server', 'server.language', 'zh_CN', langRange, 'server.language', 'Default display language'), Setting('setting_server', 'server.login', true, 'boolean', 'server.login', 'Allow builtin-login', FLAG_PRO), Setting('setting_limits', 'limit.problem_files_max', 100, 'number', 'limit.problem_files_max', 'Max files per problem'), + Setting('setting_limits', 'limit.problem_files_size_max', 128 * 1024 * 1024, 'number', 'limit.problem_files_size_max', 'Max files size per problem'), Setting('setting_basic', 'default.priv', builtin.PRIV.PRIV_DEFAULT, 'number', 'default.priv', 'Default Privilege', FLAG_PRO), Setting('setting_basic', 'problem.categories', builtin.CATEGORIES, 'yaml', 'problem.categories', 'Problem Categories'), Setting('setting_basic', 'pagination.problem', 100, 'number', 'pagination.problem', 'Problems per page'), diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index 745528c5f..b0eb1be55 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/ui-default", - "version": "4.8.35", + "version": "4.8.36", "author": "undefined ", "license": "AGPL-3.0", "main": "hydro.js", diff --git a/packages/ui-default/pages/problem_main.page.js b/packages/ui-default/pages/problem_main.page.js index fcb6a7304..19c6bc7ad 100644 --- a/packages/ui-default/pages/problem_main.page.js +++ b/packages/ui-default/pages/problem_main.page.js @@ -1,19 +1,38 @@ import _ from 'lodash'; - +import { dump } from 'js-yaml'; +import * as streamsaver from 'streamsaver'; import { NamedPage } from 'vj/misc/Page'; import Notification from 'vj/components/notification'; import { ConfirmDialog } from 'vj/components/dialog'; import Dropdown from 'vj/components/dropdown/Dropdown'; +import { createZipStream } from 'vj/utils/zip'; import pjax from 'vj/utils/pjax'; import substitute from 'vj/utils/substitute'; import request from 'vj/utils/request'; import tpl from 'vj/utils/tpl'; +import pipeStream from 'vj/utils/pipeStream'; import delay from 'vj/utils/delay'; import i18n from 'vj/utils/i18n'; const categories = {}; const dirtyCategories = []; const selections = []; +// Firefox have no WritableStream +if (!window.WritableStream) streamsaver.WritableStream = WritableStream; +if (window.location.protocol === 'https:' + || window.location.protocol === 'chrome-extension:' + || window.location.hostname === 'localhost') { + streamsaver.mitm = '/streamsaver/mitm.html'; +} + +let isBeforeUnloadTriggeredByLibrary = !window.isSecureContext; +function onBeforeUnload(e) { + if (isBeforeUnloadTriggeredByLibrary) { + isBeforeUnloadTriggeredByLibrary = false; + return; + } + e.returnValue = ''; +} function setDomSelected($dom, selected) { if (selected) { @@ -202,6 +221,60 @@ async function handleOperation(operation) { } } +async function handleDownload() { + const pids = ensureAndGetSelectedPids(); + const fileStream = streamsaver.createWriteStream('Export.zip'); + const targets = []; + for (const pid of pids) { + const { pdoc } = await request.get(`p/${pid}`); + targets.push({ filename: `${pid}/problem.yaml`, content: dump(pdoc) }); + let { links } = await request.post(`p/${pid}/files`, { operation: 'get_links', files: pdoc.data.map((i) => i.name), type: 'testdata' }); + for (const filename of Object.keys(links)) { + targets.push({ filename: `${pid}/testdata/${filename}`, url: links[filename] }); + } + ({ links } = await request.post(`p/${pid}/files`, { + operation: 'get_links', files: pdoc.additional_file.map((i) => i.name), type: 'additional_file', + })); + for (const filename of Object.keys(links)) { + targets.push({ filename: `${pid}/additional_file/${filename}`, url: links[filename] }); + } + } + let i = 0; + const zipStream = createZipStream({ + // eslint-disable-next-line consistent-return + async pull(ctrl) { + if (i === targets.length) return ctrl.close(); + try { + if (targets[i].url) { + const response = await fetch(targets[i].url); + if (!response.ok) throw response.statusText; + ctrl.enqueue({ + name: targets[i].filename, + stream: () => response.body, + }); + } else { + ctrl.enqueue({ + name: targets[i].filename, + stream: () => new Blob([targets[i].content], { type: 'application/json' }).stream(), + }); + } + } catch (e) { + // eslint-disable-next-line no-use-before-define + stopDownload(); + Notification.error(i18n('problem_files.download_as_archive_error', [targets[i].filename, e.toString()])); + } + i++; + }, + }); + const abortCallbackReceiver = {}; + function stopDownload() { abortCallbackReceiver.abort(); } + window.addEventListener('unload', stopDownload); + window.addEventListener('beforeunload', onBeforeUnload); + await pipeStream(zipStream, fileStream, abortCallbackReceiver); + window.removeEventListener('unload', stopDownload); + window.removeEventListener('beforeunload', onBeforeUnload); +} + const page = new NamedPage(['problem_main', 'problem_category'], () => { const doc = document.documentElement; doc.className += ' display-mode'; @@ -216,6 +289,7 @@ const page = new NamedPage(['problem_main', 'problem_category'], () => { $('[name="remove_selected_problems"]').on('click', () => handleOperation('delete')); $('[name="hide_selected_problems"]').on('click', () => handleOperation('hide')); $('[name="unhide_selected_problems"]').on('click', () => handleOperation('unhide')); + $('[name="download_selected_problems"]').on('click', handleDownload); $('#search').on('click', (ev) => { ev.preventDefault(); updateSelection(); diff --git a/packages/ui-default/templates/problem_main.html b/packages/ui-default/templates/problem_main.html index e5b865332..d434e83d1 100644 --- a/packages/ui-default/templates/problem_main.html +++ b/packages/ui-default/templates/problem_main.html @@ -36,6 +36,10 @@ {{ _('Unhide Selected') }} + + + {{ _('Download Selected') }} + {{ _('Remove Selected') }}