Skip to content

Commit

Permalink
Merge pull request #694 from projectcaluma/fix-copy-s3
Browse files Browse the repository at this point in the history
fix(file): clone files with s3 copy
  • Loading branch information
winged authored Dec 16, 2024
2 parents 7bfc17f + f191fae commit bd2f41d
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 9 deletions.
41 changes: 32 additions & 9 deletions alexandria/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from manabi.token import Key, Token

from alexandria.core.presign_urls import make_signature_components
from alexandria.storages.backends.s3 import S3Storage
from alexandria.storages.fields import DynamicStorageFileField


Expand Down Expand Up @@ -166,15 +167,37 @@ def clone(self):
self.pk = None
self.save()

with NamedTemporaryFile() as tmp:
temp_file = Path(tmp.name)
with temp_file.open("w+b") as file:
file.write(latest_original.content.file.file.read())

latest_original.content = DjangoFile(file)
latest_original.pk = None
latest_original.document = self
latest_original.save()
storage = File.content.field.storage
latest_original.pk = None
latest_original.document = self
if isinstance(storage, S3Storage):
new_name = f"{self.pk}_{latest_original.name}"
copy_args = {
"CopySource": {
"Bucket": settings.ALEXANDRIA_S3_BUCKET_NAME,
"Key": latest_original.content.name,
},
# Destination settings
"Bucket": settings.ALEXANDRIA_S3_BUCKET_NAME,
"Key": new_name,
}

if settings.ALEXANDRIA_ENABLE_AT_REST_ENCRYPTION:
copy_args["CopySourceSSECustomerKey"] = storage.ssec_secret
copy_args["CopySourceSSECustomerAlgorithm"] = "AES256"
copy_args["SSECustomerKey"] = storage.ssec_secret
copy_args["SSECustomerAlgorithm"] = "AES256"

storage.bucket.meta.client.copy_object(**copy_args)
latest_original.content = new_name
latest_original.save()
else:
with NamedTemporaryFile() as tmp:
temp_file = Path(tmp.name)
with temp_file.open("w+b") as file:
file.write(latest_original.content.file.file.read())
latest_original.content = DjangoFile(file)
latest_original.save()

return self

Expand Down
42 changes: 42 additions & 0 deletions alexandria/core/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pytest
from django.core.files import File as DjangoFile
from django.core.files.uploadedfile import SimpleUploadedFile
from django.db.models import ObjectDoesNotExist

Expand Down Expand Up @@ -50,3 +51,44 @@ def test_document_no_files(
document.get_latest_original()
except ObjectDoesNotExist:
assert True


def test_clone_document_s3(db, mocker, settings, file_factory):
settings.ALEXANDRIA_FILE_STORAGE = (
"alexandria.storages.backends.s3.SsecGlobalS3Storage"
)
settings.ALEXANDRIA_ENABLE_AT_REST_ENCRYPTION = True
name = "name-of-the-file"
mocker.patch("storages.backends.s3.S3Storage.save", return_value=name)
mocker.patch(
"storages.backends.s3.S3Storage.open",
return_value=DjangoFile(open("README.md", "rb")),
)
mocked = mocker.patch("botocore.client.BaseClient._make_api_call")

original_file = file_factory(
variant=File.Variant.ORIGINAL,
content=SimpleUploadedFile(
name="test.png",
content=FileData.png,
content_type="image/png",
),
)

file_factory(
original=original_file,
variant=File.Variant.THUMBNAIL,
document=original_file.document,
)

original_document_pk = original_file.document.pk

clone = original_file.document.clone()
original = Document.objects.get(pk=original_document_pk)

assert clone.pk != original.pk
assert (
clone.get_latest_original().content.name
!= original.get_latest_original().content.name
)
assert mocked.call_args[0][1]["CopySource"]["Key"] == name
4 changes: 4 additions & 0 deletions alexandria/storages/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ class DynamicStorageFileField(models.FileField):
# the storage code into it's own project, so we can reuse it outside
# of Alexandria: https://github.com/projectcaluma/alexandria/issues/480

def __init__(self, storage=None, **kwargs):
storage = storages.create_storage({"BACKEND": settings.ALEXANDRIA_FILE_STORAGE})
super().__init__(storage=storage, **kwargs)

def pre_save(self, instance, add):
# set storage to default storage class to prevent reusing the last selection
self.storage = storages.create_storage(
Expand Down

0 comments on commit bd2f41d

Please sign in to comment.