mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
core&ui: goodbye, minio (#417)
This commit is contained in:
1 parent
7abc0a7d1b
commit
e06c86e9dd
34 files changed
+405
-317
No files matched your search
@@ -18,4 +18,3 @@ RUN npm i pm2 yarn -g && \
|
|||||||
sudo apt-get install clang -y --no-install-recommends && \
|
sudo apt-get install clang -y --no-install-recommends && \
|
||||||
mkdir -p ~/.hydro && \
|
mkdir -p ~/.hydro && \
|
||||||
echo '{"host":"127.0.0.1","port":"27017","name":"hydro","username":"","password":""}' >~/.hydro/config.json && \
|
echo '{"host":"127.0.0.1","port":"27017","name":"hydro","username":"","password":""}' >~/.hydro/config.json && \
|
||||||
echo "MINIO_ACCESS_KEY=minioadmin\nMINIO_SECRET_KEY=minioadmin" >~/.hydro/env
|
|
||||||
@@ -9,6 +9,7 @@ services:
|
|||||||
VARIANT: 16
|
VARIANT: 16
|
||||||
volumes:
|
volumes:
|
||||||
- ..:/workspace:cached
|
- ..:/workspace:cached
|
||||||
|
- testdata:/data/file
|
||||||
command: sleep infinity
|
command: sleep infinity
|
||||||
network_mode: service:db
|
network_mode: service:db
|
||||||
db:
|
db:
|
||||||
@@ -16,14 +17,7 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- mongodb-data:/data/db
|
- mongodb-data:/data/db
|
||||||
minio:
|
|
||||||
image: minio/minio:latest
|
|
||||||
restart: unless-stopped
|
|
||||||
volumes:
|
|
||||||
- minio-data:/data
|
|
||||||
network_mode: service:db
|
|
||||||
command: minio server /data --console-address ":9001"
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
mongodb-data:
|
mongodb-data:
|
||||||
minio-data:
|
testdata:
|
||||||
Vendored
+2
-3
@@ -1,10 +1,9 @@
|
|||||||
FROM gitpod/workspace-mongodb
|
FROM gitpod/workspace-mongodb
|
||||||
RUN npm i pm2 -g && \
|
RUN npm i pm2 -g && \
|
||||||
sudo wget https://dl.min.io/server/minio/release/linux-amd64/minio -O /usr/bin/minio && \
|
|
||||||
sudo chmod 755 /usr/bin/minio && \
|
|
||||||
sudo apt-get update && \
|
sudo apt-get update && \
|
||||||
sudo apt-get install clang -y && \
|
sudo apt-get install clang -y && \
|
||||||
cargo install sonic-server --version 1.3.0 && \
|
cargo install sonic-server --version 1.3.0 && \
|
||||||
mkdir -p /home/gitpod/.hydro && \
|
mkdir -p /home/gitpod/.hydro && \
|
||||||
echo '{"host":"127.0.0.1","port":"27017","name":"hydro","username":"","password":""}' >/home/gitpod/.hydro/config.json && \
|
echo '{"host":"127.0.0.1","port":"27017","name":"hydro","username":"","password":""}' >/home/gitpod/.hydro/config.json && \
|
||||||
echo "MINIO_ACCESS_KEY=hydro\nMINIO_SECRET_KEY=hydrohydro" >/home/gitpod/.hydro/env
|
mkdir /data/file -p
|
||||||
|
chmod 777 /data/file
|
||||||
@@ -4,7 +4,6 @@ tasks:
|
|||||||
- init: |
|
- init: |
|
||||||
yarn
|
yarn
|
||||||
pm2 start mongod
|
pm2 start mongod
|
||||||
pm2 start "MINIO_ACCESS_KEY=hydro MINIO_SECRET_KEY=hydrohydro minio server /home/gitpod/file" --name minio
|
|
||||||
yarn build:ui
|
yarn build:ui
|
||||||
npx hydrooj cli system set server.port 2333
|
npx hydrooj cli system set server.port 2333
|
||||||
npx hydrooj cli user create root@hydro.local root rootroot 2
|
npx hydrooj cli user create root@hydro.local root rootroot 2
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ fi
|
|||||||
|
|
||||||
if [ ! -f "$ROOT/first" ]; then
|
if [ ! -f "$ROOT/first" ]; then
|
||||||
echo "for marking use only!" > "$ROOT/first"
|
echo "for marking use only!" > "$ROOT/first"
|
||||||
hydrooj cli system set file.endPoint http://oj-minio:9000/
|
|
||||||
|
|
||||||
hydrooj cli user create systemjudge@systemjudge.local root rootroot
|
hydrooj cli user create systemjudge@systemjudge.local root rootroot
|
||||||
hydrooj cli user setSuperAdmin 2
|
hydrooj cli user setSuperAdmin 2
|
||||||
|
|||||||
@@ -1,16 +1,6 @@
|
|||||||
version: '3.7'
|
version: '3.7'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
oj-minio:
|
|
||||||
image: minio/minio
|
|
||||||
container_name: oj-minio
|
|
||||||
command: server /data
|
|
||||||
restart: always
|
|
||||||
volumes:
|
|
||||||
- ./data/minio:/data
|
|
||||||
environment:
|
|
||||||
- MINIO_ACCESS_KEY=CHANGE_THIS
|
|
||||||
- MINIO_SECRET_KEY=CHANGE_THIS
|
|
||||||
|
|
||||||
# Warning: mongodb here is not password-protected.
|
# Warning: mongodb here is not password-protected.
|
||||||
# DO NOT EXPOSE THIS SERVICE TO THE PUBLIC.
|
# DO NOT EXPOSE THIS SERVICE TO THE PUBLIC.
|
||||||
@@ -26,13 +16,10 @@ services:
|
|||||||
container_name: oj-backend
|
container_name: oj-backend
|
||||||
restart: always
|
restart: always
|
||||||
depends_on:
|
depends_on:
|
||||||
- oj-minio
|
|
||||||
- oj-mongo
|
- oj-mongo
|
||||||
volumes:
|
volumes:
|
||||||
|
- ./data/file:/data/file
|
||||||
- ./data/backend:/root/.hydro
|
- ./data/backend:/root/.hydro
|
||||||
environment:
|
|
||||||
- MINIO_ACCESS_KEY=CHANGE_THIS
|
|
||||||
- MINIO_SECRET_KEY=CHANGE_THIS
|
|
||||||
ports:
|
ports:
|
||||||
- "0.0.0.0:80:8888" # In docker mode, change THIS port instead of port in system settings!
|
- "0.0.0.0:80:8888" # In docker mode, change THIS port instead of port in system settings!
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ const locales = {
|
|||||||
'error.nodeVersionPraseFail': '无法解析 Node 版本号,请尝试手动安装。',
|
'error.nodeVersionPraseFail': '无法解析 Node 版本号,请尝试手动安装。',
|
||||||
'install.pm2': '正在安装 PM2...',
|
'install.pm2': '正在安装 PM2...',
|
||||||
'install.createDatabaseUser': '正在创建数据库用户...',
|
'install.createDatabaseUser': '正在创建数据库用户...',
|
||||||
'install.minio': '正在安装 MinIO...',
|
|
||||||
'install.compiler': '正在安装编译器...',
|
'install.compiler': '正在安装编译器...',
|
||||||
'install.hydro': '正在安装 Hydro...',
|
'install.hydro': '正在安装 Hydro...',
|
||||||
'install.done': 'Hydro 安装成功!',
|
'install.done': 'Hydro 安装成功!',
|
||||||
@@ -45,7 +44,6 @@ const locales = {
|
|||||||
'error.nodeVersionPraseFail': 'Unable to parse Node version, please try to install manually.',
|
'error.nodeVersionPraseFail': 'Unable to parse Node version, please try to install manually.',
|
||||||
'install.pm2': 'Installing PM2...',
|
'install.pm2': 'Installing PM2...',
|
||||||
'install.createDatabaseUser': 'Creating database user...',
|
'install.createDatabaseUser': 'Creating database user...',
|
||||||
'install.minio': 'Installing MinIO...',
|
|
||||||
'install.compiler': 'Installing compiler...',
|
'install.compiler': 'Installing compiler...',
|
||||||
'install.hydro': 'Installing Hydro...',
|
'install.hydro': 'Installing Hydro...',
|
||||||
'install.done': 'Hydro installation completed!',
|
'install.done': 'Hydro installation completed!',
|
||||||
@@ -83,8 +81,6 @@ if (!cpuInfoFile.includes('avx2')) {
|
|||||||
let migration;
|
let migration;
|
||||||
let retry = 0;
|
let retry = 0;
|
||||||
log.info('install.start');
|
log.info('install.start');
|
||||||
const MINIO_ACCESS_KEY = randomstring(32);
|
|
||||||
const MINIO_SECRET_KEY = randomstring(32);
|
|
||||||
let DATABASE_PASSWORD = randomstring(32);
|
let DATABASE_PASSWORD = randomstring(32);
|
||||||
// TODO read from args
|
// TODO read from args
|
||||||
const CN = true;
|
const CN = true;
|
||||||
@@ -193,13 +189,6 @@ To disable this feature, checkout our sourcecode.`);
|
|||||||
skip: () => !exec('pm2 -v').code,
|
skip: () => !exec('pm2 -v').code,
|
||||||
operations: ['yarn global add pm2'],
|
operations: ['yarn global add pm2'],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
init: 'install.minio',
|
|
||||||
skip: () => !exec('minio -v').code,
|
|
||||||
operations: [
|
|
||||||
'nix-env -iA nixpkgs.minio',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
init: 'install.compiler',
|
init: 'install.compiler',
|
||||||
operations: [
|
operations: [
|
||||||
@@ -249,8 +238,6 @@ To disable this feature, checkout our sourcecode.`);
|
|||||||
operations: [
|
operations: [
|
||||||
['pm2 stop all', { ignore: true }],
|
['pm2 stop all', { ignore: true }],
|
||||||
() => fs.writefile(`${__env.HOME}/.hydro/mount.yaml`, mount),
|
() => fs.writefile(`${__env.HOME}/.hydro/mount.yaml`, mount),
|
||||||
`echo "MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY}\nMINIO_SECRET_KEY=${MINIO_SECRET_KEY}" >/root/.hydro/env`,
|
|
||||||
`pm2 start "MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY} MINIO_SECRET_KEY=${MINIO_SECRET_KEY} minio server /data/file" --name minio`,
|
|
||||||
'pm2 start mongod --name mongodb -- --auth --bind_ip 0.0.0.0',
|
'pm2 start mongod --name mongodb -- --auth --bind_ip 0.0.0.0',
|
||||||
() => sleep(1000),
|
() => sleep(1000),
|
||||||
`pm2 start bash --name hydro-sandbox -- -c "ulimit -s unlimited && hydro-sandbox -mount-conf ${__env.HOME}/.hydro/mount.yaml"`,
|
`pm2 start bash --name hydro-sandbox -- -c "ulimit -s unlimited && hydro-sandbox -mount-conf ${__env.HOME}/.hydro/mount.yaml"`,
|
||||||
@@ -291,8 +278,6 @@ To disable this feature, checkout our sourcecode.`);
|
|||||||
() => log.info('extra.restartTerm'),
|
() => log.info('extra.restartTerm'),
|
||||||
() => log.info('extra.dbUser'),
|
() => log.info('extra.dbUser'),
|
||||||
() => log.info('extra.dbPassword', DATABASE_PASSWORD),
|
() => log.info('extra.dbPassword', DATABASE_PASSWORD),
|
||||||
() => log.info('MINIO_ACCESS_KEY=%s', MINIO_ACCESS_KEY),
|
|
||||||
() => log.info('MINIO_SECRET_KEY=%s', MINIO_SECRET_KEY),
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ in pkgs.dockerTools.buildImage {
|
|||||||
name = "hydro-web";
|
name = "hydro-web";
|
||||||
paths = [
|
paths = [
|
||||||
hydro.mongodb4
|
hydro.mongodb4
|
||||||
pkgs.minio
|
|
||||||
pkgs.nodejs
|
pkgs.nodejs
|
||||||
pkgs.yarn
|
pkgs.yarn
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ in pkgs.buildEnv {
|
|||||||
name = "hydro-env";
|
name = "hydro-env";
|
||||||
paths = [
|
paths = [
|
||||||
mongo
|
mongo
|
||||||
pkgs.minio
|
|
||||||
pkgs.nodejs
|
pkgs.nodejs
|
||||||
pkgs.yarn
|
pkgs.yarn
|
||||||
pkgs.git
|
pkgs.git
|
||||||
|
|||||||
+1
-2
@@ -15,7 +15,6 @@ mongo 127.0.0.1:27017/hydro /tmp/createUser.js
|
|||||||
echo "{\"host\":\"127.0.0.1\",\"port\":\"27017\",\"name\":\"hydro\",\"username\":\"hydro\",\"password\":\"$db_password\"}" >~/.hydro/config.json
|
echo "{\"host\":\"127.0.0.1\",\"port\":\"27017\",\"name\":\"hydro\",\"username\":\"hydro\",\"password\":\"$db_password\"}" >~/.hydro/config.json
|
||||||
pm2 stop mongod
|
pm2 stop mongod
|
||||||
pm2 del mongod
|
pm2 del mongod
|
||||||
pm2 restart minio
|
|
||||||
pm2 start mongodb
|
pm2 start mongodb
|
||||||
pm2 restart hydrooj
|
pm2 restart all
|
||||||
pm2 save
|
pm2 save
|
||||||
@@ -7,8 +7,6 @@ echo "详情请参阅文档 -> https://hydro.js.org"
|
|||||||
echo "使用 Ctrl-C 退出该脚本,或是等待十秒后继续。"
|
echo "使用 Ctrl-C 退出该脚本,或是等待十秒后继续。"
|
||||||
echo "Will continue installation in 10 secs, press Ctrl-C to exit"
|
echo "Will continue installation in 10 secs, press Ctrl-C to exit"
|
||||||
sleep 10
|
sleep 10
|
||||||
MINIO_ACCESS_KEY=$(cat /dev/urandom | head -n 10 | md5sum | head -c 20)
|
|
||||||
MINIO_SECRET_KEY=$(cat /dev/urandom | head -n 10 | md5sum | head -c 20)
|
|
||||||
|
|
||||||
# Basic
|
# Basic
|
||||||
echo "apt-get update"
|
echo "apt-get update"
|
||||||
@@ -54,11 +52,6 @@ pm2 del mongod >/dev/null
|
|||||||
echo 'Starting mongodb'
|
echo 'Starting mongodb'
|
||||||
pm2 start "mongod --auth --bind_ip 0.0.0.0" --name mongodb
|
pm2 start "mongod --auth --bind_ip 0.0.0.0" --name mongodb
|
||||||
|
|
||||||
# Install MinIO
|
|
||||||
wget http://dl.minio.org.cn/server/minio/release/linux-amd64/minio
|
|
||||||
chmod +x minio
|
|
||||||
pm2 start "MINIO_ACCESS_KEY=$MINIO_ACCESS_KEY MINIO_SECRET_KEY=$MINIO_SECRET_KEY ./minio server /data/file" --name minio
|
|
||||||
|
|
||||||
# Install Compiler
|
# Install Compiler
|
||||||
echo 'Installing g++'
|
echo 'Installing g++'
|
||||||
apt-get install -y g++ >/dev/null
|
apt-get install -y g++ >/dev/null
|
||||||
@@ -81,5 +74,3 @@ pm2 save
|
|||||||
echo "Done"
|
echo "Done"
|
||||||
echo "Database username: hydro"
|
echo "Database username: hydro"
|
||||||
echo "Database password: $db_password"
|
echo "Database password: $db_password"
|
||||||
echo "MINIO_ACCESS_KEY=$MINIO_ACCESS_KEY
|
|
||||||
MINIO_SECRET_KEY=$MINIO_SECRET_KEY" >~/.hydro/env
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hydrooj",
|
"name": "hydrooj",
|
||||||
"version": "3.15.19",
|
"version": "3.16.0",
|
||||||
"bin": "bin/hydrooj.js",
|
"bin": "bin/hydrooj.js",
|
||||||
"main": "src/loader",
|
"main": "src/loader",
|
||||||
"module": "src/loader",
|
"module": "src/loader",
|
||||||
@@ -15,7 +15,6 @@
|
|||||||
"@graphql-tools/schema": "^8.5.1",
|
"@graphql-tools/schema": "^8.5.1",
|
||||||
"@hydrooj/utils": "workspace:*",
|
"@hydrooj/utils": "workspace:*",
|
||||||
"adm-zip": "0.5.5",
|
"adm-zip": "0.5.5",
|
||||||
"ajv": "^8.11.0",
|
|
||||||
"cac": "^6.7.12",
|
"cac": "^6.7.12",
|
||||||
"cookies": "^0.8.0",
|
"cookies": "^0.8.0",
|
||||||
"detect-browser": "^5.3.0",
|
"detect-browser": "^5.3.0",
|
||||||
|
|||||||
@@ -88,8 +88,9 @@ export async function load() {
|
|||||||
require('../options');
|
require('../options');
|
||||||
const opts = options();
|
const opts = options();
|
||||||
await db.start(opts);
|
await db.start(opts);
|
||||||
|
await require('../settings').loadConfig();
|
||||||
const storage = require('../service/storage');
|
const storage = require('../service/storage');
|
||||||
await storage.start();
|
await storage.loadStorageService();
|
||||||
require('../lib/index');
|
require('../lib/index');
|
||||||
await lib(pending, fail);
|
await lib(pending, fail);
|
||||||
const systemModel = require('../model/system');
|
const systemModel = require('../model/system');
|
||||||
|
|||||||
@@ -37,12 +37,13 @@ export async function load() {
|
|||||||
if (detail) logger.info('finish: locale/template/static');
|
if (detail) logger.info('finish: locale/template/static');
|
||||||
const opts = options();
|
const opts = options();
|
||||||
await db.start(opts);
|
await db.start(opts);
|
||||||
|
await require('../settings').loadConfig();
|
||||||
if (detail) logger.info('finish: db.connect');
|
if (detail) logger.info('finish: db.connect');
|
||||||
const modelSystem = require('../model/system');
|
const modelSystem = require('../model/system');
|
||||||
await modelSystem.runConfig();
|
await modelSystem.runConfig();
|
||||||
if (detail) logger.info('finish: config');
|
if (detail) logger.info('finish: config');
|
||||||
const storage = require('../service/storage');
|
const storage = require('../service/storage');
|
||||||
await storage.start();
|
await storage.loadStorageService();
|
||||||
if (detail) logger.info('finish: storage.connect');
|
if (detail) logger.info('finish: storage.connect');
|
||||||
require('../lib/index');
|
require('../lib/index');
|
||||||
if (detail) logger.info('finish: lib.builtin');
|
if (detail) logger.info('finish: lib.builtin');
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { exec } from 'child_process';
|
import { exec } from 'child_process';
|
||||||
import { inspect } from 'util';
|
import { inspect } from 'util';
|
||||||
import Ajv from 'ajv';
|
|
||||||
import * as yaml from 'js-yaml';
|
import * as yaml from 'js-yaml';
|
||||||
|
import Schema from 'schemastery';
|
||||||
import * as check from '../check';
|
import * as check from '../check';
|
||||||
import { BadRequestError, ValidationError } from '../error';
|
import { BadRequestError, ValidationError } from '../error';
|
||||||
import {
|
import {
|
||||||
@@ -19,12 +19,10 @@ import {
|
|||||||
Connection, ConnectionHandler, Handler,
|
Connection, ConnectionHandler, Handler,
|
||||||
param, Route, Types,
|
param, Route, Types,
|
||||||
} from '../service/server';
|
} from '../service/server';
|
||||||
import { schema } from '../settings';
|
import { configSource, saveConfig, SystemSettings } from '../settings';
|
||||||
import * as judge from './judge';
|
import * as judge from './judge';
|
||||||
|
|
||||||
const logger = new Logger('manage');
|
const logger = new Logger('manage');
|
||||||
const ajv = new Ajv({ useDefaults: true });
|
|
||||||
const validator = ajv.compile<any>(schema);
|
|
||||||
|
|
||||||
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]) {
|
||||||
@@ -156,55 +154,12 @@ class SystemSettingHandler extends SystemHandler {
|
|||||||
for (const s of this.response.body.settings) {
|
for (const s of this.response.body.settings) {
|
||||||
this.response.body.current[s.key] = system.get(s.key);
|
this.response.body.current[s.key] = system.get(s.key);
|
||||||
}
|
}
|
||||||
const hide = [];
|
|
||||||
const raw = system.get('_') || '';
|
|
||||||
let config = yaml.load(raw);
|
|
||||||
const valid = validator(config);
|
|
||||||
if (!(raw.trim() && valid)) {
|
|
||||||
const data = {};
|
|
||||||
for (const key in schema.properties) {
|
|
||||||
data[key] = {};
|
|
||||||
for (const subkey in (schema.definitions[key] as any).properties || {}) {
|
|
||||||
if (system.get(`${key}.${subkey}`)) {
|
|
||||||
data[key][subkey] = system.get(`${key}.${subkey}`);
|
|
||||||
if ((schema.definitions[key] as any).properties[subkey]?.writeOnly) {
|
|
||||||
if (data[key][subkey] instanceof Array) {
|
|
||||||
hide.push(...data[key][subkey]);
|
|
||||||
} else hide.push(data[key][subkey]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
validator(data);
|
|
||||||
config = yaml.dump(data);
|
|
||||||
} else config = yaml.dump(config);
|
|
||||||
this.response.body.hide = hide;
|
|
||||||
this.response.body.config = config;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async post(args: any) {
|
async post(args: any) {
|
||||||
const tasks = [];
|
const tasks = [];
|
||||||
const booleanKeys = args.booleanKeys || {};
|
const booleanKeys = args.booleanKeys || {};
|
||||||
delete args.booleanKeys;
|
delete args.booleanKeys;
|
||||||
if (args._) {
|
|
||||||
if (typeof args._ !== 'string') throw new ValidationError('config');
|
|
||||||
try {
|
|
||||||
const payload = yaml.load(args._);
|
|
||||||
const valid = validator(payload);
|
|
||||||
if (!valid) {
|
|
||||||
throw new ValidationError('config', null, validator.errors[0]);
|
|
||||||
}
|
|
||||||
for (const key in payload) {
|
|
||||||
for (const subkey in payload[key]) {
|
|
||||||
tasks.push(system.set(`${key}.${subkey}`, payload[key][subkey]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
throw new ValidationError('config', null, e.message);
|
|
||||||
}
|
|
||||||
await system.set('_', args._);
|
|
||||||
delete args._;
|
|
||||||
}
|
|
||||||
for (const key in args) {
|
for (const key in args) {
|
||||||
if (typeof args[key] === 'object') {
|
if (typeof args[key] === 'object') {
|
||||||
for (const subkey in args[key]) {
|
for (const subkey in args[key]) {
|
||||||
@@ -228,10 +183,25 @@ class SystemSettingHandler extends SystemHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SystemSettingSchemaHandler extends SystemHandler {
|
class SystemConfigHandler extends SystemHandler {
|
||||||
async get() {
|
async get() {
|
||||||
this.response.body = JSON.stringify(schema);
|
this.response.template = 'manage_config.html';
|
||||||
this.response.type = 'application/json';
|
this.response.body = {
|
||||||
|
schema: Schema.intersect(SystemSettings).toJSON(),
|
||||||
|
value: configSource,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@param('value', Types.String)
|
||||||
|
async post(domainId: string, value: string) {
|
||||||
|
let config;
|
||||||
|
try {
|
||||||
|
config = yaml.load(value);
|
||||||
|
Schema.intersect(SystemSettings)(config);
|
||||||
|
} catch (e) {
|
||||||
|
throw new ValidationError('value');
|
||||||
|
}
|
||||||
|
await saveConfig(config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,7 +264,7 @@ async function apply() {
|
|||||||
Route('manage_dashboard', '/manage/dashboard', SystemDashboardHandler);
|
Route('manage_dashboard', '/manage/dashboard', SystemDashboardHandler);
|
||||||
Route('manage_script', '/manage/script', SystemScriptHandler);
|
Route('manage_script', '/manage/script', SystemScriptHandler);
|
||||||
Route('manage_setting', '/manage/setting', SystemSettingHandler);
|
Route('manage_setting', '/manage/setting', SystemSettingHandler);
|
||||||
Route('manage_setting_schema', '/manage/setting/schema.json', SystemSettingSchemaHandler);
|
Route('manage_config', '/manage/config', SystemConfigHandler);
|
||||||
Route('manage_user_import', '/manage/userimport', SystemUserImportHandler);
|
Route('manage_user_import', '/manage/userimport', SystemUserImportHandler);
|
||||||
Connection('manage_check', '/manage/check-conn', SystemCheckConnHandler);
|
Connection('manage_check', '/manage/check-conn', SystemCheckConnHandler);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
import { statSync } from 'fs';
|
import { statSync } from 'fs';
|
||||||
import { pick } from 'lodash';
|
import { pick } from 'lodash';
|
||||||
|
import { lookup } from 'mime-types';
|
||||||
import {
|
import {
|
||||||
BadRequestError, ForbiddenError, ValidationError,
|
BadRequestError, ForbiddenError, ValidationError,
|
||||||
} from '../error';
|
} from '../error';
|
||||||
|
import { md5 } from '../lib/crypto';
|
||||||
import { PRIV } from '../model/builtin';
|
import { PRIV } from '../model/builtin';
|
||||||
import * as oplog from '../model/oplog';
|
import * as oplog from '../model/oplog';
|
||||||
import storage from '../model/storage';
|
import storage from '../model/storage';
|
||||||
@@ -12,6 +14,8 @@ import user from '../model/user';
|
|||||||
import {
|
import {
|
||||||
Handler, param, post, Route, Types,
|
Handler, param, post, Route, Types,
|
||||||
} from '../service/server';
|
} from '../service/server';
|
||||||
|
import { encodeRFC5987ValueChars } from '../service/storage';
|
||||||
|
import { builtinConfig } from '../settings';
|
||||||
import { sortFiles } from '../utils';
|
import { sortFiles } from '../utils';
|
||||||
|
|
||||||
class SwitchLanguageHandler extends Handler {
|
class SwitchLanguageHandler extends Handler {
|
||||||
@@ -101,6 +105,23 @@ export class FSDownloadHandler extends Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class StorageHandler extends Handler {
|
||||||
|
@param('target', Types.Name)
|
||||||
|
@param('filename', Types.Name, true)
|
||||||
|
@param('expire', Types.UnsignedInt)
|
||||||
|
@param('secret', Types.String)
|
||||||
|
async get(domainId: string, target: string, filename = '', expire: number, secret: string) {
|
||||||
|
const expected = md5(`${target}/${expire}/${builtinConfig.file.secret}`);
|
||||||
|
if (expire < Date.now()) throw new ForbiddenError('Link expired');
|
||||||
|
if (secret !== expected) throw new ForbiddenError('Invalid secret');
|
||||||
|
this.response.body = await storage.get(target);
|
||||||
|
this.response.type = (target.endsWith('.out') || target.endsWith('.ans'))
|
||||||
|
? 'text/plain'
|
||||||
|
: lookup(target) || 'application/octet-stream';
|
||||||
|
if (filename) this.response.disposition = `attachment; filename="${encodeRFC5987ValueChars(filename)}"`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class SwitchAccountHandler extends Handler {
|
export class SwitchAccountHandler extends Handler {
|
||||||
@param('uid', Types.Int)
|
@param('uid', Types.Int)
|
||||||
async get(domainId: string, uid: number) {
|
async get(domainId: string, uid: number) {
|
||||||
@@ -113,6 +134,7 @@ export async function apply() {
|
|||||||
Route('switch_language', '/language/:lang', SwitchLanguageHandler);
|
Route('switch_language', '/language/:lang', SwitchLanguageHandler);
|
||||||
Route('home_files', '/file', FilesHandler);
|
Route('home_files', '/file', FilesHandler);
|
||||||
Route('fs_download', '/file/:uid/:filename', FSDownloadHandler);
|
Route('fs_download', '/file/:uid/:filename', FSDownloadHandler);
|
||||||
|
Route('storage', '/storage', StorageHandler);
|
||||||
Route('switch_account', '/account', SwitchAccountHandler, PRIV.PRIV_EDIT_SYSTEM);
|
Route('switch_account', '/account', SwitchAccountHandler, PRIV.PRIV_EDIT_SYSTEM);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -183,14 +183,6 @@ const ignoreUA = [
|
|||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
SystemSetting(
|
SystemSetting(
|
||||||
Setting('setting_file', 'file.endPoint', 'http://127.0.0.1:9000', 'text', 'file.endPoint', 'Storage engine endPoint'),
|
|
||||||
Setting('setting_file', 'file.accessKey', null, 'text', 'file.accessKey', 'Storage engine accessKey'),
|
|
||||||
Setting('setting_file', 'file.secretKey', null, 'password', 'file.secretKey', 'Storage engine secret', FLAG_SECRET),
|
|
||||||
Setting('setting_file', 'file.bucket', 'hydro', 'text', 'file.bucket', 'Storage engine bucket'),
|
|
||||||
Setting('setting_file', 'file.region', 'us-east-1', 'text', 'file.region', 'Storage engine region'),
|
|
||||||
Setting('setting_file', 'file.pathStyle', true, 'boolean', 'file.pathStyle', 'pathStyle endpoint'),
|
|
||||||
Setting('setting_file', 'file.endPointForUser', '/fs/', 'text', 'file.endPointForUser', 'EndPoint for user'),
|
|
||||||
Setting('setting_file', 'file.endPointForJudge', '/fs/', 'text', 'file.endPointForJudge', 'EndPoint for judge'),
|
|
||||||
Setting('setting_smtp', 'smtp.user', null, 'text', 'smtp.user', 'SMTP Username'),
|
Setting('setting_smtp', 'smtp.user', null, 'text', 'smtp.user', 'SMTP Username'),
|
||||||
Setting('setting_smtp', 'smtp.pass', null, 'password', 'smtp.pass', 'SMTP Password', FLAG_SECRET),
|
Setting('setting_smtp', 'smtp.pass', null, 'password', 'smtp.pass', 'SMTP Password', FLAG_SECRET),
|
||||||
Setting('setting_smtp', 'smtp.host', null, 'text', 'smtp.host', 'SMTP Server Host'),
|
Setting('setting_smtp', 'smtp.host', null, 'text', 'smtp.host', 'SMTP Server Host'),
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { extname } from 'path';
|
import { extname } from 'path';
|
||||||
import { escapeRegExp } from 'lodash';
|
import { escapeRegExp } from 'lodash';
|
||||||
import { lookup } from 'mime-types';
|
import { lookup } from 'mime-types';
|
||||||
import { ItemBucketMetadata } from 'minio';
|
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import type { Readable } from 'stream';
|
import type { Readable } from 'stream';
|
||||||
@@ -14,11 +13,8 @@ import TaskModel from './task';
|
|||||||
export class StorageModel {
|
export class StorageModel {
|
||||||
static coll = db.collection('storage');
|
static coll = db.collection('storage');
|
||||||
|
|
||||||
static async put(path: string, file: string | Buffer | Readable, meta?: ItemBucketMetadata, owner?: number);
|
static async put(path: string, file: string | Buffer | Readable, owner?: number) {
|
||||||
static async put(path: string, file: string | Buffer | Readable, owner?: number);
|
const meta = {};
|
||||||
static async put(path: string, file: string | Buffer | Readable, arg0?: ItemBucketMetadata | number, arg1?: number) {
|
|
||||||
const meta = typeof arg0 === 'object' ? arg0 : {};
|
|
||||||
const owner = (typeof arg0 === 'number' ? arg0 : arg1) ?? 1;
|
|
||||||
await StorageModel.del([path]);
|
await StorageModel.del([path]);
|
||||||
meta['Content-Type'] = (path.endsWith('.ans') || path.endsWith('.out'))
|
meta['Content-Type'] = (path.endsWith('.ans') || path.endsWith('.out'))
|
||||||
? 'text/plain'
|
? 'text/plain'
|
||||||
@@ -27,7 +23,7 @@ export class StorageModel {
|
|||||||
// Make sure id is not used
|
// Make sure id is not used
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
while (await StorageModel.coll.findOne({ _id })) _id = `${nanoid(3)}/${nanoid()}${extname(path)}`;
|
while (await StorageModel.coll.findOne({ _id })) _id = `${nanoid(3)}/${nanoid()}${extname(path)}`;
|
||||||
await storage.put(_id, file, meta);
|
await storage.put(_id, file);
|
||||||
const { metaData, size, etag } = await storage.getMeta(_id);
|
const { metaData, size, etag } = await storage.getMeta(_id);
|
||||||
await StorageModel.coll.insertOne({
|
await StorageModel.coll.insertOne({
|
||||||
_id, meta: metaData, path, size, etag, lastModified: new Date(), owner,
|
_id, meta: metaData, path, size, etag, lastModified: new Date(), owner,
|
||||||
@@ -63,13 +59,8 @@ export class StorageModel {
|
|||||||
static async list(target: string, recursive = true) {
|
static async list(target: string, recursive = true) {
|
||||||
if (target.includes('..') || target.includes('//')) throw new Error('Invalid path');
|
if (target.includes('..') || target.includes('//')) throw new Error('Invalid path');
|
||||||
if (target.length && !target.endsWith('/')) target += '/';
|
if (target.length && !target.endsWith('/')) target += '/';
|
||||||
const results = recursive
|
const results = await StorageModel.coll.find({
|
||||||
? await StorageModel.coll.find({
|
path: { $regex: new RegExp(`^${escapeRegExp(target)}${recursive ? '' : '[^/]+$'}`, 'i') },
|
||||||
path: { $regex: new RegExp(`^${escapeRegExp(target)}`, 'i') },
|
|
||||||
autoDelete: null,
|
|
||||||
}).toArray()
|
|
||||||
: await StorageModel.coll.find({
|
|
||||||
path: { $regex: new RegExp(`^${escapeRegExp(target)}[^/]+$`) },
|
|
||||||
autoDelete: null,
|
autoDelete: null,
|
||||||
}).toArray();
|
}).toArray();
|
||||||
return results.map((i) => ({
|
return results.map((i) => ({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import fs from 'fs';
|
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { existsSync, readFileSync } from 'fs-extra';
|
||||||
import { findFileSync } from '@hydrooj/utils/lib/utils';
|
import { findFileSync } from '@hydrooj/utils/lib/utils';
|
||||||
import { Logger } from './logger';
|
import { Logger } from './logger';
|
||||||
|
|
||||||
@@ -8,8 +8,8 @@ const logger = new Logger('options');
|
|||||||
|
|
||||||
export = function load() {
|
export = function load() {
|
||||||
const envFile = path.resolve(os.homedir(), '.hydro', 'env');
|
const envFile = path.resolve(os.homedir(), '.hydro', 'env');
|
||||||
if (fs.existsSync(envFile)) {
|
if (existsSync(envFile)) {
|
||||||
const content = fs.readFileSync(envFile).toString().replace(/\r/g, '');
|
const content = readFileSync(envFile).toString().replace(/\r/g, '');
|
||||||
for (const line of content.split('\n')) {
|
for (const line of content.split('\n')) {
|
||||||
if (!line.includes('=')) continue;
|
if (!line.includes('=')) continue;
|
||||||
process.env[line.split('=')[0]] = line.split('=')[1].trim();
|
process.env[line.split('=')[0]] = line.split('=')[1].trim();
|
||||||
@@ -19,7 +19,7 @@ export = function load() {
|
|||||||
if (!f) return null;
|
if (!f) return null;
|
||||||
let result: any = {};
|
let result: any = {};
|
||||||
try {
|
try {
|
||||||
result = JSON.parse(fs.readFileSync(f).toString());
|
result = JSON.parse(readFileSync(f, 'utf-8'));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Cannot read config file %o', e);
|
logger.error('Cannot read config file %o', e);
|
||||||
result = {};
|
result = {};
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { PERM, PRIV } from '../model/builtin';
|
|||||||
import * as opcount from '../model/opcount';
|
import * as opcount from '../model/opcount';
|
||||||
import * as system from '../model/system';
|
import * as system from '../model/system';
|
||||||
import { User } from '../model/user';
|
import { User } from '../model/user';
|
||||||
|
import { builtinConfig } from '../settings';
|
||||||
import { errorMessage } from '../utils';
|
import { errorMessage } from '../utils';
|
||||||
import * as bus from './bus';
|
import * as bus from './bus';
|
||||||
import * as decorators from './decorators';
|
import * as decorators from './decorators';
|
||||||
@@ -29,6 +30,7 @@ import rendererLayer from './layers/renderer';
|
|||||||
import responseLayer from './layers/response';
|
import responseLayer from './layers/response';
|
||||||
import userLayer from './layers/user';
|
import userLayer from './layers/user';
|
||||||
import { Router } from './router';
|
import { Router } from './router';
|
||||||
|
import { encodeRFC5987ValueChars } from './storage';
|
||||||
|
|
||||||
export * from './decorators';
|
export * from './decorators';
|
||||||
|
|
||||||
@@ -109,11 +111,17 @@ const serializer = (showDisplayName = false) => (k: string, v: any) => {
|
|||||||
|
|
||||||
export async function prepare() {
|
export async function prepare() {
|
||||||
app.keys = system.get('session.keys') as unknown as string[];
|
app.keys = system.get('session.keys') as unknown as string[];
|
||||||
app.use(proxy('/fs', {
|
const proxyMiddleware = proxy('/fs', {
|
||||||
target: system.get('file.endPoint'),
|
target: builtinConfig.file.endPoint,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (p) => p.replace('/fs', ''),
|
rewrite: (p) => p.replace('/fs', ''),
|
||||||
}));
|
});
|
||||||
|
app.use(async (ctx, next) => {
|
||||||
|
if (!ctx.path.startsWith('/fs/')) return await next();
|
||||||
|
if (ctx.request.search.toLowerCase().includes('x-amz-credential')) return await proxyMiddleware(ctx, next);
|
||||||
|
ctx.request.path = ctx.path = ctx.path.split('/fs')[1];
|
||||||
|
return await next();
|
||||||
|
});
|
||||||
app.use(Compress());
|
app.use(Compress());
|
||||||
for (const dir of global.publicDirs) {
|
for (const dir of global.publicDirs) {
|
||||||
app.use(cache(dir, {
|
app.use(cache(dir, {
|
||||||
@@ -122,9 +130,9 @@ export async function prepare() {
|
|||||||
}
|
}
|
||||||
if (process.env.DEV) {
|
if (process.env.DEV) {
|
||||||
app.use(async (ctx: Context, next: Function) => {
|
app.use(async (ctx: Context, next: Function) => {
|
||||||
const startTime = new Date().getTime();
|
const startTime = Date.now();
|
||||||
await next();
|
await next();
|
||||||
const endTime = new Date().getTime();
|
const endTime = Date.now();
|
||||||
if (ctx.nolog || ctx.response.headers.nolog) return;
|
if (ctx.nolog || ctx.response.headers.nolog) return;
|
||||||
ctx._remoteAddress = ctx.request.ip;
|
ctx._remoteAddress = ctx.request.ip;
|
||||||
logger.debug(`${ctx.request.method} /${ctx.domainId || 'system'}${ctx.request.path} \
|
logger.debug(`${ctx.request.method} /${ctx.domainId || 'system'}${ctx.request.path} \
|
||||||
@@ -216,7 +224,7 @@ export class Handler extends HandlerCommon {
|
|||||||
this.response.body = data;
|
this.response.body = data;
|
||||||
this.response.template = null;
|
this.response.template = null;
|
||||||
this.response.type = 'application/octet-stream';
|
this.response.type = 'application/octet-stream';
|
||||||
if (name) this.response.disposition = `attachment; filename="${encodeURIComponent(name)}"`;
|
if (name) this.response.disposition = `attachment; filename="${encodeRFC5987ValueChars(name)}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
|
import { dirname, resolve } from 'path';
|
||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
import { URL } from 'url';
|
import { URL } from 'url';
|
||||||
import { createReadStream } from 'fs-extra';
|
import {
|
||||||
|
copyFile, createReadStream, ensureDir,
|
||||||
|
remove, stat, writeFile,
|
||||||
|
} from 'fs-extra';
|
||||||
|
import { lookup } from 'mime-types';
|
||||||
import { BucketItem, Client, ItemBucketMetadata } from 'minio';
|
import { BucketItem, Client, ItemBucketMetadata } from 'minio';
|
||||||
|
import { md5 } from '../lib/crypto';
|
||||||
import { Logger } from '../logger';
|
import { Logger } from '../logger';
|
||||||
import * as system from '../model/system';
|
import { builtinConfig } from '../settings';
|
||||||
|
import { MaybeArray } from '../typeutils';
|
||||||
|
|
||||||
const logger = new Logger('storage');
|
const logger = new Logger('storage');
|
||||||
|
|
||||||
@@ -18,38 +25,38 @@ interface StorageOptions {
|
|||||||
endPointForJudge?: string;
|
endPointForJudge?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MinioEndpointConfig {
|
interface EndpointConfig {
|
||||||
endPoint: string;
|
endPoint: string;
|
||||||
port: number;
|
port: number;
|
||||||
useSSL: boolean;
|
useSSL: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseMainEndpointUrl(endpoint: string): MinioEndpointConfig {
|
function parseMainEndpointUrl(endpoint: string): EndpointConfig {
|
||||||
if (!endpoint) throw new Error('Empty endpoint');
|
if (!endpoint) throw new Error('Empty endpoint');
|
||||||
const url = new URL(endpoint);
|
const url = new URL(endpoint);
|
||||||
const result: Partial<MinioEndpointConfig> = {};
|
const result: Partial<EndpointConfig> = {};
|
||||||
if (url.pathname !== '/') throw new Error('Main MinIO endpoint URL of a sub-directory is not supported.');
|
if (url.pathname !== '/') throw new Error('Main endpoint URL of a sub-directory is not supported.');
|
||||||
if (url.username || url.password || url.hash || url.search) {
|
if (url.username || url.password || url.hash || url.search) {
|
||||||
throw new Error('Authorization, search parameters and hash are not supported for main MinIO endpoint URL.');
|
throw new Error('Authorization, search parameters and hash are not supported for main endpoint URL.');
|
||||||
}
|
}
|
||||||
if (url.protocol === 'http:') result.useSSL = false;
|
if (url.protocol === 'http:') result.useSSL = false;
|
||||||
else if (url.protocol === 'https:') result.useSSL = true;
|
else if (url.protocol === 'https:') result.useSSL = true;
|
||||||
else {
|
else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Invalid protocol "${url.protocol}" for main MinIO endpoint URL. Only HTTP and HTTPS are supported.`,
|
`Invalid protocol "${url.protocol}" for main endpoint URL. Only HTTP and HTTPS are supported.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
result.endPoint = url.hostname;
|
result.endPoint = url.hostname;
|
||||||
result.port = url.port ? Number(url.port) : result.useSSL ? 443 : 80;
|
result.port = url.port ? Number(url.port) : result.useSSL ? 443 : 80;
|
||||||
return result as MinioEndpointConfig;
|
return result as EndpointConfig;
|
||||||
}
|
}
|
||||||
function parseAlternativeEndpointUrl(endpoint: string): (originalUrl: string) => string {
|
function parseAlternativeEndpointUrl(endpoint: string): (originalUrl: string) => string {
|
||||||
if (!endpoint) return (originalUrl) => originalUrl;
|
if (!endpoint) return (originalUrl) => originalUrl;
|
||||||
const pathonly = endpoint.startsWith('/');
|
const pathonly = endpoint.startsWith('/');
|
||||||
if (pathonly) endpoint = `https://localhost${endpoint}`;
|
if (pathonly) endpoint = `https://localhost${endpoint}`;
|
||||||
const url = new URL(endpoint);
|
const url = new URL(endpoint);
|
||||||
if (url.hash || url.search) throw new Error('Search parameters and hash are not supported for alternative MinIO endpoint URL.');
|
if (url.hash || url.search) throw new Error('Search parameters and hash are not supported for alternative endpoint URL.');
|
||||||
if (!url.pathname.endsWith('/')) throw new Error("Alternative MinIO endpoint URL's pathname must ends with '/'.");
|
if (!url.pathname.endsWith('/')) throw new Error("Alternative endpoint URL's pathname must ends with '/'.");
|
||||||
return (originalUrl) => {
|
return (originalUrl) => {
|
||||||
const parsedOriginUrl = new URL(originalUrl);
|
const parsedOriginUrl = new URL(originalUrl);
|
||||||
const replaced = new URL(parsedOriginUrl.pathname.slice(1) + parsedOriginUrl.search + parsedOriginUrl.hash, url).toString();
|
const replaced = new URL(parsedOriginUrl.pathname.slice(1) + parsedOriginUrl.search + parsedOriginUrl.hash, url).toString();
|
||||||
@@ -59,7 +66,7 @@ function parseAlternativeEndpointUrl(endpoint: string): (originalUrl: string) =>
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
|
||||||
function encodeRFC5987ValueChars(str: string) {
|
export function encodeRFC5987ValueChars(str: string) {
|
||||||
return (
|
return (
|
||||||
encodeURIComponent(str)
|
encodeURIComponent(str)
|
||||||
// Note that although RFC3986 reserves "!", RFC5987 does not,
|
// Note that although RFC3986 reserves "!", RFC5987 does not,
|
||||||
@@ -72,7 +79,7 @@ function encodeRFC5987ValueChars(str: string) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class StorageService {
|
class RemoteStorageService {
|
||||||
public client: Client;
|
public client: Client;
|
||||||
public error = '';
|
public error = '';
|
||||||
public opts: StorageOptions;
|
public opts: StorageOptions;
|
||||||
@@ -80,13 +87,17 @@ class StorageService {
|
|||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
try {
|
try {
|
||||||
const [
|
logger.info('Starting storage service with endpoint:', builtinConfig.file.endPoint);
|
||||||
endPoint, accessKey, secretKey, bucket, region,
|
const {
|
||||||
pathStyle, endPointForUser, endPointForJudge,
|
endPoint,
|
||||||
] = system.getMany([
|
accessKey,
|
||||||
'file.endPoint', 'file.accessKey', 'file.secretKey', 'file.bucket', 'file.region',
|
secretKey,
|
||||||
'file.pathStyle', 'file.endPointForUser', 'file.endPointForJudge',
|
bucket,
|
||||||
]);
|
region,
|
||||||
|
pathStyle,
|
||||||
|
endPointForUser,
|
||||||
|
endPointForJudge,
|
||||||
|
} = builtinConfig.file;
|
||||||
this.opts = {
|
this.opts = {
|
||||||
endPoint,
|
endPoint,
|
||||||
accessKey,
|
accessKey,
|
||||||
@@ -97,11 +108,6 @@ class StorageService {
|
|||||||
endPointForUser,
|
endPointForUser,
|
||||||
endPointForJudge,
|
endPointForJudge,
|
||||||
};
|
};
|
||||||
if (process.env.MINIO_ACCESS_KEY) {
|
|
||||||
logger.info('Using MinIO key from environment variables');
|
|
||||||
this.opts.accessKey = process.env.MINIO_ACCESS_KEY;
|
|
||||||
this.opts.secretKey = process.env.MINIO_SECRET_KEY;
|
|
||||||
}
|
|
||||||
this.client = new Client({
|
this.client = new Client({
|
||||||
...parseMainEndpointUrl(this.opts.endPoint),
|
...parseMainEndpointUrl(this.opts.endPoint),
|
||||||
pathStyle: this.opts.pathStyle,
|
pathStyle: this.opts.pathStyle,
|
||||||
@@ -133,7 +139,7 @@ class StorageService {
|
|||||||
if (target.includes('..') || target.includes('//')) throw new Error('Invalid path');
|
if (target.includes('..') || target.includes('//')) throw new Error('Invalid path');
|
||||||
if (typeof file === 'string') file = createReadStream(file);
|
if (typeof file === 'string') file = createReadStream(file);
|
||||||
try {
|
try {
|
||||||
return await this.client.putObject(this.opts.bucket, target, file, meta);
|
await this.client.putObject(this.opts.bucket, target, file, meta);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
e.stack = new Error().stack;
|
e.stack = new Error().stack;
|
||||||
throw e;
|
throw e;
|
||||||
@@ -154,11 +160,7 @@ class StorageService {
|
|||||||
async del(target: string | string[]) {
|
async del(target: string | string[]) {
|
||||||
if (typeof target === 'string') {
|
if (typeof target === 'string') {
|
||||||
if (target.includes('..') || target.includes('//')) throw new Error('Invalid path');
|
if (target.includes('..') || target.includes('//')) throw new Error('Invalid path');
|
||||||
} else {
|
} else if (target.find((t) => t.includes('..') || t.includes('//'))) throw new Error('Invalid path');
|
||||||
for (const t of target) {
|
|
||||||
if (t.includes('..') || t.includes('//')) throw new Error('Invalid path');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
if (typeof target === 'string') return await this.client.removeObject(this.opts.bucket, target);
|
if (typeof target === 'string') return await this.client.removeObject(this.opts.bucket, target);
|
||||||
return await this.client.removeObjects(this.opts.bucket, target);
|
return await this.client.removeObjects(this.opts.bucket, target);
|
||||||
@@ -173,7 +175,7 @@ class StorageService {
|
|||||||
if (target.includes('..') || target.includes('//')) throw new Error('Invalid path');
|
if (target.includes('..') || target.includes('//')) throw new Error('Invalid path');
|
||||||
try {
|
try {
|
||||||
const stream = this.client.listObjects(this.opts.bucket, target, recursive);
|
const stream = this.client.listObjects(this.opts.bucket, target, recursive);
|
||||||
return await new Promise<BucketItem[]>((resolve, reject) => {
|
return await new Promise<BucketItem[]>((r, reject) => {
|
||||||
const results: BucketItem[] = [];
|
const results: BucketItem[] = [];
|
||||||
stream.on('data', (result) => {
|
stream.on('data', (result) => {
|
||||||
if (result.size) {
|
if (result.size) {
|
||||||
@@ -184,7 +186,7 @@ class StorageService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
stream.on('end', () => resolve(results));
|
stream.on('end', () => r(results));
|
||||||
stream.on('error', reject);
|
stream.on('error', reject);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -237,6 +239,92 @@ class StorageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const service = new StorageService();
|
class LocalStorageService {
|
||||||
global.Hydro.service.storage = service;
|
client: null;
|
||||||
export = service;
|
error: null;
|
||||||
|
dir: string;
|
||||||
|
opts: null;
|
||||||
|
private replaceWithAlternativeUrlFor: Record<'user' | 'judge', (originalUrl: string) => string>;
|
||||||
|
|
||||||
|
async start() {
|
||||||
|
logger.debug('Loading local storage service with path:', builtinConfig.file.path);
|
||||||
|
await ensureDir(builtinConfig.file.path);
|
||||||
|
this.dir = builtinConfig.file.path;
|
||||||
|
this.replaceWithAlternativeUrlFor = {
|
||||||
|
user: parseAlternativeEndpointUrl(builtinConfig.file.endPointForUser),
|
||||||
|
judge: parseAlternativeEndpointUrl(builtinConfig.file.endPointForJudge),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async put(target: string, file: string | Buffer | Readable) {
|
||||||
|
if (target.includes('..') || target.includes('//')) throw new Error('Invalid path');
|
||||||
|
target = resolve(this.dir, target);
|
||||||
|
await ensureDir(dirname(target));
|
||||||
|
if (typeof file === 'string') await copyFile(file, target);
|
||||||
|
else await writeFile(target, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(target: string, path?: string) {
|
||||||
|
if (target.includes('..') || target.includes('//')) throw new Error('Invalid path');
|
||||||
|
target = resolve(this.dir, target);
|
||||||
|
if (path) await copyFile(target, path);
|
||||||
|
return createReadStream(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
async del(target: MaybeArray<string>) {
|
||||||
|
const targets = typeof target === 'string' ? [target] : target;
|
||||||
|
if (targets.find((i) => i.includes('..') || i.includes('//'))) throw new Error('Invalid path');
|
||||||
|
await Promise.all(targets.map((i) => remove(resolve(this.dir, i))));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMeta(target: string) {
|
||||||
|
if (target.includes('..') || target.includes('//')) throw new Error('Invalid path');
|
||||||
|
target = resolve(this.dir, target);
|
||||||
|
const file = await stat(target);
|
||||||
|
return {
|
||||||
|
size: file.size,
|
||||||
|
etag: Buffer.from(target).toString('base64'),
|
||||||
|
lastModified: file.mtime,
|
||||||
|
metaData: {
|
||||||
|
'Content-Type': (target.endsWith('.ans') || target.endsWith('.out'))
|
||||||
|
? 'text/plain'
|
||||||
|
: lookup(target) || 'application/octet-stream',
|
||||||
|
'Content-Length': file.size,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async signDownloadLink(target: string, filename = '', noExpire = false, useAlternativeEndpointFor?: 'user' | 'judge'): Promise<string> {
|
||||||
|
if (target.includes('..') || target.includes('//')) throw new Error('Invalid path');
|
||||||
|
const url = new URL('https://localhost/storage');
|
||||||
|
url.searchParams.set('target', target);
|
||||||
|
if (filename) url.searchParams.set('filename', filename);
|
||||||
|
const expire = (Date.now() + (noExpire ? 7 * 24 * 3600 : 600) * 1000).toString();
|
||||||
|
url.searchParams.set('expire', expire);
|
||||||
|
url.searchParams.set('secret', md5(`${target}/${expire}/${builtinConfig.file.secret}`));
|
||||||
|
if (useAlternativeEndpointFor) return this.replaceWithAlternativeUrlFor[useAlternativeEndpointFor](url.toString());
|
||||||
|
return `/${url.toString().split('localhost/')[1]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async signUpload() {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
async list() {
|
||||||
|
throw new Error('deprecated');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let service; // eslint-disable-line import/no-mutable-exports
|
||||||
|
|
||||||
|
export async function loadStorageService() {
|
||||||
|
service = builtinConfig.file.type === 's3' ? new RemoteStorageService() : new LocalStorageService();
|
||||||
|
global.Hydro.service.storage = service;
|
||||||
|
await service.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new Proxy({}, {
|
||||||
|
get(self, key) {
|
||||||
|
return service[key];
|
||||||
|
},
|
||||||
|
}) as RemoteStorageService | LocalStorageService;
|
||||||
+101
-126
@@ -1,136 +1,111 @@
|
|||||||
import { JSONSchema7Definition } from 'json-schema';
|
import yaml from 'js-yaml';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
import Schema from 'schemastery';
|
||||||
|
import * as bus from 'hydrooj/src/service/bus';
|
||||||
|
import { Logger } from './logger';
|
||||||
|
import { NestKeys } from './typeutils';
|
||||||
|
|
||||||
type Def = Exclude<JSONSchema7Definition, boolean>;
|
const defaultPath = process.env.CI ? '/tmp/file' : '/data/file/hydro';
|
||||||
|
const FileSetting = Schema.intersect([
|
||||||
|
Schema.object({
|
||||||
|
type: Schema.union([
|
||||||
|
Schema.const('file').description('local file provider').required(),
|
||||||
|
Schema.const('s3').description('s3 provider').required(),
|
||||||
|
] as const).description('provider type').default('file'),
|
||||||
|
endPointForUser: Schema.string().default('/fs/').required(),
|
||||||
|
endPointForJudge: Schema.string().default('/fs/').required(),
|
||||||
|
}).description('setting_file'),
|
||||||
|
Schema.union([
|
||||||
|
Schema.object({
|
||||||
|
type: Schema.const('file').required(),
|
||||||
|
path: Schema.string().default(defaultPath).description('Storage path').required(),
|
||||||
|
secret: Schema.string().description('Download file sign secret').default(nanoid()),
|
||||||
|
}),
|
||||||
|
Schema.object({
|
||||||
|
type: Schema.const('s3').required(),
|
||||||
|
endPoint: Schema.string().required(),
|
||||||
|
accessKey: Schema.string().required().description('access key'),
|
||||||
|
secretKey: Schema.string().required().description('secret key').role('secret'),
|
||||||
|
bucket: Schema.string().default('hydro').required(),
|
||||||
|
region: Schema.string().default('us-east-1').required(),
|
||||||
|
pathStyle: Schema.boolean().default(true).required(),
|
||||||
|
}),
|
||||||
|
] as const),
|
||||||
|
] as const).default({
|
||||||
|
type: 'file',
|
||||||
|
path: defaultPath,
|
||||||
|
endPointForUser: '/fs/',
|
||||||
|
endPointForJudge: '/fs/',
|
||||||
|
secret: nanoid(),
|
||||||
|
});
|
||||||
|
|
||||||
function port(examples: number[] = []) {
|
const builtinSettings = Schema.object({
|
||||||
const res: Def = {
|
file: FileSetting,
|
||||||
type: 'integer', minimum: 1, maximum: 65535,
|
});
|
||||||
};
|
export const SystemSettings: Schema[] = [builtinSettings];
|
||||||
if (examples.length) {
|
export let configSource = ''; // eslint-disable-line import/no-mutable-exports
|
||||||
res.default = examples[0];
|
export let systemConfig: any = {}; // eslint-disable-line import/no-mutable-exports
|
||||||
res.examples = examples;
|
const logger = new Logger('settings');
|
||||||
|
const update = [];
|
||||||
|
|
||||||
|
export async function loadConfig() {
|
||||||
|
const config = await global.Hydro.service.db.collection('system').findOne({ _id: 'config' });
|
||||||
|
try {
|
||||||
|
configSource = config?.value || '{}';
|
||||||
|
systemConfig = yaml.load(configSource);
|
||||||
|
logger.info('Successfully loaded config');
|
||||||
|
for (const u of update) u();
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Failed to load config', e.message);
|
||||||
}
|
}
|
||||||
return res;
|
}
|
||||||
|
export async function saveConfig(config: any) {
|
||||||
|
Schema.intersect(SystemSettings)(config);
|
||||||
|
const value = yaml.dump(config);
|
||||||
|
await global.Hydro.service.db.collection('system').updateOne({ _id: 'config' }, { $set: { value } }, { upsert: true });
|
||||||
|
bus.broadcast('config/update');
|
||||||
|
}
|
||||||
|
export async function setConfig(key: string, value: any) {
|
||||||
|
const path = key.split('.');
|
||||||
|
const t = path.pop();
|
||||||
|
let cursor = systemConfig;
|
||||||
|
for (const p of path) {
|
||||||
|
if (!cursor[p]) cursor[p] = {};
|
||||||
|
cursor = cursor[p];
|
||||||
|
}
|
||||||
|
cursor[t] = value;
|
||||||
|
await saveConfig(systemConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Schema = {
|
export function requestConfig<T, S>(s: Schema<T, S>): {
|
||||||
string<T extends Def>(title: string, defaultValue: string, extra?: T) {
|
config: ReturnType<Schema<T, S>>,
|
||||||
|
setConfig: (key: NestKeys<ReturnType<Schema<T, S>>>, value: any) => Promise<void>,
|
||||||
|
} {
|
||||||
|
SystemSettings.push(s);
|
||||||
|
let curValue = s(systemConfig);
|
||||||
|
update.push(() => {
|
||||||
|
try {
|
||||||
|
curValue = s(systemConfig);
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('Cannot read config: ', e.message);
|
||||||
|
curValue = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
type: 'string' as 'string',
|
config: new Proxy(curValue as any, {
|
||||||
default: defaultValue,
|
get(self, key: string) {
|
||||||
title,
|
return curValue?.[key];
|
||||||
...extra,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
boolean<T extends Def>(title: string, defaultValue: boolean, extra?: T) {
|
set(self) {
|
||||||
return {
|
throw new Error(`Not allowed to set setting ${self.p.join('.')}`);
|
||||||
type: 'boolean' as 'boolean',
|
|
||||||
default: defaultValue,
|
|
||||||
title,
|
|
||||||
...extra,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
integer<T extends Def>(title: string, defaultValue: number, extra?: T) {
|
|
||||||
return {
|
|
||||||
type: 'integer' as 'integer',
|
|
||||||
default: defaultValue,
|
|
||||||
title,
|
|
||||||
...extra,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const definitions: Record<string, Def> = {
|
|
||||||
smtp: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
user: Schema.string('SMTP Username', 'noreply@hydro.ac'),
|
|
||||||
from: Schema.string('Mail From', 'Hydro <noreply@hydro.ac>'),
|
|
||||||
pass: Schema.string('SMTP Password', '', { writeOnly: true }),
|
|
||||||
host: Schema.string('SMTP Server Host', 'smtp.hydro.ac', { pattern: '^[a-zA-Z0-9\\-\\.]+$' }),
|
|
||||||
port: Schema.integer('SMTP Server Port', 25, { examples: [25, 465], minimum: 1, maximum: 65535 }),
|
|
||||||
secure: Schema.boolean('Use SSL', false),
|
|
||||||
verify: Schema.boolean('Verify register email', false),
|
|
||||||
},
|
|
||||||
additionalProperties: false,
|
|
||||||
},
|
|
||||||
file: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
endPoint: Schema.string('Storage engine endPoint', 'http://localhost:9000', {
|
|
||||||
pattern: '^https?://[a-zA-Z0-9\\-\\.]+/?$',
|
|
||||||
}),
|
}),
|
||||||
accessKey: Schema.string('Storage engine accessKey', ''),
|
setConfig,
|
||||||
secretKey: Schema.string('Storage engine secretKey', '', { writeOnly: true }),
|
};
|
||||||
bucket: Schema.string('Storage engine bucket', 'hydro'),
|
|
||||||
region: Schema.string('Storage engine region', 'us-east-1'),
|
|
||||||
pathStyle: Schema.boolean('pathStyle endpoint', true),
|
|
||||||
endPointForUser: Schema.string('EndPoint for user', '/fs/'),
|
|
||||||
endPointForJudge: Schema.string('EndPoint for judge', '/fs/'),
|
|
||||||
},
|
|
||||||
required: ['endPoint', 'accessKey', 'secretKey'],
|
|
||||||
additionalProperties: false,
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
name: Schema.string('Server Name', 'Hydro'),
|
|
||||||
url: Schema.string('Self URL', 'https://hydro.ac/', { pattern: '/$' }),
|
|
||||||
cdn: Schema.string('CDN prefix', '/', {
|
|
||||||
pattern: '/$', examples: ['/', 'https://cdn.hydro.ac/'],
|
|
||||||
}),
|
|
||||||
port: port([8888, 80, 443]),
|
|
||||||
xff: Schema.string('IP Header', '', { examples: ['x-forwarded-for', 'x-real-ip'], pattern: '^[a-z-]+$' }),
|
|
||||||
xhost: Schema.string('Host Header', '', { examples: ['x-real-host'], pattern: '^[a-z-]+$' }),
|
|
||||||
language: { type: 'string', enum: Object.keys(global.Hydro.locales) },
|
|
||||||
upload: Schema.string('Upload size limit', '256m', { pattern: '^[0-9]+[mkg]b?$' }),
|
|
||||||
login: Schema.boolean('Enable builtin login', true),
|
|
||||||
message: Schema.boolean('Enable message', true),
|
|
||||||
blog: Schema.boolean('Enable blog', true),
|
|
||||||
checkUpdate: Schema.boolean('Daily update check', true),
|
|
||||||
},
|
|
||||||
required: ['url', 'port', 'language'],
|
|
||||||
},
|
|
||||||
limit: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
problem_files_max: { type: 'integer', minimum: 0 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
session: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
keys: {
|
|
||||||
type: 'array', items: { type: 'string' }, default: [String.random(32)], writeOnly: true,
|
|
||||||
},
|
|
||||||
secure: { type: 'boolean', default: false },
|
|
||||||
saved_expire_seconds: { type: 'integer', minimum: 300, default: 3600 * 24 * 30 },
|
|
||||||
unsaved_expire_seconds: { type: 'integer', minimum: 60, default: 3600 * 3 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
user: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
quota: { type: 'integer', minimum: 0 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const schema: Def = {
|
|
||||||
type: 'object',
|
|
||||||
definitions,
|
|
||||||
properties: {
|
|
||||||
smtp: definitions.smtp,
|
|
||||||
file: definitions.file,
|
|
||||||
server: definitions.server,
|
|
||||||
limit: definitions.limit,
|
|
||||||
session: definitions.session,
|
|
||||||
user: definitions.user,
|
|
||||||
},
|
|
||||||
additionalProperties: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
export function addDef(key: string, def: Def) {
|
|
||||||
definitions[key] = def;
|
|
||||||
schema.properties[key] = definitions[key];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const builtin = requestConfig(builtinSettings);
|
||||||
|
export const builtinConfig = builtin.config;
|
||||||
|
export const setBuiltinConfig = builtin.setConfig;
|
||||||
|
|
||||||
|
bus.on('config/update', loadConfig);
|
||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
} from './pipelineUtils';
|
} from './pipelineUtils';
|
||||||
import db from './service/db';
|
import db from './service/db';
|
||||||
import storage from './service/storage';
|
import storage from './service/storage';
|
||||||
|
import { setBuiltinConfig } from './settings';
|
||||||
import { streamToBuffer } from './utils';
|
import { streamToBuffer } from './utils';
|
||||||
import welcome from './welcome';
|
import welcome from './welcome';
|
||||||
|
|
||||||
@@ -94,7 +95,7 @@ const scripts: UpgradeScript[] = [
|
|||||||
try {
|
try {
|
||||||
const [file, current] = await Promise.all([
|
const [file, current] = await Promise.all([
|
||||||
streamToBuffer(gridfs.openDownloadStream(pdoc.data)),
|
streamToBuffer(gridfs.openDownloadStream(pdoc.data)),
|
||||||
storage.list(`problem/${pdoc.domainId}/${pdoc.docId}/testdata/`),
|
storage.list(`problem/${pdoc.domainId}/${pdoc.docId}/testdata/`) as any,
|
||||||
]);
|
]);
|
||||||
const zip = new AdmZip(file);
|
const zip = new AdmZip(file);
|
||||||
const entries = zip.getEntries();
|
const entries = zip.getEntries();
|
||||||
@@ -146,8 +147,8 @@ const scripts: UpgradeScript[] = [
|
|||||||
await iterateAllProblem(['docId', 'domainId', 'config'], async (pdoc) => {
|
await iterateAllProblem(['docId', 'domainId', 'config'], async (pdoc) => {
|
||||||
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/`) as any,
|
||||||
storage.list(`problem/${pdoc.domainId}/${pdoc.docId}/additional_file/`),
|
storage.list(`problem/${pdoc.domainId}/${pdoc.docId}/additional_file/`) as any,
|
||||||
]);
|
]);
|
||||||
await problem.edit(
|
await problem.edit(
|
||||||
pdoc.domainId, pdoc.docId,
|
pdoc.domainId, pdoc.docId,
|
||||||
@@ -802,6 +803,33 @@ const scripts: UpgradeScript[] = [
|
|||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
async function _66_67() {
|
||||||
|
const [
|
||||||
|
endPoint, accessKey, secretKey, bucket, region,
|
||||||
|
pathStyle, endPointForUser, endPointForJudge,
|
||||||
|
] = system.getMany([
|
||||||
|
'file.endPoint', 'file.accessKey', 'file.secretKey', 'file.bucket', 'file.region',
|
||||||
|
'file.pathStyle', 'file.endPointForUser', 'file.endPointForJudge',
|
||||||
|
]);
|
||||||
|
if ((endPoint && accessKey) || process.env.MINIO_ACCESS_KEY) {
|
||||||
|
await setBuiltinConfig('file', {
|
||||||
|
type: 's3',
|
||||||
|
endPoint: process.env.MINIO_ACCESS_KEY ? 'http://127.0.0.1:9000/' : endPoint,
|
||||||
|
accessKey: process.env.MINIO_ACCESS_KEY || accessKey,
|
||||||
|
secretKey: process.env.MINIO_SECRET_KEY || secretKey,
|
||||||
|
bucket,
|
||||||
|
region,
|
||||||
|
pathStyle,
|
||||||
|
endPointForUser,
|
||||||
|
endPointForJudge,
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
logger.info('Upgrade done. please restart the server.');
|
||||||
|
process.exit(0);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default scripts;
|
export default scripts;
|
||||||
@@ -96,7 +96,7 @@ export default function (env: { production?: boolean, measure?: boolean } = {})
|
|||||||
chunkFilename: '[name].[chunkhash].chunk.js',
|
chunkFilename: '[name].[chunkhash].chunk.js',
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
extensions: ['.js', '.jsx', '.ts', '.tsx', '.cjs'],
|
||||||
alias: {
|
alias: {
|
||||||
vj: root(),
|
vj: root(),
|
||||||
},
|
},
|
||||||
@@ -148,8 +148,9 @@ export default function (env: { production?: boolean, measure?: boolean } = {})
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.[jt]sx?$/,
|
test: /\.[mc]?[jt]sx?$/,
|
||||||
exclude: /@types\//,
|
exclude: /@types\//,
|
||||||
|
type: 'javascript/auto',
|
||||||
use: [esbuildLoader()],
|
use: [esbuildLoader()],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -107,7 +107,6 @@ async function onCommentClickReplyComment(ev, options: any = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onCommentClickReplyReply(ev) {
|
async function onCommentClickReplyReply(ev) {
|
||||||
console.log(ev);
|
|
||||||
const $evTarget = $(ev.currentTarget);
|
const $evTarget = $(ev.currentTarget);
|
||||||
const $mediaBody = $evTarget.closest('.media__body');
|
const $mediaBody = $evTarget.closest('.media__body');
|
||||||
const uid = $mediaBody
|
const uid = $mediaBody
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ function runSubstitute($container: JQuery<Document | HTMLElement>) {
|
|||||||
$container.find(`textarea[data-${language}]`).get().forEach((element) => {
|
$container.find(`textarea[data-${language}]`).get().forEach((element) => {
|
||||||
const config: any = { language };
|
const config: any = { language };
|
||||||
if ($(element).data('model')) config.model = $(element).data('model');
|
if ($(element).data('model')) config.model = $(element).data('model');
|
||||||
if ($(element).data('hide')) config.hide = $(element).data('hide');
|
|
||||||
CmEditor.getOrConstruct($(element), config);
|
CmEditor.getOrConstruct($(element), config);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ setDiagnosticsOptions({
|
|||||||
schema: problemConfigSchema as any,
|
schema: problemConfigSchema as any,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
uri: `${UiContext.cdn_prefix}manage/setting/schema.json`,
|
uri: '/manage/config/schema.json',
|
||||||
fileMatch: ['hydro://system/setting.yaml'],
|
fileMatch: ['hydro://system/setting.yaml'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -11,9 +11,12 @@ import user from 'hydrooj/src/model/user';
|
|||||||
import * as bus from 'hydrooj/src/service/bus';
|
import * as bus from 'hydrooj/src/service/bus';
|
||||||
import { UiContextBase } from 'hydrooj/src/service/layers/base';
|
import { UiContextBase } from 'hydrooj/src/service/layers/base';
|
||||||
import { Handler, Route } from 'hydrooj/src/service/server';
|
import { Handler, Route } from 'hydrooj/src/service/server';
|
||||||
|
import { SystemSettings } from 'hydrooj/src/settings';
|
||||||
import { ObjectID } from 'mongodb';
|
import { ObjectID } from 'mongodb';
|
||||||
import { tmpdir } from 'os';
|
import { tmpdir } from 'os';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
import Schema from 'schemastery';
|
||||||
|
import convert from 'schemastery-jsonschema';
|
||||||
import markdown from './backendlib/markdown';
|
import markdown from './backendlib/markdown';
|
||||||
|
|
||||||
declare module 'hydrooj/src/interface' {
|
declare module 'hydrooj/src/interface' {
|
||||||
@@ -157,6 +160,13 @@ class LanguageHandler extends ResourceHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SystemConfigSchemaHandler extends Handler {
|
||||||
|
async get() {
|
||||||
|
const schema = convert(Schema.intersect(SystemSettings) as any, true);
|
||||||
|
this.response.body = schema;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class RichMediaHandler extends Handler {
|
class RichMediaHandler extends Handler {
|
||||||
async renderUser(domainId, payload) {
|
async renderUser(domainId, payload) {
|
||||||
let d = payload.domainId || domainId;
|
let d = payload.domainId || domainId;
|
||||||
@@ -213,6 +223,7 @@ global.Hydro.handler.ui = async () => {
|
|||||||
Route('set_theme', '/set_theme/:theme', SetThemeHandler);
|
Route('set_theme', '/set_theme/:theme', SetThemeHandler);
|
||||||
Route('constant', '/constant/:version', UiConstantsHandler);
|
Route('constant', '/constant/:version', UiConstantsHandler);
|
||||||
Route('markdown', '/markdown', MarkdownHandler);
|
Route('markdown', '/markdown', MarkdownHandler);
|
||||||
|
Route('config_schema', '/manage/config/schema.json', SystemConfigSchemaHandler, PRIV.PRIV_EDIT_SYSTEM);
|
||||||
Route('lang', '/l/:lang', LanguageHandler);
|
Route('lang', '/l/:lang', LanguageHandler);
|
||||||
Route('media', '/media', RichMediaHandler);
|
Route('media', '/media', RichMediaHandler);
|
||||||
};
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/ui-default",
|
"name": "@hydrooj/ui-default",
|
||||||
"version": "4.39.8",
|
"version": "4.39.9",
|
||||||
"author": "undefined <i@undefined.moe>",
|
"author": "undefined <i@undefined.moe>",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"main": "hydro.js",
|
"main": "hydro.js",
|
||||||
@@ -129,6 +129,7 @@
|
|||||||
"mongodb": "^3.7.3",
|
"mongodb": "^3.7.3",
|
||||||
"nunjucks": "^3.2.3",
|
"nunjucks": "^3.2.3",
|
||||||
"p-queue": "^7.3.0",
|
"p-queue": "^7.3.0",
|
||||||
|
"schemastery-jsonschema": "^1.0.3",
|
||||||
"streamsaver": "^2.0.6",
|
"streamsaver": "^2.0.6",
|
||||||
"xss": "^1.0.14"
|
"xss": "^1.0.14"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import yaml from 'js-yaml';
|
||||||
|
import Schema from 'schemastery';
|
||||||
|
import Notification from 'vj/components/notification';
|
||||||
|
import { NamedPage } from 'vj/misc/Page';
|
||||||
|
import request from 'vj/utils/request';
|
||||||
|
|
||||||
|
const page = new NamedPage('manage_config', async () => {
|
||||||
|
const schema = new Schema(UiContext.schema);
|
||||||
|
setInterval(() => {
|
||||||
|
try {
|
||||||
|
const v = yaml.load($('#config').val().toString());
|
||||||
|
schema(v);
|
||||||
|
$('#info').text('');
|
||||||
|
} catch (e) {
|
||||||
|
console.debug(e);
|
||||||
|
$('#info').text(e.message);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
$('#submit').on('click', () => {
|
||||||
|
const value = $('#config').val();
|
||||||
|
request.post('', { value }).then(() => {
|
||||||
|
Notification.success('保存成功');
|
||||||
|
}).catch((e) => {
|
||||||
|
Notification.error('保存失败:', e.message);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export default page;
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
{{ sidemenu.render_item(null, 'manage_script') }}
|
{{ sidemenu.render_item(null, 'manage_script') }}
|
||||||
{{ sidemenu.render_item(null, 'manage_user_import') }}
|
{{ sidemenu.render_item(null, 'manage_user_import') }}
|
||||||
{{ sidemenu.render_item(null, 'manage_setting') }}
|
{{ sidemenu.render_item(null, 'manage_setting') }}
|
||||||
|
{{ sidemenu.render_item(null, 'manage_config') }}
|
||||||
</ol>
|
</ol>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{% extends "manage_base.html" %}
|
||||||
|
{% block manage_content %}
|
||||||
|
{{ set(UiContext, 'schema', schema) }}
|
||||||
|
<div class="section">
|
||||||
|
<div class="section__body typo">
|
||||||
|
<div class="row">
|
||||||
|
<div class="medium-10 columns form__item">
|
||||||
|
<label>
|
||||||
|
config
|
||||||
|
<div name="form_item_config" class="textarea-container">
|
||||||
|
<textarea id="config" data-yaml data-model="hydro://system/setting.yaml" placeholder="" class="textbox">{{ value }}</textarea>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<p class="help-text" id="info"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="medium-10 columns form__item end">
|
||||||
|
<button id="submit" class="rounded primary button">{{ _('Save All Changes') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -8,7 +8,6 @@
|
|||||||
{% if not setting.flag|bitand(model.setting.FLAG_HIDDEN) %}
|
{% if not setting.flag|bitand(model.setting.FLAG_HIDDEN) %}
|
||||||
{% set secret = setting.flag|bitand(model.setting.FLAG_SECRET) != 0 %}
|
{% set secret = setting.flag|bitand(model.setting.FLAG_SECRET) != 0 %}
|
||||||
{% if setting.type == 'text' or setting.type == 'password' or setting.type == 'number' or setting.type == 'float' %}
|
{% if setting.type == 'text' or setting.type == 'password' or setting.type == 'number' or setting.type == 'float' %}
|
||||||
{% set isFileConfig = (setting.name === 'file.accessKey' or setting.name === 'file.secretKey') and process.env.MINIO_ACCESS_KEY %}
|
|
||||||
{{ form.form_text({
|
{{ form.form_text({
|
||||||
type:setting.type,
|
type:setting.type,
|
||||||
label:setting.name,
|
label:setting.name,
|
||||||
@@ -16,7 +15,7 @@
|
|||||||
name:setting.key,
|
name:setting.key,
|
||||||
value:'' if (secret or isFileConfig) else (current[setting.key]|default(setting.value)),
|
value:'' if (secret or isFileConfig) else (current[setting.key]|default(setting.value)),
|
||||||
disabled:setting.flag|bitand(2),
|
disabled:setting.flag|bitand(2),
|
||||||
placeholder:'Please edit in ~/.hydro/env instead' if isFileConfig else (_('(Not changed)') if secret else '')
|
placeholder:_('(Not changed)') if secret else ''
|
||||||
}) }}
|
}) }}
|
||||||
{% elif setting.type == 'select' %}
|
{% elif setting.type == 'select' %}
|
||||||
{{ form.form_select({
|
{{ form.form_select({
|
||||||
|
|||||||
@@ -214,7 +214,6 @@ export default class CodeforcesProvider implements IBasicProvider {
|
|||||||
await page.waitForRequest((req) => {
|
await page.waitForRequest((req) => {
|
||||||
if (req.method() !== 'POST') return false;
|
if (req.method() !== 'POST') return false;
|
||||||
if (!req.url().endsWith('/enter')) return false;
|
if (!req.url().endsWith('/enter')) return false;
|
||||||
console.log(req);
|
|
||||||
return true;
|
return true;
|
||||||
}, { timeout: 24 * 3600 * 1000 });
|
}, { timeout: 24 * 3600 * 1000 });
|
||||||
await page.waitForTimeout(10 * 1000);
|
await page.waitForTimeout(10 * 1000);
|
||||||
|
|||||||
Reference in new issue
Block a user