cloudbridge 4.2.0__tar.gz → 4.2.1__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.
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/PKG-INFO +5 -2
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/__init__.py +1 -1
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/interfaces/resources.py +17 -1
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/aws/resources.py +10 -2
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/azure/azure_client.py +6 -2
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/azure/resources.py +6 -2
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/gcp/resources.py +13 -2
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/openstack/resources.py +21 -3
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge.egg-info/PKG-INFO +5 -2
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/pyproject.toml +7 -2
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/tests/test_object_store_service.py +63 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/LICENSE +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/README.rst +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/base/__init__.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/base/helpers.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/base/middleware.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/base/provider.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/base/resources.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/base/services.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/base/subservices.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/factory.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/interfaces/__init__.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/interfaces/exceptions.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/interfaces/provider.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/interfaces/services.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/interfaces/subservices.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/__init__.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/aws/__init__.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/aws/helpers.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/aws/provider.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/aws/services.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/aws/subservices.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/azure/__init__.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/azure/helpers.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/azure/provider.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/azure/services.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/azure/subservices.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/gcp/__init__.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/gcp/helpers.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/gcp/provider.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/gcp/services.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/gcp/subservices.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/mock/__init__.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/mock/provider.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/openstack/__init__.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/openstack/helpers.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/openstack/provider.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/openstack/services.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/openstack/subservices.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/py.typed +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge.egg-info/SOURCES.txt +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge.egg-info/dependency_links.txt +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge.egg-info/requires.txt +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge.egg-info/top_level.txt +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/setup.cfg +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/tests/test_base_helpers.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/tests/test_block_store_service.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/tests/test_cloud_factory.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/tests/test_cloud_helpers.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/tests/test_compute_service.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/tests/test_dns_service.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/tests/test_image_service.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/tests/test_interface.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/tests/test_middleware_system.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/tests/test_multipart_driver.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/tests/test_network_service.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/tests/test_object_life_cycle.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/tests/test_region_service.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/tests/test_security_service.py +0 -0
- {cloudbridge-4.2.0 → cloudbridge-4.2.1}/tests/test_vm_types_service.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cloudbridge
|
|
3
|
-
Version: 4.2.
|
|
3
|
+
Version: 4.2.1
|
|
4
4
|
Summary: A simple layer of abstraction over multiple cloud providers.
|
|
5
5
|
Author-email: Galaxy and GVL Projects <help@genome.edu.au>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -15,10 +15,13 @@ Classifier: Intended Audience :: System Administrators
|
|
|
15
15
|
Classifier: Operating System :: OS Independent
|
|
16
16
|
Classifier: Programming Language :: Python
|
|
17
17
|
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
21
|
Classifier: Programming Language :: Python :: 3.13
|
|
19
22
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
20
23
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
-
Requires-Python: >=3.
|
|
24
|
+
Requires-Python: >=3.10
|
|
22
25
|
Description-Content-Type: text/x-rst
|
|
23
26
|
License-File: LICENSE
|
|
24
27
|
Requires-Dist: tenacity>=6.0
|
|
@@ -2435,7 +2435,9 @@ class BucketObject(CloudResource):
|
|
|
2435
2435
|
pass
|
|
2436
2436
|
|
|
2437
2437
|
@abstractmethod
|
|
2438
|
-
def generate_url(self, expires_in: int, writable: bool = False
|
|
2438
|
+
def generate_url(self, expires_in: int, writable: bool = False,
|
|
2439
|
+
content_disposition: str | None = None,
|
|
2440
|
+
content_type: str | None = None) -> str:
|
|
2439
2441
|
"""
|
|
2440
2442
|
Generate a signed URL to this object.
|
|
2441
2443
|
|
|
@@ -2449,6 +2451,20 @@ class BucketObject(CloudResource):
|
|
|
2449
2451
|
:param writable: Write permission for this signed URL. Users with the URL
|
|
2450
2452
|
will be able to upload to this object, but they will NOT be able to
|
|
2451
2453
|
read from it.
|
|
2454
|
+
:type content_disposition: ``str``
|
|
2455
|
+
:param content_disposition: When set, ask the backing store to serve
|
|
2456
|
+
the object with this ``Content-Disposition`` response header on
|
|
2457
|
+
GET (e.g. ``attachment; filename="data.txt"``). This is a
|
|
2458
|
+
response-serving hint: every provider accepts it and honors it
|
|
2459
|
+
where the backing store supports response-header overrides (AWS,
|
|
2460
|
+
Azure and GCP fully; OpenStack Swift honors the filename portion
|
|
2461
|
+
via its tempurl ``filename`` parameter). Ignored when
|
|
2462
|
+
``writable`` is ``True``.
|
|
2463
|
+
:type content_type: ``str``
|
|
2464
|
+
:param content_type: When set, ask the backing store to serve the
|
|
2465
|
+
object with this ``Content-Type`` response header on GET. Honored
|
|
2466
|
+
by AWS, Azure and GCP; OpenStack Swift cannot override the
|
|
2467
|
+
content type. Ignored when ``writable`` is ``True``.
|
|
2452
2468
|
|
|
2453
2469
|
:rtype: ``str``
|
|
2454
2470
|
:return: A URL to access the object.
|
|
@@ -947,11 +947,19 @@ class AWSBucketObject(BaseBucketObject):
|
|
|
947
947
|
def delete(self) -> None:
|
|
948
948
|
self._obj.delete()
|
|
949
949
|
|
|
950
|
-
def generate_url(self, expires_in: int, writable: bool = False
|
|
950
|
+
def generate_url(self, expires_in: int, writable: bool = False,
|
|
951
|
+
content_disposition: str | None = None,
|
|
952
|
+
content_type: str | None = None) -> str:
|
|
953
|
+
params = {'Bucket': self._obj.bucket_name, 'Key': self.id}
|
|
954
|
+
if not writable:
|
|
955
|
+
if content_disposition:
|
|
956
|
+
params['ResponseContentDisposition'] = content_disposition
|
|
957
|
+
if content_type:
|
|
958
|
+
params['ResponseContentType'] = content_type
|
|
951
959
|
return cast("AWSCloudProvider", self._provider).s3_conn.meta.client \
|
|
952
960
|
.generate_presigned_url(
|
|
953
961
|
'put_object' if writable else 'get_object',
|
|
954
|
-
Params=
|
|
962
|
+
Params=params,
|
|
955
963
|
ExpiresIn=expires_in)
|
|
956
964
|
|
|
957
965
|
def refresh(self) -> None:
|
|
@@ -508,7 +508,9 @@ class AzureClient(object):
|
|
|
508
508
|
blob_client.delete_blob(delete_snapshots)
|
|
509
509
|
|
|
510
510
|
def get_blob_url(self, container_name: Any, blob_name: str,
|
|
511
|
-
expiry_time: int, writable: bool
|
|
511
|
+
expiry_time: int, writable: bool,
|
|
512
|
+
content_disposition: str | None = None,
|
|
513
|
+
content_type: str | None = None) -> str:
|
|
512
514
|
now = datetime.datetime.utcnow()
|
|
513
515
|
expiry = now + datetime.timedelta(
|
|
514
516
|
seconds=expiry_time)
|
|
@@ -520,7 +522,9 @@ class AzureClient(object):
|
|
|
520
522
|
sas = generate_blob_sas(
|
|
521
523
|
self.storage_account, container_name, blob_name,
|
|
522
524
|
permission=BlobSasPermissions(read=True, write=writable), expiry=expiry,
|
|
523
|
-
user_delegation_key=delegation_key
|
|
525
|
+
user_delegation_key=delegation_key,
|
|
526
|
+
content_disposition=content_disposition,
|
|
527
|
+
content_type=content_type
|
|
524
528
|
)
|
|
525
529
|
url = f"https://{self.storage_account}.blob.core.windows.net/{container_name}/{blob_name}?{sas}"
|
|
526
530
|
return url
|
|
@@ -311,13 +311,17 @@ class AzureBucketObject(BaseBucketObject):
|
|
|
311
311
|
"""
|
|
312
312
|
self._blob_client.delete_blob()
|
|
313
313
|
|
|
314
|
-
def generate_url(self, expires_in: int, writable: bool = False
|
|
314
|
+
def generate_url(self, expires_in: int, writable: bool = False,
|
|
315
|
+
content_disposition: str | None = None,
|
|
316
|
+
content_type: str | None = None) -> str:
|
|
315
317
|
"""
|
|
316
318
|
Generate a URL to this object.
|
|
317
319
|
"""
|
|
318
320
|
return cast(
|
|
319
321
|
"AzureCloudProvider", self._provider).azure_client.get_blob_url(
|
|
320
|
-
self._container, self.name, expires_in, writable
|
|
322
|
+
self._container, self.name, expires_in, writable,
|
|
323
|
+
content_disposition=None if writable else content_disposition,
|
|
324
|
+
content_type=None if writable else content_type)
|
|
321
325
|
|
|
322
326
|
def refresh(self) -> None:
|
|
323
327
|
pass
|
|
@@ -2198,7 +2198,9 @@ class GCPBucketObject(BaseBucketObject):
|
|
|
2198
2198
|
.delete(bucket=self._obj['bucket'], object=self.name)
|
|
2199
2199
|
.execute())
|
|
2200
2200
|
|
|
2201
|
-
def generate_url(self, expires_in: int, writable: bool = False
|
|
2201
|
+
def generate_url(self, expires_in: int, writable: bool = False,
|
|
2202
|
+
content_disposition: str | None = None,
|
|
2203
|
+
content_type: str | None = None) -> str:
|
|
2202
2204
|
"""
|
|
2203
2205
|
Generates a signed URL accessible to everyone.
|
|
2204
2206
|
|
|
@@ -2210,10 +2212,19 @@ class GCPBucketObject(BaseBucketObject):
|
|
|
2210
2212
|
provider = cast("GCPCloudProvider", self._provider)
|
|
2211
2213
|
http_method = "PUT" if writable else "GET"
|
|
2212
2214
|
|
|
2215
|
+
query_parameters: dict[str, Any] = {}
|
|
2216
|
+
if not writable:
|
|
2217
|
+
if content_disposition:
|
|
2218
|
+
query_parameters[
|
|
2219
|
+
'response-content-disposition'] = content_disposition
|
|
2220
|
+
if content_type:
|
|
2221
|
+
query_parameters['response-content-type'] = content_type
|
|
2222
|
+
|
|
2213
2223
|
# pylint:disable=protected-access
|
|
2214
2224
|
return helpers.generate_signed_url(
|
|
2215
2225
|
provider._credentials, self._obj['bucket'], self.name,
|
|
2216
|
-
expiration=expires_in, http_method=http_method
|
|
2226
|
+
expiration=expires_in, http_method=http_method,
|
|
2227
|
+
query_parameters=query_parameters or None)
|
|
2217
2228
|
|
|
2218
2229
|
def refresh(self) -> None:
|
|
2219
2230
|
# pylint:disable=protected-access
|
|
@@ -14,6 +14,7 @@ from typing import IO
|
|
|
14
14
|
from typing import Iterable
|
|
15
15
|
from typing import TYPE_CHECKING
|
|
16
16
|
from typing import cast
|
|
17
|
+
from urllib.parse import quote
|
|
17
18
|
from urllib.parse import urljoin
|
|
18
19
|
from urllib.parse import urlparse
|
|
19
20
|
|
|
@@ -1474,7 +1475,9 @@ class OpenStackBucketObject(BaseBucketObject):
|
|
|
1474
1475
|
for del_res in swift.delete(self.cbcontainer.name, [self.name, ]):
|
|
1475
1476
|
del_res['success']
|
|
1476
1477
|
|
|
1477
|
-
def generate_url(self, expires_in: int, writable: bool = False
|
|
1478
|
+
def generate_url(self, expires_in: int, writable: bool = False,
|
|
1479
|
+
content_disposition: str | None = None,
|
|
1480
|
+
content_type: str | None = None) -> str:
|
|
1478
1481
|
http_method = "PUT" if writable else "GET"
|
|
1479
1482
|
# Set a temp url key on the object (http://bit.ly/2NBiXGD)
|
|
1480
1483
|
temp_url_key = "cloudbridge-tmp-url-key"
|
|
@@ -1484,8 +1487,23 @@ class OpenStackBucketObject(BaseBucketObject):
|
|
|
1484
1487
|
base_url = urlparse(swift.get_service_auth()[0])
|
|
1485
1488
|
access_point = "{0}://{1}".format(base_url.scheme, base_url.netloc)
|
|
1486
1489
|
url_path = "/".join([base_url.path, self.cbcontainer.name, self.name])
|
|
1487
|
-
|
|
1488
|
-
|
|
1490
|
+
url = urljoin(access_point, generate_temp_url(url_path, expires_in,
|
|
1491
|
+
temp_url_key,
|
|
1492
|
+
http_method))
|
|
1493
|
+
if not writable and content_disposition:
|
|
1494
|
+
# Swift's tempurl middleware serves Content-Disposition via the
|
|
1495
|
+
# unsigned `filename`/`inline` query parameters (the signature
|
|
1496
|
+
# covers only method, path and expiry); Content-Type cannot be
|
|
1497
|
+
# overridden.
|
|
1498
|
+
match = re.search(r'filename\*?=(?:"([^"]+)"|([^;]+))',
|
|
1499
|
+
content_disposition)
|
|
1500
|
+
filename = (match.group(1) or match.group(2)).strip() \
|
|
1501
|
+
if match else None
|
|
1502
|
+
if content_disposition.strip().lower().startswith('inline'):
|
|
1503
|
+
url += '&inline'
|
|
1504
|
+
if filename:
|
|
1505
|
+
url += '&filename=' + quote(filename)
|
|
1506
|
+
return url
|
|
1489
1507
|
|
|
1490
1508
|
def refresh(self) -> None:
|
|
1491
1509
|
self._obj = self.cbcontainer.objects.get(self.id)._obj
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cloudbridge
|
|
3
|
-
Version: 4.2.
|
|
3
|
+
Version: 4.2.1
|
|
4
4
|
Summary: A simple layer of abstraction over multiple cloud providers.
|
|
5
5
|
Author-email: Galaxy and GVL Projects <help@genome.edu.au>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -15,10 +15,13 @@ Classifier: Intended Audience :: System Administrators
|
|
|
15
15
|
Classifier: Operating System :: OS Independent
|
|
16
16
|
Classifier: Programming Language :: Python
|
|
17
17
|
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
21
|
Classifier: Programming Language :: Python :: 3.13
|
|
19
22
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
20
23
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
-
Requires-Python: >=3.
|
|
24
|
+
Requires-Python: >=3.10
|
|
22
25
|
Description-Content-Type: text/x-rst
|
|
23
26
|
License-File: LICENSE
|
|
24
27
|
Requires-Dist: tenacity>=6.0
|
|
@@ -8,7 +8,7 @@ description = "A simple layer of abstraction over multiple cloud providers."
|
|
|
8
8
|
readme = "README.rst"
|
|
9
9
|
license = "MIT"
|
|
10
10
|
license-files = ["LICENSE"]
|
|
11
|
-
requires-python = ">=3.
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
12
|
authors = [
|
|
13
13
|
{ name = "Galaxy and GVL Projects", email = "help@genome.edu.au" },
|
|
14
14
|
]
|
|
@@ -21,6 +21,9 @@ classifiers = [
|
|
|
21
21
|
"Operating System :: OS Independent",
|
|
22
22
|
"Programming Language :: Python",
|
|
23
23
|
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
24
27
|
"Programming Language :: Python :: 3.13",
|
|
25
28
|
"Programming Language :: Python :: Implementation :: CPython",
|
|
26
29
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
@@ -118,7 +121,9 @@ parallel = true
|
|
|
118
121
|
# factory) is held to a strict baseline so downstream users get a fully-typed
|
|
119
122
|
# API; the base implementations and providers (which wrap untyped cloud SDKs)
|
|
120
123
|
# are temporarily exempted below and ratcheted to strict module-by-module.
|
|
121
|
-
|
|
124
|
+
# Type-check against the LOWEST supported version so use of newer-stdlib
|
|
125
|
+
# APIs is caught at lint time.
|
|
126
|
+
python_version = "3.10"
|
|
122
127
|
files = ["cloudbridge"]
|
|
123
128
|
ignore_missing_imports = true # untyped cloud SDKs (boto3, azure-*, ...) -> Any
|
|
124
129
|
namespace_packages = true
|
|
@@ -229,6 +229,69 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
|
|
|
229
229
|
obj.save_content(target_stream)
|
|
230
230
|
self.assertEqual(target_stream.getvalue(), content)
|
|
231
231
|
|
|
232
|
+
@helpers.skipIfNoService(['storage.buckets'])
|
|
233
|
+
def test_generate_url_with_response_headers(self):
|
|
234
|
+
name = "cbtestbucketobjs-{0}".format(helpers.get_uuid())
|
|
235
|
+
test_bucket = self.provider.storage.buckets.create(name)
|
|
236
|
+
|
|
237
|
+
with cb_helpers.cleanup_action(lambda: test_bucket.delete()):
|
|
238
|
+
obj_name = "hello_response_headers.txt"
|
|
239
|
+
obj = test_bucket.objects.create(obj_name)
|
|
240
|
+
|
|
241
|
+
with cb_helpers.cleanup_action(lambda: obj.delete()):
|
|
242
|
+
content = b"Hello World. Serve me with response headers."
|
|
243
|
+
obj.upload(content)
|
|
244
|
+
|
|
245
|
+
disposition = 'attachment; filename="hello.txt"'
|
|
246
|
+
content_type = "application/octet-stream"
|
|
247
|
+
url = obj.generate_url(100,
|
|
248
|
+
content_disposition=disposition,
|
|
249
|
+
content_type=content_type)
|
|
250
|
+
if isinstance(self.provider, TestMockHelperMixin):
|
|
251
|
+
# Presigned URLs are constructed client-side, so the
|
|
252
|
+
# response-header overrides can be asserted on the query
|
|
253
|
+
# string without a network round trip.
|
|
254
|
+
self.assertIn('response-content-disposition', url)
|
|
255
|
+
self.assertIn('response-content-type', url)
|
|
256
|
+
raise self.skipTest(
|
|
257
|
+
"Skipping rest of test - mock providers can't"
|
|
258
|
+
" access generated url")
|
|
259
|
+
response = requests.get(url)
|
|
260
|
+
self.assertEqual(response.content, content)
|
|
261
|
+
got_disposition = response.headers.get(
|
|
262
|
+
'Content-Disposition', '')
|
|
263
|
+
self.assertTrue(got_disposition.startswith('attachment'),
|
|
264
|
+
"Expected attachment disposition, got: "
|
|
265
|
+
f"{got_disposition}")
|
|
266
|
+
self.assertIn('hello.txt', got_disposition)
|
|
267
|
+
if self.provider.PROVIDER_ID != 'openstack':
|
|
268
|
+
# Swift's tempurl middleware cannot override Content-Type
|
|
269
|
+
# (it only honors the filename portion of the disposition
|
|
270
|
+
# via its `filename` query parameter).
|
|
271
|
+
self.assertEqual(response.headers.get('Content-Type'),
|
|
272
|
+
content_type)
|
|
273
|
+
|
|
274
|
+
@helpers.skipIfNoService(['storage.buckets'])
|
|
275
|
+
def test_generate_url_writable_ignores_response_headers(self):
|
|
276
|
+
name = "cbtestbucketobjs-{0}".format(helpers.get_uuid())
|
|
277
|
+
test_bucket = self.provider.storage.buckets.create(name)
|
|
278
|
+
|
|
279
|
+
with cb_helpers.cleanup_action(lambda: test_bucket.delete()):
|
|
280
|
+
obj_name = "hello_response_headers.txt"
|
|
281
|
+
obj = test_bucket.objects.create(obj_name)
|
|
282
|
+
|
|
283
|
+
with cb_helpers.cleanup_action(lambda: obj.delete()):
|
|
284
|
+
url = obj.generate_url(
|
|
285
|
+
100, writable=True,
|
|
286
|
+
content_disposition='attachment; filename="hello.txt"',
|
|
287
|
+
content_type="application/octet-stream")
|
|
288
|
+
# Response-header overrides only make sense for reads; a
|
|
289
|
+
# writable URL must not carry them (across all providers:
|
|
290
|
+
# AWS/GCP query params, Azure SAS rscd/rsct, Swift filename).
|
|
291
|
+
self.assertNotIn('response-content-disposition', url)
|
|
292
|
+
self.assertNotIn('rscd=', url)
|
|
293
|
+
self.assertNotIn('filename=', url)
|
|
294
|
+
|
|
232
295
|
@helpers.skipIfNoService(['storage.buckets'])
|
|
233
296
|
def test_upload_download_bucket_content_from_file(self):
|
|
234
297
|
name = "cbtestbucketobjs-{0}".format(helpers.get_uuid())
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|