mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-07-19 21:09:37 +02:00
feat: enhance Immich integration to support image downloading for shared users and improve access control for adventure images
This commit is contained in:
parent
b336a24401
commit
442a7724a0
2 changed files with 125 additions and 14 deletions
|
@ -3,9 +3,14 @@ from rest_framework.decorators import action
|
|||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from django.db.models import Q
|
||||
from django.core.files.base import ContentFile
|
||||
from adventures.models import Adventure, AdventureImage
|
||||
from adventures.serializers import AdventureImageSerializer
|
||||
from integrations.models import ImmichIntegration
|
||||
import uuid
|
||||
import requests
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
class AdventureImageViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = AdventureImageSerializer
|
||||
|
@ -56,6 +61,77 @@ class AdventureImageViewSet(viewsets.ModelViewSet):
|
|||
else:
|
||||
return Response({"error": "User does not own this adventure"}, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
# Handle Immich ID for shared users by downloading the image
|
||||
if (request.user != adventure.user_id and
|
||||
'immich_id' in request.data and
|
||||
request.data.get('immich_id')):
|
||||
|
||||
immich_id = request.data.get('immich_id')
|
||||
|
||||
# Get the shared user's Immich integration
|
||||
try:
|
||||
user_integration = ImmichIntegration.objects.get(user_id=request.user)
|
||||
except ImmichIntegration.DoesNotExist:
|
||||
return Response({
|
||||
"error": "No Immich integration found for your account. Please set up Immich integration first.",
|
||||
"code": "immich_integration_not_found"
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Download the image from the shared user's Immich server
|
||||
try:
|
||||
immich_response = requests.get(
|
||||
f'{user_integration.server_url}/assets/{immich_id}/thumbnail?size=preview',
|
||||
headers={'x-api-key': user_integration.api_key},
|
||||
timeout=10
|
||||
)
|
||||
immich_response.raise_for_status()
|
||||
|
||||
# Create a temporary file with the downloaded content
|
||||
content_type = immich_response.headers.get('Content-Type', 'image/jpeg')
|
||||
if not content_type.startswith('image/'):
|
||||
return Response({
|
||||
"error": "Invalid content type returned from Immich server.",
|
||||
"code": "invalid_content_type"
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Determine file extension from content type
|
||||
ext_map = {
|
||||
'image/jpeg': '.jpg',
|
||||
'image/png': '.png',
|
||||
'image/webp': '.webp',
|
||||
'image/gif': '.gif'
|
||||
}
|
||||
file_ext = ext_map.get(content_type, '.jpg')
|
||||
filename = f"immich_{immich_id}{file_ext}"
|
||||
|
||||
# Create a Django ContentFile from the downloaded image
|
||||
image_file = ContentFile(immich_response.content, name=filename)
|
||||
|
||||
# Modify request data to use the downloaded image instead of immich_id
|
||||
request_data = request.data.copy()
|
||||
request_data.pop('immich_id', None) # Remove immich_id
|
||||
|
||||
# Create the serializer with the modified data
|
||||
serializer = self.get_serializer(data=request_data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
# Save with the downloaded image
|
||||
adventure = serializer.validated_data['adventure']
|
||||
serializer.save(user_id=adventure.user_id, image=image_file)
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
return Response({
|
||||
"error": f"Failed to fetch image from Immich server: {str(e)}",
|
||||
"code": "immich_fetch_failed"
|
||||
}, status=status.HTTP_502_BAD_GATEWAY)
|
||||
except Exception as e:
|
||||
return Response({
|
||||
"error": f"Unexpected error processing Immich image: {str(e)}",
|
||||
"code": "immich_processing_error"
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
return super().create(request, *args, **kwargs)
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
|
@ -110,15 +186,25 @@ class AdventureImageViewSet(viewsets.ModelViewSet):
|
|||
except ValueError:
|
||||
return Response({"error": "Invalid adventure ID"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Updated queryset to include images from adventures the user owns OR has shared access to
|
||||
queryset = AdventureImage.objects.filter(
|
||||
Q(adventure__id=adventure_uuid) & Q(user_id=request.user)
|
||||
Q(adventure__id=adventure_uuid) & (
|
||||
Q(adventure__user_id=request.user) | # User owns the adventure
|
||||
Q(adventure__collection__shared_with=request.user) # User has shared access via collection
|
||||
)
|
||||
).distinct()
|
||||
|
||||
serializer = self.get_serializer(queryset, many=True, context={'request': request})
|
||||
return Response(serializer.data)
|
||||
|
||||
def get_queryset(self):
|
||||
return AdventureImage.objects.filter(user_id=self.request.user)
|
||||
# Updated to include images from adventures the user owns OR has shared access to
|
||||
return AdventureImage.objects.filter(
|
||||
Q(adventure__user_id=self.request.user) | # User owns the adventure
|
||||
Q(adventure__collection__shared_with=self.request.user) # User has shared access via collection
|
||||
).distinct()
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(user_id=self.request.user)
|
||||
# Always set the image owner to the adventure owner, not the current user
|
||||
adventure = serializer.validated_data['adventure']
|
||||
serializer.save(user_id=adventure.user_id)
|
|
@ -232,9 +232,12 @@ class ImmichIntegrationView(viewsets.ViewSet):
|
|||
def get_by_integration(self, request, integration_id=None, imageid=None):
|
||||
"""
|
||||
GET an Immich image using the integration and asset ID.
|
||||
- Public adventures: accessible by anyone
|
||||
- Private adventures: accessible only to the owner
|
||||
- No AdventureImage: owner can still view via integration
|
||||
Access levels (in order of priority):
|
||||
1. Public adventures: accessible by anyone
|
||||
2. Private adventures in public collections: accessible by anyone
|
||||
3. Private adventures in private collections shared with user: accessible by shared users
|
||||
4. Private adventures: accessible only to the owner
|
||||
5. No AdventureImage: owner can still view via integration
|
||||
"""
|
||||
if not imageid or not integration_id:
|
||||
return Response({
|
||||
|
@ -247,22 +250,45 @@ class ImmichIntegrationView(viewsets.ViewSet):
|
|||
integration = get_object_or_404(ImmichIntegration, id=integration_id)
|
||||
owner_id = integration.user_id
|
||||
|
||||
# Try to find the image entry
|
||||
# Try to find the image entry with collection and sharing information
|
||||
image_entry = (
|
||||
AdventureImage.objects
|
||||
.filter(immich_id=imageid, user_id=owner_id)
|
||||
.select_related('adventure')
|
||||
.order_by('-adventure__is_public') # True (1) first, False (0) last
|
||||
.select_related('adventure', 'adventure__collection')
|
||||
.prefetch_related('adventure__collection__shared_with')
|
||||
.order_by(
|
||||
'-adventure__is_public', # Public adventures first
|
||||
'-adventure__collection__is_public' # Then public collections
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
# Access control
|
||||
if image_entry:
|
||||
if image_entry.adventure.is_public:
|
||||
adventure = image_entry.adventure
|
||||
collection = adventure.collection
|
||||
|
||||
# Determine access level
|
||||
is_authorized = False
|
||||
|
||||
# Level 1: Public adventure (highest priority)
|
||||
if adventure.is_public:
|
||||
is_authorized = True
|
||||
|
||||
# Level 2: Private adventure in public collection
|
||||
elif collection and collection.is_public:
|
||||
is_authorized = True
|
||||
|
||||
# Level 3: Owner access
|
||||
elif request.user.is_authenticated and request.user.id == owner_id:
|
||||
is_authorized = True
|
||||
else:
|
||||
|
||||
# Level 4: Shared collection access
|
||||
elif (request.user.is_authenticated and collection and
|
||||
collection.shared_with.filter(id=request.user.id).exists()):
|
||||
is_authorized = True
|
||||
|
||||
if not is_authorized:
|
||||
return Response({
|
||||
'message': 'This image belongs to a private adventure and you are not authorized.',
|
||||
'error': True,
|
||||
|
@ -276,7 +302,6 @@ class ImmichIntegrationView(viewsets.ViewSet):
|
|||
'error': True,
|
||||
'code': 'immich.not_found'
|
||||
}, status=status.HTTP_404_NOT_FOUND)
|
||||
is_authorized = True # Integration owner fallback
|
||||
|
||||
# Fetch from Immich
|
||||
try:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue