refactor(media): use asset-centric storage key layout
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { randomUUID } from "node:crypto"
|
||||
import { hasPermission } from "@cms/content/rbac"
|
||||
import { createMediaAsset } from "@cms/db"
|
||||
|
||||
@@ -57,6 +58,20 @@ function parseTags(formData: FormData): string[] {
|
||||
.filter((item) => item.length > 0)
|
||||
}
|
||||
|
||||
function deriveTitleFromFilename(fileName: string): string {
|
||||
const trimmed = fileName.trim()
|
||||
|
||||
if (!trimmed) {
|
||||
return "Untitled media"
|
||||
}
|
||||
|
||||
const dotIndex = trimmed.lastIndexOf(".")
|
||||
const base = dotIndex > 0 ? trimmed.slice(0, dotIndex) : trimmed
|
||||
const normalized = base.trim()
|
||||
|
||||
return normalized.length > 0 ? normalized : "Untitled media"
|
||||
}
|
||||
|
||||
function isMimeAllowed(mediaType: string, mimeType: string): boolean {
|
||||
const rule = ALLOWED_MIME_BY_TYPE[mediaType]
|
||||
|
||||
@@ -116,14 +131,9 @@ export async function POST(request: Request): Promise<Response> {
|
||||
return badRequest("Invalid form payload.")
|
||||
}
|
||||
|
||||
const title = parseTextField(formData, "title")
|
||||
const type = parseTextField(formData, "type")
|
||||
const fileEntry = formData.get("file")
|
||||
|
||||
if (!title) {
|
||||
return badRequest("Title is required.")
|
||||
}
|
||||
|
||||
if (!type) {
|
||||
return badRequest("Type is required.")
|
||||
}
|
||||
@@ -146,13 +156,21 @@ export async function POST(request: Request): Promise<Response> {
|
||||
return badRequest(`File type ${fileEntry.type || "unknown"} is not allowed for ${type}.`)
|
||||
}
|
||||
|
||||
const title = parseTextField(formData, "title") || deriveTitleFromFilename(fileEntry.name)
|
||||
const mediaAssetId = randomUUID()
|
||||
const variant = "original"
|
||||
const fileRole = "original"
|
||||
|
||||
try {
|
||||
const stored = await storeUpload({
|
||||
file: fileEntry,
|
||||
mediaType: type,
|
||||
assetId: mediaAssetId,
|
||||
variant,
|
||||
fileRole,
|
||||
})
|
||||
|
||||
const created = await createMediaAsset({
|
||||
id: mediaAssetId,
|
||||
title,
|
||||
type,
|
||||
description: parseOptionalField(formData, "description"),
|
||||
@@ -171,6 +189,7 @@ export async function POST(request: Request): Promise<Response> {
|
||||
{
|
||||
id: created.id,
|
||||
provider: stored.provider,
|
||||
warning: stored.fallbackReason,
|
||||
notice: "Media uploaded successfully.",
|
||||
},
|
||||
{ status: 201 },
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getMediaFoundationSummary, listMediaAssets } from "@cms/db"
|
||||
|
||||
import { AdminShell } from "@/components/admin-shell"
|
||||
import { FlashQueryCleanup } from "@/components/media/flash-query-cleanup"
|
||||
import { MediaUploadForm } from "@/components/media/media-upload-form"
|
||||
import { resolveMediaStorageProvider } from "@/lib/media/storage"
|
||||
import { requirePermissionForRoute } from "@/lib/route-guards"
|
||||
@@ -34,7 +35,10 @@ export default async function MediaManagementPage({
|
||||
])
|
||||
const notice = readFirstValue(resolvedSearchParams.notice)
|
||||
const error = readFirstValue(resolvedSearchParams.error)
|
||||
const uploadedVia = readFirstValue(resolvedSearchParams.uploadedVia)
|
||||
const warning = readFirstValue(resolvedSearchParams.warning)
|
||||
const activeStorageProvider = resolveMediaStorageProvider(process.env.CMS_MEDIA_STORAGE_PROVIDER)
|
||||
const hasFlashQuery = Boolean(notice || error || warning || uploadedVia)
|
||||
|
||||
return (
|
||||
<AdminShell
|
||||
@@ -44,9 +48,18 @@ export default async function MediaManagementPage({
|
||||
title="Media"
|
||||
description="Media foundation baseline for assets, artwork renditions, and grouping metadata."
|
||||
>
|
||||
<FlashQueryCleanup enabled={hasFlashQuery} />
|
||||
|
||||
{notice ? (
|
||||
<section className="rounded-xl border border-emerald-300 bg-emerald-50 px-4 py-3 text-sm text-emerald-800">
|
||||
{notice}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span>{notice}</span>
|
||||
{uploadedVia ? (
|
||||
<span className="rounded border border-emerald-300 bg-white px-2 py-0.5 text-xs font-medium uppercase tracking-wide text-emerald-700">
|
||||
Stored via: {uploadedVia}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
@@ -56,6 +69,12 @@ export default async function MediaManagementPage({
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{warning ? (
|
||||
<section className="rounded-xl border border-amber-300 bg-amber-50 px-4 py-3 text-sm text-amber-900">
|
||||
{warning}
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<section className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
||||
<article className="rounded-xl border border-neutral-200 p-4">
|
||||
<p className="text-xs uppercase tracking-[0.2em] text-neutral-500">Media Assets</p>
|
||||
|
||||
Reference in New Issue
Block a user