diff --git a/stac_fastapi/eodag/extensions/data_download.py b/stac_fastapi/eodag/extensions/data_download.py index bba3168..893b891 100644 --- a/stac_fastapi/eodag/extensions/data_download.py +++ b/stac_fastapi/eodag/extensions/data_download.py @@ -28,8 +28,9 @@ from eodag.api.core import EODataAccessGateway from eodag.api.product._product import EOProduct from eodag.api.product.metadata_mapping import ONLINE_STATUS, STAGING_STATUS, get_metadata_path_value +from eodag.utils.exceptions import EodagError from fastapi import APIRouter, FastAPI, Path, Request -from fastapi.responses import StreamingResponse +from fastapi.responses import RedirectResponse, StreamingResponse from stac_fastapi.api.errors import NotFoundError from stac_fastapi.api.routes import create_async_endpoint from stac_fastapi.types.extension import ApiExtension @@ -100,7 +101,7 @@ def get_data( item_id: str, asset_name: Optional[str], request: Request, - ) -> StreamingResponse: + ) -> StreamingResponse | RedirectResponse: """Download an asset""" dag = cast(EODataAccessGateway, request.app.state.dag) # type: ignore @@ -181,6 +182,18 @@ def get_data( raise NotFoundError(f"Item {item_id} does not exist. Please order it first") from e raise NotFoundError(e) from e + if product.downloader_auth and asset_name and asset_name != "downloadLink": + asset_values = product.assets[asset_name] + # return presigned url if available + try: + presigned_url = product.downloader_auth.presign_url(asset_values) + headers = {"content-disposition": f"attachment; filename={asset_name}"} + return RedirectResponse(presigned_url, status_code=302, headers=headers) + except NotImplementedError: + logger.info("Presigned urls not supported for %s with auth %s", product.downloader, auth) + except EodagError: + logger.info("Presigned url could not be fetched for %s", asset_name) + try: s = product.downloader._stream_download_dict( product, diff --git a/tests/conftest.py b/tests/conftest.py index 72021e0..20997ce 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -31,6 +31,7 @@ from eodag.api.product.metadata_mapping import OFFLINE_STATUS, ONLINE_STATUS from eodag.api.search_result import SearchResult from eodag.config import PluginConfig +from eodag.plugins.authentication.aws_auth import AwsAuth from eodag.plugins.authentication.base import Authentication from eodag.plugins.authentication.openid_connect import OIDCRefreshTokenBase from eodag.plugins.authentication.token import TokenAuth @@ -337,7 +338,7 @@ def mock_http_base_stream_download_dict(mocker): @pytest.fixture(scope="function") def mock_order(mocker): """ - Mocks the `HTTPDownload` method of the `HTTPDownload` download plugin. + Mocks the `order` method of the `HTTPDownload` download plugin. """ return mocker.patch.object(HTTPDownload, "order") @@ -374,6 +375,14 @@ def mock_oidc_token_exchange_auth_authenticate(mocker): return mocker.patch.object(OIDCTokenExchangeAuth, "authenticate") +@pytest.fixture(scope="function") +def mock_aws_authenticate(mocker, app): + """ + Mocks the `authenticate` method of the `AwsAuth` plugin. + """ + return mocker.patch.object(AwsAuth, "authenticate") + + @pytest.fixture(scope="function") def tmp_dir(): """ @@ -396,6 +405,7 @@ async def _request_valid_raw( search_call_count: Optional[int] = None, search_result: Optional[SearchResult] = None, expected_status_code: int = 200, + follow_redirects: bool = True, ): if search_result: mock_search.return_value = search_result @@ -406,7 +416,7 @@ async def _request_valid_raw( method, url, json=post_data, - follow_redirects=True, + follow_redirects=follow_redirects, headers={"Content-Type": "application/json"} if method == "POST" else {}, ) @@ -586,6 +596,12 @@ async def _request_accepted(url: str): return _request_accepted +@pytest.fixture(scope="function") +def mock_presign_url(mocker): + """Fixture for the presign_url function""" + return mocker.patch.object(AwsAuth, "presign_url") + + @dataclass class TestDefaults: """ diff --git a/tests/test_download.py b/tests/test_download.py index 66dd4b0..7714ef0 100644 --- a/tests/test_download.py +++ b/tests/test_download.py @@ -22,6 +22,8 @@ from eodag import SearchResult from eodag.api.product import EOProduct from eodag.config import PluginConfig +from eodag.plugins.authentication.aws_auth import AwsAuth +from eodag.plugins.download.aws import AwsDownload from eodag.plugins.download.http import HTTPDownload from stac_fastapi.eodag.config import get_settings @@ -104,3 +106,35 @@ async def test_download_auto_order_whitelist( # restore the original auto_order_whitelist setting get_settings().auto_order_whitelist = auto_order_whitelist + + +async def test_download_redirect_response(request_valid_raw, mock_search, mock_presign_url, mock_aws_authenticate): + """test that a reponse with status code 302 is returned if presigned urls are used""" + product_type = "MO_GLOBAL_ANALYSISFORECAST_PHY_001_024" + product = EOProduct( + "cop_marine", + dict( + geometry="POINT (0 0)", + title="dummy_product", + id="dummy", + ), + productType=product_type, + ) + product.assets.update({"a1": {"href": "https://s3.waw3-1.cloudferro.com/b1/a1/a1.json"}}) + product.assets.update({"a2": {"href": "https://s3.waw3-1.cloudferro.com/b1/a2/a2.json"}}) + + config = PluginConfig() + config.priority = 0 + downloader = AwsDownload("cop_marine", config) + download_auth = AwsAuth("cop_marine", config) + product.register_downloader(downloader=downloader, authenticator=download_auth) + mock_search.return_value = SearchResult([product]) + + mock_presign_url.return_value = "s3://s3.abc.com/a1/b1?AWSAccesskeyId=123&expires=1543649" + + await request_valid_raw( + f"data/cop_marine/{product_type}/foo/a1", + search_result=SearchResult([product]), + expected_status_code=302, + follow_redirects=False, + )