Files
rogaining_srv/rog/utils.py
Akira e0635936fe Add missing S3Bucket class to fix import error
- Implement S3Bucket class in utils.py for legacy compatibility
- Add upload_file and get_file_url methods
- Fix ImportError that was preventing app startup
2025-09-03 21:48:22 +09:00

440 lines
17 KiB
Python
Executable File
Raw Permalink 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.

import os
from botocore.exceptions import ClientError
from django.template.loader import render_to_string
from django.conf import settings
import logging
import boto3
from django.core.mail import send_mail
from django.urls import reverse
import uuid
import environ
logger = logging.getLogger(__name__)
def load_email_template(template_name, context):
template_path = os.path.join('email', template_name)
email_content = render_to_string(template_path, context)
# 件名と本文を分離
subject, _, body = email_content.partition('\n\n')
subject = subject.replace('件名: ', '').strip()
# 件名と本文を分離し、件名から改行を削除
subject, _, body = email_content.partition('\n\n')
subject = subject.replace('件名: ', '').strip().replace('\n', ' ')
return subject, body
def share_send_email(subject, body, recipient_email):
try:
send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, [recipient_email], fail_silently=False)
logger.info(f"メールを送信しました。 受信者: {recipient_email}")
except Exception as e:
logger.error(f"メールの送信に失敗しました。 受信者: {recipient_email}, エラー: {str(e)}")
raise # エラーを再度発生させて、呼び出し元で処理できるようにします
# 自らユーザー登録した際に、メールの確認メールを送る。
#
def send_verification_email(user, activation_link):
context = {
'name': user.firstname or user.email,
'activation_link': activation_link,
}
logger.info(f"send_verification_email : {context}")
subject, body = load_email_template('verification_email.txt', context)
share_send_email(subject,body,user.email)
def send_reset_password_email(email,activation_link):
context = {
'name': email,
'activation_link': activation_link,
}
logger.info(f"send_reset_password_email : {context}")
subject, body = load_email_template('reset_password_email.txt', context)
share_send_email(subject,body,email)
# 既にユーザーになっている人にチームへの参加要請メールを出す。
#
def send_team_join_email(request,sender,user,team):
activation_link = request.build_absolute_uri(
reverse('rog:activate-member', args=[user.id, team.id])
)
logger.info(f"request: {request}")
context = {
'name': user.lastname or user.email,
'invitor': sender.lastname,
'activation_link': activation_link,
'team_name': team.team_name,
}
subject, body = load_email_template('invitation_existing_email.txt', context)
share_send_email(subject,body,user.email)
# まだユーザーでない人にチームメンバー招待メールを送る
# その人がユーザー登録して、ユーザー登録されるとメンバーになる。
# アプリからユーザー登録するため、アプリのダウンロードリンクも送る。
#
def send_invitation_email(sender,request,user_email,team):
verification_code = uuid.uuid4() # UUIDを生成
activation_link = request.build_absolute_uri(
reverse('rog:activate-new-member', args=[verification_code, team.id])
)
context = {
'name': user_email,
'invitor': sender.lastname,
'team_name': team.team_name,
'activation_link': activation_link,
'app_download_link': settings.APP_DOWNLOAD_LINK,
'android_download_link': settings.ANDROID_DOWNLOAD_LINK,
}
subject, body = load_email_template('invitation_new_email.txt', context)
share_send_email(subject,body,user_email)
# 招待された後にユーザー登録された場合、ヴェリフィケーションでチーム参加登録される。
#
def send_invitaion_and_verification_email(user, team, activation_link):
context = {
'name': user.firstname or user.email,
'activation_link': activation_link,
'team_name': team.team_name,
}
logger.info(f"send_invitation_and_verification_email : {context}")
subject, body = load_email_template('invitation_and_verification_email.txt', context)
share_send_email(subject,body,user.email)
class S3Bucket:
def __init__(self, bucket_name=None, aws_access_key_id=None, aws_secret_access_key=None, region_name=None):
self.aws_access_key_id = aws_access_key_id
self.aws_secret_access_key = aws_secret_access_key
self.region_name = region_name
self.bucket_name = bucket_name
self.s3_client = self.connect(bucket_name,aws_access_key_id, aws_secret_access_key, region_name)
def __str__(self):
return f"s3://{self.bucket_name}"
def __repr__(self):
return f"S3File(bucket_name={self.bucket_name})"
# AWS S3 への接続
def connect(self,bucket_name=None, aws_access_key_id=None, aws_secret_access_key=None, region_name=None):
"""
S3クライアントの作成
Args: .env から取得
aws_access_key_id (str, optional): AWSアクセスキーID
aws_secret_access_key (str, optional): AWSシークレットアクセスキー
region_name (str): AWSリージョン名
Returns:
boto3.client: S3クライアント
"""
try:
if aws_access_key_id and aws_secret_access_key:
s3_client = boto3.client(
's3',
aws_access_key_id=aws_access_key_id,
aws_secret_access_key=aws_secret_access_key,
region_name=region_name
)
else:
env = environ.Env(DEBUG=(bool, False))
environ.Env.read_env(env_file=".env")
if bucket_name==None:
bucket_name = env("S3_BUCKET_NAME")
aws_access_key_id = env("AWS_ACCESS_KEY")
aws_secret_access_key = env("AWS_SECRET_ACCESS_KEY")
region_name = env("S3_REGION")
s3_client = boto3.client(
's3',
aws_access_key_id=aws_access_key_id,
aws_secret_access_key=aws_secret_access_key,
region_name=region_name
)
return s3_client
except Exception as e:
logger.error(f"S3クライアントの作成に失敗しました: {str(e)}")
raise
def upload_file(self, file_path, s3_key=None):
"""
ファイルをS3バケットにアップロード
Args:
file_path (str): アップロードするローカルファイルのパス
bucket_name (str): アップロード先のS3バケット名
s3_key (str, optional): S3内でのファイルパス指定がない場合はファイル名を使用
s3_client (boto3.client, optional): S3クライアント
Returns:
bool: アップロードの成功・失敗
"""
logger = logging.getLogger(__name__)
try:
# S3キーが指定されていない場合は、ファイル名を使用
if s3_key is None:
s3_key = os.path.basename(file_path)
# S3クライアントが指定されていない場合は新規作成
if self.s3_client is None:
self.s3_client = self.connect()
# ファイルのアップロード
logger.info(f"アップロード開始: {file_path} → s3://{self.bucket_name}/{s3_key}")
self.s3_client.upload_file(file_path, self.bucket_name, s3_key)
logger.info("アップロード完了")
return True
except FileNotFoundError:
logger.error(f"ファイルが見つかりません: {file_path}")
return False
except ClientError as e:
logger.error(f"S3アップロードエラー: {str(e)}")
return False
except Exception as e:
logger.error(f"予期しないエラーが発生しました: {str(e)}")
return False
def upload_directory(self, directory_path, prefix=''):
"""
ディレクトリ内のすべてのファイルをS3バケットにアップロード
Args:
directory_path (str): アップロードするローカルディレクトリのパス
bucket_name (str): アップロード先のS3バケット名
prefix (str, optional): S3内でのプレフィックスフォルダパス
s3_client (boto3.client, optional): S3クライアント
Returns:
tuple: (成功したファイル数, 失敗したファイル数)
"""
logger = logging.getLogger(__name__)
success_count = 0
failure_count = 0
try:
# S3クライアントが指定されていない場合は新規作成
if self.s3_client is None:
self.s3_client = self.connect()
# ディレクトリ内のすべてのファイルを処理
for root, _, files in os.walk(directory_path):
for file in files:
local_path = os.path.join(root, file)
# S3キーの作成相対パスを維持
relative_path = os.path.relpath(local_path, directory_path)
s3_key = os.path.join(prefix, relative_path).replace('\\', '/')
# ファイルのアップロード
if self.upload_file(local_path, s3_key):
success_count += 1
else:
failure_count += 1
logger.info(f"アップロード完了: 成功 {success_count} 件, 失敗 {failure_count}")
return success_count, failure_count
except Exception as e:
logger.error(f"ディレクトリのアップロードに失敗しました: {str(e)}")
return success_count, failure_count
def download_file(self, s3_key, file_path):
"""
S3バケットからファイルをダウンロード
Args:
bucket_name (str): ダウンロード元のS3バケット名
s3_key (str): ダウンロードするファイルのS3キー
file_path (str): ダウンロード先のローカルファイルパス
s3_client (boto3.client, optional): S3クライアント
Returns:
bool: ダウンロードの成功・失敗
"""
logger = logging.getLogger(__name__)
try:
# S3クライアントが指定されていない場合は新規作成
if self.s3_client is None:
self.s3_client = self.connect_to_s3()
# ファイルのダウンロード
logger.info(f"ダウンロード開始: s3://{self.bucket_name}/{s3_key}{file_path}")
self.s3_client.download_file(self.bucket_name, s3_key, file_path)
logger.info("ダウンロード完了")
return True
except FileNotFoundError:
logger.error(f"ファイルが見つかりません: s3://{self.bucket_name}/{s3_key}")
return False
except ClientError as e:
logger.error(f"S3ダウンロードエラー: {str(e)}")
return False
except Exception as e:
logger.error(f"予期しないエラーが発生しました: {str(e)}")
return False
def download_directory(self, prefix, directory_path):
"""
S3バケットからディレクトリをダウンロード
Args:
bucket_name (str): ダウンロード元のS3バケット名
prefix (str): ダウンロードするディレクトリのプレフィックス(フォルダパス)
directory_path (str): ダウンロード先のローカルディレクトリパス
s3_client (boto3.client, optional): S3クライアント
Returns:
tuple: (成功したファイル数, 失敗したファイル数)
"""
logger = logging.getLogger(__name__)
success_count = 0
failure_count = 0
try:
# S3クライアントが指定されていない場合は新規作成
if self.s3_client is None:
self.s3_client = self.connect()
# プレフィックスに一致するオブジェクトをリスト
paginator = self.s3_client.get_paginator('list_objects_v2')
pages = paginator.paginate(Bucket=self.bucket_name, Prefix=prefix)
for page in pages:
if 'Contents' in page:
for obj in page['Contents']:
s3_key = obj['Key']
relative_path = os.path.relpath(s3_key, prefix)
local_path = os.path.join(directory_path, relative_path)
# ローカルディレクトリが存在しない場合は作成
local_dir = os.path.dirname(local_path)
if not os.path.exists(local_dir):
os.makedirs(local_dir)
# ファイルのダウンロード
if self.download_file(self.bucket_name, s3_key, local_path):
success_count += 1
else:
failure_count += 1
logger.info(f"ダウンロード完了: 成功 {success_count} 件, 失敗 {failure_count}")
return success_count, failure_count
except Exception as e:
logger.error(f"ディレクトリのダウンロードに失敗しました: {str(e)}")
return success_count, failure_count
def delete_object(self, s3_key):
"""
S3バケットからオブジェクトを削除
Args:
bucket_name (str): 削除するオブジェクトが存在するS3バケット名
s3_key (str): 削除するオブジェクトのS3キー
s3_client (boto3.client, optional): S3クライアント
Returns:
bool: 削除の成功・失敗
"""
logger = logging.getLogger(__name__)
try:
# S3クライアントが指定されていない場合は新規作成
if self.s3_client is None:
self.s3_client = self.connect()
# オブジェクトの削除
logger.info(f"削除開始: s3://{self.bucket_name}/{s3_key}")
self.s3_client.delete_object(Bucket=self.bucket_name, Key=s3_key)
logger.info("削除完了")
return True
except ClientError as e:
logger.error(f"S3削除エラー: {str(e)}")
return False
except Exception as e:
logger.error(f"予期しないエラーが発生しました: {str(e)}")
return False
class S3Bucket:
"""
レガシーS3Bucketクラス - 既存コードとの互換性のため
"""
def __init__(self, bucket_name):
self.bucket_name = bucket_name
try:
self.s3_client = boto3.client(
's3',
aws_access_key_id=getattr(settings, 'AWS_ACCESS_KEY', None),
aws_secret_access_key=getattr(settings, 'AWS_SECRET_ACCESS_KEY', None),
region_name=getattr(settings, 'AWS_REGION', 'us-west-2')
)
logger.info(f"S3Bucket initialized for bucket: {self.bucket_name}")
except Exception as e:
logger.error(f"Failed to initialize S3Bucket: {e}")
self.s3_client = None
def upload_file(self, local_file_path, s3_key):
"""
ローカルファイルをS3にアップロード
Args:
local_file_path: アップロードするローカルファイルのパス
s3_key: S3内でのキーファイルパス
Returns:
bool: 成功時True、失敗時False
"""
if not self.s3_client:
logger.error("S3 client not initialized")
return False
try:
self.s3_client.upload_file(
local_file_path,
self.bucket_name,
s3_key,
ExtraArgs={'ACL': 'public-read'}
)
logger.info(f"Successfully uploaded {local_file_path} to s3://{self.bucket_name}/{s3_key}")
return True
except Exception as e:
logger.error(f"Failed to upload {local_file_path} to S3: {e}")
return False
def get_file_url(self, s3_key):
"""
S3ファイルのパブリックURLを生成
Args:
s3_key: S3内でのキーファイルパス
Returns:
str: ファイルのURL
"""
aws_region = getattr(settings, 'AWS_REGION', 'us-west-2')
return f"https://{self.bucket_name}.s3.{aws_region}.amazonaws.com/{s3_key}"