diff --git a/config/urls.py b/config/urls.py index 6c7c519..fb9ec04 100644 --- a/config/urls.py +++ b/config/urls.py @@ -20,7 +20,7 @@ from django.conf import settings from django.conf.urls.static import static from django.conf.urls.i18n import i18n_patterns from django.contrib.admin.views.decorators import staff_member_required -from utils import UploadTmpMedia +from utils import UploadTmpMedia, UploadChatMedia from django.http import JsonResponse from django.shortcuts import render from django.views.decorators.csrf import csrf_exempt @@ -87,7 +87,8 @@ api_patterns = [ path('settings/', include('dynamic_preferences.urls')), - path('upload-tmp-media/', UploadTmpMedia.as_view()), + path('upload-chat-media/', UploadChatMedia.as_view()), # دائمی در /media/chat/ + path('upload-tmp-media/', UploadTmpMedia.as_view()), # موقت در /static/tmp/ ] diff --git a/utils/__init__.py b/utils/__init__.py index d5ab321..21f2d64 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -263,6 +263,83 @@ class FileFieldSerializer(serializers.CharField): return File(open(fpath, 'rb'), os.path.basename(data)) +class UploadChatMediaSerializer(serializers.Serializer): + """Upload files permanently to /media/chat/""" + file = serializers.FileField() + url = serializers.URLField(read_only=True) + name = serializers.CharField(read_only=True) + size = serializers.CharField(read_only=True) + mime_type = serializers.CharField(read_only=True) + thumbnail_url = serializers.URLField(read_only=True, required=False) + + def to_representation(self, instance): + data = super(UploadChatMediaSerializer, self).to_representation(instance) + data['file'] = instance['file'] + return data + + def store_file(self, file): + from django.conf import settings + from utils.image_utils import ( + create_thumbnail, + is_image_file, + is_video_file, + extract_video_thumbnail + ) + media_path = settings.MEDIA_ROOT + + os.makedirs(f'{media_path}/chat/uploads', exist_ok=True) + fpath = f"/chat/uploads/{secrets.token_urlsafe(4)}-{file.name}" + full_path = media_path + fpath + shutil.move(file.temporary_file_path(), full_path) + os.chmod(full_path, 0o644) + + result = { + 'file': fpath, + 'url': absolute_url(self.context['request'], f"/media{fpath}"), + 'name': file.name, + 'size': sizeof_fmt(file.size), + 'mime_type': guess_file_type(fpath) + } + + # Generate thumbnail if file is an image (low quality for preview) + if is_image_file(full_path): + try: + thumbnail_path = create_thumbnail(full_path, size=(200, 200), quality=60) + thumbnail_relative = thumbnail_path.replace(media_path, '') + result['thumbnail_url'] = absolute_url( + self.context['request'], + f"/media{thumbnail_relative}" + ) + except Exception as e: + print(f"Failed to generate image thumbnail: {e}") + result['thumbnail_url'] = None + # Generate thumbnail if file is a video (low quality for preview) + elif is_video_file(full_path): + try: + thumbnail_path = extract_video_thumbnail( + full_path, + time_offset='00:00:01', + size=(200, 200), + quality=60 + ) + thumbnail_relative = thumbnail_path.replace(media_path, '') + result['thumbnail_url'] = absolute_url( + self.context['request'], + f"/media{thumbnail_relative}" + ) + except Exception as e: + print(f"Failed to generate video thumbnail: {e}") + result['thumbnail_url'] = None + else: + result['thumbnail_url'] = None + + return result + + def validate(self, attrs): + file_details = self.store_file(attrs['file']) + return file_details + + class UploadTmpSerializer(serializers.Serializer): file = serializers.FileField() url = serializers.URLField(read_only=True) @@ -300,37 +377,33 @@ class UploadTmpSerializer(serializers.Serializer): 'mime_type': guess_file_type(fpath) } - # Generate thumbnail if file is an image + # Generate thumbnail if file is an image (low quality for preview) if is_image_file(full_path): try: - thumbnail_path = create_thumbnail(full_path, size=(300, 300), quality=85) - # Convert absolute path to relative URL path + thumbnail_path = create_thumbnail(full_path, size=(200, 200), quality=60) thumbnail_relative = thumbnail_path.replace(static_path, '') result['thumbnail_url'] = absolute_url( self.context['request'], f"/static{thumbnail_relative}" ) except Exception as e: - # Log error but don't fail the upload print(f"Failed to generate image thumbnail: {e}") result['thumbnail_url'] = None - # Generate thumbnail if file is a video + # Generate thumbnail if file is a video (low quality for preview) elif is_video_file(full_path): try: thumbnail_path = extract_video_thumbnail( full_path, - time_offset='00:00:01', # Extract frame at 1 second - size=(300, 300), - quality=85 + time_offset='00:00:01', + size=(200, 200), + quality=60 ) - # Convert absolute path to relative URL path thumbnail_relative = thumbnail_path.replace(static_path, '') result['thumbnail_url'] = absolute_url( self.context['request'], f"/static{thumbnail_relative}" ) except Exception as e: - # Log error but don't fail the upload print(f"Failed to generate video thumbnail: {e}") result['thumbnail_url'] = None else: @@ -343,9 +416,27 @@ class UploadTmpSerializer(serializers.Serializer): return file_details +class UploadChatMedia(GenericAPIView): + """ + Upload files permanently to /media/chat/ + Files are stored permanently and will NOT be removed + """ + parser_classes = (FormParser, MultiPartParser) + serializer_class = UploadChatMediaSerializer + + def post(self, request: HttpRequest, *args, **kwargs): + serializer = UploadChatMediaSerializer(data=request.FILES, context={'request': request}) + is_valid = serializer.is_valid(raise_exception=True) + if not is_valid: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + return Response(serializer.data) + + class UploadTmpMedia(GenericAPIView): """ - Files will remove every 1 hour + Upload files temporarily to /static/tmp/ + Files will be removed every 1 hour """ parser_classes = (FormParser, MultiPartParser) serializer_class = UploadTmpSerializer