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; +export function url(file: any, name: string | undefined): string; +export function url(arg0: any, name: any) { + if (arg0 instanceof ObjectID) return _url(arg0, name); + return __url(arg0, name); +} + global.Hydro.model.file = { add, get, del, inc, dec, getMeta, getWithSecret, getMulti, getMetaDict, url, }; diff --git a/hydro/model/setting.ts b/hydro/model/setting.ts index 9278dab98..cc0621100 100644 --- a/hydro/model/setting.ts +++ b/hydro/model/setting.ts @@ -99,6 +99,7 @@ AccountSetting( Setting('setting_customize', 'backgroundImage', null, '/components/profile/backgrounds/1.jpg', 'text', 'Profile Background Image', 'Choose the background image in your profile page.'), + Setting('setting_storage', 'usage', null, 0, 'number', 'Userfile Usage', null, FLAG_DISABLED | FLAG_HIDDEN), ); DomainSetting( @@ -118,7 +119,6 @@ SystemSetting( 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, 'boolean', 'SSL'), - Setting('setting_db', 'db.ver', null, 1, 'number', 'Database version', null, FLAG_DISABLED | FLAG_HIDDEN), Setting('setting_server', 'server.worker', null, 1, 'number', 'Server Workers Number'), Setting('setting_server', 'server.hostname', null, null, 'text', 'Server Hostname'), Setting('setting_server', 'server.host', null, null, 'text', 'Server Host'), @@ -154,6 +154,8 @@ SystemSetting( Setting('setting_constant', 'DISCUSSION_ON_MAIN', null, 20, 'number', 'DISCUSSION_ON_MAIN'), Setting('setting_constant', 'CONTEST_ON_MAIN', null, 10, 'number', 'CONTEST_ON_MAIN'), Setting('setting_constant', 'TRAINING_ON_MAIN', null, 10, 'number', 'TRAINING_ON_MAIN'), + Setting('setting_storage', 'db.ver', null, 1, 'number', 'Database version', null, FLAG_DISABLED | FLAG_HIDDEN), + Setting('setting_storage', 'user', null, 1, 'number', 'User Counter', null, FLAG_DISABLED | FLAG_HIDDEN), ); global.Hydro.postInit.push( diff --git a/hydro/model/user.ts b/hydro/model/user.ts index 83978706a..b2b7d13dc 100644 --- a/hydro/model/user.ts +++ b/hydro/model/user.ts @@ -68,14 +68,14 @@ export class User implements Udoc { for (const key in setting.SETTINGS_BY_KEY) { if (udoc[key]) this[key] = udoc[key]; - else if (setting.SETTINGS_BY_KEY[key].value) { + else if (setting.SETTINGS_BY_KEY[key].value !== null) { this[key] = setting.SETTINGS_BY_KEY[key].value; } } for (const key in setting.DOMAIN_USER_SETTINGS_BY_KEY) { if (dudoc[key]) this[key] = dudoc[key]; - else if (setting.DOMAIN_USER_SETTINGS_BY_KEY[key].value) { + else if (setting.DOMAIN_USER_SETTINGS_BY_KEY[key].value !== null) { this[key] = setting.DOMAIN_USER_SETTINGS_BY_KEY[key].value; } } diff --git a/templates/fs_upload.html b/templates/fs_upload.html index d93d43083..398962734 100644 --- a/templates/fs_upload.html +++ b/templates/fs_upload.html @@ -5,19 +5,17 @@

{{ _('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') }}:

![{{ _('Description') }}](vijos:/{{ reverse_url('fs_get', domain_id=vj4.model.builtin.DOMAIN_ID_SYSTEM, secret=fdoc['metadata']['secret']) }})

+

{{ _('URL') }}: {{ model.file.url(fdoc) }}

{% endif %}

{{ _('New file') }}: - +

-

{{ _('Note: file size should be less than or equal to {0}, and file type should be in {1}. Empty files are not allowed.').format(vj4.handler.fs.FILE_MAX_LENGTH|format_size, vj4.handler.fs.ALLOWED_MIMETYPE_PREFIX) }}

-

{{ _('Usage: {0}, Quota: {1}').format(usage|format_size, quota|format_size) }}

+

{{ _('Note: file size should be less than or equal to {0}, and file type should be in {1}. Empty files are not allowed.').format(size(FILE_MAX_LENGTH), ALLOWED_MIMETYPE_PREFIX) }}

+

{{ _('Usage: {0}, Quota: {1}').format(size(usage), size(quota)) }}

{% 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 @@

{{ _('My Files') }}

- {% if handler.has_priv(vj4.model.builtin.PRIV_CREATE_FILE) %} - {{ _('Upload File') }} + {% if handler.user.hasPriv(model.builtin.PRIV.PRIV_CREATE_FILE) %} + {{ _('Upload File') }} {% endif %} {{ _("What's file?") }}
@@ -28,14 +28,12 @@ {% for ufdoc in ufdocs %} - {% with fdoc = fdict.get(ufdoc['file_id'], {}) %} - {{ ufdoc['content'] }} - {{ fdoc['length']|format_size if fdoc['length'] else '?' }} - {{ fdoc['md5']|default('?') }} - {{ _('Download') }} ยท {{ _('Code') }} + {{ ufdoc.filename }} + {{ size(ufdoc.size) if ufdoc.size else '?' }} + {{ ufdoc.md5|default('?') }} + {{ _('Download') }} - {% endwith %} {% endfor %} diff --git a/templates/partials/nav.html b/templates/partials/nav.html index 0488d4682..43833352c 100644 --- a/templates/partials/nav.html +++ b/templates/partials/nav.html @@ -51,6 +51,17 @@ + + +