Mercurial > libervia-web
comparison libervia/server/pages.py @ 1253:6d49fae517ba
pages: browser metadata + root `_browser`:
- the `_browser` directory can now be put in root of a site `pages` directory, it will then
include modules for the whole website
- in `_browser` directories (notably the root one), a `browser_meta.json` file can be put to
specify settings for a browser engine
- pathlib.Path is now used LiberviaRootResource.site_path
- introduced some type hints
- task_brython copy modules in root `_browser` to build_path root.
- minimal python version is now 3.7 due to type hints
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 29 Apr 2020 17:34:53 +0200 |
parents | aaf28d45ae67 |
children | b1fb57e9176d |
comparison
equal
deleted
inserted
replaced
1252:80a92eb82b7f | 1253:6d49fae517ba |
---|---|
1 #!/usr/bin/env python3 | 1 #!/usr/bin/env python3 |
2 | |
3 | 2 |
4 # Libervia: a Salut à Toi frontend | 3 # Libervia: a Salut à Toi frontend |
5 # Copyright (C) 2011-2020 Jérôme Poisson <goffi@goffi.org> | 4 # Copyright (C) 2011-2020 Jérôme Poisson <goffi@goffi.org> |
6 | 5 |
7 # This program is free software: you can redistribute it and/or modify | 6 # This program is free software: you can redistribute it and/or modify |
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
15 # GNU Affero General Public License for more details. | 14 # GNU Affero General Public License for more details. |
16 | 15 |
17 # You should have received a copy of the GNU Affero General Public License | 16 # You should have received a copy of the GNU Affero General Public License |
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. | 17 # along with this program. If not, see <http://www.gnu.org/licenses/>. |
18 | |
19 from __future__ import annotations | |
19 | 20 |
20 import uuid | 21 import uuid |
21 import os.path | 22 import os.path |
22 import urllib.request, urllib.parse, urllib.error | 23 import urllib.request, urllib.parse, urllib.error |
23 import time | 24 import time |
24 import hashlib | 25 import hashlib |
25 import copy | 26 import copy |
27 import json | |
28 from pathlib import Path | |
26 from functools import reduce | 29 from functools import reduce |
27 from pathlib import Path | 30 from typing import Optional, List |
28 | 31 |
29 from twisted.web import server | 32 from twisted.web import server |
30 from twisted.web import resource as web_resource | 33 from twisted.web import resource as web_resource |
31 from twisted.web import util as web_util | 34 from twisted.web import util as web_util |
32 from twisted.internet import defer | 35 from twisted.internet import defer |
34 from twisted.python import failure | 37 from twisted.python import failure |
35 | 38 |
36 from sat.core.i18n import _ | 39 from sat.core.i18n import _ |
37 from sat.core import exceptions | 40 from sat.core import exceptions |
38 from sat.tools.common import date_utils | 41 from sat.tools.common import date_utils |
42 from sat.tools.common import utils | |
39 from sat.core.log import getLogger | 43 from sat.core.log import getLogger |
40 from sat_frontends.bridge.bridge_frontend import BridgeException | 44 from sat_frontends.bridge.bridge_frontend import BridgeException |
41 | 45 |
42 from libervia.server.constants import Const as C | 46 from libervia.server.constants import Const as C |
43 from libervia.server import session_iface | 47 from libervia.server import session_iface |
121 LiberviaPages are the main resources of Libervia, using easy to set python files | 125 LiberviaPages are the main resources of Libervia, using easy to set python files |
122 The non mandatory arguments are the variables found in page_meta.py | 126 The non mandatory arguments are the variables found in page_meta.py |
123 @param host(Libervia): the running instance of Libervia | 127 @param host(Libervia): the running instance of Libervia |
124 @param vhost_root(web_resource.Resource): root resource of the virtual host which | 128 @param vhost_root(web_resource.Resource): root resource of the virtual host which |
125 handle this page. | 129 handle this page. |
126 @param root_dir(unicode): aboslute file path of the page | 130 @param root_dir(Path): aboslute file path of the page |
127 @param url(unicode): relative URL to the page | 131 @param url(unicode): relative URL to the page |
128 this URL may not be valid, as pages may require path arguments | 132 this URL may not be valid, as pages may require path arguments |
129 @param name(unicode, None): if not None, a unique name to identify the page | 133 @param name(unicode, None): if not None, a unique name to identify the page |
130 can then be used for e.g. redirection | 134 can then be used for e.g. redirection |
131 "/" is not allowed in names (as it can be used to construct URL paths) | 135 "/" is not allowed in names (as it can be used to construct URL paths) |
258 | 262 |
259 @staticmethod | 263 @staticmethod |
260 def createPage(host, meta_path, vhost_root, url_elts, replace_on_conflict=False): | 264 def createPage(host, meta_path, vhost_root, url_elts, replace_on_conflict=False): |
261 """Create a LiberviaPage instance | 265 """Create a LiberviaPage instance |
262 | 266 |
263 @param meta_path(unicode): path to the page_meta.py file | 267 @param meta_path(Path): path to the page_meta.py file |
264 @param vhost_root(resource.Resource): root resource of the virtual host | 268 @param vhost_root(resource.Resource): root resource of the virtual host |
265 @param url_elts(list[unicode]): list of path element from root site to this page | 269 @param url_elts(list[unicode]): list of path element from root site to this page |
266 @param replace_on_conflict(bool): same as for [LiberviaPage] | 270 @param replace_on_conflict(bool): same as for [LiberviaPage] |
267 @return (tuple[dict, LiberviaPage]): tuple with: | 271 @return (tuple[dict, LiberviaPage]): tuple with: |
268 - page_data: dict containing data of the page | 272 - page_data: dict containing data of the page |
269 - libervia_page: created resource | 273 - libervia_page: created resource |
270 """ | 274 """ |
271 dir_path = os.path.dirname(meta_path) | 275 dir_path = meta_path.parent |
272 page_data = {"__name__": ".".join(["page"] + url_elts)} | 276 page_data = {"__name__": ".".join(["page"] + url_elts)} |
273 # we don't want to force the presence of __init__.py | 277 # we don't want to force the presence of __init__.py |
274 # so we use execfile instead of import. | 278 # so we use execfile instead of import. |
275 # TODO: when moved to Python 3, __init__.py is not mandatory anymore | 279 # TODO: when moved to Python 3, __init__.py is not mandatory anymore |
276 # so we can switch to import | 280 # so we can switch to import |
293 on_signal=page_data.get("on_signal"), | 297 on_signal=page_data.get("on_signal"), |
294 url_cache=page_data.get("url_cache", False), | 298 url_cache=page_data.get("url_cache", False), |
295 replace_on_conflict=replace_on_conflict | 299 replace_on_conflict=replace_on_conflict |
296 ) | 300 ) |
297 | 301 |
302 @staticmethod | |
303 def createBrowserData( | |
304 vhost_root, | |
305 resource: Optional(LiberviaPage), | |
306 browser_path: Path, | |
307 path_elts: Optional(List[str]), | |
308 engine: str = "brython" | |
309 ) -> None: | |
310 """create and store data for browser dynamic code""" | |
311 dyn_data = { | |
312 "path": browser_path, | |
313 "url_hash": ( | |
314 hashlib.sha256('/'.join(path_elts).encode()).hexdigest() | |
315 if path_elts is not None else None | |
316 ), | |
317 } | |
318 browser_meta_path = browser_path / C.PAGES_BROWSER_META_FILE | |
319 if browser_meta_path.is_file(): | |
320 with browser_meta_path.open() as f: | |
321 browser_meta = json.load(f) | |
322 utils.recursive_update(vhost_root.browser_modules, browser_meta) | |
323 if resource is not None: | |
324 utils.recursive_update(resource.dyn_data, browser_meta) | |
325 | |
326 init_path = browser_path / '__init__.py' | |
327 if init_path.is_file(): | |
328 vhost_root.browser_modules.setdefault( | |
329 engine, []).append(dyn_data) | |
330 if resource is not None: | |
331 resource.dyn_data[engine] = dyn_data | |
332 elif path_elts is None: | |
333 try: | |
334 next(browser_path.glob('*.py')) | |
335 except StopIteration: | |
336 # no python file, nothing for Brython | |
337 pass | |
338 else: | |
339 vhost_root.browser_modules.setdefault( | |
340 engine, []).append(dyn_data) | |
341 | |
342 | |
298 @classmethod | 343 @classmethod |
299 def importPages(cls, host, vhost_root, root_path=None, _parent=None, _path=None, | 344 def importPages(cls, host, vhost_root, root_path=None, _parent=None, _path=None, |
300 _extra_pages=False): | 345 _extra_pages=False): |
301 """Recursively import Libervia pages | 346 """Recursively import Libervia pages |
302 | 347 |
303 @param host(Libervia): Libervia instance | 348 @param host(Libervia): Libervia instance |
304 @param vhost_root(LiberviaRootResource): root of this VirtualHost | 349 @param vhost_root(LiberviaRootResource): root of this VirtualHost |
305 @param root_path(unicode, None): use this root path instead of vhost_root's one | 350 @param root_path(Path, None): use this root path instead of vhost_root's one |
306 Used to add default site pages to external sites | 351 Used to add default site pages to external sites |
307 @param _parent(Resource, None): _parent page. Do not set yourself, this is for | 352 @param _parent(Resource, None): _parent page. Do not set yourself, this is for |
308 internal use only | 353 internal use only |
309 @param _path(list(unicode), None): current path. Do not set yourself, this is for | 354 @param _path(list(unicode), None): current path. Do not set yourself, this is for |
310 internal use only | 355 internal use only |
313 """ | 358 """ |
314 if _path is None: | 359 if _path is None: |
315 _path = [] | 360 _path = [] |
316 if _parent is None: | 361 if _parent is None: |
317 if root_path is None: | 362 if root_path is None: |
318 root_dir = os.path.join(vhost_root.site_path, C.PAGES_DIR) | 363 root_dir = vhost_root.site_path / C.PAGES_DIR |
319 else: | 364 else: |
320 root_dir = os.path.join(root_path, C.PAGES_DIR) | 365 root_dir = root_path / C.PAGES_DIR |
321 _extra_pages = True | 366 _extra_pages = True |
322 _parent = vhost_root | 367 _parent = vhost_root |
368 root_browser_path = root_dir / C.PAGES_BROWSER_DIR | |
369 if root_browser_path.is_dir(): | |
370 cls.createBrowserData(vhost_root, None, root_browser_path, None) | |
323 else: | 371 else: |
324 root_dir = _parent.root_dir | 372 root_dir = _parent.root_dir |
373 | |
325 for d in os.listdir(root_dir): | 374 for d in os.listdir(root_dir): |
326 dir_path = os.path.join(root_dir, d) | 375 dir_path = root_dir / d |
327 if not os.path.isdir(dir_path): | 376 if not dir_path.is_dir(): |
328 continue | 377 continue |
329 if _extra_pages and d in _parent.children: | 378 if _extra_pages and d in _parent.children: |
330 log.debug(_("[{host_name}] {path} is already present, ignoring it") | 379 log.debug(_("[{host_name}] {path} is already present, ignoring it") |
331 .format(host_name=vhost_root.host_name, path='/'.join(_path+[d]))) | 380 .format(host_name=vhost_root.host_name, path='/'.join(_path+[d]))) |
332 continue | 381 continue |
333 meta_path = os.path.join(dir_path, C.PAGES_META_FILE) | 382 meta_path = dir_path / C.PAGES_META_FILE |
334 if os.path.isfile(meta_path): | 383 if meta_path.is_file(): |
335 new_path = _path + [d] | 384 new_path = _path + [d] |
336 try: | 385 try: |
337 page_data, resource = cls.createPage( | 386 page_data, resource = cls.createPage( |
338 host, meta_path, vhost_root, new_path) | 387 host, meta_path, vhost_root, new_path) |
339 except exceptions.ConflictError as e: | 388 except exceptions.ConflictError as e: |
340 if _extra_pages: | 389 if _extra_pages: |
341 # extra pages are discarded if there is already an existing page | 390 # extra pages are discarded if there is already an existing page |
342 continue | 391 continue |
343 else: | 392 else: |
344 raise e | 393 raise e |
345 _parent.putChild(d.encode('utf-8'), resource) | 394 _parent.putChild(str(d).encode(), resource) |
346 log_msg = ("[{host_name}] Added /{path} page".format( | 395 log_msg = ("[{host_name}] Added /{path} page".format( |
347 host_name=vhost_root.host_name, | 396 host_name=vhost_root.host_name, |
348 path="[…]/".join(new_path))) | 397 path="[…]/".join(new_path))) |
349 if _extra_pages: | 398 if _extra_pages: |
350 log.debug(log_msg) | 399 log.debug(log_msg) |
372 | 421 |
373 LiberviaPage.importPages( | 422 LiberviaPage.importPages( |
374 host, vhost_root, _parent=resource, _path=new_path, | 423 host, vhost_root, _parent=resource, _path=new_path, |
375 _extra_pages=_extra_pages) | 424 _extra_pages=_extra_pages) |
376 # now we check if there is some code for browser | 425 # now we check if there is some code for browser |
377 browser_path = Path(dir_path) / C.PAGES_BROWSER_DIR | 426 browser_path = dir_path / C.PAGES_BROWSER_DIR |
378 if browser_path.is_dir(): | 427 if browser_path.is_dir(): |
379 # for now we only handle Brython | 428 cls.createBrowserData(vhost_root, resource, browser_path, new_path) |
380 dyn_data = { | |
381 "path": browser_path, | |
382 "url_hash": hashlib.sha256( | |
383 '/'.join(new_path).encode()).hexdigest(), | |
384 } | |
385 vhost_root.browser_modules.setdefault( | |
386 "brython", []).append(dyn_data) | |
387 resource.dyn_data['brython'] = dyn_data | |
388 | 429 |
389 @classmethod | 430 @classmethod |
390 def onFileChange(cls, host, file_path, flags, site_root, site_path): | 431 def onFileChange(cls, host, file_path, flags, site_root, site_path): |
391 """Method triggered by file_watcher when something is changed in files | 432 """Method triggered by file_watcher when something is changed in files |
392 | 433 |