117 lines
4.9 KiB
TypeScript
117 lines
4.9 KiB
TypeScript
|
import { UUID } from "https://deno.land/x/web_bson@v0.3.0/mod.js";
|
||
|
|
||
|
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 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";
|
||
|
import AppDatabase from "../AppDatabase.ts";
|
||
|
|
||
|
export default class ImageRouter extends AppRouter {
|
||
|
public static deserialize(image: Image) {
|
||
|
return {
|
||
|
author: image.author,
|
||
|
created: image.created,
|
||
|
name: image.name,
|
||
|
visibility: image.visibility,
|
||
|
id: image._id,
|
||
|
};
|
||
|
}
|
||
|
|
||
|
@rest('GET', '/')
|
||
|
async get(@schema('uuid') id: UUID, @jwt(v => v.salt, false) @auth() jwt?: JWTPayload) {
|
||
|
const image = await this.db.images.findOne({ _id: new UUID(id) });
|
||
|
|
||
|
if (
|
||
|
!image ||
|
||
|
image.visibility === Visibility.Private && image.author !== jwt?.name
|
||
|
) throw new HttpError("Image doesn't exist.");
|
||
|
|
||
|
return ImageRouter.deserialize(image);
|
||
|
}
|
||
|
@rest('GET', '/feed')
|
||
|
async self(@page() page: Page) {
|
||
|
const res = await page.apply(this.db.images.find({})).toArray();
|
||
|
if (!res) throw new HttpError('User not found.');
|
||
|
return res.map(v => ImageRouter.deserialize(v));
|
||
|
}
|
||
|
|
||
|
@rest('POST', '/upload')
|
||
|
async upload(@body() body: Blob, @headers() headers: Headers, @jwt(v => v.salt, true) @auth() jwt: JWTPayload) {
|
||
|
const user = await this.db.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 {
|
||
|
await this.db.images.insertOne(img);
|
||
|
await this.db.users.updateOne({ username: user.username }, { $push: { images: img._id } });
|
||
|
return ImageRouter.deserialize(img);
|
||
|
}
|
||
|
catch (e) {
|
||
|
await Deno.remove(`images/${img._id}.${ext}`);
|
||
|
throw e;
|
||
|
}
|
||
|
}
|
||
|
@rest('POST', '/change')
|
||
|
async change(@body() raw: unknown, @jwt(v => v.salt, true) @auth() jwt: JWTPayload) {
|
||
|
const body = await convert(raw, { id: 'uuid', name: 'string?', visibility: 'number?' });
|
||
|
const user = await this.db.users.findOne({ username: jwt.name });
|
||
|
if (!user) throw new HttpError("You don't exist.");
|
||
|
|
||
|
const img = await this.db.images.findOne({ _id: body.id });
|
||
|
if (!img) throw new HttpError("Image doesn't exist.");
|
||
|
if (user.username !== img.author) throw new HttpError("You don't own the image.");
|
||
|
|
||
|
await this.db.images.updateOne({ _id: body.id }, { $set: { name: body.name, visibility: body.visibility } });
|
||
|
return ImageRouter.deserialize(img);
|
||
|
}
|
||
|
|
||
|
public constructor(private db: AppDatabase) {
|
||
|
super();
|
||
|
}
|
||
|
}
|