%PDF- %PDF-
Direktori : /lib/calibre/calibre/srv/ |
Current File : //lib/calibre/calibre/srv/opts.py |
#!/usr/bin/env python3 __license__ = 'GPL v3' __copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>' import errno, os, numbers from collections import namedtuple, OrderedDict from operator import attrgetter from functools import partial from calibre.constants import config_dir from calibre.utils.lock import ExclusiveFile from polyglot.builtins import itervalues from itertools import zip_longest Option = namedtuple('Option', 'name default longdoc shortdoc choices') class Choices(frozenset): def __new__(cls, *args): self = super().__new__(cls, args) self.default = args[0] return self raw_options = ( _('Path to the SSL certificate file'), 'ssl_certfile', None, None, _('Path to the SSL private key file'), 'ssl_keyfile', None, None, _('Time (in seconds) after which an idle connection is closed'), 'timeout', 120.0, None, _('Time (in seconds) to wait for a response from the server when making queries'), 'ajax_timeout', 60.0, None, _('Total time in seconds to wait for clean shutdown'), 'shutdown_timeout', 5.0, None, _('Socket pre-allocation, for example, with systemd socket activation'), 'allow_socket_preallocation', True, None, _('Max. size of single HTTP header (in KB)'), 'max_header_line_size', 8.0, None, _('Max. allowed size for files uploaded to the server (in MB)'), 'max_request_body_size', 500.0, None, _('Minimum size for which responses use data compression (in bytes)'), 'compress_min_size', 1024, None, _('Number of worker threads used to process requests'), 'worker_count', 10, None, _('Maximum number of worker processes'), 'max_jobs', 0, _('Worker processes are launched as needed and used for large jobs such as preparing' ' a book for viewing, adding books, converting, etc. Normally, the max.' ' number of such processes is based on the number of CPU cores. You can' ' control it by this setting.'), _('Maximum time for worker processes'), 'max_job_time', 60, _('Maximum amount of time worker processes are allowed to run (in minutes). Set' ' to zero for no limit.'), _('The port on which to listen for connections'), 'port', 8080, None, _('A prefix to prepend to all URLs'), 'url_prefix', None, _('Useful if you wish to run this server behind a reverse proxy. For example use, /calibre as the URL prefix.'), _('Number of books to show in a single page'), 'num_per_page', 50, _('The number of books to show in a single page in the browser.'), _('Advertise OPDS feeds via BonJour'), 'use_bonjour', True, _('Advertise the OPDS feeds via the BonJour service, so that OPDS based' ' reading apps can detect and connect to the server automatically.'), _('Maximum number of books in OPDS feeds'), 'max_opds_items', 30, _('The maximum number of books that the server will return in a single' ' OPDS acquisition feed.'), _('Maximum number of ungrouped items in OPDS feeds'), 'max_opds_ungrouped_items', 100, _('Group items in categories such as author/tags by first letter when' ' there are more than this number of items. Set to zero to disable.'), _('The interface on which to listen for connections'), 'listen_on', '0.0.0.0', _('The default is to listen on all available IPv4 interfaces. You can change this to, for' ' example, "127.0.0.1" to only listen for connections from the local machine, or' ' to "::" to listen to all incoming IPv6 and IPv4 connections.'), _('Fallback to auto-detected interface'), 'fallback_to_detected_interface', True, _('If for some reason the server is unable to bind to the interface specified in' ' the listen_on option, then it will try to detect an interface that connects' ' to the outside world and bind to that.'), _('Zero copy file transfers for increased performance'), 'use_sendfile', True, _('This will use zero-copy in-kernel transfers when sending files over the network,' ' increasing performance. However, it can cause corrupted file transfers on some' ' broken filesystems. If you experience corrupted file transfers, turn it off.'), _('Max. log file size (in MB)'), 'max_log_size', 20, _('The maximum size of log files, generated by the server. When the log becomes larger' ' than this size, it is automatically rotated. Set to zero to disable log rotation.'), _('Log HTTP 404 (Not Found) requests'), 'log_not_found', True, _('Normally, the server logs all HTTP requests for resources that are not found.' ' This can generate a lot of log spam, if your server is targeted by bots.' ' Use this option to turn it off.'), _('Password based authentication to access the server'), 'auth', False, _('Normally, the server is unrestricted, allowing anyone to access it. You can' ' restrict access to predefined users with this option.'), _('Allow un-authenticated local connections to make changes'), 'local_write', False, _('Normally, if you do not turn on authentication, the server operates in' ' read-only mode, so as to not allow anonymous users to make changes to your' ' calibre libraries. This option allows anybody connecting from the same' ' computer as the server is running on to make changes. This is useful' ' if you want to run the server without authentication but still' ' use calibredb to make changes to your calibre libraries. Note that' ' turning on this option means any program running on the computer' ' can make changes to your calibre libraries.'), _('Allow un-authenticated connections from specific IP addresses to make changes'), 'trusted_ips', None, _('Normally, if you do not turn on authentication, the server operates in' ' read-only mode, so as to not allow anonymous users to make changes to your' ' calibre libraries. This option allows anybody connecting from the specified' ' IP addresses to make changes. Must be a comma separated list of address or network specifications.' ' This is useful if you want to run the server without authentication but still' ' use calibredb to make changes to your calibre libraries. Note that' ' turning on this option means anyone connecting from the specified IP addresses' ' can make changes to your calibre libraries.'), _('Path to user database'), 'userdb', None, _('Path to a file in which to store the user and password information. Normally a' ' file in the calibre configuration folder is used.'), _('Choose the type of authentication used'), 'auth_mode', Choices('auto', 'basic', 'digest'), _('Set the HTTP authentication mode used by the server. Set to "basic" if you are' ' putting this server behind an SSL proxy. Otherwise, leave it as "auto", which' ' will use "basic" if SSL is configured otherwise it will use "digest".'), _('Ban IP addresses that have repeated login failures'), 'ban_for', 0, _('Temporarily bans access for IP addresses that have repeated login failures for the' ' specified number of minutes. Useful to prevent attempts at guessing passwords. If' ' set to zero, no banning is done.'), _('Number of login failures for ban'), 'ban_after', 5, _('The number of login failures after which an IP address is banned'), _('Ignored user-defined metadata fields'), 'ignored_fields', None, _('Comma separated list of user-defined metadata fields that will not be displayed' ' by the Content server in the /opds and /mobile views. For example: {}').format( 'my_rating,my_tags'), _('Restrict displayed user-defined fields'), 'displayed_fields', None, _('Comma separated list of user-defined metadata fields that will be displayed' ' by the Content server in the /opds and /mobile views. If you specify this' ' option, any fields not in this list will not be displayed. For example: {}').format( 'my_rating,my_tags'), _('Choose the default book list mode'), 'book_list_mode', Choices('cover_grid', 'details_list', 'custom_list'), _('Set the default book list mode that will be used for new users. Individual users' ' can override the default in their own settings. The default is to use a cover grid.'), ) assert len(raw_options) % 4 == 0 options = [] def grouper(n, iterable, fillvalue=None): "grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx" args = [iter(iterable)] * n return zip_longest(*args, fillvalue=fillvalue) for shortdoc, name, default, doc in grouper(4, raw_options): choices = None if isinstance(default, Choices): choices = sorted(default) default = default.default options.append(Option(name, default, doc, shortdoc, choices)) options = OrderedDict([(o.name, o) for o in sorted(options, key=attrgetter('name'))]) del raw_options class Options: __slots__ = tuple(name for name in options) def __init__(self, **kwargs): for opt in itervalues(options): setattr(self, opt.name, kwargs.get(opt.name, opt.default)) def opt_to_cli_help(opt): ans = opt.shortdoc if not ans.endswith('.'): ans += '.' if opt.longdoc: ans += '\n\t' + opt.longdoc return ans def bool_callback(option, opt_str, value, parser, *args, **kwargs): setattr(parser.values, option.dest, opt_str.startswith('--enable-')) def boolean_option(add_option, opt): name = opt.name.replace('_', '-') help = opt_to_cli_help(opt) help += '\n' + (_('By default, this option is enabled.') if opt.default else _('By default, this option is disabled.')) add_option('--enable-' + name, '--disable-' + name, action='callback', callback=bool_callback, help=help) def opts_to_parser(usage): from calibre.utils.config import OptionParser parser = OptionParser(usage) for opt in itervalues(options): add_option = partial(parser.add_option, dest=opt.name, help=opt_to_cli_help(opt), default=opt.default) if opt.default is True or opt.default is False: boolean_option(add_option, opt) elif opt.choices: name = '--' + opt.name.replace('_', '-') add_option(name, choices=opt.choices) else: name = '--' + opt.name.replace('_', '-') otype = 'string' if isinstance(opt.default, numbers.Number): otype = type(opt.default).__name__ add_option(name, type=otype) return parser DEFAULT_CONFIG = os.path.join(config_dir, 'server-config.txt') def parse_config_file(path=DEFAULT_CONFIG): try: with ExclusiveFile(path) as f: raw = f.read().decode('utf-8') except OSError as err: if err.errno != errno.ENOENT: raise raw = '' ans = {} for line in raw.splitlines(): line = line.strip() if line.startswith('#'): continue key, rest = line.partition(' ')[::2] opt = options.get(key) if opt is None: continue val = rest if isinstance(opt.default, bool): val = val.lower() in ('true', 'yes', 'y') elif isinstance(opt.default, numbers.Number): try: val = type(opt.default)(rest) except Exception: raise ValueError(f'The value for {key}: {rest} is not a valid number') elif opt.choices: if rest not in opt.choices: raise ValueError(f'The value for {key}: {rest} is not valid') ans[key] = val return Options(**ans) def write_config_file(opts, path=DEFAULT_CONFIG): changed = {name:getattr(opts, name) for name in options if getattr(opts, name) != options[name].default} lines = [] for name in sorted(changed): o = options[name] lines.append('# ' + o.shortdoc) if o.longdoc: lines.append('# ' + o.longdoc) lines.append(f'{name} {changed[name]}') raw = '\n'.join(lines).encode('utf-8') with ExclusiveFile(path) as f: f.truncate() f.write(raw) def server_config(refresh=False): if refresh or not hasattr(server_config, 'ans'): server_config.ans = parse_config_file() return server_config.ans def change_settings(**kwds): new_opts = {} opts = server_config() for name in options: if name in kwds: new_opts[name] = kwds[name] else: new_opts[name] = getattr(opts, name) new_opts = server_config.ans = Options(**new_opts) write_config_file(new_opts)