From b6ae09b7463313d2633dd12781e6f45dcca147ae Mon Sep 17 00:00:00 2001 From: Christoph Steiger Date: Tue, 25 Nov 2025 10:16:05 +0100 Subject: [PATCH 1/2] feat(download): add session adapter for local files Add an adapter to the request session to allow access to local files. Any URLs that start with `file:///` will be picked up by this adapter and copied locally. Signed-off-by: Christoph Steiger --- src/debsbom/commands/download.py | 2 ++ src/debsbom/download/adapters.py | 44 ++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 src/debsbom/download/adapters.py diff --git a/src/debsbom/commands/download.py b/src/debsbom/commands/download.py index b54f3eed..7cab3170 100644 --- a/src/debsbom/commands/download.py +++ b/src/debsbom/commands/download.py @@ -18,6 +18,7 @@ from zstandard import ZstdCompressor, ZstdDecompressor import requests from ..snapshot import client as sdlclient + from ..download.adapters import LocalFileAdapter from ..download.download import PackageDownloader from ..download.resolver import PersistentResolverCache, UpstreamResolver from debsbom.download.download import DownloadStatus, DownloadResult @@ -77,6 +78,7 @@ def run(cls, args): else: resolver = cls.get_pkgstream_resolver() rs = requests.Session() + rs.mount("file:///", LocalFileAdapter()) rs.headers.update({"User-Agent": f"debsbom/{version('debsbom')}"}) sdl = sdlclient.SnapshotDataLake(session=rs) u_resolver = UpstreamResolver(sdl, cache) diff --git a/src/debsbom/download/adapters.py b/src/debsbom/download/adapters.py new file mode 100644 index 00000000..1993ed34 --- /dev/null +++ b/src/debsbom/download/adapters.py @@ -0,0 +1,44 @@ +# Copyright (C) 2025 Siemens +# +# SPDX-License-Identifier: MIT + +import errno +from io import BytesIO +import locale +from pathlib import Path +from requests import Response, Request, Session, codes +from requests.adapters import BaseAdapter +from urllib.parse import unquote, urlparse + + +class LocalFileAdapter(BaseAdapter): + """Adapter for local file access.""" + + def send(self, request, **kwargs) -> Response: + if request.method != "GET": + raise ValueError(f"Request method {request.method} is not supported") + + response = Response() + response.request = request + response.url = request.url + + path = Path(unquote(urlparse(request.url).path)) + try: + response.raw = open(path, "rb") + # make sure we properly close the file when we are done + response.raw.release_conn = response.raw.close + response.status_code = codes.ok + except IOError as e: + if e.errno == errno.EACCES: + response.status_code = codes.forbidden + elif e.errno == errno.ENOENT: + response.status_code = codes.not_found + else: + response.status_code = codes.bad_request + response.raw = BytesIO(str(e).encode(locale.getpreferredencoding())) + response.reason = str(e) + + return response + + def close(self): + pass From cb4c2eb66b80b80981dcd4ef15be26fdb5957a48 Mon Sep 17 00:00:00 2001 From: Christoph Steiger Date: Tue, 25 Nov 2025 11:02:22 +0100 Subject: [PATCH 2/2] chore(tests): add tests for local downloads Signed-off-by: Christoph Steiger --- tests/data/local-download | 1 + tests/test_download.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 tests/data/local-download diff --git a/tests/data/local-download b/tests/data/local-download new file mode 100644 index 00000000..25e8e657 --- /dev/null +++ b/tests/data/local-download @@ -0,0 +1 @@ +This is a test file for the local file adapter test. diff --git a/tests/test_download.py b/tests/test_download.py index 5e7ccf29..84766ef2 100644 --- a/tests/test_download.py +++ b/tests/test_download.py @@ -8,6 +8,7 @@ import jsonschema import pytest +from debsbom.download.adapters import LocalFileAdapter from debsbom.download import ( PackageDownloader, PersistentResolverCache, @@ -31,6 +32,7 @@ import spdx_tools.spdx.writer.json.json_writer as spdx_json_writer import cyclonedx.output as cdx_output import cyclonedx.schema as cdx_schema +from requests import Session from unittest import mock @@ -291,3 +293,18 @@ def test_download_result_invalid(dlschema): } with pytest.raises(jsonschema.ValidationError): jsonschema.validate(data, schema=dlschema) + + +def test_local_file(): + session = Session() + session.mount("file:///", LocalFileAdapter()) + with session.get("file://" + str(Path("tests/data/local-download").absolute())) as r: + assert r.status_code == 200 + assert r.content == b"This is a test file for the local file adapter test.\n" + + +def test_local_file_404(): + session = Session() + session.mount("file:///", LocalFileAdapter()) + with session.get("file:///does-not-exist") as r: + assert r.status_code == 404