mirror of
https://github.com/wahyd4/links.git
synced 2026-08-09 05:06:16 +10:00
be able to upload and view images
This commit is contained in:
+20
-1
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user