2021-01-25 22:59:40 +00:00
|
|
|
import hashlib
|
|
|
|
|
2018-07-23 03:18:06 +00:00
|
|
|
from voussoirkit import cacheclass
|
2021-01-25 22:59:40 +00:00
|
|
|
from voussoirkit import spinal
|
2018-03-19 04:42:31 +00:00
|
|
|
|
2018-11-05 03:27:20 +00:00
|
|
|
import etiquette
|
|
|
|
|
2021-01-05 20:59:18 +00:00
|
|
|
class FileEtagManager:
|
2018-07-23 03:18:06 +00:00
|
|
|
'''
|
2021-01-05 20:59:18 +00:00
|
|
|
The FileEtagManager serves ETag and Cache-Control headers for disk files to
|
|
|
|
enable client-side caching.
|
2018-07-23 03:18:06 +00:00
|
|
|
|
|
|
|
We consider the following cases:
|
|
|
|
|
|
|
|
Client does not have the file (or has disabled their cache):
|
|
|
|
Server sends file, provides ETag, 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 old file.
|
|
|
|
|
|
|
|
Client has the file, and it is within the max-age:
|
|
|
|
Client does not make a request at all.
|
|
|
|
|
|
|
|
We use the file's MD5 hash as the ETag, and will only recalculate it if the
|
|
|
|
file's mtime has changed since the last request.
|
2021-01-05 21:03:41 +00:00
|
|
|
|
|
|
|
Note, this class does not store any of the file's content data.
|
2018-07-23 03:18:06 +00:00
|
|
|
'''
|
|
|
|
def __init__(self, maxlen, max_age, max_filesize):
|
2021-01-05 20:59:18 +00:00
|
|
|
'''
|
|
|
|
max_len:
|
|
|
|
The number of files to track in this cache.
|
|
|
|
max_age:
|
|
|
|
Integer number of seconds that will be send to the client as
|
|
|
|
Cache-Control:max-age=x
|
|
|
|
max_filesize:
|
|
|
|
Integer number of bytes. Because we use the file's MD5 as its etag,
|
|
|
|
you may wish to prevent the reading of large files. Files larger
|
|
|
|
than this size will not be etagged.
|
|
|
|
'''
|
2018-03-19 04:42:31 +00:00
|
|
|
self.cache = cacheclass.Cache(maxlen=maxlen)
|
|
|
|
self.max_age = int(max_age)
|
2018-07-23 03:18:06 +00:00
|
|
|
self.max_filesize = max(int(max_filesize), 0) or None
|
2018-03-19 04:42:31 +00:00
|
|
|
|
2021-01-05 20:59:18 +00:00
|
|
|
def get_304_headers(self, request, filepath):
|
|
|
|
'''
|
|
|
|
Given a request object and a filepath that we would like to send back
|
|
|
|
as the response, check if the client's provided etag matches the
|
|
|
|
server's cached etag, and return the headers to be used in a 304
|
|
|
|
response (etag, cache-control).
|
2018-07-23 03:18:06 +00:00
|
|
|
|
2021-01-05 20:59:18 +00:00
|
|
|
If the client did not provide an etag, or their etag does not match the
|
|
|
|
current file, or the file cannot be cached, return None.
|
|
|
|
'''
|
2018-07-23 03:18:06 +00:00
|
|
|
client_etag = request.headers.get('If-None-Match', None)
|
|
|
|
if client_etag is None:
|
2021-01-05 20:59:18 +00:00
|
|
|
return None
|
2018-07-23 03:18:06 +00:00
|
|
|
|
2021-01-05 20:59:18 +00:00
|
|
|
server_value = self.get_file(filepath)
|
2018-07-23 03:18:06 +00:00
|
|
|
if server_value is None:
|
2021-01-05 20:59:18 +00:00
|
|
|
return None
|
2018-07-23 03:18:06 +00:00
|
|
|
|
|
|
|
server_etag = server_value.get_etag()
|
|
|
|
if client_etag != server_etag:
|
2021-01-05 20:59:18 +00:00
|
|
|
return None
|
2018-07-23 03:18:06 +00:00
|
|
|
|
|
|
|
return server_value.get_headers()
|
|
|
|
|
2021-01-05 20:59:18 +00:00
|
|
|
def get_file(self, filepath):
|
|
|
|
'''
|
|
|
|
Return a FileEtag object if the filepath can be cached, or None if it
|
|
|
|
cannot (size greater than max_filesize).
|
|
|
|
'''
|
|
|
|
try:
|
|
|
|
return self.cache[filepath]
|
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
if (self.max_filesize is not None) and (filepath.size > self.max_filesize):
|
|
|
|
return None
|
|
|
|
|
|
|
|
cache_file = FileEtag(filepath, max_age=self.max_age)
|
|
|
|
self.cache[filepath] = cache_file
|
|
|
|
return cache_file
|
|
|
|
|
|
|
|
class FileEtag:
|
|
|
|
'''
|
|
|
|
This class represents an individual disk file that is being managed by the
|
|
|
|
FileEtagManager.
|
|
|
|
'''
|
2018-03-19 04:42:31 +00:00
|
|
|
def __init__(self, filepath, max_age):
|
|
|
|
self.filepath = filepath
|
2018-07-23 03:18:06 +00:00
|
|
|
self.max_age = int(max_age)
|
2018-03-19 04:42:31 +00:00
|
|
|
self._stored_hash_time = None
|
|
|
|
self._stored_hash_value = None
|
|
|
|
|
|
|
|
def get_etag(self):
|
2018-07-23 03:18:06 +00:00
|
|
|
mtime = self.filepath.stat.st_mtime
|
|
|
|
do_refresh = (self._stored_hash_value is None) or (mtime > self._stored_hash_time)
|
|
|
|
|
|
|
|
if do_refresh:
|
|
|
|
self._stored_hash_time = mtime
|
2021-01-25 22:59:40 +00:00
|
|
|
self._stored_hash_value = spinal.hash_file(self.filepath, hash_class=hashlib.md5)
|
2021-01-05 20:59:18 +00:00
|
|
|
|
2018-03-19 04:42:31 +00:00
|
|
|
return self._stored_hash_value
|
|
|
|
|
|
|
|
def get_headers(self):
|
|
|
|
headers = {
|
|
|
|
'ETag': self.get_etag(),
|
2018-07-19 01:36:36 +00:00
|
|
|
'Cache-Control': f'max-age={self.max_age}',
|
2018-03-19 04:42:31 +00:00
|
|
|
}
|
|
|
|
return headers
|