feat(media): add admin media CRUD preview and storage cleanup
This commit is contained in:
120
apps/admin/src/app/api/media/file/[id]/route.ts
Normal file
120
apps/admin/src/app/api/media/file/[id]/route.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { readFile } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { GetObjectCommand } from "@aws-sdk/client-s3"
|
||||
import { hasPermission } from "@cms/content/rbac"
|
||||
import { getMediaAssetById } from "@cms/db"
|
||||
|
||||
import { auth, resolveRoleFromAuthSession } from "@/lib/auth/server"
|
||||
import { resolveLocalMediaBaseDirectory } from "@/lib/media/local-storage"
|
||||
import { createS3Client, resolveS3Config } from "@/lib/media/s3-storage"
|
||||
import { resolveMediaStorageProvider } from "@/lib/media/storage"
|
||||
|
||||
export const runtime = "nodejs"
|
||||
|
||||
type RouteContext = {
|
||||
params: Promise<{ id: string }>
|
||||
}
|
||||
|
||||
async function readFromLocalStorage(storageKey: string): Promise<Uint8Array> {
|
||||
const baseDirectory = resolveLocalMediaBaseDirectory()
|
||||
const outputPath = path.join(baseDirectory, storageKey)
|
||||
|
||||
return readFile(outputPath)
|
||||
}
|
||||
|
||||
async function readFromS3Storage(storageKey: string): Promise<Uint8Array> {
|
||||
const config = resolveS3Config()
|
||||
const client = createS3Client(config)
|
||||
|
||||
const response = await client.send(
|
||||
new GetObjectCommand({
|
||||
Bucket: config.bucket,
|
||||
Key: storageKey,
|
||||
}),
|
||||
)
|
||||
|
||||
if (!response.Body) {
|
||||
throw new Error("S3 object body is empty")
|
||||
}
|
||||
|
||||
return response.Body.transformToByteArray()
|
||||
}
|
||||
|
||||
function toBody(data: Uint8Array): BodyInit {
|
||||
const bytes = new Uint8Array(data.byteLength)
|
||||
bytes.set(data)
|
||||
return bytes
|
||||
}
|
||||
|
||||
export async function GET(request: Request, context: RouteContext): Promise<Response> {
|
||||
const session = await auth.api
|
||||
.getSession({
|
||||
headers: request.headers,
|
||||
})
|
||||
.catch(() => null)
|
||||
const role = resolveRoleFromAuthSession(session)
|
||||
|
||||
if (!role) {
|
||||
return Response.json(
|
||||
{
|
||||
message: "Unauthorized",
|
||||
},
|
||||
{ status: 401 },
|
||||
)
|
||||
}
|
||||
|
||||
if (!hasPermission(role, "media:read", "team")) {
|
||||
return Response.json(
|
||||
{
|
||||
message: "Missing permission: media:read",
|
||||
},
|
||||
{ status: 403 },
|
||||
)
|
||||
}
|
||||
|
||||
const { id } = await context.params
|
||||
const asset = await getMediaAssetById(id)
|
||||
|
||||
if (!asset || !asset.storageKey) {
|
||||
return Response.json(
|
||||
{
|
||||
message: "Media file not found",
|
||||
},
|
||||
{ status: 404 },
|
||||
)
|
||||
}
|
||||
|
||||
const preferred = resolveMediaStorageProvider(process.env.CMS_MEDIA_STORAGE_PROVIDER)
|
||||
const reads =
|
||||
preferred === "s3"
|
||||
? [
|
||||
() => readFromS3Storage(asset.storageKey as string),
|
||||
() => readFromLocalStorage(asset.storageKey as string),
|
||||
]
|
||||
: [
|
||||
() => readFromLocalStorage(asset.storageKey as string),
|
||||
() => readFromS3Storage(asset.storageKey as string),
|
||||
]
|
||||
|
||||
for (const read of reads) {
|
||||
try {
|
||||
const data = await read()
|
||||
return new Response(toBody(data), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": asset.mimeType || "application/octet-stream",
|
||||
"cache-control": "private, max-age=0, no-store",
|
||||
},
|
||||
})
|
||||
} catch {
|
||||
// Try next backend.
|
||||
}
|
||||
}
|
||||
|
||||
return Response.json(
|
||||
{
|
||||
message: "Unable to read media file from configured storage backends",
|
||||
},
|
||||
{ status: 404 },
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user