Ebook Arduino !

couverture ebook

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!


Licence CC BY