优化安装流程

This commit is contained in:
masnn committed 2020-06-07 13:59:22 +08:00
1 parent 63d55cedf4
commit 508f5644c3
30 files changed
+279 -139

No files matched your search

+1 -3
View File
@@ -1,7 +1,5 @@
FROM mhart/alpine-node:slim-10
WORKDIR /app
ADD .uibuild .build /app/
RUN mkdir /config
VOLUME [ "/config" ]
ADD .build /app
EXPOSE 8888
CMD ["node", ".build/development.js"]
+2 -5
View File
@@ -56,14 +56,11 @@ async function build(type) {
rmdir(root(`.build/module/${f}`));
}
}
const modules = [
'builtin', 'contest-rule-homework', 'judger', 'migrate-vijos', 'module-pastebin',
'problem-import-syzoj', 'wiki',
];
const modules = await fsp.readdir(root('.build/module'));
const j = {};
for (const m of modules) {
try {
const d = await fsp.readFile(root(`.build/module/${m}.hydro`));
const d = await fsp.readFile(root(`.build/module/${m}`));
j[m] = d.toString('base64');
} catch (e) {
console.error(`Module pack failed: ${m}`);
+2 -2
View File
@@ -163,9 +163,9 @@ class RoleAlreadyExistError extends ForbiddenError {
}
class ProblemNotFoundError extends NotFoundError {
constructor(pid) {
constructor(domainId, pid) {
super('ProblemNotFoundError');
this.params = [pid];
this.params = [domainId, pid];
}
}
+34 -22
View File
@@ -1,7 +1,9 @@
const { ObjectID } = require('bson');
const paginate = require('../lib/paginate');
const system = require('../model/system');
const user = require('../model/user');
const discussion = require('../model/discussion');
const document = require('../model/document');
const { Route, Handler } = require('../service/server');
const { DiscussionNotFoundError, DocumentNotFoundError } = require('../error');
const {
@@ -10,17 +12,24 @@ const {
PERM_LOGGEDIN, PERM_CREATE_DISCUSSION, PERM_REPLY_DISCUSSION,
} = require('../permission');
const typeMapper = {
problem: document.TYPE_PROBLEM,
contest: document.TYPE_CONTEST,
node: document.TYPE_DISCUSSION_NODE,
training: document.TYPE_TRAINING,
homework: document.TYPE_HOMEWORK,
};
class DiscussionHandler extends Handler {
async _prepare({
domainId, type = 'node', docId, name, did, drid, drrid,
domainId, type, name, did, drid, drrid,
}) {
this.checkPerm(PERM_VIEW_DISCUSSION);
docId = docId || name;
if (did) {
this.ddoc = await discussion.get(domainId, did);
if (!this.ddoc) throw new DiscussionNotFoundError(did);
type = this.ddoc.parentType;
docId = this.ddoc.parentId;
type = discussion.typeDisplay[this.ddoc.parentType];
name = this.ddoc.parentId;
if (drid) {
this.drdoc = await discussion.getReply(domainId, drid, did);
if (!this.drdoc) throw new DiscussionNotFoundError(drid);
@@ -37,7 +46,11 @@ class DiscussionHandler extends Handler {
// TODO(twd2): do more visibility check eg. contest
// TODO(twd2): exclude problem/contest discussions?
// TODO(iceboy): continuation based pagination.
this.vnode = await discussion.getVnode(domainId, { parentType: type, parentId: docId }, this);
if (ObjectID.isValid(name)) name = new ObjectID(name);
this.vnode = await discussion.getVnode(domainId, {
parentType: typeMapper[type],
parentId: name,
}, this);
if (this.ddoc) {
this.ddoc.parentType = this.ddoc.parentType || this.vnode.type;
this.ddoc.parentId = this.ddoc.parentId || this.vnode.id;
@@ -45,7 +58,7 @@ class DiscussionHandler extends Handler {
}
}
class DiscussionMainHandler extends DiscussionHandler {
class DiscussionMainHandler extends Handler {
async get({ domainId, page = 1 }) {
const [ddocs, dpcount] = await paginate(
discussion.getMulti(domainId),
@@ -57,20 +70,20 @@ class DiscussionMainHandler extends DiscussionHandler {
['Hydro', '/'],
['discussion_main', null],
];
const vndict = await discussion.getListVnodes(domainId, ddocs, this);
this.response.template = 'discussion_main_or_node.html';
this.response.body = {
ddocs, dpcount, udict, page, page_name: 'discussion_main', vnode: {}, path,
ddocs, dpcount, udict, page, page_name: 'discussion_main', vndict, vnode: {}, path,
};
}
}
class DiscussionNodeHandler extends DiscussionHandler {
async get({
domainId, type = 'node', docId, name, page = 1,
domainId, type, name, page = 1,
}) {
docId = docId || name;
const [ddocs, dpcount] = await paginate(
discussion.getMulti(domainId, { parentType: type, parentId: docId }),
discussion.getMulti(domainId, { parentType: typeMapper[type], parentId: name }),
page,
await system.get('DISCUSSION_PER_PAGE'),
);
@@ -80,6 +93,7 @@ class DiscussionNodeHandler extends DiscussionHandler {
['discussion_main', '/discuss'],
[this.vnode.title, null, true],
];
const vndict = { [typeMapper[type]]: { [name]: this.vnode } };
this.response.template = 'discussion_main_or_node.html';
this.response.body = {
ddocs,
@@ -87,6 +101,7 @@ class DiscussionNodeHandler extends DiscussionHandler {
udict,
path,
page,
vndict,
vnode: this.vnode,
page_name: 'discussion_node',
};
@@ -99,12 +114,11 @@ class DiscussionCreateHandler extends DiscussionHandler {
this.checkPerm(PERM_CREATE_DISCUSSION);
}
async get({ type = 'node', docId, name }) {
docId = docId || name;
async get({ type, name }) {
const path = [
['Hydro', '/'],
['discussion_main', '/discuss'],
[this.vnode.title, `/discuss/${type}/${docId}`, true],
[this.vnode.title, `/discuss/${type}/${name}`, true],
['discussion_create', null],
];
this.response.template = 'discussion_create.html';
@@ -112,13 +126,13 @@ class DiscussionCreateHandler extends DiscussionHandler {
}
async post({
domainId, type = 'node', docId, name, title, content, highlight,
domainId, type, name, title, content, highlight,
}) {
docId = docId || name;
this.limitRate('add_discussion', 3600, 30);
if (ObjectID.isValid(name)) name = new ObjectID(name);
if (highlight) this.checkPerm(PERM_HIGHLIGHT_DISCUSSION);
const did = await discussion.add(
domainId, type, docId, this.user._id,
domainId, typeMapper[type], name, this.user._id,
title, content, this.request.ip, highlight,
);
this.response.body = { did };
@@ -136,7 +150,7 @@ class DiscussionDetailHandler extends DiscussionHandler {
page,
await system.get('REPLY_PER_PAGE'),
);
const uids = drdocs.map(domainId, (drdoc) => drdoc.owner);
const uids = drdocs.map((drdoc) => drdoc.owner);
uids.push(this.ddoc.owner);
for (const drdoc of drdocs) {
if (drdoc.reply) {
@@ -149,7 +163,7 @@ class DiscussionDetailHandler extends DiscussionHandler {
const path = [
['Hydro', '/'],
['discussion_main', '/discuss'],
[this.vnode.title, `/discuss/${this.ddoc.parentType}`, true],
[this.vnode.title, `/discuss/${discussion.typeDisplay[this.ddoc.parentType]}/${this.ddoc.parentId}`, true],
[this.ddoc.title, null, true],
];
this.response.template = 'discussion_detail.html';
@@ -275,10 +289,8 @@ async function apply() {
Route('/discuss/:did/raw', module.exports.DiscussionDetailRawHandler);
Route('/discuss/:did/:drid/raw', module.exports.DiscussionReplyRawHandler);
Route('/discuss/:did/:drid/:drrid/raw', module.exports.DiscussionTailReplyRawHandler);
Route('/discuss/node/:name', module.exports.DiscussionNodeHandler);
Route('/discuss/node/:name/create', module.exports.DiscussionCreateHandler);
Route('/discuss/:type/:docId', module.exports.DiscussionNodeHandler);
Route('/discuss/:type/:docId/create', module.exports.DiscussionCreateHandler);
Route('/discuss/:type/:name', module.exports.DiscussionNodeHandler);
Route('/discuss/:type/:name/create', module.exports.DiscussionCreateHandler);
}
global.Hydro.handler.discussion = module.exports = {
+7 -3
View File
@@ -100,7 +100,6 @@ class HomeSecurityHandler extends Handler {
async get() {
// TODO(iceboy): pagination? or limit session count for uid?
// TODO(masnn): UA & IP
const sessions = await token.getSessionListByUid(this.user._id);
const parsed = [];
for (const session of sessions) {
@@ -173,8 +172,13 @@ class HomeSettingsHandler extends Handler {
}
async post(args) {
// FIXME validation
await user.setById(args.domainId, this.user._id, args);
const $set = {};
for (const key in args) {
if (setting.SETTINGS_BY_KEY[args]) {
$set[key] = args[key];
}
}
await user.setById(args.domainId, this.user._id, $set);
this.back();
}
}
+13 -2
View File
@@ -78,10 +78,21 @@ class SystemSettingHandler extends SystemHandler {
if (typeof args[key] === 'object') {
const subtasks = [];
for (const sub in args[key]) {
subtasks.push(system.set(`${key}.${sub}`, args[key][sub]));
const s = setting.SYSTEM_SETTINGS_BY_KEY[`${key}.${sub}`];
if (s) {
if (s.ui === 'number') args[key][sub] = Number(args[key][sub]);
console.log(s);
subtasks.push(system.set(`${key}.${sub}`, args[key][sub]));
}
}
tasks.push(Promise.all(subtasks));
} else tasks.push(system.set(key, args[key]));
} else {
const s = setting.SYSTEM_SETTINGS_BY_KEY[key];
if (s) {
if (s.ui === 'number') args[key] = Number(args[key]);
tasks.push(system.set(key, args[key]));
}
}
}
await Promise.all(tasks);
this.back();
+2 -1
View File
@@ -25,9 +25,10 @@ class RecordListHandler extends RecordHandler {
];
}
const rdocs = await record.getMany(domainId, q, { _id: -1 }, page, await system.get('RECORD_PER_PAGE'));
console.log(rdocs);
const [udict, pdict] = await Promise.all([
user.getList(domainId, rdocs.map((rdoc) => rdoc.uid)),
problem.getList(domainId, rdocs.map((rdoc) => rdoc.pid)),
problem.getList(domainId, rdocs.map((rdoc) => rdoc.pid), false),
]);
const path = [
['Hydro', '/'],
+1 -1
View File
@@ -22,6 +22,6 @@ Item('/homework', 'homework_main', 'homework', permission.PERM_VIEW_HOMEWORK);
Item('/discuss', 'discussion_main', 'discussion', permission.PERM_VIEW_DISCUSSION);
Item('/c', 'contest_main', 'contest', permission.PERM_VIEW_CONTEST);
Item('/domain/dashboard', 'domain_dashboard', 'domain', permission.PERM_MANAGE);
Item('/manage/dashboard', 'manage_dashboard', 'manage', null, (handler) => handler.user.priv === 1);
Item('/manage/dashboard', 'manage', 'manage', null, (handler) => handler.user.priv === 1);
global.Hydro.lib.nav = module.exports = Item;
+1 -1
View File
@@ -242,7 +242,7 @@ async function edit(domainId, tid, $set, type = document.TYPE_CONTEST) {
* @param {string} domainId
* @param {ObjectID} tid
* @param {number} type
* @returns {Tdoc}
* @returns {Promise<Tdoc>}
*/
async function get(domainId, tid, type = document.TYPE_CONTEST) {
let tdoc;
+32 -8
View File
@@ -2,9 +2,18 @@ const { ObjectID } = require('bson');
const problem = require('./problem');
const document = require('./document');
const contest = require('./contest');
const training = require('./training');
const { PERM_VIEW_PROBLEM_HIDDEN } = require('../permission');
const { DocumentNotFoundError } = require('../error');
const typeDisplay = {
[document.TYPE_PROBLEM]: 'problem',
[document.TYPE_CONTEST]: 'contest',
[document.TYPE_DISCUSSION_NODE]: 'node',
[document.TYPE_TRAINING]: 'training',
[document.TYPE_HOMEWORK]: 'homework',
};
function add(domainId, parentType, parentId, owner, title, content, ip = null, highlight = false) {
return document.add(
domainId, content, owner, document.TYPE_DISCUSSION,
@@ -115,9 +124,7 @@ function getStatus(domainId, did, uid) {
}
function addNode(domainId, _id, category) {
return document.add(
domainId, _id, 1, document.TYPE_DISCUSSION_NODE, null, null, null, { category },
);
return document.add(domainId, category, 1, document.TYPE_DISCUSSION_NODE, _id);
}
function getNode(domainId, _id) {
@@ -125,17 +132,32 @@ function getNode(domainId, _id) {
}
async function getVnode(domainId, ddoc, handler) {
if (ddoc.parentType === 'problem') {
if (ddoc.parentType === document.TYPE_PROBLEM) {
const pdoc = await problem.getById(domainId, ddoc.parentId);
if (!pdoc) return null;
if (pdoc.hidden && handler) handler.checkPerm(PERM_VIEW_PROBLEM_HIDDEN);
return { ...pdoc, type: ddoc.parentType, id: ddoc.parentId };
} if (ddoc.parentType === 'contest') {
}
if (ddoc.parentType === document.TYPE_CONTEST) {
const tdoc = await contest.get(domainId, ddoc.parentId);
return { ...tdoc, type: ddoc.parentType, id: ddoc.parentId };
} if (ddoc.parentType === 'node') {
}
if (ddoc.parentType === document.TYPE_DISCUSSION_NODE) {
const ndoc = await getNode(domainId, ddoc.parentId);
return { title: ndoc._id, type: ddoc.parentType, id: ddoc.parentId };
return {
...ndoc,
title: ddoc.parentId,
type: ddoc.parentType,
id: ddoc.parentId,
};
}
if (ddoc.parentType === document.TYPE_TRAINING) {
const tdoc = await training.get(domainId, ddoc.parentId);
return { ...tdoc, type: ddoc.parentType, id: ddoc.parentId };
}
if (ddoc.parentType === document.TYPE_HOMEWORK) {
const tdoc = await contest.get(domainId, ddoc.parentId, document.TYPE_HOMEWORK);
return { ...tdoc, type: ddoc.parentType, id: ddoc.parentId };
}
return {
title: 'Missing Node',
@@ -147,13 +169,15 @@ async function getVnode(domainId, ddoc, handler) {
async function getListVnodes(domainId, ddocs, handler) {
const res = {};
for (const ddoc of ddocs) {
if (!res[ddoc.parentType]) res[ddoc.parentType] = {};
// FIXME no-await-in-loop
res[ddoc._id] = await getVnode(domainId, ddoc, handler); // eslint-disable-line no-await-in-loop
res[ddoc.parentType][ddoc.parentId] = await getVnode(domainId, ddoc, handler); // eslint-disable-line no-await-in-loop
}
return res;
}
global.Hydro.model.discussion = module.exports = {
typeDisplay,
add,
get,
edit,
+6 -2
View File
@@ -30,8 +30,12 @@ function del(_id) {
}
async function inc(_id) {
const file = await coll.findOne({ _id });
file.count++;
const doc = await coll.findOneAndUpdate(
{ _id },
{ $inc: { count: 1 } },
{ returnOriginal: false },
);
return (doc.value || {}).count;
}
async function dec(_id) {
+1
View File
@@ -14,5 +14,6 @@ module.exports = {
task: require('./task'),
message: require('./message'),
setting: require('./setting'),
discussion: require('./discussion'),
file: require('./file'),
};
+9 -4
View File
@@ -45,7 +45,7 @@ async function get(domainId, pid, uid = null) {
const pdoc = Number.isInteger(pid)
? await document.get(domainId, document.TYPE_PROBLEM, pid)
: (await document.getMulti(domainId, document.TYPE_PROBLEM, { pid }).toArray())[0];
if (!pdoc) throw new ProblemNotFoundError(pid);
if (!pdoc) throw new ProblemNotFoundError(domainId, pid);
if (uid) {
pdoc.psdoc = await document.getStatus(domainId, document.TYPE_PROBLEM, pdoc.docId, uid);
}
@@ -110,10 +110,15 @@ async function random(domainId, query) {
} return null;
}
async function getList(domainId, pids) {
async function getList(domainId, pids, doThrow = true) {
const r = {};
// eslint-disable-next-line no-await-in-loop
for (const pid of pids) r[pid] = await get(domainId, pid);
if (doThrow) {
// eslint-disable-next-line no-await-in-loop
for (const pid of pids) r[pid] = await get(domainId, pid);
} else {
// eslint-disable-next-line no-await-in-loop
for (const pid of pids) r[pid] = await get(domainId, pid).catch((e) => e);
}
return r;
}
+24 -11
View File
@@ -41,7 +41,7 @@ const ACCOUNT_SETTINGS = [
const SETTINGS = [...PREFERENCE_SETTINGS, ...ACCOUNT_SETTINGS];
const SETTINGS_BY_KEY = {};
for (const setting in SETTINGS) SETTINGS_BY_KEY[setting.key] = setting;
for (const setting of SETTINGS) SETTINGS_BY_KEY[setting.key] = setting;
const SYSTEM_SETTINGS = [
Setting('setting_basic', 'user.register', null, false, 'checkbox', 'Allow register'),
@@ -49,19 +49,32 @@ const SYSTEM_SETTINGS = [
Setting('setting_smtp', 'smtp.user', null, null, 'text', 'SMTP Username'),
Setting('setting_smtp', 'smtp.pass', null, null, 'password', 'SMTP Password'),
Setting('setting_smtp', 'smtp.host', null, null, 'text', 'SMTP Server Host'),
Setting('setting_smtp', 'smtp.port', null, 465, 'text', 'SMTP Server Port'),
Setting('setting_smtp', 'smtp.port', null, 465, 'number', 'SMTP Server Port'),
Setting('setting_smtp', 'smtp.from', null, null, 'text', 'Mail From'),
Setting('setting_smtp', 'smtp.secure', null, false, 'checkbox', 'SSL'),
Setting('setting_server', 'listen.port', null, 8888, 'text', 'Server Port', ''),
Setting('setting_constant', 'PROBLEM_PER_PAGE', null, 100, 'text', 'Problems per Page'),
Setting('setting_constant', 'CONTEST_PER_PAGE', null, 20, 'text', 'Contests per Page'),
Setting('setting_constant', 'DISCUSSION_PER_PAGE', null, 50, 'text', 'Discussion per Page'),
Setting('setting_constant', 'RECORD_PER_PAGE', null, 100, 'text', 'Record per Page'),
Setting('setting_constant', 'SOLUTION_PER_PAGE', null, 20, 'text', 'Solutions per Page'),
Setting('setting_constant', 'TRAINING_PER_PAGE', null, 10, 'text', 'Training per Page'),
Setting('setting_constant', 'REPLY_PER_PAGE', null, 50, 'text', 'Reply per Page'),
Setting('setting_server', 'server.hostname', null, null, 'text', 'Server Hostname'),
Setting('setting_server', 'server.host', null, null, 'text', 'Server Host'),
Setting('setting_server', 'server.url', null, null, 'text', 'Server BaseURL'),
Setting('setting_server', 'server.port', null, 8888, 'number', 'Server Port'),
Setting('setting_constant', 'PROBLEM_PER_PAGE', null, 100, 'number', 'Problems per Page'),
Setting('setting_constant', 'CONTEST_PER_PAGE', null, 20, 'number', 'Contests per Page'),
Setting('setting_constant', 'DISCUSSION_PER_PAGE', null, 50, 'number', 'Discussion per Page'),
Setting('setting_constant', 'RECORD_PER_PAGE', null, 100, 'number', 'Record per Page'),
Setting('setting_constant', 'SOLUTION_PER_PAGE', null, 20, 'number', 'Solutions per Page'),
Setting('setting_constant', 'TRAINING_PER_PAGE', null, 10, 'number', 'Training per Page'),
Setting('setting_constant', 'REPLY_PER_PAGE', null, 50, 'number', 'Reply per Page'),
];
const SYSTEM_SETTINGS_BY_KEY = {};
for (const setting of SYSTEM_SETTINGS) SYSTEM_SETTINGS_BY_KEY[setting.key] = setting;
global.Hydro.model.setting = module.exports = {
Setting, PREFERENCE_SETTINGS, ACCOUNT_SETTINGS, SETTINGS, SETTINGS_BY_KEY, SYSTEM_SETTINGS,
Setting,
PREFERENCE_SETTINGS,
ACCOUNT_SETTINGS,
SETTINGS,
SETTINGS_BY_KEY,
SYSTEM_SETTINGS,
SYSTEM_SETTINGS_BY_KEY,
};
+6 -9
View File
@@ -19,7 +19,7 @@ const allowFail = async (func, ...args) => {
return res;
};
async function run(username, password) {
async function run({ username, password } = {}) {
const def = {
PROBLEM_PER_PAGE: 100,
RECORD_PER_PAGE: 100,
@@ -39,7 +39,7 @@ async function run(username, password) {
'smtp.pass': '',
'smtp.secure': false,
'db.ver': 1,
'listen.port': 8888,
'server.port': 8888,
'session.keys': ['Hydro'],
'session.secure': false,
'session.saved_expire_seconds': 3600 * 24,
@@ -49,7 +49,10 @@ async function run(username, password) {
};
let tasks = [];
for (const key in def) {
tasks.push(system.set(key, def[key]));
tasks.push(system.get(key).then((value) => {
if (!value) return system.set(key, def[key]);
return Promise.resolve();
}));
}
await Promise.all(tasks);
tasks = [
@@ -91,12 +94,6 @@ async function run(username, password) {
}, { upsert: true }),
);
}
for (const category in builtin.DEFAULT_NODES) {
const nodes = builtin.DEFAULT_NODES[category];
for (const node of nodes) {
tasks.push(discussion.addNode(node.name, category));
}
}
await Promise.all(tasks);
}
+3 -3
View File
@@ -10,9 +10,9 @@ mongourl += `${options.host}:${options.port}/${options.name}`;
*/
let db = null;
Mongo.MongoClient.connect(mongourl, { useNewUrlParser: true, useUnifiedTopology: true })
.then((Database) => {
db = Database.db(options.name);
db.Db = Database;
.then((Client) => {
db = Client.db(options.name);
db.Client = Client;
bus.publish('system_database_connected', null);
});
+9 -4
View File
@@ -98,6 +98,8 @@ class Handler {
constructor(ctx) {
this.ctx = ctx;
this.request = {
host: ctx.request.host,
hostname: ctx.request.hostname,
ip: ctx.request.ip,
headers: ctx.request.headers,
cookies: ctx.cookies,
@@ -193,8 +195,8 @@ class Handler {
token.TYPE_SESSION,
this._handler.expireSeconds,
{
update_ip: this.request.ip,
update_ua: this.request.headers['user-agent'] || '',
updateIp: this.request.ip,
updateUa: this.request.headers['user-agent'] || '',
},
) : { uid: 1 };
if (!this.session) this.session = { uid: 1 };
@@ -302,6 +304,9 @@ function Route(route, RouteHandler, permission = null) {
const args = {
...ctx.params, ...ctx.query, ...ctx.request.body, domainId: 'system',
};
if (h.request.host !== await system.get('server.host')) {
args.domainId = h.request.url.split('//')[1].split(`.${await system.get('server.host')}`)[0] || '';
}
if (h.___prepare) await h.___prepare(args);
if (permission) h.checkPerm(permission);
@@ -437,8 +442,8 @@ async function start() {
app.use(morgan(':method :url :status :res[content-length] - :response-time ms'));
app.use(router.routes()).use(router.allowedMethods());
Route('*', Handler);
server.listen(await system.get('listen.port'));
console.log('Server listening at: %s', await system.get('listen.port'));
server.listen(await system.get('server.port'));
console.log('Server listening at: %s', await system.get('server.port'));
}
global.Hydro.service.server = module.exports = {
+23 -18
View File
@@ -1,8 +1,8 @@
const fs = require('fs');
const os = require('os');
const path = require('path');
const http = require('http');
const Koa = require('koa');
const morgan = require('koa-morgan');
const Body = require('koa-body');
const Router = require('koa-router');
const cache = require('koa-static-cache');
@@ -19,20 +19,11 @@ class Loader {
}
}
class Nunjucks extends nunjucks.Environment {
constructor() {
super(new Loader(), { autoescape: true, trimBlocks: true });
this.addFilter('json', (self) => JSON.stringify(self), false);
this.addFilter('base64_encode', (s) => Buffer.from(s).toString('base64'));
}
}
const env = new Nunjucks();
const env = new nunjucks.Environment(new Loader(), { autoescape: true, trimBlocks: true });
function render(name) {
return new Promise((resolve, reject) => {
env.render(name, {
typeof: (o) => typeof o,
static_url: (str) => `/${str}`,
handler: { renderTitle: (str) => str },
_: (str) => str,
}, (err, res) => {
if (err) reject(err);
@@ -40,15 +31,16 @@ function render(name) {
});
});
}
async function setup() {
const app = new Koa();
const server = http.createServer(app.callback());
const router = new Router();
app.keys = ['Hydro'];
app.use(cache(path.join(process.cwd(), '.uibuild'), {
app.use(cache(path.join(os.tmpdir(), 'hydro', 'builtin'), {
maxAge: 365 * 24 * 60 * 60,
}));
app.use(Body({ formidable: { maxFileSize: 256 * 1024 * 1024 } }));
app.use(Body());
router.get('/', async (ctx) => {
ctx.body = await render('setup.html');
ctx.response.type = 'text/html';
@@ -66,9 +58,23 @@ async function setup() {
useNewUrlParser: true, useUnifiedTopology: true,
});
const db = Database.db(name);
// Check Write Access
await db.collection('test').insertOne({ test: 'test' });
await db.collection('test').deleteOne({ test: 'test' });
await Promise.all([
db.collection('system').updateOne(
{ _id: 'server.host' },
{ $set: { value: ctx.request.host } },
{ upsert: true },
),
db.collection('system').updateOne(
{ _id: 'server.hostname' },
{ $set: { value: ctx.request.hostname } },
{ upsert: true },
),
db.collection('system').updateOne(
{ _id: 'server.url' },
{ $set: { value: ctx.request.href } },
{ upsert: true },
),
]);
fs.writeFileSync(path.resolve(process.cwd(), 'config.json'), JSON.stringify({
host, port, name, username, password,
}));
@@ -79,7 +85,6 @@ async function setup() {
}
});
});
app.use(morgan(':method :url :status :res[content-length] - :response-time ms'));
app.use(router.routes()).use(router.allowedMethods());
server.listen(8888);
console.log('Server listening at: 8888');
+2 -1
View File
@@ -307,7 +307,8 @@ Create Domain: 创建域
Properties: 属性
Access Control: 访问控制
domain_main: 首页
manage: 管理域
manage: 管理
manage_domain: 管理域
manage_dashboard: 概况
manage_discussion: 讨论节点
manage_edit: 编辑域资料
+2 -1
View File
@@ -301,7 +301,8 @@ Create Domain: 創建域
Properties: 屬性
Access Control: 訪問控制
domain_main: 首頁
manage: 管理域
manage: 管理
manage_domain: 管理域
manage_dashboard: 概況
manage_edit: 編輯域資料
manage_join_applications: 加域申請
+7
View File
@@ -0,0 +1,7 @@
const fs = require('fs');
const path = require('path');
const child = require('child_process');
const appPath = path.resolve(__dirname, '..', 'hydro', 'development.js');
const app = child.fork(appPath);
+66 -11
View File
@@ -1,13 +1,12 @@
/* eslint-disable no-await-in-loop */
const { mongodb } = global.Hydro.nodeModules;
const dst = global.Hydro.service.db;
const { file, problem, discussion } = global.Hydro.model;
const map = {};
const pid = (id) => {
if (map[id.toString()]) return map[id.toString()];
if (id.generationTime) return id;
id = id.toString();
return id;
};
@@ -114,6 +113,8 @@ const tasks = {
res[mapper[key]] = doc[key];
} else if (typeof mapper[key] === 'object') {
res[mapper[key].field] = mapper[key].processer(doc[key]);
} else if (mapper[key] === null) {
// Ignore this key
} else {
console.log('Unknown key:', key);
}
@@ -214,7 +215,7 @@ const tasks = {
'fs.chunks': async (doc) => doc,
file: async (doc) => ({
_id: doc._id,
count: doc.metadata.link,
count: 0,
secret: doc.metadata.secret,
size: doc.length,
md5: doc.md5,
@@ -223,7 +224,7 @@ const tasks = {
const cursor = {
user: (s) => s.collection('user').find(),
document: (s) => s.collection('document').find(),
document: (s) => s.collection('document').find({ doc_type: { $ne: 20 } }),
'document.status': (s) => s.collection('document.status').find(),
record: (s) => s.collection('record').find(),
'fs.files': (s) => s.collection('fs.files').find(),
@@ -231,6 +232,29 @@ const cursor = {
file: (s) => s.collection('fs.files').find(),
};
async function discussionNode(src, report) {
const count = await src.collection('document').find({ doc_type: 20 }).count();
await report({ progress: 1, message: `discussion.node: ${count}` });
const total = Math.floor(count / 5);
for (let i = 0; i <= total; i++) {
const docs = await src.collection('document')
.find({ doc_type: 20 }).skip(i * 5).limit(5)
.toArray();
for (const doc of docs) {
const t = [];
for (const item of doc.content || []) {
const category = item[0];
const nodes = item[1];
for (const node of nodes || []) {
t.push(discussion.addNode(doc.domain_id, node.name, category));
}
}
await Promise.all(t).catch((e) => e);
}
await report({ progress: Math.round(100 * ((i + 1) / (total + 1))) });
}
}
async function fixPid(report) {
const count = await dst.collection('document').find({ docType: 10 }).count();
await report({ progress: 1, message: `Fix pid: ${count}` });
@@ -239,12 +263,28 @@ async function fixPid(report) {
const docs = await dst.collection('document')
.find({ docType: 10 }).skip(i * 50).limit(50)
.toArray();
const t = [];
for (const doc of docs) {
dst.collection('document').updateOne(
{ _id: doc._id },
{ $set: { pid: doc.pid || doc.docId.toString() } },
t.push(
dst.collection('document').updateOne(
{ _id: doc._id },
{ $set: { pid: doc.pid || doc.docId.toString() } },
),
);
if (doc.data && doc.data.generationTime) {
t.push(file.inc(doc.data));
} else if (doc.data) {
const pdoc = await problem.get(doc.data.domain, pid(doc.data.pid));
t.push(
file.inc(pdoc.data),
dst.collection('document').updateOne(
{ _id: doc._id },
{ $set: { data: pdoc.data } },
),
);
}
}
await Promise.all(t);
await report({ progress: Math.round(100 * ((i + 1) / (total + 1))) });
}
}
@@ -273,9 +313,23 @@ async function task(name, src, report) {
if (d.domainId && d.docId && d.docType) {
const query = { domainId: d.domainId, docId: d.docId, docType: d.docType };
if (d.uid) query.uid = d.uid;
res.push(dst.collection(name).updateOne(
query, { $set: docWithoutDid }, { upsert: true },
));
res.push((async () => {
const data = await dst.collection(name).findOne(query);
if (data) {
await dst.collection(name).updateOne(query, { $set: docWithoutId });
} else if (d._id) {
const dat = await dst.collection(name).findOne({ _id: d._id });
if (dat) {
await dst.collection(name).updateOne({
_id: d._id,
}, { $set: docWithoutId });
} else {
await dst.collection(name).insertOne(d);
}
} else {
await dst.collection(name).insertOne(d);
}
})());
} else if (d._id) {
res.push(dst.collection(name).updateOne({
_id: d._id,
@@ -290,7 +344,6 @@ async function task(name, src, report) {
lastProgress = progress;
}
}
await fixPid(report);
}
async function migrateVijos({
@@ -325,6 +378,8 @@ async function migrateVijos({
await dst.collection('user').deleteMany({ _id: { $nin: [0, 1] } });
const d = ['user', 'document', 'document.status', 'record', 'file'];
for (const i of d) await task(i, src, report);
await fixPid(report);
await discussionNode(src, report);
return true;
}
+1
View File
@@ -1,3 +1,4 @@
{# No Compression #}
{% macro form_begin(args) %}
{% set label = args.label %}
{% set columns = args.columns %}
+1 -1
View File
@@ -13,7 +13,7 @@
</a>
{% if badge %}
<a class="user-profile-badge v-center badge--lv{{ udoc['level']|default(0) }}" href="/u/{{ udoc['_id']|default(0) }}" title="LV {{ udoc['level']|default(0) }}: Top {{ model.builtin.LEVELS[udoc['level']|default(0)]|default('N/A') }}%">LV {{ udoc['level']|default(0) }}</a>
{% if modbadge and handler.hasPerm(perm.PERM_MOD_BADGE) %}
{% if modbadge and udoc.hasPerm(perm.PERM_MOD_BADGE) %}
<span class="user-profile-badge v-center badge--mod" title="Moderator">MOD</span>
{% endif %}
{% endif %}
+4 -4
View File
@@ -7,15 +7,15 @@
</div>
</div>
<div class="medium-3 columns">
{% if vnode.parentType == 'problem' %}
{% if vnode.parentType == model.document.TYPE_PROBLEM %}
{% set pdoc = vnode %}
{% set owner_udoc = udict[vnode.owner] %}
{% include "partials/problem_sidebar.html" %}
{% elif vnode.parentType == 'contest' %}
{% elif vnode.parentType == model.document.TYPE_CONTEST %}
{% set tdoc = vnode %}
{% set owner_udoc = udict[vnode.owner] %}
{% include "partials/contest_sidebar.html" %}
{% elif vnode.parentType == 'node' %}
{% elif vnode.parentType == model.document.TYPE_DISCUSSION_NODE %}
<div class="section side">
{% if vnode.pic %}<div class="discussion-node__bg {{ vnode.pic }}"></div>{% endif %}
<div class="section__header">
@@ -24,7 +24,7 @@
<div class="section__body">
{% if vnode %}
{% if handler.hasPerm(perm.PERM_CREATE_DISCUSSION) %}
<p><a href="/discuss/{{ vnode.parentType }}/{{ vnode.parentId }}" class="expanded primary button">{{ _('Create a Discussion') }}</a></p>
<p><a href="/discuss/{{ model.discussion.typeDisplay[vnode.parentType] }}/{{ vnode.parentId }}/create" class="expanded primary button">{{ _('Create a Discussion') }}</a></p>
{% else %}
{% if not handler.hasPerm(perm.PERM_LOGGEDIN) %}
<p><a href="javascript:showSignInDialog();" class="expanded primary button">{{ _('Login to Create a Discussion') }}</a></p>
+7
View File
@@ -3,6 +3,13 @@
<div class="section">
<div class="section__body">
<form method="post">
{{ form.form_text({
label:'ID',
name:'id',
value:domain._id,
disabled:true,
required:true
}) }}
{{ form.form_text({
label:'Name',
name:'name',
+2 -9
View File
@@ -6,18 +6,12 @@
<h1>{{ _('Add Module') }}</h1>
</div></div>
<div class="row">
<div class="medium-6 columns">
<div class="medium-12 columns">
<label>
{{ _('URL') }}
<input name="url" type="text" class="textbox" autocomplete="off" data-autofocus>
</label>
</div>
<div class="medium-6 columns">
<label>
{{ _('ID (Optional)') }}
<input name="id" type="text" class="textbox" autocomplete="off">
</label>
</div>
</div>
</div>
</div>
@@ -55,10 +49,9 @@
<td class="col--size">{{ size(module.size) if module.size else '?' }}</td>
<td class="col--version">{{ module.version|default('?') }}</td>
<td class="col--action">
<a class="typo-a" href="/manage/module/download/{{ module.filename }}">{{ _('Download') }}</a>
<form method="post">
<input type="hidden" name="operation" value="delete">
<input type="hidden" name="id" value="{{ module.id }}">
<input type="hidden" name="id" value="{{ module.filename }}">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<input type="submit" class="typo-a" value="{{ _('Delete') }}">
+5 -5
View File
@@ -15,18 +15,18 @@
</div>
</div>
<div class="media__body top">
<h1 class="discussion__title"><a href="/discuss/{{ ddoc._id }}#{{ handler.datetime_stamp(ddoc.updateAt) if ddoc.update_at else '' }}" data-emoji-enabled>{{ ddoc['title'] }}</a></h1>
<h1 class="discussion__title"><a href="/discuss/{{ ddoc._id }}#{{ ddoc.updateAt }}" data-emoji-enabled>{{ ddoc['title'] }}</a></h1>
<ul class="supplementary list">
<li>
<a class="discussion-node-tag" href="/discuss/{{ ddoc.parentType }}/{{ ddoc.parentId }}">
{% if ddoc.parentType == 'node' %}
<a class="discussion-node-tag" href="/discuss/{{ model.discussion.typeDisplay[ddoc.parentType] }}/{{ ddoc.parentId }}">
{% if ddoc.parentType == model.document.TYPE_PROBLEM %}
<span class="v-center icon icon-tag"></span>
{% elif ddoc.parentType == 'contest' %}
{% elif ddoc.parentType == model.document.TYPE_CONTEST %}
<span class="v-center icon icon-award"></span>
{% else %}
<span class="v-center icon icon-flag"></span>
{% endif %}
{{ vndict[ddoc._id].title|default(_('(missing)')) }}
{{ vndict[ddoc.parentType][ddoc.parentId].title|default(_('(missing)')) }}
</a>
</li>
<li>
+1 -1
View File
@@ -5,7 +5,7 @@
</div>
<div class="section__body">
{% for setting in family_settings %}
{% if setting.ui == 'text' or setting.ui == 'password' %}
{% if setting.ui == 'text' or setting.ui == 'password' or setting.ui == 'number' %}
{{ form.form_text({
type:setting.ui,
label:setting.name,
+5 -7
View File
@@ -7,8 +7,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<meta name="msapplication-TileColor" content="#579e9a">
<meta name="theme-color" content="#56758f">
<link rel="stylesheet" media="all" href="{{ static_url('vj4.css') }}">
<link rel="stylesheet" media="all" href="/vj4.css">
<title>{{ _('Setup') }}</title>
<script>
var _htmlNode = document.documentElement;
@@ -31,25 +30,25 @@
<div class="row"><div class="columns">
<label class="inverse material textbox">
{{ _('Database Port') }}
<input name="port" type="number" value="27017" autofocus>
<input name="port" type="number" value="27017">
</label>
</div></div>
<div class="row"><div class="columns">
<label class="inverse material textbox">
{{ _('Database Name') }}
<input name="name" type="text" value="hydro" autofocus>
<input name="name" type="text" value="hydro">
</label>
</div></div>
<div class="row"><div class="columns">
<label class="inverse material textbox">
{{ _('Database Username') }}
<input name="username" type="text" placeholder="{{ _('Leave blank if none') }}" autofocus>
<input name="username" type="text" placeholder="{{ _('Leave blank if none') }}">
</label>
</div></div>
<div class="row"><div class="columns">
<label class="inverse material textbox">
{{ _('Database Password') }}
<input name="password" type="password" placeholder="{{ _('Leave blank if none') }}" autofocus>
<input name="password" type="password" placeholder="{{ _('Leave blank if none') }}">
</label>
</div></div>
<div class="row"><div class="columns">
@@ -62,6 +61,5 @@
</div></div>
</div>
</div>
<script type="text/javascript" src="{{ static_url('vj4.js') }}"></script>
</body>
</html>