1
This commit is contained in:
332
api/services/oss_tool_service.js
Normal file
332
api/services/oss_tool_service.js
Normal file
@@ -0,0 +1,332 @@
|
||||
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 } = config.oos;
|
||||
this.client = new OSS({
|
||||
region,
|
||||
accessKeyId,
|
||||
accessKeySecret,
|
||||
bucket,
|
||||
})
|
||||
|
||||
|
||||
// 基础存储路径前缀
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user