""" S3画像アップロードユーティリティ チェックイン時の画像をS3にアップロードし、URLを生成します """ import boto3 import logging import uuid from datetime import datetime from django.conf import settings from botocore.exceptions import ClientError, NoCredentialsError import base64 import io import requests logger = logging.getLogger(__name__) class S3ImageUploader: def __init__(self): """S3クライアントを初期化""" try: # AWS認証情報の確認 aws_access_key = getattr(settings, 'AWS_ACCESS_KEY_ID', None) aws_secret_key = getattr(settings, 'AWS_SECRET_ACCESS_KEY', None) if not aws_access_key or not aws_secret_key: logger.warning("AWS credentials not configured, S3 upload will be disabled") self.s3_client = None self.bucket_name = None return self.s3_client = boto3.client( 's3', aws_access_key_id=aws_access_key, aws_secret_access_key=aws_secret_key, region_name=getattr(settings, 'AWS_S3_REGION_NAME', 'us-west-2') ) self.bucket_name = getattr(settings, 'AWS_STORAGE_BUCKET_NAME', 'sumasenrogaining') logger.info(f"S3 client initialized for bucket: {self.bucket_name}") except Exception as e: logger.error(f"Failed to initialize S3 client: {e}") self.s3_client = None self.bucket_name = None def upload_checkin_image(self, image_data, event_code, zekken_number, cp_number): """ チェックイン画像をS3にアップロード Args: image_data: 画像データ(URLまたはBase64) event_code: イベントコード zekken_number: ゼッケン番号 cp_number: チェックポイント番号 Returns: str: S3のURL、失敗時は元のimage_data """ if not self.s3_client or not image_data: logger.warning("S3 client not available or no image data, returning original") return image_data try: # 画像データを取得 image_binary = self._get_image_binary(image_data) if not image_binary: logger.error("Failed to get image binary data, returning original") return image_data # S3キーを生成: {event_code}/{zekken_number}/{cp_number}.jpg s3_key = f"{event_code}/{zekken_number}/{cp_number}.jpg" # S3にアップロード self.s3_client.put_object( Bucket=self.bucket_name, Key=s3_key, Body=image_binary, ContentType='image/jpeg', ACL='public-read' # 公開読み取り可能 ) # S3 URLを生成 aws_region = getattr(settings, 'AWS_REGION', 'us-west-2') s3_url = f"https://{self.bucket_name}.s3.{aws_region}.amazonaws.com/{s3_key}" logger.info(f"Successfully uploaded image to S3: {s3_url}") return s3_url except ClientError as e: logger.error(f"S3 upload failed: {e}, returning original URL") return image_data except Exception as e: logger.error(f"Unexpected error during S3 upload: {e}, returning original URL") return image_data def _get_image_binary(self, image_data): """ 画像データからバイナリデータを取得 Args: image_data: 画像URL(HTTP)またはBase64エンコードされた画像データ Returns: bytes: 画像のバイナリデータ """ try: if isinstance(image_data, str): # HTTPURLの場合 if image_data.startswith('http'): return self._download_image_from_url(image_data) # Base64の場合 elif self._is_base64(image_data): return base64.b64decode(image_data) # data:image/jpeg;base64,の形式の場合 elif image_data.startswith('data:image'): base64_data = image_data.split(',')[1] return base64.b64decode(base64_data) logger.error(f"Unsupported image data format: {type(image_data)}") return None except Exception as e: logger.error(f"Error processing image data: {e}") return None def _download_image_from_url(self, url): """URLから画像をダウンロード""" try: response = requests.get(url, timeout=30) response.raise_for_status() return response.content except Exception as e: logger.error(f"Failed to download image from URL {url}: {e}") return None def _is_base64(self, data): """文字列がBase64かどうかをチェック""" try: if isinstance(data, str): base64.b64decode(data, validate=True) return True except Exception: return False return False def generate_s3_url(self, event_code, zekken_number, cp_number): """ S3 URLを生成(アップロード済みの画像用) Args: event_code: イベントコード zekken_number: ゼッケン番号 cp_number: チェックポイント番号 Returns: str: S3のURL """ aws_region = getattr(settings, 'AWS_REGION', 'us-west-2') s3_key = f"{event_code}/{zekken_number}/{cp_number}.jpg" return f"https://{self.bucket_name}.s3.{aws_region}.amazonaws.com/{s3_key}" # グローバルインスタンス s3_uploader = S3ImageUploader()