diff --git a/hydro/handler/home.ts b/hydro/handler/home.ts
index 7787875a0..cdea59ef6 100644
--- a/hydro/handler/home.ts
+++ b/hydro/handler/home.ts
@@ -308,6 +308,7 @@ class HomeMessagesHandler extends Handler {
async postSend(domainId: string, uid: number, content: string) {
const udoc = await user.getById('system', uid);
if (!udoc) throw new UserNotFoundError(uid);
+ if (udoc.gravatar) udoc.gravatar = misc.gravatar(udoc.gravatar);
const mdoc = await message.send(this.user._id, uid, content);
// TODO(twd2): improve here: projection
if (this.user._id !== uid) {
@@ -330,7 +331,7 @@ class HomeMessagesConnectionHandler extends ConnectionHandler {
bus.subscribe([`user_message-${this.user._id}`], this, 'onMessageReceived');
}
- async onMessageReceived(e) {
+ async onMessageReceived(e: any) {
this.send(e.value);
}
@@ -342,17 +343,17 @@ class HomeMessagesConnectionHandler extends ConnectionHandler {
class HomeFileHandler extends Handler {
async get() {
const ufdocs = await file.getMulti({ owner: this.user._id }).toArray();
- const fdict = await file.getMetaDict(ufdocs.map((ufdoc) => ufdoc._id));
this.response.template = 'home_file.html';
- this.response.body = { ufdocs, fdict };
+ this.response.body = { ufdocs };
}
- async postDelete(ufid) {
+ @param('ufid', Types.ObjectID)
+ async postDelete(domainId: string, ufid: ObjectID) {
const ufdoc = await file.getMeta(ufid);
if (ufdoc.owner !== this.user._id) this.checkPriv(PRIV.PRIV_DELETE_FILE);
else this.checkPriv(PRIV.PRIV_DELETE_FILE_SELF);
const result = await file.del(ufdoc._id);
- if (result) await user.inc(this.user._id, 'usage', ufdoc.length);
+ if (result) await user.inc(this.user._id, 'usage', -ufdoc.size);
this.back();
}
}
diff --git a/hydro/handler/misc.ts b/hydro/handler/misc.ts
index 7d432cdca..2f144f177 100644
--- a/hydro/handler/misc.ts
+++ b/hydro/handler/misc.ts
@@ -1,4 +1,5 @@
/* eslint-disable camelcase */
+import fs from 'fs';
import { ObjectID } from 'mongodb';
import { PRIV } from '../model/builtin';
import * as system from '../model/system';
@@ -9,6 +10,7 @@ import * as db from '../service/db';
import {
Route, Handler, Types, param,
} from '../service/server';
+import { BadRequestError } from '../error';
const coll = db.collection('status');
@@ -23,6 +25,41 @@ class FileDownloadHandler extends Handler {
}
}
+class FileUploadHandler extends Handler {
+ async getQuota() {
+ let quota = await system.get('user.quota');
+ if (this.user.hasPriv(PRIV.PRIV_UNLIMITED_QUOTA)) {
+ quota = 2 ** 63 - 1;
+ }
+ return quota;
+ }
+
+ async get() {
+ this.response.template = 'fs_upload.html';
+ this.response.body = { fdoc: null, usage: this.user.usage, quota: await this.getQuota() };
+ }
+
+ @param('title', Types.String)
+ async post(domainId: string, title: string) {
+ if (!this.request.files.file) throw new BadRequestError();
+ const quota = await this.getQuota();
+ const lfdoc = await fs.promises.stat(this.request.files.file.path);
+ let ufid: ObjectID;
+ const udoc = await user.inc(this.user._id, 'usage', lfdoc.size);
+ try {
+ ufid = await file.add(this.request.files.file.path, title, this.user._id);
+ } catch (e) {
+ await user.inc(this.user._id, 'usage', -lfdoc.size);
+ throw e;
+ }
+ const fdoc = await file.getMeta(ufid);
+ this.response.template = 'fs_upload.html';
+ this.response.body = {
+ fdoc, ufid, usage: udoc.usage, quota,
+ };
+ }
+}
+
class StatusHandler extends Handler {
async get() {
const stats = await coll.find().sort({ type: 1, updateAt: -1 }).toArray();
@@ -109,6 +146,7 @@ class UiSettingsHandler extends Handler {
export async function apply() {
Route('file_download', '/fs/:fileId/:secret', FileDownloadHandler);
Route('file_download_with_name', '/fs/:fileId/:name/:secret', FileDownloadHandler);
+ Route('file_upload', '/fs/upload', FileUploadHandler, PRIV.PRIV_CREATE_FILE);
Route('status', '/status', StatusHandler);
Route('status_update', '/status/update', StatusUpdateHandler);
Route('switch_language', '/language/:lang', SwitchLanguageHandler);
diff --git a/hydro/model/file.ts b/hydro/model/file.ts
index 36a1b15b2..3dd26fe82 100644
--- a/hydro/model/file.ts
+++ b/hydro/model/file.ts
@@ -13,20 +13,25 @@ function _timestamp() {
return Math.floor(Number(new Date()) / 1000000);
}
-export async function add(streamOrPath: fs.ReadStream | string, filename: string, meta = {}) {
+export async function add(
+ streamOrPath: fs.ReadStream | string, filename: string,
+ owner = 1, meta = {},
+) {
let stream: fs.ReadStream;
if (typeof streamOrPath === 'string') stream = fs.createReadStream(streamOrPath);
else stream = streamOrPath;
const w = gridfs.openUploadStream(filename);
- await coll.insertOne({ ...meta, _id: w.id, secret: String.random(32) });
+ await coll.insertOne({
+ ...meta, _id: w.id, secret: String.random(32), owner, filename,
+ });
await new Promise((resolve, reject) => {
w.on('error', reject);
w.on('finish', resolve);
stream.pipe(w);
});
const c = await gridfs.find({ _id: w.id }).toArray();
- await coll.updateOne({ _id: w.id }, { $set: { md5: c[0].md5, size: c[0].size } });
- return w.id;
+ await coll.updateOne({ _id: w.id }, { $set: { md5: c[0].md5, size: c[0].length } });
+ return w.id as ObjectID;
}
export function del(_id: ObjectID) {
@@ -92,13 +97,26 @@ export function getMulti(query: any) {
return coll.find(query);
}
-export async function url(_id: ObjectID, name: string | undefined) {
+async function _url(_id: ObjectID, name: string | undefined) {
const file = await coll.findOne({ _id });
const secret = hash(file.secret, _timestamp().toString());
if (name) return `/fs/${_id}/${Buffer.from(name).toString('base64')}/${secret}`;
return `/fs/${_id}/${secret}`;
}
+function __url(file: any, name: string | undefined) {
+ const secret = hash(file.secret, _timestamp().toString());
+ if (name) return `/fs/${file._id}/${Buffer.from(name).toString('base64')}/${secret}`;
+ return `/fs/${file._id}/${secret}`;
+}
+
+export function url(_id: ObjectID, name: string | undefined): Promise {{ _('ID') }}: {{ fdoc['_id'] }} {{ _('Userfile doc_id') }}: {{ ufid }} {{ _('MD5') }}: {{ fdoc['md5'] }} {{ _('Secret (keep it secret)') }}: {{ fdoc['metadata']['secret'] }} {{ _('URL') }}: {{ reverse_url('fs_get', domain_id=vj4.model.builtin.DOMAIN_ID_SYSTEM, secret=fdoc['metadata']['secret']) }} {{ _('Image Markdown') }}:  }})
{{ _('URL') }}: {{ model.file.url(fdoc) }}
{% endif %} {% endblock %} diff --git a/templates/home_file.html b/templates/home_file.html index e55b61c82..604f9bd91 100644 --- a/templates/home_file.html +++ b/templates/home_file.html @@ -4,8 +4,8 @@${message}
`).appendTo($dom); actionButtons.forEach((button) => { $(tpl``).appendTo($dom); }); diff --git a/ui/components/notification/notification.page.styl b/ui/components/notification/notification.page.styl index a5d6ba0d0..506a6cd59 100644 --- a/ui/components/notification/notification.page.styl +++ b/ui/components/notification/notification.page.styl @@ -14,6 +14,9 @@ box-shadow: 0 4px 10px rgba(#000, 0.4) min-width: rem(150px) + img + border-radius: 50% + &.hide opacity: 0 transform: translateY(20px) scale(0.9) diff --git a/ui/components/socket/index.js b/ui/components/socket/index.js new file mode 100644 index 000000000..b560cff02 --- /dev/null +++ b/ui/components/socket/index.js @@ -0,0 +1,42 @@ +import SockJS from 'sockjs-client'; +import Notification from 'vj/components/notification'; +import i18n from 'vj/utils/i18n'; + +export default class Sock { + constructor(url) { + this.url = url; + this.reconnect = false; + this.init(); + } + + init() { + this.sock = new SockJS(this.url); + this.sock.onopen = () => { + if (this.reconnect) Notification.warn(i18n('Reconnected to the server.')); + if (this.onopen) this.onopen(this.reconnect); + }; + this.sock.onclose = (code, reason) => { + console.warn('Connection closed, ', code, reason); + if (this.closed) { + if (this.onclose) this.onclose(code, reason); + return; + } + Notification.warn(i18n('Disconnected from the server. Reconnecting...')); + this.reconnect = setTimeout(() => { this.init(); }, 3000); + if (this.onclose) this.onclose(code, reason); + }; + this.sock.onmessage = (message) => { + if (process.env.NODE_ENV !== 'production') console.log('Sock.onmessage: ', message); + if (this.onmessage) this.onmessage(message); + }; + } + + send(data) { + this.sock.send(data); + } + + close() { + this.closed = true; + this.sock.close(); + } +} diff --git a/ui/pages/home_messages.page.js b/ui/pages/home_messages.page.js index 50d0f4091..c1bf6259f 100644 --- a/ui/pages/home_messages.page.js +++ b/ui/pages/home_messages.page.js @@ -23,7 +23,7 @@ const page = new NamedPage('home_messages', () => { } async function mountComponent() { - const { default: SockJs } = await import('sockjs-client'); + const { default: SockJs } = await import('../components/socket'); const { default: MessagePadApp } = await import('../components/messagepad'); const { default: MessagePadReducer } = await import('../components/messagepad/reducers'); const { diff --git a/ui/pages/judge_playground.page.js b/ui/pages/judge_playground.page.js index 334ce0cf3..35c6ca191 100644 --- a/ui/pages/judge_playground.page.js +++ b/ui/pages/judge_playground.page.js @@ -3,7 +3,7 @@ import { NamedPage } from 'vj/misc/PageLoader'; import * as recordEnum from 'vj/constant/record'; const page = new NamedPage('judge_playground', async () => { - const { default: SockJs } = await import('sockjs-client'); + const { default: SockJs } = await import('../components/socket'); const sock = new SockJs(`/judge/consume-conn?token=${UiContext.token}`); sock.onopen = () => { diff --git a/ui/pages/manage_dashboard.page.js b/ui/pages/manage_dashboard.page.js index d6feea14d..2924f9245 100644 --- a/ui/pages/manage_dashboard.page.js +++ b/ui/pages/manage_dashboard.page.js @@ -1,7 +1,7 @@ import { NamedPage } from 'vj/misc/PageLoader'; const page = new NamedPage('manage_dashboard', async () => { - const { default: SockJs } = await import('sockjs-client'); + const { default: SockJs } = await import('../components/socket'); const sock = new SockJs(Context.socketUrl); sock.onopen = () => { diff --git a/ui/pages/problem_detail.page.js b/ui/pages/problem_detail.page.js index bb67c186d..3fb80431e 100644 --- a/ui/pages/problem_detail.page.js +++ b/ui/pages/problem_detail.page.js @@ -160,7 +160,7 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor if (reactLoaded) return; $('.loader-container').show(); - const { default: SockJs } = await import('sockjs-client'); + const { default: SockJs } = await import('../components/socket'); const { default: ScratchpadApp } = await import('../components/scratchpad'); const { default: ScratchpadReducer } = await import('../components/scratchpad/reducers'); const { diff --git a/ui/pages/record_detail.page.js b/ui/pages/record_detail.page.js index 03723a466..6751f21e6 100644 --- a/ui/pages/record_detail.page.js +++ b/ui/pages/record_detail.page.js @@ -1,7 +1,7 @@ import { NamedPage } from 'vj/misc/PageLoader'; const page = new NamedPage('record_detail', async () => { - const { default: SockJs } = await import('sockjs-client'); + const { default: SockJs } = await import('../components/socket'); const { DiffDOM } = await import('diff-dom'); const sock = new SockJs(Context.socketUrl); diff --git a/ui/pages/record_main.page.js b/ui/pages/record_main.page.js index 7339d44f6..904a7bd1b 100644 --- a/ui/pages/record_main.page.js +++ b/ui/pages/record_main.page.js @@ -2,7 +2,7 @@ import { NamedPage } from 'vj/misc/PageLoader'; import UserSelectAutoComplete from 'vj/components/autocomplete/UserSelectAutoComplete'; const page = new NamedPage('record_main', async () => { - const { default: SockJs } = await import('sockjs-client'); + const { default: SockJs } = await import('../components/socket'); const { DiffDOM } = await import('diff-dom'); const sock = new SockJs(Context.socketUrl);