path_deploy.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. import os.path
  2. import tornado
  3. from tornado import template
  4. from tornado.web import HTTPError, Finish
  5. from tornado.web import StaticFileHandler
  6. from .httpbased import HttpHandler
  7. from .tornado import webio_handler, set_ioloop
  8. from .tornado_http import TornadoHttpContext
  9. from .utils import cdn_validation, make_applications
  10. from ..session import register_session_implement, CoroutineBasedSession, ThreadBasedSession
  11. from ..utils import get_free_port, STATIC_PATH, parse_file_size
  12. from functools import partial
  13. def filename_ok(f):
  14. return not f.startswith(('.', '_'))
  15. def valid_and_norm_path(base, subpath):
  16. """
  17. :param str base: MUST a absolute path
  18. :param str subpath:
  19. :return: Normalize path. None returned if the sub path is not valid
  20. """
  21. subpath = subpath.lstrip('/')
  22. full_path = os.path.normpath(os.path.join(base, subpath))
  23. if not full_path.startswith(base):
  24. return None
  25. parts = subpath.split('/')
  26. for i in parts:
  27. if not filename_ok(i):
  28. return None
  29. return full_path
  30. _cached_modules = {}
  31. def _get_module(path, reload=False):
  32. # Credit: https://stackoverflow.com/questions/67631/how-to-import-a-module-given-the-full-path
  33. global _cached_modules
  34. import importlib.util
  35. if not reload and path in _cached_modules:
  36. return _cached_modules[path]
  37. # import_name will be the `__name__` of the imported module
  38. import_name = "__pywebio__"
  39. spec = importlib.util.spec_from_file_location(import_name, path)
  40. module = importlib.util.module_from_spec(spec)
  41. spec.loader.exec_module(module)
  42. _cached_modules[path] = module
  43. return module
  44. _app_list_tpl = template.Template("""
  45. <!DOCTYPE html>
  46. <html lang="">
  47. <head>
  48. <meta charset="UTF-8">
  49. <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  50. <title>{{ title }}</title>
  51. <meta name="description" content="PyWebIO applications index">
  52. <style>a{text-decoration:none}</style>
  53. </head>
  54. <body>
  55. <h1>{{ title }}</h1>
  56. <hr>
  57. <pre style="line-height: 1.6em; font-size: 16px;">
  58. {% for f in files %} <a href="{{ f }}">{{ f }}</a>
  59. {% end %}</pre>
  60. <hr>
  61. </body>
  62. </html>
  63. """.strip())
  64. def default_index_page(path, base):
  65. urlpath = path[len(base):] or '/'
  66. title = "Index of %s" % urlpath
  67. dirs = [] if path == base else ['../']
  68. files = []
  69. for f in os.listdir(path):
  70. if not filename_ok(f):
  71. continue
  72. if os.path.isfile(os.path.join(path, f)):
  73. if f.endswith('.py'):
  74. files.append(f[:-3])
  75. else:
  76. dirs.append(f + '/')
  77. return _app_list_tpl.generate(files=dirs + files, title=title)
  78. def get_app_from_path(request_path, base, index, reload=False):
  79. """Get PyWebIO app
  80. :param str request_path: request path
  81. :param str base: dir base path, MUST a absolute path
  82. :param callable index:
  83. :return: ('error', http error code in int) / ('app', pywebio task function) / ('html', Html content in bytes)
  84. """
  85. path = valid_and_norm_path(base, request_path)
  86. if path is None:
  87. return 'error', 403
  88. if os.path.isdir(path):
  89. if not request_path.endswith('/'):
  90. return 'error', 404
  91. if os.path.isfile(os.path.join(path, 'index.py')):
  92. path = os.path.join(path, 'index.py')
  93. elif index:
  94. content = index(path)
  95. return 'html', content
  96. else:
  97. return 'error', 404
  98. else:
  99. path += '.py'
  100. if not os.path.isfile(path):
  101. return 'error', 404
  102. module = _get_module(path, reload=reload)
  103. if hasattr(module, 'main'):
  104. return 'app', make_applications(module.main)
  105. return 'error', 404
  106. def _path_deploy(base, port=0, host='',
  107. static_dir=None, cdn=True, **tornado_app_settings):
  108. if not host:
  109. host = '0.0.0.0'
  110. if port == 0:
  111. port = get_free_port()
  112. for k in list(tornado_app_settings.keys()):
  113. if tornado_app_settings[k] is None:
  114. del tornado_app_settings[k]
  115. abs_base = os.path.normpath(os.path.abspath(base))
  116. cdn = cdn_validation(cdn, 'warn', stacklevel=4) # if CDN is not available, warn user and disable CDN
  117. cdn_url = '/_pywebio_static/' if not cdn else cdn
  118. register_session_implement(CoroutineBasedSession)
  119. register_session_implement(ThreadBasedSession)
  120. RequestHandler = yield cdn_url, abs_base
  121. handlers = []
  122. if static_dir is not None:
  123. handlers.append((r"/static/(.*)", StaticFileHandler, {"path": static_dir}))
  124. if not cdn:
  125. handlers.append((r"/_pywebio_static/(.*)", StaticFileHandler, {"path": STATIC_PATH}))
  126. handlers.append((r"/.*", RequestHandler))
  127. print('Listen on %s:%s' % (host or '0.0.0.0', port))
  128. set_ioloop(tornado.ioloop.IOLoop.current()) # to enable bokeh app
  129. app = tornado.web.Application(handlers=handlers, **tornado_app_settings)
  130. app.listen(port, address=host)
  131. tornado.ioloop.IOLoop.current().start()
  132. def path_deploy(base, port=0, host='',
  133. index=True, static_dir=None,
  134. cdn=True, debug=True,
  135. allowed_origins=None, check_origin=None,
  136. websocket_max_message_size=None,
  137. websocket_ping_interval=None,
  138. websocket_ping_timeout=None,
  139. **tornado_app_settings):
  140. """Deploy the PyWebIO applications from a directory.
  141. The server communicates with the browser using WebSocket protocol.
  142. :param str base: Base directory to load PyWebIO application.
  143. :param int port: The port the server listens on.
  144. :param str host: The host the server listens on.
  145. :param bool/callable index: Whether to provide a default index page when request a directory, default is ``True``.
  146. ``index`` also accepts a function to custom index page, which receives the requested directory path as parameter and return HTML content in string.
  147. You can override the index page by add a `index.py` PyWebIO app file to the directory.
  148. :param str static_dir: Directory to store the application static files.
  149. The files in this directory can be accessed via ``http://<host>:<port>/static/files``.
  150. For example, if there is a ``A/B.jpg`` file in ``http_static_dir`` path,
  151. it can be accessed via ``http://<host>:<port>/static/A/B.jpg``.
  152. The rest arguments of ``path_deploy()`` have the same meaning as for :func:`pywebio.platform.tornado.start_server`
  153. """
  154. gen = _path_deploy(base, port=port, host=host,
  155. static_dir=static_dir,
  156. cdn=cdn, debug=debug,
  157. websocket_max_message_size=websocket_max_message_size,
  158. websocket_ping_interval=websocket_ping_interval,
  159. websocket_ping_timeout=parse_file_size(websocket_ping_timeout or '10M'),
  160. **tornado_app_settings)
  161. cdn_url, abs_base = next(gen)
  162. index_func = {True: partial(default_index_page, base=abs_base), False: lambda p: '403 Forbidden'}.get(index, index)
  163. Handler = webio_handler(lambda: None, cdn_url, allowed_origins=allowed_origins, check_origin=check_origin)
  164. class WSHandler(Handler):
  165. def get_app(self):
  166. reload = self.get_query_argument('reload', None) is not None
  167. type, res = get_app_from_path(self.request.path, abs_base, index=index_func, reload=reload)
  168. if type == 'error':
  169. raise HTTPError(status_code=res)
  170. elif type == 'html':
  171. raise Finish(res)
  172. app_name = self.get_query_argument('app', 'index')
  173. app = res.get(app_name) or res['index']
  174. return app
  175. gen.send(WSHandler)
  176. gen.close()
  177. def path_deploy_http(base, port=0, host='',
  178. index=True, static_dir=None,
  179. cdn=True, debug=True,
  180. allowed_origins=None, check_origin=None,
  181. session_expire_seconds=None,
  182. session_cleanup_interval=None,
  183. **tornado_app_settings):
  184. """Deploy the PyWebIO applications from a directory.
  185. The server communicates with the browser using HTTP protocol.
  186. The ``base``, ``port``, ``host``, ``index``, ``static_dir`` arguments of ``path_deploy_http()``
  187. have the same meaning as for :func:`pywebio.platform.path_deploy.path_deploy`
  188. The rest arguments of ``path_deploy_http()`` have the same meaning as for :func:`pywebio.platform.tornado_http.start_server`
  189. """
  190. gen = _path_deploy(base, port=port, host=host,
  191. static_dir=static_dir,
  192. cdn=cdn, debug=debug,
  193. **tornado_app_settings)
  194. cdn_url, abs_base = next(gen)
  195. index_func = {True: partial(default_index_page, base=abs_base), False: lambda p: '403 Forbidden'}.get(index, index)
  196. def get_app(context: TornadoHttpContext):
  197. reload = context.request_url_parameter('reload', None) is not None
  198. type, res = get_app_from_path(context.get_path(), abs_base, index=index_func, reload=reload)
  199. if type == 'error':
  200. raise HTTPError(status_code=res)
  201. elif type == 'html':
  202. raise Finish(res)
  203. app_name = context.request_url_parameter('app', 'index')
  204. return res.get(app_name) or res['index']
  205. handler = HttpHandler(app_loader=get_app, cdn=cdn_url,
  206. session_expire_seconds=session_expire_seconds,
  207. session_cleanup_interval=session_cleanup_interval,
  208. allowed_origins=allowed_origins,
  209. check_origin=check_origin)
  210. class ReqHandler(tornado.web.RequestHandler):
  211. def options(self):
  212. return self.get()
  213. def post(self):
  214. return self.get()
  215. def get(self):
  216. context = TornadoHttpContext(self)
  217. self.write(handler.handle_request(context))
  218. gen.send(ReqHandler)
  219. gen.close()