mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
core: 支持题目导出
This commit is contained in:
1 parent
a5004a7c1b
commit
ee0f5deedc
11 files changed
+87
-33
No files matched your search
+4
-4
@@ -20,17 +20,17 @@
|
|||||||
"@shelf/jest-mongodb": "^1.2.3",
|
"@shelf/jest-mongodb": "^1.2.3",
|
||||||
"@types/cross-spawn": "^6.0.2",
|
"@types/cross-spawn": "^6.0.2",
|
||||||
"@types/jest": "^26.0.14",
|
"@types/jest": "^26.0.14",
|
||||||
"@types/node": "^14.14.5",
|
"@types/node": "^14.14.6",
|
||||||
"@types/semver": "^7.3.4",
|
"@types/semver": "^7.3.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^3.9.1",
|
"@typescript-eslint/eslint-plugin": "^3.9.1",
|
||||||
"@typescript-eslint/parser": "^3.9.1",
|
"@typescript-eslint/parser": "^3.9.1",
|
||||||
"cross-spawn": "^7.0.3",
|
"cross-spawn": "^7.0.3",
|
||||||
"eslint": "^7.12.1",
|
"eslint": "^7.13.0",
|
||||||
"eslint-config-airbnb-base": "^14.2.0",
|
"eslint-config-airbnb-base": "^14.2.1",
|
||||||
"eslint-import-resolver-typescript": "^2.3.0",
|
"eslint-import-resolver-typescript": "^2.3.0",
|
||||||
"eslint-plugin-import": "^2.22.1",
|
"eslint-plugin-import": "^2.22.1",
|
||||||
"globby": "^11.0.1",
|
"globby": "^11.0.1",
|
||||||
"jest": "^26.6.1",
|
"jest": "^26.6.3",
|
||||||
"latest-version": "^5.1.0",
|
"latest-version": "^5.1.0",
|
||||||
"ora": "^5.1.0",
|
"ora": "^5.1.0",
|
||||||
"semver": "^7.3.2",
|
"semver": "^7.3.2",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
"p-queue": "^6.6.1",
|
"p-queue": "^6.6.1",
|
||||||
"shell-quote": "^1.7.2",
|
"shell-quote": "^1.7.2",
|
||||||
"supports-color": "^7.2.0",
|
"supports-color": "^7.2.0",
|
||||||
"systeminformation": "^4.27.11",
|
"systeminformation": "^4.28.1",
|
||||||
"yargs": "^16.1.0"
|
"yargs": "^16.1.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bson": "^4.0.2",
|
"@types/bson": "^4.0.2",
|
||||||
"@types/fs-extra": "^9.0.1",
|
"@types/fs-extra": "^9.0.3",
|
||||||
"@types/js-yaml": "^3.12.5",
|
"@types/js-yaml": "^3.12.5",
|
||||||
"@types/shell-quote": "^1.7.0"
|
"@types/shell-quote": "^1.7.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hydrooj",
|
"name": "hydrooj",
|
||||||
"version": "2.13.40",
|
"version": "2.13.41",
|
||||||
"bin": "bin/hydrooj.js",
|
"bin": "bin/hydrooj.js",
|
||||||
"main": "dist/loader.js",
|
"main": "dist/loader.js",
|
||||||
"typings": "dist/loader.d.ts",
|
"typings": "dist/loader.d.ts",
|
||||||
@@ -19,31 +19,31 @@
|
|||||||
"js-yaml": "^3.13.1",
|
"js-yaml": "^3.13.1",
|
||||||
"koa": "^2.13.0",
|
"koa": "^2.13.0",
|
||||||
"koa-body": "^4.2.0",
|
"koa-body": "^4.2.0",
|
||||||
"koa-router": "^9.4.0",
|
"koa-router": "^10.0.0",
|
||||||
"koa-static-cache": "^5.1.3",
|
"koa-static-cache": "^5.1.3",
|
||||||
"lodash": "^4.17.20",
|
"lodash": "^4.17.20",
|
||||||
"lru-cache": "^6.0.0",
|
"lru-cache": "^6.0.0",
|
||||||
"moment-timezone": "^0.5.31",
|
"moment-timezone": "^0.5.31",
|
||||||
"mongodb": "^3.6.2",
|
"mongodb": "^3.6.3",
|
||||||
"nodemailer": "^6.4.13",
|
"nodemailer": "^6.4.15",
|
||||||
"ps-list": "^7.2.0",
|
"ps-list": "^7.2.0",
|
||||||
"serialize-javascript": "^5.0.1",
|
"serialize-javascript": "^5.0.1",
|
||||||
"sockjs": "^0.3.20",
|
"sockjs": "^0.3.20",
|
||||||
"superagent": "^6.1.0",
|
"superagent": "^6.1.0",
|
||||||
"supports-color": "^7.2.0",
|
"supports-color": "^7.2.0",
|
||||||
"systeminformation": "^4.27.11",
|
"systeminformation": "^4.28.1",
|
||||||
"yargs": "^16.0.3"
|
"yargs": "^16.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/adm-zip": "^0.4.33",
|
"@types/adm-zip": "^0.4.33",
|
||||||
"@types/fs-extra": "^9.0.1",
|
"@types/fs-extra": "^9.0.3",
|
||||||
"@types/js-yaml": "^3.12.5",
|
"@types/js-yaml": "^3.12.5",
|
||||||
"@types/koa": "^2.11.4",
|
"@types/koa": "^2.11.4",
|
||||||
"@types/koa-router": "^7.4.1",
|
"@types/koa-router": "^7.4.1",
|
||||||
"@types/koa-static-cache": "^5.1.0",
|
"@types/koa-static-cache": "^5.1.0",
|
||||||
"@types/lodash": "^4.14.161",
|
"@types/lodash": "^4.14.165",
|
||||||
"@types/lru-cache": "^5.1.0",
|
"@types/lru-cache": "^5.1.0",
|
||||||
"@types/mongodb": "^3.5.27",
|
"@types/mongodb": "^3.5.33",
|
||||||
"@types/nodemailer": "^6.4.0",
|
"@types/nodemailer": "^6.4.0",
|
||||||
"@types/serialize-javascript": "^4.0.0",
|
"@types/serialize-javascript": "^4.0.0",
|
||||||
"@types/sockjs": "^0.3.32",
|
"@types/sockjs": "^0.3.32",
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { isSafeInteger, flatten } from 'lodash';
|
import { isSafeInteger, flatten, pick } from 'lodash';
|
||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
import { FilterQuery, ObjectID } from 'mongodb';
|
import { FilterQuery, ObjectID } from 'mongodb';
|
||||||
|
import AdmZip from 'adm-zip';
|
||||||
import {
|
import {
|
||||||
NoProblemError, ProblemDataNotFoundError, BadRequestError,
|
NoProblemError, ProblemDataNotFoundError, BadRequestError,
|
||||||
SolutionNotFoundError, ProblemNotFoundError, ValidationError,
|
SolutionNotFoundError, ProblemNotFoundError, ValidationError,
|
||||||
PermissionError,
|
PermissionError,
|
||||||
} from '../error';
|
} from '../error';
|
||||||
|
import { streamToBuffer } from '../utils';
|
||||||
import {
|
import {
|
||||||
Pdoc, User, Rdoc, PathComponent,
|
Pdoc, User, Rdoc, PathComponent,
|
||||||
} from '../interface';
|
} from '../interface';
|
||||||
@@ -13,6 +15,7 @@ import paginate from '../lib/paginate';
|
|||||||
import {
|
import {
|
||||||
checkPid, isTitle, isContent, isPid,
|
checkPid, isTitle, isContent, isPid,
|
||||||
} from '../lib/validator';
|
} from '../lib/validator';
|
||||||
|
import { ProblemAdd } from '../lib/ui';
|
||||||
import * as file from '../model/file';
|
import * as file from '../model/file';
|
||||||
import * as problem from '../model/problem';
|
import * as problem from '../model/problem';
|
||||||
import * as record from '../model/record';
|
import * as record from '../model/record';
|
||||||
@@ -233,6 +236,23 @@ export class ProblemDetailHandler extends ProblemHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ProblemExportHandler extends ProblemDetailHandler {
|
||||||
|
async get() {
|
||||||
|
const hasPerm = (this.user._id === this.pdoc.owner && this.user.hasPerm(PERM.PERM_READ_PROBLEM_DATA_SELF))
|
||||||
|
|| this.user.hasPerm(PERM.PERM_READ_PROBLEM_DATA_SELF);
|
||||||
|
const pdoc = pick(this.pdoc, ['pid', 'acMsg', 'content', 'config', 'title', 'html', 'tag', 'category']);
|
||||||
|
let zip: AdmZip;
|
||||||
|
if (hasPerm) {
|
||||||
|
if (this.pdoc.data instanceof ObjectID) {
|
||||||
|
const buf = await streamToBuffer(await file.get(this.pdoc.data));
|
||||||
|
zip = new AdmZip(buf);
|
||||||
|
} else zip = new AdmZip();
|
||||||
|
} else zip = new AdmZip();
|
||||||
|
zip.addFile('problem.json', Buffer.from(JSON.stringify(pdoc)));
|
||||||
|
this.response.attachment(`${this.pdoc.title}.zip`, zip.toBuffer());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class ProblemSubmitHandler extends ProblemDetailHandler {
|
export class ProblemSubmitHandler extends ProblemDetailHandler {
|
||||||
@param('pid', Types.String, null, parsePid)
|
@param('pid', Types.String, null, parsePid)
|
||||||
async get(domainId: string, pid: string | number) {
|
async get(domainId: string, pid: string | number) {
|
||||||
@@ -615,7 +635,6 @@ export class ProblemSolutionReplyRawHandler extends ProblemDetailHandler {
|
|||||||
export class ProblemCreateHandler extends Handler {
|
export class ProblemCreateHandler extends Handler {
|
||||||
async get() {
|
async get() {
|
||||||
this.response.template = 'problem_edit.html';
|
this.response.template = 'problem_edit.html';
|
||||||
this.checkPerm(PERM.PERM_CREATE_PROBLEM);
|
|
||||||
this.response.body = {
|
this.response.body = {
|
||||||
path: [
|
path: [
|
||||||
['Hydro', 'homepage'],
|
['Hydro', 'homepage'],
|
||||||
@@ -640,11 +659,32 @@ export class ProblemCreateHandler extends Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ProblemImportHandler extends Handler {
|
||||||
|
@param('ufid', Types.ObjectID, true)
|
||||||
|
async get(domainId: string, ufid?: ObjectID) {
|
||||||
|
if (ufid) {
|
||||||
|
const stat = await file.getMeta(ufid);
|
||||||
|
if (stat.size > 128 * 1024 * 1024) throw new BadRequestError('File too large');
|
||||||
|
const stream = await file.get(ufid);
|
||||||
|
const buf = await streamToBuffer(stream);
|
||||||
|
const zip = new AdmZip(buf);
|
||||||
|
const pdoc = JSON.parse(zip.getEntry('problem.json').getData().toString());
|
||||||
|
const pid = await problem.add(domainId, pdoc.pid, pdoc.title, pdoc.content, this.user._id, pdoc.tags, pdoc.category);
|
||||||
|
await problem.setTestdata(domainId, pid, buf);
|
||||||
|
await problem.edit(domainId, pid, { html: pdoc.html });
|
||||||
|
this.response.redirect = this.url('problem_detail', { pid });
|
||||||
|
await file.del(ufid);
|
||||||
|
} else this.response.template = 'problem_import.html';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function apply() {
|
export async function apply() {
|
||||||
|
ProblemAdd('problem_import', {}, 'copy', 'Import From Hydro');
|
||||||
Route('problem_main', '/p', ProblemMainHandler);
|
Route('problem_main', '/p', ProblemMainHandler);
|
||||||
Route('problem_category', '/p/category/:category', ProblemCategoryHandler);
|
Route('problem_category', '/p/category/:category', ProblemCategoryHandler);
|
||||||
Route('problem_random', '/problem/random', ProblemRandomHandler);
|
Route('problem_random', '/problem/random', ProblemRandomHandler);
|
||||||
Route('problem_detail', '/p/:pid', ProblemDetailHandler);
|
Route('problem_detail', '/p/:pid', ProblemDetailHandler);
|
||||||
|
Route('problem_export', '/p/:pid/export', ProblemExportHandler);
|
||||||
Route('problem_submit', '/p/:pid/submit', ProblemSubmitHandler, PERM.PERM_SUBMIT_PROBLEM);
|
Route('problem_submit', '/p/:pid/submit', ProblemSubmitHandler, PERM.PERM_SUBMIT_PROBLEM);
|
||||||
Route('problem_pretest', '/p/:pid/pretest', ProblemPretestHandler);
|
Route('problem_pretest', '/p/:pid/pretest', ProblemPretestHandler);
|
||||||
Route('problem_settings', '/p/:pid/settings', ProblemSettingsHandler);
|
Route('problem_settings', '/p/:pid/settings', ProblemSettingsHandler);
|
||||||
@@ -655,7 +695,8 @@ export async function apply() {
|
|||||||
Route('problem_solution', '/p/:pid/solution', ProblemSolutionHandler);
|
Route('problem_solution', '/p/:pid/solution', ProblemSolutionHandler);
|
||||||
Route('problem_solution_raw', '/p/:pid/solution/:psid/raw', ProblemSolutionRawHandler);
|
Route('problem_solution_raw', '/p/:pid/solution/:psid/raw', ProblemSolutionRawHandler);
|
||||||
Route('problem_solution_reply_raw', '/p/:pid/solution/:psid/:psrid/raw', ProblemSolutionReplyRawHandler);
|
Route('problem_solution_reply_raw', '/p/:pid/solution/:psid/:psrid/raw', ProblemSolutionReplyRawHandler);
|
||||||
Route('problem_create', '/problem/create', ProblemCreateHandler);
|
Route('problem_create', '/problem/create', ProblemCreateHandler, PERM.PERM_CREATE_PROBLEM);
|
||||||
|
Route('problem_import', '/problem/import', ProblemImportHandler, PERM.PERM_CREATE_PROBLEM);
|
||||||
Connection('problem_pretest_conn', '/conn/pretest', ProblemPretestConnectionHandler);
|
Connection('problem_pretest_conn', '/conn/pretest', ProblemPretestConnectionHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import Zip from 'adm-zip';
|
|||||||
import { safeLoad } from 'js-yaml';
|
import { safeLoad } from 'js-yaml';
|
||||||
import { LocalProblemConfig } from '../interface';
|
import { LocalProblemConfig } from '../interface';
|
||||||
|
|
||||||
export async function readConfig(filePath: string) {
|
export async function readConfig(file: string | Buffer) {
|
||||||
const data = new Zip(filePath);
|
const data = new Zip(file);
|
||||||
const entries = data.getEntries();
|
const entries = data.getEntries();
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (entry.name.toLowerCase() === 'config.yaml') {
|
if (entry.name.toLowerCase() === 'config.yaml') {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import * as db from '../service/db';
|
|||||||
import * as bus from '../service/bus';
|
import * as bus from '../service/bus';
|
||||||
import gridfs from '../service/gridfs';
|
import gridfs from '../service/gridfs';
|
||||||
import hash from '../lib/hash.hydro';
|
import hash from '../lib/hash.hydro';
|
||||||
|
import { bufferToStream } from '../utils';
|
||||||
|
|
||||||
const coll: Collection<Ufdoc> = db.collection('file');
|
const coll: Collection<Ufdoc> = db.collection('file');
|
||||||
const collFile = db.collection('fs.files');
|
const collFile = db.collection('fs.files');
|
||||||
@@ -17,12 +18,13 @@ function _timestamp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function add(
|
export async function add(
|
||||||
streamOrPath: fs.ReadStream | string, filename: string,
|
f: NodeJS.ReadableStream | Buffer | string, filename: string,
|
||||||
owner = 1, meta = {},
|
owner = 1, meta = {},
|
||||||
) {
|
) {
|
||||||
let stream: fs.ReadStream;
|
let stream: NodeJS.ReadableStream;
|
||||||
if (typeof streamOrPath === 'string') stream = fs.createReadStream(streamOrPath);
|
if (typeof f === 'string') stream = fs.createReadStream(f);
|
||||||
else stream = streamOrPath;
|
else if (f instanceof Buffer) stream = bufferToStream(f);
|
||||||
|
else stream = f;
|
||||||
const w = gridfs.openUploadStream(filename);
|
const w = gridfs.openUploadStream(filename);
|
||||||
await coll.insertOne({
|
await coll.insertOne({
|
||||||
...meta,
|
...meta,
|
||||||
|
|||||||
@@ -187,10 +187,10 @@ export function setStar(domainId: string, pid: number, uid: number, star: boolea
|
|||||||
return document.setStatus(domainId, document.TYPE_PROBLEM, pid, uid, { star });
|
return document.setStatus(domainId, document.TYPE_PROBLEM, pid, uid, { star });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setTestdata(domainId: string, _id: number, filePath: string) {
|
export async function setTestdata(domainId: string, _id: number, f: string | Buffer) {
|
||||||
const pdoc = await get(domainId, _id);
|
const pdoc = await get(domainId, _id);
|
||||||
const config = await testdataConfig.readConfig(filePath);
|
const config = await testdataConfig.readConfig(f);
|
||||||
const id = await file.add(filePath, 'data.zip', 1);
|
const id = await file.add(f, 'data.zip', 1);
|
||||||
if (pdoc.data instanceof ObjectID) file.del(pdoc.data);
|
if (pdoc.data instanceof ObjectID) file.del(pdoc.data);
|
||||||
return await edit(domainId, _id, { data: id, config: safeLoad(config) as any });
|
return await edit(domainId, _id, { data: id, config: safeLoad(config) as any });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -357,11 +357,14 @@ export class Handler {
|
|||||||
status: null,
|
status: null,
|
||||||
template: null,
|
template: null,
|
||||||
redirect: null,
|
redirect: null,
|
||||||
attachment: (name, stream) => {
|
attachment: (name, streamOrBuffer) => {
|
||||||
ctx.attachment(name);
|
ctx.attachment(name);
|
||||||
if (stream) {
|
if (streamOrBuffer instanceof Buffer) {
|
||||||
this.response.body = null;
|
this.response.body = null;
|
||||||
ctx.body = stream.pipe(new PassThrough());
|
ctx.body = streamOrBuffer;
|
||||||
|
} else {
|
||||||
|
this.response.body = null;
|
||||||
|
ctx.body = streamOrBuffer.pipe(new PassThrough());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
disposition: null,
|
disposition: null,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import { Duplex } from 'stream';
|
||||||
import cluster from 'cluster';
|
import cluster from 'cluster';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import serialize from 'serialize-javascript';
|
import serialize from 'serialize-javascript';
|
||||||
@@ -180,6 +181,13 @@ export function streamToBuffer(stream): Promise<Buffer> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function bufferToStream(buffer: Buffer): NodeJS.ReadableStream {
|
||||||
|
const stream = new Duplex();
|
||||||
|
stream.push(buffer);
|
||||||
|
stream.push(null);
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
export function buildProjection(fields: string[]): Record<string, 1> {
|
export function buildProjection(fields: string[]): Record<string, 1> {
|
||||||
const o = {};
|
const o = {};
|
||||||
for (const k of fields) o[k] = 1;
|
for (const k of fields) o[k] = 1;
|
||||||
|
|||||||
@@ -16,6 +16,6 @@
|
|||||||
"ws": "^7.3.1"
|
"ws": "^7.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/ws": "^7.2.8"
|
"@types/ws": "^7.2.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,13 +11,13 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/js-yaml": "^3.12.5",
|
"@types/js-yaml": "^3.12.5",
|
||||||
"@types/mongodb": "^3.5.27"
|
"@types/mongodb": "^3.5.33"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"hydrooj": "^2.13.25"
|
"hydrooj": "^2.13.25"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"js-yaml": "^3.14.0",
|
"js-yaml": "^3.14.0",
|
||||||
"mongodb": "^3.6.2"
|
"mongodb": "^3.6.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user