Tighter client side integrations for uploads.

This commit is contained in:
flash 2025-03-27 21:27:49 +00:00
parent 41e27cdffa
commit 12d40e69a5
Signed by: flash
GPG key ID: 2C9C2C574D47FE3E
17 changed files with 277 additions and 220 deletions

View file

@ -1,6 +1,7 @@
#include array.js
#include csrf.js
#include html.js
#include meta.js
#include uniqstr.js
#include xhr.js

36
assets/common.js/meta.js Normal file
View file

@ -0,0 +1,36 @@
#include html.js
const $meta = (() => {
return {
get(name, prefixed=true) {
if(!name) return;
if(prefixed) name = `msz-${name}`;
const elem = $query(`meta[name="${name}"]`);
if(elem instanceof HTMLMetaElement && typeof elem.content === 'string')
return elem.content;
return null;
},
set(name, value, prefixed=true) {
if(!name) return;
if(prefixed) name = `msz-${name}`;
let elem = $query(`meta[name="${name}"]`);
if(elem instanceof HTMLMetaElement) {
if(typeof value === 'string')
elem.content = value;
else
elem.remove();
} else {
if(typeof value !== 'string')
return;
elem = document.createElement('meta');
elem.name = name;
elem.content = value;
document.head.appendChild(elem);
}
},
};
})();

View file

@ -1,8 +1,6 @@
const MszEEPROM = function(appId, endPoint) {
const MszEEPROM = function(appId) {
if(typeof appId !== 'string')
throw 'appId must be a string';
if(typeof endPoint !== 'string')
throw 'endPoint must be a string';
return {
create: fileInput => {
@ -42,7 +40,7 @@ const MszEEPROM = function(appId, endPoint) {
formData.append('src', appId);
formData.append('file', fileInput);
const { status, body } = await $xhr.post(`${endPoint}/uploads`, {
const { status, body } = await $xhr.post('/uploads', {
type: 'json',
authed: true,
upload: reportProgress,

View file

@ -19,142 +19,144 @@ const MszForumEditor = function(form) {
let lastPostText = '';
let lastPostParser;
const eepromClient = new MszEEPROM(peepApp, peepPath);
const eepromHistory = <div class="eeprom-widget-history-items"/>;
const storagePool = $meta.get('forum-storage-pool');
if(storagePool) {
const eepromClient = new MszEEPROM(storagePool);
const eepromHistory = <div class="eeprom-widget-history-items"/>;
const eepromHandleFileUpload = async file => {
const uploadElemNameValue = <div class="eeprom-widget-file-name-value" title={file.name}>{file.name}</div>;
const uploadElemName = <a class="eeprom-widget-file-name" target="_blank">{uploadElemNameValue}</a>;
const uploadElemProgressText = <div class="eeprom-widget-file-progress">Please wait...</div>;
const uploadElemProgressBarValue = <div class="eeprom-widget-file-bar-fill" style={{ width: '0%' }}/>;
const uploadElem = <div class="eeprom-widget-file">
<div class="eeprom-widget-file-info">
{uploadElemName}
{uploadElemProgressText}
</div>
<div class="eeprom-widget-file-bar">
{uploadElemProgressBarValue}
</div>
</div>;
const eepromHandleFileUpload = async file => {
const uploadElemNameValue = <div class="eeprom-widget-file-name-value" title={file.name}>{file.name}</div>;
const uploadElemName = <a class="eeprom-widget-file-name" target="_blank">{uploadElemNameValue}</a>;
const uploadElemProgressText = <div class="eeprom-widget-file-progress">Please wait...</div>;
const uploadElemProgressBarValue = <div class="eeprom-widget-file-bar-fill" style={{ width: '0%' }}/>;
const uploadElem = <div class="eeprom-widget-file">
<div class="eeprom-widget-file-info">
{uploadElemName}
{uploadElemProgressText}
eepromHistory.insertAdjacentElement('afterbegin', uploadElem);
const explodeUploadElem = () => { uploadElem.remove(); };
const uploadTask = eepromClient.create(file);
uploadTask.onProgress(prog => {
uploadElemProgressBarValue.style.width = `${Math.ceil(prog.progress * 100)}%`;
uploadElemProgressText.textContent = `${prog.progress.toLocaleString(undefined, { style: 'percent' })} (${prog.total - prog.loaded} bytes remaining)`;
});
try {
const fileInfo = await uploadTask.start();
uploadElem.classList.add('eeprom-widget-file-done');
uploadElemName.href = fileInfo.url;
uploadElemProgressText.textContent = '';
const insertTheLinkIntoTheBoxEx2 = () => {
let insertText = location.protocol + fileInfo.url;
if(parserElem.value == 'bb') { // bbcode
if(fileInfo.isImage())
insertText = `[img]${fileInfo.url}[/img]`;
else if(fileInfo.isAudio())
insertText = `[audio]${fileInfo.url}[/audio]`;
else if(fileInfo.isVideo())
insertText = `[video]${fileInfo.url}[/video]`;
} else if(parserElem.value == 'md') { // markdown
if(fileInfo.isMedia())
insertText = `![](${fileInfo.url})`;
}
$insertTags(textElem, insertText, '');
textElem.value = textElem.value.trim();
};
uploadElemProgressText.appendChild(<a href="javascript:void(0)" onclick={() => insertTheLinkIntoTheBoxEx2()}>Insert</a>);
uploadElemProgressText.appendChild($text(' '));
uploadElemProgressText.appendChild(<a href="javascript:void(0)" onclick={() => {
eepromClient.delete(fileInfo)
.then(() => explodeUploadElem())
.catch(ex => {
console.error(ex);
MszShowMessageBox(ex, 'Upload Error');
});
}}>Delete</a>);
insertTheLinkIntoTheBoxEx2();
} catch(ex) {
let errorText = 'Upload aborted.';
if(!ex.aborted) {
console.error(ex);
errorText = ex.toString();
}
uploadElem.classList.add('eeprom-widget-file-fail');
uploadElemProgressText.textContent = errorText;
await MszShowMessageBox(errorText, 'Upload Error');
}
};
const eepromFormInput = <input type="file" multiple={true} class="eeprom-widget-form-input"
onchange={() => {
const files = eepromFormInput.files;
for(const file of files)
eepromHandleFileUpload(file);
eepromFormInput.value = '';
}}/>;
const eepromForm = <label class="eeprom-widget-form">
{eepromFormInput}
<div class="eeprom-widget-form-text">
Select Files...
</div>
<div class="eeprom-widget-file-bar">
{uploadElemProgressBarValue}
</label>;
const eepromWidget = <div class="eeprom-widget">
{eepromForm}
<div class="eeprom-widget-history">
{eepromHistory}
</div>
</div>;
eepromHistory.insertAdjacentElement('afterbegin', uploadElem);
form.appendChild(eepromWidget);
const explodeUploadElem = () => { uploadElem.remove(); };
const uploadTask = eepromClient.create(file);
textElem.addEventListener('paste', ev => {
if(ev.clipboardData && ev.clipboardData.files.length > 0) {
ev.preventDefault();
uploadTask.onProgress(prog => {
uploadElemProgressBarValue.style.width = `${Math.ceil(prog.progress * 100)}%`;
uploadElemProgressText.textContent = `${prog.progress.toLocaleString(undefined, { style: 'percent' })} (${prog.total - prog.loaded} bytes remaining)`;
const files = ev.clipboardData.files;
for(const file of files)
eepromHandleFileUpload(file);
}
});
try {
const fileInfo = await uploadTask.start();
uploadElem.classList.add('eeprom-widget-file-done');
uploadElemName.href = fileInfo.url;
uploadElemProgressText.textContent = '';
const insertTheLinkIntoTheBoxEx2 = () => {
let insertText = location.protocol + fileInfo.url;
if(parserElem.value == 'bb') { // bbcode
if(fileInfo.isImage())
insertText = `[img]${fileInfo.url}[/img]`;
else if(fileInfo.isAudio())
insertText = `[audio]${fileInfo.url}[/audio]`;
else if(fileInfo.isVideo())
insertText = `[video]${fileInfo.url}[/video]`;
} else if(parserElem.value == 'md') { // markdown
if(fileInfo.isMedia())
insertText = `![](${fileInfo.url})`;
}
$insertTags(textElem, insertText, '');
textElem.value = textElem.value.trim();
};
uploadElemProgressText.appendChild(<a href="javascript:void(0)" onclick={() => insertTheLinkIntoTheBoxEx2()}>Insert</a>);
uploadElemProgressText.appendChild($text(' '));
uploadElemProgressText.appendChild(<a href="javascript:void(0)" onclick={() => {
eepromClient.delete(fileInfo)
.then(() => explodeUploadElem())
.catch(ex => {
console.error(ex);
MszShowMessageBox(ex, 'Upload Error');
});
}}>Delete</a>);
insertTheLinkIntoTheBoxEx2();
} catch(ex) {
let errorText = 'Upload aborted.';
if(!ex.aborted) {
console.error(ex);
errorText = ex.toString();
}
uploadElem.classList.add('eeprom-widget-file-fail');
uploadElemProgressText.textContent = errorText;
await MszShowMessageBox(errorText, 'Upload Error');
}
};
const eepromFormInput = <input type="file" multiple={true} class="eeprom-widget-form-input"
onchange={() => {
const files = eepromFormInput.files;
for(const file of files)
eepromHandleFileUpload(file);
eepromFormInput.value = '';
}}/>;
const eepromForm = <label class="eeprom-widget-form">
{eepromFormInput}
<div class="eeprom-widget-form-text">
Select Files...
</div>
</label>;
const eepromWidget = <div class="eeprom-widget">
{eepromForm}
<div class="eeprom-widget-history">
{eepromHistory}
</div>
</div>;
form.appendChild(eepromWidget);
textElem.addEventListener('paste', ev => {
if(ev.clipboardData && ev.clipboardData.files.length > 0) {
document.body.addEventListener('dragenter', ev => {
ev.preventDefault();
ev.stopPropagation();
});
document.body.addEventListener('dragover', ev => {
ev.preventDefault();
ev.stopPropagation();
});
document.body.addEventListener('dragleave', ev => {
ev.preventDefault();
ev.stopPropagation();
});
document.body.addEventListener('drop', ev => {
ev.preventDefault();
ev.stopPropagation();
const files = ev.clipboardData.files;
for(const file of files)
eepromHandleFileUpload(file);
}
});
document.body.addEventListener('dragenter', ev => {
ev.preventDefault();
ev.stopPropagation();
});
document.body.addEventListener('dragover', ev => {
ev.preventDefault();
ev.stopPropagation();
});
document.body.addEventListener('dragleave', ev => {
ev.preventDefault();
ev.stopPropagation();
});
document.body.addEventListener('drop', ev => {
ev.preventDefault();
ev.stopPropagation();
if(ev.dataTransfer && ev.dataTransfer.files.length > 0) {
const files = ev.dataTransfer.files;
for(const file of files)
eepromHandleFileUpload(file);
}
});
if(ev.dataTransfer && ev.dataTransfer.files.length > 0) {
const files = ev.dataTransfer.files;
for(const file of files)
eepromHandleFileUpload(file);
}
});
}
// hack: don't prompt user when hitting submit, really need to make this not stupid.
buttonsElem.firstChild.addEventListener('click', () => MszForumEditorAllowClose = true);

View file

@ -65,73 +65,76 @@ const MszMessagesReply = function(element) {
// this implementation is godawful but it'll do for now lol
// need to make it easier to share the forum's implementation
const eepromClient = new MszEEPROM(peepApp, peepPath);
const eepromHandleFileUpload = async file => {
const uploadTask = eepromClient.create(file);
const storagePool = $meta.get('messages-storage-pool');
if(storagePool) {
const eepromClient = new MszEEPROM(storagePool);
const eepromHandleFileUpload = async file => {
const uploadTask = eepromClient.create(file);
try {
const fileInfo = await uploadTask.start();
const parserMode = parseInt(parserSelect.value);
let insertText = location.protocol + fileInfo.url;
try {
const fileInfo = await uploadTask.start();
const parserMode = parseInt(parserSelect.value);
let insertText = location.protocol + fileInfo.url;
if(parserMode == 1) { // bbcode
if(fileInfo.isImage())
insertText = `[img]${fileInfo.url}[/img]`;
else if(fileInfo.isAudio())
insertText = `[audio]${fileInfo.url}[/audio]`;
else if(fileInfo.isVideo())
insertText = `[video]${fileInfo.url}[/video]`;
} else if(parserMode == 2) { // markdown
if(fileInfo.isMedia())
insertText = `![](${fileInfo.url})`;
if(parserMode == 1) { // bbcode
if(fileInfo.isImage())
insertText = `[img]${fileInfo.url}[/img]`;
else if(fileInfo.isAudio())
insertText = `[audio]${fileInfo.url}[/audio]`;
else if(fileInfo.isVideo())
insertText = `[video]${fileInfo.url}[/video]`;
} else if(parserMode == 2) { // markdown
if(fileInfo.isMedia())
insertText = `![](${fileInfo.url})`;
}
$insertTags(bodyElem, insertText, '');
bodyElem.value = bodyElem.value.trim();
} catch(ex) {
let errorText = 'Upload aborted.';
if(!ex.aborted) {
console.error(ex);
errorText = ex.toString();
}
await MszShowMessageBox(errorText, 'Upload Error');
}
};
$insertTags(bodyElem, insertText, '');
bodyElem.value = bodyElem.value.trim();
} catch(ex) {
let errorText = 'Upload aborted.';
bodyElem.addEventListener('paste', ev => {
if(ev.clipboardData && ev.clipboardData.files.length > 0) {
ev.preventDefault();
if(!ex.aborted) {
console.error(ex);
errorText = ex.toString();
const files = ev.clipboardData.files;
for(const file of files)
eepromHandleFileUpload(file);
}
});
await MszShowMessageBox(errorText, 'Upload Error');
}
};
bodyElem.addEventListener('paste', ev => {
if(ev.clipboardData && ev.clipboardData.files.length > 0) {
document.body.addEventListener('dragenter', ev => {
ev.preventDefault();
ev.stopPropagation();
});
document.body.addEventListener('dragover', ev => {
ev.preventDefault();
ev.stopPropagation();
});
document.body.addEventListener('dragleave', ev => {
ev.preventDefault();
ev.stopPropagation();
});
document.body.addEventListener('drop', ev => {
ev.preventDefault();
ev.stopPropagation();
const files = ev.clipboardData.files;
for(const file of files)
eepromHandleFileUpload(file);
}
});
document.body.addEventListener('dragenter', ev => {
ev.preventDefault();
ev.stopPropagation();
});
document.body.addEventListener('dragover', ev => {
ev.preventDefault();
ev.stopPropagation();
});
document.body.addEventListener('dragleave', ev => {
ev.preventDefault();
ev.stopPropagation();
});
document.body.addEventListener('drop', ev => {
ev.preventDefault();
ev.stopPropagation();
if(ev.dataTransfer && ev.dataTransfer.files.length > 0) {
const files = ev.dataTransfer.files;
for(const file of files)
eepromHandleFileUpload(file);
}
});
if(ev.dataTransfer && ev.dataTransfer.files.length > 0) {
const files = ev.dataTransfer.files;
for(const file of files)
eepromHandleFileUpload(file);
}
});
}
return {
get element() { return element; },

View file

@ -8,7 +8,6 @@ Below are a number of links to source code repositories related to Flashii.net a
- [Futami](https://patchii.net/flashii/futami): Common data shared between the chat clients.
- [Mami](https://patchii.net/flashii/mami): Web client for chat.
- [Ami](https://patchii.net/flashii/ami): Web client for chat for older browsers.
- [EEPROM](https://patchii.net/flashii/eeprom): Service for file uploading.
- [Uiharu](https://patchii.net/flashii/uiharu): Service for looking up URL metadata.
- [Seria](https://patchii.net/flashii/seria): Software used by the downloads tracker.
- [Mince](https://patchii.net/flashii/mince): Source code for the Minecraft servers subwebsite.

View file

@ -2,6 +2,7 @@
namespace Misuzu\Forum;
use stdClass;
use Index\Config\Config;
use Index\Db\DbConnection;
use Misuzu\Parsers\TextFormat;
use Misuzu\Users\UserInfo;
@ -22,7 +23,14 @@ class ForumContext {
/** @var array<string, ForumSignatureInfo> */
private array $cachedSignatures = [];
public function __construct(DbConnection $dbConn) {
public string $storagePoolName {
get => $this->config->getString('storage_pool');
}
public function __construct(
DbConnection $dbConn,
public private(set) Config $config,
) {
$this->categories = new ForumCategoriesData($dbConn);
$this->topics = new ForumTopicsData($dbConn);
$this->topicRedirects = new ForumTopicRedirectsData($dbConn);

View file

@ -1,12 +1,20 @@
<?php
namespace Misuzu\Messages;
use Index\Config\Config;
use Index\Db\DbConnection;
class MessagesContext {
public private(set) MessagesData $database;
public function __construct(DbConnection $dbConn) {
public string $storagePoolName {
get => $this->config->getString('storage_pool');
}
public function __construct(
DbConnection $dbConn,
public private(set) Config $config,
) {
$this->database = new MessagesData($dbConn);
}
}

View file

@ -94,8 +94,14 @@ class MisuzuContext {
config: $this->config->scopeTo('auth'),
));
$this->deps->register($this->commentsCtx = $this->deps->constructLazy(Comments\CommentsContext::class));
$this->deps->register($this->forumCtx = $this->deps->constructLazy(Forum\ForumContext::class));
$this->deps->register($this->messagesCtx = $this->deps->constructLazy(Messages\MessagesContext::class));
$this->deps->register($this->forumCtx = $this->deps->constructLazy(
Forum\ForumContext::class,
config: $this->config->scopeTo('forum'),
));
$this->deps->register($this->messagesCtx = $this->deps->constructLazy(
Messages\MessagesContext::class,
config: $this->config->scopeTo('messages'),
));
$this->deps->register($this->oauth2Ctx = $this->deps->constructLazy(
OAuth2\OAuth2Context::class,
config: $this->config->scopeTo('oauth2'),
@ -153,17 +159,17 @@ class MisuzuContext {
}
public function startTemplating(bool $cache = true): void {
$globals = $this->config->getValues([
['eeprom.path:s', '', 'eeprom_path'],
['eeprom.app:s', '', 'eeprom_app'],
['eeprom.appmsgs:s', '', 'eeprom_app_messages'],
]);
$isDebug = Misuzu::debug();
$globals['site_info'] = $this->siteInfo;
$globals['auth_info'] = $this->authInfo;
$globals['active_ban_info'] = $this->usersCtx->tryGetActiveBan($this->authInfo->userInfo);
$globals['display_timings_info'] = $isDebug || $this->authInfo->getPerms('global')->check(Perm::G_TIMINGS_VIEW);
$globals = [
'site_info' => $this->siteInfo,
'auth_info' => $this->authInfo,
'active_ban_info' => $this->usersCtx->tryGetActiveBan($this->authInfo->userInfo),
'display_timings_info' => $isDebug || $this->authInfo->getPerms('global')->check(Perm::G_TIMINGS_VIEW),
'meta' => [
'forum-storage-pool' => $this->forumCtx->storagePoolName,
'messages-storage-pool' => $this->messagesCtx->storagePoolName,
],
];
$this->templating = new TplEnvironment(
Misuzu::PATH_TEMPLATES,

View file

@ -146,7 +146,7 @@ class FilesContext {
throw new InvalidArgumentException('$format is not a supported format');
$type = 'application/octet-stream';
$path = tempnam(sys_get_temp_dir(), 'eeprom-thumbnail-');
$path = tempnam(sys_get_temp_dir(), 'msz-thumbnail-');
$imagick = new Imagick;
try {
$this->readIntoImagick($imagick, $original);
@ -174,7 +174,7 @@ class FilesContext {
if(!is_file($sourcePath))
throw new RuntimeException('local data for provided $original does not exist');
$targetPath = tempnam(sys_get_temp_dir(), sprintf('eeprom-crop-%d-', $dimensions));
$targetPath = tempnam(sys_get_temp_dir(), sprintf('msz-crop-%d-', $dimensions));
$imagick = new Imagick;
try {
if($original->isImage)

View file

@ -5,4 +5,5 @@ enum PoolInfoGetField {
case Id;
case Name;
case UploadId;
case IdOrName;
}

View file

@ -49,6 +49,7 @@ class PoolsData {
$field = match($field) {
PoolInfoGetField::Id => 'pool_id = ?',
PoolInfoGetField::Name => 'pool_name = ?',
PoolInfoGetField::IdOrName => sprintf('pool_%s = ?', ctype_digit($value) ? 'id' : 'name'),
PoolInfoGetField::UploadId => 'pool_id = (SELECT pool_id FROM msz_storage_uploads WHERE upload_id = ?)',
};

View file

@ -29,6 +29,6 @@ class TasksContext {
if($taskInfo instanceof TaskInfo)
$taskInfo = $taskInfo->id;
return implode(DIRECTORY_SEPARATOR, [sys_get_temp_dir(), sprintf('eeprom-upload-%s', $taskInfo)]);
return implode(DIRECTORY_SEPARATOR, [sys_get_temp_dir(), sprintf('msz-upload-%s', $taskInfo)]);
}
}

View file

@ -14,7 +14,7 @@ use Index\Urls\{UrlFormat,UrlSource,UrlSourceCommon};
use Misuzu\Perm;
use Misuzu\Auth\AuthInfo;
use Misuzu\Storage\Denylist\{DenylistContext,DenylistReason};
use Misuzu\Storage\Pools\PoolsContext;
use Misuzu\Storage\Pools\{PoolsContext,PoolInfoGetField};
use Misuzu\Storage\Pools\Rules\{ConstrainSizeRule,EnsureVariantRule,EnsureVariantRuleThumb};
use Misuzu\Storage\Files\{FilesContext,FileImportMode,FileInfoGetFileField};
use Misuzu\Users\UsersContext;
@ -180,7 +180,10 @@ class UploadsLegacyRoutes implements RouteHandler, UrlSource {
}
try {
$poolInfo = $this->poolsCtx->pools->getPool((string)$content->getFilteredParam('src', FILTER_VALIDATE_INT));
$poolInfo = $this->poolsCtx->pools->getPool(
(string)$content->getParam('src'),
PoolInfoGetField::IdOrName,
);
if($poolInfo->protected) {
$response->statusCode = 404;
return [
@ -227,7 +230,7 @@ class UploadsLegacyRoutes implements RouteHandler, UrlSource {
];
}
$tmpFile = tempnam(sys_get_temp_dir(), 'eeprom-upload-');
$tmpFile = tempnam(sys_get_temp_dir(), 'msz-upload-');
$file->moveTo($tmpFile);
$hash = hash_file('sha256', $tmpFile, true);

View file

@ -109,10 +109,4 @@
</div>
</div>
</form>
{% if globals.eeprom_path is not empty and globals.eeprom_app is not empty %}
<script>
const peepPath = '{{ globals.eeprom_path }}', peepApp = '{{ globals.eeprom_app }}';
</script>
{% endif %}
{% endblock %}

View file

@ -5,6 +5,9 @@
<link href="/vendor/fontawesome/css/all.min.css" rel="stylesheet">
<link href="{{ asset('common.css') }}" rel="stylesheet">
<link href="{{ asset('misuzu.css') }}" rel="stylesheet">
{% for name, value in globals.meta %}
{% if value is not empty %}<meta name="msz-{{ name }}" content="{{ value }}">{% endif %}
{% endfor %}
{% if main_css_vars is defined and main_css_vars is iterable and main_css_vars is not empty %}
<style>
:root {

View file

@ -3,10 +3,4 @@
{% block content %}
{% block messages_content %}
{% endblock %}
{% if globals.eeprom_path is not empty and globals.eeprom_app_messages is not empty %}
<script>
const peepPath = '{{ globals.eeprom_path }}', peepApp = '{{ globals.eeprom_app_messages }}';
</script>
{% endif %}
{% endblock %}