django-file-tools 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 enricobarzetti
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,35 @@
1
+ Metadata-Version: 2.1
2
+ Name: django-file-tools
3
+ Version: 0.1.0
4
+ Summary: Tools for working with files in Django
5
+ Home-page: https://github.com/enricobarzetti/django-file-tools
6
+ License: MIT
7
+ Author: Enrico Barzetti
8
+ Requires-Python: >=3.9,<4.0
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Requires-Dist: boto3 (>=1.34.12,<2.0.0)
16
+ Requires-Dist: django (>=3.0)
17
+ Requires-Dist: django-environ (>=0.11.2,<0.12.0)
18
+ Requires-Dist: django-storages (>=1.14,<2.0)
19
+ Requires-Dist: djangorestframework (>=3.12.0,<4.0.0)
20
+ Description-Content-Type: text/markdown
21
+
22
+ Run MinIO:
23
+
24
+ `MINIO_ROOT_USER=admin MINIO_ROOT_PASSWORD=password ./minio server temp --address ":9000" --console-address ":9001"`
25
+
26
+ Go to the MinIO console and generate access keys. Set values in .env:
27
+
28
+ ```
29
+ MINIO_ENDPOINT=http://127.0.0.1:9000
30
+ MINIO_ACCESS_KEY=value
31
+ MINIO_SECRET_ACCESS_KEY=value
32
+ ```
33
+
34
+ When `MINIO_ENDPOINT` is set, MinIO will be used instead of S3.
35
+
@@ -0,0 +1,13 @@
1
+ Run MinIO:
2
+
3
+ `MINIO_ROOT_USER=admin MINIO_ROOT_PASSWORD=password ./minio server temp --address ":9000" --console-address ":9001"`
4
+
5
+ Go to the MinIO console and generate access keys. Set values in .env:
6
+
7
+ ```
8
+ MINIO_ENDPOINT=http://127.0.0.1:9000
9
+ MINIO_ACCESS_KEY=value
10
+ MINIO_SECRET_ACCESS_KEY=value
11
+ ```
12
+
13
+ When `MINIO_ENDPOINT` is set, MinIO will be used instead of S3.
File without changes
@@ -0,0 +1 @@
1
+ TEMP_MARKER = 'temp'
@@ -0,0 +1,66 @@
1
+ from pathlib import PurePath
2
+
3
+ from django.db.models.fields import files
4
+
5
+ from django_file_tools.constants import TEMP_MARKER
6
+ from django_file_tools.s3 import get_file_without_prefix
7
+
8
+
9
+ class FieldFile(files.FieldFile):
10
+ def copy(self, original, save=True, tags=None):
11
+ path = PurePath(original)
12
+ new_name = path.parts[-1]
13
+ new_name = self.field.generate_filename(self.instance, new_name)
14
+ self.name = self.storage.copy(original, new_name, max_length=self.field.max_length, tags=tags)
15
+ setattr(self.instance, self.field.name, self.name)
16
+
17
+ # Save the object because it has changed, unless save is False
18
+ if save:
19
+ self.instance.save()
20
+
21
+ def copy_from_other_bucket(self, bucket_name, key, prefix=None, save=True, tags=None):
22
+ new_name = get_file_without_prefix(key, prefix)
23
+ new_name = self.field.generate_filename(self.instance, new_name)
24
+ self.name = self.storage.copy_from_other_bucket(bucket_name, key, new_name, max_length=self.field.max_length, tags=tags)
25
+ setattr(self.instance, self.field.name, self.name)
26
+
27
+ # Save the object because it has changed, unless save is False
28
+ if save:
29
+ self.instance.save()
30
+
31
+ def replace(self, save=True, tags=None):
32
+ original = self.name
33
+ self.copy(original, save, tags)
34
+ self.storage.delete(original)
35
+
36
+ @property
37
+ def url(self):
38
+ self._require_file()
39
+ method_name = f'get_{self.field.name}_filename'
40
+ parameters = None
41
+ if hasattr(self.instance, method_name):
42
+ filename = getattr(self.instance, method_name)()
43
+ filename = self.storage.generate_filename(filename)
44
+ parameters = {'ResponseContentDisposition': f'attachment; filename={filename}'}
45
+ return self.storage.url(self.name, parameters=parameters)
46
+
47
+
48
+ class FileField(files.FileField):
49
+ attr_class = FieldFile
50
+
51
+
52
+ def copy_from_temp_storage(instance, tags=None):
53
+ # If tags is not passed then clear them
54
+ if tags is None:
55
+ tags = {}
56
+
57
+ save = False
58
+ for field in instance._meta.fields:
59
+ if isinstance(field, FileField):
60
+ file = getattr(instance, field.name)
61
+ if file.name != '':
62
+ if PurePath(file.name).parts[0].startswith(TEMP_MARKER):
63
+ file.replace(save=False, tags=tags)
64
+ save = True
65
+ if save:
66
+ instance.save()
@@ -0,0 +1,55 @@
1
+ from functools import wraps
2
+ from pathlib import PurePath
3
+
4
+
5
+ class Pather:
6
+ """
7
+ class AnalyzeEditingInputFilePather(AnalyzeEditingFilesPather):
8
+ @staticmethod
9
+ def get_constructor_kwargs(instance):
10
+ return {'instance': instance}
11
+
12
+ def __init__(self, instance):
13
+ self.instance = instance
14
+
15
+ def base(self):
16
+ return PurePath(f'my_model/{self.instance.pk}/')
17
+
18
+ def input_dir(self):
19
+ return self.base() / PurePath('inputs')
20
+
21
+ def input_file(self, instance, filename):
22
+ return self.input_dir() / PurePath(filename)
23
+
24
+ @AnalyzeEditingInputFilePather.upload_to('input_file')
25
+ def location_input():
26
+ pass
27
+
28
+ class UploadedFile(models.Model):
29
+ content = FileField(blank=True, upload_to=location_input, max_length=1000)
30
+ """
31
+ @classmethod
32
+ def make_upload_to_callable(cls, method):
33
+ def f(instance, filename):
34
+ kwargs = cls.get_constructor_kwargs(instance)
35
+ pather = cls(**kwargs)
36
+ path = getattr(pather, method)(instance, filename)
37
+ return str(path)
38
+ return f
39
+
40
+ @classmethod
41
+ def upload_to(cls, method):
42
+ def decorator(h):
43
+ @wraps(h)
44
+ def f(instance, filename):
45
+ callable = cls.make_upload_to_callable(method)
46
+ return callable(instance, filename)
47
+ return f
48
+ return decorator
49
+
50
+ @staticmethod
51
+ def get_constructor_kwargs(instance):
52
+ raise NotImplementedError
53
+
54
+ def base(self):
55
+ return PurePath('/')
@@ -0,0 +1,278 @@
1
+ import errno
2
+ import os
3
+ import posixpath
4
+ from datetime import date
5
+ from pathlib import PurePath
6
+
7
+ import boto3
8
+ from botocore.exceptions import ClientError
9
+ from django.conf import settings
10
+
11
+ from django_file_tools.constants import TEMP_MARKER
12
+
13
+ RETENTION = 'retention'
14
+ EXPIRE_FAST = 'expire_fast'
15
+ EXPIRE_SLOW = 'expire_slow'
16
+
17
+
18
+ def get_client_resource():
19
+ client = boto3.client(
20
+ service_name='s3',
21
+ aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
22
+ aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
23
+ endpoint_url=settings.AWS_S3_ENDPOINT_URL,
24
+ )
25
+ resource = boto3.resource(
26
+ service_name='s3',
27
+ aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
28
+ aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
29
+ endpoint_url=settings.AWS_S3_ENDPOINT_URL,
30
+ )
31
+ return client, resource
32
+
33
+
34
+ client, resource = get_client_resource()
35
+
36
+
37
+ def reset_bucket(bucket):
38
+ bucket = resource.Bucket(bucket)
39
+ bucket.objects.all().delete()
40
+
41
+
42
+ def bucket_exists(bucket_name):
43
+ try:
44
+ client.head_bucket(Bucket=bucket_name)
45
+ return True
46
+ except ClientError:
47
+ return False
48
+
49
+ def assert_dir_exists(path):
50
+ try:
51
+ os.makedirs(path)
52
+ except OSError as e:
53
+ if e.errno != errno.EEXIST:
54
+ raise
55
+
56
+
57
+ def download_dir(bucket, path, target):
58
+ # Handle missing / at end of prefix
59
+ if not path.endswith('/'):
60
+ path += '/'
61
+
62
+ paginator = client.get_paginator('list_objects_v2')
63
+ for result in paginator.paginate(Bucket=bucket, Prefix=path):
64
+ # Download each file individually
65
+ for key in result['Contents']:
66
+ # Calculate relative path
67
+ rel_path = key['Key'][len(path):]
68
+ # Skip paths ending in /
69
+ if not key['Key'].endswith('/'):
70
+ local_file_path = os.path.join(target, rel_path)
71
+ # Make sure directories exist
72
+ local_file_dir = os.path.dirname(local_file_path)
73
+ assert_dir_exists(local_file_dir)
74
+ client.download_file(bucket, key['Key'], local_file_path)
75
+
76
+
77
+ def path_is_file_or_directory(bucket, path):
78
+ paginator = client.get_paginator('list_objects_v2')
79
+ total = 0
80
+ for result in paginator.paginate(Bucket=bucket, Prefix=path):
81
+ total += result['KeyCount']
82
+
83
+ if not path.endswith('/'):
84
+ path += '/'
85
+ total_with_slash = 0
86
+ for result in paginator.paginate(Bucket=bucket, Prefix=path):
87
+ total_with_slash += result['KeyCount']
88
+ if total == total_with_slash and total > 1:
89
+ return 'directory'
90
+ else:
91
+ return 'file'
92
+
93
+
94
+ def download(bucket, path, target):
95
+ is_directory = path_is_file_or_directory(bucket, path) == 'directory'
96
+ if is_directory:
97
+ download_dir(bucket, path, target)
98
+ return (target, is_directory)
99
+ else:
100
+ assert_dir_exists(target)
101
+ filename = os.path.split(path)[1]
102
+ target_path = f'{target}/{filename}'
103
+ client.download_file(bucket, path, target_path)
104
+ return (target_path, is_directory)
105
+
106
+
107
+ def normalize_prefix(prefix):
108
+ # The prefix needs to end with a slash, but if the root is empty, leave
109
+ # it.
110
+ if prefix and not prefix.endswith('/'):
111
+ prefix += '/'
112
+ return prefix
113
+
114
+
115
+ def get_files_under_prefix(bucket_name, prefix):
116
+ def ls(bucket_name, prefix):
117
+ # Use a hash lookup instead of an array to prevent duplicate directories
118
+ directories = {}
119
+ files = []
120
+ paginator = client.get_paginator('list_objects')
121
+ pages = paginator.paginate(Bucket=bucket_name, Delimiter='/', Prefix=prefix)
122
+ for page in pages:
123
+ for entry in page.get('CommonPrefixes', ()):
124
+ key = posixpath.relpath(entry['Prefix'], prefix)
125
+ if not key in directories:
126
+ directories[key] = True
127
+ for entry in page.get('Contents', ()):
128
+ files.append(posixpath.relpath(entry['Key'], prefix))
129
+ return directories.keys(), files
130
+
131
+ def collect_files(bucket_name, prefix, ret):
132
+ prefix = normalize_prefix(prefix)
133
+
134
+ directories, files = ls(bucket_name, prefix)
135
+ for file_ in files:
136
+ # S3 files named . are special files that are created when the "Create Folder" button is used on S3. They
137
+ # show up when calling list_objects(), but will fail when head_object() is called on them.
138
+ if file_ == '.':
139
+ continue
140
+ ret.append(f'{prefix}{file_}')
141
+ for directory in directories:
142
+ ret = collect_files(bucket_name, f'{prefix}{directory}', ret)
143
+ return ret
144
+
145
+ return collect_files(bucket_name, prefix, [])
146
+
147
+
148
+ def files_exist_in_prefix(bucket_name, prefix):
149
+ prefix = normalize_prefix(prefix)
150
+ paginator = client.get_paginator('list_objects')
151
+ pages = paginator.paginate(Bucket=bucket_name, Delimiter='/', Prefix=prefix)
152
+ page = next(iter(pages))
153
+ if 'Contents' in page:
154
+ return True
155
+ return False
156
+
157
+
158
+ def file_exists(bucket_name, key):
159
+ try:
160
+ client.get_object(Bucket=bucket_name, Key=key)
161
+ except ClientError as ex:
162
+ if ex.response['Error']['Code'] == 'NoSuchKey':
163
+ return False
164
+ else:
165
+ raise
166
+ else:
167
+ return True
168
+
169
+
170
+ def get_file_without_prefix(file_, prefix):
171
+ if prefix:
172
+ prefix = normalize_prefix(prefix)
173
+ return file_.split(prefix)[-1]
174
+ else:
175
+ return file_
176
+
177
+
178
+ def get_s3_path(path):
179
+ path = PurePath(settings.AWS_STORAGE_BUCKET_NAME) / PurePath(path)
180
+ return f's3://{path}'
181
+
182
+
183
+ def s3_read(key):
184
+ s3_object = client.get_object(Bucket=settings.AWS_STORAGE_BUCKET_NAME, Key=key)
185
+ body = s3_object['Body']
186
+ return body.read()
187
+
188
+
189
+ def s3_write(key, content):
190
+ client.put_object(Bucket=settings.AWS_STORAGE_BUCKET_NAME, Key=key, Body=content)
191
+
192
+
193
+ def s3_delete(key):
194
+ client.delete_object(Bucket=settings.AWS_STORAGE_BUCKET_NAME, Key=key)
195
+
196
+
197
+ def set_tags(key, tags):
198
+ if tags is None:
199
+ return
200
+
201
+ tag_set = []
202
+ for k, v in tags.items():
203
+ tag_set.append({
204
+ 'Key': k,
205
+ 'Value': v,
206
+ })
207
+ client.put_object_tagging(
208
+ Bucket=settings.AWS_STORAGE_BUCKET_NAME,
209
+ Key=key,
210
+ Tagging={
211
+ 'TagSet': tag_set
212
+ }
213
+ )
214
+
215
+
216
+ def s3_temp_folder_cleanup():
217
+ def dir(bucket_name, prefix):
218
+ # Use a hash lookup instead of an array to prevent duplicate directories
219
+ directories = {}
220
+ paginator = client.get_paginator('list_objects')
221
+ pages = paginator.paginate(Bucket=bucket_name, Delimiter='/', Prefix=prefix)
222
+ for page in pages:
223
+ for entry in page.get('CommonPrefixes', ()):
224
+ key = posixpath.relpath(entry['Prefix'], prefix)
225
+ if not key in directories:
226
+ directories[key] = True
227
+ return directories.keys()
228
+
229
+ delete_set = dict(Objects=[])
230
+ for prefix in dir(bucket_name=settings.AWS_STORAGE_BUCKET_NAME, prefix=TEMP_MARKER):
231
+ response = client.list_objects(Bucket=settings.AWS_STORAGE_BUCKET_NAME, Prefix=prefix, MaxKeys=1)
232
+ for entry in response.get('Contents', ()):
233
+ diff_in_hours = (date.today() - entry['LastModified']).total_seconds() / 3600
234
+ # If the first file in the folder (prefix) is over a day old, delete the whole folder
235
+ if diff_in_hours >= 24:
236
+ delete_set['Objects'].append(dict(Key=entry['Key']))
237
+ if len(delete_set['Objects']) >= 1000:
238
+ client.delete_objects(Bucket=settings.AWS_STORAGE_BUCKET_NAME, Delete=delete_set)
239
+ delete_set = dict(Objects=[])
240
+ break
241
+ if len(delete_set['Objects']) > 0:
242
+ client.delete_objects(Bucket=settings.AWS_STORAGE_BUCKET_NAME, Delete=delete_set)
243
+
244
+
245
+ def s3_lifecycle_configuration():
246
+ client.put_bucket_lifecycle_configuration(
247
+ Bucket=settings.AWS_STORAGE_BUCKET_NAME,
248
+ LifecycleConfiguration={
249
+ 'Rules': [
250
+ {
251
+ 'ID': 'Expire fast',
252
+ 'Filter': {
253
+ 'Tag': {
254
+ 'Key': RETENTION,
255
+ 'Value': EXPIRE_FAST,
256
+ },
257
+ },
258
+ 'Status': 'Enabled',
259
+ 'Expiration': {
260
+ 'Days': 1,
261
+ },
262
+ },
263
+ {
264
+ 'ID': 'Expire slow',
265
+ 'Filter': {
266
+ 'Tag': {
267
+ 'Key': RETENTION,
268
+ 'Value': EXPIRE_SLOW,
269
+ },
270
+ },
271
+ 'Status': 'Enabled',
272
+ 'Expiration': {
273
+ 'Days': 180,
274
+ },
275
+ },
276
+ ]
277
+ }
278
+ )
@@ -0,0 +1,46 @@
1
+ from django.core.files.storage import default_storage
2
+ from rest_framework import fields
3
+ from rest_framework import serializers
4
+
5
+ from django_file_tools.s3 import bucket_exists
6
+ from django_file_tools.s3 import file_exists
7
+ from django_file_tools.s3 import files_exist_in_prefix
8
+
9
+
10
+ class FileField(fields.FileField):
11
+ """Similar to the standard serializer FileField but takes strings as paths in the storage instead of file objects"""
12
+ default_error_messages = fields.FileField.default_error_messages
13
+ default_error_messages['does not exist'] = 'The file referenced does not exist'
14
+
15
+ def get_storage(self):
16
+ return default_storage
17
+
18
+ def to_internal_value(self, data):
19
+ storage = self.get_storage()
20
+ if not storage.exists(data):
21
+ self.fail('does not exist')
22
+ return data
23
+
24
+
25
+ class FileFieldForModelSerializer(FileField):
26
+ def get_storage(self):
27
+ model = self.parent.Meta.model
28
+ return getattr(model, self.field_name).field.storage
29
+
30
+
31
+ class S3PathField(fields.CharField):
32
+ def to_internal_value(self, value):
33
+ if value:
34
+ value = value.replace('s3://', '')
35
+ bucket_name, prefix = value.split('/', 1)
36
+ if not bucket_exists(bucket_name):
37
+ raise serializers.ValidationError(f'bucket {bucket_name} does not exist')
38
+
39
+ if_file_exists = file_exists(bucket_name, prefix)
40
+ if_folder_exists = files_exist_in_prefix(bucket_name, prefix)
41
+ if (not if_file_exists) and (not if_folder_exists):
42
+ if not prefix.endswith('/'):
43
+ raise serializers.ValidationError(f'{value} does not exist')
44
+ else:
45
+ raise serializers.ValidationError(f'{value} has no files')
46
+ return value
@@ -0,0 +1,11 @@
1
+ import copy
2
+
3
+ from django.db import models
4
+ from rest_framework import serializers
5
+
6
+ from . import serializer_fields
7
+
8
+
9
+ class ModelSerializer(serializers.ModelSerializer):
10
+ serializer_field_mapping = copy.deepcopy(serializers.ModelSerializer.serializer_field_mapping)
11
+ serializer_field_mapping[models.FileField] = serializer_fields.FileFieldForModelSerializer
@@ -0,0 +1,61 @@
1
+ from storages.backends.s3boto3 import S3Boto3Storage
2
+ from storages.utils import clean_name
3
+
4
+
5
+ class StorageCopyMixin:
6
+ def copy(self, original, new_name, max_length=None, tags=None):
7
+ new_name = self.get_available_name(new_name, max_length=max_length)
8
+ return self._copy(original, new_name, tags)
9
+
10
+ def copy_from_other_bucket(self, bucket_name, key, new_name, max_length=None, tags=None):
11
+ new_name = self.get_available_name(new_name, max_length=max_length)
12
+ return self._copy_from_other_bucket(bucket_name, key, new_name, tags)
13
+
14
+
15
+ class S3Storage(StorageCopyMixin, S3Boto3Storage):
16
+ default_acl = 'private'
17
+ file_overwrite = False
18
+ custom_domain = False
19
+
20
+ @property
21
+ def s3_client(self):
22
+ return self.connection.meta.client
23
+
24
+ def set_tags(self, key, tags):
25
+ if tags is None:
26
+ return
27
+
28
+ tag_set = []
29
+ for k, v in tags.items():
30
+ tag_set.append({
31
+ 'Key': k,
32
+ 'Value': v,
33
+ })
34
+ self.s3_client.put_object_tagging(
35
+ Bucket=self.bucket_name,
36
+ Key=key,
37
+ Tagging={
38
+ 'TagSet': tag_set
39
+ }
40
+ )
41
+
42
+ def _copy(self, original, new_name, tags=None):
43
+ normalized_original = self._normalize_name(clean_name(original))
44
+ normalized_new_name = self._normalize_name(clean_name(new_name))
45
+ copy_source = {
46
+ 'Bucket': self.bucket_name,
47
+ 'Key': normalized_original
48
+ }
49
+ self.bucket.copy(copy_source, normalized_new_name)
50
+ self.set_tags(normalized_new_name, tags)
51
+ return normalized_new_name
52
+
53
+ def _copy_from_other_bucket(self, bucket_name, key, new_name, tags=None):
54
+ normalized_new_name = self._normalize_name(clean_name(new_name))
55
+ copy_source = {
56
+ 'Bucket': bucket_name,
57
+ 'Key': key
58
+ }
59
+ self.bucket.copy(copy_source, normalized_new_name)
60
+ self.set_tags(normalized_new_name, tags)
61
+ return normalized_new_name
@@ -0,0 +1,58 @@
1
+ import datetime
2
+
3
+ from django.conf import settings
4
+ from django.contrib.auth.decorators import login_required
5
+ from django.http.response import Http404
6
+ from django.http.response import JsonResponse
7
+
8
+ from django_file_tools.model_fields import TEMP_MARKER
9
+ from django_file_tools.s3 import EXPIRE_FAST
10
+ from django_file_tools.s3 import RETENTION
11
+ from django_file_tools.s3 import client
12
+
13
+
14
+ @login_required
15
+ def get_s3_signature(request):
16
+ service = 's3'
17
+ region = 'us-east-1'
18
+ t = datetime.datetime.utcnow()
19
+ algorithm = 'AWS4-HMAC-SHA256'
20
+ credential_scope = '/'.join([t.strftime('%Y%m%d'), region, service, 'aws4_request'])
21
+
22
+ name = request.GET.get('name')
23
+ if name is None:
24
+ raise Http404
25
+
26
+ def get_tag_xml(key, value):
27
+ return f"<Tagging><TagSet><Tag><Key>{key}</Key><Value>{value}</Value></Tag></TagSet></Tagging>"
28
+
29
+ conditions = [
30
+ {"x-amz-algorithm": algorithm},
31
+ {"x-amz-credential": credential_scope},
32
+ {"x-amz-date": t.isoformat()},
33
+ {"tagging": get_tag_xml(RETENTION, EXPIRE_FAST)},
34
+ {"success_action_status": "201"},
35
+ {"bucket": settings.AWS_STORAGE_BUCKET_NAME},
36
+ ["starts-with", "$key", TEMP_MARKER],
37
+ ]
38
+
39
+ fields = {
40
+ "x-amz-algorithm": algorithm,
41
+ "x-amz-credential": credential_scope,
42
+ "x-amz-date": t.isoformat(),
43
+ "tagging": get_tag_xml(RETENTION, EXPIRE_FAST),
44
+ "success_action_status": "201",
45
+ }
46
+
47
+ presigned = client.generate_presigned_post(
48
+ settings.AWS_STORAGE_BUCKET_NAME,
49
+ name,
50
+ Fields=fields,
51
+ Conditions=conditions,
52
+ ExpiresIn=7*24*60,
53
+ )
54
+
55
+ return JsonResponse({
56
+ 'signature': presigned['fields'],
57
+ 'postEndpoint': presigned['url'],
58
+ })
@@ -0,0 +1,26 @@
1
+ [tool.poetry]
2
+ name = "django-file-tools"
3
+ version = "0.1.0"
4
+ description = "Tools for working with files in Django"
5
+ authors = ["Enrico Barzetti"]
6
+ readme = "README.md"
7
+ homepage = "https://github.com/enricobarzetti/django-file-tools"
8
+ license = "mit"
9
+ packages = [
10
+ { include = "django_file_tools" },
11
+ ]
12
+
13
+ [tool.poetry.dependencies]
14
+ python = "^3.9"
15
+ django = ">=3.0"
16
+ django-storages = "^1.14"
17
+ djangorestframework = "^3.12.0"
18
+ boto3 = "^1.34.12"
19
+ django-environ = "^0.11.2"
20
+
21
+ [tool.poetry.group.dev.dependencies]
22
+ isort = "^5.13.2"
23
+
24
+ [build-system]
25
+ requires = ["poetry-core"]
26
+ build-backend = "poetry.core.masonry.api"