Skip to content

Commit e1ba8b5

Browse files
authored
feat: redirect to pre-signed url (#42)
If only one asset is download, we attempt to create a pre-signed url. If this is successful, a `RedirectResponse` is returned instead of calling the `_stream_download_dict` method.
1 parent c541296 commit e1ba8b5

4 files changed

Lines changed: 68 additions & 6 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ license = { file = "LICENSE" }
88
requires-python = ">= 3.9"
99
dependencies = [
1010
"attr",
11-
"eodag[all-providers]",
11+
"eodag[all-providers] < 4.0.0",
1212
"fastapi",
1313
"geojson-pydantic",
1414
"orjson",

stac_fastapi/eodag/extensions/data_download.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,15 @@
2222
import os
2323
from io import BufferedReader
2424
from shutil import make_archive, rmtree
25-
from typing import Annotated, Iterator, Optional, cast
25+
from typing import Annotated, Iterator, Optional, Union, cast
2626

2727
import attr
2828
from eodag.api.core import EODataAccessGateway
2929
from eodag.api.product._product import EOProduct
3030
from eodag.api.product.metadata_mapping import ONLINE_STATUS, STAGING_STATUS, get_metadata_path_value
31+
from eodag.utils.exceptions import EodagError
3132
from fastapi import APIRouter, FastAPI, Path, Request
32-
from fastapi.responses import StreamingResponse
33+
from fastapi.responses import RedirectResponse, StreamingResponse
3334
from stac_fastapi.api.errors import NotFoundError
3435
from stac_fastapi.api.routes import create_async_endpoint
3536
from stac_fastapi.types.extension import ApiExtension
@@ -100,7 +101,7 @@ def get_data(
100101
item_id: str,
101102
asset_name: Optional[str],
102103
request: Request,
103-
) -> StreamingResponse:
104+
) -> Union[StreamingResponse, RedirectResponse]:
104105
"""Download an asset"""
105106

106107
dag = cast(EODataAccessGateway, request.app.state.dag) # type: ignore
@@ -181,6 +182,17 @@ def get_data(
181182
raise NotFoundError(f"Item {item_id} does not exist. Please order it first") from e
182183
raise NotFoundError(e) from e
183184

185+
if product.downloader_auth and asset_name and asset_name != "downloadLink":
186+
asset_values = product.assets[asset_name]
187+
# return presigned url if available
188+
try:
189+
presigned_url = product.downloader_auth.presign_url(asset_values)
190+
return RedirectResponse(presigned_url, status_code=302)
191+
except NotImplementedError:
192+
logger.info("Presigned urls not supported for %s with auth %s", product.downloader, auth)
193+
except EodagError:
194+
logger.info("Presigned url could not be fetched for %s", asset_name)
195+
184196
try:
185197
s = product.downloader._stream_download_dict(
186198
product,

tests/conftest.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from eodag.api.product.metadata_mapping import OFFLINE_STATUS, ONLINE_STATUS
3232
from eodag.api.search_result import SearchResult
3333
from eodag.config import PluginConfig
34+
from eodag.plugins.authentication.aws_auth import AwsAuth
3435
from eodag.plugins.authentication.base import Authentication
3536
from eodag.plugins.authentication.openid_connect import OIDCRefreshTokenBase
3637
from eodag.plugins.authentication.token import TokenAuth
@@ -338,7 +339,7 @@ def mock_http_base_stream_download_dict(mocker):
338339
@pytest.fixture(scope="function")
339340
def mock_order(mocker):
340341
"""
341-
Mocks the `HTTPDownload` method of the `HTTPDownload` download plugin.
342+
Mocks the `order` method of the `HTTPDownload` download plugin.
342343
"""
343344
return mocker.patch.object(HTTPDownload, "order")
344345

@@ -375,6 +376,14 @@ def mock_oidc_token_exchange_auth_authenticate(mocker):
375376
return mocker.patch.object(OIDCTokenExchangeAuth, "authenticate")
376377

377378

379+
@pytest.fixture(scope="function")
380+
def mock_aws_authenticate(mocker, app):
381+
"""
382+
Mocks the `authenticate` method of the `AwsAuth` plugin.
383+
"""
384+
return mocker.patch.object(AwsAuth, "authenticate")
385+
386+
378387
@pytest.fixture(scope="function")
379388
def tmp_dir():
380389
"""
@@ -397,6 +406,7 @@ async def _request_valid_raw(
397406
search_call_count: Optional[int] = None,
398407
search_result: Optional[SearchResult] = None,
399408
expected_status_code: int = 200,
409+
follow_redirects: bool = True,
400410
):
401411
if search_result:
402412
mock_search.return_value = search_result
@@ -407,7 +417,7 @@ async def _request_valid_raw(
407417
method,
408418
url,
409419
json=post_data,
410-
follow_redirects=True,
420+
follow_redirects=follow_redirects,
411421
headers={"Content-Type": "application/json"} if method == "POST" else {},
412422
)
413423

@@ -587,6 +597,12 @@ async def _request_accepted(url: str):
587597
return _request_accepted
588598

589599

600+
@pytest.fixture(scope="function")
601+
def mock_presign_url(mocker):
602+
"""Fixture for the presign_url function"""
603+
return mocker.patch.object(AwsAuth, "presign_url")
604+
605+
590606
@dataclass
591607
class TestDefaults:
592608
"""

tests/test_download.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
from eodag import SearchResult
2323
from eodag.api.product import EOProduct
2424
from eodag.config import PluginConfig
25+
from eodag.plugins.authentication.aws_auth import AwsAuth
26+
from eodag.plugins.download.aws import AwsDownload
2527
from eodag.plugins.download.http import HTTPDownload
2628

2729
from stac_fastapi.eodag.config import get_settings
@@ -104,3 +106,35 @@ async def test_download_auto_order_whitelist(
104106

105107
# restore the original auto_order_whitelist setting
106108
get_settings().auto_order_whitelist = auto_order_whitelist
109+
110+
111+
async def test_download_redirect_response(request_valid_raw, mock_search, mock_presign_url, mock_aws_authenticate):
112+
"""test that a reponse with status code 302 is returned if presigned urls are used"""
113+
product_type = "MO_GLOBAL_ANALYSISFORECAST_PHY_001_024"
114+
product = EOProduct(
115+
"cop_marine",
116+
dict(
117+
geometry="POINT (0 0)",
118+
title="dummy_product",
119+
id="dummy",
120+
),
121+
productType=product_type,
122+
)
123+
product.assets.update({"a1": {"href": "https://s3.waw3-1.cloudferro.com/b1/a1/a1.json"}})
124+
product.assets.update({"a2": {"href": "https://s3.waw3-1.cloudferro.com/b1/a2/a2.json"}})
125+
126+
config = PluginConfig()
127+
config.priority = 0
128+
downloader = AwsDownload("cop_marine", config)
129+
download_auth = AwsAuth("cop_marine", config)
130+
product.register_downloader(downloader=downloader, authenticator=download_auth)
131+
mock_search.return_value = SearchResult([product])
132+
133+
mock_presign_url.return_value = "s3://s3.abc.com/a1/b1?AWSAccesskeyId=123&expires=1543649"
134+
135+
await request_valid_raw(
136+
f"data/cop_marine/{product_type}/foo/a1",
137+
search_result=SearchResult([product]),
138+
expected_status_code=302,
139+
follow_redirects=False,
140+
)

0 commit comments

Comments
 (0)