Files
jurong_circle_black/migrate-to-minio.js
2025-09-02 16:50:35 +08:00

398 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;