101 lines
3.6 KiB
TypeScript
101 lines
3.6 KiB
TypeScript
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
|