1"""
2server_cli
3==========
4
5:Author: Martin Wendt
6:Copyright: Licensed under the MIT license, see LICENSE file in this package.
7
8Standalone server that runs WsgiDAV.
9
10These tasks are performed:
11
12 - Set up the configuration from defaults, configuration file, and command line
13 options.
14 - Instantiate the WsgiDAVApp object (which is a WSGI application)
15 - Start a WSGI server for this WsgiDAVApp object
16
17Configuration is defined like this:
18
19 1. Get the name of a configuration file from command line option
20 ``--config-file=FILENAME`` (or short ``-cFILENAME``).
21 If this option is omitted, we use ``wsgidav.yaml`` in the current
22 directory.
23 2. Set reasonable default settings.
24 3. If configuration file exists: read and use it to overwrite defaults.
25 4. If command line options are passed, use them to override settings:
26
27 ``--host`` option overrides ``hostname`` setting.
28
29 ``--port`` option overrides ``port`` setting.
30
31 ``--root=FOLDER`` option creates a FilesystemProvider that publishes
32 FOLDER on the '/' share.
33"""
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 http://wsgidav.readthedocs.io/en/latest/run-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 f"Could not find specified configuration file: {args.config_file}"
266 )
267
268 # Convert args object to dictionary
269 cmdLineOpts = args.__dict__.copy()
270 if args.verbose >= 5:
271 print("Command line args:")
272 for k, v in cmdLineOpts.items():
273 print(f" {k:>12}: {v}")
274 return cmdLineOpts, parser
275
276
277def _read_config_file(config_file, _verbose):
278 """Read configuration file options into a dictionary."""
279
280 config_file = os.path.abspath(config_file)
281
282 if not os.path.exists(config_file):
283 raise RuntimeError(f"Couldn't open configuration file {config_file!r}.")
284
285 if config_file.endswith(".json"):
286 with open(config_file, encoding="utf-8-sig") as fp:
287 conf = json_load(fp)
288
289 elif config_file.endswith(".yaml"):
290 with open(config_file, encoding="utf-8-sig") as fp:
291 conf = yaml.safe_load(fp)
292
293 else:
294 raise RuntimeError(
295 f"Unsupported config file format (expected yaml or json): {config_file}"
296 )
297
298 conf["_config_file"] = config_file
299 conf["_config_root"] = os.path.dirname(config_file)
300 return conf
301
302
303def _init_config():
304 """Setup configuration dictionary from default, command line and configuration file."""
305 cli_opts, parser = _init_command_line_options()
306 cli_verbose = cli_opts["verbose"]
307
308 # Set config defaults
309 config = copy.deepcopy(DEFAULT_CONFIG)
310 config["_config_file"] = None
311 config["_config_root"] = os.getcwd()
312
313 # Configuration file overrides defaults
314 config_file = cli_opts.get("config_file")
315 if config_file:
316 file_opts = _read_config_file(config_file, cli_verbose)
317 util.deep_update(config, file_opts)
318 if cli_verbose != DEFAULT_VERBOSE and "verbose" in file_opts:
319 if cli_verbose >= 2:
320 print(
321 "Config file defines 'verbose: {}' but is overridden by command line: {}.".format(
322 file_opts["verbose"], cli_verbose
323 )
324 )
325 config["verbose"] = cli_verbose
326 else:
327 if cli_verbose >= 2:
328 print("Running without configuration file.")
329
330 # Command line overrides file
331 if cli_opts.get("port"):
332 config["port"] = cli_opts.get("port")
333 if cli_opts.get("host"):
334 config["host"] = cli_opts.get("host")
335 if cli_opts.get("profile") is not None:
336 config["profile"] = True
337 if cli_opts.get("server") is not None:
338 config["server"] = cli_opts.get("server")
339 if cli_opts.get("ssl_adapter") is not None:
340 config["ssl_adapter"] = cli_opts.get("ssl_adapter")
341
342 # Command line overrides file only if -v or -q where passed:
343 if cli_opts.get("verbose") != DEFAULT_VERBOSE:
344 config["verbose"] = cli_opts.get("verbose")
345
346 if cli_opts.get("root_path"):
347 root_path = os.path.abspath(cli_opts.get("root_path"))
348 config["provider_mapping"]["/"] = FilesystemProvider(
349 root_path,
350 fs_opts=config.get("fs_dav_provider"),
351 )
352
353 if config["verbose"] >= 5:
354 # TODO: remove passwords from user_mapping
355 config_cleaned = util.purge_passwords(config)
356 print(
357 "Configuration({}):\n{}".format(
358 cli_opts["config_file"], pformat(config_cleaned)
359 )
360 )
361
362 if not config["provider_mapping"]:
363 parser.error("No DAV provider defined.")
364
365 # Quick-configuration of DomainController
366 auth = cli_opts.get("auth")
367 auth_conf = util.get_dict_value(config, "http_authenticator", as_dict=True)
368 if auth and auth_conf.get("domain_controller"):
369 parser.error(
370 "--auth option can only be used when no domain_controller is configured"
371 )
372
373 if auth == "anonymous":
374 if config["simple_dc"]["user_mapping"]:
375 parser.error(
376 "--auth=anonymous can only be used when no user_mapping is configured"
377 )
378 auth_conf.update(
379 {
380 "domain_controller": "wsgidav.dc.simple_dc.SimpleDomainController",
381 "accept_basic": True,
382 "accept_digest": True,
383 "default_to_digest": True,
384 }
385 )
386 config["simple_dc"]["user_mapping"] = {"*": True}
387 elif auth == "nt":
388 if config.get("nt_dc"):
389 parser.error(
390 "--auth=nt can only be used when no nt_dc settings are configured"
391 )
392 auth_conf.update(
393 {
394 "domain_controller": "wsgidav.dc.nt_dc.NTDomainController",
395 "accept_basic": True,
396 "accept_digest": False,
397 "default_to_digest": False,
398 }
399 )
400 config["nt_dc"] = {}
401 elif auth == "pam-login":
402 if config.get("pam_dc"):
403 parser.error(
404 "--auth=pam-login can only be used when no pam_dc settings are configured"
405 )
406 auth_conf.update(
407 {
408 "domain_controller": "wsgidav.dc.pam_dc.PAMDomainController",
409 "accept_basic": True,
410 "accept_digest": False,
411 "default_to_digest": False,
412 }
413 )
414 config["pam_dc"] = {"service": "login"}
415 # print(config)
416
417 # if cli_opts.get("reload"):
418 # print("Installing paste.reloader.", file=sys.stderr)
419 # from paste import reloader # @UnresolvedImport
420
421 # reloader.install()
422 # if config_file:
423 # # Add config file changes
424 # reloader.watch_file(config_file)
425 # # import pydevd
426 # # pydevd.settrace()
427
428 if config["suppress_version_info"]:
429 util.public_wsgidav_info = "WsgiDAV"
430 util.public_python_info = f"Python/{sys.version_info[0]}"
431
432 return cli_opts, config
433
434
435def _run_cheroot(app, config, _server):
436 """Run WsgiDAV using cheroot.server (https://cheroot.cherrypy.dev/)."""
437 try:
438 from cheroot import server, wsgi
439 except ImportError:
440 _logger.exception("Could not import Cheroot (https://cheroot.cherrypy.dev/).")
441 _logger.error("Try `pip install cheroot`.")
442 return False
443
444 version = (
445 f"{util.public_wsgidav_info} {wsgi.Server.version} {util.public_python_info}"
446 )
447 # wsgi.Server.version = version
448
449 info = _get_common_info(config)
450
451 # Support SSL
452 if info["use_ssl"]:
453 ssl_adapter = info["ssl_adapter"]
454 ssl_adapter = server.get_ssl_adapter_class(ssl_adapter)
455 wsgi.Server.ssl_adapter = ssl_adapter(
456 info["ssl_cert"], info["ssl_pk"], info["ssl_chain"]
457 )
458 _logger.info(f"SSL / HTTPS enabled. Adapter: {ssl_adapter}")
459
460 _logger.info(f"Running {version}")
461 _logger.info(f"Serving on {info['url']} ...")
462
463 server_args = {
464 "bind_addr": (config["host"], config["port"]),
465 "wsgi_app": app,
466 "server_name": version,
467 # File Explorer needs lot of threads (see issue #149):
468 "numthreads": 50, # TODO: still required?
469 }
470 # Override or add custom args
471 custom_args = util.get_dict_value(config, "server_args", as_dict=True)
472 server_args.update(custom_args)
473
474 class PatchedServer(wsgi.Server):
475 STARTUP_NOTIFICATION_DELAY = 0.5
476
477 def serve(self, *args, **kwargs):
478 _logger.error("wsgi.Server.serve")
479 if startup_event and not startup_event.is_set():
480 Timer(self.STARTUP_NOTIFICATION_DELAY, startup_event.set).start()
481 _logger.error("wsgi.Server is ready")
482 return super().serve(*args, **kwargs)
483
484 # If the caller passed a startup event, monkey patch the server to set it
485 # when the request handler loop is entered
486 startup_event = config.get("startup_event")
487 if startup_event:
488 server = PatchedServer(**server_args)
489 else:
490 server = wsgi.Server(**server_args)
491
492 try:
493 server.start()
494 except KeyboardInterrupt:
495 _logger.warning("Caught Ctrl-C, shutting down...")
496 finally:
497 server.stop()
498
499 return
500
501
502def _run_ext_wsgiutils(app, config, _server):
503 """Run WsgiDAV using ext_wsgiutils_server from the wsgidav package."""
504 from wsgidav.server import ext_wsgiutils_server
505
506 _logger.warning(
507 "WARNING: This single threaded server (ext-wsgiutils) is not meant for production."
508 )
509 try:
510 ext_wsgiutils_server.serve(config, app)
511 except KeyboardInterrupt:
512 _logger.warning("Caught Ctrl-C, shutting down...")
513 return
514
515
516def _run_gevent(app, config, server):
517 """Run WsgiDAV using gevent if gevent (https://www.gevent.org).
518
519 See
520 https://github.com/gevent/gevent/blob/master/src/gevent/pywsgi.py#L1356
521 https://github.com/gevent/gevent/blob/master/src/gevent/server.py#L38
522 for more options.
523 """
524 try:
525 import gevent
526 import gevent.monkey
527 from gevent.pywsgi import WSGIServer
528 except ImportError:
529 _logger.exception("Could not import gevent (http://www.gevent.org).")
530 _logger.error("Try `pip install gevent`.")
531 return False
532
533 gevent.monkey.patch_all()
534
535 info = _get_common_info(config)
536 version = f"gevent/{gevent.__version__}"
537 version = f"{util.public_wsgidav_info} {version} {util.public_python_info}"
538
539 # Override or add custom args
540 server_args = {
541 "wsgi_app": app,
542 "bind_addr": (config["host"], config["port"]),
543 }
544 custom_args = util.get_dict_value(config, "server_args", as_dict=True)
545 server_args.update(custom_args)
546
547 if info["use_ssl"]:
548 dav_server = WSGIServer(
549 server_args["bind_addr"],
550 app,
551 keyfile=info["ssl_pk"],
552 certfile=info["ssl_cert"],
553 ca_certs=info["ssl_chain"],
554 )
555 else:
556 dav_server = WSGIServer(server_args["bind_addr"], app)
557
558 # If the caller passed a startup event, monkey patch the server to set it
559 # when the request handler loop is entered
560 startup_event = config.get("startup_event")
561 if startup_event:
562
563 def _patched_start():
564 dav_server.start_accepting = org_start # undo the monkey patch
565 org_start()
566 _logger.info("gevent is ready")
567 startup_event.set()
568
569 org_start = dav_server.start_accepting
570 dav_server.start_accepting = _patched_start
571
572 _logger.info(f"Running {version}")
573 _logger.info(f"Serving on {info['url']} ...")
574 try:
575 gevent.spawn(dav_server.serve_forever())
576 except KeyboardInterrupt:
577 _logger.warning("Caught Ctrl-C, shutting down...")
578 return
579
580
581def _run_gunicorn(app, config, server):
582 """Run WsgiDAV using Gunicorn (https://gunicorn.org)."""
583 try:
584 import gunicorn.app.base
585 except ImportError:
586 _logger.exception("Could not import Gunicorn (https://gunicorn.org).")
587 _logger.error("Try `pip install gunicorn` (UNIX only).")
588 return False
589
590 info = _get_common_info(config)
591
592 class GunicornApplication(gunicorn.app.base.BaseApplication):
593 def __init__(self, app, options=None):
594 self.options = options or {}
595 self.application = app
596 super().__init__()
597
598 def load_config(self):
599 config = {
600 key: value
601 for key, value in self.options.items()
602 if key in self.cfg.settings and value is not None
603 }
604 for key, value in config.items():
605 self.cfg.set(key.lower(), value)
606
607 def load(self):
608 return self.application
609
610 # See https://docs.gunicorn.org/en/latest/settings.html
611 server_args = {
612 "bind": "{}:{}".format(config["host"], config["port"]),
613 "threads": 50,
614 "timeout": 1200,
615 }
616 if info["use_ssl"]:
617 server_args.update(
618 {
619 "keyfile": info["ssl_pk"],
620 "certfile": info["ssl_cert"],
621 "ca_certs": info["ssl_chain"],
622 # "ssl_version": ssl_version
623 # "cert_reqs": ssl_cert_reqs
624 # "ciphers": ssl_ciphers
625 }
626 )
627 # Override or add custom args
628 custom_args = util.get_dict_value(config, "server_args", as_dict=True)
629 server_args.update(custom_args)
630
631 version = f"gunicorn/{gunicorn.__version__}"
632 version = f"{util.public_wsgidav_info} {version} {util.public_python_info}"
633 _logger.info(f"Running {version} ...")
634
635 GunicornApplication(app, server_args).run()
636
637
638def _run_paste(app, config, server):
639 """Run WsgiDAV using paste.httpserver, if Paste is installed.
640
641 See http://pythonpaste.org/modules/httpserver.html for more options
642 """
643 try:
644 from paste import httpserver
645 except ImportError:
646 _logger.exception(
647 "Could not import paste.httpserver (https://github.com/cdent/paste)."
648 )
649 _logger.error("Try `pip install paste`.")
650 return False
651
652 info = _get_common_info(config)
653
654 version = httpserver.WSGIHandler.server_version
655 version = f"{util.public_wsgidav_info} {version} {util.public_python_info}"
656
657 # See http://pythonpaste.org/modules/httpserver.html for more options
658 server = httpserver.serve(
659 app,
660 host=config["host"],
661 port=config["port"],
662 server_version=version,
663 # This option enables handling of keep-alive and expect-100:
664 protocol_version="HTTP/1.1",
665 start_loop=False,
666 )
667
668 if config["verbose"] >= 5:
669 __handle_one_request = server.RequestHandlerClass.handle_one_request
670
671 def handle_one_request(self):
672 __handle_one_request(self)
673 if self.close_connection == 1:
674 _logger.debug("HTTP Connection : close")
675 else:
676 _logger.debug("HTTP Connection : continue")
677
678 server.RequestHandlerClass.handle_one_request = handle_one_request
679
680 _logger.info(f"Running {version} ...")
681 host, port = server.server_address
682 if host == "0.0.0.0":
683 _logger.info(f"Serving on 0.0.0.0:{port} view at http://127.0.0.1:{port}")
684 else:
685 _logger.info(f"Serving on {info['url']}")
686
687 try:
688 server.serve_forever()
689 except KeyboardInterrupt:
690 _logger.warning("Caught Ctrl-C, shutting down...")
691 return
692
693
694def _run_uvicorn(app, config, server):
695 """Run WsgiDAV using Uvicorn (https://www.uvicorn.org)."""
696 try:
697 import uvicorn
698 except ImportError:
699 _logger.exception("Could not import Uvicorn (https://www.uvicorn.org).")
700 _logger.error("Try `pip install uvicorn`.")
701 return False
702
703 info = _get_common_info(config)
704
705 # See https://www.uvicorn.org/settings/
706 server_args = {
707 "interface": "wsgi",
708 "host": config["host"],
709 "port": config["port"],
710 # TODO: see _run_cheroot()
711 }
712 if info["use_ssl"]:
713 server_args.update(
714 {
715 "ssl_keyfile": info["ssl_pk"],
716 "ssl_certfile": info["ssl_cert"],
717 "ssl_ca_certs": info["ssl_chain"],
718 # "ssl_keyfile_password": ssl_keyfile_password
719 # "ssl_version": ssl_version
720 # "ssl_cert_reqs": ssl_cert_reqs
721 # "ssl_ciphers": ssl_ciphers
722 }
723 )
724 # Override or add custom args
725 custom_args = util.get_dict_value(config, "server_args", as_dict=True)
726 server_args.update(custom_args)
727
728 version = f"uvicorn/{uvicorn.__version__}"
729 version = f"{util.public_wsgidav_info} {version} {util.public_python_info}"
730 _logger.info(f"Running {version} ...")
731
732 uvicorn.run(app, **server_args)
733
734
735def _run_wsgiref(app, config, _server):
736 """Run WsgiDAV using wsgiref.simple_server (https://docs.python.org/3/library/wsgiref.html)."""
737 from wsgiref.simple_server import WSGIRequestHandler, make_server
738
739 version = WSGIRequestHandler.server_version
740 version = f"{util.public_wsgidav_info} {version}" # {util.public_python_info}"
741 _logger.info(f"Running {version} ...")
742
743 _logger.warning(
744 "WARNING: This single threaded server (wsgiref) is not meant for production."
745 )
746 WSGIRequestHandler.server_version = version
747 httpd = make_server(config["host"], config["port"], app)
748 # httpd.RequestHandlerClass.server_version = version
749 try:
750 httpd.serve_forever()
751 except KeyboardInterrupt:
752 _logger.warning("Caught Ctrl-C, shutting down...")
753 return
754
755
756SUPPORTED_SERVERS = {
757 "cheroot": _run_cheroot,
758 "ext-wsgiutils": _run_ext_wsgiutils,
759 "gevent": _run_gevent,
760 "gunicorn": _run_gunicorn,
761 "paste": _run_paste,
762 "uvicorn": _run_uvicorn,
763 "wsgiref": _run_wsgiref,
764}
765
766
767def run():
768 cli_opts, config = _init_config()
769
770 # util.init_logging(config) # now handled in constructor:
771 config["logging"]["enable"] = True
772
773 info = _get_common_info(config)
774
775 app = WsgiDAVApp(config)
776
777 server = config["server"]
778 handler = SUPPORTED_SERVERS.get(server)
779 if not handler:
780 raise RuntimeError(
781 "Unsupported server type {!r} (expected {!r})".format(
782 server, "', '".join(SUPPORTED_SERVERS.keys())
783 )
784 )
785
786 if not use_lxml and config["verbose"] >= 3:
787 _logger.warning(
788 "Could not import lxml: using xml instead (up to 10% slower). "
789 "Consider `pip install lxml`(see https://pypi.python.org/pypi/lxml)."
790 )
791
792 if cli_opts["browse"]:
793 BROWSE_DELAY = 2.0
794
795 def _worker():
796 url = info["url"]
797 url = url.replace("0.0.0.0", "127.0.0.1")
798 _logger.info(f"Starting browser on {url} ...")
799 webbrowser.open(url)
800
801 Timer(BROWSE_DELAY, _worker).start()
802
803 handler(app, config, server)
804 return
805
806
807if __name__ == "__main__":
808 # Just in case...
809 from multiprocessing import freeze_support
810
811 freeze_support()
812
813 run()