Finish basic API implementation
This commit is contained in:
424
rog/multi_image_upload_views.py
Normal file
424
rog/multi_image_upload_views.py
Normal file
@ -0,0 +1,424 @@
|
||||
"""
|
||||
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
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user