feat: add image uploading
This commit is contained in:
parent
b54241a495
commit
fba42d96c0
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,4 +2,5 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
keys/
|
keys/
|
||||||
yarn.lock
|
yarn.lock
|
||||||
|
pacakage.json
|
||||||
package.lock.json
|
package.lock.json
|
5
package.json
Normal file
5
package.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"typescript": "^5.1.5"
|
||||||
|
}
|
||||||
|
}
|
@ -8,9 +8,8 @@ export enum Visibility {
|
|||||||
|
|
||||||
export default interface Image {
|
export default interface Image {
|
||||||
_id: UUID;
|
_id: UUID;
|
||||||
backingURL: string;
|
author: string;
|
||||||
name: string;
|
name: string;
|
||||||
visibility: Visibility;
|
visibility: Visibility;
|
||||||
created: Date;
|
created: number;
|
||||||
author: UUID;
|
|
||||||
}
|
}
|
@ -1,19 +1,22 @@
|
|||||||
import { Collection } from "https://deno.land/x/mongo@v0.31.2/mod.ts";
|
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 { 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 { JWTPayload } from "../utils/JWT.ts";
|
||||||
import AppRouter from "./AppRouter.ts";
|
import AppRouter from "./AppRouter.ts";
|
||||||
import HttpError from "../server/HttpError.ts";
|
import HttpError from "../server/HttpError.ts";
|
||||||
import User from "../models/User.ts";
|
import User from "../models/User.ts";
|
||||||
import Image, { Visibility } from "../models/Image.ts";
|
import Image, { Visibility } from "../models/Image.ts";
|
||||||
import { Page } from "../server/decorators/page.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 {
|
export default class ImageRouter extends AppRouter {
|
||||||
public static serialize(image: Image) {
|
public static serialize(image: Image) {
|
||||||
return {
|
return {
|
||||||
author: image.author,
|
author: image.author,
|
||||||
created: image.created.getTime() / 1000,
|
created: image.created,
|
||||||
name: image.name,
|
name: image.name,
|
||||||
visibility: image.visibility,
|
visibility: image.visibility,
|
||||||
id: image._id,
|
id: image._id,
|
||||||
@ -40,6 +43,63 @@ export default class ImageRouter extends AppRouter {
|
|||||||
return res.map(v => ImageRouter.serialize(v));
|
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>) {
|
public constructor(private salt: string, private images: Collection<Image>, private users: Collection<User>) {
|
||||||
super();
|
super();
|
||||||
users.createIndexes({ indexes: [ { key: { username: 1 }, name: 'Username Index' } ] });
|
users.createIndexes({ indexes: [ { key: { username: 1 }, name: 'Username Index' } ] });
|
||||||
|
@ -5,5 +5,6 @@ import route from "./decorators/route.ts";
|
|||||||
import schema from "./decorators/schema.ts";
|
import schema from "./decorators/schema.ts";
|
||||||
import jwt from "./decorators/jwt.ts";
|
import jwt from "./decorators/jwt.ts";
|
||||||
import page from "./decorators/page.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 };
|
6
src/server/decorators/headers.ts
Normal file
6
src/server/decorators/headers.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { makeParameterModifier } from "../Router.ts";
|
||||||
|
|
||||||
|
|
||||||
|
export default function headers() {
|
||||||
|
return makeParameterModifier(req => req.headers);
|
||||||
|
}
|
@ -2,7 +2,9 @@ import JWT from "../../utils/JWT.ts";
|
|||||||
import HttpError from "../HttpError.ts";
|
import HttpError from "../HttpError.ts";
|
||||||
import Router, { makeParameterModifier } from "../Router.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) {
|
return makeParameterModifier<T>(function (_req, val?: string) {
|
||||||
if (val === undefined) return undefined;
|
if (val === undefined) return undefined;
|
||||||
const s = typeof salt === 'function' ? salt(this) : this[salt] as string;
|
const s = typeof salt === 'function' ? salt(this) : this[salt] as string;
|
||||||
|
@ -4,11 +4,30 @@ import HttpError from "../HttpError.ts";
|
|||||||
import { makeParameterModifier } from "../Router.ts";
|
import { makeParameterModifier } from "../Router.ts";
|
||||||
|
|
||||||
export type PrimitiveSchema = 'string' | 'number' | 'boolean' | 'uuid' | 'object';
|
export type PrimitiveSchema = 'string' | 'number' | 'boolean' | 'uuid' | 'object';
|
||||||
export type OptionalSchema = `${PrimitiveSchema}?`;
|
export type OptionalSchema<T extends PrimitiveSchema = PrimitiveSchema> = `${T}?`;
|
||||||
export type Schema = 'any' | 'null' | PrimitiveSchema | OptionalSchema | Schema[] | ({ $optional?: boolean; } & { [key: string]: Schema });
|
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) {
|
export type ObjectSchemaType<T extends ObjectSchema> =
|
||||||
function stringify(desc: Schema): string {
|
({ [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 (typeof desc === 'string') return desc;
|
||||||
if (desc instanceof Array) return desc.map(stringify).join(' | ');
|
if (desc instanceof Array) return desc.map(stringify).join(' | ');
|
||||||
|
|
||||||
@ -24,8 +43,8 @@ export default function schema(desc: Schema) {
|
|||||||
if (desc.$optional) res += '?';
|
if (desc.$optional) res += '?';
|
||||||
|
|
||||||
return 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('.');
|
const _path = path.join('.');
|
||||||
if (val instanceof Blob) val = await val.text();
|
if (val instanceof Blob) val = await val.text();
|
||||||
if (val instanceof ReadableStream) {
|
if (val instanceof ReadableStream) {
|
||||||
@ -38,7 +57,7 @@ export default function schema(desc: Schema) {
|
|||||||
|
|
||||||
val = await new Blob(res).text();
|
val = await new Blob(res).text();
|
||||||
}
|
}
|
||||||
if (desc === 'any') return val;
|
if (desc === 'any') return val as any;
|
||||||
|
|
||||||
if (typeof desc === 'string') {
|
if (typeof desc === 'string') {
|
||||||
let type: string = desc;
|
let type: string = desc;
|
||||||
@ -77,7 +96,7 @@ export default function schema(desc: Schema) {
|
|||||||
else if (desc instanceof Array) {
|
else if (desc instanceof Array) {
|
||||||
for (const type of desc) {
|
for (const type of desc) {
|
||||||
try {
|
try {
|
||||||
return convert(path, val, type);
|
return await _convert(path, val, type);
|
||||||
}
|
}
|
||||||
catch { /**/ }
|
catch { /**/ }
|
||||||
}
|
}
|
||||||
@ -92,19 +111,24 @@ export default function schema(desc: Schema) {
|
|||||||
val = JSON.parse(val);
|
val = JSON.parse(val);
|
||||||
}
|
}
|
||||||
catch (e) {
|
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.`);
|
else throw new HttpError(`${_path}: Expected an object or a valid json string.`);
|
||||||
|
|
||||||
for (const key in desc) {
|
for (const key in desc) {
|
||||||
if (key === '$optional') continue;
|
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 val;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export function convert<T extends Schema>(val: unknown, desc: T, path?: string): Promise<SchemaType<T>> {
|
||||||
return makeParameterModifier(async (_req, val, name) => await convert([name], val, desc));
|
return _convert(path ? [path] : [], val, desc);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default function schema(desc: Schema) {
|
||||||
|
return makeParameterModifier(async (_req, val, name) => await _convert([name], val, desc));
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
|
"include": [ "src/**/*.ts" ],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"emitDecoratorMetadata": true,
|
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user