From 62045c2b5ca3d0ed87c58adb31281f93d1c8002e Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Sun, 30 Dec 2018 18:40:03 +0100 Subject: [PATCH] Support for multiple resource_dirs on the HTTP backend It is now possible to map multiple static resource directories to the Flask HTTP server --- docs/wiki | 2 +- platypush/backend/http/__init__.py | 70 ++++++++++++++++++++++++++---- 2 files changed, 62 insertions(+), 10 deletions(-) diff --git a/docs/wiki b/docs/wiki index 642eb703..6c0e65cc 160000 --- a/docs/wiki +++ b/docs/wiki @@ -1 +1 @@ -Subproject commit 642eb7030cfc599b62fcf304bc94e4d84fecde8c +Subproject commit 6c0e65ccfe020bf6ce2eca43c387e827d4764c12 diff --git a/platypush/backend/http/__init__.py b/platypush/backend/http/__init__.py index 40e3890f..b391af18 100644 --- a/platypush/backend/http/__init__.py +++ b/platypush/backend/http/__init__.py @@ -62,7 +62,7 @@ class HttpBackend(Backend): } def __init__(self, port=8008, websocket_port=8009, disable_websocket=False, - redis_queue='platypush/http', dashboard={}, + redis_queue='platypush/http', dashboard={}, resource_dirs={}, ssl_cert=None, ssl_key=None, ssl_cafile=None, ssl_capath=None, maps={}, **kwargs): """ @@ -90,6 +90,12 @@ class HttpBackend(Backend): :param ssl_capath: Set it to the path of your certificate authority directory if you want to enable HTTPS (default: None) :type ssl_capath: str + :param resource_dirs: Static resources directories that will be + accessible through ``/resources/``. It is expressed as a map + where the key is the relative path under ``/resources`` to expose and + the value is the absolute path to expose. + :type resource_dirs: dict[str, str] + :param dashboard: Set it if you want to use the dashboard service. It will contain the configuration for the widgets to be used (look under ``platypush/backend/http/templates/widgets/`` for the available widgets). Example configuration:: @@ -105,7 +111,7 @@ class HttpBackend(Backend): columns: 3 image-carousel: # Image carousel columns: 6 - images_path: /static/resources/Dropbox/Photos/carousel # Path (relative to ``platypush/backend/http``) containing the carousel pictures + images_path: ~/Dropbox/Photos/carousel # Absolute path (valid as long as it's a subdirectory of one of the available `resource_dirs`) refresh_seconds: 15 rss-news: # RSS feeds widget # Requires backend.http.poll to be enabled with some RSS sources and write them to sqlite db @@ -128,6 +134,8 @@ class HttpBackend(Backend): self.websocket_thread = None self.redis_thread = None self.redis = None + self.resource_dirs = { name: os.path.abspath( + os.path.expanduser(d)) for name, d in resource_dirs.items() } self.active_websockets = set() self.ssl_context = get_ssl_server_context(ssl_cert=ssl_cert, ssl_key=ssl_key, @@ -226,7 +234,6 @@ class HttpBackend(Backend): """ Web server main process """ basedir = os.path.dirname(inspect.getfile(self.__class__)) template_dir = os.path.join(basedir, 'templates') - static_dir = os.path.join(basedir, 'static') app = Flask(__name__, template_folder=template_dir) self.redis_thread = Thread(target=self.redis_poll) self.redis_thread.start() @@ -288,10 +295,36 @@ class HttpBackend(Backend): redis.rpush(self.redis_queue, str(event)) return jsonify({ 'status': 'ok' }) - @app.route('/static/', methods=['GET']) + @app.route('/resources/', methods=['GET']) def static_path(path): """ Static resources """ - return send_from_directory(static_dir, filename) + base_path = os.path.dirname(path).split('/') + while base_path: + if os.sep.join(base_path) in self.resource_dirs: + break + base_path.pop() + + if not base_path: + abort(404) + + base_path = os.sep.join(base_path) + real_base_path = self.resource_dirs[base_path] + real_path = real_base_path + + file_path = [s for s in + re.sub(r'^{}(.*)$'.format(base_path), '\\1', path) \ + .split('/') if s] + + for p in file_path[:-1]: + real_path += os.sep + p + file_path.pop(0) + + file_path = file_path.pop(0) + if not real_path.startswith(real_base_path): + # Directory climbing attempt + abort(404) + + return send_from_directory(real_path, file_path) @app.route('/dashboard', methods=['GET']) def dashboard(): @@ -482,10 +515,29 @@ class HttpUtils(object): @classmethod def search_web_directory(cls, directory, *extensions): - directory = re.sub('^/+', '', directory) - basedir = os.path.dirname(inspect.getfile(cls)) - results = cls.search_directory(os.path.join(basedir, directory), *extensions) - return [item[len(basedir):] for item in results] + directory = os.path.abspath(os.path.expanduser(directory)) + resource_dirs = get_backend('http').resource_dirs + resource_path = None + uri = '' + + for name, resource_path in resource_dirs.items(): + if directory.startswith(resource_path): + subdir = re.sub('^{}(.*)$'.format(resource_path), + '\\1', directory) + uri = '/resources/' + name + break + + if not uri: + raise RuntimeError('Directory {} not found among the available ' + + 'static resources on the webserver'.format( + directory)) + + results = [ + re.sub('^{}(.*)$'.format(resource_path), uri + '\\1', path) + for path in cls.search_directory(directory, *extensions) + ] + + return results @classmethod def to_json(cls, data):