diff --git a/alexandria/core/models.py b/alexandria/core/models.py index 1755cb86..64b19d2f 100644 --- a/alexandria/core/models.py +++ b/alexandria/core/models.py @@ -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 @@ -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 diff --git a/alexandria/core/tests/test_models.py b/alexandria/core/tests/test_models.py index 19baef4a..bdf3066c 100644 --- a/alexandria/core/tests/test_models.py +++ b/alexandria/core/tests/test_models.py @@ -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 @@ -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 diff --git a/alexandria/storages/fields.py b/alexandria/storages/fields.py index 19d5a95f..6b3d027a 100644 --- a/alexandria/storages/fields.py +++ b/alexandria/storages/fields.py @@ -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(