mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
Make media uploads work with s3 backend
This commit is contained in:
parent
9064590292
commit
818fcf8666
|
|
@ -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,
|
||||||
|
|
|
||||||
123
server/api/api/v1/media/index.ts
Normal file
123
server/api/api/v1/media/index.ts
Normal 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));
|
||||||
|
};
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue