Files
XCDesktop/api/core/upload/routes.ts
2026-03-08 01:34:54 +08:00

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