Add caching.py to enable browser caching of files.
This commit is contained in:
parent
ea7401b4f2
commit
b5274fefb9
2 changed files with 88 additions and 1 deletions
72
frontends/etiquette_flask/etiquette_flask/caching.py
Normal file
72
frontends/etiquette_flask/etiquette_flask/caching.py
Normal 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
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in a new issue