Skip to content

Commit 0705b77

Browse files
Merge pull request #1880 from MetOffice/1049_cache_busting
Add cache busting to CSET web UI to avoid showing old versions
2 parents 18543e9 + f1f2745 commit 0705b77

File tree

4 files changed

+57
-15
lines changed

4 files changed

+57
-15
lines changed

src/CSET/cset_workflow/app/finish_website/bin/finish_website.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@
2121
web interface.
2222
"""
2323

24-
import datetime
2524
import json
2625
import logging
2726
import os
2827
import shutil
28+
import time
2929
from importlib.metadata import version
3030
from pathlib import Path
3131

@@ -95,11 +95,30 @@ def construct_index(www_content: Path):
9595
json.dump(index, fp, indent=2)
9696

9797

98+
def bust_cache(www_content: Path):
99+
"""Add a unique query string to static requests to avoid stale caches.
100+
101+
We only need to do this for static resources referenced from the index page,
102+
as each plot already uses a unique filename based on the recipe.
103+
"""
104+
# Search and replace the string "CACHEBUSTER".
105+
CACHEBUSTER = str(int(time.time()))
106+
with open(www_content / "index.html", "r+t") as fp:
107+
content = fp.read()
108+
new_content = content.replace("CACHEBUSTER", CACHEBUSTER)
109+
fp.seek(0)
110+
fp.truncate()
111+
fp.write(new_content)
112+
113+
# Move plots directory so it has a unique name.
114+
os.rename(www_content / "plots", www_content / f"plots-{CACHEBUSTER}")
115+
116+
98117
def update_workflow_status(www_content: Path):
99118
"""Update the workflow status on the front page of the web interface."""
100119
with open(www_content / "placeholder.html", "r+t") as fp:
101120
content = fp.read()
102-
finish_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M")
121+
finish_time = time.strftime("%Y-%m-%d %H:%M", time.localtime())
103122
status = f"Completed at {finish_time} using CSET v{version('CSET')}"
104123
new_content = content.replace(
105124
'<p id="workflow-status">Unknown</p>',
@@ -125,9 +144,10 @@ def run():
125144
www_content = Path(os.environ["CYLC_WORKFLOW_SHARE_DIR"] + "/web")
126145

127146
install_website_skeleton(www_root_link, www_content)
147+
copy_rose_config(www_content)
128148
construct_index(www_content)
149+
bust_cache(www_content)
129150
update_workflow_status(www_content)
130-
copy_rose_config(www_content)
131151

132152

133153
if __name__ == "__main__": # pragma: no cover

src/CSET/cset_workflow/app/finish_website/file/html/index.html

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<meta charset="UTF-8">
66
<meta name="viewport" content="width=device-width, initial-scale=1.0">
77
<title>CSET</title>
8-
<link rel="stylesheet" href="style.css">
8+
<link rel="stylesheet" href="style.css?v=CACHEBUSTER">
99
<link rel="icon" href="favicon.ico" type="image/x-icon">
1010
</head>
1111

@@ -32,7 +32,8 @@ <h1>CSET</h1>
3232
</article>
3333
</div>
3434
</main>
35-
<script src="script.js"></script>
35+
<script>const PLOTS_PATH = "plots-CACHEBUSTER";</script>
36+
<script src="script.js?v=CACHEBUSTER"></script>
3637
</body>
3738

3839
</html>

src/CSET/cset_workflow/app/finish_website/file/html/script.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ function construct_sidebar_from_data(data) {
115115
event.preventDefault();
116116
// Set the appropriate frame layout.
117117
position == "full" ? ensure_single_frame() : ensure_dual_frame();
118-
document.getElementById(`plot-frame-${position}`).src = `plots/${plot}`;
118+
document.getElementById(`plot-frame-${position}`).src = `${PLOTS_PATH}/${plot}`;
119119
});
120120

121121
// Add button to chooser.
@@ -146,7 +146,7 @@ function setup_plots_sidebar() {
146146
return;
147147
}
148148
// Loading of plot index file, and adding them to the sidebar.
149-
fetch("plots/index.json")
149+
fetch(`${PLOTS_PATH}/index.json`)
150150
.then((response) => {
151151
// Display a message and stop if the fetch fails.
152152
if (!response.ok) {

tests/workflow_utils/test_finish_website.py

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,22 @@ def test_copy_rose_config(monkeypatch, tmp_path):
4949
assert fp.read() == "Test rose-suite.conf file\n"
5050

5151

52+
def test_bust_cache(tmp_path):
53+
"""Cache busting query string is added where requested."""
54+
with open(tmp_path / "index.html", "wt") as fp:
55+
fp.write('<img href="image.jpg?v=CACHEBUSTER" />\n')
56+
(tmp_path / "plots").mkdir()
57+
finish_website.bust_cache(tmp_path)
58+
# Check cache busting query has been added.
59+
with open(tmp_path / "index.html", "rt") as fp:
60+
content = fp.read()
61+
assert "CACHEBUSTER" not in content
62+
assert re.search(r"\?v=\d+", content)
63+
renamed_plot_dir = list(tmp_path.glob("plots-*"))
64+
assert len(renamed_plot_dir) == 1
65+
assert renamed_plot_dir[0].is_dir()
66+
67+
5268
def test_write_workflow_status(tmp_path):
5369
"""Workflow finish status gets written to placeholder file."""
5470
web_dir = tmp_path / "web"
@@ -150,22 +166,27 @@ def test_entrypoint(monkeypatch):
150166
# are all run.
151167
counter = 0
152168

169+
def increment_counter(*args, **kwargs):
170+
nonlocal counter
171+
counter += 1
172+
173+
def check_single_arg(www_content: Path):
174+
assert www_content == Path("/share/web")
175+
increment_counter()
176+
153177
def check_args(www_root_link: Path, www_content: Path):
154178
assert www_root_link == Path("/var/www/cset")
155179
assert www_content == Path("/share/web")
156180
increment_counter()
157181

158-
def increment_counter(*args, **kwargs):
159-
nonlocal counter
160-
counter += 1
161-
162182
monkeypatch.setattr(finish_website, "install_website_skeleton", check_args)
163-
monkeypatch.setattr(finish_website, "construct_index", increment_counter)
164-
monkeypatch.setattr(finish_website, "update_workflow_status", increment_counter)
165-
monkeypatch.setattr(finish_website, "copy_rose_config", increment_counter)
183+
monkeypatch.setattr(finish_website, "copy_rose_config", check_single_arg)
184+
monkeypatch.setattr(finish_website, "construct_index", check_single_arg)
185+
monkeypatch.setattr(finish_website, "bust_cache", check_single_arg)
186+
monkeypatch.setattr(finish_website, "update_workflow_status", check_single_arg)
166187
monkeypatch.setenv("WEB_DIR", "/var/www/cset")
167188
monkeypatch.setenv("CYLC_WORKFLOW_SHARE_DIR", "/share")
168189

169190
# Check that it runs all the needed subfunctions.
170191
finish_website.run()
171-
assert counter == 4
192+
assert counter == 5

0 commit comments

Comments
 (0)