almost finish migrate new circumstances
This commit is contained in:
424
migrate_local_images_to_s3.py
Normal file
424
migrate_local_images_to_s3.py
Normal file
@ -0,0 +1,424 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
ローカル画像をS3に移行するスクリプト
|
||||
|
||||
使用方法:
|
||||
python migrate_local_images_to_s3.py
|
||||
|
||||
機能:
|
||||
- GoalImagesのローカル画像をS3に移行
|
||||
- CheckinImagesのローカル画像をS3に移行
|
||||
- 標準画像(start/goal/rule/map)も移行対象(存在する場合)
|
||||
- 移行後にデータベースのパスを更新
|
||||
- バックアップとロールバック機能付き
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import django
|
||||
from pathlib import Path
|
||||
import json
|
||||
from datetime import datetime
|
||||
import shutil
|
||||
import traceback
|
||||
|
||||
# Django settings setup
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
sys.path.append(str(BASE_DIR))
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||
django.setup()
|
||||
|
||||
from django.conf import settings
|
||||
from rog.models import GoalImages, CheckinImages
|
||||
from rog.services.s3_service import S3Service
|
||||
from django.core.files.storage import default_storage
|
||||
from django.core.files.base import ContentFile
|
||||
import logging
|
||||
|
||||
# ロギング設定
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler(f'migration_log_{datetime.now().strftime("%Y%m%d_%H%M%S")}.log'),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ImageMigrationService:
|
||||
"""画像移行サービス"""
|
||||
|
||||
def __init__(self):
|
||||
self.s3_service = S3Service()
|
||||
self.migration_stats = {
|
||||
'total_goal_images': 0,
|
||||
'total_checkin_images': 0,
|
||||
'successfully_migrated_goal': 0,
|
||||
'successfully_migrated_checkin': 0,
|
||||
'failed_migrations': [],
|
||||
'migration_details': []
|
||||
}
|
||||
self.backup_file = f'migration_backup_{datetime.now().strftime("%Y%m%d_%H%M%S")}.json'
|
||||
|
||||
def backup_database_state(self):
|
||||
"""移行前のデータベース状態をバックアップ"""
|
||||
logger.info("データベース状態をバックアップ中...")
|
||||
|
||||
backup_data = {
|
||||
'goal_images': [],
|
||||
'checkin_images': [],
|
||||
'migration_timestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
# GoalImages のバックアップ
|
||||
for goal_img in GoalImages.objects.all():
|
||||
backup_data['goal_images'].append({
|
||||
'id': goal_img.id,
|
||||
'original_path': str(goal_img.goalimage) if goal_img.goalimage else None,
|
||||
'user_id': goal_img.user.id,
|
||||
'team_name': goal_img.team_name,
|
||||
'event_code': goal_img.event_code,
|
||||
'cp_number': goal_img.cp_number
|
||||
})
|
||||
|
||||
# CheckinImages のバックアップ
|
||||
for checkin_img in CheckinImages.objects.all():
|
||||
backup_data['checkin_images'].append({
|
||||
'id': checkin_img.id,
|
||||
'original_path': str(checkin_img.checkinimage) if checkin_img.checkinimage else None,
|
||||
'user_id': checkin_img.user.id,
|
||||
'team_name': checkin_img.team_name,
|
||||
'event_code': checkin_img.event_code,
|
||||
'cp_number': checkin_img.cp_number
|
||||
})
|
||||
|
||||
with open(self.backup_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(backup_data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
logger.info(f"バックアップ完了: {self.backup_file}")
|
||||
return backup_data
|
||||
|
||||
def migrate_goal_images(self):
|
||||
"""ゴール画像をS3に移行"""
|
||||
logger.info("=== ゴール画像の移行開始 ===")
|
||||
|
||||
goal_images = GoalImages.objects.filter(goalimage__isnull=False).exclude(goalimage='')
|
||||
self.migration_stats['total_goal_images'] = goal_images.count()
|
||||
|
||||
logger.info(f"移行対象のゴール画像: {self.migration_stats['total_goal_images']}件")
|
||||
|
||||
for goal_img in goal_images:
|
||||
try:
|
||||
logger.info(f"処理中: GoalImage ID={goal_img.id}, Path={goal_img.goalimage}")
|
||||
|
||||
# ローカルファイルパスの構築
|
||||
local_file_path = os.path.join(settings.MEDIA_ROOT, str(goal_img.goalimage))
|
||||
|
||||
if not os.path.exists(local_file_path):
|
||||
logger.warning(f"ファイルが見つかりません: {local_file_path}")
|
||||
self.migration_stats['failed_migrations'].append({
|
||||
'type': 'goal',
|
||||
'id': goal_img.id,
|
||||
'reason': 'File not found',
|
||||
'path': local_file_path
|
||||
})
|
||||
continue
|
||||
|
||||
# ファイルを読み込み
|
||||
with open(local_file_path, 'rb') as f:
|
||||
file_content = f.read()
|
||||
|
||||
# ContentFileとして準備
|
||||
file_name = os.path.basename(local_file_path)
|
||||
content_file = ContentFile(file_content, name=file_name)
|
||||
|
||||
# S3にアップロード(ゴール画像として扱う)
|
||||
s3_url = self.s3_service.upload_checkin_image(
|
||||
image_file=content_file,
|
||||
event_code=goal_img.event_code,
|
||||
team_code=goal_img.team_name,
|
||||
cp_number=goal_img.cp_number,
|
||||
is_goal=True # ゴール画像フラグ
|
||||
)
|
||||
|
||||
if s3_url:
|
||||
# データベースを更新(S3パスを保存)
|
||||
original_path = str(goal_img.goalimage)
|
||||
goal_img.goalimage = s3_url.replace(f'https://{settings.AWS_S3_CUSTOM_DOMAIN}/', '')
|
||||
goal_img.save()
|
||||
|
||||
self.migration_stats['successfully_migrated_goal'] += 1
|
||||
self.migration_stats['migration_details'].append({
|
||||
'type': 'goal',
|
||||
'id': goal_img.id,
|
||||
'original_path': original_path,
|
||||
'new_s3_url': s3_url,
|
||||
'local_file': local_file_path
|
||||
})
|
||||
|
||||
logger.info(f"✅ 成功: {file_name} -> {s3_url}")
|
||||
|
||||
else:
|
||||
raise Exception("S3アップロードが失敗しました")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ エラー: GoalImage ID={goal_img.id}, Error={str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
self.migration_stats['failed_migrations'].append({
|
||||
'type': 'goal',
|
||||
'id': goal_img.id,
|
||||
'reason': str(e),
|
||||
'path': str(goal_img.goalimage)
|
||||
})
|
||||
|
||||
def migrate_checkin_images(self):
|
||||
"""チェックイン画像をS3に移行"""
|
||||
logger.info("=== チェックイン画像の移行開始 ===")
|
||||
|
||||
checkin_images = CheckinImages.objects.filter(checkinimage__isnull=False).exclude(checkinimage='')
|
||||
self.migration_stats['total_checkin_images'] = checkin_images.count()
|
||||
|
||||
logger.info(f"移行対象のチェックイン画像: {self.migration_stats['total_checkin_images']}件")
|
||||
|
||||
for checkin_img in checkin_images:
|
||||
try:
|
||||
logger.info(f"処理中: CheckinImage ID={checkin_img.id}, Path={checkin_img.checkinimage}")
|
||||
|
||||
# ローカルファイルパスの構築
|
||||
local_file_path = os.path.join(settings.MEDIA_ROOT, str(checkin_img.checkinimage))
|
||||
|
||||
if not os.path.exists(local_file_path):
|
||||
logger.warning(f"ファイルが見つかりません: {local_file_path}")
|
||||
self.migration_stats['failed_migrations'].append({
|
||||
'type': 'checkin',
|
||||
'id': checkin_img.id,
|
||||
'reason': 'File not found',
|
||||
'path': local_file_path
|
||||
})
|
||||
continue
|
||||
|
||||
# ファイルを読み込み
|
||||
with open(local_file_path, 'rb') as f:
|
||||
file_content = f.read()
|
||||
|
||||
# ContentFileとして準備
|
||||
file_name = os.path.basename(local_file_path)
|
||||
content_file = ContentFile(file_content, name=file_name)
|
||||
|
||||
# S3にアップロード
|
||||
s3_url = self.s3_service.upload_checkin_image(
|
||||
image_file=content_file,
|
||||
event_code=checkin_img.event_code,
|
||||
team_code=checkin_img.team_name,
|
||||
cp_number=checkin_img.cp_number
|
||||
)
|
||||
|
||||
if s3_url:
|
||||
# データベースを更新(S3パスを保存)
|
||||
original_path = str(checkin_img.checkinimage)
|
||||
checkin_img.checkinimage = s3_url.replace(f'https://{settings.AWS_S3_CUSTOM_DOMAIN}/', '')
|
||||
checkin_img.save()
|
||||
|
||||
self.migration_stats['successfully_migrated_checkin'] += 1
|
||||
self.migration_stats['migration_details'].append({
|
||||
'type': 'checkin',
|
||||
'id': checkin_img.id,
|
||||
'original_path': original_path,
|
||||
'new_s3_url': s3_url,
|
||||
'local_file': local_file_path
|
||||
})
|
||||
|
||||
logger.info(f"✅ 成功: {file_name} -> {s3_url}")
|
||||
|
||||
else:
|
||||
raise Exception("S3アップロードが失敗しました")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ エラー: CheckinImage ID={checkin_img.id}, Error={str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
self.migration_stats['failed_migrations'].append({
|
||||
'type': 'checkin',
|
||||
'id': checkin_img.id,
|
||||
'reason': str(e),
|
||||
'path': str(checkin_img.checkinimage)
|
||||
})
|
||||
|
||||
def migrate_standard_images(self):
|
||||
"""規定画像をS3に移行(存在する場合)"""
|
||||
logger.info("=== 規定画像の移行チェック開始 ===")
|
||||
|
||||
standard_types = ['start', 'goal', 'rule', 'map']
|
||||
media_root = Path(settings.MEDIA_ROOT)
|
||||
|
||||
# 各イベントフォルダをチェック
|
||||
events_found = set()
|
||||
|
||||
# GoalImagesとCheckinImagesから一意のイベントコードを取得
|
||||
goal_events = set(GoalImages.objects.values_list('event_code', flat=True))
|
||||
checkin_events = set(CheckinImages.objects.values_list('event_code', flat=True))
|
||||
all_events = goal_events.union(checkin_events)
|
||||
|
||||
logger.info(f"検出されたイベント: {all_events}")
|
||||
|
||||
for event_code in all_events:
|
||||
# 各標準画像タイプをチェック
|
||||
for image_type in standard_types:
|
||||
# 一般的な画像拡張子をチェック
|
||||
for ext in ['.jpg', '.jpeg', '.png', '.JPG', '.JPEG', '.PNG']:
|
||||
# 複数の可能なパスパターンをチェック
|
||||
possible_paths = [
|
||||
media_root / f'{event_code}_{image_type}{ext}',
|
||||
media_root / event_code / f'{image_type}{ext}',
|
||||
media_root / 'standards' / event_code / f'{image_type}{ext}',
|
||||
media_root / f'{image_type}_{event_code}{ext}',
|
||||
]
|
||||
|
||||
for possible_path in possible_paths:
|
||||
if possible_path.exists():
|
||||
try:
|
||||
logger.info(f"規定画像発見: {possible_path}")
|
||||
|
||||
# ファイルを読み込み
|
||||
with open(possible_path, 'rb') as f:
|
||||
file_content = f.read()
|
||||
|
||||
# ContentFileとして準備
|
||||
content_file = ContentFile(file_content, name=possible_path.name)
|
||||
|
||||
# S3にアップロード
|
||||
s3_url = self.s3_service.upload_standard_image(
|
||||
image_file=content_file,
|
||||
event_code=event_code,
|
||||
image_type=image_type
|
||||
)
|
||||
|
||||
if s3_url:
|
||||
self.migration_stats['migration_details'].append({
|
||||
'type': 'standard',
|
||||
'event_code': event_code,
|
||||
'image_type': image_type,
|
||||
'original_path': str(possible_path),
|
||||
'new_s3_url': s3_url
|
||||
})
|
||||
|
||||
logger.info(f"✅ 規定画像移行成功: {possible_path.name} -> {s3_url}")
|
||||
break # 同じタイプの画像が見つかったら他のパスはスキップ
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 規定画像移行エラー: {possible_path}, Error={str(e)}")
|
||||
self.migration_stats['failed_migrations'].append({
|
||||
'type': 'standard',
|
||||
'event_code': event_code,
|
||||
'image_type': image_type,
|
||||
'reason': str(e),
|
||||
'path': str(possible_path)
|
||||
})
|
||||
|
||||
def generate_migration_report(self):
|
||||
"""移行レポートを生成"""
|
||||
logger.info("=== 移行レポート生成 ===")
|
||||
|
||||
report = {
|
||||
'migration_timestamp': datetime.now().isoformat(),
|
||||
'summary': {
|
||||
'total_goal_images': self.migration_stats['total_goal_images'],
|
||||
'successfully_migrated_goal': self.migration_stats['successfully_migrated_goal'],
|
||||
'total_checkin_images': self.migration_stats['total_checkin_images'],
|
||||
'successfully_migrated_checkin': self.migration_stats['successfully_migrated_checkin'],
|
||||
'total_failed': len(self.migration_stats['failed_migrations']),
|
||||
'success_rate_goal': (
|
||||
self.migration_stats['successfully_migrated_goal'] / max(self.migration_stats['total_goal_images'], 1) * 100
|
||||
),
|
||||
'success_rate_checkin': (
|
||||
self.migration_stats['successfully_migrated_checkin'] / max(self.migration_stats['total_checkin_images'], 1) * 100
|
||||
)
|
||||
},
|
||||
'details': self.migration_stats['migration_details'],
|
||||
'failures': self.migration_stats['failed_migrations']
|
||||
}
|
||||
|
||||
# レポートファイルの保存
|
||||
report_file = f'migration_report_{datetime.now().strftime("%Y%m%d_%H%M%S")}.json'
|
||||
with open(report_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(report, f, ensure_ascii=False, indent=2)
|
||||
|
||||
# コンソール出力
|
||||
print("\n" + "="*60)
|
||||
print("🎯 画像S3移行レポート")
|
||||
print("="*60)
|
||||
print(f"📊 ゴール画像: {report['summary']['successfully_migrated_goal']}/{report['summary']['total_goal_images']} "
|
||||
f"({report['summary']['success_rate_goal']:.1f}%)")
|
||||
print(f"📊 チェックイン画像: {report['summary']['successfully_migrated_checkin']}/{report['summary']['total_checkin_images']} "
|
||||
f"({report['summary']['success_rate_checkin']:.1f}%)")
|
||||
print(f"❌ 失敗数: {report['summary']['total_failed']}")
|
||||
print(f"📄 詳細レポート: {report_file}")
|
||||
print(f"💾 バックアップファイル: {self.backup_file}")
|
||||
|
||||
if report['summary']['total_failed'] > 0:
|
||||
print("\n⚠️ 失敗した移行:")
|
||||
for failure in self.migration_stats['failed_migrations'][:5]: # 最初の5件のみ表示
|
||||
print(f" - {failure['type']} ID={failure.get('id', 'N/A')}: {failure['reason']}")
|
||||
if len(self.migration_stats['failed_migrations']) > 5:
|
||||
print(f" ... 他 {len(self.migration_stats['failed_migrations']) - 5} 件")
|
||||
|
||||
return report
|
||||
|
||||
def run_migration(self):
|
||||
"""メイン移行処理"""
|
||||
logger.info("🚀 画像S3移行開始")
|
||||
print("🚀 画像S3移行を開始します...")
|
||||
|
||||
try:
|
||||
# 1. バックアップ
|
||||
self.backup_database_state()
|
||||
|
||||
# 2. ゴール画像移行
|
||||
self.migrate_goal_images()
|
||||
|
||||
# 3. チェックイン画像移行
|
||||
self.migrate_checkin_images()
|
||||
|
||||
# 4. 規定画像移行
|
||||
self.migrate_standard_images()
|
||||
|
||||
# 5. レポート生成
|
||||
report = self.generate_migration_report()
|
||||
|
||||
logger.info("✅ 移行完了")
|
||||
print("\n✅ 移行が完了しました!")
|
||||
|
||||
return report
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"💥 移行中に重大なエラーが発生: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
print(f"\n💥 移行エラー: {str(e)}")
|
||||
print(f"バックアップファイル: {self.backup_file}")
|
||||
raise
|
||||
|
||||
def main():
|
||||
"""メイン関数"""
|
||||
print("="*60)
|
||||
print("🔄 ローカル画像S3移行ツール")
|
||||
print("="*60)
|
||||
print("このツールは以下を実行します:")
|
||||
print("1. データベースの現在の状態をバックアップ")
|
||||
print("2. GoalImages のローカル画像をS3に移行")
|
||||
print("3. CheckinImages のローカル画像をS3に移行")
|
||||
print("4. 標準画像(存在する場合)をS3に移行")
|
||||
print("5. 移行レポートの生成")
|
||||
print()
|
||||
|
||||
# 確認プロンプト
|
||||
confirm = input("移行を開始しますか? [y/N]: ").strip().lower()
|
||||
if confirm not in ['y', 'yes']:
|
||||
print("移行をキャンセルしました。")
|
||||
return
|
||||
|
||||
# 移行実行
|
||||
migration_service = ImageMigrationService()
|
||||
migration_service.run_migration()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user