Add delete files, file upload size limit and file manager barebone
This commit is contained in:
parent
b8c1ed1139
commit
3e1a9a8ca6
@ -16,4 +16,6 @@ export default Object.assign(require("wms-core/config/default").default, {
|
|||||||
},
|
},
|
||||||
gitlab_webhook_token: 'secret',
|
gitlab_webhook_token: 'secret',
|
||||||
newlyGeneratedSlugSize: 3,
|
newlyGeneratedSlugSize: 3,
|
||||||
|
default_file_ttl: 30, // 30 seconds
|
||||||
|
max_upload_size: '100mb',
|
||||||
});
|
});
|
@ -14,4 +14,5 @@ export default Object.assign(require("wms-core/config/production").default, {
|
|||||||
},
|
},
|
||||||
gitlab_webhook_token: 'CHANGEME',
|
gitlab_webhook_token: 'CHANGEME',
|
||||||
newlyGeneratedSlugSize: 5,
|
newlyGeneratedSlugSize: 5,
|
||||||
|
default_file_ttl: 30 * 24 * 3600, // 30 days
|
||||||
});
|
});
|
@ -125,10 +125,10 @@ export default class App extends Application {
|
|||||||
// Core functionality
|
// Core functionality
|
||||||
this.use(new MailController());
|
this.use(new MailController());
|
||||||
|
|
||||||
// Other functionality
|
|
||||||
this.use(new FileController());
|
|
||||||
|
|
||||||
// Semi-static
|
// Semi-static
|
||||||
this.use(new AboutController());
|
this.use(new AboutController());
|
||||||
|
|
||||||
|
// Global slug
|
||||||
|
this.use(new FileController());
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,31 +1,35 @@
|
|||||||
import Controller from "wms-core/Controller";
|
import Controller from "wms-core/Controller";
|
||||||
import {REQUIRE_AUTH_MIDDLEWARE} from "wms-core/auth/AuthComponent";
|
import {REQUIRE_AUTH_MIDDLEWARE} from "wms-core/auth/AuthComponent";
|
||||||
import {Request, Response} from "express";
|
import express, {Request, Response} from "express";
|
||||||
import {BadRequestError, NotFoundHttpError, ServerError} from "wms-core/HttpError";
|
import {BadRequestError, NotFoundHttpError, ServerError} from "wms-core/HttpError";
|
||||||
import FileModel from "../models/FileModel";
|
import FileModel from "../models/FileModel";
|
||||||
import fileUpload from "express-fileupload";
|
import fileUpload from "express-fileupload";
|
||||||
import {cryptoRandomDictionary} from "wms-core/Utils";
|
import {cryptoRandomDictionary} from "wms-core/Utils";
|
||||||
import config from "config";
|
import config from "config";
|
||||||
|
import * as fs from "fs";
|
||||||
const FILE_UPLOAD_MIDDLEWARE = fileUpload({
|
|
||||||
tempFileDir: 'storage/tmp',
|
|
||||||
useTempFiles: true,
|
|
||||||
abortOnLimit: true,
|
|
||||||
createParentPath: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const SLUG_DICTIONARY = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
const SLUG_DICTIONARY = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||||
export default class FileController extends Controller {
|
export default class FileController extends Controller {
|
||||||
routes(): void {
|
routes(): void {
|
||||||
this.get('/f/:slug', this.downloadFile, 'get-file');
|
this.get('/files/:page?', this.getFileManager, 'file-manager', REQUIRE_AUTH_MIDDLEWARE);
|
||||||
this.post('/f', this.postFile, 'post-file', REQUIRE_AUTH_MIDDLEWARE, FILE_UPLOAD_MIDDLEWARE);
|
this.get('/files/delete/:slug', this.deleteFile, 'delete-file-frontend', REQUIRE_AUTH_MIDDLEWARE);
|
||||||
this.put('/f/:slug', this.putFile, 'put-file', REQUIRE_AUTH_MIDDLEWARE, FILE_UPLOAD_MIDDLEWARE);
|
|
||||||
this.delete('/f/:slug', this.deleteFile, 'delete-file', REQUIRE_AUTH_MIDDLEWARE);
|
this.get('/:slug', this.downloadFile, 'get-file');
|
||||||
|
this.post('/', this.postFile, 'post-file', REQUIRE_AUTH_MIDDLEWARE, FILE_UPLOAD_MAX_SIZE_MIDDLEWARE, FILE_UPLOAD_MIDDLEWARE);
|
||||||
|
this.put('/:slug', this.putFile, 'put-file', REQUIRE_AUTH_MIDDLEWARE, FILE_UPLOAD_MAX_SIZE_MIDDLEWARE, FILE_UPLOAD_MIDDLEWARE);
|
||||||
|
this.delete('/:slug', this.deleteFile, 'delete-file', REQUIRE_AUTH_MIDDLEWARE);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async getFileManager(req: Request, res: Response): Promise<void> {
|
||||||
|
res.render('file-manager', {
|
||||||
|
files: await FileModel.paginate<FileModel>(req, 100),
|
||||||
|
max_upload_size: config.get<string>('max_upload_size'),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async downloadFile(req: Request, res: Response): Promise<void> {
|
protected async downloadFile(req: Request, res: Response): Promise<void> {
|
||||||
const file = await FileModel.getBySlug(req.params.slug);
|
const file = await FileModel.getBySlug(req.params.slug);
|
||||||
if (!file) throw new NotFoundHttpError('File', req.url);
|
if (!file || file.shouldBeDeleted()) throw new NotFoundHttpError('File', req.url);
|
||||||
|
|
||||||
switch (file.storage_type) {
|
switch (file.storage_type) {
|
||||||
case 'local':
|
case 'local':
|
||||||
@ -37,7 +41,7 @@ export default class FileController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async postFile(req: Request, res: Response): Promise<void> {
|
protected async postFile(req: Request, res: Response): Promise<void> {
|
||||||
await this.handleFileUpload(await this.generateSlug(10), req, res);
|
await this.handleFileUpload(req.body.slug || await this.generateSlug(10), req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async putFile(req: Request, res: Response): Promise<void> {
|
protected async putFile(req: Request, res: Response): Promise<void> {
|
||||||
@ -63,6 +67,7 @@ export default class FileController extends Controller {
|
|||||||
real_name: upload.name,
|
real_name: upload.name,
|
||||||
storage_type: 'local',
|
storage_type: 'local',
|
||||||
storage_path: 'storage/uploads/' + slug,
|
storage_path: 'storage/uploads/' + slug,
|
||||||
|
ttl: req.body.ttl !== undefined ? parseInt(req.body.ttl) : config.get<number>('default_file_ttl'),
|
||||||
});
|
});
|
||||||
|
|
||||||
await file.save();
|
await file.save();
|
||||||
@ -73,13 +78,36 @@ export default class FileController extends Controller {
|
|||||||
url: file.getURL(),
|
url: file.getURL(),
|
||||||
}),
|
}),
|
||||||
text: () => res.send(file.getURL()),
|
text: () => res.send(file.getURL()),
|
||||||
html: () => res.render('upload_success', {url: file.getURL()}),
|
html: () => {
|
||||||
|
req.flash('success', 'Upload success! ' + file.getURL());
|
||||||
|
res.redirectBack('/');
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async deleteFile(req: Request, res: Response): Promise<void> {
|
protected async deleteFile(req: Request, res: Response): Promise<void> {
|
||||||
const file = await FileModel.getBySlug(req.params.slug);
|
const file = await FileModel.getBySlug(req.params.slug);
|
||||||
if (!file) throw new NotFoundHttpError('File', req.url);
|
if (!file) throw new NotFoundHttpError('File', req.url);
|
||||||
|
|
||||||
|
switch (file.storage_type) {
|
||||||
|
case 'local':
|
||||||
|
await file.delete();
|
||||||
|
fs.unlinkSync(file.storage_path);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ServerError(`This file cannot be deleted. Deletion protocol for ${file.storage_type} storage type not implemented.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.format({
|
||||||
|
json: () => res.json({
|
||||||
|
status: 'success',
|
||||||
|
}),
|
||||||
|
text: () => res.send('success'),
|
||||||
|
html: () => {
|
||||||
|
req.flash('success', 'Successfully deleted file.');
|
||||||
|
res.redirectBack('/');
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async generateSlug(tries: number): Promise<string> {
|
private async generateSlug(tries: number): Promise<string> {
|
||||||
@ -93,4 +121,13 @@ export default class FileController extends Controller {
|
|||||||
} while (i < tries)
|
} while (i < tries)
|
||||||
throw new ServerError('Failed to generate slug; newly generated slug size should be increased by 1.');
|
throw new ServerError('Failed to generate slug; newly generated slug size should be increased by 1.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FILE_UPLOAD_MAX_SIZE_MIDDLEWARE = express.urlencoded({limit: config.get<string>('max_upload_size')});
|
||||||
|
|
||||||
|
const FILE_UPLOAD_MIDDLEWARE = fileUpload({
|
||||||
|
tempFileDir: 'storage/tmp',
|
||||||
|
useTempFiles: true,
|
||||||
|
abortOnLimit: true,
|
||||||
|
createParentPath: true,
|
||||||
|
});
|
||||||
|
@ -8,7 +8,7 @@ export default class CreateAuthTokensTable extends Migration {
|
|||||||
'user_id INT NOT NULL,' +
|
'user_id INT NOT NULL,' +
|
||||||
'secret VARCHAR(64) UNIQUE NOT NULL,' +
|
'secret VARCHAR(64) UNIQUE NOT NULL,' +
|
||||||
'created_at DATETIME NOT NULL DEFAULT NOW(),' +
|
'created_at DATETIME NOT NULL DEFAULT NOW(),' +
|
||||||
'ttl INT NOT NULL,' +
|
'ttl INT UNSIGNED NOT NULL,' +
|
||||||
'PRIMARY KEY (id)' +
|
'PRIMARY KEY (id)' +
|
||||||
')', connection);
|
')', connection);
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,8 @@ export default class CreateFilesTable extends Migration {
|
|||||||
'real_name VARCHAR(259) NOT NULL,' +
|
'real_name VARCHAR(259) NOT NULL,' +
|
||||||
'storage_type VARCHAR(64) NOT NULL,' +
|
'storage_type VARCHAR(64) NOT NULL,' +
|
||||||
'storage_path VARCHAR(1745) NOT NULL,' +
|
'storage_path VARCHAR(1745) NOT NULL,' +
|
||||||
|
'created_at DATETIME NOT NULL DEFAULT NOW(),' +
|
||||||
|
'ttl INT UNSIGNED NOT NULL,' +
|
||||||
'PRIMARY KEY (id)' +
|
'PRIMARY KEY (id)' +
|
||||||
')', connection);
|
')', connection);
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ export default class AuthToken extends Model implements AuthProof {
|
|||||||
public getExpirationDate(): Date {
|
public getExpirationDate(): Date {
|
||||||
if (!this.created_at) return new Date();
|
if (!this.created_at) return new Date();
|
||||||
|
|
||||||
return new Date(this.created_at.getTime() + this.ttl);
|
return new Date(this.created_at.getTime() + this.ttl * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getEmail(): Promise<string> {
|
public async getEmail(): Promise<string> {
|
||||||
|
@ -17,12 +17,16 @@ export default class FileModel extends Model {
|
|||||||
public readonly real_name!: string;
|
public readonly real_name!: string;
|
||||||
public readonly storage_type!: FileStorage;
|
public readonly storage_type!: FileStorage;
|
||||||
public readonly storage_path!: string;
|
public readonly storage_path!: string;
|
||||||
|
public created_at?: Date;
|
||||||
|
public readonly ttl!: number;
|
||||||
|
|
||||||
protected defineProperties() {
|
protected defineProperties() {
|
||||||
this.defineProperty('slug', new Validator().defined().minLength(1).maxLength(259).unique(this, 'slug'));
|
this.defineProperty('slug', new Validator().defined().minLength(1).maxLength(259).unique(this, 'slug'));
|
||||||
this.defineProperty('real_name', new Validator().defined().minLength(1).maxLength(259));
|
this.defineProperty('real_name', new Validator().defined().minLength(1).maxLength(259));
|
||||||
this.defineProperty('storage_type', new Validator().defined().maxLength(64));
|
this.defineProperty('storage_type', new Validator().defined().maxLength(64));
|
||||||
this.defineProperty('storage_path', new Validator().defined().maxLength(1745));
|
this.defineProperty('storage_path', new Validator().defined().maxLength(1745));
|
||||||
|
this.defineProperty('created_at', new Validator());
|
||||||
|
this.defineProperty('ttl', new Validator().defined().min(0).max(4294967295));
|
||||||
}
|
}
|
||||||
|
|
||||||
public getURL(): string {
|
public getURL(): string {
|
||||||
@ -30,6 +34,16 @@ export default class FileModel extends Model {
|
|||||||
slug: this.slug,
|
slug: this.slug,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getExpirationDate(): Date {
|
||||||
|
if (!this.created_at) return new Date();
|
||||||
|
|
||||||
|
return new Date(this.created_at.getTime() + this.ttl * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
public shouldBeDeleted(): boolean {
|
||||||
|
return new Date().getTime() >= this.getExpirationDate().getTime();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FileStorage = 'local';
|
export type FileStorage = 'local';
|
||||||
|
23
views/file-manager.njk
Normal file
23
views/file-manager.njk
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{% extends 'layouts/base.njk' %}
|
||||||
|
|
||||||
|
{% set title = 'ily.li - File manager' %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<h1>File manager</h1>
|
||||||
|
<p>You're their manager, please be nice with them.</p>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<section class="panel">
|
||||||
|
<h2>Upload a file</h2>
|
||||||
|
|
||||||
|
<form action="{{ route('post-file') }}" method="POST">
|
||||||
|
{{ macros.field(_locals, 'file', 'upload', '', 'Choose wisely', 'The maximum upload size is' + max_upload_size, validation_attributes='required') }}
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<h2>File list</h2>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
Loading…
Reference in New Issue
Block a user