Files
autoAiWorkSys/api/services/oss_tool_service.js
张成 4361478ebe 1
2025-12-18 10:17:51 +08:00

336 lines
9.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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