Files
easyimg/server/utils/upload.js
T

234 lines
6.1 KiB
JavaScript

import { existsSync, mkdirSync, unlinkSync } from 'fs'
import { writeFile } from 'fs/promises'
import { join } from 'path'
import { v4 as uuidv4 } from 'uuid'
import { processImage, getImageMetadata } from './image.js'
import db from './db.js'
/**
* 根据配置处理图片格式转换/压缩
* 压缩与格式转换相互独立:
* - enableCompression:仅控制是否压缩(按质量压缩),不影响格式转换
* - convertToWebp / convertToPng / convertToJpg:格式转换独立开关(三选一,webp > jpg > png 优先级)
* @param {Buffer} buffer - 原始图片数据
* @param {string} fileExt - 原始文件扩展名
* @param {Object} config - 配置对象
* @param {boolean} config.enableCompression - 是否开启压缩
* @param {number} config.compressionQuality - 压缩质量 (0-100)
* @param {boolean} config.convertToWebp - 是否转换为 WebP
* @param {boolean} config.convertToPng - 是否转换为 PNG
* @param {boolean} config.convertToJpg - 是否转换为 JPG
* @param {number} [minSizeKb=0] - 触发压缩的最小文件大小(KB),0 表示不限
* @returns {Promise<{processedBuffer: Buffer, finalFormat: string, isWebp: boolean}>}
*/
export async function processImageWithConfig(buffer, fileExt, config, minSizeKb = 0) {
let processedBuffer = buffer
let finalFormat = fileExt
let isWebp = false
const isGif = fileExt === 'gif'
const fileSizeInKb = buffer.length / 1024
// 确定目标格式(格式转换独立于压缩)
let targetFormat = fileExt
if (!isGif) {
if (config.convertToWebp) {
targetFormat = 'webp'
isWebp = true
} else if (config.convertToJpg) {
targetFormat = 'jpg'
} else if (config.convertToPng) {
targetFormat = 'png'
}
}
finalFormat = targetFormat
// 判断是否需要格式转换
const needConvert = !isGif && targetFormat !== fileExt
// 判断是否需要压缩(低于 100KB 的图片不执行压缩,格式转换不受此限制)
const compressMinSizeKb = minSizeKb > 0 ? minSizeKb : 100
const needCompress = config.enableCompression
&& !isGif
&& fileSizeInKb > compressMinSizeKb
if (needConvert || needCompress) {
const processOptions = {
format: targetFormat === 'jpg' ? 'jpeg' : targetFormat,
quality: needCompress ? (config.compressionQuality || 80) : 100
}
processedBuffer = await processImage(buffer, processOptions)
}
return { processedBuffer, finalFormat, isWebp }
}
// 上传目录:生产环境使用 /app/uploads,开发环境使用项目根目录下的 uploads
const uploadsDir = process.env.NODE_ENV === 'production'
? '/app/uploads'
: join(process.cwd(), 'uploads')
// 确保上传目录存在
if (!existsSync(uploadsDir)) {
mkdirSync(uploadsDir, { recursive: true })
}
console.log('[Upload] 上传目录:', uploadsDir)
/**
* 解析 multipart/form-data 请求
*/
export async function parseFormData(event) {
const formData = await readMultipartFormData(event)
if (!formData || formData.length === 0) {
return { file: null }
}
// 查找文件字段
const fileField = formData.find(field => field.name === 'file' || field.name === 'image')
if (!fileField || !fileField.data) {
return { file: null }
}
return {
file: {
buffer: fileField.data,
originalFilename: fileField.filename || 'unknown',
mimetype: fileField.type,
size: fileField.data.length
}
}
}
/**
* 获取文件扩展名(内部使用)
*/
function getFileExtension(filename) {
const ext = filename.split('.').pop().toLowerCase()
return ext
}
/**
* 验证文件格式
*/
export function validateFormat(filename, allowedFormats) {
const ext = getFileExtension(filename)
return allowedFormats.map(f => f.toLowerCase()).includes(ext)
}
/**
* 验证文件大小
*/
export function validateSize(size, maxSize) {
return size <= maxSize
}
/**
* 保存图片文件
* @param {Buffer} buffer - 图片数据
* @param {Object} options - 配置选项
* @param {string} options.originalName - 原始文件名
* @param {boolean} options.convertToWebp - 是否转换为 WebP
* @param {number} options.webpQuality - WebP 质量
* @param {string} options.uploadedBy - 上传者
* @param {string} options.ip - 上传者 IP
* @param {boolean} options.isPublic - 是否为公共上传
*/
export async function saveUploadedImage(buffer, options) {
const {
originalName,
convertToWebp = false,
webpQuality = 80,
uploadedBy = '访客',
ip = '',
isPublic = true
} = options
const uuid = uuidv4()
const originalExt = getFileExtension(originalName)
let finalBuffer = buffer
let finalExt = originalExt
let isWebp = false
// 如果需要转换为 WebP
if (convertToWebp && originalExt !== 'gif') {
finalBuffer = await processImage(buffer, {
format: 'webp',
quality: webpQuality
})
finalExt = 'webp'
isWebp = true
}
// 获取图片信息
const imageInfo = await getImageMetadata(finalBuffer)
// 生成文件名和路径
const filename = `${uuid}.${finalExt}`
const filepath = join(uploadsDir, filename)
// 保存文件
await writeFile(filepath, finalBuffer)
// 保存到数据库
const imageRecord = {
_id: uuidv4(),
uuid,
originalName,
filename,
size: finalBuffer.length,
format: finalExt,
width: imageInfo.width,
height: imageInfo.height,
isWebp,
isPublic,
uploadedBy,
ip,
isDeleted: false,
uploadedAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
}
await db.images.insert(imageRecord)
// 返回图片信息(不包含敏感信息)
return {
uuid,
filename,
format: finalExt,
size: finalBuffer.length,
width: imageInfo.width,
height: imageInfo.height,
url: `/i/${uuid}.${finalExt}`
}
}
/**
* 删除图片文件
*/
export async function deleteImageFile(filename) {
const filepath = join(uploadsDir, filename)
if (existsSync(filepath)) {
unlinkSync(filepath)
return true
}
return false
}
/**
* 获取图片文件路径
*/
export function getImagePath(filename) {
return join(uploadsDir, filename)
}
/**
* 获取上传目录路径
*/
export function getUploadsDirPath() {
return uploadsDir
}