const OSS = require('ali-oss') const fs = require('fs') const config = require('../../config/config.js'); const uuid = require('node-uuid') const logs = require('../middleware/logProxy') /** * OSS 文件上传服务 * 统一管理文件上传、存储路径、文件类型等 */ class OSSToolService { constructor() { const { accessKeyId, accessKeySecret, bucket, region, timeout } = config.oos; // 设置超时时间:默认30分钟(1800000ms),适用于大文件上传 // 可以从配置文件读取,如果没有配置则使用默认值 this.client = new OSS({ region, accessKeyId, accessKeySecret, bucket, timeout: timeout || 30 * 60 * 1000, // 30分钟超时(1800000ms) }) // 基础存储路径前缀 this.basePrefix = 'front/work' // 文件类型映射 this.fileTypeMap = { // 图片类型 'image/jpeg': 'jpg', 'image/jpg': 'jpg', 'image/png': 'png', 'image/gif': 'gif', 'image/webp': 'webp', 'image/svg+xml': 'svg', // 视频类型 'video/mp4': 'mp4', 'video/avi': 'avi', 'video/mov': 'mov', 'video/wmv': 'wmv', 'video/flv': 'flv', 'video/webm': 'webm', 'video/mkv': 'mkv', // 音频类型 'audio/mp3': 'mp3', 'audio/wav': 'wav', 'audio/aac': 'aac', 'audio/ogg': 'ogg', 'audio/flac': 'flac', // 文档类型 'application/pdf': 'pdf', 'application/msword': 'doc', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx', 'application/vnd.ms-excel': 'xls', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'xlsx', 'application/vnd.ms-powerpoint': 'ppt', 'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'pptx', 'text/plain': 'txt', 'text/html': 'html', 'text/css': 'css', 'application/javascript': 'js', 'application/json': 'json' } } /** * 获取文件后缀名 * @param {Object} file - 文件对象(兼容 formidable 格式) * @returns {string} 文件后缀名 */ getFileSuffix(file) { // 优先使用 MIME 类型判断(兼容 type 和 mimetype) const mimeType = file.mimetype || file.type if (mimeType && this.fileTypeMap[mimeType]) { return this.fileTypeMap[mimeType] } // 备用方案:从文件名获取(兼容 originalFilename 和 name) const fileName = file.originalFilename || file.name if (fileName) { const lastIndex = fileName.lastIndexOf('.') if (lastIndex > -1) { return fileName.substring(lastIndex + 1).toLowerCase() } } return 'bin' } /** * 获取文件存储路径 * @param {Object} file - 文件对象(兼容 formidable 格式) * @param {string} category - 存储分类 * @returns {string} 完整的存储路径 */ getStoragePath(file, category = 'files') { const suffix = this.getFileSuffix(file) const uid = uuid.v4() // 根据文件类型确定子路径(兼容 mimetype 和 type) let subPath = category const mimeType = file.mimetype || file.type if (mimeType) { if (mimeType.startsWith('image/')) { subPath = 'images' } else if (mimeType.startsWith('video/')) { subPath = 'videos' } else if (mimeType.startsWith('audio/')) { subPath = 'audios' } else if (mimeType.startsWith('application/') || mimeType.startsWith('text/')) { subPath = 'documents' } } // 完整路径:front/ball/{subPath}/{uid}.{suffix} return `${this.basePrefix}/${subPath}/${uid}.${suffix}` } /** * 核心文件上传方法 * @param {Object} file - 文件对象(兼容 formidable 格式) * @param {string} category - 存储分类 * @returns {Object} 上传结果 */ async uploadFile(file, category = 'files') { try { // 兼容不同的文件对象格式(filepath 或 path) const filePath = file.filepath || file.path // 验证文件 if (!file || !filePath) { return { success: false, error: '无效的文件对象' } } const stream = fs.createReadStream(filePath) const storagePath = this.getStoragePath(file, category) const suffix = this.getFileSuffix(file) // 设置 content-type(兼容 mimetype 和 type) const contentType = file.mimetype || file.type || 'application/octet-stream' // 上传到 OSS const result = await this.client.put(storagePath, stream, { headers: { 'content-disposition': 'inline', "content-type": contentType } }) if (result.res.status === 200) { const ossPath = config.ossUrl + '/' + result.name // 上传成功后删除临时文件 try { if (fs.existsSync(filePath)) { fs.unlinkSync(filePath) } } catch (unlinkError) { logs.error('删除临时文件失败:', unlinkError) } return { success: true, name: result.name, path: result.url, ossPath, fileType: file.mimetype || file.type, fileSize: file.size, originalName: file.originalFilename || file.name, suffix: suffix, storagePath: storagePath } } else { return { success: false, error: 'OSS 上传失败' } } } catch (error) { logs.error('文件上传错误:', error) // 上传失败也要清理临时文件 try { const filePath = file.filepath || file.path if (filePath && fs.existsSync(filePath)) { fs.unlinkSync(filePath) } } catch (unlinkError) { logs.error('删除临时文件失败:', unlinkError) } return { success: false, error: error.message } } } /** * 上传流数据 * @param {Stream} stream - 文件流 * @param {string} contentType - 内容类型 * @param {string} suffix - 文件后缀 * @returns {Object} 上传结果 */ async uploadStream(stream, contentType, suffix) { try { const uid = uuid.v4() const storagePath = `${this.basePrefix}/files/${uid}.${suffix}` const result = await this.client.put(storagePath, stream, { headers: { 'content-disposition': 'inline', "content-type": contentType } }) if (result.res.status === 200) { const ossPath = config.ossUrl + result.name return { success: true, name: result.name, path: result.url, ossPath, storagePath: storagePath } } else { return { success: false, error: 'OSS 上传失败' } } } catch (error) { logs.error('流上传错误:', error) return { success: false, error: error.message } } } /** * 删除文件 * @param {string} filePath - 文件路径 * @returns {Object} 删除结果 */ async deleteFile(filePath) { try { if (!filePath) { return { success: false, error: '文件路径不能为空' } } // 从完整 URL 中提取相对路径 const relativePath = filePath.replace(config.ossUrl + '/', '') const result = await this.client.delete(relativePath) if (result.res.status === 204) { return { success: true, message: '文件删除成功' } } else { return { success: false, error: '文件删除失败' } } } catch (error) { logs.error('文件删除错误:', error) return { success: false, error: error.message } } } /** * 获取文件信息 * @param {string} filePath - 文件路径 * @returns {Object} 文件信息 */ async getFileInfo(filePath) { try { if (!filePath) { return { success: false, error: '文件路径不能为空' } } const relativePath = filePath.replace(config.ossUrl + '/', '') const result = await this.client.head(relativePath) return { success: true, size: result.res.headers['content-length'], type: result.res.headers['content-type'], lastModified: result.res.headers['last-modified'], etag: result.res.headers['etag'] } } catch (error) { logs.error('获取文件信息错误:', error) return { success: false, error: error.message } } } // ==================== 便捷方法 ==================== /** ) * @param {Object} file - 图片文件 * @returns {Object} 上传结果 */ async putImg(file) { return await this.uploadFile(file, 'images') } /** * 上传视频文件 * @param {Object} file - 视频文件 * @returns {Object} 上传结果 */ async uploadVideo(file) { return await this.uploadFile(file, 'videos') } /** * 上传音频文件 * @param {Object} file - 音频文件 * @returns {Object} 上传结果 */ async uploadAudio(file) { return await this.uploadFile(file, 'audios') } /** * 上传文档文件 * @param {Object} file - 文档文件 * @returns {Object} 上传结果 */ async uploadDocument(file) { return await this.uploadFile(file, 'documents') } } // 创建单例实例 const ossToolService = new OSSToolService() // 导出实例(保持向后兼容) module.exports = ossToolService