Files
jurong_circle_black/migrate-to-minio.js

398 lines
13 KiB
JavaScript
Raw Normal View History

2025-09-02 16:50:35 +08:00
const fs = require('fs');
const path = require('path');
const minioService = require('./services/minioService');
const { getDB, initDB } = require('./database');
/**
* 文件迁移到 MinIO 的脚本
* 将本地 uploads 目录下的文件迁移到 MinIO 存储
*/
class FilesMigration {
constructor() {
this.minioService = minioService;
this.uploadsDir = path.join(__dirname, 'uploads');
this.migratedFiles = [];
this.failedFiles = [];
}
/**
* 递归获取目录下所有文件
* @param {string} dir - 目录路径
* @param {string} baseDir - 基础目录路径
* @returns {Array} 文件列表
*/
getAllFiles(dir, baseDir = dir) {
const files = [];
const items = fs.readdirSync(dir);
for (const item of items) {
const fullPath = path.join(dir, item);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
files.push(...this.getAllFiles(fullPath, baseDir));
} else {
const relativePath = path.relative(baseDir, fullPath);
files.push({
fullPath,
relativePath: relativePath.replace(/\\/g, '/'), // 统一使用正斜杠
fileName: item,
size: stat.size,
mtime: stat.mtime
});
}
}
return files;
}
/**
* 生成新的 MinIO 路径去掉 uploads 前缀
* @param {Object} fileInfo - 文件信息
* @returns {string} 去掉 uploads 前缀的文件路径
*/
generateMinioPath(fileInfo) {
// 去掉 uploads/ 前缀,因为数据库中存储的是 /uploads/xxx而 MinIO 中应该是 xxx
let minioPath = fileInfo.relativePath;
if (minioPath.startsWith('uploads/')) {
minioPath = minioPath.substring('uploads/'.length);
}
return minioPath;
}
/**
* 上传单个文件到 MinIO
* @param {Object} fileInfo - 文件信息
* @returns {Promise<Object>} 上传结果
*/
async uploadFileToMinio(fileInfo) {
try {
const minioPath = this.generateMinioPath(fileInfo);
// 使用固定的存储桶名称
const bucketName = 'jurongquan';
console.log(`正在上传: ${fileInfo.relativePath} -> ${minioPath}`);
// 读取文件内容
const fileBuffer = fs.readFileSync(fileInfo.fullPath);
// 上传到 MinIO
const uploadResult = await this.minioService.uploadFileForMigration(
bucketName,
minioPath,
fileBuffer,
this.getContentType(fileInfo.fileName)
);
const result = {
url: uploadResult.data.url,
path: uploadResult.data.path
};
return {
success: true,
originalPath: fileInfo.relativePath,
minioPath,
minioUrl: result.url,
fileInfo
};
} catch (error) {
console.error(`上传失败 ${fileInfo.relativePath}:`, error.message);
return {
success: false,
originalPath: fileInfo.relativePath,
error: error.message,
fileInfo
};
}
}
/**
* 根据文件扩展名获取 Content-Type
* @param {string} fileName - 文件名
* @returns {string} Content-Type
*/
getContentType(fileName) {
const ext = path.extname(fileName).toLowerCase();
const contentTypes = {
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif',
'.webp': 'image/webp',
'.pdf': 'application/pdf',
'.doc': 'application/msword',
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'.txt': 'text/plain'
};
return contentTypes[ext] || 'application/octet-stream';
}
/**
* 更新数据库中的文件路径引用
* @param {Array} migratedFiles - 已迁移的文件列表
*/
async updateDatabaseReferences(migratedFiles) {
const db = getDB();
let updatedCount = 0;
console.log('\n开始更新数据库中的文件路径引用...');
for (const file of migratedFiles) {
console.log(file,'file');
try {
// 原路径:/uploads/avatars/xxx.jpg
const oldPath = `/${file.originalPath}`;
// 新路径:/avatars/xxx.jpg (去掉 uploads 前缀)
const newPath = `/${file.minioPath}`;
// 更新用户头像
const [avatarResult] = await db.execute(
'UPDATE users SET avatar = ? WHERE avatar = ?',
[newPath, oldPath]
);
if (avatarResult.affectedRows > 0) {
console.log(`更新用户头像: ${oldPath} -> ${newPath}`);
updatedCount += avatarResult.affectedRows;
}
// 更新用户微信收款码
const [wechatQrResult] = await db.execute(
'UPDATE users SET wechat_qr = ? WHERE wechat_qr = ?',
[newPath, oldPath]
);
if (wechatQrResult.affectedRows > 0) {
console.log(`更新用户微信收款码: ${oldPath} -> ${newPath}`);
updatedCount += wechatQrResult.affectedRows;
}
// 更新用户支付宝收款码
const [alipayQrResult] = await db.execute(
'UPDATE users SET alipay_qr = ? WHERE alipay_qr = ?',
[newPath, oldPath]
);
if (alipayQrResult.affectedRows > 0) {
console.log(`更新用户支付宝收款码: ${oldPath} -> ${newPath}`);
updatedCount += alipayQrResult.affectedRows;
}
// 更新用户云闪付收款码
const [unionpayQrResult] = await db.execute(
'UPDATE users SET unionpay_qr = ? WHERE unionpay_qr = ?',
[newPath, oldPath]
);
if (unionpayQrResult.affectedRows > 0) {
console.log(`更新用户云闪付收款码: ${oldPath} -> ${newPath}`);
updatedCount += unionpayQrResult.affectedRows;
}
// 更新用户营业执照
const [businessLicenseResult] = await db.execute(
'UPDATE users SET business_license = ? WHERE business_license = ?',
[newPath, oldPath]
);
if (businessLicenseResult.affectedRows > 0) {
console.log(`更新用户营业执照: ${oldPath} -> ${newPath}`);
updatedCount += businessLicenseResult.affectedRows;
}
// 更新用户身份证正面
const [idCardFrontResult] = await db.execute(
'UPDATE users SET id_card_front = ? WHERE id_card_front = ?',
[newPath, oldPath]
);
if (idCardFrontResult.affectedRows > 0) {
console.log(`更新用户身份证正面: ${oldPath} -> ${newPath}`);
updatedCount += idCardFrontResult.affectedRows;
}
// 更新用户身份证反面
const [idCardBackResult] = await db.execute(
'UPDATE users SET id_card_back = ? WHERE id_card_back = ?',
[newPath, oldPath]
);
if (idCardBackResult.affectedRows > 0) {
console.log(`更新用户身份证反面: ${oldPath} -> ${newPath}`);
updatedCount += idCardBackResult.affectedRows;
}
// 更新产品图片
const [productResult] = await db.execute(
'UPDATE products SET image_url = ? WHERE image_url = ?',
[newPath, oldPath]
);
if (productResult.affectedRows > 0) {
console.log(`更新产品图片: ${oldPath} -> ${newPath}`);
updatedCount += productResult.affectedRows;
}
// 更新产品店铺头像
const [shopAvatarResult] = await db.execute(
'UPDATE products SET shop_avatar = ? WHERE shop_avatar = ?',
[newPath, oldPath]
);
if (shopAvatarResult.affectedRows > 0) {
console.log(`更新产品店铺头像: ${oldPath} -> ${newPath}`);
updatedCount += shopAvatarResult.affectedRows;
}
// 更新产品图片JSON 字段中的图片)
const [products] = await db.execute(
'SELECT id, images FROM products WHERE images LIKE ?',
[`%${oldPath}%`]
);
for (const product of products) {
if (product.images) {
const updatedImages = product.images.replace(new RegExp(oldPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newPath);
await db.execute(
'UPDATE products SET images = ? WHERE id = ?',
[updatedImages, product.id]
);
console.log(`更新产品图片集合: 产品ID ${product.id}`);
updatedCount++;
}
}
// 更新转账凭证
const [transferResult] = await db.execute(
'UPDATE transfers SET voucher_url = ? WHERE voucher_url = ?',
[newPath, oldPath]
);
if (transferResult.affectedRows > 0) {
console.log(`更新转账凭证: ${oldPath} -> ${newPath}`);
updatedCount += transferResult.affectedRows;
}
// 更新区域代理收款码
const [agentQrResult] = await db.execute(
'UPDATE regional_agents SET qr_code_url = ? WHERE qr_code_url = ?',
[newPath, oldPath]
);
if (agentQrResult.affectedRows > 0) {
console.log(`更新区域代理收款码: ${oldPath} -> ${newPath}`);
updatedCount += agentQrResult.affectedRows;
}
// 更新规格图片
const [specResult] = await db.execute(
'UPDATE spec_values SET image_url = ? WHERE image_url = ?',
[newPath, oldPath]
);
if (specResult.affectedRows > 0) {
console.log(`更新规格图片: ${oldPath} -> ${newPath}`);
updatedCount += specResult.affectedRows;
}
} catch (error) {
console.error(`更新数据库引用失败 ${file.originalPath}:`, error.message);
}
}
console.log(`\n数据库更新完成,共更新 ${updatedCount} 条记录`);
}
/**
* 执行迁移
*/
async migrate() {
try {
console.log('开始文件迁移到 MinIO...');
console.log(`源目录: ${this.uploadsDir}`);
// 初始化数据库连接
console.log('初始化数据库连接...');
await initDB();
console.log('数据库连接初始化完成');
// 检查源目录是否存在
if (!fs.existsSync(this.uploadsDir)) {
console.log('uploads 目录不存在,无需迁移');
return;
}
// 获取所有文件
const allFiles = this.getAllFiles(this.uploadsDir);
console.log(`找到 ${allFiles.length} 个文件需要迁移`);
if (allFiles.length === 0) {
console.log('没有文件需要迁移');
return;
}
// 逐个上传文件
for (let i = 0; i < allFiles.length; i++) {
const file = allFiles[i];
console.log(`\n进度: ${i + 1}/${allFiles.length}`);
const result = await this.uploadFileToMinio(file);
if (result.success) {
this.migratedFiles.push(result);
} else {
this.failedFiles.push(result);
}
// 添加小延迟避免过快请求
await new Promise(resolve => setTimeout(resolve, 100));
}
// 输出迁移结果
console.log('\n=== 迁移结果 ===');
console.log(`成功迁移: ${this.migratedFiles.length} 个文件`);
console.log(`迁移失败: ${this.failedFiles.length} 个文件`);
if (this.failedFiles.length > 0) {
console.log('\n失败的文件:');
this.failedFiles.forEach(file => {
console.log(`- ${file.originalPath}: ${file.error}`);
});
}
// 更新数据库引用,将 /uploads/xxx 路径更新为 /xxx
if (this.migratedFiles.length > 0) {
await this.updateDatabaseReferences(this.migratedFiles);
}
// 生成迁移报告
await this.generateMigrationReport();
console.log('\n迁移完成');
console.log('\n注意事项:');
console.log('1. 请验证文件访问是否正常');
console.log('2. 确认数据库中的文件路径已正确更新');
console.log('3. 测试完成后可以删除本地 uploads 目录');
console.log('4. 查看 migration-report.json 了解详细迁移信息');
} catch (error) {
console.error('迁移过程中发生错误:', error);
}
}
/**
* 生成迁移报告
*/
async generateMigrationReport() {
const report = {
migrationDate: new Date().toISOString(),
totalFiles: this.migratedFiles.length + this.failedFiles.length,
successCount: this.migratedFiles.length,
failedCount: this.failedFiles.length,
migratedFiles: this.migratedFiles,
failedFiles: this.failedFiles
};
const reportPath = path.join(__dirname, 'migration-report.json');
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
console.log(`\n迁移报告已生成: ${reportPath}`);
}
}
// 如果直接运行此脚本
if (require.main === module) {
const migration = new FilesMigration();
migration.migrate().catch(console.error);
}
module.exports = FilesMigration;