diff --git a/python/lsst/daf/butler/remote_butler/server/_config.py b/python/lsst/daf/butler/remote_butler/server/_config.py
new file mode 100644
index 0000000000..16537eff09
--- /dev/null
+++ b/python/lsst/daf/butler/remote_butler/server/_config.py
@@ -0,0 +1,54 @@
+# This file is part of daf_butler.
+#
+# Developed for the LSST Data Management System.
+# This product includes software developed by the LSST Project
+# (http://www.lsst.org).
+# See the COPYRIGHT file at the top-level directory of this distribution
+# for details of code ownership.
+#
+# This software is dual licensed under the GNU General Public License and also
+# under a 3-clause BSD license. Recipients may choose which of these licenses
+# to use; please see the files gpl-3.0.txt and/or bsd_license.txt,
+# respectively. If you choose the GPL option then the following text applies
+# (but note that there is still no warranty even if you opt for BSD instead):
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+from __future__ import annotations
+
+import dataclasses
+import os
+
+
+@dataclasses.dataclass(frozen=True)
+class ButlerServerConfig:
+ """Butler server configuration loaded from environment variables.
+
+ Notes
+ -----
+ Besides these variables, there is one critical environment variable. The
+ list of repositories to be hosted must be defined using
+ ``DAF_BUTLER_REPOSITORIES`` or ``DAF_BUTLER_REPOSITORY_INDEX`` -- see
+ `ButlerRepoIndex`.
+ """
+
+ static_files_path: str | None
+ """Absolute path to a directory of files that will be served to end-users
+ as static files from the `configs/` HTTP route.
+ """
+
+
+def load_config() -> ButlerServerConfig:
+ """Read the Butler server configuration from the environment."""
+ return ButlerServerConfig(static_files_path=os.environ.get("DAF_BUTLER_SERVER_STATIC_FILES_PATH"))
diff --git a/python/lsst/daf/butler/remote_butler/server/_server.py b/python/lsst/daf/butler/remote_butler/server/_server.py
index d0547ee201..9c72f520d5 100644
--- a/python/lsst/daf/butler/remote_butler/server/_server.py
+++ b/python/lsst/daf/butler/remote_butler/server/_server.py
@@ -34,11 +34,13 @@
import safir.dependencies.logger
from fastapi import FastAPI, Request, Response
from fastapi.middleware.gzip import GZipMiddleware
+from fastapi.staticfiles import StaticFiles
from safir.logging import configure_logging, configure_uvicorn_logging
from ..._exceptions import ButlerUserError
from .._errors import serialize_butler_user_error
from ..server_models import CLIENT_REQUEST_ID_HEADER_NAME, ERROR_STATUS_CODE, ErrorResponseModel
+from ._config import load_config
from .handlers._external import external_router
from .handlers._external_query import query_router
from .handlers._internal import internal_router
@@ -49,6 +51,8 @@
def create_app() -> FastAPI:
"""Create a Butler server FastAPI application."""
+ config = load_config()
+
app = FastAPI()
app.add_middleware(GZipMiddleware, minimum_size=1000)
@@ -67,6 +71,17 @@ def create_app() -> FastAPI:
)
app.include_router(internal_router)
+ # If configured to do so, serve a directory of static files via HTTP.
+ #
+ # Until we are able to fully transition away from DirectButler for the RSP,
+ # we need a place to host DirectButler configuration files. Since this
+ # same configuration is needed for Butler server, it's easier to configure
+ # in Phalanx if we just host them from Butler server itself.
+ # This will also host the end-user repository index file for the RSP, for
+ # lack of a better place to put it.
+ if config.static_files_path:
+ app.mount(f"{default_api_path}/configs", StaticFiles(directory=config.static_files_path))
+
# Any time an exception is returned by a handler, add a log message that
# includes the username and request ID from the client. This will make it
# easier to track down user-reported issues in the logs.
diff --git a/python/lsst/daf/butler/tests/server_utils.py b/python/lsst/daf/butler/tests/server_utils.py
index 5f4c37f9e3..c92db6a1f0 100644
--- a/python/lsst/daf/butler/tests/server_utils.py
+++ b/python/lsst/daf/butler/tests/server_utils.py
@@ -70,5 +70,7 @@ def _is_authenticated_endpoint(path: str) -> bool:
return False
if path.endswith("/butler.json"):
return False
+ if path.startswith("/api/butler/configs"):
+ return False
return True
diff --git a/tests/test_server.py b/tests/test_server.py
index e28ff2eb58..c3fb6c31a3 100644
--- a/tests/test_server.py
+++ b/tests/test_server.py
@@ -26,6 +26,7 @@
# along with this program. If not, see .
import os.path
+import tempfile
import unittest
import uuid
@@ -106,6 +107,17 @@ def test_health_check(self):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["name"], "butler")
+ def test_static_files(self):
+ with tempfile.TemporaryDirectory() as tmpdir:
+ with open(os.path.join(tmpdir, "temp.txt"), "w") as fh:
+ fh.write("test data 123")
+
+ with mock_env({"DAF_BUTLER_SERVER_STATIC_FILES_PATH": tmpdir}):
+ with create_test_server(TESTDIR) as server:
+ response = server.client.get("/api/butler/configs/temp.txt")
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.text, "test data 123")
+
def test_dimension_universe(self):
universe = self.butler.dimensions
self.assertEqual(universe.namespace, "daf_butler")