1# -*- coding: utf-8 -*-
2"""
3server_cli
4==========
5
6:Author: Martin Wendt
7:Copyright: Licensed under the MIT license, see LICENSE file in this package.
8
9Standalone server that runs WsgiDAV.
10
11These tasks are performed:
12
13 - Set up the configuration from defaults, configuration file, and command line
14 options.
15 - Instantiate the WsgiDAVApp object (which is a WSGI application)
16 - Start a WSGI server for this WsgiDAVApp object
17
18Configuration is defined like this:
19
20 1. Get the name of a configuration file from command line option
21 ``--config-file=FILENAME`` (or short ``-cFILENAME``).
22 If this option is omitted, we use ``wsgidav.yaml`` in the current
23 directory.
24 2. Set reasonable default settings.
25 3. If configuration file exists: read and use it to overwrite defaults.
26 4. If command line options are passed, use them to override settings:
27
28 ``--host`` option overrides ``hostname`` setting.
29
30 ``--port`` option overrides ``port`` setting.
31
32 ``--root=FOLDER`` option creates a FilesystemProvider that publishes
33 FOLDER on the '/' share.
34"""
35import argparse
36import copy
37import logging
38import os
39import platform
40import sys
41import webbrowser
42from pprint import pformat
43from threading import Timer
44
45import yaml
46
47from wsgidav import __version__, util
48from wsgidav.default_conf import DEFAULT_CONFIG, DEFAULT_VERBOSE
49from wsgidav.fs_dav_provider import FilesystemProvider
50from wsgidav.wsgidav_app import WsgiDAVApp
51from wsgidav.xml_tools import use_lxml
52
53try:
54 # Try pyjson5 first because it's faster than json5
55 from pyjson5 import load as json_load
56except ImportError:
57 from json5 import load as json_load
58
59
60__docformat__ = "reStructuredText"
61
62#: Try this config files if no --config=... option is specified
63DEFAULT_CONFIG_FILES = ("wsgidav.yaml", "wsgidav.json")
64
65_logger = logging.getLogger("wsgidav")
66
67
68def _get_common_info(config):
69 """Calculate some common info."""
70 # Support SSL
71 ssl_certificate = util.fix_path(config.get("ssl_certificate"), config)
72 ssl_private_key = util.fix_path(config.get("ssl_private_key"), config)
73 ssl_certificate_chain = util.fix_path(config.get("ssl_certificate_chain"), config)
74 ssl_adapter = config.get("ssl_adapter", "builtin")
75 use_ssl = False
76 if ssl_certificate and ssl_private_key:
77 use_ssl = True
78 # _logger.info("SSL / HTTPS enabled. Adapter: {}".format(ssl_adapter))
79 elif ssl_certificate or ssl_private_key:
80 raise RuntimeError(
81 "Option 'ssl_certificate' and 'ssl_private_key' must be used together."
82 )
83
84 protocol = "https" if use_ssl else "http"
85 url = f"{protocol}://{config['host']}:{config['port']}"
86 info = {
87 "use_ssl": use_ssl,
88 "ssl_cert": ssl_certificate,
89 "ssl_pk": ssl_private_key,
90 "ssl_adapter": ssl_adapter,
91 "ssl_chain": ssl_certificate_chain,
92 "protocol": protocol,
93 "url": url,
94 }
95 return info
96
97
98class FullExpandedPath(argparse.Action):
99 """Expand user- and relative-paths"""
100
101 def __call__(self, parser, namespace, values, option_string=None):
102 new_val = os.path.abspath(os.path.expanduser(values))
103 setattr(namespace, self.dest, new_val)
104
105
106def _init_command_line_options():
107 """Parse command line options into a dictionary."""
108 description = """\
109
110Run a WEBDAV server to share file system folders.
111
112Examples:
113
114 Share filesystem folder '/temp' for anonymous access (no config file used):
115 wsgidav --port=80 --host=0.0.0.0 --root=/temp --auth=anonymous
116
117 Run using a specific configuration file:
118 wsgidav --port=80 --host=0.0.0.0 --config=~/my_wsgidav.yaml
119
120 If no config file is specified, the application will look for a file named
121 'wsgidav.yaml' in the current directory.
122 See
123 https://wsgidav.readthedocs.io/en/latest/user_guide_configure.html
124 for some explanation of the configuration file format.
125 """
126
127 epilog = """\
128Licensed under the MIT license.
129See https://github.com/mar10/wsgidav for additional information.
130
131"""
132
133 parser = argparse.ArgumentParser(
134 prog="wsgidav",
135 description=description,
136 epilog=epilog,
137 allow_abbrev=False,
138 formatter_class=argparse.RawTextHelpFormatter,
139 )
140 parser.add_argument(
141 "-p",
142 "--port",
143 type=int,
144 # default=8080,
145 help="port to serve on (default: 8080)",
146 )
147 parser.add_argument(
148 "-H", # '-h' conflicts with --help
149 "--host",
150 help=(
151 "host to serve from (default: localhost). 'localhost' is only "
152 "accessible from the local computer. Use 0.0.0.0 to make your "
153 "application public"
154 ),
155 )
156 parser.add_argument(
157 "-r",
158 "--root",
159 dest="root_path",
160 action=FullExpandedPath,
161 help="path to a file system folder to publish for RW as share '/'.",
162 )
163 parser.add_argument(
164 "--auth",
165 choices=("anonymous", "nt", "pam-login"),
166 help="quick configuration of a domain controller when no config file "
167 "is used",
168 )
169 parser.add_argument(
170 "--server",
171 choices=SUPPORTED_SERVERS.keys(),
172 # default="cheroot",
173 help="type of pre-installed WSGI server to use (default: cheroot).",
174 )
175 parser.add_argument(
176 "--ssl-adapter",
177 choices=("builtin", "pyopenssl"),
178 # default="builtin",
179 help="used by 'cheroot' server if SSL certificates are configured "
180 "(default: builtin).",
181 )
182
183 qv_group = parser.add_mutually_exclusive_group()
184 qv_group.add_argument(
185 "-v",
186 "--verbose",
187 action="count",
188 default=3,
189 help="increment verbosity by one (default: %(default)s, range: 0..5)",
190 )
191 qv_group.add_argument(
192 "-q", "--quiet", default=0, action="count", help="decrement verbosity by one"
193 )
194
195 qv_group = parser.add_mutually_exclusive_group()
196 qv_group.add_argument(
197 "-c",
198 "--config",
199 dest="config_file",
200 action=FullExpandedPath,
201 help=(
202 f"configuration file (default: {DEFAULT_CONFIG_FILES} in current directory)"
203 ),
204 )
205
206 qv_group.add_argument(
207 "--no-config",
208 action="store_true",
209 help=f"do not try to load default {DEFAULT_CONFIG_FILES}",
210 )
211
212 parser.add_argument(
213 "--browse",
214 action="store_true",
215 help="open browser on start",
216 )
217
218 parser.add_argument(
219 "-V",
220 "--version",
221 action="store_true",
222 help="print version info and exit (may be combined with --verbose)",
223 )
224
225 args = parser.parse_args()
226
227 args.verbose -= args.quiet
228 del args.quiet
229
230 if args.root_path and not os.path.isdir(args.root_path):
231 msg = f"{args.root_path} is not a directory"
232 parser.error(msg)
233
234 if args.version:
235 if args.verbose >= 4:
236 version_info = "WsgiDAV/{} Python/{}({} bit) {}".format(
237 __version__,
238 util.PYTHON_VERSION,
239 "64" if sys.maxsize > 2**32 else "32",
240 platform.platform(aliased=True),
241 )
242 version_info += f"\nPython from: {sys.executable}"
243 else:
244 version_info = f"{__version__}"
245 print(version_info)
246 sys.exit()
247
248 if args.no_config:
249 pass
250 # ... else ignore default config files
251 elif args.config_file is None:
252 # If --config was omitted, use default (if it exists)
253 for filename in DEFAULT_CONFIG_FILES:
254 defPath = os.path.abspath(filename)
255 if os.path.exists(defPath):
256 if args.verbose >= 3:
257 print(f"Using default configuration file: {defPath}")
258 args.config_file = defPath
259 break
260 else:
261 # If --config was specified convert to absolute path and assert it exists
262 args.config_file = os.path.abspath(args.config_file)
263 if not os.path.isfile(args.config_file):
264 parser.error(
265 "Could not find specified configuration file: {}".format(
266 args.config_file
267 )
268 )
269
270 # Convert args object to dictionary
271 cmdLineOpts = args.__dict__.copy()
272 if args.verbose >= 5:
273 print("Command line args:")
274 for k, v in cmdLineOpts.items():
275 print(" {:>12}: {}".format(k, v))
276 return cmdLineOpts, parser
277
278
279def _read_config_file(config_file, _verbose):
280 """Read configuration file options into a dictionary."""
281
282 config_file = os.path.abspath(config_file)
283
284 if not os.path.exists(config_file):
285 raise RuntimeError(f"Couldn't open configuration file {config_file!r}.")
286
287 if config_file.endswith(".json"):
288 with open(config_file, mode="rt", encoding="utf-8-sig") as fp:
289 conf = json_load(fp)
290
291 elif config_file.endswith(".yaml"):
292 with open(config_file, mode="rt", encoding="utf-8-sig") as fp:
293 conf = yaml.safe_load(fp)
294
295 else:
296 raise RuntimeError(
297 f"Unsupported config file format (expected yaml or json): {config_file}"
298 )
299
300 conf["_config_file"] = config_file
301 conf["_config_root"] = os.path.dirname(config_file)
302 return conf
303
304
305def _init_config():
306 """Setup configuration dictionary from default, command line and configuration file."""
307 cli_opts, parser = _init_command_line_options()
308 cli_verbose = cli_opts["verbose"]
309
310 # Set config defaults
311 config = copy.deepcopy(DEFAULT_CONFIG)
312 config["_config_file"] = None
313 config["_config_root"] = os.getcwd()
314
315 # Configuration file overrides defaults
316 config_file = cli_opts.get("config_file")
317 if config_file:
318 file_opts = _read_config_file(config_file, cli_verbose)
319 util.deep_update(config, file_opts)
320 if cli_verbose != DEFAULT_VERBOSE and "verbose" in file_opts:
321 if cli_verbose >= 2:
322 print(
323 "Config file defines 'verbose: {}' but is overridden by command line: {}.".format(
324 file_opts["verbose"], cli_verbose
325 )
326 )
327 config["verbose"] = cli_verbose
328 else:
329 if cli_verbose >= 2:
330 print("Running without configuration file.")
331
332 # Command line overrides file
333 if cli_opts.get("port"):
334 config["port"] = cli_opts.get("port")
335 if cli_opts.get("host"):
336 config["host"] = cli_opts.get("host")
337 if cli_opts.get("profile") is not None:
338 config["profile"] = True
339 if cli_opts.get("server") is not None:
340 config["server"] = cli_opts.get("server")
341 if cli_opts.get("ssl_adapter") is not None:
342 config["ssl_adapter"] = cli_opts.get("ssl_adapter")
343
344 # Command line overrides file only if -v or -q where passed:
345 if cli_opts.get("verbose") != DEFAULT_VERBOSE:
346 config["verbose"] = cli_opts.get("verbose")
347
348 if cli_opts.get("root_path"):
349 root_path = os.path.abspath(cli_opts.get("root_path"))
350 config["provider_mapping"]["/"] = FilesystemProvider(
351 root_path,
352 fs_opts=config.get("fs_dav_provider"),
353 )
354
355 if config["verbose"] >= 5:
356 # TODO: remove passwords from user_mapping
357 config_cleaned = util.purge_passwords(config)
358 print(
359 "Configuration({}):\n{}".format(
360 cli_opts["config_file"], pformat(config_cleaned)
361 )
362 )
363
364 if not config["provider_mapping"]:
365 parser.error("No DAV provider defined.")
366
367 # Quick-configuration of DomainController
368 auth = cli_opts.get("auth")
369 auth_conf = util.get_dict_value(config, "http_authenticator", as_dict=True)
370 if auth and auth_conf.get("domain_controller"):
371 parser.error(
372 "--auth option can only be used when no domain_controller is configured"
373 )
374
375 if auth == "anonymous":
376 if config["simple_dc"]["user_mapping"]:
377 parser.error(
378 "--auth=anonymous can only be used when no user_mapping is configured"
379 )
380 auth_conf.update(
381 {
382 "domain_controller": "wsgidav.dc.simple_dc.SimpleDomainController",
383 "accept_basic": True,
384 "accept_digest": True,
385 "default_to_digest": True,
386 }
387 )
388 config["simple_dc"]["user_mapping"] = {"*": True}
389 elif auth == "nt":
390 if config.get("nt_dc"):
391 parser.error(
392 "--auth=nt can only be used when no nt_dc settings are configured"
393 )
394 auth_conf.update(
395 {
396 "domain_controller": "wsgidav.dc.nt_dc.NTDomainController",
397 "accept_basic": True,
398 "accept_digest": False,
399 "default_to_digest": False,
400 }
401 )
402 config["nt_dc"] = {}
403 elif auth == "pam-login":
404 if config.get("pam_dc"):
405 parser.error(
406 "--auth=pam-login can only be used when no pam_dc settings are configured"
407 )
408 auth_conf.update(
409 {
410 "domain_controller": "wsgidav.dc.pam_dc.PAMDomainController",
411 "accept_basic": True,
412 "accept_digest": False,
413 "default_to_digest": False,
414 }
415 )
416 config["pam_dc"] = {"service": "login"}
417 # print(config)
418
419 # if cli_opts.get("reload"):
420 # print("Installing paste.reloader.", file=sys.stderr)
421 # from paste import reloader # @UnresolvedImport
422
423 # reloader.install()
424 # if config_file:
425 # # Add config file changes
426 # reloader.watch_file(config_file)
427 # # import pydevd
428 # # pydevd.settrace()
429
430 if config["suppress_version_info"]:
431 util.public_wsgidav_info = "WsgiDAV"
432 util.public_python_info = f"Python/{sys.version_info[0]}"
433
434 return cli_opts, config
435
436
437def _run_cheroot(app, config, _server):
438 """Run WsgiDAV using cheroot.server (https://cheroot.cherrypy.dev/)."""
439 try:
440 from cheroot import server, wsgi
441 except ImportError:
442 _logger.exception("Could not import Cheroot (https://cheroot.cherrypy.dev/).")
443 _logger.error("Try `pip install cheroot`.")
444 return False
445
446 version = (
447 f"{util.public_wsgidav_info} {wsgi.Server.version} {util.public_python_info}"
448 )
449 # wsgi.Server.version = version
450
451 info = _get_common_info(config)
452
453 # Support SSL
454 if info["use_ssl"]:
455 ssl_adapter = info["ssl_adapter"]
456 ssl_adapter = server.get_ssl_adapter_class(ssl_adapter)
457 wsgi.Server.ssl_adapter = ssl_adapter(
458 info["ssl_cert"], info["ssl_pk"], info["ssl_chain"]
459 )
460 _logger.info("SSL / HTTPS enabled. Adapter: {}".format(ssl_adapter))
461
462 _logger.info(f"Running {version}")
463 _logger.info(f"Serving on {info['url']} ...")
464
465 server_args = {
466 "bind_addr": (config["host"], config["port"]),
467 "wsgi_app": app,
468 "server_name": version,
469 # File Explorer needs lot of threads (see issue #149):
470 "numthreads": 50, # TODO: still required?
471 }
472 # Override or add custom args
473 custom_args = util.get_dict_value(config, "server_args", as_dict=True)
474 server_args.update(custom_args)
475
476 class PatchedServer(wsgi.Server):
477 STARTUP_NOTIFICATION_DELAY = 0.5
478
479 def serve(self, *args, **kwargs):
480 _logger.error("wsgi.Server.serve")
481 if startup_event and not startup_event.is_set():
482 Timer(self.STARTUP_NOTIFICATION_DELAY, startup_event.set).start()
483 _logger.error("wsgi.Server is ready")
484 return super().serve(*args, **kwargs)
485
486 # If the caller passed a startup event, monkey patch the server to set it
487 # when the request handler loop is entered
488 startup_event = config.get("startup_event")
489 if startup_event:
490 server = PatchedServer(**server_args)
491 else:
492 server = wsgi.Server(**server_args)
493
494 try:
495 server.start()
496 except KeyboardInterrupt:
497 _logger.warning("Caught Ctrl-C, shutting down...")
498 finally:
499 server.stop()
500
501 return
502
503
504def _run_ext_wsgiutils(app, config, _server):
505 """Run WsgiDAV using ext_wsgiutils_server from the wsgidav package."""
506 from wsgidav.server import ext_wsgiutils_server
507
508 _logger.warning(
509 "WARNING: This single threaded server (ext-wsgiutils) is not meant for production."
510 )
511 try:
512 ext_wsgiutils_server.serve(config, app)
513 except KeyboardInterrupt:
514 _logger.warning("Caught Ctrl-C, shutting down...")
515 return
516
517
518def _run_gevent(app, config, server):
519 """Run WsgiDAV using gevent if gevent (https://www.gevent.org).
520
521 See
522 https://github.com/gevent/gevent/blob/master/src/gevent/pywsgi.py#L1356
523 https://github.com/gevent/gevent/blob/master/src/gevent/server.py#L38
524 for more options.
525 """
526 try:
527 import gevent
528 import gevent.monkey
529 from gevent.pywsgi import WSGIServer
530 except ImportError:
531 _logger.exception("Could not import gevent (http://www.gevent.org).")
532 _logger.error("Try `pip install gevent`.")
533 return False
534
535 gevent.monkey.patch_all()
536
537 info = _get_common_info(config)
538 version = f"gevent/{gevent.__version__}"
539 version = f"{util.public_wsgidav_info} {version} {util.public_python_info}"
540
541 # Override or add custom args
542 server_args = {
543 "wsgi_app": app,
544 "bind_addr": (config["host"], config["port"]),
545 }
546 custom_args = util.get_dict_value(config, "server_args", as_dict=True)
547 server_args.update(custom_args)
548
549 if info["use_ssl"]:
550 dav_server = WSGIServer(
551 server_args["bind_addr"],
552 app,
553 keyfile=info["ssl_pk"],
554 certfile=info["ssl_cert"],
555 ca_certs=info["ssl_chain"],
556 )
557 else:
558 dav_server = WSGIServer(server_args["bind_addr"], app)
559
560 # If the caller passed a startup event, monkey patch the server to set it
561 # when the request handler loop is entered
562 startup_event = config.get("startup_event")
563 if startup_event:
564
565 def _patched_start():
566 dav_server.start_accepting = org_start # undo the monkey patch
567 org_start()
568 _logger.info("gevent is ready")
569 startup_event.set()
570
571 org_start = dav_server.start_accepting
572 dav_server.start_accepting = _patched_start
573
574 _logger.info(f"Running {version}")
575 _logger.info(f"Serving on {info['url']} ...")
576 try:
577 gevent.spawn(dav_server.serve_forever())
578 except KeyboardInterrupt:
579 _logger.warning("Caught Ctrl-C, shutting down...")
580 return
581
582
583def _run_gunicorn(app, config, server):
584 """Run WsgiDAV using Gunicorn (https://gunicorn.org)."""
585 try:
586 import gunicorn.app.base
587 except ImportError:
588 _logger.exception("Could not import Gunicorn (https://gunicorn.org).")
589 _logger.error("Try `pip install gunicorn` (UNIX only).")
590 return False
591
592 info = _get_common_info(config)
593
594 class GunicornApplication(gunicorn.app.base.BaseApplication):
595 def __init__(self, app, options=None):
596 self.options = options or {}
597 self.application = app
598 super().__init__()
599
600 def load_config(self):
601 config = {
602 key: value
603 for key, value in self.options.items()
604 if key in self.cfg.settings and value is not None
605 }
606 for key, value in config.items():
607 self.cfg.set(key.lower(), value)
608
609 def load(self):
610 return self.application
611
612 # See https://docs.gunicorn.org/en/latest/settings.html
613 server_args = {
614 "bind": "{}:{}".format(config["host"], config["port"]),
615 "threads": 50,
616 "timeout": 1200,
617 }
618 if info["use_ssl"]:
619 server_args.update(
620 {
621 "keyfile": info["ssl_pk"],
622 "certfile": info["ssl_cert"],
623 "ca_certs": info["ssl_chain"],
624 # "ssl_version": ssl_version
625 # "cert_reqs": ssl_cert_reqs
626 # "ciphers": ssl_ciphers
627 }
628 )
629 # Override or add custom args
630 custom_args = util.get_dict_value(config, "server_args", as_dict=True)
631 server_args.update(custom_args)
632
633 version = f"gunicorn/{gunicorn.__version__}"
634 version = f"{util.public_wsgidav_info} {version} {util.public_python_info}"
635 _logger.info(f"Running {version} ...")
636
637 GunicornApplication(app, server_args).run()
638
639
640def _run_paste(app, config, server):
641 """Run WsgiDAV using paste.httpserver, if Paste is installed.
642
643 See http://pythonpaste.org/modules/httpserver.html for more options
644 """
645 try:
646 from paste import httpserver
647 except ImportError:
648 _logger.exception(
649 "Could not import paste.httpserver (https://github.com/cdent/paste)."
650 )
651 _logger.error("Try `pip install paste`.")
652 return False
653
654 info = _get_common_info(config)
655
656 version = httpserver.WSGIHandler.server_version
657 version = f"{util.public_wsgidav_info} {version} {util.public_python_info}"
658
659 # See http://pythonpaste.org/modules/httpserver.html for more options
660 server = httpserver.serve(
661 app,
662 host=config["host"],
663 port=config["port"],
664 server_version=version,
665 # This option enables handling of keep-alive and expect-100:
666 protocol_version="HTTP/1.1",
667 start_loop=False,
668 )
669
670 if config["verbose"] >= 5:
671 __handle_one_request = server.RequestHandlerClass.handle_one_request
672
673 def handle_one_request(self):
674 __handle_one_request(self)
675 if self.close_connection == 1:
676 _logger.debug("HTTP Connection : close")
677 else:
678 _logger.debug("HTTP Connection : continue")
679
680 server.RequestHandlerClass.handle_one_request = handle_one_request
681
682 _logger.info(f"Running {version} ...")
683 host, port = server.server_address
684 if host == "0.0.0.0":
685 _logger.info(f"Serving on 0.0.0.0:{port} view at http://127.0.0.1:{port}")
686 else:
687 _logger.info(f"Serving on {info['url']}")
688
689 try:
690 server.serve_forever()
691 except KeyboardInterrupt:
692 _logger.warning("Caught Ctrl-C, shutting down...")
693 return
694
695
696def _run_uvicorn(app, config, server):
697 """Run WsgiDAV using Uvicorn (https://www.uvicorn.org)."""
698 try:
699 import uvicorn
700 except ImportError:
701 _logger.exception("Could not import Uvicorn (https://www.uvicorn.org).")
702 _logger.error("Try `pip install uvicorn`.")
703 return False
704
705 info = _get_common_info(config)
706
707 # See https://www.uvicorn.org/settings/
708 server_args = {
709 "interface": "wsgi",
710 "host": config["host"],
711 "port": config["port"],
712 # TODO: see _run_cheroot()
713 }
714 if info["use_ssl"]:
715 server_args.update(
716 {
717 "ssl_keyfile": info["ssl_pk"],
718 "ssl_certfile": info["ssl_cert"],
719 "ssl_ca_certs": info["ssl_chain"],
720 # "ssl_keyfile_password": ssl_keyfile_password
721 # "ssl_version": ssl_version
722 # "ssl_cert_reqs": ssl_cert_reqs
723 # "ssl_ciphers": ssl_ciphers
724 }
725 )
726 # Override or add custom args
727 custom_args = util.get_dict_value(config, "server_args", as_dict=True)
728 server_args.update(custom_args)
729
730 version = f"uvicorn/{uvicorn.__version__}"
731 version = f"{util.public_wsgidav_info} {version} {util.public_python_info}"
732 _logger.info(f"Running {version} ...")
733
734 uvicorn.run(app, **server_args)
735
736
737def _run_wsgiref(app, config, _server):
738 """Run WsgiDAV using wsgiref.simple_server (https://docs.python.org/3/library/wsgiref.html)."""
739 from wsgiref.simple_server import WSGIRequestHandler, make_server
740
741 version = WSGIRequestHandler.server_version
742 version = f"{util.public_wsgidav_info} {version}" # {util.public_python_info}"
743 _logger.info(f"Running {version} ...")
744
745 _logger.warning(
746 "WARNING: This single threaded server (wsgiref) is not meant for production."
747 )
748 WSGIRequestHandler.server_version = version
749 httpd = make_server(config["host"], config["port"], app)
750 # httpd.RequestHandlerClass.server_version = version
751 try:
752 httpd.serve_forever()
753 except KeyboardInterrupt:
754 _logger.warning("Caught Ctrl-C, shutting down...")
755 return
756
757
758SUPPORTED_SERVERS = {
759 "cheroot": _run_cheroot,
760 "ext-wsgiutils": _run_ext_wsgiutils,
761 "gevent": _run_gevent,
762 "gunicorn": _run_gunicorn,
763 "paste": _run_paste,
764 "uvicorn": _run_uvicorn,
765 "wsgiref": _run_wsgiref,
766}
767
768
769def run():
770 cli_opts, config = _init_config()
771
772 # util.init_logging(config) # now handled in constructor:
773 config["logging"]["enable"] = True
774
775 info = _get_common_info(config)
776
777 app = WsgiDAVApp(config)
778
779 server = config["server"]
780 handler = SUPPORTED_SERVERS.get(server)
781 if not handler:
782 raise RuntimeError(
783 "Unsupported server type {!r} (expected {!r})".format(
784 server, "', '".join(SUPPORTED_SERVERS.keys())
785 )
786 )
787
788 if not use_lxml and config["verbose"] >= 3:
789 _logger.warning(
790 "Could not import lxml: using xml instead (up to 10% slower). "
791 "Consider `pip install lxml`(see https://pypi.python.org/pypi/lxml)."
792 )
793
794 if cli_opts["browse"]:
795 BROWSE_DELAY = 2.0
796
797 def _worker():
798 url = info["url"]
799 url = url.replace("0.0.0.0", "127.0.0.1")
800 _logger.info(f"Starting browser on {url} ...")
801 webbrowser.open(url)
802
803 Timer(BROWSE_DELAY, _worker).start()
804
805 handler(app, config, server)
806 return
807
808
809if __name__ == "__main__":
810 # Just in case...
811 from multiprocessing import freeze_support
812
813 freeze_support()
814
815 run()