Skip to content

Commit

Permalink
refactor upload_to
Browse files Browse the repository at this point in the history
  • Loading branch information
knifecake committed Dec 27, 2024
1 parent 36b7051 commit 2dfcbc3
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 46 deletions.
17 changes: 5 additions & 12 deletions anchor/admin.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import os
from typing import Any

from django import forms
from django.conf import settings
from django.contrib import admin
from django.template.defaultfilters import filesizeformat
from django.utils import timezone
from django.utils.html import format_html

from anchor.models import Attachment, Blob, VariantRecord
Expand All @@ -31,19 +27,16 @@ def save(self, commit=True):
return Blob.objects.create(
file=self.cleaned_data["file"],
backend=self.cleaned_data["backend"],
key=self.get_key(self.cleaned_data["file"]),
key=Blob.key_with_upload_to(
upload_to=anchor_settings.ADMIN_UPLOAD_TO,
instance=None,
file=self.cleaned_data["file"],
),
)

def save_m2m(self):
pass

def get_key(self, file: Any) -> str:
if anchor_settings.ADMIN_UPLOAD_TO:
dirname = timezone.now().strftime(anchor_settings.ADMIN_UPLOAD_TO)
return os.path.join(dirname, Blob.generate_key())

return Blob.generate_key()


@admin.register(Attachment)
class AttachmentAdmin(admin.ModelAdmin):
Expand Down
22 changes: 2 additions & 20 deletions anchor/models/blob/blob.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import logging
import mimetypes
import os
from secrets import token_bytes
from typing import Any, Optional

from django.core.files import File as DjangoFile
Expand All @@ -16,6 +15,7 @@
from anchor.settings import anchor_settings
from anchor.support.signing import AnchorSigner

from .keys import KeysMixin
from .representations import RepresentationsMixin

logger = logging.getLogger("anchor")
Expand Down Expand Up @@ -48,7 +48,7 @@ def from_path(self, path: str, **kwargs):
return self.create(file=f, **kwargs)


class Blob(RepresentationsMixin, BaseModel):
class Blob(KeysMixin, RepresentationsMixin, BaseModel):
"""
Stores metadata for files stored by Django Anchor.
"""
Expand Down Expand Up @@ -271,24 +271,6 @@ def storage(self) -> Storage:
"""
return storages.create_storage(storages.backends[self.backend])

@classmethod
def generate_key(cls):
"""
Generates a random key to store this blob in the storage backend.
Keys are hard to guess, but shouldn't be shared directly with users,
preferring to use :py:attr:`signed_id` instead, which can be expired.
They use the base32 encoding to ensure no compatibility issues arise in
case insensitive file systems.
"""
return (
base64.b32encode(token_bytes(cls.KEY_LENGTH))
.decode("utf-8")
.replace("=", "")
.lower()
)

def url(self, expires_in: timezone.timedelta = None, disposition: str = "inline"):
"""
Returns a URL to the file's location in the storage backend.
Expand Down
47 changes: 47 additions & 0 deletions anchor/models/blob/keys.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import base64
import os
from secrets import token_bytes
from typing import Any, Callable

from django.db import models
from django.utils import timezone

FileLike = Any
UploadToCallable = Callable[[models.Model, FileLike], str] | str | None


class KeysMixin:
@classmethod
def generate_key(cls):
"""
Generates a random key to store this blob in the storage backend.
Keys are hard to guess, but shouldn't be shared directly with users,
preferring to use :py:attr:`signed_id` instead, which can be expired.
They use the base32 encoding to ensure no compatibility issues arise in
case insensitive file systems.
"""
return (
base64.b32encode(token_bytes(cls.KEY_LENGTH))
.decode("utf-8")
.replace("=", "")
.lower()
)

@classmethod
def key_with_upload_to(
cls,
upload_to: UploadToCallable = None,
instance: models.Model = None,
file: FileLike = None,
) -> str:
if upload_to is None:
return cls.generate_key()
elif isinstance(upload_to, str) and file is not None:
dir = timezone.now().strftime(str(upload_to))
return os.path.join(dir, cls.generate_key())
elif callable(upload_to):
return upload_to(instance, file)
else:
raise ValueError("upload_to must be a string or a callable")
17 changes: 3 additions & 14 deletions anchor/models/fields/single_attachment.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import os
from typing import Any, Callable

from django.contrib.contenttypes.fields import GenericRel, GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.db.models import Model
from django.db.models.fields.related_descriptors import ReverseOneToOneDescriptor
from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.text import capfirst

Expand Down Expand Up @@ -82,7 +80,9 @@ def __set__(self, instance, value):
blob = value
elif hasattr(value, "read"): # quacks like a file?
blob = Blob.objects.create(
file=value, backend=self.backend, key=self.get_key(value)
file=value,
backend=self.backend,
key=Blob.key_with_upload_to(self.upload_to, value),
)
else:
raise ValueError(
Expand Down Expand Up @@ -138,17 +138,6 @@ def instance_attr(i):
False,
)

def get_key(self, value: Any):
if self.upload_to is None:
return Blob.generate_key()
elif isinstance(self.upload_to, str):
dir = timezone.now().strftime(str(self.upload_to))
return os.path.join(dir, Blob.generate_key())
elif callable(self.upload_to):
return self.upload_to(self.model, value)
else:
raise ValueError("upload_to must be a string or a callable")


class SingleAttachmentField(GenericRelation):
"""
Expand Down

0 comments on commit 2dfcbc3

Please sign in to comment.