dcor-shared 0.5.4__tar.gz → 0.5.6__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.
- {dcor_shared-0.5.4 → dcor_shared-0.5.6}/CHANGELOG +5 -0
- {dcor_shared-0.5.4 → dcor_shared-0.5.6}/PKG-INFO +1 -1
- {dcor_shared-0.5.4 → dcor_shared-0.5.6}/dcor_shared/_version_save.py +1 -1
- {dcor_shared-0.5.4 → dcor_shared-0.5.6}/dcor_shared/s3.py +48 -28
- {dcor_shared-0.5.4 → dcor_shared-0.5.6}/dcor_shared.egg-info/PKG-INFO +1 -1
- {dcor_shared-0.5.4 → dcor_shared-0.5.6}/tests/test_s3.py +42 -2
- {dcor_shared-0.5.4 → dcor_shared-0.5.6}/LICENSE +0 -0
- {dcor_shared-0.5.4 → dcor_shared-0.5.6}/MANIFEST.in +0 -0
- {dcor_shared-0.5.4 → dcor_shared-0.5.6}/README.rst +0 -0
- {dcor_shared-0.5.4 → dcor_shared-0.5.6}/dcor_shared/__init__.py +0 -0
- {dcor_shared-0.5.4 → dcor_shared-0.5.6}/dcor_shared/_version.py +0 -0
- {dcor_shared-0.5.4 → dcor_shared-0.5.6}/dcor_shared/ckan.py +0 -0
- {dcor_shared-0.5.4 → dcor_shared-0.5.6}/dcor_shared/data.py +0 -0
- {dcor_shared-0.5.4 → dcor_shared-0.5.6}/dcor_shared/mime.py +0 -0
- {dcor_shared-0.5.4 → dcor_shared-0.5.6}/dcor_shared/parse.py +0 -0
- {dcor_shared-0.5.4 → dcor_shared-0.5.6}/dcor_shared/paths.py +0 -0
- {dcor_shared-0.5.4 → dcor_shared-0.5.6}/dcor_shared.egg-info/SOURCES.txt +0 -0
- {dcor_shared-0.5.4 → dcor_shared-0.5.6}/dcor_shared.egg-info/dependency_links.txt +0 -0
- {dcor_shared-0.5.4 → dcor_shared-0.5.6}/dcor_shared.egg-info/requires.txt +0 -0
- {dcor_shared-0.5.4 → dcor_shared-0.5.6}/dcor_shared.egg-info/top_level.txt +0 -0
- {dcor_shared-0.5.4 → dcor_shared-0.5.6}/setup.cfg +0 -0
- {dcor_shared-0.5.4 → dcor_shared-0.5.6}/setup.py +0 -0
- {dcor_shared-0.5.4 → dcor_shared-0.5.6}/tests/test_basic.py +0 -0
- {dcor_shared-0.5.4 → dcor_shared-0.5.6}/tests/test_data.py +0 -0
|
@@ -11,6 +11,36 @@ import botocore.exceptions
|
|
|
11
11
|
from .ckan import get_ckan_config_option
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
def compute_checksum(bucket_name, object_name, max_size=None):
|
|
15
|
+
"""Compute the SHA256 checksum of an S3 object
|
|
16
|
+
|
|
17
|
+
The hash is computed in memory as the file is downloaded in chunks.
|
|
18
|
+
"""
|
|
19
|
+
s3_client, _, s3_resource = get_s3()
|
|
20
|
+
if max_size is None:
|
|
21
|
+
obj = s3_resource.Object(bucket_name=bucket_name, key=object_name)
|
|
22
|
+
max_size = obj.content_length
|
|
23
|
+
hasher = hashlib.sha256()
|
|
24
|
+
# This is an increment of 1MB. If you change this, please also update
|
|
25
|
+
# the tests for large uploads.
|
|
26
|
+
increment = 2 ** 20
|
|
27
|
+
start_byte = 0
|
|
28
|
+
stop_byte = min(increment, max_size)
|
|
29
|
+
while start_byte < max_size:
|
|
30
|
+
resp = s3_client.get_object(
|
|
31
|
+
Bucket=bucket_name,
|
|
32
|
+
Key=object_name,
|
|
33
|
+
Range=f"bytes={start_byte}-{stop_byte}")
|
|
34
|
+
content = resp['Body'].read()
|
|
35
|
+
if not content:
|
|
36
|
+
break
|
|
37
|
+
hasher.update(content)
|
|
38
|
+
start_byte = stop_byte + 1 # range is inclusive
|
|
39
|
+
stop_byte = min(max_size, stop_byte + increment)
|
|
40
|
+
s3_sha256 = hasher.hexdigest()
|
|
41
|
+
return s3_sha256
|
|
42
|
+
|
|
43
|
+
|
|
14
44
|
def create_presigned_url(bucket_name, object_name, expiration=3600,
|
|
15
45
|
filename=None):
|
|
16
46
|
"""Generate a presigned URL to share an S3 object
|
|
@@ -165,6 +195,19 @@ def make_object_public(bucket_name, object_name, missing_ok=False):
|
|
|
165
195
|
)
|
|
166
196
|
|
|
167
197
|
|
|
198
|
+
def object_exists(bucket_name, object_name):
|
|
199
|
+
"""Check whether an object exists"""
|
|
200
|
+
s3_client, _, _ = get_s3()
|
|
201
|
+
try:
|
|
202
|
+
s3_client.head_object(Bucket=bucket_name, Key=object_name)
|
|
203
|
+
except (s3_client.exceptions.NoSuchKey,
|
|
204
|
+
s3_client.exceptions.ClientError):
|
|
205
|
+
obj_exists = False
|
|
206
|
+
else:
|
|
207
|
+
obj_exists = True
|
|
208
|
+
return obj_exists
|
|
209
|
+
|
|
210
|
+
|
|
168
211
|
@functools.lru_cache()
|
|
169
212
|
def require_bucket(bucket_name):
|
|
170
213
|
"""Create an S3 bucket if it does not exist yet
|
|
@@ -272,14 +315,8 @@ def upload_file(bucket_name, object_name, path, sha256, private=True,
|
|
|
272
315
|
|
|
273
316
|
perform_upload = True
|
|
274
317
|
if not override:
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
except (s3_client.exceptions.NoSuchKey,
|
|
278
|
-
s3_client.exceptions.ClientError):
|
|
279
|
-
object_exists = False
|
|
280
|
-
else:
|
|
281
|
-
object_exists = True
|
|
282
|
-
perform_upload = not object_exists
|
|
318
|
+
perform_upload = not object_exists(bucket_name=bucket_name,
|
|
319
|
+
object_name=object_name)
|
|
283
320
|
|
|
284
321
|
if perform_upload:
|
|
285
322
|
s3_bucket.upload_file(Filename=str(path),
|
|
@@ -292,26 +329,9 @@ def upload_file(bucket_name, object_name, path, sha256, private=True,
|
|
|
292
329
|
# }
|
|
293
330
|
)
|
|
294
331
|
# Make sure the upload worked properly by computing the SHA256 sum.
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
# the tests for large uploads.
|
|
299
|
-
increment = 2**20
|
|
300
|
-
start_byte = 0
|
|
301
|
-
max_size = path_size
|
|
302
|
-
stop_byte = min(increment, max_size)
|
|
303
|
-
while start_byte < max_size:
|
|
304
|
-
resp = s3_client.get_object(
|
|
305
|
-
Bucket=bucket_name,
|
|
306
|
-
Key=object_name,
|
|
307
|
-
Range=f"bytes={start_byte}-{stop_byte}")
|
|
308
|
-
content = resp['Body'].read()
|
|
309
|
-
if not content:
|
|
310
|
-
break
|
|
311
|
-
hasher.update(content)
|
|
312
|
-
start_byte = stop_byte + 1 # range is inclusive
|
|
313
|
-
stop_byte = min(max_size, stop_byte + increment)
|
|
314
|
-
s3_sha256 = hasher.hexdigest()
|
|
332
|
+
s3_sha256 = compute_checksum(bucket_name=bucket_name,
|
|
333
|
+
object_name=object_name,
|
|
334
|
+
max_size=path_size)
|
|
315
335
|
if sha256 != s3_sha256:
|
|
316
336
|
raise ValueError(
|
|
317
337
|
f"Checksum mismatch for {bucket_name}:{object_name}!")
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import hashlib
|
|
2
|
+
from unittest import mock
|
|
2
3
|
import pathlib
|
|
3
4
|
import uuid
|
|
4
5
|
|
|
@@ -11,12 +12,30 @@ from dcor_shared import s3, sha256sum
|
|
|
11
12
|
data_path = pathlib.Path(__file__).parent / "data"
|
|
12
13
|
|
|
13
14
|
|
|
15
|
+
def test_compute_checksum():
|
|
16
|
+
path = data_path / "calibration_beads_47.rtdc"
|
|
17
|
+
bucket_name = f"test-circle-{uuid.uuid4()}"
|
|
18
|
+
rid = str(uuid.uuid4())
|
|
19
|
+
object_name = f"resource/{rid[:3]}/{rid[3:6]}/{rid[6:]}"
|
|
20
|
+
s3.upload_file(
|
|
21
|
+
bucket_name=bucket_name,
|
|
22
|
+
object_name=object_name,
|
|
23
|
+
path=path,
|
|
24
|
+
sha256=sha256sum(path),
|
|
25
|
+
private=True)
|
|
26
|
+
|
|
27
|
+
hash_exp = hashlib.sha256(path.read_bytes()).hexdigest()
|
|
28
|
+
hash_act = s3.compute_checksum(bucket_name=bucket_name,
|
|
29
|
+
object_name=object_name)
|
|
30
|
+
assert hash_exp == hash_act
|
|
31
|
+
|
|
32
|
+
|
|
14
33
|
def test_create_bucket_again():
|
|
15
34
|
bucket_name = f"test-circle-{uuid.uuid4()}"
|
|
16
35
|
bucket = s3.require_bucket(bucket_name)
|
|
17
36
|
# this is cached
|
|
18
37
|
bucket2 = s3.require_bucket(bucket_name)
|
|
19
|
-
assert bucket2 is bucket, "
|
|
38
|
+
assert bucket2 is bucket, "cached"
|
|
20
39
|
s3.require_bucket.cache_clear()
|
|
21
40
|
bucket3 = s3.require_bucket(bucket_name)
|
|
22
41
|
assert bucket3 is not bucket, "new object"
|
|
@@ -77,6 +96,27 @@ def test_make_object_public_no_such_key(tmp_path):
|
|
|
77
96
|
missing_ok=False)
|
|
78
97
|
|
|
79
98
|
|
|
99
|
+
def test_object_exists():
|
|
100
|
+
path = data_path / "calibration_beads_47.rtdc"
|
|
101
|
+
bucket_name = f"test-circle-{uuid.uuid4()}"
|
|
102
|
+
rid = str(uuid.uuid4())
|
|
103
|
+
object_name = f"resource/{rid[:3]}/{rid[3:6]}/{rid[6:]}"
|
|
104
|
+
s3.upload_file(
|
|
105
|
+
bucket_name=bucket_name,
|
|
106
|
+
object_name=object_name,
|
|
107
|
+
path=path,
|
|
108
|
+
sha256=sha256sum(path),
|
|
109
|
+
private=True)
|
|
110
|
+
|
|
111
|
+
assert s3.object_exists(bucket_name=bucket_name,
|
|
112
|
+
object_name=object_name)
|
|
113
|
+
# sanity checks
|
|
114
|
+
assert not s3.object_exists(bucket_name=bucket_name,
|
|
115
|
+
object_name=f"peter/pan-{uuid.uuid4()}")
|
|
116
|
+
assert not s3.object_exists(bucket_name=f"hansgunter-{uuid.uuid4()}",
|
|
117
|
+
object_name=object_name)
|
|
118
|
+
|
|
119
|
+
|
|
80
120
|
def test_presigned_url(tmp_path):
|
|
81
121
|
path = data_path / "calibration_beads_47.rtdc"
|
|
82
122
|
bucket_name = f"test-circle-{uuid.uuid4()}"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|