Skip to content

Commit 1d55d90

Browse files
committed
feat(storages): move copy to storage field
fix document clone ssec storage usage
1 parent fea6b20 commit 1d55d90

File tree

4 files changed

+56
-44
lines changed

4 files changed

+56
-44
lines changed

alexandria/core/models.py

Lines changed: 2 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import re
22
import uuid
33
from pathlib import Path
4-
from tempfile import NamedTemporaryFile
54

65
from django.conf import settings
76
from django.contrib.postgres.fields import ArrayField
87
from django.contrib.postgres.indexes import GinIndex
98
from django.contrib.postgres.search import SearchVectorField
109
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
11-
from django.core.files import File as DjangoFile
1210
from django.core.validators import RegexValidator
1311
from django.db import models, transaction
1412
from django.dispatch import receiver
@@ -18,7 +16,6 @@
1816
from manabi.token import Key, Token
1917
from rest_framework_json_api.relations import reverse
2018

21-
from alexandria.storages.backends.s3 import S3Storage
2219
from alexandria.storages.fields import DynamicStorageFileField
2320

2421

@@ -171,38 +168,11 @@ def clone(self):
171168

172169
self.pk = None
173170
self.save()
171+
new_name = f"{self.pk}_{latest_original.name}"
174172

175-
storage = File.content.field.storage
176173
latest_original.pk = None
177174
latest_original.document = self
178-
if isinstance(storage, S3Storage):
179-
new_name = f"{self.pk}_{latest_original.name}"
180-
copy_args = {
181-
"CopySource": {
182-
"Bucket": settings.ALEXANDRIA_S3_BUCKET_NAME,
183-
"Key": latest_original.content.name,
184-
},
185-
# Destination settings
186-
"Bucket": settings.ALEXANDRIA_S3_BUCKET_NAME,
187-
"Key": new_name,
188-
}
189-
190-
if settings.ALEXANDRIA_ENABLE_AT_REST_ENCRYPTION:
191-
copy_args["CopySourceSSECustomerKey"] = storage.ssec_secret
192-
copy_args["CopySourceSSECustomerAlgorithm"] = "AES256"
193-
copy_args["SSECustomerKey"] = storage.ssec_secret
194-
copy_args["SSECustomerAlgorithm"] = "AES256"
195-
196-
storage.bucket.meta.client.copy_object(**copy_args)
197-
latest_original.content = new_name
198-
latest_original.save()
199-
else:
200-
with NamedTemporaryFile() as tmp:
201-
temp_file = Path(tmp.name)
202-
with temp_file.open("w+b") as file:
203-
file.write(latest_original.content.file.file.read())
204-
latest_original.content = DjangoFile(file)
205-
latest_original.save()
175+
latest_original.content.copy(new_name)
206176

207177
return self
208178

alexandria/core/tests/test_models.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,7 @@ def test_document_no_files(
5454

5555

5656
def test_clone_document_s3(db, mocker, settings, file_factory):
57-
settings.ALEXANDRIA_FILE_STORAGE = (
58-
"alexandria.storages.backends.s3.SsecGlobalS3Storage"
59-
)
57+
settings.ALEXANDRIA_FILE_STORAGE = "alexandria.storages.backends.s3.S3Storage"
6058
settings.ALEXANDRIA_ENABLE_AT_REST_ENCRYPTION = True
6159
name = "name-of-the-file"
6260
mocker.patch("storages.backends.s3.S3Storage.save", return_value=name)
@@ -73,12 +71,14 @@ def test_clone_document_s3(db, mocker, settings, file_factory):
7371
content=FileData.png,
7472
content_type="image/png",
7573
),
74+
encryption_status=File.EncryptionStatus.SSEC_GLOBAL_KEY,
7675
)
7776

7877
file_factory(
7978
original=original_file,
8079
variant=File.Variant.THUMBNAIL,
8180
document=original_file.document,
81+
encryption_status=File.EncryptionStatus.SSEC_GLOBAL_KEY,
8282
)
8383

8484
original_document_pk = original_file.document.pk

alexandria/storages/fields.py

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
from pathlib import Path
2+
from tempfile import NamedTemporaryFile
3+
14
from django.conf import ImproperlyConfigured, settings
5+
from django.core.files import File as DjangoFile
26
from django.core.files.storage import storages
37
from django.db import models
48
from django.db.models.fields.files import FieldFile
@@ -10,14 +14,52 @@
1014
class DynamicStorageFieldFile(FieldFile):
1115
def __init__(self, instance, field, name):
1216
super().__init__(instance, field, name)
13-
self.storage = storages.create_storage(
14-
{"BACKEND": settings.ALEXANDRIA_FILE_STORAGE}
15-
)
17+
storage_backend = settings.ALEXANDRIA_FILE_STORAGE
1618
if settings.ALEXANDRIA_ENABLE_AT_REST_ENCRYPTION:
1719
from alexandria.core.models import File
1820

1921
if instance.encryption_status == File.EncryptionStatus.SSEC_GLOBAL_KEY:
20-
self.storage = SsecGlobalS3Storage()
22+
storage_backend = "alexandria.storages.backends.s3.SsecGlobalS3Storage"
23+
self.storage = storages.create_storage({"BACKEND": storage_backend})
24+
25+
def copy(self, target_name: str):
26+
"""
27+
Copy file content to target.
28+
29+
NOTE: this will copy the content and this copy will be the new reference of the FileField.
30+
This means that there will be no pointer to the original content,
31+
unless you made a copy of the parent model instance beforhands by e.g.
32+
setting `file_instance.pk` to `None` before calling `file_instance.content.copy`.
33+
"""
34+
# S3 compatible storage: copy in same bucket with s3 copy
35+
if isinstance(self.storage, S3Storage):
36+
copy_args = {
37+
"CopySource": {
38+
"Bucket": self.storage.bucket,
39+
"Key": self.name,
40+
},
41+
# Destination settings
42+
"Bucket": self.storage.bucket,
43+
"Key": target_name,
44+
}
45+
if isinstance(self.storage, SsecGlobalS3Storage):
46+
copy_args["CopySourceSSECustomerKey"] = self.storage.ssec_secret
47+
copy_args["CopySourceSSECustomerAlgorithm"] = "AES256"
48+
copy_args["SSECustomerKey"] = self.storage.ssec_secret
49+
copy_args["SSECustomerAlgorithm"] = "AES256"
50+
51+
self.storage.bucket.meta.client.copy_object(**copy_args)
52+
self.instance.content = target_name
53+
self.instance.save()
54+
return
55+
56+
# otherwise use filesystem storage
57+
with NamedTemporaryFile() as tmp:
58+
temp_file = Path(tmp.name)
59+
with temp_file.open("w+b") as file:
60+
file.write(self.read())
61+
self.instance.content = DjangoFile(file, target_name)
62+
self.instance.save()
2163

2264

2365
class DynamicStorageFileField(models.FileField):
@@ -56,8 +98,8 @@ def pre_save(self, instance, add):
5698
"Set `ALEXANDRIA_FILE_STORAGE` to `alexandria.storages.s3.S3Storage`."
5799
)
58100
raise ImproperlyConfigured(msg)
59-
storage = SsecGlobalS3Storage()
60101
if instance.encryption_status == File.EncryptionStatus.SSEC_GLOBAL_KEY:
61-
self.storage = storage
62-
_file = super().pre_save(instance, add)
63-
return _file
102+
self.storage = storages.create_storage(
103+
{"BACKEND": "alexandria.storages.backends.s3.SsecGlobalS3Storage"}
104+
)
105+
return super().pre_save(instance, add)

alexandria/storages/tests/test_dynamic_field.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def test_dynamic_storage_select_global_ssec(
2727
mocker.patch("alexandria.core.tasks.create_thumbnail.delay", side_effect=None)
2828
if raises is not None:
2929
with pytest.raises(raises):
30-
file_factory()
30+
file_factory(encryption_status=settings.ALEXANDRIA_ENCRYPTION_METHOD)
3131
return
3232
file_factory(encryption_status=settings.ALEXANDRIA_ENCRYPTION_METHOD)
3333
assert SsecGlobalS3Storage.save.called_once()

0 commit comments

Comments
 (0)