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 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 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 = "{} is not a directory".format(args.root_path)
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 += "\nPython from: {}".format(sys.executable)
243 else:
244 version_info = "{}".format(__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("Using default configuration file: {}".format(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(root_path)
351
352 if config["verbose"] >= 5:
353 # TODO: remove passwords from user_mapping
354 config_cleaned = util.purge_passwords(config)
355 print(
356 "Configuration({}):\n{}".format(
357 cli_opts["config_file"], pformat(config_cleaned)
358 )
359 )
360
361 if not config["provider_mapping"]:
362 parser.error("No DAV provider defined.")
363
364 # Quick-configuration of DomainController
365 auth = cli_opts.get("auth")
366 auth_conf = util.get_dict_value(config, "http_authenticator", as_dict=True)
367 if auth and auth_conf.get("domain_controller"):
368 parser.error(
369 "--auth option can only be used when no domain_controller is configured"
370 )
371
372 if auth == "anonymous":
373 if config["simple_dc"]["user_mapping"]:
374 parser.error(
375 "--auth=anonymous can only be used when no user_mapping is configured"
376 )
377 auth_conf.update(
378 {
379 "domain_controller": "wsgidav.dc.simple_dc.SimpleDomainController",
380 "accept_basic": True,
381 "accept_digest": True,
382 "default_to_digest": True,
383 }
384 )
385 config["simple_dc"]["user_mapping"] = {"*": True}
386 elif auth == "nt":
387 if config.get("nt_dc"):
388 parser.error(
389 "--auth=nt can only be used when no nt_dc settings are configured"
390 )
391 auth_conf.update(
392 {
393 "domain_controller": "wsgidav.dc.nt_dc.NTDomainController",
394 "accept_basic": True,
395 "accept_digest": False,
396 "default_to_digest": False,
397 }
398 )
399 config["nt_dc"] = {}
400 elif auth == "pam-login":
401 if config.get("pam_dc"):
402 parser.error(
403 "--auth=pam-login can only be used when no pam_dc settings are configured"
404 )
405 auth_conf.update(
406 {
407 "domain_controller": "wsgidav.dc.pam_dc.PAMDomainController",
408 "accept_basic": True,
409 "accept_digest": False,
410 "default_to_digest": False,
411 }
412 )
413 config["pam_dc"] = {"service": "login"}
414 # print(config)
415
416 # if cli_opts.get("reload"):
417 # print("Installing paste.reloader.", file=sys.stderr)
418 # from paste import reloader # @UnresolvedImport
419
420 # reloader.install()
421 # if config_file:
422 # # Add config file changes
423 # reloader.watch_file(config_file)
424 # # import pydevd
425 # # pydevd.settrace()
426
427 return cli_opts, config
428
429
430def _run_cheroot(app, config, _server):
431 """Run WsgiDAV using cheroot.server (https://cheroot.cherrypy.dev/)."""
432 try:
433 from cheroot import server, wsgi
434 except ImportError:
435 _logger.exception("Could not import Cheroot (https://cheroot.cherrypy.dev/).")
436 _logger.error("Try `pip install cheroot`.")
437 return False
438
439 version = wsgi.Server.version
440 version = f"WsgiDAV/{__version__} {version} Python {util.PYTHON_VERSION}"
441 wsgi.Server.version = version
442
443 info = _get_common_info(config)
444
445 # Support SSL
446 if info["use_ssl"]:
447 ssl_adapter = info["ssl_adapter"]
448 ssl_adapter = server.get_ssl_adapter_class(ssl_adapter)
449 wsgi.Server.ssl_adapter = ssl_adapter(
450 info["ssl_cert"], info["ssl_pk"], info["ssl_chain"]
451 )
452 _logger.info("SSL / HTTPS enabled. Adapter: {}".format(ssl_adapter))
453
454 _logger.info(f"Running {version}")
455 _logger.info(f"Serving on {info['url']} ...")
456
457 server_args = {
458 "bind_addr": (config["host"], config["port"]),
459 "wsgi_app": app,
460 "server_name": version,
461 # File Explorer needs lot of threads (see issue #149):
462 "numthreads": 50, # TODO: still required?
463 }
464 # Override or add custom args
465 custom_args = util.get_dict_value(config, "server_args", as_dict=True)
466 server_args.update(custom_args)
467
468 class PatchedServer(wsgi.Server):
469 STARTUP_NOTIFICATION_DELAY = 0.5
470
471 def serve(self, *args, **kwargs):
472 _logger.error("wsgi.Server.serve")
473 if startup_event and not startup_event.is_set():
474 Timer(self.STARTUP_NOTIFICATION_DELAY, startup_event.set).start()
475 _logger.error("wsgi.Server is ready")
476 return super().serve(*args, **kwargs)
477
478 # If the caller passed a startup event, monkey patch the server to set it
479 # when the request handler loop is entered
480 startup_event = config.get("startup_event")
481 if startup_event:
482 server = PatchedServer(**server_args)
483 else:
484 server = wsgi.Server(**server_args)
485
486 try:
487 server.start()
488 except KeyboardInterrupt:
489 _logger.warning("Caught Ctrl-C, shutting down...")
490 finally:
491 server.stop()
492
493 return
494
495
496def _run_ext_wsgiutils(app, config, _server):
497 """Run WsgiDAV using ext_wsgiutils_server from the wsgidav package."""
498 from wsgidav.server import ext_wsgiutils_server
499
500 _logger.warning(
501 "WARNING: This single threaded server (ext-wsgiutils) is not meant for production."
502 )
503 try:
504 ext_wsgiutils_server.serve(config, app)
505 except KeyboardInterrupt:
506 _logger.warning("Caught Ctrl-C, shutting down...")
507 return
508
509
510# def _run_flup(app, config, server):
511# """Run WsgiDAV using flup.server.fcgi (http://trac.saddi.com/flup/wiki/FlupServers)."""
512# try:
513# if server == "flup-fcgi":
514# from flup.server.fcgi import WSGIServer
515# from flup.server.fcgi import __version__ as flupver
516# elif server == "flup-fcgi-fork":
517# from flup.server.fcgi_fork import WSGIServer
518# from flup.server.fcgi_fork import __version__ as flupver
519# else:
520# raise ValueError
521# except ImportError:
522# _logger.exception(f"Could not import {server} (https://gunicorn.org).")
523# _logger.error("Try `pip install flup`.")
524# return False
525
526# version = f"{WSGIServer.__module__}/{flupver}"
527# version = f"WsgiDAV/{__version__} {version} Python {util.PYTHON_VERSION}"
528# _logger.info(f"Running {version} ...")
529
530# server = WSGIServer(
531# app,
532# bindAddress=(config["host"], config["port"]),
533# # debug=True,
534# )
535# try:
536# server.run()
537# except KeyboardInterrupt:
538# _logger.warning("Caught Ctrl-C, shutting down...")
539# return
540
541
542def _run_gevent(app, config, server):
543 """Run WsgiDAV using gevent if gevent (https://www.gevent.org).
544
545 See
546 https://github.com/gevent/gevent/blob/master/src/gevent/pywsgi.py#L1356
547 https://github.com/gevent/gevent/blob/master/src/gevent/server.py#L38
548 for more options.
549 """
550 try:
551 import gevent
552 import gevent.monkey
553 from gevent.pywsgi import WSGIServer
554 except ImportError:
555 _logger.exception("Could not import gevent (http://www.gevent.org).")
556 _logger.error("Try `pip install gevent`.")
557 return False
558
559 gevent.monkey.patch_all()
560
561 info = _get_common_info(config)
562 version = f"gevent/{gevent.__version__}"
563 version = f"WsgiDAV/{__version__} {version} Python {util.PYTHON_VERSION}"
564
565 # Override or add custom args
566 server_args = {
567 "wsgi_app": app,
568 "bind_addr": (config["host"], config["port"]),
569 }
570 custom_args = util.get_dict_value(config, "server_args", as_dict=True)
571 server_args.update(custom_args)
572
573 if info["use_ssl"]:
574 dav_server = WSGIServer(
575 server_args["bind_addr"],
576 app,
577 keyfile=info["ssl_pk"],
578 certfile=info["ssl_cert"],
579 ca_certs=info["ssl_chain"],
580 )
581 else:
582 dav_server = WSGIServer(server_args["bind_addr"], app)
583
584 # If the caller passed a startup event, monkey patch the server to set it
585 # when the request handler loop is entered
586 startup_event = config.get("startup_event")
587 if startup_event:
588
589 def _patched_start():
590 dav_server.start_accepting = org_start # undo the monkey patch
591 org_start()
592 _logger.info("gevent is ready")
593 startup_event.set()
594
595 org_start = dav_server.start_accepting
596 dav_server.start_accepting = _patched_start
597
598 _logger.info(f"Running {version}")
599 _logger.info(f"Serving on {info['url']} ...")
600 try:
601 gevent.spawn(dav_server.serve_forever())
602 except KeyboardInterrupt:
603 _logger.warning("Caught Ctrl-C, shutting down...")
604 return
605
606
607def _run_gunicorn(app, config, server):
608 """Run WsgiDAV using Gunicorn (https://gunicorn.org)."""
609 try:
610 import gunicorn.app.base
611 except ImportError:
612 _logger.exception("Could not import Gunicorn (https://gunicorn.org).")
613 _logger.error("Try `pip install gunicorn` (UNIX only).")
614 return False
615
616 info = _get_common_info(config)
617
618 class GunicornApplication(gunicorn.app.base.BaseApplication):
619 def __init__(self, app, options=None):
620 self.options = options or {}
621 self.application = app
622 super().__init__()
623
624 def load_config(self):
625 config = {
626 key: value
627 for key, value in self.options.items()
628 if key in self.cfg.settings and value is not None
629 }
630 for key, value in config.items():
631 self.cfg.set(key.lower(), value)
632
633 def load(self):
634 return self.application
635
636 # See https://docs.gunicorn.org/en/latest/settings.html
637 server_args = {
638 "bind": "{}:{}".format(config["host"], config["port"]),
639 "threads": 50,
640 "timeout": 1200,
641 }
642 if info["use_ssl"]:
643 server_args.update(
644 {
645 "keyfile": info["ssl_pk"],
646 "certfile": info["ssl_cert"],
647 "ca_certs": info["ssl_chain"],
648 # "ssl_version": ssl_version
649 # "cert_reqs": ssl_cert_reqs
650 # "ciphers": ssl_ciphers
651 }
652 )
653 # Override or add custom args
654 custom_args = util.get_dict_value(config, "server_args", as_dict=True)
655 server_args.update(custom_args)
656
657 version = f"gunicorn/{gunicorn.__version__}"
658 version = f"WsgiDAV/{__version__} {version} Python {util.PYTHON_VERSION}"
659 _logger.info(f"Running {version} ...")
660
661 GunicornApplication(app, server_args).run()
662
663
664def _run_paste(app, config, server):
665 """Run WsgiDAV using paste.httpserver, if Paste is installed.
666
667 See http://pythonpaste.org/modules/httpserver.html for more options
668 """
669 try:
670 from paste import httpserver
671 except ImportError:
672 _logger.exception(
673 "Could not import paste.httpserver (https://github.com/cdent/paste)."
674 )
675 _logger.error("Try `pip install paste`.")
676 return False
677
678 info = _get_common_info(config)
679
680 version = httpserver.WSGIHandler.server_version
681 version = f"WsgiDAV/{__version__} {version} Python {util.PYTHON_VERSION}"
682
683 # See http://pythonpaste.org/modules/httpserver.html for more options
684 server = httpserver.serve(
685 app,
686 host=config["host"],
687 port=config["port"],
688 server_version=version,
689 # This option enables handling of keep-alive and expect-100:
690 protocol_version="HTTP/1.1",
691 start_loop=False,
692 )
693
694 if config["verbose"] >= 5:
695 __handle_one_request = server.RequestHandlerClass.handle_one_request
696
697 def handle_one_request(self):
698 __handle_one_request(self)
699 if self.close_connection == 1:
700 _logger.debug("HTTP Connection : close")
701 else:
702 _logger.debug("HTTP Connection : continue")
703
704 server.RequestHandlerClass.handle_one_request = handle_one_request
705
706 _logger.info(f"Running {version} ...")
707 host, port = server.server_address
708 if host == "0.0.0.0":
709 _logger.info(f"Serving on 0.0.0.0:{port} view at http://127.0.0.1:{port}")
710 else:
711 _logger.info(f"Serving on {info['url']}")
712
713 try:
714 server.serve_forever()
715 except KeyboardInterrupt:
716 _logger.warning("Caught Ctrl-C, shutting down...")
717 return
718
719
720def _run_uvicorn(app, config, server):
721 """Run WsgiDAV using Uvicorn (https://www.uvicorn.org)."""
722 try:
723 import uvicorn
724 except ImportError:
725 _logger.exception("Could not import Uvicorn (https://www.uvicorn.org).")
726 _logger.error("Try `pip install uvicorn`.")
727 return False
728
729 info = _get_common_info(config)
730
731 # See https://www.uvicorn.org/settings/
732 server_args = {
733 "interface": "wsgi",
734 "host": config["host"],
735 "port": config["port"],
736 # TODO: see _run_cheroot()
737 }
738 if info["use_ssl"]:
739 server_args.update(
740 {
741 "ssl_keyfile": info["ssl_pk"],
742 "ssl_certfile": info["ssl_cert"],
743 "ssl_ca_certs": info["ssl_chain"],
744 # "ssl_keyfile_password": ssl_keyfile_password
745 # "ssl_version": ssl_version
746 # "ssl_cert_reqs": ssl_cert_reqs
747 # "ssl_ciphers": ssl_ciphers
748 }
749 )
750 # Override or add custom args
751 custom_args = util.get_dict_value(config, "server_args", as_dict=True)
752 server_args.update(custom_args)
753
754 version = f"uvicorn/{uvicorn.__version__}"
755 version = f"WsgiDAV/{__version__} {version} Python {util.PYTHON_VERSION}"
756 _logger.info(f"Running {version} ...")
757
758 uvicorn.run(app, **server_args)
759
760
761def _run_wsgiref(app, config, _server):
762 """Run WsgiDAV using wsgiref.simple_server (https://docs.python.org/3/library/wsgiref.html)."""
763 from wsgiref.simple_server import WSGIRequestHandler, make_server
764
765 version = WSGIRequestHandler.server_version
766 version = f"WsgiDAV/{__version__} {version}" # Python {util.PYTHON_VERSION}"
767 _logger.info(f"Running {version} ...")
768
769 _logger.warning(
770 "WARNING: This single threaded server (wsgiref) is not meant for production."
771 )
772 WSGIRequestHandler.server_version = version
773 httpd = make_server(config["host"], config["port"], app)
774 # httpd.RequestHandlerClass.server_version = version
775 try:
776 httpd.serve_forever()
777 except KeyboardInterrupt:
778 _logger.warning("Caught Ctrl-C, shutting down...")
779 return
780
781
782SUPPORTED_SERVERS = {
783 "cheroot": _run_cheroot,
784 # "cherrypy": _run__cherrypy,
785 "ext-wsgiutils": _run_ext_wsgiutils,
786 # "flup-fcgi_fork": _run_flup,
787 # "flup-fcgi": _run_flup,
788 "gevent": _run_gevent,
789 "gunicorn": _run_gunicorn,
790 "paste": _run_paste,
791 "uvicorn": _run_uvicorn,
792 "wsgiref": _run_wsgiref,
793}
794
795
796def run():
797 cli_opts, config = _init_config()
798
799 util.init_logging(config)
800
801 info = _get_common_info(config)
802
803 app = WsgiDAVApp(config)
804
805 server = config["server"]
806 handler = SUPPORTED_SERVERS.get(server)
807 if not handler:
808 raise RuntimeError(
809 "Unsupported server type {!r} (expected {!r})".format(
810 server, "', '".join(SUPPORTED_SERVERS.keys())
811 )
812 )
813
814 if not use_lxml and config["verbose"] >= 3:
815 _logger.warning(
816 "Could not import lxml: using xml instead (up to 10% slower). "
817 "Consider `pip install lxml`(see https://pypi.python.org/pypi/lxml)."
818 )
819
820 if cli_opts["browse"]:
821 BROWSE_DELAY = 2.0
822
823 def _worker():
824 url = info["url"]
825 url = url.replace("0.0.0.0", "127.0.0.1")
826 _logger.info(f"Starting browser on {url} ...")
827 webbrowser.open(url)
828
829 Timer(BROWSE_DELAY, _worker).start()
830
831 handler(app, config, server)
832 return
833
834
835if __name__ == "__main__":
836 # Just in case...
837 from multiprocessing import freeze_support
838
839 freeze_support()
840
841 run()