be able to upload and view images

This commit is contained in:
2024-11-17 12:26:23 +11:00
parent f8abdfee75
commit bbda32887e
7 changed files with 132 additions and 35 deletions
+20 -1
View File
@@ -223,7 +223,7 @@ MEDIA_ROOT = os.path.join(BASE_DIR, 'data', 'media')
# Ensure media files are served in all environments
SERVE_MEDIA = True
CELERY_RESULT_BACKEND
# Celery Beat settings
CELERYBEAT_SCHEDULE_FILENAME = os.path.join(BASE_DIR, 'data', 'celerybeat-schedule')
@@ -232,3 +232,22 @@ R2_ENDPOINT_URL = os.environ.get('R2_ENDPOINT_URL')
R2_ACCESS_KEY_ID = os.environ.get('R2_ACCESS_KEY_ID')
R2_SECRET_ACCESS_KEY = os.environ.get('R2_SECRET_ACCESS_KEY')
R2_BUCKET_NAME = os.environ.get('R2_BUCKET_NAME')
# For debugging
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'level': 'DEBUG',
},
},
'loggers': {
'links': {
'handlers': ['console'],
'level': 'DEBUG',
'propagate': True,
},
},
}
BIN
View File
Binary file not shown.
+2
View File
@@ -16,6 +16,8 @@ services:
- ./data/celery:/app/data/celery
ports:
- "8000:8000"
env_file:
- .env
environment:
- DJANGO_SETTINGS_MODULE=core.settings
- CELERY_BROKER_URL=redis://redis:6379/0
+28 -2
View File
@@ -5,6 +5,9 @@ from .models import ImageCollection, Image
from .serializers import ImageCollectionSerializer, ImageSerializer
from .storage import R2Storage
import uuid
import logging
import os
logger = logging.getLogger(__name__)
class ImageCollectionViewSet(viewsets.ModelViewSet):
queryset = ImageCollection.objects.all()
@@ -12,18 +15,38 @@ class ImageCollectionViewSet(viewsets.ModelViewSet):
@action(detail=True, methods=['post'])
def upload_images(self, request, pk=None):
"""API endpoint to upload images to a collection"""
collection = self.get_object()
files = request.FILES.getlist('files')
files = request.FILES.getlist('file')
storage = R2Storage()
logger.debug(f"Processing upload request for collection {collection.id}")
logger.debug(f"Number of files: {len(files)}")
uploaded_images = []
for file in files:
try:
logger.info(os.environ.get('R2_ENDPOINT_URL'))
logger.debug(f"Processing file: {file.name}")
logger.debug(f"File size: {file.size}")
logger.debug(f"Content type: {file.content_type}")
# Generate unique file key
file_key = f"images/{collection.id}/{uuid.uuid4()}/{file.name}"
# Validate file
if not hasattr(file, 'read'):
raise ValueError("Invalid file object - missing read method")
if not file.content_type.startswith('image/'):
raise ValueError("Invalid file type. Only images are allowed.")
# Upload to R2
storage.upload_file(file, file_key, file.content_type)
try:
storage.upload_file(file, file_key, file.content_type)
except Exception as e:
logger.error(f"Storage upload failed", exc_info=True)
raise ValueError(f"Storage upload failed: {str(e)}")
# Create image record
image = Image.objects.create(
@@ -34,8 +57,11 @@ class ImageCollectionViewSet(viewsets.ModelViewSet):
size=file.size
)
logger.debug(f"Created image record: {image.id}")
uploaded_images.append(ImageSerializer(image).data)
except Exception as e:
logger.error(f"Upload failed for {file.name}", exc_info=True)
return Response({
'error': str(e)
}, status=status.HTTP_400_BAD_REQUEST)
+9
View File
@@ -7,6 +7,8 @@ from django.utils import timezone
from django.conf import settings
import os
import uuid
from links.storage import R2Storage
from datetime import timedelta
logger = logging.getLogger(__name__)
@@ -272,3 +274,10 @@ class Image(models.Model):
def __str__(self):
return self.title
def get_url(self, expires_in=3600):
"""Get a signed URL for the image that expires after the specified time"""
if not self.file_key:
return None
storage = R2Storage()
return storage.get_url(self.file_key, expires_in=expires_in)
+56 -25
View File
@@ -1,9 +1,14 @@
import boto3
from django.conf import settings
from botocore.config import Config
import logging
from django.core.files.base import File
logger = logging.getLogger(__name__)
class R2Storage:
def __init__(self):
logger.debug(f"Initializing R2Storage with endpoint: {settings.R2_ENDPOINT_URL}")
self.client = boto3.client('s3',
endpoint_url=settings.R2_ENDPOINT_URL,
aws_access_key_id=settings.R2_ACCESS_KEY_ID,
@@ -12,35 +17,61 @@ class R2Storage:
region_name='auto'
)
self.bucket = settings.R2_BUCKET_NAME
logger.debug(f"Using bucket: {self.bucket}")
def upload_file(self, file_obj, key, content_type=None):
"""Upload a file to R2"""
extra_args = {}
if content_type:
extra_args['ContentType'] = content_type
try:
logger.debug(f"Starting file upload: {key}")
logger.debug(f"File object type: {type(file_obj)}")
self.client.upload_fileobj(
file_obj,
self.bucket,
key,
ExtraArgs=extra_args
)
return key
# Reset file pointer and read content
file_obj.seek(0)
file_content = file_obj.read()
def delete_file(self, key):
"""Delete a file from R2"""
self.client.delete_object(
Bucket=self.bucket,
Key=key
)
logger.debug(f"File content size: {len(file_content) if file_content else 'None'}")
if not file_content:
raise ValueError("File content is empty")
from io import BytesIO
file_data = BytesIO(file_content)
extra_args = {
'ContentType': content_type or 'application/octet-stream',
}
logger.debug(f"Uploading to R2 with extra args: {extra_args}")
self.client.upload_fileobj(
file_data,
self.bucket,
key,
ExtraArgs=extra_args
)
logger.debug(f"Successfully uploaded file: {key}")
return key
except Exception as e:
logger.error(f"Upload failed: {str(e)}", exc_info=True)
raise
def get_url(self, key, expires_in=3600):
"""Generate a presigned URL for the file"""
return self.client.generate_presigned_url(
'get_object',
Params={
'Bucket': self.bucket,
'Key': key
},
ExpiresIn=expires_in
)
"""Generate a signed URL that expires after the specified time"""
if not key:
return None
try:
url = self.client.generate_presigned_url(
'get_object',
Params={
'Bucket': self.bucket,
'Key': key
},
ExpiresIn=expires_in # URL expires in 1 hour by default
)
logger.debug(f"Generated signed URL for {key}")
return url
except Exception as e:
logger.error(f"Failed to generate signed URL: {str(e)}")
return None
+17 -7
View File
@@ -150,14 +150,15 @@
<script>
Dropzone.autoDiscover = false;
new Dropzone("#uploadForm", {
const dropzone = new Dropzone("#uploadForm", {
url: `/api/collections/{{ collection.pk }}/upload_images`,
headers: {
'X-CSRFToken': getCookie('csrftoken')
},
paramName: "file",
acceptedFiles: 'image/*',
parallelUploads: 4,
maxFilesize: 10, // MB
maxFilesize: 10,
dictDefaultMessage: `
<div class="text-center">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -169,11 +170,20 @@ new Dropzone("#uploadForm", {
</p>
</div>
`,
success: function(file, response) {
location.reload();
},
error: function(file, errorMessage) {
alert(errorMessage);
init: function() {
this.on("success", function(file, response) {
console.log("Upload successful:", response);
});
this.on("error", function(file, errorMessage) {
console.error("Upload error:", errorMessage);
alert(typeof errorMessage === 'string' ? errorMessage : errorMessage.error || 'Upload failed');
});
this.on("queuecomplete", function() {
console.log("All uploads completed");
setTimeout(() => {
location.reload();
}, 1000);
});
}
});