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