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