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
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;
|