Skip to content

Commit b8f4c87

Browse files
committed
Serve static files with their expected hashes for cache-busting (upstream PR cms-dev#1524)
This makes it unnecessary to do a hard refresh after editing static files (especially relevant for cws_utils.js or cws_style.css), as the edited files will have a different hash and thus will be cached separately.
1 parent ea763f5 commit b8f4c87

File tree

13 files changed

+110
-42
lines changed

13 files changed

+110
-42
lines changed

cms/io/web_service.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
# You should have received a copy of the GNU Affero General Public License
2020
# along with this program. If not, see <http://www.gnu.org/licenses/>.
2121

22+
import hashlib
2223
import logging
24+
import importlib.resources
2325

2426
import collections
2527
try:
@@ -36,6 +38,7 @@
3638

3739
from cms.db.filecacher import FileCacher
3840
from cms.server.file_middleware import FileServerMiddleware
41+
from cms.server.util import Url
3942
from .service import Service
4043
from .web_rpc import RPCMiddleware
4144

@@ -45,6 +48,58 @@
4548

4649
SECONDS_IN_A_YEAR = 365 * 24 * 60 * 60
4750

51+
class StaticFileHasher:
52+
"""
53+
Constructs URLs to static files. The result of make() is similar to the
54+
url() function that's used in the templates, in that it constructs a
55+
relative URL, but it also adds a "?h=12345678" query parameter which forces
56+
browsers to reload the resource when it has changed.
57+
"""
58+
def __init__(self, files: list[tuple[str, str]]):
59+
"""
60+
Initialize.
61+
62+
files: list of static file locations, each in the format that would be
63+
passed to SharedDataMiddleware.
64+
"""
65+
# Cache of the hashes of files, to prevent re-hashing them on every request.
66+
self.cache: dict[tuple[str, ...], str] = {}
67+
# We reverse the order, because in WSGI later-added middlewares
68+
# override earlier ones, but here we iterate the locations and use the
69+
# first found match.
70+
self.static_locations = files[::-1]
71+
72+
def make(self, base_url: Url):
73+
"""
74+
Create a new url helper function (called once per request).
75+
76+
The returned function takes arguments in the same format as `Url`, and
77+
returns a string in the same format as `Url` except with a hash
78+
appended as a query string.
79+
"""
80+
def inner_func(*paths: str):
81+
# WebService always serves the static files under /static.
82+
assert paths[0] == "static"
83+
84+
url_path_part = base_url(*paths)
85+
86+
if paths in self.cache:
87+
return url_path_part + self.cache[paths]
88+
89+
for module_name, dir in self.static_locations:
90+
resource = importlib.resources.files(module_name).joinpath(dir, *paths[1:])
91+
if resource.is_file():
92+
with resource.open('rb') as file:
93+
hash = hashlib.file_digest(file, hashlib.sha256).hexdigest()
94+
result = "?h=" + hash[:24]
95+
break
96+
else:
97+
logger.warning(f"Did not find path passed to static_url(): {paths}")
98+
result = ""
99+
100+
self.cache[paths] = result
101+
return url_path_part + result
102+
return inner_func
48103

49104
class WebService(Service):
50105
"""RPC service with Web server capabilities.
@@ -78,6 +133,8 @@ def __init__(
78133
cache=True, cache_timeout=SECONDS_IN_A_YEAR,
79134
fallback_mimetype="application/octet-stream")
80135

136+
self.static_file_hasher = StaticFileHasher(static_files)
137+
81138
self.file_cacher = FileCacher(self)
82139
self.wsgi_app = FileServerMiddleware(self.file_cacher, self.wsgi_app)
83140

cms/server/admin/handlers/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,7 @@ def render_params(self) -> dict:
326326
params["timestamp"] = make_datetime()
327327
params["contest"] = self.contest
328328
params["url"] = self.url
329+
params["static_url"] = self.static_url_helper
329330
params["xsrf_form_html"] = self.xsrf_form_html()
330331
# FIXME These objects provide too broad an access: their usage
331332
# should be extracted into with narrower-scoped parameters.

cms/server/admin/templates/base.html

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,18 @@
22
<html lang="en">
33
<head>
44
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
5-
<link rel="shortcut icon" href="{{ url("static", "favicon.ico") }}" />
6-
<link rel="stylesheet" type="text/css" href="{{ url("static", "reset.css") }}">
7-
<link rel="stylesheet" type="text/css" href="{{ url("static", "aws_style.css") }}">
8-
<script src="{{ url("static", "web_rpc.js") }}"></script>
9-
<script src="{{ url("static", "aws_utils.js") }}"></script>
10-
<script src="{{ url("static", "jq", "jquery-3.6.0.min.js") }}"></script>
11-
<script src="{{ url("static", "jq", "jquery.jqplot.min.js") }}"></script>
12-
<script src="{{ url("static", "jq", "jqplot.dateAxisRenderer.min.js") }}"></script>
13-
<script src="{{ url("static", "jq", "jqplot.enhancedLegendRenderer.min.js") }}"></script>
14-
<link rel="stylesheet" type="text/css" href="{{ url("static", "jq", "jquery.jqplot.min.css") }}"/>
15-
<link rel="stylesheet" type="text/css" href="{{ url("static", "prism.css") }}">
16-
<script src="{{ url("static", "prism.js") }}" data-manual></script>
5+
<link rel="shortcut icon" href="{{ static_url("static", "favicon.ico") }}" />
6+
<link rel="stylesheet" type="text/css" href="{{ static_url("static", "reset.css") }}">
7+
<link rel="stylesheet" type="text/css" href="{{ static_url("static", "aws_style.css") }}">
8+
<script src="{{ static_url("static", "web_rpc.js") }}"></script>
9+
<script src="{{ static_url("static", "aws_utils.js") }}"></script>
10+
<script src="{{ static_url("static", "jq", "jquery-3.6.0.min.js") }}"></script>
11+
<script src="{{ static_url("static", "jq", "jquery.jqplot.min.js") }}"></script>
12+
<script src="{{ static_url("static", "jq", "jqplot.dateAxisRenderer.min.js") }}"></script>
13+
<script src="{{ static_url("static", "jq", "jqplot.enhancedLegendRenderer.min.js") }}"></script>
14+
<link rel="stylesheet" type="text/css" href="{{ static_url("static", "jq", "jquery.jqplot.min.css") }}"/>
15+
<link rel="stylesheet" type="text/css" href="{{ static_url("static", "prism.css") }}">
16+
<script src="{{ static_url("static", "prism.js") }}" data-manual></script>
1717

1818
{% if contest is none %}
1919
<title>Admin</title>

cms/server/admin/templates/login.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
<html lang="en">
44
<head>
55
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
6-
<link rel="shortcut icon" href="{{ url("static", "favicon.ico") }}" />
7-
<link rel="stylesheet" type="text/css" href="{{ url("static", "reset.css") }}">
8-
<link rel="stylesheet" type="text/css" href="{{ url("static", "aws_style.css") }}">
6+
<link rel="shortcut icon" href="{{ static_url("static", "favicon.ico") }}" />
7+
<link rel="stylesheet" type="text/css" href="{{ static_url("static", "reset.css") }}">
8+
<link rel="stylesheet" type="text/css" href="{{ static_url("static", "aws_style.css") }}">
99
<title>Admin</title>
1010
</head>
1111
<body class="admin">

cms/server/admin/templates/overview.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ <h2 id="title_submissions_status" class="toggling_on">Submissions status</h2>
258258
</tr>
259259
</thead>
260260
<tbody>
261-
<tr><td style="text-align: center;" colspan="2"><img src="{{ url("static", "loading.gif") }}" alt="loading..." /></td></tr>
261+
<tr><td style="text-align: center;" colspan="2"><img src="{{ static_url("static", "loading.gif") }}" alt="loading..." /></td></tr>
262262
</tbody>
263263
</table>
264264
<div class="hr"></div>
@@ -277,7 +277,7 @@ <h2 id="title_queue_status" class="toggling_on">Queue status</h2>
277277
</tr>
278278
</thead>
279279
<tbody>
280-
<tr><td style="text-align: center;" colspan="4"><img src="{{ url("static", "loading.gif") }}" alt="loading..." /></td></tr>
280+
<tr><td style="text-align: center;" colspan="4"><img src="{{ static_url("static", "loading.gif") }}" alt="loading..." /></td></tr>
281281
</tbody>
282282
</table>
283283
<div class="hr"></div>
@@ -296,7 +296,7 @@ <h2 id="title_workers_status" class="toggling_on">Workers status</h2>
296296
</tr>
297297
</thead>
298298
<tbody>
299-
<tr><td style="text-align: center;" colspan="5"><img src="{{ url("static", "loading.gif") }}" alt="loading..." /></td></tr>
299+
<tr><td style="text-align: center;" colspan="5"><img src="{{ static_url("static", "loading.gif") }}" alt="loading..." /></td></tr>
300300
</tbody>
301301
</table>
302302
<div class="hr"></div>
@@ -316,7 +316,7 @@ <h2 id="title_logs" class="toggling_on">Logs</h2>
316316
</tr>
317317
</thead>
318318
<tbody>
319-
<tr><td style="text-align: center;" colspan="5"><img src="{{ url("static", "loading.gif") }}" alt="loading..." /></td></tr>
319+
<tr><td style="text-align: center;" colspan="5"><img src="{{ static_url("static", "loading.gif") }}" alt="loading..." /></td></tr>
320320
</tbody>
321321
</table>
322322
<div class="hr"></div>

cms/server/admin/templates/resources.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@
211211

212212
kill_service: function(s, link)
213213
{
214-
link.parentNode.innerHTML = '<img src="{{ url("static", "loading.gif") }}" alt="loading..." />';
214+
link.parentNode.innerHTML = '<img src="{{ static_url("static", "loading.gif") }}" alt="loading..." />';
215215
cmsrpc_request("ResourceService", this.shard,
216216
"kill_service",
217217
{"service": s});
@@ -302,7 +302,7 @@ <h2 id="title_machine_{{ i }}" class="toggling_on">Machine {{ i }} ({{ resource_
302302
</tr>
303303
</thead>
304304
<tbody>
305-
<tr><td style="text-align: center;" colspan="8"><img src="{{ url("static", "loading.gif") }}" alt="loading..." /></td></tr>
305+
<tr><td style="text-align: center;" colspan="8"><img src="{{ static_url("static", "loading.gif") }}" alt="loading..." /></td></tr>
306306
</tbody>
307307
</table>
308308

cms/server/contest/handlers/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ def render_params(self) -> dict:
136136
ret["now"] = self.timestamp
137137
ret["utc"] = utc_tzinfo
138138
ret["url"] = self.url
139+
ret["static_url"] = self.static_url_helper
139140

140141
ret["available_translations"] = self.available_translations
141142

cms/server/contest/templates/base.html

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@
66

77
<title>{% block title %}{% endblock title %}</title>
88

9-
<link rel="shortcut icon" href="{{ url("static", "favicon.ico") }}" />
10-
<link rel="stylesheet" href="{{ url("static", "css", "bootstrap.css") }}">
11-
<link rel="stylesheet" href="{{ url("static", "cws_style.css") }}">
9+
<link rel="shortcut icon" href="{{ static_url("static", "favicon.ico") }}" />
10+
<link rel="stylesheet" href="{{ static_url("static", "css", "bootstrap.css") }}">
11+
<link rel="stylesheet" href="{{ static_url("static", "cws_style.css") }}">
1212

13-
<script src="{{ url("static", "jq", "jquery-3.6.0.min.js") }}"></script>
13+
<script src="{{ static_url("static", "jq", "jquery-3.6.0.min.js") }}"></script>
1414
{# For compatibility with Bootstrap 2.x #}
15-
<script src="{{ url("static", "jq", "jquery-migrate-3.3.2.min.js") }}"></script>
16-
<script src="{{ url("static", "js", "bootstrap.js") }}"></script>
17-
<script src="{{ url("static", "cws_utils.js") }}"></script>
15+
<script src="{{ static_url("static", "jq", "jquery-migrate-3.3.2.min.js") }}"></script>
16+
<script src="{{ static_url("static", "js", "bootstrap.js") }}"></script>
17+
<script src="{{ static_url("static", "cws_utils.js") }}"></script>
1818

1919
{% block js %}{% endblock js %}
2020
</head>

cms/server/contest/templates/macro/submission.html

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
{% macro rows(url, contest_url, translation, xsrf_form_html,
1+
{% macro rows(url, static_url, contest_url, translation, xsrf_form_html,
22
actual_phase, task, submissions,
33
can_use_tokens, can_play_token, can_play_token_now,
44
submissions_download_allowed, official) -%}
55
{#
66
Render a submission table with all (un)official submissions passed.
77

88
url (Url): the URL instance referring to the root of CWS.
9+
static_url: static url helper constructed from url
910
contest_url (Url): the URL instance referring to the main contest page.
1011
translation (Translation): locale to use to show messages.
1112
xsrf_form_html (str): input element for the XSRF protection.
@@ -90,6 +91,7 @@
9091
{# loop.revindex is broken: https://github.com/pallets/jinja/issues/794 #}
9192
{{ row(
9293
url,
94+
static_url,
9395
contest_url,
9496
translation,
9597
xsrf_form_html,
@@ -108,14 +110,15 @@
108110
</table>
109111
{%- endmacro %}
110112

111-
{% macro row(url, contest_url, translation, xsrf_form_html,
113+
{% macro row(url, static_url, contest_url, translation, xsrf_form_html,
112114
actual_phase, s, opaque_id, show_date,
113115
can_use_tokens, can_play_token, can_play_token_now,
114116
submissions_download_allowed) -%}
115117
{#
116118
Render a row in a submission table.
117119

118120
url (Url): the URL instance referring to the root of CWS.
121+
static_url: static url helper constructed from url
119122
contest_url (Url): the URL instance referring to the main contest page.
120123
translation (Translation): locale to use to show messages.
121124
xsrf_form_html (str): input element for the XSRF protection.
@@ -146,16 +149,16 @@
146149
<td class="status">
147150
{% if status == SubmissionResult.COMPILING %}
148151
{% trans %}Compiling...{% endtrans %}
149-
<img class="details" src="{{ url("static", "loading.gif") }}" />
152+
<img class="details" src="{{ static_url("static", "loading.gif") }}" />
150153
{% elif status == SubmissionResult.COMPILATION_FAILED %}
151154
{% trans %}Compilation failed{% endtrans %}
152155
<a class="details">{% trans %}details{% endtrans %}</a>
153156
{% elif status == SubmissionResult.EVALUATING %}
154157
{% trans %}Evaluating...{% endtrans %}
155-
<img class="details" src="{{ url("static", "loading.gif") }}" />
158+
<img class="details" src="{{ static_url("static", "loading.gif") }}" />
156159
{% elif status == SubmissionResult.SCORING %}
157160
{% trans %}Scoring...{% endtrans %}
158-
<img class="details" src="{{ url("static", "loading.gif") }}" />
161+
<img class="details" src="{{ static_url("static", "loading.gif") }}" />
159162
{% elif status == SubmissionResult.SCORED %}
160163
{% trans %}Evaluated{% endtrans %}
161164
<a class="details">{% trans %}details{% endtrans %}</a>

cms/server/contest/templates/task_description.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,9 +224,9 @@ <h2>{% trans %}Attachments{% endtrans %}</h2>
224224
<li>
225225
<a href="{{ contest_url("tasks", task.name, "attachments", filename) }}" class="btn">
226226
{% if type_icon is not none %}
227-
<img src="{{ url("static", "img", "mimetypes", "%s.png"|format(type_icon)) }}" alt="{{ mime_type }}" />
227+
<img src="{{ static_url("static", "img", "mimetypes", "%s.png"|format(type_icon)) }}" alt="{{ mime_type }}" />
228228
{% else %}
229-
<img src="{{ url("static", "img", "mimetypes", "unknown.png") }}" alt="{% trans %}unknown{% endtrans %}" />
229+
<img src="{{ static_url("static", "img", "mimetypes", "unknown.png") }}" alt="{% trans %}unknown{% endtrans %}" />
230230
{% endif %}
231231
<span class="first_line">
232232
<span class="name">{{ filename }}</span>

0 commit comments

Comments
 (0)