feat(media): add admin media CRUD preview and storage cleanup
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { mkdir, writeFile } from "node:fs/promises"
|
||||
import { mkdir, rm, writeFile } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
|
||||
import { buildMediaStorageKey } from "@/lib/media/storage-key"
|
||||
@@ -15,7 +15,7 @@ type StoredUpload = {
|
||||
storageKey: string
|
||||
}
|
||||
|
||||
function resolveBaseDirectory(): string {
|
||||
export function resolveLocalMediaBaseDirectory(): string {
|
||||
const configured = process.env.CMS_MEDIA_LOCAL_STORAGE_DIR?.trim()
|
||||
|
||||
if (configured) {
|
||||
@@ -33,7 +33,7 @@ export async function storeUploadLocally(params: StoreLocalUploadParams): Promis
|
||||
variant: params.variant,
|
||||
fileName: params.file.name,
|
||||
})
|
||||
const baseDirectory = resolveBaseDirectory()
|
||||
const baseDirectory = resolveLocalMediaBaseDirectory()
|
||||
const outputPath = path.join(baseDirectory, storageKey)
|
||||
|
||||
await mkdir(path.dirname(outputPath), { recursive: true })
|
||||
@@ -43,3 +43,24 @@ export async function storeUploadLocally(params: StoreLocalUploadParams): Promis
|
||||
|
||||
return { storageKey }
|
||||
}
|
||||
|
||||
export async function deleteLocalStorageObject(storageKey: string): Promise<boolean> {
|
||||
const baseDirectory = resolveLocalMediaBaseDirectory()
|
||||
const outputPath = path.join(baseDirectory, storageKey)
|
||||
|
||||
try {
|
||||
await rm(outputPath)
|
||||
return true
|
||||
} catch (error) {
|
||||
const code =
|
||||
typeof error === "object" && error !== null && "code" in error
|
||||
? String((error as { code?: unknown }).code)
|
||||
: ""
|
||||
|
||||
if (code === "ENOENT") {
|
||||
return false
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3"
|
||||
import { DeleteObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3"
|
||||
|
||||
import { buildMediaStorageKey } from "@/lib/media/storage-key"
|
||||
|
||||
@@ -27,7 +27,7 @@ function parseBoolean(value: string | undefined): boolean {
|
||||
return value?.toLowerCase() === "true"
|
||||
}
|
||||
|
||||
function resolveS3Config(): S3Config {
|
||||
export function resolveS3Config(): S3Config {
|
||||
const bucket = process.env.CMS_MEDIA_S3_BUCKET?.trim()
|
||||
const region = process.env.CMS_MEDIA_S3_REGION?.trim()
|
||||
const accessKeyId = process.env.CMS_MEDIA_S3_ACCESS_KEY_ID?.trim()
|
||||
@@ -50,7 +50,7 @@ function resolveS3Config(): S3Config {
|
||||
}
|
||||
}
|
||||
|
||||
function createS3Client(config: S3Config): S3Client {
|
||||
export function createS3Client(config: S3Config): S3Client {
|
||||
return new S3Client({
|
||||
region: config.region,
|
||||
endpoint: config.endpoint,
|
||||
@@ -87,3 +87,17 @@ export async function storeUploadToS3(params: StoreS3UploadParams): Promise<Stor
|
||||
|
||||
return { storageKey }
|
||||
}
|
||||
|
||||
export async function deleteS3Object(storageKey: string): Promise<boolean> {
|
||||
const config = resolveS3Config()
|
||||
const client = createS3Client(config)
|
||||
|
||||
await client.send(
|
||||
new DeleteObjectCommand({
|
||||
Bucket: config.bucket,
|
||||
Key: storageKey,
|
||||
}),
|
||||
)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { storeUploadLocally } from "@/lib/media/local-storage"
|
||||
import { storeUploadToS3 } from "@/lib/media/s3-storage"
|
||||
import { deleteLocalStorageObject, storeUploadLocally } from "@/lib/media/local-storage"
|
||||
import { deleteS3Object, storeUploadToS3 } from "@/lib/media/s3-storage"
|
||||
|
||||
export type MediaStorageProvider = "local" | "s3"
|
||||
|
||||
@@ -121,3 +121,29 @@ export async function storeUpload(params: StoreUploadParams): Promise<StoredUplo
|
||||
provider,
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteStoredMediaObject(storageKey: string): Promise<void> {
|
||||
const preferred = resolveMediaStorageProvider(process.env.CMS_MEDIA_STORAGE_PROVIDER)
|
||||
const deleteOperations =
|
||||
preferred === "s3"
|
||||
? [() => deleteS3Object(storageKey), () => deleteLocalStorageObject(storageKey)]
|
||||
: [() => deleteLocalStorageObject(storageKey), () => deleteS3Object(storageKey)]
|
||||
const errors: string[] = []
|
||||
|
||||
for (const performDelete of deleteOperations) {
|
||||
try {
|
||||
const deleted = await performDelete()
|
||||
|
||||
if (deleted) {
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
const detail = describeS3Error(error)
|
||||
errors.push(detail)
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(`Storage object deletion failed for key "${storageKey}": ${errors.join(" | ")}`)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user