Merge pull request #32 from versia-pub/refactor/media

Refactor and simplify the media pipeline
This commit is contained in:
Gaspard Wierzbinski 2025-02-01 11:02:28 +01:00 committed by GitHub
commit 450058213d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
60 changed files with 10789 additions and 2118 deletions

View file

@ -1,16 +1,14 @@
import { apiRoute, auth, jsonOrForm } from "@/api";
import { mimeLookup } from "@/content_types";
import { mergeAndDeduplicate } from "@/lib";
import { sanitizedHtmlStrip } from "@/sanitization";
import { createRoute } from "@hono/zod-openapi";
import { Emoji, Media, User } from "@versia/kit/db";
import { Emoji, User } from "@versia/kit/db";
import { RolePermissions, Users } from "@versia/kit/tables";
import { and, eq, isNull } from "drizzle-orm";
import ISO6391 from "iso-639-1";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { contentToHtml } from "~/classes/functions/status";
import { MediaManager } from "~/classes/media/media-manager";
import { config } from "~/packages/config-manager/index.ts";
import { ErrorSchema } from "~/types/api";
@ -61,6 +59,7 @@ const schemas = {
.min(1)
.max(2000)
.url()
.transform((a) => new URL(a))
.or(
z
.instanceof(File)
@ -76,6 +75,7 @@ const schemas = {
.min(1)
.max(2000)
.url()
.transform((v) => new URL(v))
.or(
z
.instanceof(File)
@ -204,8 +204,6 @@ export default apiRoute((app) =>
display_name ?? "",
);
const mediaManager = new MediaManager(config);
if (display_name) {
self.displayName = sanitizedDisplayName;
}
@ -247,37 +245,17 @@ export default apiRoute((app) =>
if (avatar) {
if (avatar instanceof File) {
const { path, uploadedFile } =
await mediaManager.addFile(avatar);
const contentType = uploadedFile.type;
self.avatar = Media.getUrl(path);
self.source.avatar = {
content_type: contentType,
};
await user.avatar?.updateFromFile(avatar);
} else {
self.avatar = avatar;
self.source.avatar = {
content_type: await mimeLookup(avatar),
};
await user.avatar?.updateFromUrl(avatar);
}
}
if (header) {
if (header instanceof File) {
const { path, uploadedFile } =
await mediaManager.addFile(header);
const contentType = uploadedFile.type;
self.header = Media.getUrl(path);
self.source.header = {
content_type: contentType,
};
await user.header?.updateFromFile(header);
} else {
self.header = header;
self.source.header = {
content_type: await mimeLookup(header),
};
await user.header?.updateFromUrl(header);
}
}

View file

@ -1,12 +1,10 @@
import { apiRoute, auth, emojiValidator, jsonOrForm } from "@/api";
import { mimeLookup } from "@/content_types";
import { createRoute } from "@hono/zod-openapi";
import { Emoji, Media, db } from "@versia/kit/db";
import { Emojis, RolePermissions } from "@versia/kit/tables";
import { eq } from "drizzle-orm";
import { Emoji } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { MediaManager } from "~/classes/media/media-manager";
import { config } from "~/packages/config-manager";
import { ErrorSchema } from "~/types/api";
@ -31,6 +29,7 @@ const schemas = {
.min(1)
.max(2000)
.url()
.transform((a) => new URL(a))
.or(
z
.instanceof(File)
@ -230,8 +229,6 @@ export default apiRoute((app) => {
);
}
const mediaManager = new MediaManager(config);
const {
global: emojiGlobal,
alt,
@ -248,11 +245,9 @@ export default apiRoute((app) => {
);
}
const modified = structuredClone(emoji.data);
if (element) {
// Check of emoji is an image
let contentType =
const contentType =
element instanceof File
? element.type
: await mimeLookup(element);
@ -265,30 +260,24 @@ export default apiRoute((app) => {
);
}
let url = "";
if (element instanceof File) {
const uploaded = await mediaManager.addFile(element);
url = Media.getUrl(uploaded.path);
contentType = uploaded.uploadedFile.type;
await emoji.media.updateFromFile(element);
} else {
url = element;
await emoji.media.updateFromUrl(element);
}
modified.url = url;
modified.contentType = contentType;
}
modified.shortcode = shortcode ?? modified.shortcode;
modified.alt = alt ?? modified.alt;
modified.category = category ?? modified.category;
if (emojiGlobal !== undefined) {
modified.ownerId = emojiGlobal ? null : user.data.id;
if (alt) {
await emoji.media.updateMetadata({
description: alt,
});
}
await emoji.update(modified);
await emoji.update({
shortcode,
ownerId: emojiGlobal ? null : user.data.id,
category,
});
return context.json(emoji.toApi(), 200);
});
@ -315,11 +304,7 @@ export default apiRoute((app) => {
);
}
const mediaManager = new MediaManager(config);
await mediaManager.deleteFileByUrl(emoji.data.url);
await db.delete(Emojis).where(eq(Emojis.id, id));
await emoji.delete();
return context.body(null, 204);
});

View file

@ -6,7 +6,6 @@ import { Emojis, RolePermissions } from "@versia/kit/tables";
import { and, eq, isNull, or } from "drizzle-orm";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { MediaManager } from "~/classes/media/media-manager";
import { config } from "~/packages/config-manager";
import { ErrorSchema } from "~/types/api";
@ -27,6 +26,7 @@ const schemas = {
.min(1)
.max(2000)
.url()
.transform((a) => new URL(a))
.or(
z
.instanceof(File)
@ -130,10 +130,8 @@ export default apiRoute((app) =>
);
}
let url = "";
// Check of emoji is an image
let contentType =
const contentType =
element instanceof File ? element.type : await mimeLookup(element);
if (!contentType.startsWith("image/")) {
@ -144,25 +142,21 @@ export default apiRoute((app) =>
);
}
if (element instanceof File) {
const mediaManager = new MediaManager(config);
const uploaded = await mediaManager.addFile(element);
url = Media.getUrl(uploaded.path);
contentType = uploaded.uploadedFile.type;
} else {
url = element;
}
const media =
element instanceof File
? await Media.fromFile(element, {
description: alt,
})
: await Media.fromUrl(element, {
description: alt,
});
const emoji = await Emoji.insert({
shortcode,
url,
mediaId: media.id,
visibleInPicker: true,
ownerId: global ? null : user.id,
category,
contentType,
alt,
});
return context.json(emoji.toApi(), 201);

View file

@ -4,7 +4,6 @@ import { Media } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { MediaManager } from "~/classes/media/media-manager";
import { config } from "~/packages/config-manager/index.ts";
import { ErrorSchema } from "~/types/api";
@ -101,38 +100,26 @@ export default apiRoute((app) => {
app.openapi(routePut, async (context) => {
const { id } = context.req.valid("param");
const attachment = await Media.fromId(id);
const media = await Media.fromId(id);
if (!attachment) {
if (!media) {
throw new ApiError(404, "Media not found");
}
const { description, thumbnail } = context.req.valid("form");
const { description, thumbnail: thumbnailFile } =
context.req.valid("form");
let thumbnailUrl = attachment.data.thumbnailUrl;
const mediaManager = new MediaManager(config);
if (thumbnail) {
const { path } = await mediaManager.addFile(thumbnail);
thumbnailUrl = Media.getUrl(path);
if (thumbnailFile) {
await media.updateThumbnail(thumbnailFile);
}
const descriptionText = description || attachment.data.description;
if (
descriptionText !== attachment.data.description ||
thumbnailUrl !== attachment.data.thumbnailUrl
) {
await attachment.update({
description: descriptionText,
thumbnailUrl,
if (description) {
await media.updateMetadata({
description,
});
return context.json(attachment.toApi(), 200);
}
return context.json(attachment.toApi(), 200);
return context.json(media.toApi(), 200);
});
app.openapi(routeGet, async (context) => {

View file

@ -30,9 +30,7 @@ export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { user } = context.get("auth");
await user.update({
avatar: "",
});
await user.header?.delete();
return context.json(user.toApi(true), 200);
}),

View file

@ -30,9 +30,7 @@ export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { user } = context.get("auth");
await user.update({
header: "",
});
await user.header?.delete();
return context.json(user.toApi(true), 200);
}),

View file

@ -1,12 +1,13 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import type { Status as ApiStatus } from "@versia/client/types";
import { db } from "@versia/kit/db";
import { Media, db } from "@versia/kit/db";
import { Emojis } from "@versia/kit/tables";
import { eq } from "drizzle-orm";
import { config } from "~/packages/config-manager/index.ts";
import { fakeRequest, getTestUsers } from "~/tests/utils";
const { users, tokens, deleteUsers } = await getTestUsers(5);
let media: Media;
afterAll(async () => {
await deleteUsers();
@ -14,10 +15,17 @@ afterAll(async () => {
});
beforeAll(async () => {
media = await Media.insert({
content: {
"image/png": {
content: "https://example.com/test.png",
remote: true,
},
},
});
await db.insert(Emojis).values({
contentType: "image/png",
shortcode: "test",
url: "https://example.com/test.png",
mediaId: media.id,
visibleInPicker: true,
});
});