const fs = require('fs'); const path = require('path'); const { createHash } = require('crypto'); const OSSClient = require('ali-oss'); const ossConfig = require('./oss-config'); // 创建OSS客户端 - ali-oss 6.23.0版本的正确配置 let client = null; // 初始化OSS客户端的函数 function initOSSClient() { try { console.log('初始化OSS客户端配置:', { region: ossConfig.region, accessKeyId: ossConfig.accessKeyId ? '已配置' : '未配置', accessKeySecret: ossConfig.accessKeySecret ? '已配置' : '未配置', bucket: ossConfig.bucket, endpoint: `https://${ossConfig.endpoint}` }); client = new OSSClient({ region: ossConfig.region, accessKeyId: ossConfig.accessKeyId, accessKeySecret: ossConfig.accessKeySecret, bucket: ossConfig.bucket, endpoint: `https://${ossConfig.endpoint}`, // 添加https协议前缀 cname: false // 对于标准OSS域名,不需要启用cname模式 }); return client; } catch (error) { console.error('初始化OSS客户端失败:', error); throw error; } } // 延迟初始化,避免应用启动时就连接OSS function getOSSClient() { if (!client) { return initOSSClient(); } return client; } class OssUploader { /** * 上传文件到OSS * @param {String} filePath - 本地文件路径 * @param {String} folder - OSS上的文件夹路径 * @param {String} fileType - 文件类型,默认为'image' * @returns {Promise} - 上传后的文件URL */ /** * 计算文件的MD5哈希值 * @param {String} filePath - 文件路径 * @returns {Promise} - MD5哈希值 */ static async getFileHash(filePath) { return new Promise((resolve, reject) => { const hash = createHash('md5'); const stream = fs.createReadStream(filePath); stream.on('error', reject); stream.on('data', chunk => hash.update(chunk)); stream.on('end', () => resolve(hash.digest('hex'))); }); } /** * 计算缓冲区的MD5哈希值 * @param {Buffer} buffer - 数据缓冲区 * @returns {String} - MD5哈希值 */ static getBufferHash(buffer) { return createHash('md5').update(buffer).digest('hex'); } static async uploadFile(filePath, folder = 'images', fileType = 'image') { try { console.log('【OSS上传】开始上传文件:', filePath, '到目录:', folder); // 确保文件存在 const fileExists = await fs.promises.access(filePath).then(() => true).catch(() => false); if (!fileExists) { throw new Error(`文件不存在: ${filePath}`); } // 获取文件扩展名 const extname = path.extname(filePath).toLowerCase(); if (!extname) { throw new Error(`无法获取文件扩展名: ${filePath}`); } // 基于文件内容计算MD5哈希值,实现文件级去重 console.log('【文件去重】开始计算文件哈希值...'); const fileHash = await this.getFileHash(filePath); console.log(`【文件去重】文件哈希计算完成: ${fileHash}`); // 使用哈希值作为文件名,确保相同内容的文件生成相同的文件名 const uniqueFilename = `${fileHash}${extname}`; const ossFilePath = `${folder}/${fileType}/${uniqueFilename}`; console.log(`【文件去重】使用基于内容的文件名: ${uniqueFilename}`); // 获取OSS客户端,延迟初始化 const ossClient = getOSSClient(); // 测试OSS连接 try { await ossClient.list({ max: 1 }); console.log('OSS连接测试成功'); } catch (connError) { console.error('OSS连接测试失败,尝试重新初始化客户端:', connError.message); // 尝试重新初始化客户端 initOSSClient(); } // 检查OSS客户端配置 console.log('【OSS上传】OSS配置检查 - region:', ossClient.options.region, 'bucket:', ossClient.options.bucket); // 上传文件,明确设置为公共读权限 console.log(`开始上传文件到OSS: ${filePath} -> ${ossFilePath}`); const result = await ossClient.put(ossFilePath, filePath, { headers: { 'x-oss-object-acl': 'public-read' // 确保文件可以公开访问 }, acl: 'public-read' // 额外设置ACL参数,确保文件公开可读 }); console.log(`文件上传成功: ${result.url}`); console.log('已设置文件为公共读权限'); // 返回完整的文件URL return result.url; } catch (error) { console.error('【OSS上传】上传文件失败:', error); console.error('【OSS上传】错误详情:', error.message); console.error('【OSS上传】错误栈:', error.stack); throw error; } } /** * 从缓冲区上传文件到OSS * @param {Buffer} buffer - 文件数据缓冲区 * @param {String} filename - 文件名 * @param {String} folder - OSS上的文件夹路径 * @param {String} fileType - 文件类型,默认为'image' * @returns {Promise} - 上传后的文件URL */ static async uploadBuffer(buffer, filename, folder = 'images', fileType = 'image') { try { // 获取文件扩展名 const extname = path.extname(filename).toLowerCase(); if (!extname) { throw new Error(`无法获取文件扩展名: ${filename}`); } // 基于文件内容计算MD5哈希值,实现文件级去重 console.log('【文件去重】开始计算缓冲区哈希值...'); const bufferHash = this.getBufferHash(buffer); console.log(`【文件去重】缓冲区哈希计算完成: ${bufferHash}`); // 使用哈希值作为文件名,确保相同内容的文件生成相同的文件名 const uniqueFilename = `${bufferHash}${extname}`; const ossFilePath = `${folder}/${fileType}/${uniqueFilename}`; console.log(`【文件去重】使用基于内容的文件名: ${uniqueFilename}`); // 获取OSS客户端,延迟初始化 const ossClient = getOSSClient(); // 上传缓冲区,明确设置为公共读权限 console.log(`开始上传缓冲区到OSS: ${ossFilePath}`); const result = await ossClient.put(ossFilePath, buffer, { headers: { 'x-oss-object-acl': 'public-read' // 确保文件可以公开访问 }, acl: 'public-read' // 额外设置ACL参数,确保文件公开可读 }); console.log(`缓冲区上传成功: ${result.url}`); console.log('已设置文件为公共读权限'); // 返回完整的文件URL return result.url; } catch (error) { console.error('OSS缓冲区上传失败:', error); console.error('OSS缓冲区上传错误详情:', error.message); console.error('OSS缓冲区上传错误栈:', error.stack); throw error; } } /** * 批量上传文件到OSS * @param {Array} filePaths - 本地文件路径数组 * @param {String} folder - OSS上的文件夹路径 * @param {String} fileType - 文件类型,默认为'image' * @returns {Promise>} - 上传后的文件URL数组 */ static async uploadFiles(filePaths, folder = 'images', fileType = 'image') { try { const uploadPromises = filePaths.map(filePath => this.uploadFile(filePath, folder, fileType) ); const urls = await Promise.all(uploadPromises); console.log(`批量上传完成,成功上传${urls.length}个文件`); return urls; } catch (error) { console.error('OSS批量上传失败:', error); throw error; } } /** * 删除OSS上的文件 * @param {String} ossFilePath - OSS上的文件路径 * @returns {Promise} - 删除是否成功 */ static async deleteFile(ossFilePath) { try { console.log(`【OSS删除】开始删除OSS文件: ${ossFilePath}`); const ossClient = getOSSClient(); // 【新增】记录OSS客户端配置信息(隐藏敏感信息) console.log(`【OSS删除】OSS客户端配置 - region: ${ossClient.options.region}, bucket: ${ossClient.options.bucket}`); const result = await ossClient.delete(ossFilePath); console.log(`【OSS删除】OSS文件删除成功: ${ossFilePath}`, result); return true; } catch (error) { console.error('【OSS删除】OSS文件删除失败:', ossFilePath, '错误:', error.message); console.error('【OSS删除】错误详情:', error); // 【增强日志】详细分析错误类型 console.log('【OSS删除】=== 错误详细分析开始 ==='); console.log('【OSS删除】错误名称:', error.name); console.log('【OSS删除】错误代码:', error.code); console.log('【OSS删除】HTTP状态码:', error.status); console.log('【OSS删除】请求ID:', error.requestId); console.log('【OSS删除】主机ID:', error.hostId); // 【关键检查】判断是否为权限不足错误 const isPermissionError = error.code === 'AccessDenied' || error.status === 403 || error.message.includes('permission') || error.message.includes('AccessDenied') || error.message.includes('do not have write permission'); if (isPermissionError) { console.error('【OSS删除】❌ 确认是权限不足错误!'); console.error('【OSS删除】❌ 当前AccessKey缺少删除文件的权限'); console.error('【OSS删除】❌ 请检查RAM策略是否包含 oss:DeleteObject 权限'); console.error('【OSS删除】❌ 建议在RAM中授予 AliyunOSSFullAccess 或自定义删除权限'); } console.log('【OSS删除】=== 错误详细分析结束 ==='); // 如果文件不存在,也算删除成功 if (error.code === 'NoSuchKey' || error.status === 404) { console.log(`【OSS删除】文件不存在,视为删除成功: ${ossFilePath}`); return true; } // 【新增】对于权限错误,提供更友好的错误信息 if (isPermissionError) { const permissionError = new Error(`OSS删除权限不足: ${error.message}`); permissionError.code = 'OSS_ACCESS_DENIED'; permissionError.originalError = error; throw permissionError; } throw error; } } /** * 获取OSS配置信息 * @returns {Object} - OSS配置信息 */ static getConfig() { return { region: ossConfig.region, bucket: ossConfig.bucket, endpoint: ossConfig.endpoint }; } /** * 测试OSS连接 * @returns {Promise} - 连接测试结果 */ static async testConnection() { try { console.log('【OSS连接测试】开始测试OSS连接...'); const ossClient = getOSSClient(); // 执行简单的list操作来验证连接 const result = await ossClient.list({ max: 1 }); console.log('【OSS连接测试】连接成功,存储桶中有', result.objects.length, '个对象'); return { success: true, message: 'OSS连接成功', region: ossClient.options.region, bucket: ossClient.options.bucket }; } catch (error) { console.error('【OSS连接测试】连接失败:', error.message); return { success: false, message: `OSS连接失败: ${error.message}`, error: error.message }; } } } module.exports = OssUploader;