Files
rogaining_srv/rog/multi_image_upload_views.py
2025-08-27 15:01:06 +09:00

425 lines
14 KiB
Python
Raw 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.

"""
Multi Image Upload API Views
複数画像一括アップロード機能
"""
import os
import base64
import uuid
import time
import logging
from datetime import datetime
from django.conf import settings
from django.core.files.base import ContentFile
from django.core.files.storage import default_storage
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from django.db import transaction
from PIL import Image
import io
from .models import UploadedImage, NewEvent2, Entry
from .serializers import (
MultiImageUploadSerializer,
MultiImageUploadResponseSerializer,
UploadedImageSerializer
)
logger = logging.getLogger(__name__)
@api_view(['POST'])
@permission_classes([AllowAny])
def multi_image_upload(request):
"""
複数画像一括アップロードAPI
POST /api/images/multi-upload
Request:
{
"event_code": "岐阜ロゲイニング2025",
"team_name": "チーム名",
"cp_number": 1,
"images": [
{
"file_data": "base64_encoded_image_data",
"filename": "checkpoint1_photo1.jpg",
"mime_type": "image/jpeg",
"file_size": 2048576,
"capture_timestamp": "2025-09-15T11:30:00Z"
}
],
"upload_source": "sharing_intent",
"device_platform": "ios"
}
"""
start_time = time.time()
try:
# リクエストデータ検証
serializer = MultiImageUploadSerializer(data=request.data)
if not serializer.is_valid():
return Response({
'status': 'error',
'message': 'Invalid request data',
'errors': serializer.errors
}, status=status.HTTP_400_BAD_REQUEST)
validated_data = serializer.validated_data
event_code = validated_data['event_code']
team_name = validated_data['team_name']
cp_number = validated_data['cp_number']
images_data = validated_data['images']
upload_source = validated_data.get('upload_source', 'direct')
device_platform = validated_data.get('device_platform')
# イベントの存在確認
try:
event = NewEvent2.objects.get(event_name=event_code)
except NewEvent2.DoesNotExist:
return Response({
'status': 'error',
'message': f'イベント "{event_code}" が見つかりません'
}, status=status.HTTP_404_NOT_FOUND)
# エントリーの存在確認
try:
entry = Entry.objects.filter(
event=event,
team__team_name=team_name
).first()
except Entry.DoesNotExist:
entry = None
uploaded_files = []
failed_files = []
total_upload_size = 0
# トランザクション開始
with transaction.atomic():
for i, image_data in enumerate(images_data):
try:
uploaded_image = _process_single_image(
image_data,
event_code,
team_name,
cp_number,
upload_source,
device_platform,
entry,
i
)
uploaded_files.append({
'original_filename': uploaded_image.original_filename,
'server_filename': uploaded_image.server_filename,
'file_url': uploaded_image.file_url,
'file_size': uploaded_image.file_size
})
total_upload_size += uploaded_image.file_size
except Exception as e:
logger.error(f"Failed to process image {i}: {e}")
failed_files.append({
'filename': image_data.get('filename', f'image_{i}'),
'error': str(e)
})
# 処理時間計算
processing_time_ms = int((time.time() - start_time) * 1000)
# レスポンス作成
response_data = {
'status': 'success' if not failed_files else 'partial_success',
'uploaded_count': len(uploaded_files),
'failed_count': len(failed_files),
'uploaded_files': uploaded_files,
'failed_files': failed_files,
'total_upload_size': total_upload_size,
'processing_time_ms': processing_time_ms
}
if failed_files:
response_data['message'] = f"{len(uploaded_files)}個のファイルがアップロードされ、{len(failed_files)}個が失敗しました"
else:
response_data['message'] = f"{len(uploaded_files)}個のファイルが正常にアップロードされました"
return Response(response_data, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"Multi image upload error: {e}")
return Response({
'status': 'error',
'message': 'サーバーエラーが発生しました',
'error_details': str(e)
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def image_list(request):
"""
アップロード済み画像一覧取得
GET /api/images/list/
Parameters:
- entry_id: エントリーIDオプション
- event_code: イベントコード(オプション)
- limit: 取得数上限デフォルト50
- offset: オフセットデフォルト0
"""
try:
entry_id = request.GET.get('entry_id')
event_code = request.GET.get('event_code')
limit = int(request.GET.get('limit', 50))
offset = int(request.GET.get('offset', 0))
# 基本クエリ
queryset = UploadedImage.objects.all()
# フィルタリング
if entry_id:
queryset = queryset.filter(entry_id=entry_id)
if event_code:
queryset = queryset.filter(entry__event_name=event_code)
# 並び順と取得数制限
queryset = queryset.order_by('-uploaded_at')[offset:offset+limit]
# シリアライズ
serializer = UploadedImageSerializer(queryset, many=True)
return Response({
'images': serializer.data,
'count': len(serializer.data),
'limit': limit,
'offset': offset
})
except Exception as e:
logger.error(f"Image list error: {e}")
return Response({
'error': 'Failed to get image list',
'message': str(e)
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['GET', 'DELETE'])
@permission_classes([IsAuthenticated])
def image_detail(request, image_id):
"""
画像詳細取得・削除
GET /api/images/{image_id}/ - 画像詳細取得
DELETE /api/images/{image_id}/ - 画像削除
"""
try:
image = UploadedImage.objects.get(id=image_id)
if request.method == 'GET':
serializer = UploadedImageSerializer(image)
return Response(serializer.data)
elif request.method == 'DELETE':
# ファイル削除
if image.image_file and os.path.exists(image.image_file.path):
os.remove(image.image_file.path)
if image.thumbnail and os.path.exists(image.thumbnail.path):
os.remove(image.thumbnail.path)
# データベースレコード削除
image.delete()
return Response({
'message': 'Image deleted successfully'
})
except UploadedImage.DoesNotExist:
return Response({
'error': 'Image not found'
}, status=status.HTTP_404_NOT_FOUND)
except Exception as e:
logger.error(f"Image detail error: {e}")
return Response({
'error': 'Internal server error',
'message': str(e)
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def _process_single_image(image_data, event_code, team_name, cp_number,
upload_source, device_platform, entry, index):
"""単一画像の処理"""
# Base64デコード
try:
if ',' in image_data['file_data']:
# data:image/jpeg;base64,... 形式の場合
file_data = image_data['file_data'].split(',')[1]
else:
file_data = image_data['file_data']
image_binary = base64.b64decode(file_data)
except Exception as e:
raise ValueError(f"Base64デコードに失敗しました: {e}")
# ファイル名生成
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
file_extension = _get_file_extension(image_data['mime_type'])
server_filename = f"{event_code}_{team_name}_cp{cp_number}_{timestamp}_{index:03d}{file_extension}"
# ディレクトリ作成
upload_dir = f"uploads/{datetime.now().strftime('%Y/%m/%d')}"
full_upload_dir = os.path.join(settings.MEDIA_ROOT, upload_dir)
os.makedirs(full_upload_dir, exist_ok=True)
# ファイル保存
file_path = os.path.join(upload_dir, server_filename)
full_file_path = os.path.join(settings.MEDIA_ROOT, file_path)
with open(full_file_path, 'wb') as f:
f.write(image_binary)
# ファイルURL生成
file_url = f"{settings.MEDIA_URL}{file_path}"
# HEICからJPEGへの変換iOS対応
if image_data['mime_type'] == 'image/heic' and device_platform == 'ios':
try:
file_url, server_filename = _convert_heic_to_jpeg(full_file_path, file_path)
except Exception as e:
logger.warning(f"HEIC conversion failed: {e}")
# サムネイル生成
thumbnail_url = _generate_thumbnail(full_file_path, file_path)
# データベース保存
uploaded_image = UploadedImage.objects.create(
original_filename=image_data['filename'],
server_filename=server_filename,
file_url=file_url,
file_size=image_data['file_size'],
mime_type=image_data['mime_type'],
event_code=event_code,
team_name=team_name,
cp_number=cp_number,
upload_source=upload_source,
device_platform=device_platform,
capture_timestamp=image_data.get('capture_timestamp'),
device_info=image_data.get('device_info'),
processing_status='processed',
thumbnail_url=thumbnail_url,
entry=entry
)
return uploaded_image
def _get_file_extension(mime_type):
"""MIMEタイプからファイル拡張子を取得"""
mime_to_ext = {
'image/jpeg': '.jpg',
'image/png': '.png',
'image/heic': '.heic',
'image/webp': '.webp'
}
return mime_to_ext.get(mime_type, '.jpg')
def _convert_heic_to_jpeg(heic_path, original_path):
"""HEICファイルをJPEGに変換"""
try:
# PIL-HEICライブラリが必要要インストール
from PIL import Image
# HEICファイルを開いてJPEGで保存
with Image.open(heic_path) as img:
jpeg_path = heic_path.replace('.heic', '.jpg')
rgb_img = img.convert('RGB')
rgb_img.save(jpeg_path, 'JPEG', quality=85)
# 元のHEICファイルを削除
os.remove(heic_path)
# 新しいファイル情報を返す
new_file_path = original_path.replace('.heic', '.jpg')
new_file_url = f"{settings.MEDIA_URL}{new_file_path}"
new_filename = os.path.basename(new_file_path)
return new_file_url, new_filename
except ImportError:
logger.warning("PIL-HEIC not available, keeping original HEIC file")
return f"{settings.MEDIA_URL}{original_path}", os.path.basename(original_path)
def _generate_thumbnail(image_path, original_path):
"""サムネイル画像生成"""
try:
with Image.open(image_path) as img:
# サムネイルサイズ300x300
img.thumbnail((300, 300), Image.Resampling.LANCZOS)
# サムネイルファイル名
path_parts = original_path.split('.')
thumbnail_path = f"{'.'.join(path_parts[:-1])}_thumb.{path_parts[-1]}"
thumbnail_full_path = os.path.join(settings.MEDIA_ROOT, thumbnail_path)
# サムネイル保存
img.save(thumbnail_full_path, quality=75)
return f"{settings.MEDIA_URL}{thumbnail_path}"
except Exception as e:
logger.warning(f"Thumbnail generation failed: {e}")
return None
@api_view(['GET'])
@permission_classes([AllowAny])
def uploaded_images_list(request):
"""アップロード済み画像一覧取得"""
event_code = request.GET.get('event_code')
team_name = request.GET.get('team_name')
cp_number = request.GET.get('cp_number')
queryset = UploadedImage.objects.all().order_by('-upload_timestamp')
# フィルタリング
if event_code:
queryset = queryset.filter(event_code=event_code)
if team_name:
queryset = queryset.filter(team_name=team_name)
if cp_number:
queryset = queryset.filter(cp_number=cp_number)
# ページネーション50件ずつ
page = int(request.GET.get('page', 1))
page_size = 50
start_index = (page - 1) * page_size
end_index = start_index + page_size
total_count = queryset.count()
images = queryset[start_index:end_index]
serializer = UploadedImageSerializer(images, many=True)
return Response({
'images': serializer.data,
'pagination': {
'total_count': total_count,
'page': page,
'page_size': page_size,
'has_next': end_index < total_count,
'has_previous': page > 1
}
})