feat(media): add reference-aware S3 image delivery

This commit is contained in:
2026-08-28 19:45:53 +00:00 Unverified
parent 35a786156a
commit 56460dcf13
13 changed files with 591 additions and 9 deletions
+100
View File
@@ -0,0 +1,100 @@
import "server-only";
import { and, eq } from "drizzle-orm";
import { getDb } from "@/db";
import { media } from "@/db/schema";
import { getMediaStorage } from "@/lib/media/storage";
import {
hasValidImageSignature,
MAX_MEDIA_BYTES,
safeObjectFileName,
type MediaUploadInput,
} from "@/lib/media/validation";
export async function createPendingMedia(
input: MediaUploadInput,
authorId: string,
): Promise<{ id: string; objectKey: string; uploadUrl: string; expiresIn: number }> {
const id = crypto.randomUUID();
const objectKey = `media/${id}/${safeObjectFileName(input.fileName)}`;
const expiresIn = 5 * 60;
await getDb().insert(media).values({
id,
objectKey,
fileName: input.fileName,
mimeType: input.mimeType,
byteSize: input.byteSize,
status: "pending",
createdById: authorId,
});
const uploadUrl = getMediaStorage().presign(objectKey, {
method: "PUT",
expiresIn,
type: input.mimeType,
acl: "private",
});
return { id, objectKey, uploadUrl, expiresIn };
}
export async function completePendingMedia(mediaId: string) {
const [record] = await getDb()
.select()
.from(media)
.where(and(eq(media.id, mediaId), eq(media.status, "pending")))
.limit(1);
if (!record) return { status: "not-found" as const };
const file = getMediaStorage().file(record.objectKey);
try {
const stat = await file.stat();
const prefix = await file.slice(0, 16).bytes();
const valid =
stat.size > 0 &&
stat.size <= MAX_MEDIA_BYTES &&
stat.size === record.byteSize &&
hasValidImageSignature(prefix, record.mimeType as Parameters<typeof hasValidImageSignature>[1]);
if (!valid) {
await file.delete().catch(() => undefined);
await getDb().update(media).set({ status: "failed" }).where(eq(media.id, record.id));
return { status: "invalid" as const };
}
const [updated] = await getDb()
.update(media)
.set({
status: "ready",
byteSize: stat.size,
checksum: stat.etag,
})
.where(and(eq(media.id, record.id), eq(media.status, "pending")))
.returning();
return updated
? { status: "ready" as const, media: updated }
: { status: "not-found" as const };
} catch {
return { status: "missing" as const };
}
}
export async function discardUnreferencedMedia(mediaId: string): Promise<boolean> {
const [record] = await getDb().select().from(media).where(eq(media.id, mediaId)).limit(1);
if (
!record ||
record.currentReferenceCount > 0 ||
record.revisionReferenceCount > 0
) {
return false;
}
await getMediaStorage().delete(record.objectKey).catch(() => undefined);
await getDb().delete(media).where(eq(media.id, mediaId));
return true;
}
export async function getReadyMedia(mediaId: string) {
const [record] = await getDb()
.select()
.from(media)
.where(and(eq(media.id, mediaId), eq(media.status, "ready")))
.limit(1);
return record ?? null;
}