Repository: pypi/warehouse

Issue:
Handle stale references to static files
This may be solved by #776, but if static files are changed the old files are no longer available. This means that if `/` is cached with a reference to `main.hash1.css` but that is gone now and was replaced with `main.hash2.css`. We'll want some solution to this which might be #776 or might be some sort of purging mechanism.



Discussion:

Resolving patch:
diff --git a/warehouse/config.py b/warehouse/config.py
--- a/warehouse/config.py
+++ b/warehouse/config.py
@@ -21,11 +21,11 @@
 from pyramid import renderers
 from pyramid.config import Configurator as _Configurator
 from pyramid.response import Response
-from pyramid.static import ManifestCacheBuster
 from pyramid.tweens import EXCVIEW
 from pyramid_rpc.xmlrpc import XMLRPCRenderer
 
 from warehouse import __commit__
+from warehouse.utils.static import ManifestCacheBuster
 from warehouse.utils.wsgi import ProxyFixer, VhmRootRemover
 
 
@@ -357,7 +357,11 @@ def configure(settings=None):
     )
 
     # Enable Warehouse to serve our static files
-    config.add_static_view(name="static", path="warehouse:static/dist/")
+    config.add_static_view(
+        "static",
+        "warehouse:static/dist/",
+        cache_max_age=10 * 365 * 24 * 60 * 60,  # 10 years
+    )
     config.add_cache_buster(
         "warehouse:static/dist/",
         ManifestCacheBuster(
@@ -367,7 +371,7 @@ def configure(settings=None):
     )
 
     # Enable Warehouse to serve our locale files
-    config.add_static_view(name="locales", path="warehouse:locales/")
+    config.add_static_view("locales", "warehouse:locales/")
 
     # Enable support of passing certain values like remote host, client
     # address, and protocol support in from an outer proxy to the application.
diff --git a/warehouse/utils/static.py b/warehouse/utils/static.py
new file mode 100644
--- /dev/null
+++ b/warehouse/utils/static.py
@@ -0,0 +1,30 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from pyramid.static import ManifestCacheBuster as _ManifestCacheBuster
+
+
+class ManifestCacheBuster(_ManifestCacheBuster):
+
+    def __call__(self, request, subpath, kw):
+        try:
+            return self.manifest[subpath], kw
+        except KeyError:
+            # We raise an error here even though the one from Pyramid does not.
+            # This is done because we want to be strict that all static files
+            # must be cache busted otherwise it is likely an error of some kind
+            # and should be remedied and without a loud error it's unlikely to
+            # be noticed.
+            raise ValueError(
+                "{} is not able to be cache busted.".format(subpath)
+            ) from None


Test patch:
diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py
--- a/tests/unit/test_config.py
+++ b/tests/unit/test_config.py
@@ -275,7 +275,7 @@ def __init__(self):
             lambda d: configurator_settings.update(d)
         ),
         add_tween=pretend.call_recorder(lambda tween_factory, **kw: None),
-        add_static_view=pretend.call_recorder(lambda name, path, **kw: None),
+        add_static_view=pretend.call_recorder(lambda *a, **kw: None),
         add_cache_buster=pretend.call_recorder(lambda spec, buster: None),
         scan=pretend.call_recorder(lambda ignore: None),
     )
@@ -441,8 +441,12 @@ def __init__(self):
         ),
     ]
     assert configurator_obj.add_static_view.calls == [
-        pretend.call(name="static", path="warehouse:static/dist/"),
-        pretend.call(name="locales", path="warehouse:locales/"),
+        pretend.call(
+            "static",
+            "warehouse:static/dist/",
+            cache_max_age=315360000,
+        ),
+        pretend.call("locales", "warehouse:locales/"),
     ]
     assert configurator_obj.add_cache_buster.calls == [
         pretend.call("warehouse:static/dist/", cachebuster_obj),
diff --git a/tests/unit/utils/test_static.py b/tests/unit/utils/test_static.py
new file mode 100644
--- /dev/null
+++ b/tests/unit/utils/test_static.py
@@ -0,0 +1,32 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+
+from warehouse.utils.static import ManifestCacheBuster
+
+
+class TestManifestCacheBuster:
+
+    def test_returns_when_valid(self):
+        cb = ManifestCacheBuster("warehouse:static/dist/manifest.json")
+        cb._manifest = {"/the/path/style.css": "/the/busted/path/style.css"}
+        result = cb(None, "/the/path/style.css", {"keyword": "arg"})
+
+        assert result == ("/the/busted/path/style.css", {"keyword": "arg"})
+
+    def test_raises_when_invalid(self):
+        cb = ManifestCacheBuster("warehouse:static/dist/manifest.json")
+        cb._manifest = {}
+
+        with pytest.raises(ValueError):
+            cb(None, "/the/path/style.css", {"keyword": "arg"})