Consolidate local images under data/images/, add docker migrate CLI, startup index sync with orphan cleanup, and remove upload folder UI/API.
103 lines
2.8 KiB
TypeScript
103 lines
2.8 KiB
TypeScript
import { readMultipartFormData } from 'h3'
|
||
import { verifyUploadTokenValue } from '../../utils/access'
|
||
import { checkUploadRateLimit } from '../../utils/rate-limit'
|
||
import {
|
||
detectMimeFromSignature,
|
||
isAllowedMimeType
|
||
} from '../../utils/file-signature'
|
||
import {
|
||
mimeMatchesDeclared,
|
||
processSingleImageUpload
|
||
} from '../../utils/process-image-upload'
|
||
import { getApiUploadToken } from '../../utils/env'
|
||
|
||
/** EasyImages 2.0 兼容上传(Twikoo IMAGE_CDN=easyimage) */
|
||
export default defineEventHandler(async (event) => {
|
||
setHeader(event, 'Content-Type', 'application/json; charset=utf-8')
|
||
|
||
const config = useRuntimeConfig(event)
|
||
const devBypass = import.meta.dev && config.devBypassAccess
|
||
|
||
const formData = await readMultipartFormData(event)
|
||
if (!formData?.length) {
|
||
return easyImageError(400, '未收到上传数据')
|
||
}
|
||
|
||
const tokenField = formData.find(part => part.name === 'token')
|
||
const token = tokenField?.data?.length
|
||
? new TextDecoder().decode(tokenField.data).trim()
|
||
: ''
|
||
|
||
if (!devBypass) {
|
||
if (!getApiUploadToken(event)) {
|
||
return easyImageError(500, '服务端未配置 API_UPLOAD_TOKEN')
|
||
}
|
||
if (!verifyUploadTokenValue(event, token)) {
|
||
return easyImageError(401, 'token 错误或无效')
|
||
}
|
||
checkUploadRateLimit(event, token)
|
||
}
|
||
|
||
const imagePart = formData.find(
|
||
part => part.name === 'image' && part.data?.length
|
||
)
|
||
if (!imagePart?.data?.length) {
|
||
return easyImageError(400, '未收到图片文件')
|
||
}
|
||
|
||
const bytes = new Uint8Array(imagePart.data)
|
||
const declaredMime = imagePart.type?.toLowerCase() ?? ''
|
||
const detectedMime = detectMimeFromSignature(bytes)
|
||
|
||
if (detectedMime && !mimeMatchesDeclared(declaredMime, detectedMime)) {
|
||
return easyImageError(415, '文件类型与内容不匹配')
|
||
}
|
||
if (declaredMime && isAllowedMimeType(declaredMime) && !detectedMime) {
|
||
return easyImageError(415, '无法识别的图片格式')
|
||
}
|
||
|
||
// Twikoo 走 token 上传,记入 activity log source=api
|
||
const result = await processSingleImageUpload(event, {
|
||
bytes,
|
||
filename: imagePart.filename,
|
||
source: 'api'
|
||
})
|
||
|
||
if ('error' in result) {
|
||
const code = easyImageCodeFromError(result.error.error.code)
|
||
return easyImageError(code, result.error.error.message)
|
||
}
|
||
|
||
const { item } = result
|
||
return {
|
||
result: 'success',
|
||
code: 200,
|
||
url: item.url,
|
||
srcName: item.originalName,
|
||
thumb: item.url,
|
||
del: ''
|
||
}
|
||
})
|
||
|
||
function easyImageError(code: number, message: string) {
|
||
return {
|
||
result: 'failed',
|
||
code,
|
||
message
|
||
}
|
||
}
|
||
|
||
function easyImageCodeFromError(code: string): number {
|
||
switch (code) {
|
||
case 'FILE_TOO_LARGE':
|
||
return 413
|
||
case 'UNSUPPORTED_FILE_TYPE':
|
||
case 'INVALID_FILE_SIGNATURE':
|
||
return 415
|
||
case 'UPLOAD_FAILED':
|
||
return 500
|
||
default:
|
||
return 400
|
||
}
|
||
}
|