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 | ||||
| 
 | ||||
| from voussoirkit import bytestring | ||||
| from voussoirkit import pathclass | ||||
| 
 | ||||
| from . import caching | ||||
| from . import jsonify | ||||
| from . import sessions | ||||
| 
 | ||||
|  | @ -34,7 +36,11 @@ site.debug = True | |||
| P = etiquette.photodb.PhotoDB() | ||||
| 
 | ||||
| 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_wrapped(thingid, response_type='html'): | ||||
|  | @ -100,6 +106,13 @@ def send_file(filepath, override_mimetype=None): | |||
|     if not filepath.is_file: | ||||
|         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 = {} | ||||
|     if override_mimetype is not None: | ||||
|         mimetype = override_mimetype | ||||
|  | @ -147,6 +160,8 @@ def send_file(filepath, override_mimetype=None): | |||
| 
 | ||||
|     outgoing_headers['Accept-Ranges'] = 'bytes' | ||||
|     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': | ||||
|         outgoing_data = bytes() | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue