67 lines
1.8 KiB
TypeScript
67 lines
1.8 KiB
TypeScript
import { randomUUID } from "node:crypto"
|
|
import { mkdir, writeFile } from "node:fs/promises"
|
|
import path from "node:path"
|
|
|
|
type StoreUploadParams = {
|
|
file: File
|
|
mediaType: string
|
|
}
|
|
|
|
type StoredUpload = {
|
|
storageKey: string
|
|
}
|
|
|
|
const FALLBACK_EXTENSION = "bin"
|
|
|
|
function resolveBaseDirectory(): string {
|
|
const configured = process.env.CMS_MEDIA_LOCAL_STORAGE_DIR?.trim()
|
|
|
|
if (configured) {
|
|
return path.resolve(configured)
|
|
}
|
|
|
|
return path.resolve(process.cwd(), ".data", "media")
|
|
}
|
|
|
|
function normalizeSegment(value: string): string {
|
|
return value
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9._-]+/g, "-")
|
|
.replace(/^-+|-+$/g, "")
|
|
}
|
|
|
|
function extensionFromFilename(fileName: string): string {
|
|
const extension = path.extname(fileName).slice(1)
|
|
|
|
if (!extension) {
|
|
return FALLBACK_EXTENSION
|
|
}
|
|
|
|
const normalized = normalizeSegment(extension)
|
|
|
|
return normalized.length > 0 ? normalized : FALLBACK_EXTENSION
|
|
}
|
|
|
|
function buildStorageKey(mediaType: string, fileName: string): string {
|
|
const now = new Date()
|
|
const year = String(now.getUTCFullYear())
|
|
const month = String(now.getUTCMonth() + 1).padStart(2, "0")
|
|
const normalizedType = normalizeSegment(mediaType) || "generic"
|
|
const extension = extensionFromFilename(fileName)
|
|
|
|
return [normalizedType, year, month, `${randomUUID()}.${extension}`].join("/")
|
|
}
|
|
|
|
export async function storeUploadLocally(params: StoreUploadParams): Promise<StoredUpload> {
|
|
const storageKey = buildStorageKey(params.mediaType, params.file.name)
|
|
const baseDirectory = resolveBaseDirectory()
|
|
const outputPath = path.join(baseDirectory, storageKey)
|
|
|
|
await mkdir(path.dirname(outputPath), { recursive: true })
|
|
|
|
const bytes = new Uint8Array(await params.file.arrayBuffer())
|
|
await writeFile(outputPath, bytes)
|
|
|
|
return { storageKey }
|
|
}
|