From 82683bbd921ce4cc6ef9e3b7cb277fb4e7c7d682 Mon Sep 17 00:00:00 2001 From: jlahovnik Date: Mon, 28 Jul 2025 15:31:13 +0200 Subject: [PATCH 1/5] feature: redirect to presigned url --- stac_fastapi/eodag/extensions/data_download.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/stac_fastapi/eodag/extensions/data_download.py b/stac_fastapi/eodag/extensions/data_download.py index bba3168..8f68d53 100644 --- a/stac_fastapi/eodag/extensions/data_download.py +++ b/stac_fastapi/eodag/extensions/data_download.py @@ -29,7 +29,7 @@ from eodag.api.product._product import EOProduct from eodag.api.product.metadata_mapping import ONLINE_STATUS, STAGING_STATUS, get_metadata_path_value 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 @@ -181,6 +181,17 @@ def get_data( raise NotFoundError(f"Item {item_id} does not exist. Please order it first") from e raise NotFoundError(e) from e + if asset_name and asset_name != "downloadLink": + asset_values = product.assets[asset_name] + # return presigned url if available + try: + presigned_url = product.downloader.presign_url(asset_values, auth) + headers = {"content-disposition": f"attachment; filename={asset_name}"} + if presigned_url: + 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) + try: s = product.downloader._stream_download_dict( product, From 5862bd5cee89bb1efb2d3f9d8360f59edf04c6b1 Mon Sep 17 00:00:00 2001 From: jlahovnik Date: Mon, 28 Jul 2025 17:11:56 +0200 Subject: [PATCH 2/5] test: redirect to presigned url --- tests/conftest.py | 12 ++++++++++-- tests/test_download.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index c8524b7..2a360ca 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -35,6 +35,7 @@ from eodag.plugins.authentication.openid_connect import OIDCRefreshTokenBase from eodag.plugins.authentication.token import TokenAuth from eodag.plugins.authentication.token_exchange import OIDCTokenExchangeAuth +from eodag.plugins.download.aws import AwsDownload from eodag.plugins.download.base import Download from eodag.plugins.download.http import HTTPDownload from eodag.plugins.search.qssearch import StacSearch @@ -338,7 +339,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") @@ -397,6 +398,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 @@ -407,7 +409,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 {}, ) @@ -587,6 +589,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(AwsDownload, "presign_url") + + @dataclass class TestDefaults: """ diff --git a/tests/test_download.py b/tests/test_download.py index 3b1559f..5526ac4 100644 --- a/tests/test_download.py +++ b/tests/test_download.py @@ -22,6 +22,7 @@ from eodag import SearchResult from eodag.api.product import EOProduct from eodag.config import PluginConfig +from eodag.plugins.download.aws import AwsDownload from eodag.plugins.download.http import HTTPDownload from stac_fastapi.eodag.config import get_settings @@ -104,3 +105,34 @@ 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_base_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) + product.register_downloader(downloader=downloader, authenticator=None) + 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, + ) From 04b7ef63c50048f68a8a9af0526b7303b579ed25 Mon Sep 17 00:00:00 2001 From: jlahovnik Date: Mon, 28 Jul 2025 17:49:04 +0200 Subject: [PATCH 3/5] fix: typing error --- stac_fastapi/eodag/extensions/data_download.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stac_fastapi/eodag/extensions/data_download.py b/stac_fastapi/eodag/extensions/data_download.py index 8f68d53..387f89d 100644 --- a/stac_fastapi/eodag/extensions/data_download.py +++ b/stac_fastapi/eodag/extensions/data_download.py @@ -100,7 +100,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 From 7b306b7cbeabd63a7d3f107187fe9895a3d07678 Mon Sep 17 00:00:00 2001 From: jlahovnik Date: Mon, 6 Oct 2025 12:32:20 +0200 Subject: [PATCH 4/5] refactor: presign_urls in auth plugin --- stac_fastapi/eodag/extensions/data_download.py | 10 ++++++---- tests/conftest.py | 12 ++++++++++-- tests/test_download.py | 6 ++++-- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/stac_fastapi/eodag/extensions/data_download.py b/stac_fastapi/eodag/extensions/data_download.py index 387f89d..893b891 100644 --- a/stac_fastapi/eodag/extensions/data_download.py +++ b/stac_fastapi/eodag/extensions/data_download.py @@ -28,6 +28,7 @@ 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 RedirectResponse, StreamingResponse from stac_fastapi.api.errors import NotFoundError @@ -181,16 +182,17 @@ def get_data( raise NotFoundError(f"Item {item_id} does not exist. Please order it first") from e raise NotFoundError(e) from e - if asset_name and asset_name != "downloadLink": + 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.presign_url(asset_values, auth) + presigned_url = product.downloader_auth.presign_url(asset_values) headers = {"content-disposition": f"attachment; filename={asset_name}"} - if presigned_url: - return RedirectResponse(presigned_url, status_code=302, headers=headers) + 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( diff --git a/tests/conftest.py b/tests/conftest.py index 2a360ca..1a03b58 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -31,11 +31,11 @@ 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 from eodag.plugins.authentication.token_exchange import OIDCTokenExchangeAuth -from eodag.plugins.download.aws import AwsDownload from eodag.plugins.download.base import Download from eodag.plugins.download.http import HTTPDownload from eodag.plugins.search.qssearch import StacSearch @@ -376,6 +376,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(): """ @@ -592,7 +600,7 @@ async def _request_accepted(url: str): @pytest.fixture(scope="function") def mock_presign_url(mocker): """Fixture for the presign_url function""" - return mocker.patch.object(AwsDownload, "presign_url") + return mocker.patch.object(AwsAuth, "presign_url") @dataclass diff --git a/tests/test_download.py b/tests/test_download.py index 5526ac4..85764a8 100644 --- a/tests/test_download.py +++ b/tests/test_download.py @@ -22,6 +22,7 @@ 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 @@ -107,7 +108,7 @@ async def test_download_auto_order_whitelist( get_settings().auto_order_whitelist = auto_order_whitelist -async def test_download_redirect_response(request_valid_raw, mock_search, mock_presign_url, mock_base_authenticate): +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( @@ -125,7 +126,8 @@ async def test_download_redirect_response(request_valid_raw, mock_search, mock_p config = PluginConfig() config.priority = 0 downloader = AwsDownload("cop_marine", config) - product.register_downloader(downloader=downloader, authenticator=None) + 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" From 31ab9f66c4bcdcdac96dd2a1940cab81472173a8 Mon Sep 17 00:00:00 2001 From: jlahovnik Date: Mon, 27 Oct 2025 11:10:11 +0100 Subject: [PATCH 5/5] refactor: remove header from presigned url redirect --- pyproject.toml | 2 +- stac_fastapi/eodag/extensions/data_download.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8572928..a9c4ab8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ license = { file = "LICENSE" } requires-python = ">= 3.9" dependencies = [ "attr", - "eodag[all-providers]", + "eodag[all-providers] < 4.0.0", "fastapi", "geojson-pydantic", "orjson", diff --git a/stac_fastapi/eodag/extensions/data_download.py b/stac_fastapi/eodag/extensions/data_download.py index 893b891..3828c1a 100644 --- a/stac_fastapi/eodag/extensions/data_download.py +++ b/stac_fastapi/eodag/extensions/data_download.py @@ -22,7 +22,7 @@ import os from io import BufferedReader from shutil import make_archive, rmtree -from typing import Annotated, Iterator, Optional, cast +from typing import Annotated, Iterator, Optional, Union, cast import attr from eodag.api.core import EODataAccessGateway @@ -101,7 +101,7 @@ def get_data( item_id: str, asset_name: Optional[str], request: Request, - ) -> StreamingResponse | RedirectResponse: + ) -> Union[StreamingResponse, RedirectResponse]: """Download an asset""" dag = cast(EODataAccessGateway, request.app.state.dag) # type: ignore @@ -187,8 +187,7 @@ def get_data( # 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) + return RedirectResponse(presigned_url, status_code=302) except NotImplementedError: logger.info("Presigned urls not supported for %s with auth %s", product.downloader, auth) except EodagError: