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