Begin work on media attachments

This commit is contained in:
Jesse Wierzbinski 2023-11-21 14:56:58 -10:00
parent 97af3bc2d0
commit 580958a181
No known key found for this signature in database
7 changed files with 184 additions and 2 deletions

BIN
bun.lockb

Binary file not shown.

View file

@ -0,0 +1,47 @@
import { ConfigType } from "@config";
import { Attachment } from "@prisma/client";
import { APIAsyncAttachment } from "~types/entities/async_attachment";
import { APIAttachment } from "~types/entities/attachment";
export const attachmentToAPI = (
attachment: Attachment
): APIAsyncAttachment | APIAttachment => {
let type = "unknown";
if (attachment.mime_type.startsWith("image/")) {
type = "image";
} else if (attachment.mime_type.startsWith("video/")) {
type = "video";
} else if (attachment.mime_type.startsWith("audio/")) {
type = "audio";
}
return {
id: attachment.id,
type: type as any,
url: attachment.url,
remote_url: attachment.remote_url,
preview_url: attachment.thumbnail_url,
text_url: null,
meta: {
width: attachment.width || undefined,
height: attachment.height || undefined,
fps: attachment.fps || undefined,
size: attachment.size?.toString() || undefined,
duration: attachment.duration || undefined,
length: attachment.size?.toString() || undefined,
// Idk whether size or length is the right value
},
description: attachment.description,
blurhash: attachment.blurhash,
};
};
export const getUrl = (hash: string, config: ConfigType) => {
if (config.media.backend === "local") {
return `${config.http.base_url}/media/${hash}`;
} else if (config.media.backend === "s3") {
return `${config.s3.public_url}/${hash}`;
}
return "";
};

View file

@ -160,7 +160,7 @@ const logRequest = async (req: Request) => {
};
// Remove previous console.log
console.clear();
// console.clear();
console.log(
`${chalk.green(``)} ${chalk.bold(

View file

@ -69,6 +69,7 @@
"dependencies": {
"@aws-sdk/client-s3": "^3.429.0",
"@prisma/client": "^5.6.0",
"blurhash": "^2.0.5",
"chalk": "^5.3.0",
"html-to-text": "^9.0.5",
"ip-matching": "^2.1.2",

View file

@ -121,6 +121,7 @@ model Status {
replies Status[] @relation("StatusToStatusReply")
quotes Status[] @relation("StatusToStatusQuote")
pinnedBy User[] @relation("UserPinnedNotes")
attachments Attachment[]
}
model Token {
@ -136,6 +137,24 @@ model Token {
applicationId String? @db.Uuid
}
model Attachment {
id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid
url String
remote_url String?
thumbnail_url String?
mime_type String
description String?
blurhash String?
sha256 String?
fps Int?
duration Int?
width Int?
height Int?
size Int?
status Status? @relation(fields: [statusId], references: [id], onDelete: Cascade)
statusId String? @db.Uuid
}
model User {
id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid
uri String @unique

View file

@ -0,0 +1,103 @@
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 { 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/v2/media",
auth: {
required: true,
},
});
/**
* Fetch a user
*/
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");
// 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 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 config = getConfig();
if (isImage) {
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 as string | undefined) ?? "",
size: file.size,
blurhash: blurhash ?? undefined,
width: metadata?.width ?? undefined,
height: metadata?.height ?? undefined,
},
});
// Add job to process videos and other media
return jsonResponse({
...attachmentToAPI(newAttachment),
url: undefined,
});
};

View file

@ -161,5 +161,17 @@ export interface ContentFormat {
content: string;
content_type: string;
description?: string;
size?: string;
size?: number;
hash?: {
md5?: string;
sha1?: string;
sha256?: string;
sha512?: string;
[key: string]: string | undefined;
};
blurhash?: string;
fps?: number;
width?: number;
height?: number;
duration?: number;
}