82 lines
2.9 KiB
TypeScript
82 lines
2.9 KiB
TypeScript
import type { Database } from 'better-sqlite3'
|
|
import { access, mkdir, rename } from 'node:fs/promises'
|
|
import { dirname, resolve, sep } from 'node:path'
|
|
|
|
export interface MediaAssetRow {
|
|
id: number
|
|
asset_key: string
|
|
original_name: string
|
|
mime_type: string
|
|
extension: string
|
|
bytes: number
|
|
width: number
|
|
height: number
|
|
original_path: string
|
|
derived_path: string
|
|
thumbnail_path: string
|
|
alt_text: string
|
|
source_text: string
|
|
copyright_text: string
|
|
authorization_status: 'pending' | 'authorized' | 'restricted'
|
|
status: 'active' | 'trash' | 'deleted'
|
|
created_at: string
|
|
updated_at: string
|
|
deleted_at: string | null
|
|
}
|
|
|
|
export const mediaUrl = (relativePath: string) => `/api/media/${relativePath}`
|
|
|
|
export function serializeMedia(row: MediaAssetRow, referenced = false) {
|
|
return {
|
|
id: row.id,
|
|
key: row.asset_key,
|
|
originalName: row.original_name,
|
|
mimeType: row.mime_type,
|
|
bytes: row.bytes,
|
|
width: row.width,
|
|
height: row.height,
|
|
url: mediaUrl(row.derived_path),
|
|
thumbnailUrl: mediaUrl(row.thumbnail_path),
|
|
altText: row.alt_text,
|
|
source: row.source_text,
|
|
copyright: row.copyright_text,
|
|
authorizationStatus: row.authorization_status,
|
|
status: row.status,
|
|
referenced,
|
|
createdAt: row.created_at,
|
|
deletedAt: row.deleted_at,
|
|
}
|
|
}
|
|
|
|
export function isMediaReferenced(db: Database, row: MediaAssetRow) {
|
|
const document = db.prepare('SELECT draft_json, published_json FROM site_documents WHERE document_key = ?').get('website') as { draft_json: string; published_json: string }
|
|
const needles = [mediaUrl(row.original_path), mediaUrl(row.derived_path), mediaUrl(row.thumbnail_path)]
|
|
return needles.some(path => document.draft_json.includes(path) || document.published_json.includes(path))
|
|
}
|
|
|
|
export function uploadsRoot() { return resolve(process.cwd(), 'data', 'uploads') }
|
|
|
|
export function safeUploadPath(relativePath: string) {
|
|
const root = uploadsRoot()
|
|
const fullPath = resolve(root, relativePath)
|
|
if (!fullPath.startsWith(`${root}${sep}`)) throw createError({ statusCode: 400, statusMessage: '无效的文件路径' })
|
|
return fullPath
|
|
}
|
|
|
|
export async function moveMediaFiles(row: MediaAssetRow, target: 'trash' | 'restore') {
|
|
const paths = [row.original_path, row.derived_path, row.thumbnail_path]
|
|
for (const current of paths) {
|
|
const sourceRelative = target === 'trash' ? current : current.replace(/^trash\//, '')
|
|
const destinationRelative = target === 'trash' ? `trash/${current}` : current.replace(/^trash\//, '')
|
|
const source = safeUploadPath(target === 'trash' ? sourceRelative : `trash/${sourceRelative}`)
|
|
const destination = safeUploadPath(destinationRelative)
|
|
try {
|
|
await access(source)
|
|
await mkdir(dirname(destination), { recursive: true })
|
|
await rename(source, destination)
|
|
} catch (error: any) {
|
|
if (error?.code !== 'ENOENT') throw error
|
|
}
|
|
}
|
|
}
|