--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/views/staticcontrollers.py Mon Feb 27 17:02:31 2012 +0100
@@ -0,0 +1,230 @@
+# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
+"""Set of static resources controllers for :
+
+- /data/...
+- /static/...
+- /fckeditor/...
+
+"""
+
+import os
+import os.path as osp
+import hashlib
+import mimetypes
+from time import mktime
+from datetime import datetime, timedelta
+from logging import getLogger
+
+from cubicweb.web import NotFound
+from cubicweb.web.http_headers import generateDateTime
+from cubicweb.web.controller import Controller
+from cubicweb.web.views.urlrewrite import URLRewriter
+
+
+
+class StaticFileController(Controller):
+ """an abtract class to serve static file
+
+ Make sure to add your subclass to the STATIC_CONTROLLERS list"""
+ __abstract__ = True
+ directory_listing_allowed = False
+
+ def max_age(self, path):
+ """max cache TTL"""
+ return 60*60*24*7
+
+ def static_file(self, path):
+ """Return full content of a static file.
+
+ XXX iterable content would be better
+ """
+ debugmode = self._cw.vreg.config.debugmode
+ if osp.isdir(path):
+ if self.directory_listing_allowed:
+ return u''
+ raise NotFound(path)
+ if not osp.isfile(path):
+ raise NotFound()
+ if not debugmode:
+ # XXX: Don't provide additional resource information to error responses
+ #
+ # the HTTP RFC recommands not going further than 1 year ahead
+ expires = datetime.now() + timedelta(days=6*30)
+ self._cw.set_header('Expires', generateDateTime(mktime(expires.timetuple())))
+ # XXX elif uri.startswith('/https/'): uri = uri[6:]
+ mimetype, encoding = mimetypes.guess_type(path)
+ self._cw.set_content_type(mimetype, osp.basename(path), encoding)
+ return file(path).read()
+
+ @property
+ def relpath(self):
+ """path of a requested file relative to the controller"""
+ path = self._cw.form.get('static_relative_path')
+ if path is None:
+ path = self._cw.relative_path(includeparams=True)
+ return path
+
+
+class ConcatFilesHandler(object):
+ """Emulating the behavior of modconcat
+
+ this serve multiple file as a single one.
+ """
+
+ def __init__(self, config):
+ self._resources = {}
+ self.config = config
+ self.logger = getLogger('cubicweb.web')
+
+ def _resource(self, path):
+ """get the resouce"""
+ try:
+ return self._resources[path]
+ except KeyError:
+ self._resources[path] = self.config.locate_resource(path)
+ return self._resources[path]
+
+ def _up_to_date(self, filepath, paths):
+ """
+ The concat-file is considered up-to-date if it exists.
+ In debug mode, an additional check is performed to make sure that
+ concat-file is more recent than all concatenated files
+ """
+ if not osp.isfile(filepath):
+ return False
+ if self.config.debugmode:
+ concat_lastmod = os.stat(filepath).st_mtime
+ for path in paths:
+ dirpath, rid = self._resource(path)
+ if rid is None:
+ raise NotFound(path)
+ path = osp.join(dirpath, rid)
+ if os.stat(path).st_mtime > concat_lastmod:
+ return False
+ return True
+
+ def build_filepath(self, paths):
+ """return the filepath that will be used to cache concatenation of `paths`
+ """
+ _, ext = osp.splitext(paths[0])
+ fname = 'cache_concat_' + hashlib.md5(';'.join(paths)).hexdigest() + ext
+ return osp.join(self.config.appdatahome, 'uicache', fname)
+
+ def concat_cached_filepath(self, paths):
+ filepath = self.build_filepath(paths)
+ if not self._up_to_date(filepath, paths):
+ with open(filepath, 'wb') as f:
+ for path in paths:
+ dirpath, rid = self._resource(path)
+ if rid is None:
+ # In production mode log an error, do not return a 404
+ # XXX the erroneous content is cached anyway
+ self.logger.error('concatenated data url error: %r file '
+ 'does not exist', path)
+ if self.config.debugmode:
+ raise NotFound(path)
+ else:
+ for line in open(osp.join(dirpath, rid)):
+ f.write(line)
+ f.write('\n')
+ return filepath
+
+
+class DataController(StaticFileController):
+ """Controller in charge of serving static file in /data/
+
+ Handle modeconcat like url.
+ """
+
+ __regid__ = 'data'
+
+ def __init__(self, *args, **kwargs):
+ super(DataController, self).__init__(*args, **kwargs)
+ config = self._cw.vreg.config
+ md5_version = config.instance_md5_version()
+ self.base_datapath = config.data_relpath()
+ self.data_modconcat_basepath = '%s??' % self.base_datapath
+ self.concat_files_registry = ConcatFilesHandler(config)
+
+ def publish(self, rset=None):
+ config = self._cw.vreg.config
+ # includeparams=True for modconcat-like urls
+ relpath = self.relpath
+ if relpath.startswith(self.data_modconcat_basepath):
+ paths = relpath[len(self.data_modconcat_basepath):].split(',')
+ filepath = self.concat_files_registry.concat_cached_filepath(paths)
+ return self.static_file(filepath)
+ else:
+ relpath = relpath[len(self.base_datapath):] # skip leading '/data/'
+ dirpath, rid = config.locate_resource(relpath)
+ if dirpath is None:
+ raise NotFound()
+ return self.static_file(osp.join(dirpath, rid))
+
+
+class FCKEditorController(StaticFileController):
+ """Controller in charge of serving FCKEditor related file
+
+ The motivational for a dedicated controller have been lost.
+ """
+
+ __regid__ = 'fckeditor'
+
+ def publish(self, rset=None):
+ config = self._cw.vreg.config
+ if self._cw.https:
+ uiprops = config.https_uiprops
+ else:
+ uiprops = config.uiprops
+ relpath = self.relpath
+ return self.static_file(osp.join(uiprops['FCKEDITOR_PATH'], relpath))
+
+
+class StaticDirectoryController(StaticFileController):
+ """Controller in charge of serving static file in /static/
+ """
+ __regid__ = 'static'
+
+ def publish(self, rset=None):
+ staticdir = self._cw.vreg.config.static_directory
+ relpath = self.relpath
+ return self.static_file(osp.join(staticdir, relpath))
+
+STATIC_CONTROLLERS = [DataController, FCKEditorController,
+ StaticDirectoryController]
+
+class StaticControlerRewriter(URLRewriter):
+ """a quick and dirty rewritter in charge of server static file.
+
+ This is a work around the flatness of url handling in cubicweb."""
+
+ __regid__ = 'static'
+
+ priority = 10
+
+ def rewrite(self, req, uri):
+ for ctrl in STATIC_CONTROLLERS:
+ if uri.startswith('/%s/' % ctrl.__regid__):
+ break
+ else:
+ self.debug("not a static file uri: %s", uri)
+ raise KeyError(uri)
+ relpath = self._cw.relative_path(includeparams=False)
+ self._cw.form['static_relative_path'] = self._cw.relative_path(includeparams=True)
+ return ctrl.__regid__, None