Add caching.py to enable browser caching of files.

This commit is contained in:
voussoir 2018-03-18 21:42:31 -07:00
parent ea7401b4f2
commit b5274fefb9
2 changed files with 88 additions and 1 deletions

View file

@ -0,0 +1,72 @@
'''
This file provides the FileCacheManager to serve ETag and Cache-Control headers
for files on disk.
We consider the following cases:
Client does not have the file (or has disabled their cache, effectively same):
Server sends file, provides ETag, and tells client to save it for max-age.
Client has the file, but it has been a long time, beyond the max-age:
Client provides the old ETag. If it's still valid, Server responds with
304 Not Modified and no data. Client keeps the file.
Client has the file, and it is within the max-age:
Client does not make a request at all.
This FileCacheManager uses the file's MD5 hash as the ETag, and will only
recalculate it if the file's mtime has changed since the last request.
'''
import time
import etiquette
from voussoirkit import cacheclass
from voussoirkit import pathclass
class FileCacheManager:
def __init__(self, maxlen, max_filesize, max_age):
self.cache = cacheclass.Cache(maxlen=maxlen)
self.max_filesize = int(max_filesize)
self.max_age = int(max_age)
def get(self, filepath):
if (self.max_filesize is not None) and (filepath.size > self.max_filesize):
#print('I\'m not going to cache that!')
return None
try:
return self.cache[filepath]
except KeyError:
pass
cache_file = CacheFile(filepath, max_age=self.max_age)
self.cache[filepath] = cache_file
return cache_file
class CacheFile:
def __init__(self, filepath, max_age):
self.filepath = filepath
self.max_age = max_age
self._stored_hash_time = None
self._stored_hash_value = None
def get_etag(self):
if self._stored_hash_value is None:
refresh = True
elif self.filepath.stat.st_mtime > self._stored_hash_time:
refresh = True
else:
refresh = False
if refresh:
self._stored_hash_time = self.filepath.stat.st_mtime
self._stored_hash_value = etiquette.helpers.hash_file_md5(self.filepath)
return self._stored_hash_value
def get_headers(self):
headers = {
'ETag': self.get_etag(),
'Cache-Control': 'max-age=%d' % self.max_age,
}
return headers

View file

@ -5,8 +5,10 @@ import traceback
import etiquette import etiquette
from voussoirkit import bytestring
from voussoirkit import pathclass from voussoirkit import pathclass
from . import caching
from . import jsonify from . import jsonify
from . import sessions from . import sessions
@ -34,7 +36,11 @@ site.debug = True
P = etiquette.photodb.PhotoDB() P = etiquette.photodb.PhotoDB()
session_manager = sessions.SessionManager(maxlen=10000) session_manager = sessions.SessionManager(maxlen=10000)
file_cache_manager = caching.FileCacheManager(
maxlen=10000,
max_filesize=5 * bytestring.MIBIBYTE,
max_age=180,
)
def P_wrapper(function): def P_wrapper(function):
def P_wrapped(thingid, response_type='html'): def P_wrapped(thingid, response_type='html'):
@ -100,6 +106,13 @@ def send_file(filepath, override_mimetype=None):
if not filepath.is_file: if not filepath.is_file:
flask.abort(404) flask.abort(404)
cache_file = file_cache_manager.get(filepath)
if cache_file is not None:
client_etag = request.headers.get('If-None-Match', None)
if client_etag and client_etag == cache_file.get_etag():
response = flask.Response(status=304, headers=cache_file.get_headers())
return response
outgoing_headers = {} outgoing_headers = {}
if override_mimetype is not None: if override_mimetype is not None:
mimetype = override_mimetype mimetype = override_mimetype
@ -147,6 +160,8 @@ def send_file(filepath, override_mimetype=None):
outgoing_headers['Accept-Ranges'] = 'bytes' outgoing_headers['Accept-Ranges'] = 'bytes'
outgoing_headers['Content-Length'] = (range_max - range_min) + 1 outgoing_headers['Content-Length'] = (range_max - range_min) + 1
if cache_file is not None:
outgoing_headers.update(cache_file.get_headers())
if request.method == 'HEAD': if request.method == 'HEAD':
outgoing_data = bytes() outgoing_data = bytes()