Skip to content

Commit

Permalink
clean up upload_to functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
knifecake committed Dec 26, 2024
1 parent 6fb16b0 commit 3dad445
Show file tree
Hide file tree
Showing 5 changed files with 29 additions and 72 deletions.
13 changes: 3 additions & 10 deletions anchor/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,12 @@ class Meta:
initial="default",
)
file = forms.FileField()
prefix = forms.CharField(
required=False, help_text="The folder where to store the blob into"
)

def save(self, commit=True):
blob = Blob(
prefix=self.cleaned_data["prefix"], backend=self.cleaned_data["backend"]
return Blob.objects.create(
file=self.cleaned_data["file"], backend=self.cleaned_data["backend"]
)

blob.upload(self.cleaned_data["file"])
blob.save()
return blob

def save_m2m(self):
pass

Expand Down Expand Up @@ -112,7 +105,7 @@ def get_readonly_fields(self, request, obj=None):
def get_fieldsets(self, request, obj=None):
if obj is None:
return [
(None, {"fields": ("backend", "file", "prefix")}),
(None, {"fields": ("backend", "file")}),
]

return super().get_fieldsets(request, obj)
Expand Down
32 changes: 6 additions & 26 deletions anchor/models/blob/blob.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import mimetypes
import os
from secrets import token_bytes
from typing import Any, Callable, Optional, Self
from typing import Any, Optional

from django.core.files import File as DjangoFile
from django.core.files.storage import Storage, storages
Expand Down Expand Up @@ -140,26 +140,16 @@ class Meta:
If you need to store custom metadata, refer to the :py:attr:`custom_metadata` property.
"""

upload_to: str | Callable[[Self], str] | None = None

def __init__(self, *args, upload_to=None, backend=None, **kwargs):
def __init__(self, *args, backend=None, **kwargs):
super().__init__(*args, **kwargs)
if self.key == "":
self.key = self.generate_key()
self.key = type(self).generate_key()

if backend is not None:
self.backend = backend
else:
self.backend = anchor_settings.DEFAULT_STORAGE_BACKEND

if upload_to is not None:
self.upload_to = upload_to

if isinstance(upload_to, str):
self.prefix = upload_to
elif callable(upload_to):
self.prefix = upload_to(self)

@property
def signed_id(self):
"""
Expand Down Expand Up @@ -220,17 +210,6 @@ def unsign_id(cls, signed_id: str, purpose: str = None):
def _get_signer(cls):
return AnchorSigner()

@property
def prefix(self):
return os.path.dirname(self.key)

@prefix.setter
def prefix(self, value):
if value is None:
self.key = os.path.basename(self.key)
else:
self.key = os.path.join(value, os.path.basename(self.key))

def __str__(self):
return self.filename or self.id

Expand Down Expand Up @@ -292,7 +271,8 @@ def storage(self) -> Storage:
"""
return storages.create_storage(storages.backends[self.backend])

def generate_key(self):
@classmethod
def generate_key(cls):
"""
Generates a random key to store this blob in the storage backend.
Expand All @@ -303,7 +283,7 @@ def generate_key(self):
case insensitive file systems.
"""
return (
base64.b32encode(token_bytes(self.KEY_LENGTH))
base64.b32encode(token_bytes(cls.KEY_LENGTH))
.decode("utf-8")
.replace("=", "")
.lower()
Expand Down
4 changes: 3 additions & 1 deletion anchor/models/blob/representations.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,10 @@ def representation(self, transformations: dict[str, Any]):
"""
if self.is_variable:
return self.variant(transformations)
else:
elif self.is_previewable:
return self.preview()
else:
raise NotRepresentableError()

@property
def is_representable(self) -> bool:
Expand Down
25 changes: 17 additions & 8 deletions anchor/models/fields/single_attachment.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from typing import Callable
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 @@ -41,7 +43,7 @@ def __init__(
self,
related: GenericRel,
name: str,
upload_to: str | Callable[[Blob], str] = None,
upload_to: str | Callable[[models.Model, Any], str] = None,
backend: str = None,
):
self.related = related
Expand Down Expand Up @@ -80,18 +82,14 @@ def __set__(self, instance, value):
blob = value
elif hasattr(value, "read"): # quacks like a file?
blob = Blob.objects.create(
file=value, backend=self.backend, upload_to=self.upload_to
file=value, backend=self.backend, key=self.get_key(value)
)
else:
raise ValueError(
f"Invalid value type {type(value)}. Provide a File, a Blob or an Attachment."
)

Attachment.objects.update_or_create(
# object_id=instance.id,
# content_type=ContentType.objects.get_for_model(instance),
# name=self.name,
# order=0,
**self.related.field.get_forward_related_filter(instance),
defaults={"blob": blob},
)
Expand Down Expand Up @@ -140,6 +138,17 @@ 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 Expand Up @@ -175,7 +184,7 @@ class SingleAttachmentField(GenericRelation):

def __init__(
self,
upload_to: str | Callable[[Blob], str] = None,
upload_to: str | Callable[[models.Model, Blob], str] = None,
backend: str = None,
**kwargs,
):
Expand Down
27 changes: 0 additions & 27 deletions tests/models/blob/test_blob.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,16 +89,6 @@ def test_is_image(self):


class TestBlobKeys(SimpleTestCase):
def test_upload_to_with_no_upload_to(self):
blob = Blob()
self.assertIsNone(blob.upload_to)

def test_upload_to_can_be_set(self):
blob = Blob(upload_to="test")
self.assertEqual(blob.upload_to, "test")
self.assertTrue(blob.key.startswith("test/"))
self.assertTrue(len(blob.key) > len(blob.upload_to))

def test_key_is_generated(self):
blob = Blob()
self.assertIsNotNone(blob.key)
Expand All @@ -108,10 +98,6 @@ def test_key_can_be_set(self):
blob = Blob(key="test")
self.assertEqual(blob.key, "test")

def test_key_and_upload_to_can_be_set(self):
blob = Blob(key="test", upload_to="test2")
self.assertEqual(blob.key, "test2/test")

def test_str(self):
blob = Blob(key="test")
self.assertEqual(str(blob), blob.pk)
Expand All @@ -122,26 +108,13 @@ def test_upload_file(self):
blob = Blob()
blob.upload(File(BytesIO(b"test"), name="text.txt"))

def test_upload_file_with_upload_to(self):
blob = Blob(upload_to="test")
blob.upload(File(BytesIO(b"test"), name="text.txt"))
self.assertTrue(blob.key.startswith("test/"))

@skipUnless("r2-dev" in settings.STORAGES, "R2 is not configured")
def test_upload_file_to_r2(self):
blob = Blob(backend="r2-dev")
blob.upload(File(BytesIO(b"test"), name="text.txt"))
self.assertIsNotNone(blob.key)
self.assertTrue(blob.storage.exists(blob.key))

@skipUnless("r2-dev" in settings.STORAGES, "R2 is not configured")
def test_upload_image_to_r2(self):
blob = Blob(backend="r2-dev", upload_to="test", key="image.png")
with open(GARLIC_PNG, mode="rb") as f:
blob.upload(File(f, name="image.png"))
self.assertTrue(blob.key.startswith("test/"))
self.assertTrue(blob.storage.exists(blob.key))

def test_open(self):
blob = Blob()
blob.upload(ContentFile(b"test", name="test.txt"))
Expand Down

0 comments on commit 3dad445

Please sign in to comment.