feat: add image uploading

This commit is contained in:
TopchetoEU 2023-06-28 15:46:57 +03:00
parent b54241a495
commit fba42d96c0
9 changed files with 202 additions and 103 deletions

1
.gitignore vendored
View File

@ -2,4 +2,5 @@
node_modules/
keys/
yarn.lock
pacakage.json
package.lock.json

5
package.json Normal file
View File

@ -0,0 +1,5 @@
{
"dependencies": {
"typescript": "^5.1.5"
}
}

View File

@ -8,9 +8,8 @@ export enum Visibility {
export default interface Image {
_id: UUID;
backingURL: string;
author: string;
name: string;
visibility: Visibility;
created: Date;
author: UUID;
created: number;
}

View File

@ -1,19 +1,22 @@
import { Collection } from "https://deno.land/x/mongo@v0.31.2/mod.ts";
import { UUID } from "https://deno.land/x/web_bson@v0.3.0/mod.js";
import { auth, body, jwt, page, rest, schema } from "../server/decorators.ts";
import { auth, body, headers, jwt, page, rest, schema } from "../server/decorators.ts";
import { JWTPayload } from "../utils/JWT.ts";
import AppRouter from "./AppRouter.ts";
import HttpError from "../server/HttpError.ts";
import User from "../models/User.ts";
import Image, { Visibility } from "../models/Image.ts";
import { Page } from "../server/decorators/page.ts";
import { Headers } from "../server/RestRequest.ts";
import { convert } from "../server/decorators/schema.ts";
import { now } from "../utils/utils.ts";
export default class ImageRouter extends AppRouter {
public static serialize(image: Image) {
return {
author: image.author,
created: image.created.getTime() / 1000,
created: image.created,
name: image.name,
visibility: image.visibility,
id: image._id,
@ -40,6 +43,63 @@ export default class ImageRouter extends AppRouter {
return res.map(v => ImageRouter.serialize(v));
}
@rest('POST', '/upload')
async upload(@body() body: Blob, @headers() headers: Headers, @jwt(v => v.salt, true) @auth() jwt: JWTPayload) {
const user = await this.users.findOne({ username: jwt.name });
if (!user) throw new HttpError("You don't exist.");
// Parse body
const contentType = headers['content-type'] + "";
if (!contentType.startsWith('multipart/form-data')) {
throw new HttpError("Expected a 'Content-type: multipart/form-data; ...' header.");
}
const data = await new Request('http://127.0.0.1', {
body, headers: [ ['content-type', contentType ] ],
method: 'post'
}).formData();
// Clean up data
if (!data.has('file')) throw new HttpError("Expected a 'file' entry in form data.");
if (!data.has('body')) throw new HttpError("Expected a 'body' entry in form data.");
let rawFile: File = data.get('file') as File;
let rawReq: string = data.get('body') as string;
if (typeof rawFile === 'string') rawFile = new File([rawFile], 'unknown');
if (typeof rawReq !== 'string') rawReq = await (rawReq as Blob).text();
if (rawFile.size > (1 << 20) * 2) throw new HttpError("File too large (max 2MB).");
// Extract (and check) extension
const pointI = rawFile.name.lastIndexOf(".");
if (pointI < 0) throw new HttpError("Given file has no extension.");
const ext = rawFile.name.substring(pointI + 1).trim();
if (ext === "") throw new HttpError("Given file has no extension.");
// Clean up request
const req = await convert(rawReq, { name: 'string?', visibility: 'number?' });
req.name ??= new UUID().toString();
req.visibility = 0;
if (req.visibility < 0 || req.visibility > 2) throw new HttpError("body.visibility: Must be 0, 1, or 2");
// Create file
const img: Image = { _id: new UUID(), author: user.username, created: now(), name: req.name!, visibility: req.visibility };
await Deno.mkdir('images', { recursive: true });
const out = await Deno.open(`images/${img._id}.${ext}`, { write: true, create: true });
for await (const bit of rawFile.stream()) out.write(bit);
out.close();
// Write to DB
try {
this.images.insertOne(img);
return ImageRouter.serialize(img);
}
catch (e) {
await Deno.remove(`images/${img._id}.${ext}`);
throw e;
}
}
public constructor(private salt: string, private images: Collection<Image>, private users: Collection<User>) {
super();
users.createIndexes({ indexes: [ { key: { username: 1 }, name: 'Username Index' } ] });

View File

@ -5,5 +5,6 @@ import route from "./decorators/route.ts";
import schema from "./decorators/schema.ts";
import jwt from "./decorators/jwt.ts";
import page from "./decorators/page.ts";
import headers from "./decorators/headers.ts";
export { body, schema, rest, route, auth , jwt, page };
export { body, schema, rest, route, auth , jwt, page, headers };

View File

@ -0,0 +1,6 @@
import { makeParameterModifier } from "../Router.ts";
export default function headers() {
return makeParameterModifier(req => req.headers);
}

View File

@ -2,7 +2,9 @@ import JWT from "../../utils/JWT.ts";
import HttpError from "../HttpError.ts";
import Router, { makeParameterModifier } from "../Router.ts";
export default function jwt<T extends Router>(salt: ((self: T) => string) | keyof T, required = false) {
// SHUT THE FUCK UP
// deno-lint-ignore no-explicit-any
export default function jwt<T extends Router = any>(salt: ((self: T) => string) | keyof T, required = false) {
return makeParameterModifier<T>(function (_req, val?: string) {
if (val === undefined) return undefined;
const s = typeof salt === 'function' ? salt(this) : this[salt] as string;

View File

@ -4,11 +4,30 @@ import HttpError from "../HttpError.ts";
import { makeParameterModifier } from "../Router.ts";
export type PrimitiveSchema = 'string' | 'number' | 'boolean' | 'uuid' | 'object';
export type OptionalSchema = `${PrimitiveSchema}?`;
export type Schema = 'any' | 'null' | PrimitiveSchema | OptionalSchema | Schema[] | ({ $optional?: boolean; } & { [key: string]: Schema });
export type OptionalSchema<T extends PrimitiveSchema = PrimitiveSchema> = `${T}?`;
export type ObjectSchema = { $optional?: boolean; } & { [key: string]: Schema };
export type OrSchema = [] | [Schema] | [Schema, Schema] | [Schema, Schema, Schema];
export type Schema = 'any' | 'null' | PrimitiveSchema | OptionalSchema | OrSchema | ObjectSchema;
export default function schema(desc: Schema) {
function stringify(desc: Schema): string {
export type ObjectSchemaType<T extends ObjectSchema> =
({ [x in keyof T]: SchemaType<T[x]> }) |
(T["$optional"] extends true ? undefined : never);
export type SchemaType<T extends Schema> =
T extends 'any' ? any :
T extends 'null' ? null :
T extends 'string' ? string :
T extends 'number' ? number :
T extends 'boolean' ? boolean :
T extends 'uuid' ? UUID :
T extends OptionalSchema<infer T> ? SchemaType<T> | undefined :
T extends [] ? never :
T extends [Schema] ? SchemaType<T[0]> :
T extends [Schema, Schema] ? SchemaType<T[0]> | SchemaType<T[1]> :
T extends [Schema, Schema, Schema] ? SchemaType<T[0]> | SchemaType<T[1]> | SchemaType<T[2]> :
T extends ObjectSchema ? ObjectSchemaType<T> : never;
function stringify(desc: Schema): string {
if (typeof desc === 'string') return desc;
if (desc instanceof Array) return desc.map(stringify).join(' | ');
@ -24,8 +43,8 @@ export default function schema(desc: Schema) {
if (desc.$optional) res += '?';
return res;
}
async function convert(path: string[], val: unknown, desc: Schema): Promise<any> {
}
async function _convert(path: string[], val: unknown, desc: Schema): Promise<any> {
const _path = path.join('.');
if (val instanceof Blob) val = await val.text();
if (val instanceof ReadableStream) {
@ -38,7 +57,7 @@ export default function schema(desc: Schema) {
val = await new Blob(res).text();
}
if (desc === 'any') return val;
if (desc === 'any') return val as any;
if (typeof desc === 'string') {
let type: string = desc;
@ -77,7 +96,7 @@ export default function schema(desc: Schema) {
else if (desc instanceof Array) {
for (const type of desc) {
try {
return convert(path, val, type);
return await _convert(path, val, type);
}
catch { /**/ }
}
@ -92,19 +111,24 @@ export default function schema(desc: Schema) {
val = JSON.parse(val);
}
catch (e) {
if (val === null) throw new HttpError(`${_path}: Invalid JSON given for object: ${e}`);
throw new HttpError(`${_path}: Invalid JSON given for object: ${e}`);
}
}
else throw new HttpError(`${_path}: Expected an object or a valid json string.`);
for (const key in desc) {
if (key === '$optional') continue;
(val as any)[key] = convert([ ...path, key ], (val as any)[key], desc[key]);
(val as any)[key] = await _convert([ ...path, key ], (val as any)[key], desc[key]);
}
return val;
}
}
return makeParameterModifier(async (_req, val, name) => await convert([name], val, desc));
}
export function convert<T extends Schema>(val: unknown, desc: T, path?: string): Promise<SchemaType<T>> {
return _convert(path ? [path] : [], val, desc);
}
export default function schema(desc: Schema) {
return makeParameterModifier(async (_req, val, name) => await _convert([name], val, desc));
}

View File

@ -1,6 +1,7 @@
{
"include": [ "src/**/*.ts" ],
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
}
}