Files
geMoldInsight/migrate_to_new_structure.py
T
2026-02-12 23:27:42 +08:00

457 lines
16 KiB
Python
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.
#!/usr/bin/env python3
"""
RustFS 存储结构迁移脚本 - 完整版
从旧结构迁移到新结构:
旧桶: moldinsight-storage
新桶: moldinsight/
├── stp-files/{uuid}.stp
├── geometry/{file_hash}.json
├── mold-cavities/{file_hash}.json
├── html/{file_hash}.json
└── user-files/{uuid}.{ext}
注意:这是rustFS,而不是minio,只是用了minio的通用S3接口
"""
import asyncio
import sys
import os
from pathlib import Path
from typing import Dict, List, Optional
import json
from datetime import datetime
import hashlib
# 添加项目根目录和 src 目录到 Python 路径
project_root = Path(__file__).parent
src_root = project_root / "src"
sys.path.insert(0, str(project_root))
sys.path.insert(0, str(src_root))
from storage.rustfs_storage import RustFSManager
from config.settings import settings
from utils.logger import get_logger
logger = get_logger(__name__)
class RustFSMigration:
"""RustFS 存储结构迁移器"""
def __init__(self):
self.rustfs = RustFSManager()
self.is_connected = False
# 旧桶名称
self.old_bucket = 'moldinsight-storage'
# 新桶名称
self.new_bucket = 'moldinsight'
# 文件类型前缀映射
self.file_type_mapping = {
'stp-files': 'stp_files',
'geometry': 'geometry_data',
'mold-cavities': 'mold_cavities',
'html': 'html_files',
'user-files': 'user_files'
}
async def connect(self):
"""连接到 RustFS"""
try:
await self.rustfs.connect(
endpoint=settings.RUSTFS_ENDPOINT,
access_key=settings.RUSTFS_ACCESS_KEY,
secret_key=settings.RUSTFS_SECRET_KEY,
timeout=settings.RUSTFS_TIMEOUT
)
self.is_connected = True
logger.info("RustFS 连接成功")
return True
except Exception as e:
logger.error(f"RustFS 连接失败: {e}")
return False
async def close(self):
"""关闭连接"""
await self.rustfs.close()
self.is_connected = False
logger.info("RustFS 连接已关闭")
async def list_all_buckets(self) -> List[str]:
"""列出所有桶"""
if not self.is_connected:
raise RuntimeError("RustFS 未连接")
try:
buckets = self.rustfs.client.list_buckets()
bucket_names = [bucket.name for bucket in buckets]
logger.info(f"当前存在的桶: {bucket_names}")
return bucket_names
except Exception as e:
logger.error(f"列出桶失败: {e}")
return []
async def check_old_bucket_files(self) -> List[Dict]:
"""检查旧桶中的所有文件"""
if not self.is_connected:
raise RuntimeError("RustFS 未连接")
try:
# 检查旧桶是否存在
if not self.rustfs.client.bucket_exists(self.old_bucket):
logger.info(f"旧桶不存在: {self.old_bucket}")
return []
# 列出所有文件
objects = self.rustfs.client.list_objects(self.old_bucket, recursive=True)
files = []
for obj in objects:
files.append({
'object_key': obj.object_name,
'size': obj.size,
'last_modified': obj.last_modified,
'etag': obj.etag
})
logger.info(f"旧桶 {self.old_bucket} 包含 {len(files)} 个文件")
return files
except Exception as e:
logger.error(f"检查旧桶文件失败: {e}")
return []
async def ensure_new_bucket(self):
"""确保新桶存在"""
if not self.is_connected:
raise RuntimeError("RustFS 未连接")
try:
if not self.rustfs.client.bucket_exists(self.new_bucket):
self.rustfs.client.make_bucket(self.new_bucket)
logger.info(f"创建新桶: {self.new_bucket}")
else:
logger.info(f"新桶已存在: {self.new_bucket}")
except Exception as e:
logger.error(f"确保新桶存在失败: {e}")
raise
async def migrate_file(self, old_object_key: str) -> bool:
"""迁移单个文件到新结构"""
try:
# 下载旧文件
response = self.rustfs.client.get_object(self.old_bucket, old_object_key)
file_data = response.read()
response.close()
response.release_conn()
# 解析旧对象键,确定文件类型
# 旧格式: moldinsight/{文件类型}/{文件名} 或 {文件类型}/{文件名}
parts = old_object_key.split('/')
# 确定文件类型和新对象键
if len(parts) >= 2:
# 可能是 moldinsight/{类型}/{文件} 或 {类型}/{文件}
if parts[0] == 'moldinsight' and len(parts) >= 3:
# moldinsight/{类型}/{文件}
old_type = parts[1]
filename = parts[2]
elif parts[0] in self.file_type_mapping:
# {类型}/{文件}
old_type = parts[0]
filename = parts[1]
else:
# 无法识别的格式,使用默认
old_type = 'misc'
filename = parts[-1]
else:
old_type = 'misc'
filename = parts[-1]
# 根据旧类型确定新类型
new_type = old_type # 默认保持不变
# 生成新对象键
if old_type in self.file_type_mapping:
# 直接使用类型名作为目录
new_object_key = f"{old_type}/{filename}"
else:
# 其他文件放到misc目录
new_object_key = f"misc/{filename}"
# 上传到新桶
self.rustfs.client.put_object(
self.new_bucket,
new_object_key,
data=file_data,
length=len(file_data),
content_type='application/octet-stream'
)
logger.info(f"文件迁移成功: {self.old_bucket}/{old_object_key} -> {self.new_bucket}/{new_object_key}")
return True
except Exception as e:
logger.error(f"文件迁移失败 {old_object_key}: {e}")
return False
async def migrate_all_files(self) -> Dict[str, any]:
"""迁移所有文件"""
if not self.is_connected:
raise RuntimeError("RustFS 未连接")
migration_result = {
'total_files': 0,
'successful': 0,
'failed': 0,
'failed_files': []
}
try:
# 获取所有文件
files = await self.check_old_bucket_files()
migration_result['total_files'] = len(files)
if len(files) == 0:
logger.info("旧桶中没有文件需要迁移")
return migration_result
logger.info(f"开始迁移 {len(files)} 个文件...")
for file_info in files:
old_object_key = file_info['object_key']
success = await self.migrate_file(old_object_key)
if success:
migration_result['successful'] += 1
else:
migration_result['failed'] += 1
migration_result['failed_files'].append(old_object_key)
logger.info(f"迁移完成: 成功 {migration_result['successful']}, 失败 {migration_result['failed']}")
except Exception as e:
logger.error(f"文件迁移失败: {e}")
return migration_result
async def delete_old_bucket(self) -> bool:
"""删除旧桶及其所有文件"""
if not self.is_connected:
raise RuntimeError("RustFS 未连接")
try:
# 检查桶是否存在
if not self.rustfs.client.bucket_exists(self.old_bucket):
logger.info(f"旧桶不存在,无需删除: {self.old_bucket}")
return True
# 列出所有文件
objects = list(self.rustfs.client.list_objects(self.old_bucket, recursive=True))
if len(objects) > 0:
logger.info(f"删除旧桶中的 {len(objects)} 个文件...")
# 删除所有文件
for obj in objects:
self.rustfs.client.remove_object(self.old_bucket, obj.object_name)
logger.debug(f"删除文件: {obj.object_name}")
# 删除空桶
self.rustfs.client.remove_bucket(self.old_bucket)
logger.info(f"旧桶删除成功: {self.old_bucket}")
return True
except Exception as e:
logger.error(f"删除旧桶失败 {self.old_bucket}: {e}")
return False
async def list_new_bucket_structure(self) -> Dict[str, List[str]]:
"""列出新桶的文件结构"""
if not self.is_connected:
raise RuntimeError("RustFS 未连接")
try:
if not self.rustfs.client.bucket_exists(self.new_bucket):
return {}
objects = self.rustfs.client.list_objects(self.new_bucket, recursive=True)
structure = {
'stp-files': [],
'geometry': [],
'mold-cavities': [],
'html': [],
'user-files': [],
'misc': []
}
for obj in objects:
parts = obj.object_name.split('/')
if len(parts) >= 2:
file_type = parts[0]
if file_type in structure:
structure[file_type].append(obj.object_name)
else:
structure['misc'].append(obj.object_name)
return structure
except Exception as e:
logger.error(f"列出新桶结构失败: {e}")
return {}
async def run_migration(self, delete_old_bucket: bool = False) -> Dict[str, any]:
"""运行完整迁移流程"""
migration_summary = {
'start_time': datetime.now().isoformat(),
'connection_status': False,
'all_buckets': [],
'old_bucket_files': [],
'migration_results': {},
'new_bucket_structure': {},
'deletion_result': False,
'end_time': None,
'status': 'failed'
}
try:
# 1. 连接
logger.info("=== RustFS 存储结构迁移开始 ===")
connection_result = await self.connect()
if not connection_result:
raise RuntimeError("无法连接到 RustFS")
migration_summary['connection_status'] = True
# 2. 列出所有桶
logger.info("1. 检查桶状态...")
all_buckets = await self.list_all_buckets()
migration_summary['all_buckets'] = all_buckets
# 3. 检查旧桶文件
logger.info("2. 检查旧桶文件...")
old_bucket_files = await self.check_old_bucket_files()
migration_summary['old_bucket_files'] = old_bucket_files
if not old_bucket_files:
logger.info("旧桶中没有文件,跳过迁移")
migration_summary['status'] = 'completed'
migration_summary['end_time'] = datetime.now().isoformat()
return migration_summary
# 4. 确保新桶存在
logger.info("3. 确保新桶存在...")
await self.ensure_new_bucket()
# 5. 执行迁移
logger.info("4. 开始迁移文件...")
migration_results = await self.migrate_all_files()
migration_summary['migration_results'] = migration_results
# 6. 检查新桶结构
logger.info("5. 检查新桶结构...")
new_bucket_structure = await self.list_new_bucket_structure()
migration_summary['new_bucket_structure'] = new_bucket_structure
# 7. 可选:删除旧桶
if delete_old_bucket:
logger.info("6. 删除旧桶...")
deletion_result = await self.delete_old_bucket()
migration_summary['deletion_result'] = deletion_result
else:
logger.info("6. 保留旧桶(跳过删除)")
# 8. 完成
migration_summary['status'] = 'completed'
migration_summary['end_time'] = datetime.now().isoformat()
logger.info("=== RustFS 存储结构迁移完成 ===")
return migration_summary
except Exception as e:
migration_summary['error'] = str(e)
migration_summary['end_time'] = datetime.now().isoformat()
logger.error(f"迁移失败: {e}")
return migration_summary
finally:
await self.close()
async def main():
"""主函数"""
migration = RustFSMigration()
print("=== RustFS 存储结构迁移工具 ===")
print("注意:这是rustFS,而不是minio,只是用了minio的通用S3接口")
print()
print("从旧结构迁移到新结构:")
print(" 旧桶: moldinsight-storage")
print(" 新桶: moldinsight/")
print(" ├── stp-files/")
print(" ├── geometry/")
print(" ├── mold-cavities/")
print(" ├── html/")
print(" └── user-files/")
print()
# 询问是否删除旧桶
delete_old = input("是否在迁移完成后删除旧桶 moldinsight-storage?(y/N): ").strip().lower() == 'y'
print("\n开始迁移...")
# 运行迁移
result = await migration.run_migration(delete_old_bucket=delete_old)
# 输出结果摘要
print("\n=== 迁移结果摘要 ===")
print(f"状态: {result['status']}")
print(f"开始时间: {result['start_time']}")
print(f"结束时间: {result['end_time']}")
if 'error' in result:
print(f"错误: {result['error']}")
# 桶列表
print("\n--- 当前桶列表 ---")
for bucket in result['all_buckets']:
print(f" - {bucket}")
# 旧桶文件统计
print(f"\n--- 旧桶文件统计 ---")
print(f"旧桶 {migration.old_bucket} 包含 {len(result['old_bucket_files'])} 个文件")
# 迁移结果
print("\n--- 迁移结果 ---")
migration_result = result['migration_results']
print(f"总文件数: {migration_result['total_files']}")
print(f"成功: {migration_result['successful']}")
print(f"失败: {migration_result['failed']}")
if migration_result['failed'] > 0:
print("\n失败的文件:")
for failed_file in migration_result['failed_files']:
print(f" - {failed_file}")
# 新桶结构
print("\n--- 新桶文件结构 ---")
for file_type, files in result['new_bucket_structure'].items():
if files:
print(f"{file_type}: {len(files)} 个文件")
# 删除结果
if 'deletion_result' in result:
status = "成功" if result['deletion_result'] else "失败"
print(f"\n--- 旧桶删除结果 ---")
print(f"旧桶 {migration.old_bucket} 删除: {status}")
print("\n=== 迁移完成 ===")
if __name__ == "__main__":
asyncio.run(main())