457 lines
16 KiB
Python
457 lines
16 KiB
Python
|
|
#!/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())
|