x2s3 0.8.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.
- x2s3-0.8.0/LICENSE +28 -0
- x2s3-0.8.0/PKG-INFO +120 -0
- x2s3-0.8.0/README.md +92 -0
- x2s3-0.8.0/pyproject.toml +40 -0
- x2s3-0.8.0/setup.cfg +4 -0
- x2s3-0.8.0/tests/test_awss3.py +297 -0
- x2s3-0.8.0/tests/test_base.py +41 -0
- x2s3-0.8.0/tests/test_boto.py +154 -0
- x2s3-0.8.0/tests/test_file.py +117 -0
- x2s3-0.8.0/x2s3/__init__.py +4 -0
- x2s3-0.8.0/x2s3/app.py +309 -0
- x2s3-0.8.0/x2s3/client.py +34 -0
- x2s3-0.8.0/x2s3/client_aioboto.py +292 -0
- x2s3-0.8.0/x2s3/client_file.py +208 -0
- x2s3-0.8.0/x2s3/client_registry.py +181 -0
- x2s3-0.8.0/x2s3/settings.py +85 -0
- x2s3-0.8.0/x2s3/utils.py +233 -0
- x2s3-0.8.0/x2s3.egg-info/PKG-INFO +120 -0
- x2s3-0.8.0/x2s3.egg-info/SOURCES.txt +20 -0
- x2s3-0.8.0/x2s3.egg-info/dependency_links.txt +1 -0
- x2s3-0.8.0/x2s3.egg-info/requires.txt +12 -0
- x2s3-0.8.0/x2s3.egg-info/top_level.txt +1 -0
x2s3-0.8.0/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024, Howard Hughes Medical Institute
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
x2s3-0.8.0/PKG-INFO
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: x2s3
|
|
3
|
+
Version: 0.8.0
|
|
4
|
+
Summary: RESTful web service which makes any storage system X available as an S3-compatible REST API
|
|
5
|
+
Author-email: Konrad Rokicki <rokicki@janelia.hhmi.org>
|
|
6
|
+
License: BSD-3-Clause
|
|
7
|
+
Project-URL: Homepage, https://github.com/JaneliaSciComp/x2s3
|
|
8
|
+
Project-URL: Repository, https://github.com/JaneliaSciComp/x2s3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.6
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: aiobotocore
|
|
16
|
+
Requires-Dist: boto3
|
|
17
|
+
Requires-Dist: botocore
|
|
18
|
+
Requires-Dist: fastapi
|
|
19
|
+
Requires-Dist: loguru
|
|
20
|
+
Requires-Dist: pydantic
|
|
21
|
+
Requires-Dist: pydantic-settings
|
|
22
|
+
Requires-Dist: python-dateutil
|
|
23
|
+
Requires-Dist: pytest
|
|
24
|
+
Requires-Dist: starlette
|
|
25
|
+
Requires-Dist: typing_extensions
|
|
26
|
+
Requires-Dist: uvicorn
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# x2s3
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
33
|
+
RESTful web service which makes any storage system *X* available as an S3-compatible REST API, hence the name "X to S3". It was initially built to support cloud-compatible microscopy image viewers such as [N5 Viewer](https://github.com/saalfeldlab/n5-viewer) (BigDataViewer) and [Neuroglancer](https://github.com/google/neuroglancer).
|
|
34
|
+
|
|
35
|
+
At Janelia, we use **x2s3** to make private buckets on Seagate Lyve appear public, and to proxy internal resources (e.g. VAST S3). It can also be used as a pop-up file service for quickly viewing local images in BigDataViewer or Neuroglancer.
|
|
36
|
+
|
|
37
|
+
<p align="center">
|
|
38
|
+
<img src="https://raw.githubusercontent.com/JaneliaSciComp/x2s3/main/docs/use_cases.png">
|
|
39
|
+
</p>
|
|
40
|
+
|
|
41
|
+
# Features
|
|
42
|
+
|
|
43
|
+
* Extensible support for backend storage systems
|
|
44
|
+
* Optional web-based bucket explorer
|
|
45
|
+
* Hidden buckets
|
|
46
|
+
* Partial buckets (chroot-like prefixes)
|
|
47
|
+
* Non-blocking object streaming
|
|
48
|
+
|
|
49
|
+
Inspired by S3 proxies such as [oxyno-zeta/s3-proxy](https://github.com/oxyno-zeta/s3-proxy) and [pottava/aws-s3-proxy](https://github.com/pottava/aws-s3-proxy), this service goes a step further to implement enough of the [AWS S3 API](https://docs.aws.amazon.com/AmazonS3/latest/API/Type_API_Reference.html) to be useable by AWS clients, such as BigDataViewer. The S3 proxy implements which do this well (e.g. [gaul/s3proxy](https://github.com/gaul/s3proxy)) only proxy a single bucket at a time.
|
|
50
|
+
|
|
51
|
+
S3 endpoints implemented:
|
|
52
|
+
* [GetBucketAcl](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketAcl.html)
|
|
53
|
+
* [GetObject](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html)
|
|
54
|
+
* [HeadObject](https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html)
|
|
55
|
+
* [ListBuckets](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListBuckets.html)
|
|
56
|
+
* [ListObjectsV2](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html)
|
|
57
|
+
|
|
58
|
+
S3 features omitted:
|
|
59
|
+
* Permissions
|
|
60
|
+
* Encryption
|
|
61
|
+
* Versioning
|
|
62
|
+
* RequestPayer
|
|
63
|
+
* etc.
|
|
64
|
+
|
|
65
|
+
# Running
|
|
66
|
+
|
|
67
|
+
Create a `config.yaml` file that contains all of the buckets you want to serve. You can get started quickly by using the provided example template:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
cp config.template.yaml config.yaml
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
See the [documentation](docs/Config.md) for more information about the configuration file.
|
|
74
|
+
|
|
75
|
+
The simplest way to run the service is to use Docker:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
docker run -it -p 8000:8000 -v ./config.yaml:/app/x2s3/config.yaml ghcr.io/janeliascicomp/x2s3:latest
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
You can also run the service with Python/Uvicorn directly. See the [development documentation](docs/Development.md) for more information on setting that up.
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
## Production Deployment
|
|
85
|
+
|
|
86
|
+
For production deployments, we recommend using an orchestrator (like Docker Compose) to run the prebuilt Docker container along with an Nginx reverse proxy which provides caching and TLS termination.
|
|
87
|
+
|
|
88
|
+
Create a `./docker/.env` file that looks like this:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
CONFIG_FILE=/path/to/config.yaml
|
|
92
|
+
VAR_DIR=/path/to/var/dir
|
|
93
|
+
CERT_DIR=/path/to/certs
|
|
94
|
+
NGINX_CACHE_DIR=/path/to/cache
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
These properties configure the service as follows:
|
|
98
|
+
* `CONFIG_FILE`: path to the `config.yaml` settings file
|
|
99
|
+
* `VAR_DIR`: optional path to the var directory containing access keys referenced by `config.yaml`
|
|
100
|
+
* `CERT_FILE`: optional path to the SSL cert file
|
|
101
|
+
* `KEY_FILE`: optional path to the SSL key file
|
|
102
|
+
* `NGINX_CACHE_DIR`: path for Nginx response caching (you can disable caching by editing `nginx.conf`)
|
|
103
|
+
|
|
104
|
+
Now you can bring up the container:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
cd docker/
|
|
108
|
+
docker compose up -d
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
# Additional Documentation
|
|
112
|
+
|
|
113
|
+
* [Configuation](docs/Config.md) - how to use `config.yaml` to configure the service
|
|
114
|
+
* [Development](docs/Development.md) - notes on developing the service codebase
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# Attributions
|
|
118
|
+
|
|
119
|
+
* Proxy icons created by [Uniconlabs - Flaticon](https://www.flaticon.com/free-icons/proxy)
|
|
120
|
+
* [AWS S3 API](https://docs.aws.amazon.com/AmazonS3/latest/API/Type_API_Reference.html) Reference
|
x2s3-0.8.0/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# x2s3
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
RESTful web service which makes any storage system *X* available as an S3-compatible REST API, hence the name "X to S3". It was initially built to support cloud-compatible microscopy image viewers such as [N5 Viewer](https://github.com/saalfeldlab/n5-viewer) (BigDataViewer) and [Neuroglancer](https://github.com/google/neuroglancer).
|
|
6
|
+
|
|
7
|
+
At Janelia, we use **x2s3** to make private buckets on Seagate Lyve appear public, and to proxy internal resources (e.g. VAST S3). It can also be used as a pop-up file service for quickly viewing local images in BigDataViewer or Neuroglancer.
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<img src="https://raw.githubusercontent.com/JaneliaSciComp/x2s3/main/docs/use_cases.png">
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
# Features
|
|
14
|
+
|
|
15
|
+
* Extensible support for backend storage systems
|
|
16
|
+
* Optional web-based bucket explorer
|
|
17
|
+
* Hidden buckets
|
|
18
|
+
* Partial buckets (chroot-like prefixes)
|
|
19
|
+
* Non-blocking object streaming
|
|
20
|
+
|
|
21
|
+
Inspired by S3 proxies such as [oxyno-zeta/s3-proxy](https://github.com/oxyno-zeta/s3-proxy) and [pottava/aws-s3-proxy](https://github.com/pottava/aws-s3-proxy), this service goes a step further to implement enough of the [AWS S3 API](https://docs.aws.amazon.com/AmazonS3/latest/API/Type_API_Reference.html) to be useable by AWS clients, such as BigDataViewer. The S3 proxy implements which do this well (e.g. [gaul/s3proxy](https://github.com/gaul/s3proxy)) only proxy a single bucket at a time.
|
|
22
|
+
|
|
23
|
+
S3 endpoints implemented:
|
|
24
|
+
* [GetBucketAcl](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketAcl.html)
|
|
25
|
+
* [GetObject](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html)
|
|
26
|
+
* [HeadObject](https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html)
|
|
27
|
+
* [ListBuckets](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListBuckets.html)
|
|
28
|
+
* [ListObjectsV2](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html)
|
|
29
|
+
|
|
30
|
+
S3 features omitted:
|
|
31
|
+
* Permissions
|
|
32
|
+
* Encryption
|
|
33
|
+
* Versioning
|
|
34
|
+
* RequestPayer
|
|
35
|
+
* etc.
|
|
36
|
+
|
|
37
|
+
# Running
|
|
38
|
+
|
|
39
|
+
Create a `config.yaml` file that contains all of the buckets you want to serve. You can get started quickly by using the provided example template:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
cp config.template.yaml config.yaml
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
See the [documentation](docs/Config.md) for more information about the configuration file.
|
|
46
|
+
|
|
47
|
+
The simplest way to run the service is to use Docker:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
docker run -it -p 8000:8000 -v ./config.yaml:/app/x2s3/config.yaml ghcr.io/janeliascicomp/x2s3:latest
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
You can also run the service with Python/Uvicorn directly. See the [development documentation](docs/Development.md) for more information on setting that up.
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
## Production Deployment
|
|
57
|
+
|
|
58
|
+
For production deployments, we recommend using an orchestrator (like Docker Compose) to run the prebuilt Docker container along with an Nginx reverse proxy which provides caching and TLS termination.
|
|
59
|
+
|
|
60
|
+
Create a `./docker/.env` file that looks like this:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
CONFIG_FILE=/path/to/config.yaml
|
|
64
|
+
VAR_DIR=/path/to/var/dir
|
|
65
|
+
CERT_DIR=/path/to/certs
|
|
66
|
+
NGINX_CACHE_DIR=/path/to/cache
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
These properties configure the service as follows:
|
|
70
|
+
* `CONFIG_FILE`: path to the `config.yaml` settings file
|
|
71
|
+
* `VAR_DIR`: optional path to the var directory containing access keys referenced by `config.yaml`
|
|
72
|
+
* `CERT_FILE`: optional path to the SSL cert file
|
|
73
|
+
* `KEY_FILE`: optional path to the SSL key file
|
|
74
|
+
* `NGINX_CACHE_DIR`: path for Nginx response caching (you can disable caching by editing `nginx.conf`)
|
|
75
|
+
|
|
76
|
+
Now you can bring up the container:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
cd docker/
|
|
80
|
+
docker compose up -d
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
# Additional Documentation
|
|
84
|
+
|
|
85
|
+
* [Configuation](docs/Config.md) - how to use `config.yaml` to configure the service
|
|
86
|
+
* [Development](docs/Development.md) - notes on developing the service codebase
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# Attributions
|
|
90
|
+
|
|
91
|
+
* Proxy icons created by [Uniconlabs - Flaticon](https://www.flaticon.com/free-icons/proxy)
|
|
92
|
+
* [AWS S3 API](https://docs.aws.amazon.com/AmazonS3/latest/API/Type_API_Reference.html) Reference
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=42", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "x2s3"
|
|
7
|
+
version = "0.8.0"
|
|
8
|
+
description = "RESTful web service which makes any storage system X available as an S3-compatible REST API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "Konrad Rokicki", email = "rokicki@janelia.hhmi.org" }
|
|
12
|
+
]
|
|
13
|
+
license = { text = "BSD-3-Clause" }
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: BSD License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
requires-python = ">=3.6"
|
|
20
|
+
dependencies = [
|
|
21
|
+
"aiobotocore",
|
|
22
|
+
"boto3",
|
|
23
|
+
"botocore",
|
|
24
|
+
"fastapi",
|
|
25
|
+
"loguru",
|
|
26
|
+
"pydantic",
|
|
27
|
+
"pydantic-settings",
|
|
28
|
+
"python-dateutil",
|
|
29
|
+
"pytest",
|
|
30
|
+
"starlette",
|
|
31
|
+
"typing_extensions",
|
|
32
|
+
"uvicorn"
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/JaneliaSciComp/x2s3"
|
|
37
|
+
Repository = "https://github.com/JaneliaSciComp/x2s3"
|
|
38
|
+
|
|
39
|
+
[tool.setuptools]
|
|
40
|
+
packages = ["x2s3"]
|
x2s3-0.8.0/setup.cfg
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import urllib.parse
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from fastapi.testclient import TestClient
|
|
5
|
+
from pydantic import HttpUrl
|
|
6
|
+
|
|
7
|
+
from xml.etree.ElementTree import Element
|
|
8
|
+
from x2s3.app import create_app
|
|
9
|
+
from x2s3.settings import Target, Settings
|
|
10
|
+
from x2s3.utils import parse_xml
|
|
11
|
+
|
|
12
|
+
obj_path = '/janelia-data-examples/jrc_mus_lung_covid.n5/render/v1_acquire_align___20210609_224836/s0/0/0/0'
|
|
13
|
+
|
|
14
|
+
@pytest.fixture
|
|
15
|
+
def get_settings():
|
|
16
|
+
settings = Settings()
|
|
17
|
+
settings.base_url = HttpUrl('http://testserver')
|
|
18
|
+
settings.virtual_buckets = True
|
|
19
|
+
settings.targets = [
|
|
20
|
+
Target(
|
|
21
|
+
name='janelia-data-examples',
|
|
22
|
+
options={'bucket':'janelia-data-examples'}
|
|
23
|
+
),
|
|
24
|
+
Target(
|
|
25
|
+
name='with-prefix',
|
|
26
|
+
options={
|
|
27
|
+
'bucket':'janelia-data-examples',
|
|
28
|
+
'prefix':'jrc_mus_lung_covid.n5/'
|
|
29
|
+
}
|
|
30
|
+
),
|
|
31
|
+
Target(
|
|
32
|
+
name='hidden-with-endpoint',
|
|
33
|
+
browseable=False,
|
|
34
|
+
options={
|
|
35
|
+
'bucket':'janelia-data-examples',
|
|
36
|
+
'endpoint':'https://s3.amazonaws.com',
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
]
|
|
40
|
+
return settings
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.fixture
|
|
44
|
+
def app(get_settings):
|
|
45
|
+
return create_app(get_settings)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_acl(app):
|
|
49
|
+
with TestClient(app) as client:
|
|
50
|
+
response = client.get(f"/janelia-data-examples?acl")
|
|
51
|
+
assert response.status_code == 200
|
|
52
|
+
assert response.headers['content-type'] == "application/xml"
|
|
53
|
+
assert response.text.count("<Grant>") == 1
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_get_html_root(app):
|
|
57
|
+
with TestClient(app) as client:
|
|
58
|
+
response = client.get("/")
|
|
59
|
+
assert response.status_code == 200
|
|
60
|
+
assert response.headers['content-type'].startswith("text/html")
|
|
61
|
+
for target in app.settings.targets:
|
|
62
|
+
if target.browseable:
|
|
63
|
+
assert target.name in response.text
|
|
64
|
+
else:
|
|
65
|
+
assert target.name not in response.text
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_get_html_listing(app):
|
|
69
|
+
with TestClient(app) as client:
|
|
70
|
+
response = client.get("/janelia-data-examples/jrc_mus_lung_covid.n5/")
|
|
71
|
+
assert response.status_code == 200
|
|
72
|
+
assert response.headers['content-type'].startswith("text/html")
|
|
73
|
+
assert '<html>' in response.text
|
|
74
|
+
assert '<head>' in response.text
|
|
75
|
+
assert 'attributes.json' in response.text
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_list_objects(app):
|
|
79
|
+
with TestClient(app) as client:
|
|
80
|
+
bucket_name = 'janelia-data-examples'
|
|
81
|
+
max_keys = 7
|
|
82
|
+
response = client.get(f"/{bucket_name}?list-type=2&max-keys={max_keys}")
|
|
83
|
+
assert response.status_code == 200
|
|
84
|
+
root = parse_xml(response.text)
|
|
85
|
+
assert root.tag == "ListBucketResult"
|
|
86
|
+
assert root.find('Name').text == bucket_name
|
|
87
|
+
|
|
88
|
+
# CommonPrefixes are only returned when there is a delimiter
|
|
89
|
+
assert len(root.findall('CommonPrefixes')) == 0
|
|
90
|
+
|
|
91
|
+
contents = root.findall('Contents')
|
|
92
|
+
assert len(contents) <= max_keys
|
|
93
|
+
|
|
94
|
+
if root.find('NextContinuationToken') is not None:
|
|
95
|
+
assert root.find('IsTruncated').text == "true"
|
|
96
|
+
|
|
97
|
+
assert isinstance(contents[0].find('Key'), Element)
|
|
98
|
+
assert isinstance(contents[0].find('Size'), Element)
|
|
99
|
+
assert isinstance(contents[0].find('LastModified'), Element)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_list_objects_delimiter(app):
|
|
103
|
+
with TestClient(app) as client:
|
|
104
|
+
bucket_name = 'janelia-data-examples'
|
|
105
|
+
max_keys = 9
|
|
106
|
+
response = client.get(f"/{bucket_name}?list-type=2&delimiter=/&max-keys={max_keys}")
|
|
107
|
+
assert response.status_code == 200
|
|
108
|
+
root = parse_xml(response.text)
|
|
109
|
+
assert root.tag == "ListBucketResult"
|
|
110
|
+
assert root.find('Name').text == bucket_name
|
|
111
|
+
assert root.find('Delimiter').text == '/'
|
|
112
|
+
assert len(root.findall('CommonPrefixes')) >= 1
|
|
113
|
+
assert root.find('IsTruncated').text == "false"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_list_objects_continuation(app):
|
|
117
|
+
with TestClient(app) as client:
|
|
118
|
+
bucket_name = 'janelia-data-examples'
|
|
119
|
+
max_keys = 4
|
|
120
|
+
uri = f"/{bucket_name}?list-type=2&max-keys={max_keys}&prefix=jrc_mus_lung_covid.n5/render/v1_acquire_align___20210609_224836/s0/0/0"
|
|
121
|
+
|
|
122
|
+
token_param = ''
|
|
123
|
+
total = 0
|
|
124
|
+
c = 0
|
|
125
|
+
while True:
|
|
126
|
+
url = f"{uri}{token_param}"
|
|
127
|
+
print(f"Fetching {url}")
|
|
128
|
+
response = client.get(url)
|
|
129
|
+
assert response.status_code == 200
|
|
130
|
+
root = parse_xml(response.text)
|
|
131
|
+
|
|
132
|
+
assert root.tag == "ListBucketResult"
|
|
133
|
+
assert root.find('Name').text == bucket_name
|
|
134
|
+
assert root.find('MaxKeys').text == str(max_keys)
|
|
135
|
+
assert len(root.findall('CommonPrefixes')) == 0
|
|
136
|
+
|
|
137
|
+
contents = root.findall('Contents')
|
|
138
|
+
print(f"Got {len(contents)} results")
|
|
139
|
+
total += len(contents)
|
|
140
|
+
assert len(contents) <= max_keys
|
|
141
|
+
|
|
142
|
+
next_token_elem = root.find('NextContinuationToken')
|
|
143
|
+
if next_token_elem is not None:
|
|
144
|
+
assert root.find('IsTruncated').text == "true"
|
|
145
|
+
else:
|
|
146
|
+
break
|
|
147
|
+
|
|
148
|
+
safe_token = urllib.parse.quote_plus(next_token_elem.text)
|
|
149
|
+
token_param = f"&continuation-token={safe_token}"
|
|
150
|
+
|
|
151
|
+
c += 1
|
|
152
|
+
if c>10:
|
|
153
|
+
assert False
|
|
154
|
+
|
|
155
|
+
assert total == 6
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_head_object(app):
|
|
159
|
+
with TestClient(app) as client:
|
|
160
|
+
response = client.head("/janelia-data-examples/jrc_mus_lung_covid.n5/attributes.json")
|
|
161
|
+
assert response.status_code == 200
|
|
162
|
+
response = client.head("/janelia-data-examples/jrc_mus_lung_covid.n5/")
|
|
163
|
+
assert response.status_code == 404
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_prefixed_head_object(app):
|
|
167
|
+
with TestClient(app) as client:
|
|
168
|
+
response = client.head("/with-prefix/attributes.json")
|
|
169
|
+
assert response.status_code == 200
|
|
170
|
+
response = client.head("/with-prefix/render/attributes.json")
|
|
171
|
+
assert response.status_code == 200
|
|
172
|
+
response = client.head("/with-prefix/render/")
|
|
173
|
+
assert response.status_code == 404
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_get_object(app):
|
|
177
|
+
with TestClient(app) as client:
|
|
178
|
+
response = client.get("/janelia-data-examples/jrc_mus_lung_covid.n5/attributes.json")
|
|
179
|
+
assert response.status_code == 200
|
|
180
|
+
json_obj = response.json()
|
|
181
|
+
assert 'n5' in json_obj
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_prefixed_get_object(app):
|
|
185
|
+
with TestClient(app) as client:
|
|
186
|
+
response = client.get("/with-prefix/attributes.json")
|
|
187
|
+
assert response.status_code == 200
|
|
188
|
+
json_obj = response.json()
|
|
189
|
+
assert 'n5' in json_obj
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def test_virtual_host_get_object(app):
|
|
193
|
+
with TestClient(app) as client:
|
|
194
|
+
response = client.get("/jrc_mus_lung_covid.n5/attributes.json",
|
|
195
|
+
headers={'Host':'janelia-data-examples.testserver'})
|
|
196
|
+
assert response.status_code == 200
|
|
197
|
+
json_obj = response.json()
|
|
198
|
+
assert 'n5' in json_obj
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def test_get_object_hidden(app):
|
|
202
|
+
with TestClient(app) as client:
|
|
203
|
+
response = client.get("/hidden-with-endpoint/jrc_mus_lung_covid.n5/attributes.json")
|
|
204
|
+
assert response.status_code == 200
|
|
205
|
+
json_obj = response.json()
|
|
206
|
+
assert 'n5' in json_obj
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def test_get_object_range_first(app):
|
|
210
|
+
with TestClient(app) as client:
|
|
211
|
+
# Test a valid range request (first 100 bytes)
|
|
212
|
+
response = client.get(
|
|
213
|
+
obj_path,
|
|
214
|
+
headers={"Range": "bytes=0-99"}
|
|
215
|
+
)
|
|
216
|
+
assert response.status_code == 206 # Partial Content
|
|
217
|
+
assert 'Content-Range' in response.headers
|
|
218
|
+
assert response.headers['Content-Range'] == 'bytes 0-99/987143'
|
|
219
|
+
assert len(response.content) == 100
|
|
220
|
+
|
|
221
|
+
def test_get_object_range_mid(app):
|
|
222
|
+
with TestClient(app) as client:
|
|
223
|
+
# Test a valid range request (bytes 100-199)
|
|
224
|
+
response = client.get(
|
|
225
|
+
obj_path,
|
|
226
|
+
headers={"Range": "bytes=100-199"}
|
|
227
|
+
)
|
|
228
|
+
assert response.status_code == 206 # Partial Content
|
|
229
|
+
assert 'Content-Range' in response.headers
|
|
230
|
+
assert response.headers['Content-Range'] == 'bytes 100-199/987143'
|
|
231
|
+
assert len(response.content) == 100
|
|
232
|
+
|
|
233
|
+
def test_get_object_range_last(app):
|
|
234
|
+
with TestClient(app) as client:
|
|
235
|
+
# Test a valid range request (last 100 bytes)
|
|
236
|
+
response = client.get(
|
|
237
|
+
obj_path,
|
|
238
|
+
headers={"Range": "bytes=-100"}
|
|
239
|
+
)
|
|
240
|
+
assert response.status_code == 206 # Partial Content
|
|
241
|
+
assert 'Content-Range' in response.headers
|
|
242
|
+
assert len(response.content) == 100
|
|
243
|
+
|
|
244
|
+
def test_get_object_range_out_of_bounds(app):
|
|
245
|
+
with TestClient(app) as client:
|
|
246
|
+
# Test invalid range request (out of bounds)
|
|
247
|
+
response = client.get(
|
|
248
|
+
obj_path,
|
|
249
|
+
headers={"Range": "bytes=1000000-2000000"}
|
|
250
|
+
)
|
|
251
|
+
assert response.status_code == 416 # Range Not Satisfiable
|
|
252
|
+
root = parse_xml(response.text)
|
|
253
|
+
assert root.find('Code').text == 'InvalidRange'
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def test_prefixed_list_objects(app):
|
|
257
|
+
with TestClient(app) as client:
|
|
258
|
+
bucket_name = 'with-prefix'
|
|
259
|
+
response = client.get(f"/{bucket_name}?list-type=2&delimiter=/")
|
|
260
|
+
assert response.status_code == 200
|
|
261
|
+
root = parse_xml(response.text)
|
|
262
|
+
assert root.tag == "ListBucketResult"
|
|
263
|
+
assert root.find('Name').text == bucket_name
|
|
264
|
+
assert root.find('Delimiter').text == '/'
|
|
265
|
+
assert len(root.findall('CommonPrefixes')) == 2
|
|
266
|
+
assert root.find('IsTruncated').text == "false"
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def test_get_object_missing(app):
|
|
270
|
+
with TestClient(app) as client:
|
|
271
|
+
response = client.get("/janelia-data-examples/missing")
|
|
272
|
+
assert response.status_code == 404
|
|
273
|
+
assert response.headers['content-type'] == "application/xml"
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def test_bucket_missing(app):
|
|
277
|
+
with TestClient(app) as client:
|
|
278
|
+
response = client.get("/missing/attributes.json")
|
|
279
|
+
assert response.status_code == 404
|
|
280
|
+
assert response.headers['content-type'] == "application/xml"
|
|
281
|
+
root = parse_xml(response.text)
|
|
282
|
+
assert root.find('Code').text == 'NoSuchBucket'
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def test_list_objects_error(app):
|
|
286
|
+
with TestClient(app) as client:
|
|
287
|
+
response = client.get(f"/janelia-data-examples?list-type=2&max-keys=aaa")
|
|
288
|
+
assert response.status_code == 400
|
|
289
|
+
assert response.headers['content-type'] == "application/json"
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def test_get_object_precedence(app):
|
|
293
|
+
with TestClient(app) as client:
|
|
294
|
+
response = client.get(f"/janelia-data-examples/jrc_mus_lung_covid.n5/attributes.json?list-type=2")
|
|
295
|
+
assert response.status_code == 200
|
|
296
|
+
json_obj = response.json()
|
|
297
|
+
assert 'n5' in json_obj
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import urllib.parse
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from fastapi.testclient import TestClient
|
|
5
|
+
from pydantic import HttpUrl
|
|
6
|
+
|
|
7
|
+
from xml.etree.ElementTree import Element
|
|
8
|
+
from x2s3.app import create_app
|
|
9
|
+
from x2s3.settings import Target, Settings
|
|
10
|
+
from x2s3.utils import parse_xml
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture
|
|
14
|
+
def get_settings():
|
|
15
|
+
settings = Settings()
|
|
16
|
+
settings.targets = [
|
|
17
|
+
]
|
|
18
|
+
return settings
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture
|
|
22
|
+
def app(get_settings):
|
|
23
|
+
return create_app(get_settings)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.fixture
|
|
27
|
+
def client(app):
|
|
28
|
+
return TestClient(app)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_get_favicon(client):
|
|
32
|
+
response = client.get("/favicon.ico")
|
|
33
|
+
assert response.status_code == 200
|
|
34
|
+
assert response.headers['content-type'].startswith("image")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_get_robotstxt(client):
|
|
38
|
+
response = client.get("/robots.txt")
|
|
39
|
+
assert response.status_code == 200
|
|
40
|
+
assert response.headers['content-type'].startswith("text/plain")
|
|
41
|
+
assert response.text == "User-agent: *\nDisallow: /"
|