135 lines
5.8 KiB
TypeScript
135 lines
5.8 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";
|
|
import RestResponse from "../server/RestResponse.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', '/img/:id')
|
|
async file(id: string, @jwt(v => v.salt, false) @auth() jwt?: JWTPayload) {
|
|
try {
|
|
const start = await Deno.realPath("images");
|
|
const file = await Deno.realPath(`images/${id}`);
|
|
if (!file.startsWith(start)) throw new HttpError("What the fuck are you doing?", 418);
|
|
}
|
|
catch (e) {
|
|
if (!(e instanceof HttpError)) throw new HttpError("File doesn't exist.", 404);
|
|
}
|
|
|
|
const img = await this.db.images.findOne({ _id: new UUID(id.split('.')[0]) }).catch(() => undefined);
|
|
if (img?.visibility === Visibility.Private && img.author !== jwt?.name) throw new HttpError("File doesn't exist.", 404);
|
|
return new RestResponse()
|
|
.body((await Deno.open(`images/${id}`)).readable)
|
|
.contentType(id.split('.')[1]);
|
|
}
|
|
@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();
|
|
}
|
|
} |