Make media uploads work with s3 backend

This commit is contained in:
Jesse Wierzbinski 2023-11-28 13:54:39 -10:00
parent 9064590292
commit 818fcf8666
No known key found for this signature in database
3 changed files with 141 additions and 8 deletions

View file

@ -22,6 +22,8 @@ import {
import { emojiToAPI, emojiToLysand, parseEmojis } from "./Emoji"; import { emojiToAPI, emojiToLysand, parseEmojis } from "./Emoji";
import type { APIStatus } from "~types/entities/status"; import type { APIStatus } from "~types/entities/status";
import { applicationToAPI } from "./Application"; import { applicationToAPI } from "./Application";
import { attachmentToAPI } from "./Attachment";
import type { APIAttachment } from "~types/entities/attachment";
const config = getConfig(); const config = getConfig();
@ -53,6 +55,7 @@ export const statusAndUserRelations: Prisma.StatusInclude = {
}, },
}, },
}, },
reblogs: true,
attachments: true, attachments: true,
instance: true, instance: true,
mentions: { mentions: {
@ -438,7 +441,10 @@ export const statusToAPI = async (
), ),
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
favourites_count: (status.likes ?? []).length, favourites_count: (status.likes ?? []).length,
media_attachments: [], // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
media_attachments: (status.attachments ?? []).map(
a => attachmentToAPI(a) as APIAttachment
),
// @ts-expect-error Prisma TypeScript types dont include relations // @ts-expect-error Prisma TypeScript types dont include relations
mentions: status.mentions.map(mention => userToAPI(mention)), mentions: status.mentions.map(mention => userToAPI(mention)),
language: null, language: null,
@ -458,11 +464,7 @@ export const statusToAPI = async (
reblogId: status.id, reblogId: status.id,
}, },
})), })),
reblogs_count: await client.status.count({ reblogs_count: status._count.reblogs,
where: {
reblogId: status.id,
},
}),
replies_count: status._count.replies, replies_count: status._count.replies,
sensitive: status.sensitive, sensitive: status.sensitive,
spoiler_text: status.spoilerText, spoiler_text: status.spoilerText,

View file

@ -0,0 +1,123 @@
import { applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource";
import { encode } from "blurhash";
import { getFromRequest } from "~database/entities/User";
import type { APIRouteMeta } from "~types/api";
import sharp from "sharp";
import { uploadFile } from "~classes/media";
import { getConfig } from "@config";
import { attachmentToAPI, getUrl } from "~database/entities/Attachment";
export const meta: APIRouteMeta = applyConfig({
allowedMethods: ["POST"],
ratelimits: {
max: 10,
duration: 60,
},
route: "/api/v1/media",
auth: {
required: true,
oauthPermissions: ["write:media"],
},
});
/**
* Upload new media
*/
export default async (req: Request): Promise<Response> => {
const { user } = await getFromRequest(req);
if (!user) {
return errorResponse("Unauthorized", 401);
}
const form = await req.formData();
const file = form.get("file") as unknown as File | undefined;
const thumbnail = form.get("thumbnail");
const description = form.get("description") as string | undefined;
// Floating point numbers from -1.0 to 1.0, comma delimited
// const focus = form.get("focus");
if (!file) {
return errorResponse("No file provided", 400);
}
const config = getConfig();
if (file.size > config.validation.max_media_size) {
return errorResponse(
`File too large, max size is ${config.validation.max_media_size} bytes`,
413
);
}
if (
config.validation.enforce_mime_types &&
!config.validation.allowed_mime_types.includes(file.type)
) {
return errorResponse("Invalid file type", 415);
}
if (
description &&
description.length > config.validation.max_media_description_size
) {
return errorResponse(
`Description too long, max length is ${config.validation.max_media_description_size} characters`,
413
);
}
const sha256 = new Bun.SHA256();
const isImage = file.type.startsWith("image/");
const metadata = isImage
? await sharp(await file.arrayBuffer()).metadata()
: null;
const blurhash = isImage
? encode(
new Uint8ClampedArray(await file.arrayBuffer()),
metadata?.width ?? 0,
metadata?.height ?? 0,
4,
4
)
: null;
let url = "";
const hash = await uploadFile(file, config);
url = hash ? getUrl(hash, config) : "";
let thumbnailUrl = "";
if (thumbnail) {
const hash = await uploadFile(thumbnail as unknown as File, config);
thumbnailUrl = hash ? getUrl(hash, config) : "";
}
const newAttachment = await client.attachment.create({
data: {
url,
thumbnail_url: thumbnailUrl,
sha256: sha256.update(await file.arrayBuffer()).digest("hex"),
mime_type: file.type,
description: description ?? "",
size: file.size,
blurhash: blurhash ?? undefined,
width: metadata?.width ?? undefined,
height: metadata?.height ?? undefined,
},
});
// TODO: Add job to process videos and other media
return jsonResponse(attachmentToAPI(newAttachment));
};

View file

@ -10,7 +10,10 @@ import {
statusAndUserRelations, statusAndUserRelations,
statusToAPI, statusToAPI,
} from "~database/entities/Status"; } from "~database/entities/Status";
import { getFromRequest } from "~database/entities/User"; import {
getFromRequest,
type UserWithRelations,
} from "~database/entities/User";
import type { APIRouteMeta } from "~types/api"; import type { APIRouteMeta } from "~types/api";
export const meta: APIRouteMeta = applyConfig({ export const meta: APIRouteMeta = applyConfig({
@ -84,10 +87,15 @@ export default async (
}); });
// Create notification for reblog if reblogged user is on the same instance // Create notification for reblog if reblogged user is on the same instance
if (status.reblog?.author.instanceId === user.instanceId) { if (
// @ts-expect-error Prisma relations not showing in types
(status.reblog?.author as UserWithRelations).instanceId ===
user.instanceId
) {
await client.notification.create({ await client.notification.create({
data: { data: {
accountId: user.id, accountId: user.id,
// @ts-expect-error Prisma relations not showing in types
notifiedId: status.reblog.authorId, notifiedId: status.reblog.authorId,
type: "reblog", type: "reblog",
statusId: status.reblogId, statusId: status.reblogId,