mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
feat: search with sonic
This commit is contained in:
1 parent
5d01c81607
commit
43c4bfd8e7
43 files changed
+512
-317
No files matched your search
@@ -1,6 +1,4 @@
|
|||||||
import nodejieba from 'nodejieba';
|
import nodejieba from 'nodejieba';
|
||||||
import { Pdoc } from 'hydrooj';
|
|
||||||
import * as document from 'hydrooj/dist/model/document';
|
|
||||||
import * as bus from 'hydrooj/dist/service/bus';
|
import * as bus from 'hydrooj/dist/service/bus';
|
||||||
|
|
||||||
declare module 'hydrooj' {
|
declare module 'hydrooj' {
|
||||||
@@ -9,15 +7,11 @@ declare module 'hydrooj' {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bus.on('document/add', async (doc) => {
|
const jiebaHook = async (pdoc) => {
|
||||||
if (doc.type !== document.TYPE_PROBLEM) return;
|
if (!pdoc.title) return;
|
||||||
const pdoc = doc as Pdoc;
|
|
||||||
const segments = nodejieba.cutForSearch(pdoc.title);
|
const segments = nodejieba.cutForSearch(pdoc.title);
|
||||||
pdoc.search = segments.join(' ');
|
pdoc.search = segments.join(' ');
|
||||||
});
|
};
|
||||||
|
|
||||||
bus.on('document/set', async (domainId, docType, docId, $set) => {
|
bus.on('problem/before-add', jiebaHook);
|
||||||
if (docType !== document.TYPE_PROBLEM || !$set.title) return;
|
bus.on('problem/before-edit', jiebaHook);
|
||||||
const segments = nodejieba.cutForSearch($set.title);
|
|
||||||
$set.search = segments.join(' ');
|
|
||||||
});
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/better-search",
|
"name": "@hydrooj/better-search",
|
||||||
"version": "1.0.2",
|
"version": "1.0.3",
|
||||||
"description": "Enhance Problem Search",
|
"description": "Enhance Problem Search",
|
||||||
"main": "package.json",
|
"main": "package.json",
|
||||||
"repository": "https://github.com/hydro-dev/Hydro.git",
|
"repository": "https://github.com/hydro-dev/Hydro.git",
|
||||||
@@ -8,13 +8,5 @@
|
|||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nodejieba": "^2.5.1"
|
"nodejieba": "^2.5.1"
|
||||||
},
|
|
||||||
"devDependencies": {},
|
|
||||||
"peerDependencies": {
|
|
||||||
"hydrooj": "^2.13.2"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"build": "tsc",
|
|
||||||
"lint": "eslint . --ext ts --fix"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/hydrojudge",
|
"name": "@hydrooj/hydrojudge",
|
||||||
"bin": "bin/hydrojudge.js",
|
"bin": "bin/hydrojudge.js",
|
||||||
"version": "2.4.15",
|
"version": "2.4.16",
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -44,10 +44,10 @@ async function postInit() {
|
|||||||
|
|
||||||
if (system.get('hydrojudge.disable')) return;
|
if (system.get('hydrojudge.disable')) return;
|
||||||
const info = await sysinfo.get();
|
const info = await sysinfo.get();
|
||||||
monitor.updateJudger(info);
|
monitor.updateJudge(info);
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
const [mid, inf] = await sysinfo.update();
|
const [mid, inf] = await sysinfo.update();
|
||||||
monitor.updateJudger({ mid, ...inf });
|
monitor.updateJudge({ mid, ...inf });
|
||||||
}, 1200000);
|
}, 1200000);
|
||||||
|
|
||||||
async function processData(folder: string) {
|
async function processData(folder: string) {
|
||||||
|
|||||||
@@ -206,6 +206,10 @@ export async function service(pending: string[], fail: string[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (const key in global.Hydro.service) {
|
||||||
|
const srv = global.Hydro.service[key];
|
||||||
|
if (!srv.started && srv.start) await srv.start();
|
||||||
|
}
|
||||||
await bus.serial('app/load/service');
|
await bus.serial('app/load/service');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { PERM, PRIV } from '../model/builtin';
|
|||||||
import * as contest from '../model/contest';
|
import * as contest from '../model/contest';
|
||||||
import * as system from '../model/system';
|
import * as system from '../model/system';
|
||||||
import * as document from '../model/document';
|
import * as document from '../model/document';
|
||||||
import * as problem from '../model/problem';
|
import problem from '../model/problem';
|
||||||
import domain from '../model/domain';
|
import domain from '../model/domain';
|
||||||
import record from '../model/record';
|
import record from '../model/record';
|
||||||
import user from '../model/user';
|
import user from '../model/user';
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ import {
|
|||||||
import { gravatar } from '../lib/misc';
|
import { gravatar } from '../lib/misc';
|
||||||
import paginate from '../lib/paginate';
|
import paginate from '../lib/paginate';
|
||||||
import {
|
import {
|
||||||
Route, Handler, Types, param,
|
Route, Handler, Types, param, query, post,
|
||||||
} from '../service/server';
|
} from '../service/server';
|
||||||
|
|
||||||
class DomainRankHandler extends Handler {
|
class DomainRankHandler extends Handler {
|
||||||
@param('page', Types.PositiveInt, true)
|
@query('page', Types.PositiveInt, true)
|
||||||
async get(domainId: string, page = 1) {
|
async get(domainId: string, page = 1) {
|
||||||
const [dudocs, upcount, ucount] = await paginate(
|
const [dudocs, upcount, ucount] = await paginate(
|
||||||
domain.getMultiUserInDomain(domainId).sort({ rp: -1 }),
|
domain.getMultiUserInDomain(domainId).sort({ rp: -1 }),
|
||||||
@@ -129,8 +129,8 @@ class DomainUserHandler extends ManageHandler {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@param('uid', Types.Int)
|
@post('uid', Types.Int)
|
||||||
@param('role', Types.String)
|
@post('role', Types.String)
|
||||||
async postSetUser(domainId: string, uid: number, role: string) {
|
async postSetUser(domainId: string, uid: number, role: string) {
|
||||||
await domain.setUserRole(domainId, uid, role);
|
await domain.setUserRole(domainId, uid, role);
|
||||||
this.back();
|
this.back();
|
||||||
@@ -228,10 +228,10 @@ class DomainJoinApplicationsHandler extends ManageHandler {
|
|||||||
this.response.template = 'domain_join_applications.html';
|
this.response.template = 'domain_join_applications.html';
|
||||||
}
|
}
|
||||||
|
|
||||||
@param('method', Types.Range([domain.JOIN_METHOD_NONE, domain.JOIN_METHOD_ALL, domain.JOIN_METHOD_CODE]))
|
@post('method', Types.Range([domain.JOIN_METHOD_NONE, domain.JOIN_METHOD_ALL, domain.JOIN_METHOD_CODE]))
|
||||||
@param('role', Types.String, true)
|
@post('role', Types.String, true)
|
||||||
@param('expire', Types.Int, true)
|
@post('expire', Types.Int, true)
|
||||||
@param('invitationCode', Types.String, true)
|
@post('invitationCode', Types.String, true)
|
||||||
async post(domainId: string, method: number, role: string, expire: number, invitationCode = '') {
|
async post(domainId: string, method: number, role: string, expire: number, invitationCode = '') {
|
||||||
const r = await domain.getRoles(this.domain);
|
const r = await domain.getRoles(this.domain);
|
||||||
const roles = r.map((rl) => rl._id);
|
const roles = r.map((rl) => rl._id);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import user from '../model/user';
|
|||||||
import * as system from '../model/system';
|
import * as system from '../model/system';
|
||||||
import * as contest from '../model/contest';
|
import * as contest from '../model/contest';
|
||||||
import * as discussion from '../model/discussion';
|
import * as discussion from '../model/discussion';
|
||||||
import * as problem from '../model/problem';
|
import problem from '../model/problem';
|
||||||
import record from '../model/record';
|
import record from '../model/record';
|
||||||
import * as document from '../model/document';
|
import * as document from '../model/document';
|
||||||
import paginate from '../lib/paginate';
|
import paginate from '../lib/paginate';
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import yaml from 'js-yaml';
|
|||||||
import { ValidationError, RemoteOnlineJudgeError } from '../error';
|
import { ValidationError, RemoteOnlineJudgeError } from '../error';
|
||||||
import { Logger } from '../logger';
|
import { Logger } from '../logger';
|
||||||
import type { ContentNode } from '../interface';
|
import type { ContentNode } from '../interface';
|
||||||
import * as problem from '../model/problem';
|
import problem from '../model/problem';
|
||||||
import { PERM } from '../model/builtin';
|
import { PERM } from '../model/builtin';
|
||||||
import {
|
import {
|
||||||
Route, Handler, Types, post,
|
Route, Handler, Types, post,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { sleep } from '../utils';
|
|||||||
import { Logger } from '../logger';
|
import { Logger } from '../logger';
|
||||||
import difficultyAlgorithm from '../lib/difficulty';
|
import difficultyAlgorithm from '../lib/difficulty';
|
||||||
import record from '../model/record';
|
import record from '../model/record';
|
||||||
import * as problem from '../model/problem';
|
import problem from '../model/problem';
|
||||||
import * as builtin from '../model/builtin';
|
import * as builtin from '../model/builtin';
|
||||||
import * as contest from '../model/contest';
|
import * as contest from '../model/contest';
|
||||||
import domain from '../model/domain';
|
import domain from '../model/domain';
|
||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
Route, Handler, Connection, ConnectionHandler, post, Types,
|
Route, Handler, Connection, ConnectionHandler, post, Types,
|
||||||
} from '../service/server';
|
} from '../service/server';
|
||||||
import storage from '../service/storage';
|
import storage from '../service/storage';
|
||||||
import { updateJudger } from '../service/monitor';
|
import { updateJudge } from '../service/monitor';
|
||||||
|
|
||||||
const logger = new Logger('judge');
|
const logger = new Logger('judge');
|
||||||
|
|
||||||
@@ -145,7 +145,7 @@ class JudgeConnectionHandler extends ConnectionHandler {
|
|||||||
this.processing = null;
|
this.processing = null;
|
||||||
await this.newTask();
|
await this.newTask();
|
||||||
} else if (msg.key === 'status') {
|
} else if (msg.key === 'status') {
|
||||||
await updateJudger(msg.info);
|
await updateJudge(msg.info);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import * as yaml from 'js-yaml';
|
|||||||
import * as judge from './judge';
|
import * as judge from './judge';
|
||||||
import { ValidationError } from '../error';
|
import { ValidationError } from '../error';
|
||||||
import * as check from '../check';
|
import * as check from '../check';
|
||||||
|
import { Logger } from '../logger';
|
||||||
import * as setting from '../model/setting';
|
import * as setting from '../model/setting';
|
||||||
import * as system from '../model/system';
|
import * as system from '../model/system';
|
||||||
import user from '../model/user';
|
import user from '../model/user';
|
||||||
@@ -17,6 +18,8 @@ import {
|
|||||||
validate, isEmail, isUname, isPassword,
|
validate, isEmail, isUname, isPassword,
|
||||||
} from '../lib/validator';
|
} from '../lib/validator';
|
||||||
|
|
||||||
|
const logger = new Logger('manage');
|
||||||
|
|
||||||
function set(key: string, value: any) {
|
function set(key: string, value: any) {
|
||||||
if (setting.SYSTEM_SETTINGS_BY_KEY[key]) {
|
if (setting.SYSTEM_SETTINGS_BY_KEY[key]) {
|
||||||
const s = setting.SYSTEM_SETTINGS_BY_KEY[key];
|
const s = setting.SYSTEM_SETTINGS_BY_KEY[key];
|
||||||
@@ -125,11 +128,12 @@ class SystemScriptHandler extends SystemHandler {
|
|||||||
})
|
})
|
||||||
.catch((err: Error) => {
|
.catch((err: Error) => {
|
||||||
const time = new Date().getTime() - start;
|
const time = new Date().getTime() - start;
|
||||||
|
logger.error(err);
|
||||||
judge.end({
|
judge.end({
|
||||||
domainId,
|
domainId,
|
||||||
rid,
|
rid,
|
||||||
status: STATUS.STATUS_SYSTEM_ERROR,
|
status: STATUS.STATUS_SYSTEM_ERROR,
|
||||||
message: `${err} \n${err.stack} `,
|
message: `${err.message} \n${err.stack} `,
|
||||||
judger: 1,
|
judger: 1,
|
||||||
time,
|
time,
|
||||||
memory: 0,
|
memory: 0,
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ class StatusHandler extends Handler {
|
|||||||
class StatusUpdateHandler extends Handler {
|
class StatusUpdateHandler extends Handler {
|
||||||
async post(args) {
|
async post(args) {
|
||||||
this.checkPriv(PRIV.PRIV_JUDGE);
|
this.checkPriv(PRIV.PRIV_JUDGE);
|
||||||
args.type = 'judger';
|
args.type = 'judge';
|
||||||
return coll.updateOne(
|
return coll.updateOne(
|
||||||
{ mid: args.mid, type: 'judger' },
|
{ mid: args.mid, type: 'judge' },
|
||||||
{ $set: args },
|
{ $set: args },
|
||||||
{ upsert: true },
|
{ upsert: true },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import paginate from '../lib/paginate';
|
|||||||
import { isTitle, isContent, isPid } from '../lib/validator';
|
import { isTitle, isContent, isPid } from '../lib/validator';
|
||||||
import difficultyAlgorithm from '../lib/difficulty';
|
import difficultyAlgorithm from '../lib/difficulty';
|
||||||
import * as system from '../model/system';
|
import * as system from '../model/system';
|
||||||
import * as problem from '../model/problem';
|
import problem from '../model/problem';
|
||||||
import record from '../model/record';
|
import record from '../model/record';
|
||||||
import domain from '../model/domain';
|
import domain from '../model/domain';
|
||||||
import user from '../model/user';
|
import user from '../model/user';
|
||||||
@@ -62,18 +62,26 @@ export class ProblemMainHandler extends ProblemHandler {
|
|||||||
['Hydro', 'homepage'],
|
['Hydro', 'homepage'],
|
||||||
['problem_main', null],
|
['problem_main', null],
|
||||||
];
|
];
|
||||||
|
const search = global.Hydro.lib.problemSearch;
|
||||||
|
let sort: number[];
|
||||||
if (q) {
|
if (q) {
|
||||||
query.$text = { $search: q };
|
|
||||||
path[1][1] = 'problem_main';
|
path[1][1] = 'problem_main';
|
||||||
path.push([q, null, null, true]);
|
path.push([q, null, null, true]);
|
||||||
|
if (search) {
|
||||||
|
const result = await search(domainId, q);
|
||||||
|
query.docId = { $in: result };
|
||||||
|
sort = result;
|
||||||
|
} else query.$text = { $search: q };
|
||||||
}
|
}
|
||||||
if (!this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN)) query.hidden = false;
|
if (!this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN)) query.hidden = false;
|
||||||
await bus.serial('problem/list', query, this);
|
await bus.serial('problem/list', query, this);
|
||||||
const [pdocs, ppcount, pcount] = await paginate(
|
// eslint-disable-next-line prefer-const
|
||||||
|
let [pdocs, ppcount, pcount] = await paginate(
|
||||||
problem.getMulti(domainId, query).sort({ pid: 1, docId: 1 }),
|
problem.getMulti(domainId, query).sort({ pid: 1, docId: 1 }),
|
||||||
page,
|
page,
|
||||||
system.get('pagination.problem'),
|
system.get('pagination.problem'),
|
||||||
);
|
);
|
||||||
|
if (sort) pdocs = pdocs.sort((a, b) => sort.indexOf(a.docId) - sort.indexOf(b.docId));
|
||||||
if (q && +q) {
|
if (q && +q) {
|
||||||
const pdoc = await problem.get(domainId, +q, this.user._id, problem.PROJECTION_LIST);
|
const pdoc = await problem.get(domainId, +q, this.user._id, problem.PROJECTION_LIST);
|
||||||
if (pdoc) pdocs.unshift(pdoc);
|
if (pdoc) pdocs.unshift(pdoc);
|
||||||
@@ -341,9 +349,7 @@ export class ProblemEditHandler extends ProblemManageHandler {
|
|||||||
};
|
};
|
||||||
let pdoc = await problem.get(domainId, pid);
|
let pdoc = await problem.get(domainId, pid);
|
||||||
$update.difficulty = difficultyAlgorithm(pdoc.nSubmit, pdoc.nAccept);
|
$update.difficulty = difficultyAlgorithm(pdoc.nSubmit, pdoc.nAccept);
|
||||||
await bus.serial('problem/before-edit', $update);
|
|
||||||
pdoc = await problem.edit(domainId, pdoc.docId, $update);
|
pdoc = await problem.edit(domainId, pdoc.docId, $update);
|
||||||
await bus.emit('problem/edit', pdoc);
|
|
||||||
this.response.redirect = this.url('problem_detail', { pid: newPid || pdoc.docId });
|
this.response.redirect = this.url('problem_detail', { pid: newPid || pdoc.docId });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { PermissionError, RecordNotFoundError } from '../error';
|
|||||||
import { Rdoc } from '../interface';
|
import { Rdoc } from '../interface';
|
||||||
import { PERM, STATUS, PRIV } from '../model/builtin';
|
import { PERM, STATUS, PRIV } from '../model/builtin';
|
||||||
import * as system from '../model/system';
|
import * as system from '../model/system';
|
||||||
import * as problem from '../model/problem';
|
import problem from '../model/problem';
|
||||||
import record from '../model/record';
|
import record from '../model/record';
|
||||||
import * as contest from '../model/contest';
|
import * as contest from '../model/contest';
|
||||||
import user from '../model/user';
|
import user from '../model/user';
|
||||||
@@ -49,7 +49,7 @@ class RecordListHandler extends RecordHandler {
|
|||||||
user.getList(domainId, rdocs.map((rdoc) => rdoc.uid)),
|
user.getList(domainId, rdocs.map((rdoc) => rdoc.uid)),
|
||||||
canViewProblem
|
canViewProblem
|
||||||
? problem.getList(domainId, rdocs.map((rdoc) => rdoc.pid), canViewProblemHidden, false)
|
? problem.getList(domainId, rdocs.map((rdoc) => rdoc.pid), canViewProblemHidden, false)
|
||||||
: Object.fromEntries([rdocs.map((rdoc) => [rdoc.pid, problem.Pdoc.create(rdoc.pid, rdoc.pid)])]),
|
: Object.fromEntries([rdocs.map((rdoc) => [rdoc.pid, problem.create(rdoc.pid, rdoc.pid)])]),
|
||||||
]);
|
]);
|
||||||
const path = [
|
const path = [
|
||||||
['Hydro', 'homepage'],
|
['Hydro', 'homepage'],
|
||||||
@@ -88,11 +88,11 @@ class RecordDetailHandler extends RecordHandler {
|
|||||||
user.getById(domainId, rdoc.uid),
|
user.getById(domainId, rdoc.uid),
|
||||||
]);
|
]);
|
||||||
if (!(pdoc && this.user.hasPerm(PERM.PERM_VIEW_PROBLEM))) {
|
if (!(pdoc && this.user.hasPerm(PERM.PERM_VIEW_PROBLEM))) {
|
||||||
pdoc = problem.Pdoc.create(pdoc?.docId || 0, pdoc?.pid || '*');
|
pdoc = problem.create(pdoc?.docId || 0, pdoc?.pid || '*');
|
||||||
}
|
}
|
||||||
if (!rdoc.contest && pdoc.hidden && pdoc.owner !== this.user._id) {
|
if (!rdoc.contest && pdoc.hidden && pdoc.owner !== this.user._id) {
|
||||||
if (!this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN)) {
|
if (!this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN)) {
|
||||||
pdoc = problem.Pdoc.create(pdoc.docId, pdoc.pid);
|
pdoc = problem.create(pdoc.docId, pdoc.pid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.response.body = {
|
this.response.body = {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Logger } from '../logger';
|
|||||||
import { ProblemAdd } from '../lib/ui';
|
import { ProblemAdd } from '../lib/ui';
|
||||||
import { PERM } from '../model/builtin';
|
import { PERM } from '../model/builtin';
|
||||||
import token from '../model/token';
|
import token from '../model/token';
|
||||||
import * as problem from '../model/problem';
|
import problem from '../model/problem';
|
||||||
import * as system from '../model/system';
|
import * as system from '../model/system';
|
||||||
import {
|
import {
|
||||||
Handler, Types, Route, post,
|
Handler, Types, Route, post,
|
||||||
@@ -24,9 +24,10 @@ export class ProblemSendHandler extends Handler {
|
|||||||
|
|
||||||
@post('target', Types.String)
|
@post('target', Types.String)
|
||||||
@post('pids', Types.Array)
|
@post('pids', Types.Array)
|
||||||
async postSend(domainId: string, target: any, _pids: (number | string)[]) {
|
async postSend(domainId: string, _target: string, _pids: (number | string)[]) {
|
||||||
this.checkPerm(PERM.PERM_VIEW_PROBLEM);
|
this.checkPerm(PERM.PERM_VIEW_PROBLEM);
|
||||||
target = target.split('@');
|
const target = _target.split('@');
|
||||||
|
if (target[1].endsWith('/')) target[1] = target[1].substr(0, target[1].length - 1);
|
||||||
const getHidden = this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN);
|
const getHidden = this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN);
|
||||||
const getData = this.user.hasPerm(PERM.PERM_READ_PROBLEM_DATA);
|
const getData = this.user.hasPerm(PERM.PERM_READ_PROBLEM_DATA);
|
||||||
const pdocs = await problem.getList(domainId, _pids, true, true, ['docId', 'owner', 'hidden']);
|
const pdocs = await problem.getList(domainId, _pids, true, true, ['docId', 'owner', 'hidden']);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { isTitle, isContent, isDescription } from '../lib/validator';
|
|||||||
import paginate from '../lib/paginate';
|
import paginate from '../lib/paginate';
|
||||||
import * as system from '../model/system';
|
import * as system from '../model/system';
|
||||||
import { PERM, PRIV } from '../model/builtin';
|
import { PERM, PRIV } from '../model/builtin';
|
||||||
import * as problem from '../model/problem';
|
import problem from '../model/problem';
|
||||||
import * as builtin from '../model/builtin';
|
import * as builtin from '../model/builtin';
|
||||||
import * as training from '../model/training';
|
import * as training from '../model/training';
|
||||||
import user from '../model/user';
|
import user from '../model/user';
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import user from '../model/user';
|
|||||||
import oauth from '../model/oauth';
|
import oauth from '../model/oauth';
|
||||||
import token from '../model/token';
|
import token from '../model/token';
|
||||||
import record from '../model/record';
|
import record from '../model/record';
|
||||||
import * as problem from '../model/problem';
|
import problem, { Pdoc } from '../model/problem';
|
||||||
import task from '../model/task';
|
import task from '../model/task';
|
||||||
import * as system from '../model/system';
|
import * as system from '../model/system';
|
||||||
import { PERM, PRIV } from '../model/builtin';
|
import { PERM, PRIV } from '../model/builtin';
|
||||||
@@ -183,7 +183,7 @@ class UserDetailHandler extends Handler {
|
|||||||
1,
|
1,
|
||||||
100,
|
100,
|
||||||
)
|
)
|
||||||
: [[], 0, 0] as [problem.Pdoc[], number, number],
|
: [[], 0, 0] as [Pdoc[], number, number],
|
||||||
]);
|
]);
|
||||||
const pdict = this.user.hasPerm(PERM.PERM_VIEW_PROBLEM)
|
const pdict = this.user.hasPerm(PERM.PERM_VIEW_PROBLEM)
|
||||||
? await problem.getList(
|
? await problem.getList(
|
||||||
@@ -191,7 +191,7 @@ class UserDetailHandler extends Handler {
|
|||||||
this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN), false,
|
this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN), false,
|
||||||
)
|
)
|
||||||
: Object.fromEntries(rdocs.map(
|
: Object.fromEntries(rdocs.map(
|
||||||
(rdoc) => [rdoc.pid, problem.Pdoc.create(undefined, rdoc.pid.toString())],
|
(rdoc) => [rdoc.pid, problem.create(undefined, rdoc.pid.toString())],
|
||||||
));
|
));
|
||||||
// Remove sensitive data
|
// Remove sensitive data
|
||||||
if (!isSelfProfile && sdoc) {
|
if (!isSelfProfile && sdoc) {
|
||||||
|
|||||||
@@ -458,6 +458,13 @@ export interface HydroFileSystem {
|
|||||||
|
|
||||||
export type PathComponent = [string, string, Dictionary<any>?, boolean?];
|
export type PathComponent = [string, string, Dictionary<any>?, boolean?];
|
||||||
|
|
||||||
|
export interface BaseService {
|
||||||
|
started: boolean;
|
||||||
|
error?: Error | string;
|
||||||
|
start: Function;
|
||||||
|
stop?: Function;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Collections {
|
export interface Collections {
|
||||||
'blacklist': Bdoc,
|
'blacklist': Bdoc,
|
||||||
'contest': Tdoc,
|
'contest': Tdoc,
|
||||||
@@ -483,25 +490,25 @@ export interface Collections {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Model {
|
export interface Model {
|
||||||
blacklist: typeof import('./model/blacklist'),
|
blacklist: typeof import('./model/blacklist').default,
|
||||||
builtin: typeof import('./model/builtin'),
|
builtin: typeof import('./model/builtin'),
|
||||||
contest: typeof import('./model/contest'),
|
contest: typeof import('./model/contest'),
|
||||||
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').default,
|
||||||
message: typeof import('./model/message'),
|
message: typeof import('./model/message').default,
|
||||||
opcount: typeof import('./model/opcount'),
|
opcount: typeof import('./model/opcount'),
|
||||||
problem: typeof import('./model/problem'),
|
problem: typeof import('./model/problem').default,
|
||||||
record: typeof import('./model/record'),
|
record: typeof import('./model/record').default,
|
||||||
setting: typeof import('./model/setting'),
|
setting: typeof import('./model/setting'),
|
||||||
solution: typeof import('./model/solution'),
|
solution: typeof import('./model/solution').default,
|
||||||
system: typeof import('./model/system'),
|
system: typeof import('./model/system'),
|
||||||
task: typeof import('./model/task'),
|
task: typeof import('./model/task').default,
|
||||||
oplog: typeof import('./model/oplog'),
|
oplog: typeof import('./model/oplog'),
|
||||||
token: typeof import('./model/token'),
|
token: typeof import('./model/token').default,
|
||||||
training: typeof import('./model/training'),
|
training: typeof import('./model/training'),
|
||||||
user: typeof import('./model/user'),
|
user: typeof import('./model/user').default,
|
||||||
oauth: typeof import('./model/oauth'),
|
oauth: typeof import('./model/oauth').default,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Service {
|
export interface Service {
|
||||||
|
|||||||
@@ -32,5 +32,5 @@ async function ensureIndexes() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bus.once('app/started', ensureIndexes);
|
bus.once('app/started', ensureIndexes);
|
||||||
export = BlackListModel;
|
export default BlackListModel;
|
||||||
global.Hydro.model.blacklist = BlackListModel;
|
global.Hydro.model.blacklist = BlackListModel;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ObjectID, FilterQuery } from 'mongodb';
|
import { ObjectID, FilterQuery } from 'mongodb';
|
||||||
import user from './user';
|
import user from './user';
|
||||||
import * as problem from './problem';
|
import problem from './problem';
|
||||||
import * as document from './document';
|
import * as document from './document';
|
||||||
import { PERM } from './builtin';
|
import { PERM } from './builtin';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { FilterQuery, ObjectID } from 'mongodb';
|
import { FilterQuery, ObjectID } from 'mongodb';
|
||||||
import * as problem from './problem';
|
import problem from './problem';
|
||||||
import * as contest from './contest';
|
import * as contest from './contest';
|
||||||
import * as training from './training';
|
import * as training from './training';
|
||||||
import * as document from './document';
|
import * as document from './document';
|
||||||
|
|||||||
@@ -235,5 +235,5 @@ class DomainModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export = DomainModel;
|
export default DomainModel;
|
||||||
global.Hydro.model.domain = DomainModel;
|
global.Hydro.model.domain = DomainModel;
|
||||||
@@ -76,5 +76,5 @@ function ensureIndexes() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bus.once('app/started', ensureIndexes);
|
bus.once('app/started', ensureIndexes);
|
||||||
export = MessageModel;
|
export default MessageModel;
|
||||||
global.Hydro.model.message = MessageModel;
|
global.Hydro.model.message = MessageModel;
|
||||||
@@ -25,5 +25,5 @@ class OauthModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export = OauthModel;
|
export default OauthModel;
|
||||||
global.Hydro.model.oauth = OauthModel;
|
global.Hydro.model.oauth = OauthModel;
|
||||||
@@ -13,245 +13,231 @@ import storage from '../service/storage';
|
|||||||
import * as bus from '../service/bus';
|
import * as bus from '../service/bus';
|
||||||
|
|
||||||
export interface Pdoc extends Document { }
|
export interface Pdoc extends Document { }
|
||||||
export namespace Pdoc {
|
export type Field = keyof Pdoc;
|
||||||
export type Field = keyof Pdoc;
|
const fields: Field[] = [];
|
||||||
export const fields: Field[] = [];
|
export type Getter = (docId?: number, pid?: string) => Partial<Pdoc>;
|
||||||
type Getter = (docId?: number, pid?: string) => Partial<Pdoc>;
|
const getters: Getter[] = [];
|
||||||
const getters: Getter[] = [];
|
|
||||||
export function extend(getter: Getter) {
|
export class ProblemModel {
|
||||||
|
static PROJECTION_LIST: Field[] = [
|
||||||
|
'_id', 'domainId', 'docType', 'docId', 'pid',
|
||||||
|
'owner', 'title', 'nSubmit', 'nAccept', 'difficulty',
|
||||||
|
'tag', 'hidden',
|
||||||
|
];
|
||||||
|
|
||||||
|
static PROJECTION_PUBLIC: Field[] = [
|
||||||
|
...ProblemModel.PROJECTION_LIST,
|
||||||
|
'content', 'html', 'data', 'config', 'acMsg',
|
||||||
|
'additional_file',
|
||||||
|
];
|
||||||
|
|
||||||
|
static extend(getter: Getter) {
|
||||||
getters.push(getter);
|
getters.push(getter);
|
||||||
fields.push(...Object.keys(getter(0, '0')) as any);
|
fields.push(...Object.keys(getter(0, '0')) as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
extend((docId, pid) => ({
|
static create(docId?: number, pid?: string) {
|
||||||
_id: new ObjectID(),
|
|
||||||
domainId: 'system',
|
|
||||||
docType: document.TYPE_PROBLEM,
|
|
||||||
docId: docId || -1,
|
|
||||||
pid: pid || docId.toString(),
|
|
||||||
owner: 1,
|
|
||||||
title: '*',
|
|
||||||
content: '',
|
|
||||||
html: false,
|
|
||||||
nSubmit: 0,
|
|
||||||
nAccept: 0,
|
|
||||||
tag: [],
|
|
||||||
data: [],
|
|
||||||
additional_file: [],
|
|
||||||
hidden: true,
|
|
||||||
config: {},
|
|
||||||
acMsg: '',
|
|
||||||
difficulty: 0,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export function create(docId?: number, pid?: string) {
|
|
||||||
const result = {} as Pdoc;
|
const result = {} as Pdoc;
|
||||||
for (const getter of getters) {
|
for (const getter of getters) {
|
||||||
Object.assign(result, getter(docId, pid));
|
Object.assign(result, getter(docId, pid));
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export const PROJECTION_LIST: Pdoc.Field[] = [
|
static async add(
|
||||||
'_id', 'domainId', 'docType', 'docId', 'pid',
|
domainId: string, pid: string = null, title: string, content: Content, owner: number,
|
||||||
'owner', 'title', 'nSubmit', 'nAccept', 'difficulty',
|
tag: string[] = [], hidden = false,
|
||||||
'tag', 'hidden',
|
) {
|
||||||
];
|
const pidCounter = await domain.inc(domainId, 'pidCounter', 1);
|
||||||
|
const args: Partial<Pdoc> = {
|
||||||
export const PROJECTION_PUBLIC: Pdoc.Field[] = [
|
title, tag, hidden, nSubmit: 0, nAccept: 0,
|
||||||
...PROJECTION_LIST,
|
};
|
||||||
'content', 'html', 'data', 'config', 'acMsg',
|
if (pid) args.pid = pid;
|
||||||
'additional_file',
|
await bus.serial('problem/before-add', args);
|
||||||
];
|
const result = await document.add(domainId, content, owner, document.TYPE_PROBLEM, pidCounter, null, null, args);
|
||||||
|
await bus.emit('problem/add', args, result);
|
||||||
export async function add(
|
return result;
|
||||||
domainId: string, pid: string = null, title: string, content: Content, owner: number,
|
|
||||||
tag: string[] = [], hidden = false,
|
|
||||||
) {
|
|
||||||
const pidCounter = await domain.inc(domainId, 'pidCounter', 1);
|
|
||||||
const args: Partial<Pdoc> = {
|
|
||||||
title, tag, hidden, nSubmit: 0, nAccept: 0,
|
|
||||||
};
|
|
||||||
if (pid) args.pid = pid;
|
|
||||||
return await document.add(domainId, content, owner, document.TYPE_PROBLEM, pidCounter, null, null, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function get(
|
|
||||||
domainId: string, pid: string | number,
|
|
||||||
uid: number = null, projection: Projection<Pdoc> = PROJECTION_PUBLIC,
|
|
||||||
): Promise<Pdoc> {
|
|
||||||
if (typeof pid !== 'number') {
|
|
||||||
if (Number.isSafeInteger(parseInt(pid, 10))) pid = parseInt(pid, 10);
|
|
||||||
}
|
}
|
||||||
const pdoc = typeof pid === 'number'
|
|
||||||
? await document.get(domainId, document.TYPE_PROBLEM, pid, projection)
|
static async get(
|
||||||
: (await document.getMulti(domainId, document.TYPE_PROBLEM, { pid }).toArray())[0];
|
domainId: string, pid: string | number,
|
||||||
if (!pdoc) return null;
|
uid: number = null, projection: Projection<Pdoc> = ProblemModel.PROJECTION_PUBLIC,
|
||||||
if (uid) {
|
): Promise<Pdoc> {
|
||||||
pdoc.psdoc = await document.getStatus(domainId, document.TYPE_PROBLEM, pdoc.docId, uid);
|
if (typeof pid !== 'number') {
|
||||||
|
if (Number.isSafeInteger(parseInt(pid, 10))) pid = parseInt(pid, 10);
|
||||||
|
}
|
||||||
|
const pdoc = typeof pid === 'number'
|
||||||
|
? await document.get(domainId, document.TYPE_PROBLEM, pid, projection)
|
||||||
|
: (await document.getMulti(domainId, document.TYPE_PROBLEM, { pid }).toArray())[0];
|
||||||
|
if (!pdoc) return null;
|
||||||
|
if (uid) {
|
||||||
|
pdoc.psdoc = await document.getStatus(domainId, document.TYPE_PROBLEM, pdoc.docId, uid);
|
||||||
|
}
|
||||||
|
return pdoc;
|
||||||
}
|
}
|
||||||
return pdoc;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getMulti(domainId: string, query: FilterQuery<Pdoc>, projection = PROJECTION_LIST) {
|
static getMulti(domainId: string, query: FilterQuery<Pdoc>, projection = ProblemModel.PROJECTION_LIST) {
|
||||||
return document.getMulti(domainId, document.TYPE_PROBLEM, query, projection);
|
return document.getMulti(domainId, document.TYPE_PROBLEM, query, projection);
|
||||||
}
|
|
||||||
|
|
||||||
export function getMultiStatus(domainId: string, query: FilterQuery<Pdoc>) {
|
|
||||||
return document.getMultiStatus(domainId, document.TYPE_PROBLEM, query);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function edit(domainId: string, _id: number, $set: Partial<Pdoc>): Promise<Pdoc> {
|
|
||||||
const delpid = $set.pid === '';
|
|
||||||
if (delpid) delete $set.pid;
|
|
||||||
return document.set(domainId, document.TYPE_PROBLEM, _id, $set, delpid ? { pid: '' } : undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function push<T extends ArrayKeys<Pdoc>>(domainId: string, _id: number, key: ArrayKeys<Pdoc>, value: Pdoc[T][0]) {
|
|
||||||
return document.push(domainId, document.TYPE_PROBLEM, _id, key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function pull<T extends ArrayKeys<Pdoc>>(domainId: string, pid: number, key: ArrayKeys<Pdoc>, values: Pdoc[T][0][]) {
|
|
||||||
return document.deleteSub(domainId, document.TYPE_PROBLEM, pid, key, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function inc(domainId: string, _id: number, field: NumberKeys<Pdoc>, n: number): Promise<Pdoc> {
|
|
||||||
return document.inc(domainId, document.TYPE_PROBLEM, _id, field, n);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function count(domainId: string, query: FilterQuery<Pdoc>) {
|
|
||||||
return document.count(domainId, document.TYPE_PROBLEM, query);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function del(domainId: string, docId: number) {
|
|
||||||
return Promise.all([
|
|
||||||
document.deleteOne(domainId, document.TYPE_PROBLEM, docId),
|
|
||||||
document.deleteMultiStatus(domainId, document.TYPE_PROBLEM, { docId }),
|
|
||||||
storage.list(`problem/${domainId}/${docId}/`).then((items) => storage.del(items.map((item) => item.prefix + item.name))),
|
|
||||||
bus.parallel('problem/delete', domainId, docId),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function addTestdata(domainId: string, pid: number, name: string, f: Readable | Buffer | string) {
|
|
||||||
if (!name) throw new ValidationError('name');
|
|
||||||
const [[, fileinfo]] = await Promise.all([
|
|
||||||
document.getSub(domainId, document.TYPE_PROBLEM, pid, 'data', name),
|
|
||||||
storage.put(`problem/${domainId}/${pid}/testdata/${name}`, f),
|
|
||||||
]);
|
|
||||||
const meta = await storage.getMeta(`problem/${domainId}/${pid}/testdata/${name}`);
|
|
||||||
if (!meta) throw new Error('Upload failed');
|
|
||||||
const payload = { name, ...pick(meta, ['size', 'lastModified', 'etag']) };
|
|
||||||
if (!fileinfo) await push(domainId, pid, 'data', { _id: name, ...payload });
|
|
||||||
else await document.setSub(domainId, document.TYPE_PROBLEM, pid, 'data', name, payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function delTestdata(domainId: string, pid: number, name: string | string[]) {
|
|
||||||
const names = (name instanceof Array) ? name : [name];
|
|
||||||
await storage.del(names.map((t) => `problem/${domainId}/${pid}/testdata/${t}`));
|
|
||||||
await pull(domainId, pid, 'data', names);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function addAdditionalFile(domainId: string, pid: number, name: string, f: Readable | Buffer | string) {
|
|
||||||
const [[, fileinfo]] = await Promise.all([
|
|
||||||
document.getSub(domainId, document.TYPE_PROBLEM, pid, 'additional_file', name),
|
|
||||||
storage.put(`problem/${domainId}/${pid}/additional_file/${name}`, f),
|
|
||||||
]);
|
|
||||||
const meta = await storage.getMeta(`problem/${domainId}/${pid}/additional_file/${name}`);
|
|
||||||
const payload = { name, ...pick(meta, ['size', 'lastModified', 'etag']) };
|
|
||||||
if (!fileinfo) await push(domainId, pid, 'additional_file', { _id: name, ...payload });
|
|
||||||
else await document.setSub(domainId, document.TYPE_PROBLEM, pid, 'additional_file', name, payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function delAdditionalFile(domainId: string, pid: number, name: string | string[]) {
|
|
||||||
const names = (name instanceof Array) ? name : [name];
|
|
||||||
await storage.del(names.map((t) => `problem/${domainId}/${pid}/additional_file/${t}`));
|
|
||||||
await pull(domainId, pid, 'additional_file', names);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function random(domainId: string, query: FilterQuery<Pdoc>): Promise<string | null> {
|
|
||||||
const cursor = document.getMulti(domainId, document.TYPE_PROBLEM, query);
|
|
||||||
const pcount = await cursor.count();
|
|
||||||
if (pcount) {
|
|
||||||
const pdoc = await cursor.skip(Math.floor(Math.random() * pcount)).limit(1).toArray();
|
|
||||||
return pdoc[0].pid;
|
|
||||||
} return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getList(
|
|
||||||
domainId: string, pids: Array<number | string>,
|
|
||||||
getHidden = false, doThrow = true, projection = PROJECTION_PUBLIC,
|
|
||||||
): Promise<Pdict> {
|
|
||||||
pids = Array.from(new Set(pids)).map((i) => (+i ? +i : i));
|
|
||||||
const r = {};
|
|
||||||
const l = {};
|
|
||||||
const q: any = { $or: [{ docId: { $in: pids } }, { pid: { $in: pids } }] };
|
|
||||||
if (!getHidden) q.hidden = false;
|
|
||||||
const pdocs = await document.getMulti(domainId, document.TYPE_PROBLEM, q)
|
|
||||||
.project(buildProjection(projection)).toArray();
|
|
||||||
for (const pdoc of pdocs) {
|
|
||||||
r[pdoc.docId] = pdoc;
|
|
||||||
l[pdoc.pid] = pdoc;
|
|
||||||
}
|
}
|
||||||
// TODO enhance
|
|
||||||
if (pdocs.length !== pids.length) {
|
static getMultiStatus(domainId: string, query: FilterQuery<Pdoc>) {
|
||||||
for (const pid of pids) {
|
return document.getMultiStatus(domainId, document.TYPE_PROBLEM, query);
|
||||||
if (!(r[pid] || l[pid])) {
|
}
|
||||||
if (doThrow) throw new ProblemNotFoundError(domainId, pid);
|
|
||||||
else r[pid] = Pdoc.create(undefined, pid.toString());
|
static async edit(domainId: string, _id: number, $set: Partial<Pdoc>): Promise<Pdoc> {
|
||||||
|
const delpid = $set.pid === '';
|
||||||
|
if (delpid) delete $set.pid;
|
||||||
|
await bus.serial('problem/before-edit', $set);
|
||||||
|
const result = await document.set(domainId, document.TYPE_PROBLEM, _id, $set, delpid ? { pid: '' } : undefined);
|
||||||
|
await bus.emit('problem/edit', result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static push<T extends ArrayKeys<Pdoc>>(domainId: string, _id: number, key: ArrayKeys<Pdoc>, value: Pdoc[T][0]) {
|
||||||
|
return document.push(domainId, document.TYPE_PROBLEM, _id, key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
static pull<T extends ArrayKeys<Pdoc>>(domainId: string, pid: number, key: ArrayKeys<Pdoc>, values: Pdoc[T][0][]) {
|
||||||
|
return document.deleteSub(domainId, document.TYPE_PROBLEM, pid, key, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inc(domainId: string, _id: number, field: NumberKeys<Pdoc>, n: number): Promise<Pdoc> {
|
||||||
|
return document.inc(domainId, document.TYPE_PROBLEM, _id, field, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
static count(domainId: string, query: FilterQuery<Pdoc>) {
|
||||||
|
return document.count(domainId, document.TYPE_PROBLEM, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async del(domainId: string, docId: number) {
|
||||||
|
await bus.serial('problem/before-del', domainId, docId);
|
||||||
|
const res = await Promise.all([
|
||||||
|
document.deleteOne(domainId, document.TYPE_PROBLEM, docId),
|
||||||
|
document.deleteMultiStatus(domainId, document.TYPE_PROBLEM, { docId }),
|
||||||
|
storage.list(`problem/${domainId}/${docId}/`).then((items) => storage.del(items.map((item) => item.prefix + item.name))),
|
||||||
|
bus.parallel('problem/delete', domainId, docId),
|
||||||
|
]);
|
||||||
|
await bus.emit('problem/del', domainId, docId);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async addTestdata(domainId: string, pid: number, name: string, f: Readable | Buffer | string) {
|
||||||
|
if (!name) throw new ValidationError('name');
|
||||||
|
const [[, fileinfo]] = await Promise.all([
|
||||||
|
document.getSub(domainId, document.TYPE_PROBLEM, pid, 'data', name),
|
||||||
|
storage.put(`problem/${domainId}/${pid}/testdata/${name}`, f),
|
||||||
|
]);
|
||||||
|
const meta = await storage.getMeta(`problem/${domainId}/${pid}/testdata/${name}`);
|
||||||
|
if (!meta) throw new Error('Upload failed');
|
||||||
|
const payload = { name, ...pick(meta, ['size', 'lastModified', 'etag']) };
|
||||||
|
if (!fileinfo) await ProblemModel.push(domainId, pid, 'data', { _id: name, ...payload });
|
||||||
|
else await document.setSub(domainId, document.TYPE_PROBLEM, pid, 'data', name, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async delTestdata(domainId: string, pid: number, name: string | string[]) {
|
||||||
|
const names = (name instanceof Array) ? name : [name];
|
||||||
|
await storage.del(names.map((t) => `problem/${domainId}/${pid}/testdata/${t}`));
|
||||||
|
await ProblemModel.pull(domainId, pid, 'data', names);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async addAdditionalFile(domainId: string, pid: number, name: string, f: Readable | Buffer | string) {
|
||||||
|
const [[, fileinfo]] = await Promise.all([
|
||||||
|
document.getSub(domainId, document.TYPE_PROBLEM, pid, 'additional_file', name),
|
||||||
|
storage.put(`problem/${domainId}/${pid}/additional_file/${name}`, f),
|
||||||
|
]);
|
||||||
|
const meta = await storage.getMeta(`problem/${domainId}/${pid}/additional_file/${name}`);
|
||||||
|
const payload = { name, ...pick(meta, ['size', 'lastModified', 'etag']) };
|
||||||
|
if (!fileinfo) await ProblemModel.push(domainId, pid, 'additional_file', { _id: name, ...payload });
|
||||||
|
else await document.setSub(domainId, document.TYPE_PROBLEM, pid, 'additional_file', name, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async delAdditionalFile(domainId: string, pid: number, name: string | string[]) {
|
||||||
|
const names = (name instanceof Array) ? name : [name];
|
||||||
|
await storage.del(names.map((t) => `problem/${domainId}/${pid}/additional_file/${t}`));
|
||||||
|
await ProblemModel.pull(domainId, pid, 'additional_file', names);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async random(domainId: string, query: FilterQuery<Pdoc>): Promise<string | null> {
|
||||||
|
const cursor = document.getMulti(domainId, document.TYPE_PROBLEM, query);
|
||||||
|
const pcount = await cursor.count();
|
||||||
|
if (pcount) {
|
||||||
|
const pdoc = await cursor.skip(Math.floor(Math.random() * pcount)).limit(1).toArray();
|
||||||
|
return pdoc[0].pid;
|
||||||
|
} return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getList(
|
||||||
|
domainId: string, pids: Array<number | string>,
|
||||||
|
getHidden = false, doThrow = true, projection = ProblemModel.PROJECTION_PUBLIC,
|
||||||
|
): Promise<Pdict> {
|
||||||
|
pids = Array.from(new Set(pids)).map((i) => (+i ? +i : i));
|
||||||
|
const r = {};
|
||||||
|
const l = {};
|
||||||
|
const q: any = { $or: [{ docId: { $in: pids } }, { pid: { $in: pids } }] };
|
||||||
|
if (!getHidden) q.hidden = false;
|
||||||
|
const pdocs = await document.getMulti(domainId, document.TYPE_PROBLEM, q)
|
||||||
|
.project(buildProjection(projection)).toArray();
|
||||||
|
for (const pdoc of pdocs) {
|
||||||
|
r[pdoc.docId] = pdoc;
|
||||||
|
l[pdoc.pid] = pdoc;
|
||||||
|
}
|
||||||
|
// TODO enhance
|
||||||
|
if (pdocs.length !== pids.length) {
|
||||||
|
for (const pid of pids) {
|
||||||
|
if (!(r[pid] || l[pid])) {
|
||||||
|
if (doThrow) throw new ProblemNotFoundError(domainId, pid);
|
||||||
|
else r[pid] = ProblemModel.create(undefined, pid.toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return Object.assign(r, l);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getListStatus(domainId: string, uid: number, pids: number[]) {
|
||||||
|
const psdocs = await ProblemModel.getMultiStatus(
|
||||||
|
domainId, { uid, docId: { $in: Array.from(new Set(pids)) } },
|
||||||
|
).toArray();
|
||||||
|
const r: Dictionary<ProblemStatusDoc> = {};
|
||||||
|
for (const psdoc of psdocs) r[psdoc.docId] = psdoc;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async updateStatus(
|
||||||
|
domainId: string, pid: number, uid: number,
|
||||||
|
rid: ObjectID, status: number,
|
||||||
|
) {
|
||||||
|
const res = await document.setIfNotStatus(
|
||||||
|
domainId, document.TYPE_PROBLEM, pid, uid,
|
||||||
|
'status', status, STATUS.STATUS_ACCEPTED, { rid },
|
||||||
|
);
|
||||||
|
return !!res;
|
||||||
|
}
|
||||||
|
|
||||||
|
static setStar(domainId: string, pid: number, uid: number, star: boolean) {
|
||||||
|
return document.setStatus(domainId, document.TYPE_PROBLEM, pid, uid, { star });
|
||||||
}
|
}
|
||||||
return Object.assign(r, l);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getListStatus(domainId: string, uid: number, pids: number[]) {
|
ProblemModel.extend((docId, pid) => ({
|
||||||
const psdocs = await getMultiStatus(
|
_id: new ObjectID(),
|
||||||
domainId, { uid, docId: { $in: Array.from(new Set(pids)) } },
|
domainId: 'system',
|
||||||
).toArray();
|
docType: document.TYPE_PROBLEM,
|
||||||
const r: Dictionary<ProblemStatusDoc> = {};
|
docId: docId || -1,
|
||||||
for (const psdoc of psdocs) r[psdoc.docId] = psdoc;
|
pid: pid || docId.toString(),
|
||||||
return r;
|
owner: 1,
|
||||||
}
|
title: '*',
|
||||||
|
content: '',
|
||||||
|
html: false,
|
||||||
|
nSubmit: 0,
|
||||||
|
nAccept: 0,
|
||||||
|
tag: [],
|
||||||
|
data: [],
|
||||||
|
additional_file: [],
|
||||||
|
hidden: true,
|
||||||
|
config: {},
|
||||||
|
acMsg: '',
|
||||||
|
difficulty: 0,
|
||||||
|
}));
|
||||||
|
|
||||||
export async function updateStatus(
|
global.Hydro.model.problem = ProblemModel;
|
||||||
domainId: string, pid: number, uid: number,
|
export default ProblemModel;
|
||||||
rid: ObjectID, status: number,
|
|
||||||
) {
|
|
||||||
const res = await document.setIfNotStatus(
|
|
||||||
domainId, document.TYPE_PROBLEM, pid, uid,
|
|
||||||
'status', status, STATUS.STATUS_ACCEPTED, { rid },
|
|
||||||
);
|
|
||||||
return !!res;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setStar(domainId: string, pid: number, uid: number, star: boolean) {
|
|
||||||
return document.setStatus(domainId, document.TYPE_PROBLEM, pid, uid, { star });
|
|
||||||
}
|
|
||||||
|
|
||||||
global.Hydro.model.problem = {
|
|
||||||
Pdoc,
|
|
||||||
|
|
||||||
PROJECTION_LIST,
|
|
||||||
PROJECTION_PUBLIC,
|
|
||||||
|
|
||||||
add,
|
|
||||||
inc,
|
|
||||||
get,
|
|
||||||
del,
|
|
||||||
edit,
|
|
||||||
count,
|
|
||||||
push,
|
|
||||||
pull,
|
|
||||||
random,
|
|
||||||
addTestdata,
|
|
||||||
delTestdata,
|
|
||||||
addAdditionalFile,
|
|
||||||
delAdditionalFile,
|
|
||||||
getMulti,
|
|
||||||
getList,
|
|
||||||
getListStatus,
|
|
||||||
getMultiStatus,
|
|
||||||
setStar,
|
|
||||||
updateStatus,
|
|
||||||
};
|
|
||||||
@@ -5,7 +5,7 @@ import { Dictionary } from 'lodash';
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { STATUS } from './builtin';
|
import { STATUS } from './builtin';
|
||||||
import task from './task';
|
import task from './task';
|
||||||
import * as problem from './problem';
|
import problem from './problem';
|
||||||
import { Rdoc, ContestInfo, ProblemConfig } from '../interface';
|
import { Rdoc, ContestInfo, ProblemConfig } from '../interface';
|
||||||
import { ArgMethod, Time } from '../utils';
|
import { ArgMethod, Time } from '../utils';
|
||||||
import * as bus from '../service/bus';
|
import * as bus from '../service/bus';
|
||||||
@@ -166,5 +166,5 @@ class RecordModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bus.on('problem/delete', (domainId, docId) => RecordModel.coll.deleteMany({ domainId, pid: docId }));
|
bus.on('problem/delete', (domainId, docId) => RecordModel.coll.deleteMany({ domainId, pid: docId }));
|
||||||
export = RecordModel;
|
export default RecordModel;
|
||||||
global.Hydro.model.record = RecordModel;
|
global.Hydro.model.record = RecordModel;
|
||||||
@@ -92,5 +92,5 @@ bus.on('problem/delete', async (domainId, docId) => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
export = SolutionModel;
|
export default SolutionModel;
|
||||||
global.Hydro.model.solution = SolutionModel;
|
global.Hydro.model.solution = SolutionModel;
|
||||||
@@ -97,5 +97,5 @@ class TaskModel {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export = TaskModel;
|
export default TaskModel;
|
||||||
global.Hydro.model.task = TaskModel;
|
global.Hydro.model.task = TaskModel;
|
||||||
@@ -101,5 +101,5 @@ function ensureIndexes() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bus.once('app/started', ensureIndexes);
|
bus.once('app/started', ensureIndexes);
|
||||||
export = TokenModel;
|
export default TokenModel;
|
||||||
global.Hydro.model.token = TokenModel;
|
global.Hydro.model.token = TokenModel;
|
||||||
@@ -271,5 +271,5 @@ function ensureIndexes() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bus.once('app/started', ensureIndexes);
|
bus.once('app/started', ensureIndexes);
|
||||||
export = UserModel;
|
export default UserModel;
|
||||||
global.Hydro.model.user = UserModel;
|
global.Hydro.model.user = UserModel;
|
||||||
@@ -4,7 +4,7 @@ import { ObjectID, FilterQuery } from 'mongodb';
|
|||||||
import { Tdoc, Pdoc, Udoc } from '../interface';
|
import { Tdoc, Pdoc, Udoc } from '../interface';
|
||||||
import domain from '../model/domain';
|
import domain from '../model/domain';
|
||||||
import * as contest from '../model/contest';
|
import * as contest from '../model/contest';
|
||||||
import * as problem from '../model/problem';
|
import problem from '../model/problem';
|
||||||
import record from '../model/record';
|
import record from '../model/record';
|
||||||
import { STATUS } from '../model/builtin';
|
import { STATUS } from '../model/builtin';
|
||||||
import rating from '../lib/rating';
|
import rating from '../lib/rating';
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export interface EventMap extends Record<string, any> {
|
|||||||
|
|
||||||
'system/setting': (args: Record<string, any>) => VoidReturn
|
'system/setting': (args: Record<string, any>) => VoidReturn
|
||||||
|
|
||||||
'monitor/update': (type: 'server' | 'judger', $set: any) => VoidReturn
|
'monitor/update': (type: 'server' | 'judge', $set: any) => VoidReturn
|
||||||
|
|
||||||
'user/message': (uid: number, mdoc: Mdoc, udoc: User) => void
|
'user/message': (uid: number, mdoc: Mdoc, udoc: User) => void
|
||||||
'user/get': (udoc: User) => void
|
'user/get': (udoc: User) => void
|
||||||
@@ -76,10 +76,13 @@ export interface EventMap extends Record<string, any> {
|
|||||||
ip: string | null, highlight: boolean, pin: boolean
|
ip: string | null, highlight: boolean, pin: boolean
|
||||||
) => VoidReturn
|
) => VoidReturn
|
||||||
|
|
||||||
|
'problem/before-add': (doc: Partial<Pdoc>) => VoidReturn
|
||||||
|
'problem/add': (doc: Partial<Pdoc>, docId: number) => VoidReturn
|
||||||
'problem/before-edit': (doc: Partial<Pdoc>) => VoidReturn
|
'problem/before-edit': (doc: Partial<Pdoc>) => VoidReturn
|
||||||
'problem/edit': (doc: Pdoc) => VoidReturn
|
'problem/edit': (doc: Pdoc) => VoidReturn
|
||||||
|
'problem/before-del': (domainId: string, docId: number) => VoidReturn
|
||||||
|
'problem/del': (domainId: string, docId: number) => VoidReturn
|
||||||
'problem/list': (query: FilterQuery<Pdoc>, handler: any) => VoidReturn
|
'problem/list': (query: FilterQuery<Pdoc>, handler: any) => VoidReturn
|
||||||
'problem/setting': (update: Partial<Pdoc>, handler: any) => VoidReturn
|
|
||||||
'problem/get': (doc: Pdoc, handler: any) => VoidReturn
|
'problem/get': (doc: Pdoc, handler: any) => VoidReturn
|
||||||
'problem/delete': (domainId: string, docId: number) => VoidReturn
|
'problem/delete': (domainId: string, docId: number) => VoidReturn
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Collection, Db, MongoClient } from 'mongodb';
|
import { Collection, Db, MongoClient } from 'mongodb';
|
||||||
import * as bus from './bus';
|
import * as bus from './bus';
|
||||||
import { Collections } from '../interface';
|
import { Collections, BaseService } from '../interface';
|
||||||
|
|
||||||
interface MongoConfig {
|
interface MongoConfig {
|
||||||
protocol?: string,
|
protocol?: string,
|
||||||
@@ -13,11 +13,12 @@ interface MongoConfig {
|
|||||||
prefix?: string,
|
prefix?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
class MongoService {
|
class MongoService implements BaseService {
|
||||||
public client: MongoClient;
|
public client: MongoClient;
|
||||||
public client2: MongoClient;
|
public client2: MongoClient;
|
||||||
public db: Db;
|
public db: Db;
|
||||||
public db2: Db;
|
public db2: Db;
|
||||||
|
public started = false;
|
||||||
private opts: MongoConfig;
|
private opts: MongoConfig;
|
||||||
|
|
||||||
static buildUrl(opts: MongoConfig) {
|
static buildUrl(opts: MongoConfig) {
|
||||||
@@ -35,7 +36,8 @@ class MongoService {
|
|||||||
this.db = this.client.db(opts.name);
|
this.db = this.client.db(opts.name);
|
||||||
this.client2 = await MongoClient.connect(mongourl, { useNewUrlParser: true, useUnifiedTopology: true });
|
this.client2 = await MongoClient.connect(mongourl, { useNewUrlParser: true, useUnifiedTopology: true });
|
||||||
this.db2 = this.client2.db(opts.name);
|
this.db2 = this.client2.db(opts.name);
|
||||||
bus.parallel('database/connect', this.db);
|
await bus.parallel('database/connect', this.db);
|
||||||
|
this.started = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public collection<K extends keyof Collections>(c: K): Collection<Collections[K]> {
|
public collection<K extends keyof Collections>(c: K): Collection<Collections[K]> {
|
||||||
|
|||||||
@@ -70,11 +70,11 @@ export async function update() {
|
|||||||
global.Hydro.stat.reqCount = 0;
|
global.Hydro.stat.reqCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateJudger(args) {
|
export async function updateJudge(args) {
|
||||||
const $set = { ...args, updateAt: new Date() };
|
const $set = { ...args, updateAt: new Date() };
|
||||||
await bus.serial('monitor/update', 'judger', $set);
|
await bus.serial('monitor/update', 'judge', $set);
|
||||||
return await coll.updateOne(
|
return await coll.updateOne(
|
||||||
{ mid: args.mid, type: 'judger' },
|
{ mid: args.mid, type: 'judge' },
|
||||||
{ $set },
|
{ $set },
|
||||||
{ upsert: true },
|
{ upsert: true },
|
||||||
);
|
);
|
||||||
@@ -93,4 +93,4 @@ if (cluster.isMaster) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
global.Hydro.service.monitor = { feedback, update, updateJudger };
|
global.Hydro.service.monitor = { feedback, update, updateJudge };
|
||||||
@@ -210,6 +210,7 @@ type DescriptorBuilder =
|
|||||||
& ((name: string, ...args: Array<Type | boolean | Validator | Converter>) => MethodDecorator);
|
& ((name: string, ...args: Array<Type | boolean | Validator | Converter>) => MethodDecorator);
|
||||||
|
|
||||||
export const get: DescriptorBuilder = (name, ...args) => _descriptor(_buildParam(name, 'get', ...args));
|
export const get: DescriptorBuilder = (name, ...args) => _descriptor(_buildParam(name, 'get', ...args));
|
||||||
|
export const query: DescriptorBuilder = (name, ...args) => _descriptor(_buildParam(name, 'get', ...args));
|
||||||
export const post: DescriptorBuilder = (name, ...args) => _descriptor(_buildParam(name, 'post', ...args));
|
export const post: DescriptorBuilder = (name, ...args) => _descriptor(_buildParam(name, 'post', ...args));
|
||||||
export const route: DescriptorBuilder = (name, ...args) => _descriptor(_buildParam(name, 'route', ...args));
|
export const route: DescriptorBuilder = (name, ...args) => _descriptor(_buildParam(name, 'route', ...args));
|
||||||
export const param: DescriptorBuilder = (name, ...args) => _descriptor(_buildParam(name, 'all', ...args));
|
export const param: DescriptorBuilder = (name, ...args) => _descriptor(_buildParam(name, 'all', ...args));
|
||||||
@@ -319,6 +320,7 @@ export class HandlerCommon {
|
|||||||
url(name: string, kwargs: any = {}) {
|
url(name: string, kwargs: any = {}) {
|
||||||
let res = '#';
|
let res = '#';
|
||||||
const args: any = {};
|
const args: any = {};
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
const query: any = {};
|
const query: any = {};
|
||||||
for (const key in kwargs) {
|
for (const key in kwargs) {
|
||||||
if (kwargs[key] instanceof ObjectID) args[key] = kwargs[key].toHexString();
|
if (kwargs[key] instanceof ObjectID) args[key] = kwargs[key].toHexString();
|
||||||
@@ -803,12 +805,16 @@ export function Connection(
|
|||||||
sock.installHandlers(server);
|
sock.installHandlers(server);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let started = false;
|
||||||
|
|
||||||
// TODO use postInit?
|
// TODO use postInit?
|
||||||
export function start() {
|
export function start() {
|
||||||
|
if (started) return;
|
||||||
const port = system.get('server.port');
|
const port = system.get('server.port');
|
||||||
app.use(router.routes()).use(router.allowedMethods());
|
app.use(router.routes()).use(router.allowedMethods());
|
||||||
server.listen(argv.port || port);
|
server.listen(argv.port || port);
|
||||||
logger.success('Server listening at: %d', argv.port || port);
|
logger.success('Server listening at: %d', argv.port || port);
|
||||||
|
started = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
global.Hydro.service.server = {
|
global.Hydro.service.server = {
|
||||||
@@ -818,6 +824,7 @@ global.Hydro.service.server = {
|
|||||||
router,
|
router,
|
||||||
UiContextBase,
|
UiContextBase,
|
||||||
get,
|
get,
|
||||||
|
query,
|
||||||
post,
|
post,
|
||||||
route,
|
route,
|
||||||
param,
|
param,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { streamToBuffer } from './utils';
|
|||||||
import gridfs from './service/gridfs';
|
import gridfs from './service/gridfs';
|
||||||
import storage from './service/storage';
|
import storage from './service/storage';
|
||||||
import db from './service/db';
|
import db from './service/db';
|
||||||
import * as problem from './model/problem';
|
import problem, { Field, Pdoc } from './model/problem';
|
||||||
import domain from './model/domain';
|
import domain from './model/domain';
|
||||||
import * as document from './model/document';
|
import * as document from './model/document';
|
||||||
import * as system from './model/system';
|
import * as system from './model/system';
|
||||||
@@ -25,13 +25,13 @@ export async function iterateAllDomain(cb: (ddoc: DomainDoc, current?: number, t
|
|||||||
for (const i in ddocs) await cb(ddocs[i], +i, ddocs.length);
|
for (const i in ddocs) await cb(ddocs[i], +i, ddocs.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PartialPdoc extends problem.Pdoc {
|
interface PartialPdoc extends Pdoc {
|
||||||
[key: string]: any,
|
[key: string]: any,
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function iterateAllProblemInDomain(
|
export async function iterateAllProblemInDomain(
|
||||||
domainId: string,
|
domainId: string,
|
||||||
fields: (problem.Pdoc.Field | string)[],
|
fields: (Field | string)[],
|
||||||
cb: (pdoc: PartialPdoc, current?: number, total?: number) => Promise<any>,
|
cb: (pdoc: PartialPdoc, current?: number, total?: number) => Promise<any>,
|
||||||
) {
|
) {
|
||||||
if (!fields.includes('domainId')) fields.push('domainId');
|
if (!fields.includes('domainId')) fields.push('domainId');
|
||||||
@@ -48,7 +48,7 @@ export async function iterateAllProblemInDomain(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function iterateAllProblem(
|
export async function iterateAllProblem(
|
||||||
fields: (problem.Pdoc.Field | string)[],
|
fields: (Field | string)[],
|
||||||
cb: (pdoc: PartialPdoc, current?: number, total?: number) => Promise<any>,
|
cb: (pdoc: PartialPdoc, current?: number, total?: number) => Promise<any>,
|
||||||
) {
|
) {
|
||||||
await iterateAllDomain(async (d) => {
|
await iterateAllDomain(async (d) => {
|
||||||
@@ -171,7 +171,7 @@ const scripts: UpgradeScript[] = [
|
|||||||
logger.info('%s/%s', pdoc.domainId, pdoc.docId);
|
logger.info('%s/%s', pdoc.domainId, pdoc.docId);
|
||||||
const [data, additional_file] = await Promise.all([
|
const [data, additional_file] = await Promise.all([
|
||||||
storage.list(`problem/${pdoc.domainId}/${pdoc.docId}/testdata/`),
|
storage.list(`problem/${pdoc.domainId}/${pdoc.docId}/testdata/`),
|
||||||
storage.list(`problem/${pdoc.domainId}/${pdoc.docId}/additional_files/`),
|
storage.list(`problem/${pdoc.domainId}/${pdoc.docId}/additional_file/`),
|
||||||
]);
|
]);
|
||||||
await problem.edit(pdoc.domainId, pdoc.docId, { data, additional_file });
|
await problem.edit(pdoc.domainId, pdoc.docId, { data, additional_file });
|
||||||
if (!pdoc.config) return;
|
if (!pdoc.config) return;
|
||||||
@@ -187,7 +187,7 @@ const scripts: UpgradeScript[] = [
|
|||||||
logger.info('%s/%s', pdoc.domainId, pdoc.docId);
|
logger.info('%s/%s', pdoc.domainId, pdoc.docId);
|
||||||
const [data, additional_file] = await Promise.all([
|
const [data, additional_file] = await Promise.all([
|
||||||
storage.list(`problem/${pdoc.domainId}/${pdoc.docId}/testdata/`),
|
storage.list(`problem/${pdoc.domainId}/${pdoc.docId}/testdata/`),
|
||||||
storage.list(`problem/${pdoc.domainId}/${pdoc.docId}/additional_files/`),
|
storage.list(`problem/${pdoc.domainId}/${pdoc.docId}/additional_file/`),
|
||||||
]) as any;
|
]) as any;
|
||||||
for (let i = 0; i < data.length; i++) {
|
for (let i = 0; i < data.length; i++) {
|
||||||
data[i]._id = data[i].name;
|
data[i]._id = data[i].name;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import type { ContentNode, ProblemConfig } from 'hydrooj';
|
|||||||
import { Route, Handler } from 'hydrooj/dist/service/server';
|
import { Route, Handler } from 'hydrooj/dist/service/server';
|
||||||
import { BadRequestError, ValidationError } from 'hydrooj/dist/error';
|
import { BadRequestError, ValidationError } from 'hydrooj/dist/error';
|
||||||
import { ProblemAdd } from 'hydrooj/dist/lib/ui';
|
import { ProblemAdd } from 'hydrooj/dist/lib/ui';
|
||||||
import * as problem from 'hydrooj/dist/model/problem';
|
import problem from 'hydrooj/dist/model/problem';
|
||||||
import { PERM } from 'hydrooj/dist/model/builtin';
|
import { PERM } from 'hydrooj/dist/model/builtin';
|
||||||
|
|
||||||
fs.ensureDirSync('/tmp/hydro/import-qduoj');
|
fs.ensureDirSync('/tmp/hydro/import-qduoj');
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import * as bus from 'hydrooj/dist/service/bus';
|
||||||
|
import * as system from 'hydrooj/dist/model/system';
|
||||||
|
import sonic from './service';
|
||||||
|
|
||||||
|
bus.on('problem/add', async (doc, docId) => {
|
||||||
|
await sonic.push('problem', `${doc.domainId}@title`, docId.toString(), doc.title);
|
||||||
|
await sonic.push('problem', `${doc.domainId}@content`, docId.toString(), doc.content.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
bus.on('problem/edit', async (pdoc) => {
|
||||||
|
await sonic.flusho('problem', `${pdoc.domainId}@title`, pdoc.docId.toString());
|
||||||
|
await sonic.flusho('problem', `${pdoc.domainId}@content`, pdoc.docId.toString());
|
||||||
|
await sonic.push('problem', `${pdoc.domainId}@title`, pdoc.docId.toString(), pdoc.title);
|
||||||
|
await sonic.push('problem', `${pdoc.domainId}@content`, pdoc.docId.toString(), pdoc.content.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
bus.on('problem/del', async (domainId, docId) => {
|
||||||
|
await sonic.flusho('problem', `${domainId}@title`, docId.toString());
|
||||||
|
await sonic.flusho('problem', `${domainId}@content`, docId.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
global.Hydro.lib.problemSearch = async (domainId: string, query: string) => {
|
||||||
|
const c = system.get('pagination.problem');
|
||||||
|
const docIds = await sonic.query('problem', `${domainId}@title`, query, { limit: c });
|
||||||
|
docIds.push(...await sonic.query('problem', `${domainId}@content`, query, { limit: c - docIds.length }));
|
||||||
|
return docIds.map(Number);
|
||||||
|
};
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "@hydrooj/sonic",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Sonic search service",
|
||||||
|
"main": "service.js",
|
||||||
|
"typings": "service.d.ts",
|
||||||
|
"repository": "https://github.com/hydro-dev/Hydro.git",
|
||||||
|
"author": "undefined <i@undefined.moe>",
|
||||||
|
"license": "AGPL-3.0-only",
|
||||||
|
"dependencies": {
|
||||||
|
"sonic-channel": "^1.2.6"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { iterateAllProblem, iterateAllProblemInDomain } from 'hydrooj/dist/upgrade';
|
||||||
|
import sonic from './service';
|
||||||
|
|
||||||
|
export const description = 'Sonic problem search re-index';
|
||||||
|
|
||||||
|
export async function run({ domainId }) {
|
||||||
|
if (domainId) await sonic.flushb('problem', domainId);
|
||||||
|
else await sonic.flushc('problem');
|
||||||
|
const cb = async (pdoc) => {
|
||||||
|
await sonic.push('problem', `${pdoc.domainId}@title`, pdoc.docId.toString(), pdoc.title);
|
||||||
|
await sonic.push('problem', `${pdoc.domainId}@content`, pdoc.docId.toString(), pdoc.content.toString());
|
||||||
|
};
|
||||||
|
if (domainId) await iterateAllProblemInDomain(domainId, ['title', 'content'], cb);
|
||||||
|
else await iterateAllProblem(['title', 'content'], cb);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const validate = {
|
||||||
|
$or: [
|
||||||
|
{ domainId: 'string' },
|
||||||
|
{ domainId: 'undefined' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
global.Hydro.script.ensureSonicSearch = { run, description, validate };
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import { BaseService } from 'hydrooj';
|
||||||
|
import * as system from 'hydrooj/dist/model/system';
|
||||||
|
import {
|
||||||
|
Search, Ingest, QueryOptions, SuggestOptions, PushOptions,
|
||||||
|
} from 'sonic-channel';
|
||||||
|
import { Logger } from 'hydrooj/dist/logger';
|
||||||
|
|
||||||
|
const logger = new Logger('sonic');
|
||||||
|
|
||||||
|
declare module 'hydrooj/dist/interface' {
|
||||||
|
interface SystemKeys {
|
||||||
|
'sonic.host': string;
|
||||||
|
'sonic.port': number;
|
||||||
|
'sonic.auth': string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHandler(type: string, that: any) {
|
||||||
|
return {
|
||||||
|
connected() {
|
||||||
|
that.started = true;
|
||||||
|
logger.info(`Sonic Channel succeeded to connect to host (${type}).`);
|
||||||
|
},
|
||||||
|
disconnected() {
|
||||||
|
that.started = false;
|
||||||
|
logger.error(`Sonic Channel is now disconnected (${type}).`);
|
||||||
|
},
|
||||||
|
timeout() {
|
||||||
|
logger.error(`Sonic Channel connection timed out (${type}).`);
|
||||||
|
},
|
||||||
|
retrying() {
|
||||||
|
logger.error(`Trying to reconnect to Sonic Channel (${type})...`);
|
||||||
|
},
|
||||||
|
error(error) {
|
||||||
|
that.error = error;
|
||||||
|
logger.error(`Sonic Channel failed to connect to host (${type}).`, error);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class SonicService implements BaseService {
|
||||||
|
public search: Search;
|
||||||
|
public ingest: Ingest;
|
||||||
|
public started = false;
|
||||||
|
public error = '';
|
||||||
|
|
||||||
|
async start() {
|
||||||
|
const [host, port, auth] = system.getMany(['sonic.host', 'sonic.port', 'sonic.auth']);
|
||||||
|
const cfg = {
|
||||||
|
host: host || '127.0.0.1',
|
||||||
|
port: port || 1491,
|
||||||
|
auth: auth || 'sonic-passwd',
|
||||||
|
};
|
||||||
|
this.search = new Search(cfg);
|
||||||
|
this.ingest = new Ingest(cfg);
|
||||||
|
await this.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
async connect() {
|
||||||
|
try {
|
||||||
|
this.search.connect(getHandler('search', this));
|
||||||
|
this.ingest.connect(getHandler('ingest', this));
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('Sonic init fail. will retry later.');
|
||||||
|
this.error = e.toString();
|
||||||
|
setTimeout(() => this.start(), 10000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async query(collection: string, bucket: string, terms: string, options?: QueryOptions) {
|
||||||
|
return await this.search.query(collection, bucket, terms, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async suggest(collection: string, bucket: string, word: string, options?: SuggestOptions) {
|
||||||
|
return await this.search.suggest(collection, bucket, word, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async push(collection: string, bucket: string, object: string, text: string, options?: PushOptions) {
|
||||||
|
return await this.ingest.push(collection, bucket, object, text, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async pop(collection: string, bucket: string, object: string, text: string) {
|
||||||
|
return await this.ingest.pop(collection, bucket, object, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
async count(collection: string, bucket?: string, object?: string) {
|
||||||
|
return await this.ingest.count(collection, bucket, object);
|
||||||
|
}
|
||||||
|
|
||||||
|
async flusho(collection: string, bucket: string, object: string) {
|
||||||
|
return await this.ingest.flusho(collection, bucket, object);
|
||||||
|
}
|
||||||
|
|
||||||
|
async flushb(collection: string, bucket: string) {
|
||||||
|
return await this.ingest.flushb(collection, bucket);
|
||||||
|
}
|
||||||
|
|
||||||
|
async flushc(collection: string) {
|
||||||
|
return await this.ingest.flushc(collection);
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop() {
|
||||||
|
await Promise.all([
|
||||||
|
this.search.close(),
|
||||||
|
this.ingest.close(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const service = new SonicService();
|
||||||
|
global.Hydro.service.sonic = service;
|
||||||
|
export = service;
|
||||||
|
declare module 'hydrooj/dist/interface' {
|
||||||
|
interface Service {
|
||||||
|
sonic: typeof service
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
host:
|
||||||
|
value: localhost
|
||||||
|
port:
|
||||||
|
type: number
|
||||||
|
value: 1491
|
||||||
|
auth:
|
||||||
|
type: text
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { FilterQuery } from 'mongodb';
|
import { FilterQuery } from 'mongodb';
|
||||||
import { Pdoc } from 'hydrooj/dist/interface';
|
import { Pdoc } from 'hydrooj/dist/interface';
|
||||||
import * as problem from 'hydrooj/dist/model/problem';
|
import problem from 'hydrooj/dist/model/problem';
|
||||||
import record from 'hydrooj/dist/model/record';
|
import record from 'hydrooj/dist/model/record';
|
||||||
import user from 'hydrooj/dist/model/user';
|
import user from 'hydrooj/dist/model/user';
|
||||||
import { PERM, PRIV } from 'hydrooj/dist/model/builtin';
|
import { PERM, PRIV } from 'hydrooj/dist/model/builtin';
|
||||||
|
|||||||
Reference in new issue
Block a user