diff --git a/assets/sass/fm.scss b/assets/sass/fm.scss deleted file mode 100644 index 6f5df35..0000000 --- a/assets/sass/fm.scss +++ /dev/null @@ -1,11 +0,0 @@ -@import "vars"; - -#file-upload { - padding: 8px; - background: $infoColor; - border-radius: 5px; - - .name, .status { - text-align: center; - } -} \ No newline at end of file diff --git a/assets/ts/fm.ts b/assets/ts/fm.ts deleted file mode 100644 index 4884a1c..0000000 --- a/assets/ts/fm.ts +++ /dev/null @@ -1,193 +0,0 @@ -import {applyFormMessages} from "./forms"; - -document.addEventListener('DOMContentLoaded', () => { - const form = document.querySelector('#upload-form'); - if (!form) return; - const upload = document.getElementById('file-upload'); - const uploadLink = document.querySelector('#file-upload-link'); - - const uploadField = document.querySelector('#field-upload'); - - const neverExpireCheckbox = document.querySelector('#field-never_expire'); - const expireAfterDaysField = document.querySelector('#field-expire_after_days'); - - const autogenUrlCheckbox = document.querySelector('#field-autogen_url'); - const slugField = document.querySelector('#field-slug'); - - if (expireAfterDaysField && neverExpireCheckbox) { - const updateExpireAfterDaysField = () => { - expireAfterDaysField.disabled = neverExpireCheckbox.checked; - - const parent = expireAfterDaysField.parentElement?.parentElement; - if (parent) { - if (neverExpireCheckbox.checked) { - parent.classList.add('hidden'); - } else { - parent.classList.remove('hidden'); - } - } - }; - neverExpireCheckbox.addEventListener('change', updateExpireAfterDaysField); - updateExpireAfterDaysField(); - } - - if (slugField && autogenUrlCheckbox) { - const updateSlugField = () => { - slugField.disabled = autogenUrlCheckbox.checked; - - const parent = slugField.parentElement?.parentElement; - if (parent) { - if (autogenUrlCheckbox.checked) { - parent.classList.add('hidden'); - } else { - parent.classList.remove('hidden'); - } - } - }; - autogenUrlCheckbox.addEventListener('change', updateSlugField); - updateSlugField(); - } - - let uploadForm: UploadForm | undefined; - - form.addEventListener('submit', e => { - e.preventDefault(); - if (upload && uploadLink && uploadField && uploadField.files && (!uploadForm || uploadForm.isFinished())) { - uploadForm = new UploadForm(form, upload, uploadLink, uploadField.files[0].name); - uploadForm.updateView(); - uploadForm.start(); - } - }); -}); - - -const units = ['K', 'M', 'G', 'T']; - -class UploadForm { - private finished: boolean = false; - private readonly progressBar: HTMLElement | null; - private readonly progressBarContent: HTMLElement | null; - private readonly status: HTMLElement | null; - private speed: HTMLElement | null; - private xferSpeed: number[] = []; - private lastTransferTime: number | null = null; - private lastLoaded: number = 0; - - private xhr: XMLHttpRequest; - - public constructor( - private form: HTMLFormElement, - private upload: HTMLElement, - private uploadLink: HTMLLinkElement, - private fileName: string, - ) { - this.progressBar = this.upload.querySelector('.progress-bar'); - this.progressBarContent = this.progressBar?.querySelector('.content') || null; - this.status = this.upload.querySelector('.status'); - this.speed = this.status?.querySelector('.speed') || null; - - if (!this.progressBar) - throw new Error('Invalid html'); - - this.xhr = new XMLHttpRequest(); - this.xhr.responseType = 'json'; - this.xhr.upload.addEventListener('progress', e => { - if (this.progressBar && this.progressBarContent) { - if (e.lengthComputable) { - this.progressBar.classList.remove('undefined'); - const percent = (e.loaded / e.total * 100).toFixed(2) + '%'; - this.progressBar.style.setProperty('--progress', `${percent}`); - this.progressBarContent.innerText = percent; - this.updateSpeed(e.loaded); - } else { - this.progressBar.classList.add('undefined'); - } - } - }); - this.xhr.upload.addEventListener('loadstart', () => { - this.status?.classList.remove('hidden'); - }); - this.xhr.addEventListener('load', () => { - this.finished = true; - const response = this.xhr.response; - console.log('done', response); - if (response.status === 'error') { - if (response.messages) { - this.restoreView(); - applyFormMessages(this.form, response.messages); - } - } else if (response.url) { - if (this.status) { - this.status.innerHTML = 'Done!'; - } - const uploadLinkContent = this.uploadLink.querySelector('.content'); - if (uploadLinkContent) { - uploadLinkContent.innerText = response.url; - } - this.uploadLink.classList.remove('hidden'); - } else { - window.location.reload(); - } - }); - this.xhr.addEventListener('error', (e) => { - this.finished = true; - console.error('error', e); - if (this.status) { - this.status.innerHTML = 'Error; upload was interrupted.'; - } - }); - } - - public isFinished(): boolean { - return this.finished; - } - - public updateView(): void { - const uploadName = this.upload.querySelector('.name'); - if (uploadName) { - uploadName.innerText = this.fileName; - } - this.form.classList.add('hidden'); - this.upload.classList.remove('hidden'); - if (this.status) { - this.status.innerHTML = `Uploading @ --`; - this.speed = this.status.querySelector('.speed'); - } - } - - public restoreView(): void { - if (this.status) { - this.status.classList.add('hidden'); - } - this.upload.classList.add('hidden'); - this.form.classList.remove('hidden'); - } - - public start(): void { - const formData = new FormData(this.form); - this.xhr.open('POST', this.form.action); - this.xhr.send(formData); - } - - public updateSpeed(loaded: number): void { - const time = new Date().getTime(); - if (this.lastTransferTime) { - this.xferSpeed.push((loaded - this.lastLoaded) / (time - this.lastTransferTime)); - - if (this.xferSpeed.length > 100) this.xferSpeed = this.xferSpeed.slice(1); - - let speed = this.xferSpeed.reduce((v, c) => v + c) / this.xferSpeed.length; - let unit = 0; - while (speed >= 1000 && unit < units.length - 1) { - speed /= 1000; - unit++; - } - - if (this.speed) { - this.speed.innerText = speed.toFixed(2) + units[unit] + 'Bps'; - } - } - this.lastTransferTime = time; - this.lastLoaded = loaded; - } -} diff --git a/src/assets/views/components/FileUpload.svelte b/src/assets/views/components/FileUpload.svelte new file mode 100644 index 0000000..499e3dc --- /dev/null +++ b/src/assets/views/components/FileUpload.svelte @@ -0,0 +1,192 @@ + + + + +
+
{file?.name}
+
{progress}
+
Uploading @ {speed}
+
diff --git a/src/assets/views/file-uploader.njk b/src/assets/views/file-uploader.njk deleted file mode 100644 index bac4753..0000000 --- a/src/assets/views/file-uploader.njk +++ /dev/null @@ -1,104 +0,0 @@ -{% extends 'layouts' %} - -{% set title = app.name + ' - Upload file' %} - -{% block scripts %} - -{% endblock %} - -{% block body %} -

Upload files

-

You are responsible for the files that you upload.

- -
-
-

Upload a file

- -
- {{ macros.field(_locals, 'file', 'upload', '', 'Choose wisely', 'The maximum upload size is ' + max_upload_size + 'MiB', validation_attributes='required') }} - - {{ macros.field(_locals, 'checkbox', 'never_expire', '', 'Never delete this file') }} - {{ macros.field(_locals, 'number', 'expire_after_days', '30', 'How many days to delete this file after', null, validation_attributes='max="1825"') }} - - {{ macros.field(_locals, 'checkbox', 'autogen_url', '', 'Generate url automatically', null, validation_attributes='checked') }} - {{ macros.field(_locals, 'text', 'slug', '', 'Custom url slug', 'Example: beautiful_image.jpg sets url to https://'+default_domain+'/beautiful_image.jpg', validation_attributes='disabled') }} - - {{ macros.csrf(getCsrfToken) }} - - -
- - - - - - {% set url = flash('url') %} - {% if url | length %} -
-
URL
-
{{ url }}
- -
- {% endif %} -
- - -
-

File list

- - {{ macros.paginate(files.pagination, 'file-uploader', 3) }} - - - - - - - - - - - - - - {% for file in files %} - - - - - - - - {% else %} - - - - {% endfor %} - -
#NameSizeExpires atActions
{{ file.id }}
{{ file.real_name }}
{{ (file.size / (1024 * 1024)).toFixed(2) }}MB{% if file.expires_at %}{{ file.expires_at.toISOString() }}{% else %}Never{% endif %} -
- {% if file.shouldBeDeleted() %} - Pending deletion - {% else %} - - -
- {{ macros.csrf(getCsrfToken) }} - -
- {% endif %} -
-
You haven't uploaded any file yet.
- - {{ macros.paginate(files.pagination, 'file-uploader', 3) }} -
-
-{% endblock %} diff --git a/src/assets/views/file-uploader.svelte b/src/assets/views/file-uploader.svelte new file mode 100644 index 0000000..5439f90 --- /dev/null +++ b/src/assets/views/file-uploader.svelte @@ -0,0 +1,193 @@ + + + + + +

Upload files

+ +
+
+

+ + Upload files +

+

You are responsible for the files that you upload.

+ + {#each finishedFileUploads as fileUpload} + + {/each} + + {#if uploading} + + {:else} +
+ + + + {#if !neverExpire} + + {/if} + + {#if !selectedFiles || selectedFiles.length <= 1} + + {#if !autogenUrl} + + {/if} + {:else} + + {/if} + + {/if} + + {#if $locals.flash.url} + + {/if} +
+ + +
+

+ + File list +

+ + + +
+ + + + + + + + + + + + + {#each $locals.files as file} + + + + + + + + {:else} + + + + {/each} + +
#NameSizeExpires atActions
{file.id} +
{file.real_name}
+
{(file.size / (1024 * 1024)).toFixed(2)}MB + {#if file.expires_at}{file.expires_at}{:else}Never{/if} + + {#if file.shouldBeDeleted} + Pending deletion + {:else} + + +
+ {/if} +
You haven't uploaded any file yet.
+
+ + +
+
+
diff --git a/src/controllers/FileController.ts b/src/controllers/FileController.ts index 2c4dd45..ce1cb77 100644 --- a/src/controllers/FileController.ts +++ b/src/controllers/FileController.ts @@ -27,11 +27,18 @@ export default class FileController extends Controller { protected async getFileUploader(req: Request, res: Response): Promise { const allowedDomains = config.get('allowed_url_domains'); const user = req.as(RequireAuthMiddleware).getUser(); + const files = await FileModel.paginateForUser(req, 25, user.getOrFail('id')); res.render('file-uploader', { max_upload_size: config.get('max_upload_size'), allowed_domains: allowedDomains, default_domain: allowedDomains[config.get('default_url_domain_for_files')], - files: await FileModel.paginateForUser(req, 25, user.getOrFail('id')), + files: files.map(file => ({ + ...file, + shouldBeDeleted: file.shouldBeDeleted(), + url: file.getURL(), + expires_at: file.expires_at?.toISOString(), + })), + pagination: files.pagination?.serialize(), }); } diff --git a/src/models/FileModel.ts b/src/models/FileModel.ts index db3c2e5..c283f78 100644 --- a/src/models/FileModel.ts +++ b/src/models/FileModel.ts @@ -3,6 +3,7 @@ import {Request} from "express"; import User from "swaf/auth/models/User"; import {route} from "swaf/common/Routing"; import Model from "swaf/db/Model"; +import {ModelQueryResult} from "swaf/db/ModelQuery"; import URLRedirect from "./URLRedirect.js"; @@ -15,7 +16,7 @@ export default class FileModel extends Model { return await this.select().where('slug', slug).first(); } - public static async paginateForUser(req: Request, perPage: number, user_id: number): Promise { + public static async paginateForUser(req: Request, perPage: number, user_id: number): Promise> { req.params.sortBy = 'created_at'; req.params.sortDirection = 'DESC'; return await this.paginate(req, perPage, this.select().where('user_id', user_id));