feat: 初始化慧遇书院官网一期

This commit is contained in:
2026-08-12 18:01:17 +08:00
commit 5d31ee5743
101 changed files with 17259 additions and 0 deletions

81
server/utils/media.ts Normal file
View File

@@ -0,0 +1,81 @@
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
}
}
}