import express, { type Request, type Response } from 'express' import fs from 'fs/promises' import path from 'path' import { asyncHandler } from '../../utils/asyncHandler.js' import { successResponse } from '../../utils/response.js' import { resolveNotebookPath } from '../../utils/pathSafety.js' import { getUniqueFilename, mimeToExt, validateImageBuffer, detectImageMimeType } from '../../utils/file.js' import { NOTEBOOK_ROOT } from '../../config/paths.js' import { toPosixPath } from '../../../shared/utils/path.js' import { pad2, formatTimestamp } from '../../../shared/utils/date.js' import { ValidationError, UnsupportedMediaTypeError } from '../../../shared/errors/index.js' const router = express.Router() const parseImageDataUrl = (dataUrl: string): { mimeType: string; base64Data: string } | null => { const match = dataUrl.match(/^data:(image\/[a-zA-Z0-9.+-]+);base64,([A-Za-z0-9+/=\s]+)$/) if (!match) return null const [, mimeType, base64Data] = match return { mimeType, base64Data: base64Data.replace(/\s/g, '') } } router.post( '/image', asyncHandler(async (req: Request, res: Response) => { const { image } = req.body as { image?: string } if (!image) throw new ValidationError('需要图片数据') const parsed = parseImageDataUrl(image) if (!parsed) { throw new ValidationError('无效的图片数据URL') } const ext = mimeToExt[parsed.mimeType] if (!ext) { throw new UnsupportedMediaTypeError('不支持的图片类型') } const buffer = Buffer.from(parsed.base64Data, 'base64') validateImageBuffer(buffer, parsed.mimeType) const detectedMimeType = detectImageMimeType(buffer) if (!detectedMimeType || detectedMimeType !== parsed.mimeType) { throw new ValidationError('图片内容类型不匹配或图片已损坏') } const now = new Date() const year = now.getFullYear() const month = pad2(now.getMonth() + 1) const day = pad2(now.getDate()) const imagesSubDir = `images/${year}/${month}/${day}` const { fullPath: imagesDirFullPath } = resolveNotebookPath(imagesSubDir) await fs.mkdir(imagesDirFullPath, { recursive: true }) const baseName = formatTimestamp(now) const filename = await getUniqueFilename(imagesDirFullPath, baseName, ext) const relPath = `${imagesSubDir}/${filename}` const { fullPath } = resolveNotebookPath(relPath) await fs.writeFile(fullPath, buffer) successResponse(res, { name: toPosixPath(relPath), path: toPosixPath(relPath) }) }), ) router.post( '/wallpaper', asyncHandler(async (req: Request, res: Response) => { const { image } = req.body as { image?: string } if (!image) throw new ValidationError('需要图片数据') const parsed = parseImageDataUrl(image) if (!parsed) { throw new ValidationError('无效的图片数据URL') } const allowedWallpaperTypes = ['image/png', 'image/jpeg', 'image/webp'] if (!allowedWallpaperTypes.includes(parsed.mimeType)) { throw new UnsupportedMediaTypeError('壁纸只支持PNG、JPEG和WebP格式') } const buffer = Buffer.from(parsed.base64Data, 'base64') validateImageBuffer(buffer, parsed.mimeType) const detectedMimeType = detectImageMimeType(buffer) if (!detectedMimeType || detectedMimeType !== parsed.mimeType) { throw new ValidationError('图片内容类型不匹配或图片已损坏') } const configDir = path.join(NOTEBOOK_ROOT, '.config') const backgroundPath = path.join(configDir, 'background.png') await fs.mkdir(configDir, { recursive: true }) await fs.writeFile(backgroundPath, buffer) successResponse(res, { message: '壁纸已更新' }) }), ) export default router