mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
core: testdata(zip-> multi files)
This commit is contained in:
1 parent
c30fbe1e6b
commit
e7794090b2
17 files changed
+338
-335
No files matched your search
@@ -33,6 +33,7 @@
|
|||||||
"superagent": "^6.1.0",
|
"superagent": "^6.1.0",
|
||||||
"supports-color": "^8.0.0",
|
"supports-color": "^8.0.0",
|
||||||
"systeminformation": "^4.31.1",
|
"systeminformation": "^4.31.1",
|
||||||
|
"terminal-kit": "^1.44.0",
|
||||||
"wtfnode": "^0.8.4",
|
"wtfnode": "^0.8.4",
|
||||||
"yargs": "^16.2.0"
|
"yargs": "^16.2.0"
|
||||||
},
|
},
|
||||||
@@ -52,6 +53,7 @@
|
|||||||
"@types/sockjs": "^0.3.32",
|
"@types/sockjs": "^0.3.32",
|
||||||
"@types/superagent": "^4.1.8",
|
"@types/superagent": "^4.1.8",
|
||||||
"@types/supports-color": "^7.2.0",
|
"@types/supports-color": "^7.2.0",
|
||||||
|
"@types/terminal-kit": "^1.28.2",
|
||||||
"@types/wtfnode": "^0.7.0",
|
"@types/wtfnode": "^0.7.0",
|
||||||
"@types/yargs": "^15.0.11"
|
"@types/yargs": "^15.0.11"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
import options from '../options';
|
import options from '../options';
|
||||||
import * as bus from '../service/bus';
|
import * as bus from '../service/bus';
|
||||||
import db from '../service/db';
|
import db from '../service/db';
|
||||||
|
import storage from '../service/storage';
|
||||||
|
|
||||||
const COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
|
const COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
|
||||||
const ARR = /=>.*$/mg;
|
const ARR = /=>.*$/mg;
|
||||||
@@ -56,6 +57,15 @@ export async function load() {
|
|||||||
require('../options');
|
require('../options');
|
||||||
const opts = options();
|
const opts = options();
|
||||||
await db.start(opts);
|
await db.start(opts);
|
||||||
|
const modelSystem = require('../model/system');
|
||||||
|
const [endPoint, accessKey, secretKey, bucket, region, endPointForUser, endPointForJudge] = modelSystem.getMany([
|
||||||
|
'file.endPoint', 'file.accessKey', 'file.secretKey', 'file.bucket', 'file.region',
|
||||||
|
'file.endPointForUser', 'file.endPointForJudge',
|
||||||
|
]);
|
||||||
|
const sopts = {
|
||||||
|
endPoint, accessKey, secretKey, bucket, region, endPointForUser, endPointForJudge,
|
||||||
|
};
|
||||||
|
await storage.start(sopts);
|
||||||
for (const i of builtinLib) require(`../lib/${i}`);
|
for (const i of builtinLib) require(`../lib/${i}`);
|
||||||
await lib(pending, fail);
|
await lib(pending, fail);
|
||||||
require('../service/gridfs');
|
require('../service/gridfs');
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { Logger } from '../logger';
|
|||||||
import options from '../options';
|
import options from '../options';
|
||||||
import * as bus from '../service/bus';
|
import * as bus from '../service/bus';
|
||||||
import db from '../service/db';
|
import db from '../service/db';
|
||||||
|
import storage from '../service/storage';
|
||||||
|
|
||||||
const logger = new Logger('entry/master');
|
const logger = new Logger('entry/master');
|
||||||
const tmpdir = path.resolve(os.tmpdir(), 'hydro');
|
const tmpdir = path.resolve(os.tmpdir(), 'hydro');
|
||||||
@@ -52,16 +53,25 @@ export async function load(call: Entry) {
|
|||||||
});
|
});
|
||||||
const opts = options();
|
const opts = options();
|
||||||
await db.start(opts);
|
await db.start(opts);
|
||||||
await require('../model/system').runConfig();
|
const modelSystem = require('../model/system');
|
||||||
|
await modelSystem.runConfig();
|
||||||
|
const [endPoint, accessKey, secretKey, bucket, region, endPointForUser, endPointForJudge] = modelSystem.getMany([
|
||||||
|
'file.endPoint', 'file.accessKey', 'file.secretKey', 'file.bucket', 'file.region',
|
||||||
|
'file.endPointForUser', 'file.endPointForJudge',
|
||||||
|
]);
|
||||||
|
const sopts = {
|
||||||
|
endPoint, accessKey, secretKey, bucket, region, endPointForUser, endPointForJudge,
|
||||||
|
};
|
||||||
|
await storage.start(sopts);
|
||||||
require('../service/monitor');
|
require('../service/monitor');
|
||||||
for (const i of builtinModel) require(`../model/${i}`);
|
for (const i of builtinModel) require(`../model/${i}`);
|
||||||
const modelSystem = require('../model/system');
|
|
||||||
const scripts = require('../upgrade');
|
const scripts = require('../upgrade');
|
||||||
let dbVer = (await modelSystem.get('db.ver')) ?? 0;
|
let dbVer = (await modelSystem.get('db.ver')) ?? 0;
|
||||||
const expected = scripts.length;
|
const expected = scripts.length;
|
||||||
while (dbVer < expected) {
|
while (dbVer < expected) {
|
||||||
logger.info('Upgrading database: from %d to %d', dbVer, expected);
|
logger.info('Upgrading database: from %d to %d', dbVer, expected);
|
||||||
await scripts[dbVer]();
|
const result = await scripts[dbVer]();
|
||||||
|
if (!result) break;
|
||||||
dbVer++;
|
dbVer++;
|
||||||
await modelSystem.set('db.ver', dbVer);
|
await modelSystem.set('db.ver', dbVer);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
import options from '../options';
|
import options from '../options';
|
||||||
import * as bus from '../service/bus';
|
import * as bus from '../service/bus';
|
||||||
import db from '../service/db';
|
import db from '../service/db';
|
||||||
|
import storage from '../service/storage';
|
||||||
|
|
||||||
export async function load() {
|
export async function load() {
|
||||||
const pending = global.addons;
|
const pending = global.addons;
|
||||||
@@ -23,10 +24,15 @@ export async function load() {
|
|||||||
]);
|
]);
|
||||||
const opts = options();
|
const opts = options();
|
||||||
await db.start(opts);
|
await db.start(opts);
|
||||||
await new Promise((resolve) => {
|
const modelSystem = require('../model/system');
|
||||||
bus.once('database/config', resolve);
|
const [endPoint, accessKey, secretKey, bucket, region, endPointForUser, endPointForJudge] = modelSystem.getMany([
|
||||||
require('../model/system');
|
'file.endPoint', 'file.accessKey', 'file.secretKey', 'file.bucket', 'file.region',
|
||||||
});
|
'file.endPointForUser', 'file.endPointForJudge',
|
||||||
|
]);
|
||||||
|
const sopts = {
|
||||||
|
endPoint, accessKey, secretKey, bucket, region, endPointForUser, endPointForJudge,
|
||||||
|
};
|
||||||
|
await storage.start(sopts);
|
||||||
for (const i of builtinLib) require(`../lib/${i}`);
|
for (const i of builtinLib) require(`../lib/${i}`);
|
||||||
await lib(pending, fail);
|
await lib(pending, fail);
|
||||||
require('../service/gridfs');
|
require('../service/gridfs');
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import * as bus from '../service/bus';
|
|||||||
import {
|
import {
|
||||||
Route, Connection, Handler, ConnectionHandler, param, Types,
|
Route, Connection, Handler, ConnectionHandler, param, Types,
|
||||||
} from '../service/server';
|
} from '../service/server';
|
||||||
|
import storage from '../service/storage';
|
||||||
import * as misc from '../lib/misc';
|
import * as misc from '../lib/misc';
|
||||||
import { md5 } from '../lib/crypto';
|
import { md5 } from '../lib/crypto';
|
||||||
import * as mail from '../lib/mail';
|
import * as mail from '../lib/mail';
|
||||||
@@ -15,7 +16,6 @@ import * as message from '../model/message';
|
|||||||
import * as document from '../model/document';
|
import * as document from '../model/document';
|
||||||
import * as system from '../model/system';
|
import * as system from '../model/system';
|
||||||
import * as user from '../model/user';
|
import * as user from '../model/user';
|
||||||
import * as file from '../model/file';
|
|
||||||
import * as setting from '../model/setting';
|
import * as setting from '../model/setting';
|
||||||
import * as domain from '../model/domain';
|
import * as domain from '../model/domain';
|
||||||
import * as discussion from '../model/discussion';
|
import * as discussion from '../model/discussion';
|
||||||
@@ -383,18 +383,18 @@ class HomeFileHandler extends Handler {
|
|||||||
['Hydro', 'homepage'],
|
['Hydro', 'homepage'],
|
||||||
['domain_file', null],
|
['domain_file', null],
|
||||||
];
|
];
|
||||||
const ufdocs = await file.getMulti({ owner: this.user._id }).toArray();
|
const ufdocs = await storage.list(`user/${this.user._id}/`);
|
||||||
this.response.template = 'home_file.html';
|
this.response.template = 'home_file.html';
|
||||||
this.response.body = { ufdocs, path };
|
this.response.body = { ufdocs, path };
|
||||||
}
|
}
|
||||||
|
|
||||||
@param('ufid', Types.ObjectID)
|
@param('ufid', Types.String)
|
||||||
async postDelete(domainId: string, ufid: ObjectID) {
|
async postDelete(domainId: string, target: string) {
|
||||||
const ufdoc = await file.getMeta(ufid);
|
if (!target.startsWith(`user/${this.user._id}`)) this.checkPriv(PRIV.PRIV_DELETE_FILE);
|
||||||
if (ufdoc.owner !== this.user._id) this.checkPriv(PRIV.PRIV_DELETE_FILE);
|
|
||||||
else this.checkPriv(PRIV.PRIV_DELETE_FILE_SELF);
|
else this.checkPriv(PRIV.PRIV_DELETE_FILE_SELF);
|
||||||
const result = await file.del(ufdoc._id);
|
const { size } = await storage.getMeta(target);
|
||||||
if (result) await user.inc(this.user._id, 'usage', -ufdoc.size);
|
await storage.del(target);
|
||||||
|
await user.inc(this.user._id, 'usage', -size);
|
||||||
this.back();
|
this.back();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { PERM } from '../model/builtin';
|
|||||||
import {
|
import {
|
||||||
Route, Handler, Types, param, post,
|
Route, Handler, Types, param, post,
|
||||||
} from '../service/server';
|
} from '../service/server';
|
||||||
|
import storage from '../service/storage';
|
||||||
import { isPid } from '../lib/validator';
|
import { isPid } from '../lib/validator';
|
||||||
import download from '../lib/download';
|
import download from '../lib/download';
|
||||||
|
|
||||||
@@ -89,14 +90,13 @@ class ProblemImportSYZOJHandler extends Handler {
|
|||||||
const urls = {};
|
const urls = {};
|
||||||
if (r.body.error) return;
|
if (r.body.error) return;
|
||||||
for (const t of r.body.downloadInfo) urls[t.filename] = t.downloadUrl;
|
for (const t of r.body.downloadInfo) urls[t.filename] = t.downloadUrl;
|
||||||
const zip = new AdmZip();
|
|
||||||
for (const f of result.body.testData) {
|
for (const f of result.body.testData) {
|
||||||
const p = new PassThrough();
|
const p = new PassThrough();
|
||||||
superagent.get(urls[f.filename]).pipe(p);
|
superagent.get(urls[f.filename]).pipe(p);
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
zip.addFile(f.filename, await streamToBuffer(p));
|
await storage.put(`problem/${domainId}/${docId}/testdata/${f.filename}`, p);
|
||||||
}
|
}
|
||||||
await problem.setTestdata(domainId, docId, zip.toBuffer());
|
// TODO additional_file
|
||||||
if (judge) {
|
if (judge) {
|
||||||
await problem.edit(domainId, docId, {
|
await problem.edit(domainId, docId, {
|
||||||
config: {
|
config: {
|
||||||
@@ -182,7 +182,12 @@ class ProblemImportSYZOJHandler extends Handler {
|
|||||||
w.on('error', reject);
|
w.on('error', reject);
|
||||||
r.pipe(w);
|
r.pipe(w);
|
||||||
});
|
});
|
||||||
await problem.setTestdata(domainId, docId, file);
|
const zip = new AdmZip(file);
|
||||||
|
const entries = zip.getEntries();
|
||||||
|
for (const entry of entries) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await storage.put(`problem/${domainId}/${docId}/testdata/${entry.entryName}`, entry.getData());
|
||||||
|
}
|
||||||
await problem.edit(domainId, docId, {
|
await problem.edit(domainId, docId, {
|
||||||
config: {
|
config: {
|
||||||
time: `${p.time_limit}ms`,
|
time: `${p.time_limit}ms`,
|
||||||
|
|||||||
@@ -1,29 +1,17 @@
|
|||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { ObjectID } from 'mongodb';
|
import { BadRequestError } from '../error';
|
||||||
import { PRIV } from '../model/builtin';
|
import { PRIV } from '../model/builtin';
|
||||||
import * as system from '../model/system';
|
import * as system from '../model/system';
|
||||||
import * as file from '../model/file';
|
|
||||||
import * as user from '../model/user';
|
import * as user from '../model/user';
|
||||||
import db from '../service/db';
|
import db from '../service/db';
|
||||||
import {
|
import {
|
||||||
Route, Handler, Types, param,
|
Route, Handler, Types, param,
|
||||||
} from '../service/server';
|
} from '../service/server';
|
||||||
import { BadRequestError } from '../error';
|
import storage from '../service/storage';
|
||||||
|
|
||||||
const coll = db.collection('status');
|
const coll = db.collection('status');
|
||||||
|
|
||||||
class FileDownloadHandler extends Handler {
|
|
||||||
@param('fileId', Types.ObjectID)
|
|
||||||
@param('secret', Types.String)
|
|
||||||
@param('name', Types.String, true)
|
|
||||||
async get(domainId: string, fileId: ObjectID, secret: string, name?: string) {
|
|
||||||
if (name) name = Buffer.from(name.replace(/~/g, '/'), 'base64').toString();
|
|
||||||
const doc = await file.getWithSecret(fileId, secret);
|
|
||||||
this.response.attachment(name || fileId.toHexString(), doc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class FileUploadHandler extends Handler {
|
class FileUploadHandler extends Handler {
|
||||||
async getQuota() {
|
async getQuota() {
|
||||||
let quota = system.get('user.quota');
|
let quota = system.get('user.quota');
|
||||||
@@ -44,15 +32,14 @@ class FileUploadHandler extends Handler {
|
|||||||
if (!this.request.files.file) throw new BadRequestError();
|
if (!this.request.files.file) throw new BadRequestError();
|
||||||
const quota = await this.getQuota();
|
const quota = await this.getQuota();
|
||||||
const lfdoc = await fs.promises.stat(this.request.files.file.path);
|
const lfdoc = await fs.promises.stat(this.request.files.file.path);
|
||||||
let ufid: ObjectID;
|
const ufid = `user/${this.user._id}/${title}`;
|
||||||
const udoc = await user.inc(this.user._id, 'usage', lfdoc.size);
|
const udoc = await user.inc(this.user._id, 'usage', lfdoc.size);
|
||||||
try {
|
try {
|
||||||
ufid = await file.add(this.request.files.file.path, title, this.user._id);
|
await storage.put(ufid, this.request.files.file.path);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await user.inc(this.user._id, 'usage', -lfdoc.size);
|
await user.inc(this.user._id, 'usage', -lfdoc.size);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
const fdoc = await file.getMeta(ufid);
|
|
||||||
this.response.template = 'fs_upload.html';
|
this.response.template = 'fs_upload.html';
|
||||||
if (redirect) {
|
if (redirect) {
|
||||||
this.response.redirect = redirect.includes('?')
|
this.response.redirect = redirect.includes('?')
|
||||||
@@ -60,7 +47,7 @@ class FileUploadHandler extends Handler {
|
|||||||
: `${redirect}?ufid=${ufid}`;
|
: `${redirect}?ufid=${ufid}`;
|
||||||
}
|
}
|
||||||
this.response.body = {
|
this.response.body = {
|
||||||
fdoc, ufid, usage: udoc.usage, quota,
|
ufid, usage: udoc.usage, quota,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,8 +125,6 @@ class SwitchLanguageHandler extends Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function apply() {
|
export async function apply() {
|
||||||
Route('file_download', '/fs/:fileId/:secret', FileDownloadHandler);
|
|
||||||
Route('file_download_with_name', '/fs/:fileId/:name/:secret', FileDownloadHandler);
|
|
||||||
Route('file_upload', '/fs/upload', FileUploadHandler, PRIV.PRIV_CREATE_FILE);
|
Route('file_upload', '/fs/upload', FileUploadHandler, PRIV.PRIV_CREATE_FILE);
|
||||||
Route('status', '/status', StatusHandler);
|
Route('status', '/status', StatusHandler);
|
||||||
Route('status_update', '/status/update', StatusUpdateHandler);
|
Route('status_update', '/status/update', StatusUpdateHandler);
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { isSafeInteger, flatten, pick } from 'lodash';
|
import { isSafeInteger, flatten, pick } from 'lodash';
|
||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
|
import fs from 'fs-extra';
|
||||||
import { FilterQuery, ObjectID } from 'mongodb';
|
import { FilterQuery, ObjectID } from 'mongodb';
|
||||||
import AdmZip from 'adm-zip';
|
import AdmZip from 'adm-zip';
|
||||||
import {
|
import {
|
||||||
NoProblemError, ProblemDataNotFoundError, BadRequestError,
|
NoProblemError, BadRequestError, PermissionError,
|
||||||
SolutionNotFoundError, ProblemNotFoundError, ValidationError,
|
SolutionNotFoundError, ProblemNotFoundError, ValidationError,
|
||||||
PermissionError,
|
|
||||||
} from '../error';
|
} from '../error';
|
||||||
import { streamToBuffer } from '../utils';
|
import { streamToBuffer } from '../utils';
|
||||||
import {
|
import {
|
||||||
@@ -14,13 +14,13 @@ import {
|
|||||||
import paginate from '../lib/paginate';
|
import paginate from '../lib/paginate';
|
||||||
import { isTitle, isContent, isPid } from '../lib/validator';
|
import { isTitle, isContent, isPid } from '../lib/validator';
|
||||||
import { ProblemAdd } from '../lib/ui';
|
import { ProblemAdd } from '../lib/ui';
|
||||||
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';
|
||||||
import * as domain from '../model/domain';
|
import * as domain from '../model/domain';
|
||||||
import * as user from '../model/user';
|
import * as user from '../model/user';
|
||||||
import * as solution from '../model/solution';
|
import * as solution from '../model/solution';
|
||||||
import { PERM, PRIV, CONSTANT } from '../model/builtin';
|
import { PERM, PRIV, CONSTANT } from '../model/builtin';
|
||||||
|
import storage from '../service/storage';
|
||||||
import * as bus from '../service/bus';
|
import * as bus from '../service/bus';
|
||||||
import {
|
import {
|
||||||
Route, Connection, Handler, ConnectionHandler, Types, param, post, route,
|
Route, Connection, Handler, ConnectionHandler, Types, param, post, route,
|
||||||
@@ -239,13 +239,15 @@ export class ProblemExportHandler extends ProblemDetailHandler {
|
|||||||
const hasPerm = (this.user._id === this.pdoc.owner && this.user.hasPerm(PERM.PERM_READ_PROBLEM_DATA_SELF))
|
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);
|
|| this.user.hasPerm(PERM.PERM_READ_PROBLEM_DATA_SELF);
|
||||||
const pdoc = pick(this.pdoc, ['pid', 'acMsg', 'content', 'config', 'title', 'html', 'tag', 'category']);
|
const pdoc = pick(this.pdoc, ['pid', 'acMsg', 'content', 'config', 'title', 'html', 'tag', 'category']);
|
||||||
let zip: AdmZip;
|
const zip = new AdmZip();
|
||||||
if (hasPerm) {
|
if (hasPerm) {
|
||||||
if (this.pdoc.data instanceof ObjectID) {
|
const files = await storage.list(`problem/${this.domainId}/${this.pdoc.docId}/`, true);
|
||||||
const buf = await streamToBuffer(await file.get(this.pdoc.data));
|
for (const file of files) {
|
||||||
zip = new AdmZip(buf);
|
// eslint-disable-next-line no-await-in-loop
|
||||||
} else zip = new AdmZip();
|
const buf = await streamToBuffer(storage.get(`${file.prefix}${file.name}`));
|
||||||
} else zip = new AdmZip();
|
zip.addFile(file.name, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
zip.addFile('problem.json', Buffer.from(JSON.stringify(pdoc)));
|
zip.addFile('problem.json', Buffer.from(JSON.stringify(pdoc)));
|
||||||
this.response.attachment(`${this.pdoc.title}.zip`, zip.toBuffer());
|
this.response.attachment(`${this.pdoc.title}.zip`, zip.toBuffer());
|
||||||
}
|
}
|
||||||
@@ -447,59 +449,67 @@ export class ProblemEditHandler extends ProblemManageHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ProblemDataUploadHandler extends ProblemManageHandler {
|
export class ProblemFilesHandler extends ProblemDetailHandler {
|
||||||
async get() {
|
@param('testdata', Types.Boolean)
|
||||||
if (this.pdoc.data instanceof ObjectID) {
|
@param('additional_file', Types.Boolean)
|
||||||
const f = await file.getMeta(this.pdoc.data);
|
async get(domainId: string, getTestdata = true, getAdditionalFile = true) {
|
||||||
this.response.body.md5 = f.md5;
|
const [testdata, additional_file] = await Promise.all([
|
||||||
} else if (this.pdoc.data) {
|
getTestdata
|
||||||
this.response.body.from = this.pdoc.data;
|
? storage.list(`problem/${this.pdoc.domainId}/${this.pdoc.docId}/testdata`, true)
|
||||||
}
|
: [],
|
||||||
this.response.template = 'problem_upload.html';
|
getAdditionalFile
|
||||||
|
? storage.list(`problem/${this.pdoc.domainId}/${this.pdoc.docId}/additional_file`, true)
|
||||||
|
: [],
|
||||||
|
]);
|
||||||
|
this.response.body.testdata = testdata;
|
||||||
|
this.response.body.additional_file = additional_file;
|
||||||
|
this.response.template = 'problem_files.html';
|
||||||
}
|
}
|
||||||
|
|
||||||
async post({ domainId }) {
|
@post('files', Types.Array)
|
||||||
if (!this.request.files.file) throw new ValidationError('file');
|
@post('type', Types.Range(['testdata', 'additional_file']), true)
|
||||||
this.pdoc = await problem.setTestdata(
|
async postGetLinks(domainId: string, files: string[], type = 'testdata') {
|
||||||
domainId, this.pdoc.docId, this.request.files.file.path,
|
const isJudge = this.user.hasPriv(PRIV.PRIV_JUDGE);
|
||||||
);
|
if (type === 'testdata' && !isJudge) {
|
||||||
if (this.pdoc.data instanceof ObjectID) {
|
|
||||||
const f = await file.getMeta(this.pdoc.data);
|
|
||||||
this.response.body.md5 = f.md5;
|
|
||||||
}
|
|
||||||
this.response.template = 'problem_upload.html';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ProblemDataDownloadHandler extends ProblemDetailHandler {
|
|
||||||
async get({ pid }) {
|
|
||||||
if (!this.user.hasPriv(PRIV.PRIV_JUDGE)) {
|
|
||||||
if (this.user._id !== this.pdoc.owner) {
|
if (this.user._id !== this.pdoc.owner) {
|
||||||
this.checkPerm(PERM.PERM_READ_PROBLEM_DATA);
|
this.checkPerm(PERM.PERM_READ_PROBLEM_DATA);
|
||||||
} else this.checkPerm(PERM.PERM_READ_PROBLEM_DATA_SELF);
|
} else this.checkPerm(PERM.PERM_READ_PROBLEM_DATA_SELF);
|
||||||
}
|
}
|
||||||
if (this.pdoc.data instanceof ObjectID) {
|
const links = {};
|
||||||
this.response.redirect = await file.url(this.pdoc.data, `${this.pdoc.title}.zip`);
|
for (const file of files) {
|
||||||
} else if (this.pdoc.data) {
|
// eslint-disable-next-line no-await-in-loop
|
||||||
if (!this.pdoc.data.host) {
|
links[file] = await storage.signDownloadLink(
|
||||||
this.response.redirect = this.url('problem_data', {
|
`problem/${this.pdoc.domainId}/${this.pdoc.docId}/${type}/${file}`,
|
||||||
domainId: this.pdoc.data.domainId,
|
file, false, isJudge ? 'judge' : 'user',
|
||||||
pid: this.pdoc.data.pid,
|
);
|
||||||
});
|
}
|
||||||
} else {
|
this.response.body.links = links;
|
||||||
const [scheme, raw] = this.pdoc.data.host.split('//');
|
}
|
||||||
const args = JSON.parse(Buffer.from(raw, 'base64').toString());
|
|
||||||
if (scheme === 'hydro') {
|
@post('filename', Types.String)
|
||||||
const [secure, host, port, domainId, id] = args;
|
@post('type', Types.String, true)
|
||||||
this.response.redirect = `http${secure ? 's' : ''}://${host}:${port}/d/${domainId}/p/${id}/data`;
|
async postUploadFile(domainId: string, filename: string, type = 'testdata') {
|
||||||
} else if (scheme === 'syzoj') {
|
if (!this.request.files.file) throw new ValidationError('file');
|
||||||
const [secure, host, port, id] = args;
|
if (this.pdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_PROBLEM);
|
||||||
this.response.redirect = `http${secure ? 's' : ''}://${host}:${port}/problem/${id}/testdata/download`;
|
else this.checkPerm(PERM.PERM_EDIT_PROBLEM_SELF);
|
||||||
} else {
|
if (filename === 'testdata.zip') {
|
||||||
throw new ProblemDataNotFoundError(pid);
|
const zip = new AdmZip(this.request.files.file.path);
|
||||||
}
|
const entries = zip.getEntries();
|
||||||
|
for (const entry of entries) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await storage.put(`problem/${this.pdoc.domainId}/${this.pdoc.docId}/${type}/${entry.name}`, entry.getData());
|
||||||
}
|
}
|
||||||
} else throw new ProblemDataNotFoundError(pid);
|
} else await storage.put(`problem/${this.pdoc.domainId}/${this.pdoc.docId}/${type}/${filename}`, this.request.files.file.path);
|
||||||
|
this.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
@post('files', Types.Array)
|
||||||
|
@post('type', Types.Range(['testdata', 'additional_file']), true)
|
||||||
|
async postDeleteFiles(domainId: string, files: string[], type = 'testdata') {
|
||||||
|
if (this.pdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_PROBLEM);
|
||||||
|
else this.checkPerm(PERM.PERM_EDIT_PROBLEM_SELF);
|
||||||
|
await storage.del(files.map((file) => `problem/${this.pdoc.domainId}/${this.pdoc.docId}/${type}/${file}`));
|
||||||
|
this.back();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -611,23 +621,18 @@ export class ProblemSolutionHandler extends ProblemDetailHandler {
|
|||||||
|
|
||||||
export class ProblemSolutionRawHandler extends ProblemDetailHandler {
|
export class ProblemSolutionRawHandler extends ProblemDetailHandler {
|
||||||
@param('psid', Types.ObjectID)
|
@param('psid', Types.ObjectID)
|
||||||
async get(domainId: string, psid: ObjectID) {
|
@route('psrid', Types.ObjectID, true)
|
||||||
|
async get(domainId: string, psid: ObjectID, psrid?: ObjectID) {
|
||||||
this.checkPerm(PERM.PERM_VIEW_PROBLEM_SOLUTION);
|
this.checkPerm(PERM.PERM_VIEW_PROBLEM_SOLUTION);
|
||||||
const psdoc = await solution.get(domainId, psid);
|
if (psrid) {
|
||||||
|
const [psdoc, psrdoc] = await solution.getReply(domainId, psid, psrid);
|
||||||
|
if ((!psdoc) || psdoc.pid !== this.pdoc.docId) throw new SolutionNotFoundError(psid, psrid);
|
||||||
|
this.response.body = psrdoc.content;
|
||||||
|
} else {
|
||||||
|
const psdoc = await solution.get(domainId, psid);
|
||||||
|
this.response.body = psdoc.content;
|
||||||
|
}
|
||||||
this.response.type = 'text/markdown';
|
this.response.type = 'text/markdown';
|
||||||
this.response.body = psdoc.content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ProblemSolutionReplyRawHandler extends ProblemDetailHandler {
|
|
||||||
@param('psid', Types.ObjectID)
|
|
||||||
@param('psrid', Types.ObjectID)
|
|
||||||
async get(domainId: string, psid: ObjectID, psrid: ObjectID) {
|
|
||||||
this.checkPerm(PERM.PERM_VIEW_PROBLEM_SOLUTION);
|
|
||||||
const [psdoc, psrdoc] = await solution.getReply(domainId, psid, psrid);
|
|
||||||
if ((!psdoc) || psdoc.pid !== this.pdoc.docId) throw new SolutionNotFoundError(psid, psrid);
|
|
||||||
this.response.type = 'text/markdown';
|
|
||||||
this.response.body = psrdoc.content;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -662,21 +667,26 @@ export class ProblemCreateHandler extends Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ProblemImportHandler extends Handler {
|
export class ProblemImportHandler extends Handler {
|
||||||
@param('ufid', Types.ObjectID, true)
|
async get() {
|
||||||
async get(domainId: string, ufid?: ObjectID) {
|
this.response.template = 'problem_import.html';
|
||||||
if (ufid) {
|
}
|
||||||
const stat = await file.getMeta(ufid);
|
|
||||||
if (stat.size > 128 * 1024 * 1024) throw new BadRequestError('File too large');
|
async post(domainId: string) {
|
||||||
const stream = await file.get(ufid);
|
if (!this.request.files.file) throw new ValidationError('file');
|
||||||
const buf = await streamToBuffer(stream);
|
const stat = await fs.stat(this.request.files.file.path);
|
||||||
const zip = new AdmZip(buf);
|
if (stat.size > 128 * 1024 * 1024) throw new BadRequestError('File too large');
|
||||||
const pdoc = JSON.parse(zip.getEntry('problem.json').getData().toString());
|
const zip = new AdmZip(this.request.files.file.path);
|
||||||
const pid = await problem.add(domainId, pdoc.pid, pdoc.title, pdoc.content, this.user._id, pdoc.tags, pdoc.category);
|
const pdoc = JSON.parse(zip.getEntry('problem.json').getData().toString());
|
||||||
await problem.setTestdata(domainId, pid, buf);
|
const pid = await problem.add(domainId, pdoc.pid, pdoc.title, pdoc.content, this.user._id, pdoc.tags, pdoc.category);
|
||||||
await problem.edit(domainId, pid, { html: pdoc.html });
|
const entries = zip.getEntries();
|
||||||
this.response.redirect = this.url('problem_detail', { pid });
|
for (const entry of entries) {
|
||||||
await file.del(ufid);
|
if (entry.name.startsWith('testdata/') || entry.name.startsWith('additional_file')) {
|
||||||
} else this.response.template = 'problem_import.html';
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await storage.put(`problem/${domainId}/${pid}/${entry.name}`, entry.getData());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await problem.edit(domainId, pid, { html: pdoc.html });
|
||||||
|
this.response.redirect = this.url('problem_detail', { pid });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -692,11 +702,10 @@ export async function apply() {
|
|||||||
Route('problem_settings', '/p/:pid/settings', ProblemSettingsHandler);
|
Route('problem_settings', '/p/:pid/settings', ProblemSettingsHandler);
|
||||||
Route('problem_statistics', '/p/:pid/statistics', ProblemStatisticsHandler);
|
Route('problem_statistics', '/p/:pid/statistics', ProblemStatisticsHandler);
|
||||||
Route('problem_edit', '/p/:pid/edit', ProblemEditHandler);
|
Route('problem_edit', '/p/:pid/edit', ProblemEditHandler);
|
||||||
Route('problem_upload', '/p/:pid/upload', ProblemDataUploadHandler);
|
Route('problem_files', '/p/:pid/files', ProblemFilesHandler);
|
||||||
Route('problem_data', '/p/:pid/data', ProblemDataDownloadHandler);
|
|
||||||
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', ProblemSolutionRawHandler);
|
||||||
Route('problem_create', '/problem/create', ProblemCreateHandler, PERM.PERM_CREATE_PROBLEM);
|
Route('problem_create', '/problem/create', ProblemCreateHandler, PERM.PERM_CREATE_PROBLEM);
|
||||||
Route('problem_import', '/problem/import', ProblemImportHandler, 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,7 +2,6 @@ import { FilterQuery, ObjectID } from 'mongodb';
|
|||||||
import { PermissionError, RecordNotFoundError } from '../error';
|
import { PermissionError, RecordNotFoundError } from '../error';
|
||||||
import { PERM, CONSTANT, STATUS } from '../model/builtin';
|
import { PERM, CONSTANT, STATUS } from '../model/builtin';
|
||||||
import * as problem from '../model/problem';
|
import * as problem from '../model/problem';
|
||||||
import * as file from '../model/file';
|
|
||||||
import * as record from '../model/record';
|
import * as record from '../model/record';
|
||||||
import * as contest from '../model/contest';
|
import * as contest from '../model/contest';
|
||||||
import * as user from '../model/user';
|
import * as user from '../model/user';
|
||||||
@@ -12,7 +11,6 @@ import {
|
|||||||
Route, Handler, Connection, ConnectionHandler, Types, param,
|
Route, Handler, Connection, ConnectionHandler, Types, param,
|
||||||
} from '../service/server';
|
} from '../service/server';
|
||||||
import { Rdoc } from '../interface';
|
import { Rdoc } from '../interface';
|
||||||
import { streamToBuffer } from '../utils';
|
|
||||||
|
|
||||||
const RecordHandler = contest.ContestHandlerMixin(Handler);
|
const RecordHandler = contest.ContestHandlerMixin(Handler);
|
||||||
|
|
||||||
@@ -132,35 +130,6 @@ class RecordDetailHandler extends RecordHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RecordHackHandler extends Handler {
|
|
||||||
@param('rid', Types.ObjectID)
|
|
||||||
@param('ufid', Types.ObjectID)
|
|
||||||
async get(domainId: string, rid: ObjectID, ufid?: ObjectID) {
|
|
||||||
if (ufid) {
|
|
||||||
const stream = await file.get(ufid);
|
|
||||||
const buf = await streamToBuffer(stream);
|
|
||||||
const input = buf.toString();
|
|
||||||
await this.post(domainId, rid, input);
|
|
||||||
} else this.response.template = 'record_hack.html';
|
|
||||||
}
|
|
||||||
|
|
||||||
@param('rid', Types.ObjectID)
|
|
||||||
@param('input', Types.String)
|
|
||||||
async post(domainId: string, rid: ObjectID, input: string) {
|
|
||||||
const rdoc = await record.get(domainId, rid);
|
|
||||||
if (!rdoc) throw new RecordNotFoundError(domainId, rid);
|
|
||||||
const newRid = await record.add(
|
|
||||||
domainId, rdoc.pid, this.user._id,
|
|
||||||
rdoc.lang, rdoc.code, true,
|
|
||||||
{ hack: input },
|
|
||||||
);
|
|
||||||
bus.boardcast('record/change', rdoc);
|
|
||||||
this.response.body = { rid: newRid };
|
|
||||||
this.response.redirect = this.url('record_detail', { rid: newRid });
|
|
||||||
// TODO handle hack(modify testdata, rejudge problem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated
|
* @deprecated
|
||||||
* use RecordDetailHandler.postRejudge instead.
|
* use RecordDetailHandler.postRejudge instead.
|
||||||
@@ -261,7 +230,6 @@ class RecordDetailConnectionHandler extends contest.ContestHandlerMixin(Connecti
|
|||||||
export async function apply() {
|
export async function apply() {
|
||||||
Route('record_main', '/record', RecordListHandler);
|
Route('record_main', '/record', RecordListHandler);
|
||||||
Route('record_detail', '/record/:rid', RecordDetailHandler);
|
Route('record_detail', '/record/:rid', RecordDetailHandler);
|
||||||
Route('record_hack', '/record/:rid/hack', RecordHackHandler);
|
|
||||||
Route('record_rejudge', '/record/:rid/rejudge', RecordRejudgeHandler);
|
Route('record_rejudge', '/record/:rid/rejudge', RecordRejudgeHandler);
|
||||||
Connection('record_conn', '/record-conn', RecordMainConnectionHandler);
|
Connection('record_conn', '/record-conn', RecordMainConnectionHandler);
|
||||||
Connection('record_detail_conn', '/record-detail-conn', RecordDetailConnectionHandler);
|
Connection('record_detail_conn', '/record-detail-conn', RecordDetailConnectionHandler);
|
||||||
|
|||||||
@@ -12,6 +12,13 @@ export interface System {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SystemKeys {
|
export interface SystemKeys {
|
||||||
|
'file.endPoint': string,
|
||||||
|
'file.accessKey': string,
|
||||||
|
'file.secretKey': string,
|
||||||
|
'file.bucket': string,
|
||||||
|
'file.region': string,
|
||||||
|
'file.endPointForUser': string,
|
||||||
|
'file.endPointForJudge': string,
|
||||||
'smtp.user': string,
|
'smtp.user': string,
|
||||||
'smtp.from': string,
|
'smtp.from': string,
|
||||||
'smtp.pass': string,
|
'smtp.pass': string,
|
||||||
@@ -515,7 +522,6 @@ export interface Model {
|
|||||||
discussion: typeof import('./model/discussion'),
|
discussion: typeof import('./model/discussion'),
|
||||||
document: typeof import('./model/document'),
|
document: typeof import('./model/document'),
|
||||||
domain: typeof import('./model/domain'),
|
domain: typeof import('./model/domain'),
|
||||||
file: typeof import('./model/file'),
|
|
||||||
message: typeof import('./model/message'),
|
message: typeof import('./model/message'),
|
||||||
opcount: typeof import('./model/opcount'),
|
opcount: typeof import('./model/opcount'),
|
||||||
problem: typeof import('./model/problem'),
|
problem: typeof import('./model/problem'),
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import wtfnode from 'wtfnode';
|
|||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import { argv } from 'yargs';
|
import { argv } from 'yargs';
|
||||||
import AdmZip from 'adm-zip';
|
import AdmZip from 'adm-zip';
|
||||||
import { } from './utils';
|
import './utils';
|
||||||
|
|
||||||
export * from './interface';
|
export * from './interface';
|
||||||
|
|
||||||
|
|||||||
@@ -1,130 +0,0 @@
|
|||||||
import { ObjectID, Collection } from 'mongodb';
|
|
||||||
import * as fs from 'fs';
|
|
||||||
import { Dictionary } from 'lodash';
|
|
||||||
import { ForbiddenError, NotFoundError } from '../error';
|
|
||||||
import { Ufdoc } from '../interface';
|
|
||||||
import db from '../service/db';
|
|
||||||
import * as bus from '../service/bus';
|
|
||||||
import gridfs from '../service/gridfs';
|
|
||||||
import hash from '../lib/hash.hydro';
|
|
||||||
import { bufferToStream } from '../utils';
|
|
||||||
|
|
||||||
const coll: Collection<Ufdoc> = db.collection('file');
|
|
||||||
const collFile = db.collection('fs.files');
|
|
||||||
const collChunk = db.collection('fs.chunks');
|
|
||||||
|
|
||||||
function _timestamp() {
|
|
||||||
return Math.floor(Number(new Date()) / 1000000);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function add(
|
|
||||||
f: NodeJS.ReadableStream | Buffer | string, filename: string,
|
|
||||||
owner = 1, meta = {},
|
|
||||||
) {
|
|
||||||
let stream: NodeJS.ReadableStream;
|
|
||||||
if (typeof f === 'string') stream = fs.createReadStream(f);
|
|
||||||
else if (f instanceof Buffer) stream = bufferToStream(f);
|
|
||||||
else stream = f;
|
|
||||||
const w = gridfs.openUploadStream(filename);
|
|
||||||
await coll.insertOne({
|
|
||||||
...meta,
|
|
||||||
_id: w.id,
|
|
||||||
secret: String.random(32),
|
|
||||||
owner,
|
|
||||||
filename,
|
|
||||||
});
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
w.on('error', reject);
|
|
||||||
w.on('finish', resolve);
|
|
||||||
stream.pipe(w);
|
|
||||||
});
|
|
||||||
const c = await gridfs.find({ _id: w.id }).toArray();
|
|
||||||
await coll.updateOne({ _id: w.id }, { $set: { md5: c[0].md5, size: c[0].length } });
|
|
||||||
return w.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function del(_id: ObjectID) {
|
|
||||||
return Promise.all([
|
|
||||||
coll.deleteOne({ _id }),
|
|
||||||
collFile.deleteOne({ _id }),
|
|
||||||
collChunk.deleteMany({ files_id: _id }),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getWithSecret(_id: ObjectID, secret: string, reject?: Function) {
|
|
||||||
const file = await coll.findOne({ _id });
|
|
||||||
if (!file) throw new NotFoundError(_id);
|
|
||||||
const timestamp = _timestamp();
|
|
||||||
if (hash(file.secret, timestamp.toString()) !== secret) {
|
|
||||||
if (hash(file.secret, (timestamp - 1).toString()) !== secret) {
|
|
||||||
throw new ForbiddenError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const stream = gridfs.openDownloadStream(_id);
|
|
||||||
stream.on('error', (err) => {
|
|
||||||
console.error(err);
|
|
||||||
if (reject) reject(err);
|
|
||||||
});
|
|
||||||
return stream;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function get(_id: ObjectID, reject?: Function) {
|
|
||||||
const file = await coll.findOne({ _id });
|
|
||||||
if (!file) throw new NotFoundError(_id);
|
|
||||||
const stream = gridfs.openDownloadStream(_id);
|
|
||||||
stream.on('error', (err) => {
|
|
||||||
console.error(err);
|
|
||||||
if (reject) reject(err);
|
|
||||||
});
|
|
||||||
return stream;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getMeta(_id: ObjectID): Promise<Ufdoc> {
|
|
||||||
return coll.findOne({ _id });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getMetaDict(ufids: ObjectID[]): Promise<Dictionary<Ufdoc>> {
|
|
||||||
const r = {};
|
|
||||||
ufids = Array.from(new Set(ufids));
|
|
||||||
const ufdocs = await coll.find({ _id: { $in: ufids } }).toArray();
|
|
||||||
for (const ufdoc of ufdocs) {
|
|
||||||
r[ufdoc._id.toHexString()] = ufdoc;
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getMulti(query: any) {
|
|
||||||
return coll.find(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function _url(_id: ObjectID, name?: string) {
|
|
||||||
const file = await coll.findOne({ _id });
|
|
||||||
const secret = hash(file.secret, _timestamp().toString());
|
|
||||||
if (name) return `/fs/${_id}/${Buffer.from(name).toString('base64').replace(/\//g, '~')}/${secret}`;
|
|
||||||
return `/fs/${_id}/${secret}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function __url(file: Ufdoc, name?: string) {
|
|
||||||
const secret = hash(file.secret, _timestamp().toString());
|
|
||||||
if (name) return `/fs/${file._id}/${Buffer.from(name).toString('base64').replace(/\//g, '~')}/${secret}`;
|
|
||||||
return `/fs/${file._id}/${secret}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function url(_id: ObjectID, name?: string): Promise<string>;
|
|
||||||
export function url(file: Ufdoc, name: string): string;
|
|
||||||
export function url(arg0: ObjectID | Ufdoc, name?: string) {
|
|
||||||
if (arg0 instanceof ObjectID) return _url(arg0, name);
|
|
||||||
return __url(arg0, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureIndexes() {
|
|
||||||
return Promise.all([
|
|
||||||
collChunk.createIndex({ files_id: 1, n: 1 }, { unique: true }),
|
|
||||||
collFile.createIndex({ md5: 1 }),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
bus.once('app/started', ensureIndexes);
|
|
||||||
global.Hydro.model.file = {
|
|
||||||
add, get, del, getMeta, getWithSecret, getMulti, getMetaDict, url,
|
|
||||||
};
|
|
||||||
@@ -1,15 +1,12 @@
|
|||||||
import { ObjectID, FilterQuery } from 'mongodb';
|
import { ObjectID, FilterQuery } from 'mongodb';
|
||||||
import { Dictionary } from 'lodash';
|
import { Dictionary } from 'lodash';
|
||||||
import { safeLoad } from 'js-yaml';
|
|
||||||
import { STATUS } from './builtin';
|
import { STATUS } from './builtin';
|
||||||
import * as file from './file';
|
|
||||||
import * as document from './document';
|
import * as document from './document';
|
||||||
import * as domain from './domain';
|
import * as domain from './domain';
|
||||||
import { ProblemStatusDoc, ProblemDataSource, Pdict } from '../interface';
|
import { ProblemStatusDoc, ProblemDataSource, Pdict } from '../interface';
|
||||||
import { Content } from '../loader';
|
import { Content } from '../loader';
|
||||||
import { NumberKeys, Projection } from '../typeutils';
|
import { NumberKeys, Projection } from '../typeutils';
|
||||||
import { ProblemNotFoundError } from '../error';
|
import { ProblemNotFoundError } from '../error';
|
||||||
import * as testdataConfig from '../lib/testdataConfig';
|
|
||||||
|
|
||||||
export interface Pdoc {
|
export interface Pdoc {
|
||||||
_id: ObjectID
|
_id: ObjectID
|
||||||
@@ -188,14 +185,6 @@ 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, f: string | Buffer) {
|
|
||||||
const pdoc = await get(domainId, _id);
|
|
||||||
const config = await testdataConfig.readConfig(f);
|
|
||||||
const id = await file.add(f, 'data.zip', 1);
|
|
||||||
if (pdoc.data instanceof ObjectID) file.del(pdoc.data);
|
|
||||||
return await edit(domainId, _id, { data: id, config: safeLoad(config) as any });
|
|
||||||
}
|
|
||||||
|
|
||||||
global.Hydro.model.problem = {
|
global.Hydro.model.problem = {
|
||||||
Pdoc,
|
Pdoc,
|
||||||
|
|
||||||
@@ -218,6 +207,5 @@ global.Hydro.model.problem = {
|
|||||||
getListStatus,
|
getListStatus,
|
||||||
getMultiStatus,
|
getMultiStatus,
|
||||||
setStar,
|
setStar,
|
||||||
setTestdata,
|
|
||||||
updateStatus,
|
updateStatus,
|
||||||
};
|
};
|
||||||
@@ -120,6 +120,13 @@ DomainUserSetting(
|
|||||||
);
|
);
|
||||||
|
|
||||||
SystemSetting(
|
SystemSetting(
|
||||||
|
Setting('setting_file', 'file.endPoint', null, null, 'text', 'Storage engine endPoint'),
|
||||||
|
Setting('setting_file', 'file.accessKey', null, null, 'text', 'Storage engine accessKey'),
|
||||||
|
Setting('setting_file', 'file.secretKey', null, null, 'password', 'Storage engine secret', null, FLAG_SECRET),
|
||||||
|
Setting('setting_file', 'file.bucket', null, 'hydro', 'text', 'Storage engine bucket'),
|
||||||
|
Setting('setting_file', 'file.region', null, 'us-east-1', 'text', 'Storage engine region'),
|
||||||
|
Setting('setting_file', 'file.endPointForUser', null, null, 'text', 'EndPoint for user'),
|
||||||
|
Setting('setting_file', 'file.endPointForJudge', null, null, 'text', 'EndPoint for judge'),
|
||||||
Setting('setting_smtp', 'smtp.user', null, null, 'text', 'SMTP Username'),
|
Setting('setting_smtp', 'smtp.user', null, null, 'text', 'SMTP Username'),
|
||||||
Setting('setting_smtp', 'smtp.pass', null, null, 'password', 'SMTP Password', null, FLAG_SECRET),
|
Setting('setting_smtp', 'smtp.pass', null, null, 'password', 'SMTP Password', null, FLAG_SECRET),
|
||||||
Setting('setting_smtp', 'smtp.host', null, null, 'text', 'SMTP Server Host'),
|
Setting('setting_smtp', 'smtp.host', null, null, 'text', 'SMTP Server Host'),
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export async function runConfig() {
|
|||||||
for (const setting of SYSTEM_SETTINGS) {
|
for (const setting of SYSTEM_SETTINGS) {
|
||||||
if (setting.value) cache[setting.key] = setting.value;
|
if (setting.value) cache[setting.key] = setting.value;
|
||||||
}
|
}
|
||||||
const config = await coll.find({}).toArray();
|
const config = await coll.find().toArray();
|
||||||
for (const i of config) cache[i._id] = i.value;
|
for (const i of config) cache[i._id] = i.value;
|
||||||
bus.emit('database/config');
|
bus.emit('database/config');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,88 @@
|
|||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
import Minio, { BucketItem, ItemBucketMetadata } from 'minio';
|
import { URL } from 'url';
|
||||||
|
import { Client, BucketItem, ItemBucketMetadata } from 'minio';
|
||||||
|
|
||||||
interface StorageOptions {
|
interface StorageOptions {
|
||||||
host?: string,
|
endPoint: string;
|
||||||
port?: number,
|
accessKey: string;
|
||||||
useSSL?: boolean,
|
secretKey: string;
|
||||||
accessKey?: string,
|
bucket: string;
|
||||||
secretKey?: string,
|
region?: string;
|
||||||
bucket?: string,
|
endPointForUser?: string;
|
||||||
region?: string,
|
endPointForJudge?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultConfig: StorageOptions = {
|
interface MinioEndpointConfig {
|
||||||
host: 'play.min.io',
|
endPoint: string;
|
||||||
port: 9000,
|
port: number;
|
||||||
useSSL: true,
|
useSSL: boolean;
|
||||||
accessKey: 'Q3AM3UQ867SPQQA43P2F',
|
}
|
||||||
secretKey: 'zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG',
|
|
||||||
bucket: 'hydro',
|
function parseMainEndpointUrl(endpoint: string): MinioEndpointConfig {
|
||||||
region: 'us-east-1',
|
const url = new URL(endpoint);
|
||||||
};
|
const result: Partial<MinioEndpointConfig> = {};
|
||||||
|
if (url.pathname !== '/') throw new Error('Main MinIO endpoint URL of a sub-directory is not supported.');
|
||||||
|
if (url.username || url.password || url.hash || url.search) {
|
||||||
|
throw new Error('Authorization, search parameters and hash are not supported for main MinIO endpoint URL.');
|
||||||
|
}
|
||||||
|
if (url.protocol === 'http:') result.useSSL = false;
|
||||||
|
else if (url.protocol === 'https:') result.useSSL = true;
|
||||||
|
else {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid protocol "${url.protocol}" for main MinIO endpoint URL. Only HTTP and HTTPS are supported.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
result.endPoint = url.hostname;
|
||||||
|
result.port = url.port ? Number(url.port) : result.useSSL ? 443 : 80;
|
||||||
|
return result as MinioEndpointConfig;
|
||||||
|
}
|
||||||
|
function parseAlternativeEndpointUrl(endpoint: string): (originalUrl: string) => string {
|
||||||
|
if (!endpoint) return (originalUrl) => originalUrl;
|
||||||
|
const url = new URL(endpoint);
|
||||||
|
if (url.hash || url.search) throw new Error('Search parameters and hash are not supported for alternative MinIO endpoint URL.');
|
||||||
|
if (!url.pathname.endsWith('/')) throw new Error("Alternative MinIO endpoint URL's pathname must ends with '/'.");
|
||||||
|
return (originalUrl) => {
|
||||||
|
const parsedOriginUrl = new URL(originalUrl);
|
||||||
|
return new URL(parsedOriginUrl.pathname.slice(1) + parsedOriginUrl.search + parsedOriginUrl.hash, url).toString();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
|
||||||
|
function encodeRFC5987ValueChars(str: string) {
|
||||||
|
return (
|
||||||
|
encodeURIComponent(str)
|
||||||
|
// Note that although RFC3986 reserves "!", RFC5987 does not,
|
||||||
|
// so we do not need to escape it
|
||||||
|
.replace(/['()]/g, escape) // i.e., %27 %28 %29
|
||||||
|
.replace(/\*/g, '%2A')
|
||||||
|
// The following are not required for percent-encoding per RFC5987,
|
||||||
|
// so we can allow for a little better readability over the wire: |`^
|
||||||
|
.replace(/%(?:7C|60|5E)/g, unescape)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
class StorageService {
|
class StorageService {
|
||||||
public client: Minio.Client;
|
public client: Client;
|
||||||
|
public error = '';
|
||||||
private opts: StorageOptions;
|
private opts: StorageOptions;
|
||||||
|
private replaceWithAlternativeUrlFor: Record<'user' | 'judge', (originalUrl: string) => string>;
|
||||||
|
|
||||||
async start(opts: StorageOptions) {
|
async start(opts: StorageOptions) {
|
||||||
this.opts = { ...defaultConfig, ...opts };
|
try {
|
||||||
this.client = new Minio.Client({
|
this.opts = opts;
|
||||||
endPoint: this.opts.host,
|
this.client = new Client({
|
||||||
port: this.opts.port,
|
...parseMainEndpointUrl(this.opts.endPoint),
|
||||||
useSSL: this.opts.useSSL,
|
accessKey: this.opts.accessKey,
|
||||||
accessKey: this.opts.accessKey,
|
secretKey: this.opts.secretKey,
|
||||||
secretKey: this.opts.secretKey,
|
});
|
||||||
});
|
const exists = await this.client.bucketExists(this.opts.bucket);
|
||||||
const exists = await this.client.bucketExists(this.opts.bucket);
|
if (!exists) await this.client.makeBucket(this.opts.bucket, this.opts.region);
|
||||||
if (!exists) await this.client.makeBucket(this.opts.bucket, this.opts.region);
|
this.replaceWithAlternativeUrlFor = {
|
||||||
|
user: parseAlternativeEndpointUrl(this.opts.endPointForUser),
|
||||||
|
judge: parseAlternativeEndpointUrl(this.opts.endPointForJudge),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
this.error = e.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async put(target: string, file: string | Buffer | Readable, meta: ItemBucketMetadata = {}) {
|
async put(target: string, file: string | Buffer | Readable, meta: ItemBucketMetadata = {}) {
|
||||||
@@ -56,11 +103,33 @@ class StorageService {
|
|||||||
const stream = this.client.listObjects(this.opts.bucket, target, recursive);
|
const stream = this.client.listObjects(this.opts.bucket, target, recursive);
|
||||||
return await new Promise<BucketItem[]>((resolve, reject) => {
|
return await new Promise<BucketItem[]>((resolve, reject) => {
|
||||||
const results: BucketItem[] = [];
|
const results: BucketItem[] = [];
|
||||||
stream.on('data', (result) => results.push(result));
|
stream.on('data', (result) => results.push({
|
||||||
|
...result,
|
||||||
|
prefix: target,
|
||||||
|
name: result.name.split(target)[1],
|
||||||
|
}));
|
||||||
stream.on('end', () => resolve(results));
|
stream.on('end', () => resolve(results));
|
||||||
stream.on('error', reject);
|
stream.on('error', reject);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getMeta(target: string) {
|
||||||
|
const result = await this.client.statObject(this.opts.bucket, target);
|
||||||
|
return { ...result.metaData, ...result };
|
||||||
|
}
|
||||||
|
|
||||||
|
async signDownloadLink(target: string, filename?: string, noExpire = false, useAlternativeEndpointFor?: 'user' | 'judge'): Promise<string> {
|
||||||
|
const url = await this.client.presignedGetObject(
|
||||||
|
this.opts.bucket,
|
||||||
|
target,
|
||||||
|
noExpire ? 24 * 60 * 60 * 7 : 30 * 60,
|
||||||
|
filename
|
||||||
|
? { 'response-content-disposition': `attachment; filename="${encodeRFC5987ValueChars(filename)}"` }
|
||||||
|
: {},
|
||||||
|
);
|
||||||
|
if (useAlternativeEndpointFor) return this.replaceWithAlternativeUrlFor[useAlternativeEndpointFor](url);
|
||||||
|
return url;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const service = new StorageService();
|
const service = new StorageService();
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
|
import { ObjectID, Collection } from 'mongodb';
|
||||||
|
import { terminal } from 'terminal-kit';
|
||||||
|
import { Ufdoc } from './interface';
|
||||||
|
import { Logger } from './logger';
|
||||||
|
import gridfs from './service/gridfs';
|
||||||
|
import storage from './service/storage';
|
||||||
|
import db from './service/db';
|
||||||
|
import * as problem from './model/problem';
|
||||||
import * as domain from './model/domain';
|
import * as domain from './model/domain';
|
||||||
import * as document from './model/document';
|
import * as document from './model/document';
|
||||||
import db from './service/db';
|
|
||||||
|
|
||||||
type UpgradeScript = () => Promise<void>;
|
const logger = new Logger('upgrade');
|
||||||
|
type UpgradeScript = () => Promise<boolean | void>;
|
||||||
|
|
||||||
const scripts: UpgradeScript[] = [
|
const scripts: UpgradeScript[] = [
|
||||||
// Init
|
// Init
|
||||||
@@ -11,6 +19,7 @@ const scripts: UpgradeScript[] = [
|
|||||||
const ddoc = await domain.get('system');
|
const ddoc = await domain.get('system');
|
||||||
if (!ddoc) await domain.add('system', 0, 'Hydro', 'Hydro System');
|
if (!ddoc) await domain.add('system', 0, 'Hydro', 'Hydro System');
|
||||||
// TODO discussion node?
|
// TODO discussion node?
|
||||||
|
return true;
|
||||||
},
|
},
|
||||||
// Add history column to ddoc,drdoc,psdoc
|
// Add history column to ddoc,drdoc,psdoc
|
||||||
async function _1_2() {
|
async function _1_2() {
|
||||||
@@ -29,9 +38,68 @@ const scripts: UpgradeScript[] = [
|
|||||||
});
|
});
|
||||||
if (bulk.length) await bulk.execute();
|
if (bulk.length) await bulk.execute();
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
},
|
},
|
||||||
async function _2_3() {
|
async function _2_3() {
|
||||||
await db.collection('document').updateMany({ pid: /^\d+$/i }, { $unset: { pid: '' } });
|
await db.collection('document').updateMany({ pid: /^\d+$/i }, { $unset: { pid: '' } });
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
async function _3_4() {
|
||||||
|
if (storage.error) {
|
||||||
|
logger.error('Cannot upgrade. Please change storage config.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const coll: Collection<Ufdoc> = db.collection('file');
|
||||||
|
const collFile = db.collection('fs.files');
|
||||||
|
const collChunk = db.collection('fs.chunks');
|
||||||
|
function del(_id: ObjectID) {
|
||||||
|
return Promise.all([
|
||||||
|
coll.deleteOne({ _id }),
|
||||||
|
collFile.deleteOne({ _id }),
|
||||||
|
collChunk.deleteMany({ files_id: _id }),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
async function get(_id: ObjectID, reject?: Function) {
|
||||||
|
const stream = gridfs.openDownloadStream(_id);
|
||||||
|
stream.on('error', (err) => {
|
||||||
|
console.error(err);
|
||||||
|
if (reject) reject(err);
|
||||||
|
});
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
terminal.clear('Upgrade');
|
||||||
|
logger.info('');
|
||||||
|
// Problem file and User file
|
||||||
|
// @ts-ignore
|
||||||
|
const progress = terminal.progressBar({ title: 'Changing storage engine. This may take a long time.', y: 1 });
|
||||||
|
const ddocs = await domain.getMulti().toArray();
|
||||||
|
const totalProgress = terminal.progressBar({
|
||||||
|
// @ts-ignore
|
||||||
|
items: ddocs.length, title: 'Domain', y: 3, eta: true,
|
||||||
|
});
|
||||||
|
for (let i = 0; i < ddocs.length; i++) {
|
||||||
|
const ddoc = ddocs[i];
|
||||||
|
totalProgress.startItem(ddoc._id);
|
||||||
|
const pdocs = await problem.getMulti(ddoc._id, { data: { $ne: null } }, ['docId', 'data', 'title']).toArray();
|
||||||
|
// @ts-ignore
|
||||||
|
const domainProgress = terminal.progressBar({
|
||||||
|
// @ts-ignore
|
||||||
|
items: pdocs.length, title: 'Problems', y: 4, eta: true,
|
||||||
|
});
|
||||||
|
for (let j = 0; j < pdocs.length; j++) {
|
||||||
|
const pdoc = pdocs[j];
|
||||||
|
domainProgress.startItem(`${pdoc.docId}: ${pdoc.title}`);
|
||||||
|
// TODO copy file
|
||||||
|
domainProgress.itemDone(`${pdoc.docId}: ${pdoc.title}`);
|
||||||
|
}
|
||||||
|
domainProgress.stop();
|
||||||
|
totalProgress.itemDone(ddoc._id);
|
||||||
|
}
|
||||||
|
totalProgress.stop();
|
||||||
|
// TODO userfile
|
||||||
|
progress.stop();
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user