Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions app/core/permissions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from rest_framework import permissions, request, views
from user.models import UserExt


class IsSuperUser(permissions.BasePermission):
message = "You do not have permission to perform this action."

def has_permission(self, request: request.Request, view: views.APIView) -> bool:
return isinstance(request.user, UserExt) and request.user.is_superuser
31 changes: 25 additions & 6 deletions app/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@
"django_extensions",
# django-app
"user",
"file",
"cms",
# django-constance
"constance",
Expand Down Expand Up @@ -251,29 +252,47 @@
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.2/howto/static-files/
STATIC_ROOT = BASE_DIR / "static"
MEDIA_ROOT = BASE_DIR / "media"

DEFAULT_STORAGE_BACKEND = env("DJANGO_DEFAULT_STORAGE_BACKEND", default="storages.backends.s3.S3Storage")
STATIC_STORAGE_BACKEND = env("DJANGO_STATIC_STORAGE_BACKEND", default="storages.backends.s3.S3Storage")

STORAGE_BUCKET_NAME = f"pyconkr-backend-{API_STAGE}"
PRIVATE_STORAGE_BUCKET_NAME = f"pyconkr-backend-{API_STAGE}"
PUBLIC_STORAGE_BUCKET_NAME = f"pyconkr-backend-{API_STAGE}-public"

STATIC_URL = (
f"https://s3.ap-northeast-2.amazonaws.com/{STORAGE_BUCKET_NAME}/"
f"https://s3.ap-northeast-2.amazonaws.com/{PRIVATE_STORAGE_BUCKET_NAME}/"
if STATIC_STORAGE_BACKEND == "storages.backends.s3.S3Storage"
else "static/"
)
MEDIA_URL = (
f"https://s3.ap-northeast-2.amazonaws.com/{PUBLIC_STORAGE_BUCKET_NAME}/"
if DEFAULT_STORAGE_BACKEND == "storages.backends.s3.S3Storage"
else "media/"
)

STORAGE_OPTIONS = (
STATIC_STORAGE_OPTIONS = (
{
"bucket_name": PRIVATE_STORAGE_BUCKET_NAME,
"file_overwrite": False,
"addressing_style": "path",
}
if DEFAULT_STORAGE_BACKEND == "storages.backends.s3.S3Storage"
else {}
)
PUBLIC_STORAGE_OPTIONS = (
{
"bucket_name": STORAGE_BUCKET_NAME,
"bucket_name": PRIVATE_STORAGE_BUCKET_NAME,
"file_overwrite": False,
"addressing_style": "path",
}
if DEFAULT_STORAGE_BACKEND == "storages.backends.s3.S3Storage"
else {}
)
STORAGES = {
"default": {"BACKEND": DEFAULT_STORAGE_BACKEND, "OPTIONS": STORAGE_OPTIONS},
"staticfiles": {"BACKEND": STATIC_STORAGE_BACKEND, "OPTIONS": STORAGE_OPTIONS},
"default": {"BACKEND": DEFAULT_STORAGE_BACKEND, "OPTIONS": STATIC_STORAGE_OPTIONS},
"staticfiles": {"BACKEND": STATIC_STORAGE_BACKEND, "OPTIONS": STATIC_STORAGE_OPTIONS},
"public": {"BACKEND": DEFAULT_STORAGE_BACKEND, "OPTIONS": PUBLIC_STORAGE_OPTIONS},
}

# Default primary key field type
Expand Down
6 changes: 5 additions & 1 deletion app/core/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@
path("admin/", admin.site.urls),
# V1 API
re_path("^v1/", include((v1_apis, "v1"), namespace="v1")),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
] + [
# Static files
*static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT),
*static(settings.STATIC_URL, document_root=settings.STATIC_ROOT),
]

if settings.DEBUG:
urlpatterns += [
Expand Down
Empty file added app/file/__init__.py
Empty file.
26 changes: 26 additions & 0 deletions app/file/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from django.contrib import admin
from django.http.request import HttpRequest
from django.http.response import HttpResponseNotAllowed, JsonResponse
from django.urls import re_path
from django.urls.resolvers import URLPattern
from file.models import PublicFile


@admin.register(PublicFile)
class PublicFileAdmin(admin.ModelAdmin):
fields = ["id", "file", "mimetype", "hash", "size", "created_at", "updated_at", "deleted_at"]
readonly_fields = ["id", "mimetype", "hash", "size", "created_at", "updated_at", "deleted_at"]

def get_readonly_fields(self, request: HttpRequest, obj: PublicFile | None = None) -> list[str]:
return self.readonly_fields + (["file"] if obj else [])

def get_urls(self) -> list[URLPattern]:
return [
re_path(route=r"^list/$", view=self.admin_site.admin_view(self.list_public_files)),
] + super().get_urls()

def list_public_files(self, request: HttpRequest) -> JsonResponse | HttpResponseNotAllowed:
if request.method == "GET":
data = list(PublicFile.objects.filter_active().values(*self.fields))
return JsonResponse(data=data, safe=False, json_dumps_params={"ensure_ascii": False})
return HttpResponseNotAllowed(permitted_methods=["GET"])
11 changes: 11 additions & 0 deletions app/file/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from django.apps import AppConfig


class FileConfig(AppConfig):
name = "file"

def ready(self):
from file.models import PublicFile
from simple_history import register

register(PublicFile)
149 changes: 149 additions & 0 deletions app/file/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Generated by Django 5.2 on 2025-05-20 04:57

import uuid

import django.core.files.storage
import django.db.models.deletion
import simple_history.models
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):
initial = True

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name="HistoricalPublicFile",
fields=[
("id", models.UUIDField(db_index=True, default=uuid.uuid4, editable=False)),
("created_at", models.DateTimeField(blank=True, editable=False)),
("updated_at", models.DateTimeField(blank=True, editable=False)),
("deleted_at", models.DateTimeField(blank=True, null=True)),
("file", models.TextField(db_index=True, max_length=100)),
("mimetype", models.CharField(max_length=256, null=True)),
("hash", models.CharField(max_length=256)),
("size", models.BigIntegerField()),
("history_id", models.AutoField(primary_key=True, serialize=False)),
("history_date", models.DateTimeField(db_index=True)),
("history_change_reason", models.CharField(max_length=100, null=True)),
(
"history_type",
models.CharField(choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")], max_length=1),
),
(
"created_by",
models.ForeignKey(
blank=True,
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
to=settings.AUTH_USER_MODEL,
),
),
(
"deleted_by",
models.ForeignKey(
blank=True,
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
to=settings.AUTH_USER_MODEL,
),
),
(
"history_user",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to=settings.AUTH_USER_MODEL,
),
),
(
"updated_by",
models.ForeignKey(
blank=True,
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "historical public file",
"verbose_name_plural": "historical public files",
"ordering": ("-history_date", "-history_id"),
"get_latest_by": ("history_date", "history_id"),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name="PublicFile",
fields=[
("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("deleted_at", models.DateTimeField(blank=True, null=True)),
(
"file",
models.FileField(
storage=django.core.files.storage.FileSystemStorage(), unique=True, upload_to="public/"
),
),
("mimetype", models.CharField(max_length=256, null=True)),
("hash", models.CharField(max_length=256)),
("size", models.BigIntegerField()),
(
"created_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="%(class)s_created_by",
to=settings.AUTH_USER_MODEL,
),
),
(
"deleted_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="%(class)s_deleted_by",
to=settings.AUTH_USER_MODEL,
),
),
(
"updated_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="%(class)s_updated_by",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"ordering": ["-created_at"],
},
),
migrations.AddIndex(
model_name="publicfile",
index=models.Index(fields=["file"], name="file_public_file_3d3996_idx"),
),
migrations.AddIndex(
model_name="publicfile",
index=models.Index(fields=["mimetype"], name="file_public_mimetyp_da163f_idx"),
),
migrations.AddIndex(
model_name="publicfile",
index=models.Index(fields=["hash"], name="file_public_hash_669533_idx"),
),
]
Empty file added app/file/migrations/__init__.py
Empty file.
29 changes: 29 additions & 0 deletions app/file/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import hashlib
import mimetypes

from core.models import BaseAbstractModel
from django.core.files.storage import storages
from django.db import models


class PublicFile(BaseAbstractModel):
file = models.FileField(unique=True, null=False, blank=False, upload_to="public/", storage=storages["public"])
mimetype = models.CharField(max_length=256, null=True, blank=False)
hash = models.CharField(max_length=256, null=False, blank=False)
size = models.BigIntegerField(null=False, blank=False)

class Meta:
ordering = ["-created_at"]
indexes = [models.Index(fields=["file"]), models.Index(fields=["mimetype"]), models.Index(fields=["hash"])]

def clean(self) -> None:
# 파일의 해시값, 크기, mimetype을 계산하여 저장합니다.
hash_md5 = hashlib.md5(usedforsecurity=False)
file_pointer = self.file.open("rb")

for chunk in iter(lambda: file_pointer.read(4096), b""):
hash_md5.update(chunk)

self.hash = hash_md5.hexdigest()
self.size = self.file.size
self.mimetype = mimetypes.guess_type(self.file.name)[0]