|
|
@@ -1,4 +1,12 @@
|
|
|
+import web
|
|
|
+from time import gmtime
|
|
|
+from datetime import datetime, timedelta
|
|
|
+from os import mkdir
|
|
|
from os.path import getmtime, exists
|
|
|
+from cPickle import load, dump
|
|
|
+
|
|
|
+
|
|
|
+ADMINISTRATION_FILE = 'administration.py'
|
|
|
|
|
|
|
|
|
def to_dir(path):
|
|
|
@@ -15,29 +23,41 @@ def to_dir(path):
|
|
|
return path
|
|
|
|
|
|
|
|
|
+def assert_file_exists(path):
|
|
|
+ if not exists(path):
|
|
|
+ raise IOError('File "%s" does not exist.' % path)
|
|
|
+
|
|
|
+
|
|
|
+def seconds_to_datetime(seconds):
|
|
|
+ return datetime(gmtime(seconds)[:7])
|
|
|
+
|
|
|
+
|
|
|
class Cache:
|
|
|
"""
|
|
|
A Cache instance represents a single file in the cache directory, which is
|
|
|
a concatenation of the file list.
|
|
|
"""
|
|
|
|
|
|
- def __init__(self, root='', files=[], cached='cached'):
|
|
|
+ def __init__(self, root='', files=[], cached='cached',
|
|
|
+ expires={'days': 365}):
|
|
|
"""
|
|
|
The constructor takes the following arguments (all are optional):
|
|
|
1. The directory in which the cached files are located (empty by
|
|
|
default).
|
|
|
2. An initial list of number of files to include in the cahce file.
|
|
|
3. The cached files directory (defaults to 'cached').
|
|
|
+ 4. A dictionary containing arguments to the timedelta constructor, that
|
|
|
+ indicates how long the cache object sould live.
|
|
|
"""
|
|
|
self.root = to_dir(root)
|
|
|
self.cached = to_dir(cached)
|
|
|
self.files = map(lambda f: self.root + f, files)
|
|
|
+ self.expires = expires
|
|
|
+
|
|
|
self.assert_files_exist()
|
|
|
|
|
|
def assert_files_exist(self):
|
|
|
- for path in self.files:
|
|
|
- if not exists(path):
|
|
|
- raise IOError('File "%s" does not exist.' % path)
|
|
|
+ map(assert_file_exists, self.files)
|
|
|
|
|
|
def add(self, path, absolute=False):
|
|
|
"""
|
|
|
@@ -48,6 +68,7 @@ class Cache:
|
|
|
if not absolute:
|
|
|
path = self.root + path
|
|
|
|
|
|
+ assert_file_exists(path)
|
|
|
self.files.append(path)
|
|
|
|
|
|
def remove(self, path, absolute=False):
|
|
|
@@ -61,11 +82,87 @@ class Cache:
|
|
|
|
|
|
self.files.remove(path)
|
|
|
|
|
|
- def get_modification_dates(self):
|
|
|
+ def assert_modification_dates_exist(self):
|
|
|
"""
|
|
|
Get the latest modification dates fo each file in the cache object.
|
|
|
"""
|
|
|
+ if hasattr(self, 'modified'):
|
|
|
+ return
|
|
|
+
|
|
|
self.modified = {}
|
|
|
|
|
|
for path in self.files:
|
|
|
self.modified[path] = getmtime(path)
|
|
|
+
|
|
|
+ def assert_cached_folder_exist(self):
|
|
|
+ """
|
|
|
+ Assert that the folder for cached files is created.
|
|
|
+ """
|
|
|
+ if not exists(self.cached):
|
|
|
+ mkdir(self.cached)
|
|
|
+
|
|
|
+ def save_administration(self):
|
|
|
+ """
|
|
|
+ Generate a Python file containing the modification dates of the cached
|
|
|
+ file list.
|
|
|
+ """
|
|
|
+ self.assert_modification_dates_exist()
|
|
|
+ self.assert_cached_folder_exist()
|
|
|
+
|
|
|
+ f = open(self.cached + ADMINISTRATION_FILE, 'w')
|
|
|
+ dump(self.modified, f)
|
|
|
+ f.close()
|
|
|
+
|
|
|
+ def last_modified(self):
|
|
|
+ self.assert_modification_dates_exist()
|
|
|
+
|
|
|
+ return self.modified[max(self.modified)]
|
|
|
+
|
|
|
+ def etag(self):
|
|
|
+ """
|
|
|
+ Generate an Etag for the cache object, using the names of the files
|
|
|
+ included and the latest modification date.
|
|
|
+ """
|
|
|
+ return self.filename() + str(self.last_modified())
|
|
|
+
|
|
|
+ def filename(self):
|
|
|
+ return ','.join(self.files)
|
|
|
+
|
|
|
+ def concatenate(self):
|
|
|
+ contents = ''
|
|
|
+
|
|
|
+ for path in self.files:
|
|
|
+ f = open(path, 'r')
|
|
|
+ contents += f.read()
|
|
|
+ f.close()
|
|
|
+
|
|
|
+ return contents
|
|
|
+
|
|
|
+ def output(self):
|
|
|
+ """"""
|
|
|
+ # Update cached file
|
|
|
+ last_modified = self.last_modified()
|
|
|
+ path = self.filename()
|
|
|
+ server_modified = not exists(path) or getmtime(path) < last_modified
|
|
|
+
|
|
|
+ if server_modified:
|
|
|
+ content = self.concatenate()
|
|
|
+
|
|
|
+ f = open(path, 'w')
|
|
|
+ f.write(content)
|
|
|
+ f.close()
|
|
|
+
|
|
|
+ #try:
|
|
|
+ web.http.modified(seconds_to_datetime(last_modified), self.etag())
|
|
|
+ web.http.expires(timedelta(**self.expires))
|
|
|
+
|
|
|
+ if not server_modified:
|
|
|
+ # Concatenated content has not been loaded yet, read the cached
|
|
|
+ # file
|
|
|
+ f = open(path, 'r')
|
|
|
+ content = f.read()
|
|
|
+ f.close()
|
|
|
+
|
|
|
+ return content
|
|
|
+ #except web.NotModified as e:
|
|
|
+ # web.header('Status', e.message)
|