Many bust caching solutions exists out there. However, most of them rely on the use of a query parameters and it has been proven many times over that it is not the best idea since browser might discard it.
In this article, I’ll present you a solution that virtually changes the path of your static resources based on the modified timestamp of the last updated resource.
You could also use a git hash instead of the timestamp, but as my shared hosting disallow me the use of git command, I had to settle for the next best thing.
TL;DR
In a rush? Here is the full code! Just add it to your
app.py
and you are good to go.
@app.before_first_request
def startup():
if not app.config['DEBUG']:
app.logger.info('Checking static link')
def get_last_modif_time():
"""Get the timestamp of the most recent modified file"""
path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'static')
files = [os.path.join(dp, f) for dp, dn, filenames in os.walk(path) for f in filenames]
timestamp = 0
for f in files:
if os.path.isfile(f):
timestamp = max(timestamp, int(os.stat(f).st_mtime))
return str(timestamp)
last_modif_date = get_last_modif_time()
def check_static_link(timestamp):
"""
Check that we have a link to the static folder
The link should be <timestamp -> .> (ln -s . timestamp)
inside static folder
"""
path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'static')
# Unlink previous links
files = os.listdir(path)
for f in files:
if os.path.islink(os.path.join(path, f)):
if os.readlink(os.path.join(path, f)) == path:
app.logger.info('Remove old link {}'.format(f))
os.unlink(os.path.join(path, f))
# Create the newest link
app.logger.info('Create link to static {}'.format(os.path.join(path, timestamp)))
os.symlink(path, os.path.join(path, timestamp))
check_static_link(last_modif_date)
# Cache Buster (return static url amended with last timestamp)
@app.url_defaults
def static_cache_buster(endpoint, values):
if endpoint in 'static':
values['filename'] = os.path.join(last_modif_date, values['filename'])
Detailed version
For those of view how wants to know more, here are some details.
To avoid browser caching of outdated resources, we need to change the path of the resource. The main idea behind all this code will be to generate a path like
/static/<last_timestamp>/css/style.css
for example. The important part here is the
<last_timestamp>/
fragment. This piece of string is gonna be changed everytime we reload the flask server and a static resource has been updated.
1. Finding the last timestamp
First thing to do is to identify what is the timestamp of the most recently modified resource. To do so, we will use the
os.walk()
function to get all the files in the
static/
folder. Then, with
os.stat()
we will get the last modified timestamp.
def get_last_modif_time():
"""Get the timestamp of the most recent modified file"""
path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'static')
files = [os.path.join(dp, f) for dp, dn, filenames in os.walk(path) for f in filenames]
timestamp = 0
for f in files:
if os.path.isfile(f):
timestamp = max(timestamp, int(os.stat(f).st_mtime))
return str(timestamp)
last_modif_date = get_last_modif_time()
2. Creating the new path
As mentionned before, we need to change path from
/static/css/style.css
to
/static/<last_timestamp>/css/style.css
. In order to do that without moving around all the files, we simply create a symbolic link named
last_timestamp
and pointing to its current place. Hence we get the following structure:
/home/eskimon/foo/bar/twittorama/static$ tree
.
├── 1542798662 -> /home/eskimon/foo/bar/twittorama/static
├── css
│ └── style.css
├── images
└── js
├── gallery.js
└── vuegallery.js
The function to do this need also to remove former existing link to avoid spamming the folder with new links everytime we update the site:
def check_static_link(timestamp):
"""
Check that we have a link to the static folder
The link should be <timestamp -> .> (ln -s . timestamp)
inside static folder
"""
path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'static')
# Unlink previous links
files = os.listdir(path)
for f in files:
if os.path.islink(os.path.join(path, f)):
if os.readlink(os.path.join(path, f)) == path:
app.logger.info('Remove old link {}'.format(f))
os.unlink(os.path.join(path, f))
# Create the newest link
app.logger.info('Create link to static {}'.format(os.path.join(path, timestamp)))
os.symlink(path, os.path.join(path, timestamp))
check_static_link(last_modif_date)
3. Redirecting request to static
Last thing to do is to update the
url_for
behavior for the templating. We will use the decorator
@app.url_defaults
in order to do that. The behavior is simple, everytime a request is made on the endpoint
static
, then the filename must be changed from
foo/bar/static/filename/path.css
to
foo/bar/static/<timestamp>/filename/path.css
. Because we now have a symbolic link, the path can be resolved.
# Cache Buster (return static url amended with last timestamp)
@app.url_defaults
def static_cache_buster(endpoint, values):
if endpoint in 'static':
values['filename'] = os.path.join(last_modif_date, values['filename'])
4. Wrapping it to execute only once
Last but not least, all this behavior must be executing only before the first request. The link should not be destroyed and recreate everytime a user request a page!
The solution is simply to wrap all our code in a function decorated with
@app.before_first_request
.
@app.before_first_request
def startup():
# All the code we made so far!
Conclusion
There you go, a simple yet efficient cache busting solution that doesn’t involve any query parameters!