mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 16:38:19 +01:00
Begin work on media attachments
This commit is contained in:
parent
97af3bc2d0
commit
580958a181
47
database/entities/Attachment.ts
Normal file
47
database/entities/Attachment.ts
Normal 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 "";
|
||||||
|
};
|
||||||
2
index.ts
2
index.ts
|
|
@ -160,7 +160,7 @@ const logRequest = async (req: Request) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remove previous console.log
|
// Remove previous console.log
|
||||||
console.clear();
|
// console.clear();
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`${chalk.green(`✓`)} ${chalk.bold(
|
`${chalk.green(`✓`)} ${chalk.bold(
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.429.0",
|
"@aws-sdk/client-s3": "^3.429.0",
|
||||||
"@prisma/client": "^5.6.0",
|
"@prisma/client": "^5.6.0",
|
||||||
|
"blurhash": "^2.0.5",
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
"html-to-text": "^9.0.5",
|
"html-to-text": "^9.0.5",
|
||||||
"ip-matching": "^2.1.2",
|
"ip-matching": "^2.1.2",
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,7 @@ model Status {
|
||||||
replies Status[] @relation("StatusToStatusReply")
|
replies Status[] @relation("StatusToStatusReply")
|
||||||
quotes Status[] @relation("StatusToStatusQuote")
|
quotes Status[] @relation("StatusToStatusQuote")
|
||||||
pinnedBy User[] @relation("UserPinnedNotes")
|
pinnedBy User[] @relation("UserPinnedNotes")
|
||||||
|
attachments Attachment[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Token {
|
model Token {
|
||||||
|
|
@ -136,6 +137,24 @@ model Token {
|
||||||
applicationId String? @db.Uuid
|
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 {
|
model User {
|
||||||
id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid
|
id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid
|
||||||
uri String @unique
|
uri String @unique
|
||||||
|
|
|
||||||
103
server/api/api/v2/media/index.ts
Normal file
103
server/api/api/v2/media/index.ts
Normal 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,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
@ -161,5 +161,17 @@ export interface ContentFormat {
|
||||||
content: string;
|
content: string;
|
||||||
content_type: string;
|
content_type: string;
|
||||||
description?: 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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue