#!/usr/bin/env python """ データベースパス更新ロールバックスクリプト バックアップファイルからパス情報を復元します。 """ import os import sys import django from pathlib import Path import json from datetime import datetime # 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 django.db import transaction import logging # ロギング設定 logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler(f'rollback_log_{datetime.now().strftime("%Y%m%d_%H%M%S")}.log'), logging.StreamHandler() ] ) logger = logging.getLogger(__name__) class PathRollbackService: """パスロールバックサービス""" def __init__(self, backup_file): self.backup_file = backup_file self.rollback_stats = { 'total_goal_images': 0, 'restored_goal_images': 0, 'total_checkin_images': 0, 'restored_checkin_images': 0, 'failed_restores': [] } def load_backup_data(self): """バックアップデータを読み込み""" try: with open(self.backup_file, 'r', encoding='utf-8') as f: backup_data = json.load(f) logger.info(f"バックアップファイル読み込み成功: {self.backup_file}") logger.info(f"バックアップ日時: {backup_data.get('backup_timestamp', 'Unknown')}") logger.info(f"GoalImages: {len(backup_data.get('goal_images', []))}件") logger.info(f"CheckinImages: {len(backup_data.get('checkin_images', []))}件") return backup_data except FileNotFoundError: raise Exception(f"バックアップファイルが見つかりません: {self.backup_file}") except json.JSONDecodeError: raise Exception(f"バックアップファイルの形式が不正です: {self.backup_file}") except Exception as e: raise Exception(f"バックアップファイル読み込みエラー: {str(e)}") def rollback_goal_images(self, goal_images_backup): """GoalImagesをロールバック""" logger.info("=== GoalImagesロールバック開始 ===") self.rollback_stats['total_goal_images'] = len(goal_images_backup) restored_count = 0 with transaction.atomic(): for backup_item in goal_images_backup: try: goal_img = GoalImages.objects.get(id=backup_item['id']) original_path = backup_item['original_path'] goal_img.goalimage = original_path goal_img.save() restored_count += 1 if restored_count <= 5: # 最初の5件のみログ出力 logger.info(f"✅ GoalImage ID={goal_img.id}: パス復元完了") except GoalImages.DoesNotExist: logger.warning(f"⚠️ GoalImage ID={backup_item['id']} が存在しません(削除済み)") self.rollback_stats['failed_restores'].append({ 'type': 'goal', 'id': backup_item['id'], 'reason': 'Record not found' }) except Exception as e: logger.error(f"❌ GoalImage ID={backup_item['id']} 復元エラー: {str(e)}") self.rollback_stats['failed_restores'].append({ 'type': 'goal', 'id': backup_item['id'], 'reason': str(e) }) self.rollback_stats['restored_goal_images'] = restored_count logger.info(f"GoalImagesロールバック完了: {restored_count}件復元") def rollback_checkin_images(self, checkin_images_backup): """CheckinImagesをロールバック""" logger.info("=== CheckinImagesロールバック開始 ===") self.rollback_stats['total_checkin_images'] = len(checkin_images_backup) restored_count = 0 with transaction.atomic(): for backup_item in checkin_images_backup: try: checkin_img = CheckinImages.objects.get(id=backup_item['id']) original_path = backup_item['original_path'] checkin_img.checkinimage = original_path checkin_img.save() restored_count += 1 if restored_count <= 5: # 最初の5件のみログ出力 logger.info(f"✅ CheckinImage ID={checkin_img.id}: パス復元完了") except CheckinImages.DoesNotExist: logger.warning(f"⚠️ CheckinImage ID={backup_item['id']} が存在しません(削除済み)") self.rollback_stats['failed_restores'].append({ 'type': 'checkin', 'id': backup_item['id'], 'reason': 'Record not found' }) except Exception as e: logger.error(f"❌ CheckinImage ID={backup_item['id']} 復元エラー: {str(e)}") self.rollback_stats['failed_restores'].append({ 'type': 'checkin', 'id': backup_item['id'], 'reason': str(e) }) self.rollback_stats['restored_checkin_images'] = restored_count logger.info(f"CheckinImagesロールバック完了: {restored_count}件復元") def generate_rollback_report(self): """ロールバックレポートを生成""" logger.info("=== ロールバックレポート生成 ===") total_restored = self.rollback_stats['restored_goal_images'] + self.rollback_stats['restored_checkin_images'] total_processed = self.rollback_stats['total_goal_images'] + self.rollback_stats['total_checkin_images'] report = { 'rollback_timestamp': datetime.now().isoformat(), 'backup_file': self.backup_file, 'summary': { 'total_processed': total_processed, 'total_restored': total_restored, 'goal_images_restored': self.rollback_stats['restored_goal_images'], 'checkin_images_restored': self.rollback_stats['restored_checkin_images'], 'failed_restores': len(self.rollback_stats['failed_restores']), 'success_rate': (total_restored / max(total_processed, 1) * 100) }, 'failed_restores': self.rollback_stats['failed_restores'] } # レポートファイルの保存 report_file = f'rollback_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("🔄 ロールバックレポート") print("="*60) print(f"📄 バックアップファイル: {self.backup_file}") print(f"📊 処理総数: {total_processed:,}件") print(f"✅ 復元成功: {total_restored:,}件 ({report['summary']['success_rate']:.1f}%)") print(f" - ゴール画像: {self.rollback_stats['restored_goal_images']:,}件") print(f" - チェックイン画像: {self.rollback_stats['restored_checkin_images']:,}件") print(f"❌ 失敗: {len(self.rollback_stats['failed_restores'])}件") print(f"📄 詳細レポート: {report_file}") if len(self.rollback_stats['failed_restores']) > 0: print("\n⚠️ 失敗した復元:") for failure in self.rollback_stats['failed_restores'][:5]: print(f" - {failure['type']} ID={failure['id']}: {failure['reason']}") if len(self.rollback_stats['failed_restores']) > 5: print(f" ... 他 {len(self.rollback_stats['failed_restores']) - 5} 件") return report def run_rollback(self): """メインロールバック処理""" logger.info("🔄 ロールバック開始") print("🔄 データベースパスをロールバックします...") try: # 1. バックアップデータ読み込み backup_data = self.load_backup_data() # 2. GoalImagesロールバック if backup_data.get('goal_images'): self.rollback_goal_images(backup_data['goal_images']) # 3. CheckinImagesロールバック if backup_data.get('checkin_images'): self.rollback_checkin_images(backup_data['checkin_images']) # 4. レポート生成 report = self.generate_rollback_report() logger.info("✅ ロールバック完了") print("\n✅ ロールバックが完了しました!") return report except Exception as e: logger.error(f"💥 ロールバック中に重大なエラーが発生: {str(e)}") print(f"\n💥 ロールバックエラー: {str(e)}") raise def list_backup_files(): """利用可能なバックアップファイルをリスト表示""" backup_files = [] for file in Path('.').glob('path_update_backup_*.json'): try: with open(file, 'r', encoding='utf-8') as f: backup_data = json.load(f) timestamp = backup_data.get('backup_timestamp', 'Unknown') goal_count = len(backup_data.get('goal_images', [])) checkin_count = len(backup_data.get('checkin_images', [])) backup_files.append({ 'file': str(file), 'timestamp': timestamp, 'goal_count': goal_count, 'checkin_count': checkin_count }) except: continue return backup_files def main(): """メイン関数""" print("="*60) print("🔄 データベースパスロールバックツール") print("="*60) # バックアップファイルのリスト表示 backup_files = list_backup_files() if not backup_files: print("❌ バックアップファイルが見つかりません。") print(" path_update_backup_*.json ファイルが存在することを確認してください。") return print("利用可能なバックアップファイル:") for i, backup in enumerate(backup_files, 1): print(f"{i}. {backup['file']}") print(f" 日時: {backup['timestamp']}") print(f" GoalImages: {backup['goal_count']:,}件, CheckinImages: {backup['checkin_count']:,}件") print() # バックアップファイル選択 try: choice = input(f"ロールバックするファイルを選択してください [1-{len(backup_files)}]: ").strip() choice_idx = int(choice) - 1 if choice_idx < 0 or choice_idx >= len(backup_files): print("❌ 無効な選択です。") return selected_backup = backup_files[choice_idx]['file'] except (ValueError, KeyboardInterrupt): print("❌ ロールバックをキャンセルしました。") return # 確認プロンプト print(f"\n⚠️ 以下のファイルからロールバックします:") print(f" {selected_backup}") print() print("このロールバックにより、現在のS3パス情報は元のローカルパスに戻ります。") confirm = input("ロールバックを実行しますか? [y/N]: ").strip().lower() if confirm not in ['y', 'yes']: print("ロールバックをキャンセルしました。") return # ロールバック実行 rollback_service = PathRollbackService(selected_backup) rollback_service.run_rollback() if __name__ == "__main__": main()