diff --git a/config/settings/base.py b/config/settings/base.py index 955ccf4..c05c034 100755 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -263,8 +263,10 @@ STATIC_URL = '/static/' MEDIA_URL = '/media/' STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')] -STATIC_ROOT = os.path.join(BASE_DIR, 'static', 'static') -MEDIA_ROOT = os.path.join(BASE_DIR, 'static', 'media') +# ********************************************************* +STATIC_ROOT = BASE_DIR/'staticfiles' +MEDIA_ROOT = BASE_DIR/'media' +# os.path.join(BASE_DIR, 'static', 'media') # FILER_ADMIN_ICON_SIZES = ('32', '48') diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 10fc5e8..77f3fed 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -9,7 +9,10 @@ services: dockerfile: Dockerfile.prod command: gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers=4 --timeout 560 volumes: - - static_volume:/usr/src/app/static + - static_volume:/usr/src/app/staticfiles + # /usr/src/app/static + - media_volume:/usr/src/app/media + - logs_volume:/usr/src/app/logs ports: - "8010:8000" env_file: @@ -77,7 +80,10 @@ services: volumes: postgres_data: - static_volume: + staticfiles_volume: + media_volume: + # static_volume: + logs_volume: redis_data: networks: diff --git a/utils/__init__.py b/utils/__init__.py index 21f2d64..4ad546c 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -5,6 +5,11 @@ import mimetypes import re from urllib.parse import urlparse +from django.core.files.storage import default_storage +from django.core.files.base import ContentFile + +from pathlib import Path +from django.utils.text import get_valid_filename from django.conf import settings from django.core.files import File from django.http import HttpRequest @@ -198,6 +203,26 @@ def sizeof_fmt(num, suffix="B"): num /= 1024.0 return f"{num:.1f} Yi{suffix}" +def file_location_media(path: str): + """ + Resolve a media URL/relative path to absolute filesystem path under MEDIA_ROOT. + """ + from django.conf import settings + + media_url = (getattr(settings, "MEDIA_URL", "/media/") or "/media/").rstrip("/") + media_root = settings.MEDIA_ROOT + + if path.startswith("http"): + path = exclude_host_from_url(path) + + if path.startswith(media_url + "/"): + path = path[len(media_url):] + + if path.startswith("/"): + path = path[1:] + + return os.path.join(media_root, path) + def file_location(path): from django.conf import settings @@ -253,14 +278,16 @@ class FileFieldSerializer(serializers.CharField): # value not changed and here we simply return old file path return self.get_rpath(data) - if data.startswith('http'): - data = self.get_rpath(data) + # if data.startswith('http'): + # data = self.get_rpath(data) - fpath = file_location(data) + fpath = file_location_media(data) if not os.path.exists(fpath): raise serializers.ValidationError(f"File: '{fpath}' Does not exist") - return File(open(fpath, 'rb'), os.path.basename(data)) + rel = os.path.basename(data) + return File(open(fpath, "rb"), rel) + # return File(open(fpath, 'rb'), os.path.basename(data)) class UploadChatMediaSerializer(serializers.Serializer): @@ -277,62 +304,137 @@ class UploadChatMediaSerializer(serializers.Serializer): 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 + # if hasattr(file, 'read'): + # default_storage.save(str(full_path), ContentFile(file.read())) + # else: + # default_storage.save(str(full_path), file) + # 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 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 + 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) + media_root = Path(settings.MEDIA_ROOT) # Path object + chat_dir = media_root / "chat" / "uploads" + chat_dir.mkdir(parents=True, exist_ok=True) + + safe_name = get_valid_filename(os.path.basename(file.name)) + rel_path = Path("chat") / "uploads" / f"{secrets.token_urlsafe(4)}-{safe_name}" + full_path = media_root / rel_path # Path object + + # Save via default_storage (path must be relative to MEDIA_ROOT) + if hasattr(file, "read"): + default_storage.save(str(rel_path), ContentFile(file.read())) + else: + default_storage.save(str(rel_path), file) + + # Optional: chmod only if local filesystem and OS supports it + try: + os.chmod(full_path, 0o644) + except PermissionError: + pass 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) + "file": f"/{rel_path.as_posix()}", + "url": absolute_url( + self.context["request"], + f"{settings.MEDIA_URL.rstrip('/')}/{rel_path.as_posix()}", + ), + "name": safe_name, + "size": sizeof_fmt(file.size), + "mime_type": guess_file_type(rel_path.name), } - # Generate thumbnail if file is an image (low quality for preview) - if is_image_file(full_path): + # For images + if is_image_file(str(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}" + thumbnail_path = create_thumbnail(str(full_path), size=(200, 200), quality=60) + thumb_rel = str(thumbnail_path).replace(str(media_root), "").lstrip("\\/") + result["thumbnail_url"] = absolute_url( + self.context["request"], + f"{settings.MEDIA_URL.rstrip('/')}/{thumb_rel.replace(os.sep, '/')}", ) 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): + result["thumbnail_url"] = None + # For videos + elif is_video_file(str(full_path)): try: thumbnail_path = extract_video_thumbnail( - full_path, - time_offset='00:00:01', - size=(200, 200), - quality=60 + str(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}" + thumb_rel = str(thumbnail_path).replace(str(media_root), "").lstrip("\\/") + result["thumbnail_url"] = absolute_url( + self.context["request"], + f"{settings.MEDIA_URL.rstrip('/')}/{thumb_rel.replace(os.sep, '/')}", ) except Exception as e: print(f"Failed to generate video thumbnail: {e}") - result['thumbnail_url'] = None + result["thumbnail_url"] = None else: - result['thumbnail_url'] = None - + result["thumbnail_url"] = None return result def validate(self, attrs): @@ -356,58 +458,70 @@ class UploadTmpSerializer(serializers.Serializer): 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 + create_thumbnail, + is_image_file, + is_video_file, + extract_video_thumbnail, ) - static_path = settings.STATIC_ROOT - os.makedirs(f'{static_path}/tmp', exist_ok=True) - fpath = f"/tmp/{secrets.token_urlsafe(4)}-{file.name}" - full_path = static_path + fpath - shutil.move(file.temporary_file_path(), full_path) - os.chmod(full_path, 0o644) + media_root = Path(settings.MEDIA_ROOT) + tmp_dir = media_root / "tmp" + tmp_dir.mkdir(parents=True, exist_ok=True) + + safe_name = get_valid_filename(os.path.basename(file.name)) + rel_path = Path("tmp") / f"{secrets.token_urlsafe(4)}-{safe_name}" + + # Save the file using Django storage (safe on Windows) + # If the file is an InMemoryFile or TemporaryUploadedFile, read its content + if hasattr(file, 'read'): + default_storage.save(str(rel_path), ContentFile(file.read())) + else: + default_storage.save(str(rel_path), file) + + full_path = str(media_root / rel_path) result = { - 'file': fpath, - 'url': absolute_url(self.context['request'], f"/static{fpath}"), - 'name': file.name, - 'size': sizeof_fmt(file.size), - 'mime_type': guess_file_type(fpath) + "file": f"/{rel_path.as_posix()}", + "url": absolute_url( + self.context["request"], + f"{settings.MEDIA_URL.rstrip('/')}/{rel_path.as_posix()}", + ), + "name": safe_name, + "size": sizeof_fmt(file.size), + "mime_type": guess_file_type(rel_path.name), } - # Generate thumbnail if file is an image (low quality for preview) + # Generate thumbnail if image if is_image_file(full_path): try: - 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}" + thumb_path = create_thumbnail(full_path, size=(200, 200), quality=60) + thumb_rel = thumb_path.replace(str(media_root), "").lstrip("\\/") + result["thumbnail_url"] = absolute_url( + self.context["request"], + f"{settings.MEDIA_URL.rstrip('/')}/{thumb_rel.replace(os.sep, '/')}", ) 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) + print("Failed to generate image thumbnail:", e) + result["thumbnail_url"] = None + # Generate thumbnail if video elif is_video_file(full_path): try: - thumbnail_path = extract_video_thumbnail( - full_path, - time_offset='00:00:01', - size=(200, 200), - quality=60 + thumb_path = extract_video_thumbnail( + full_path, + time_offset="00:00:01", + size=(200, 200), + quality=60, ) - thumbnail_relative = thumbnail_path.replace(static_path, '') - result['thumbnail_url'] = absolute_url( - self.context['request'], - f"/static{thumbnail_relative}" + thumb_rel = thumb_path.replace(str(media_root), "").lstrip("\\/") + result["thumbnail_url"] = absolute_url( + self.context["request"], + f"{settings.MEDIA_URL.rstrip('/')}/{thumb_rel.replace(os.sep, '/')}", ) except Exception as e: - print(f"Failed to generate video thumbnail: {e}") - result['thumbnail_url'] = None + print("Failed to generate video thumbnail:", e) + result["thumbnail_url"] = None else: - result['thumbnail_url'] = None + result["thumbnail_url"] = None return result