You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

323 lines
12 KiB

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: ossConfig.endpoint, // 直接使用配置的endpoint,不添加前缀
secure: true, // 启用HTTPS
cname: false, // 对于标准OSS域名,不需要启用cname模式
timeout: 600000, // 设置超时时间为10分钟,适应大文件上传
connectTimeout: 60000, // 连接超时时间1分钟
socketTimeout: 600000 // socket超时时间10分钟
});
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<String>} - 上传后的文件URL
*/
/**
* 计算文件的MD5哈希值
* @param {String} filePath - 文件路径
* @returns {Promise<String>} - 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<String>} - 上传后的文件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<String>} filePaths - 本地文件路径数组
* @param {String} folder - OSS上的文件夹路径
* @param {String} fileType - 文件类型,默认为'image'
* @returns {Promise<Array<String>>} - 上传后的文件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<Boolean>} - 删除是否成功
*/
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<Object>} - 连接测试结果
*/
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;