diff -r 058bb3dc685f -r 0b59724cb3f2 web/views/staticcontrollers.py --- a/web/views/staticcontrollers.py Mon Jan 04 18:40:30 2016 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,272 +0,0 @@ -# copyright 2003-2013 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 . -"""Set of static resources controllers for : - -- /data/... -- /static/... -- /fckeditor/... -""" - -import os -import os.path as osp -import hashlib -import mimetypes -import threading -import tempfile -from time import mktime -from datetime import datetime, timedelta -from logging import getLogger - -from cubicweb import Forbidden -from cubicweb.web import NotFound, Redirect -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 Forbidden(path) - if not osp.isfile(path): - raise NotFound() - if not debugmode: - # XXX: Don't provide additional resource information to error responses - # - # the HTTP RFC recommends not going further than 1 year ahead - expires = datetime.now() + timedelta(seconds=self.max_age(path)) - self._cw.set_header('Expires', generateDateTime(mktime(expires.timetuple()))) - self._cw.set_header('Cache-Control', 'max-age=%s' % self.max_age(path)) - - # XXX system call to os.stats could be cached once and for all in - # production mode (where static files are not expected to change) - # - # Note that: we do a osp.isdir + osp.isfile before and a potential - # os.read after. Improving this specific call will not help - # - # Real production environment should use dedicated static file serving. - self._cw.set_header('last-modified', generateDateTime(os.stat(path).st_mtime)) - if self._cw.is_client_cache_valid(): - return '' - # XXX elif uri.startswith('/https/'): uri = uri[6:] - mimetype, encoding = mimetypes.guess_type(path) - if mimetype is None: - mimetype = 'application/octet-stream' - self._cw.set_content_type(mimetype, osp.basename(path), encoding) - with open(path, 'rb') as resource: - return resource.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') - self.lock = threading.Lock() - - 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)).encode('ascii')).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 self.lock: - if self._up_to_date(filepath, paths): - # first check could have raced with some other thread - # updating the file - return filepath - fd, tmpfile = tempfile.mkstemp(dir=os.path.dirname(filepath)) - try: - f = os.fdopen(fd, 'wb') - 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: - with open(osp.join(dirpath, rid), 'rb') as source: - for line in source: - f.write(line) - f.write(b'\n') - f.close() - except: - os.remove(tmpfile) - raise - else: - os.rename(tmpfile, filepath) - return filepath - - -class DataController(StaticFileController): - """Controller in charge of serving static files in /data/ - - Handles mod_concat-like URLs. - """ - - __regid__ = 'data' - - def __init__(self, *args, **kwargs): - super(DataController, self).__init__(*args, **kwargs) - config = self._cw.vreg.config - 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) - else: - if not relpath.startswith(self.base_datapath): - # /data/foo, redirect to /data/{hash}/foo - prefix = 'data/' - relpath = relpath[len(prefix):] - raise Redirect(self._cw.data_url(relpath), 302) - # skip leading '/data/{hash}/' and url params - prefix = self.base_datapath - relpath = relpath[len(prefix):] - relpath = relpath.split('?', 1)[0] - dirpath, rid = config.locate_resource(relpath) - if dirpath is None: - raise NotFound() - filepath = osp.join(dirpath, rid) - return self.static_file(filepath) - - -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 - if relpath.startswith('fckeditor/'): - relpath = relpath[len('fckeditor/'):] - relpath = relpath.split('?', 1)[0] - 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[len(self.__regid__) + 1:] - 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