From ddc7abf771e433063184637195a4dc81ba6a3ed5 Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Tue, 2 May 2023 16:53:36 +0200 Subject: [PATCH 1/3] Allows immutable cache for static files in a directory --- jupyter_server/base/handlers.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/jupyter_server/base/handlers.py b/jupyter_server/base/handlers.py index 061eea672a..73eb4cbae9 100644 --- a/jupyter_server/base/handlers.py +++ b/jupyter_server/base/handlers.py @@ -951,8 +951,15 @@ class FileFindHandler(JupyterHandler, web.StaticFileHandler): def set_headers(self): """Set the headers.""" super().set_headers() + + immutable_paths = self.settings["static_immutable_cache"] or [] + + # allow immutable cache for files + if any(self.request.path.startswith(path) for path in immutable_paths): + self.set_header("Cache-Control", "public, max-age=31536000, immutable") + # disable browser caching, rely on 304 replies for savings - if "v" not in self.request.arguments or any( + elif "v" not in self.request.arguments or any( self.request.path.startswith(path) for path in self.no_cache_paths ): self.set_header("Cache-Control", "no-cache") From 51876a0ab4f38bce80534ed8c99d937acc9fc8f0 Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Tue, 2 May 2023 23:18:11 +0200 Subject: [PATCH 2/3] Add test --- jupyter_server/base/handlers.py | 10 ++++++++-- tests/base/test_handlers.py | 25 +++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/jupyter_server/base/handlers.py b/jupyter_server/base/handlers.py index 73eb4cbae9..c28807b8e2 100644 --- a/jupyter_server/base/handlers.py +++ b/jupyter_server/base/handlers.py @@ -942,7 +942,13 @@ def wrapper(self, *args, **kwargs): class FileFindHandler(JupyterHandler, web.StaticFileHandler): - """subclass of StaticFileHandler for serving files from a search path""" + """subclass of StaticFileHandler for serving files from a search path + + The setting "static_immutable_cache" can be set up to serve some static + file as immutable (e.g. file name containing a hash). The setting is a + list of base URL, every static file URL starting with one of those will + be immutable. + """ # cache search results, don't search for files more than once _static_paths: dict = {} @@ -952,7 +958,7 @@ def set_headers(self): """Set the headers.""" super().set_headers() - immutable_paths = self.settings["static_immutable_cache"] or [] + immutable_paths = self.settings.get("static_immutable_cache", []) # allow immutable cache for files if any(self.request.path.startswith(path) for path in immutable_paths): diff --git a/tests/base/test_handlers.py b/tests/base/test_handlers.py index d930bf6cc1..b08b106871 100644 --- a/tests/base/test_handlers.py +++ b/tests/base/test_handlers.py @@ -12,6 +12,7 @@ APIVersionHandler, AuthenticatedFileHandler, AuthenticatedHandler, + FileFindHandler, FilesRedirectHandler, JupyterHandler, RedirectWithParams, @@ -126,3 +127,27 @@ def test_redirect_with_params(jp_serverapp): handler._transforms = [] handler.get() assert handler.get_status() == 301 + + +async def test_static_handler(jp_serverapp, tmpdir): + async def async_magic(): + pass + + MagicMock.__await__ = lambda x: async_magic().__await__() + + test_file = tmpdir / "foo" + with open(test_file, "w") as fid: + fid.write("hello") + + app: ServerApp = jp_serverapp + request = HTTPRequest("GET", str(test_file)) + request.connection = MagicMock() + + handler = FileFindHandler(app.web_app, request, path=str(tmpdir)) + handler._transforms = [] + await handler.get("foo") + assert handler._headers["Cache-Control"] == "no-cache" + + handler.settings["static_immutable_cache"] = [str(tmpdir)] + await handler.get("foo") + assert handler._headers["Cache-Control"] == "public, max-age=31536000, immutable" From 1dcd2923434b037e6933d409b83028543005eb97 Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Wed, 3 May 2023 10:44:29 +0200 Subject: [PATCH 3/3] Adds a traitlet on immutable cache and test it --- jupyter_server/serverapp.py | 14 ++++++++++++++ tests/test_serverapp.py | 10 ++++++++++ 2 files changed, 24 insertions(+) diff --git a/jupyter_server/serverapp.py b/jupyter_server/serverapp.py index 48c60b7498..4844a2f585 100644 --- a/jupyter_server/serverapp.py +++ b/jupyter_server/serverapp.py @@ -1812,6 +1812,17 @@ def _default_terminals_enabled(self): config=True, ) + static_immutable_cache = List( + Unicode(), + help=""" + Paths to set up static files as immutable. + + This allow setting up the cache control of static files as immutable. + It should be used for static file named with a hash for instance. + """, + config=True, + ) + _starter_app = Instance( default_value=None, allow_none=True, @@ -1990,6 +2001,9 @@ def init_webapp(self): ] = self.identity_provider.get_secure_cookie_kwargs self.tornado_settings["token"] = self.identity_provider.token + if self.static_immutable_cache: + self.tornado_settings["static_immutable_cache"] = self.static_immutable_cache + # ensure default_url starts with base_url if not self.default_url.startswith(self.base_url): self.default_url = url_path_join(self.base_url, self.default_url) diff --git a/tests/test_serverapp.py b/tests/test_serverapp.py index 1bda3498fe..5f6c3eb16e 100644 --- a/tests/test_serverapp.py +++ b/tests/test_serverapp.py @@ -561,3 +561,13 @@ def test_deprecated_notebook_dir_priority(jp_configurable_serverapp, tmp_path): cfg.ServerApp.notebook_dir = str(notebook_dir) app.update_config(cfg) assert app.root_dir == str(cli_dir) + + +def test_immutable_cache_trait(): + # Verify we're working with a clean instance. + ServerApp.clear_instance() + kwargs = {"static_immutable_cache": "/test/immutable"} + serverapp = ServerApp.instance(**kwargs) + serverapp.init_configurables() + serverapp.init_webapp() + assert serverapp.web_app.settings["static_immutable_cache"] == ["/test/immutable"]