%PDF- %PDF-
| Direktori : /usr/lib/calibre/calibre/srv/ |
| Current File : //usr/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)