This commit is contained in:
Kovid Goyal 2017-06-26 21:01:47 +05:30
parent 775a946ac3
commit d983025899
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C

View File

@ -1,7 +1,6 @@
#!/usr/bin/env python2 #!/usr/bin/env python2
# vim:fileencoding=utf-8 # vim:fileencoding=utf-8
from __future__ import (unicode_literals, division, absolute_import, from __future__ import (unicode_literals, division, absolute_import, print_function)
print_function)
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
@ -49,6 +48,8 @@ def daemonize(): # {{{
# Redirect standard file descriptors. # Redirect standard file descriptors.
plugins['speedup'][0].detach(os.devnull) plugins['speedup'][0].detach(os.devnull)
# }}} # }}}
@ -65,7 +66,12 @@ class Server(object):
plugins = [] plugins = []
if opts.use_bonjour: if opts.use_bonjour:
plugins.append(BonJour()) plugins.append(BonJour())
self.loop = ServerLoop(create_http_handler(self.handler.dispatch), opts=opts, log=log, access_log=access_log, plugins=plugins) self.loop = ServerLoop(
create_http_handler(self.handler.dispatch),
opts=opts,
log=log,
access_log=access_log,
plugins=plugins)
self.handler.set_log(self.loop.log) self.handler.set_log(self.loop.log)
self.handler.set_jobs_manager(self.loop.jobs_manager) self.handler.set_jobs_manager(self.loop.jobs_manager)
self.serve_forever = self.loop.serve_forever self.serve_forever = self.loop.serve_forever
@ -74,6 +80,7 @@ class Server(object):
from calibre.utils.rapydscript import compile_srv from calibre.utils.rapydscript import compile_srv
compile_srv() compile_srv()
# Manage users CLI {{{ # Manage users CLI {{{
@ -86,7 +93,8 @@ def manage_users(path=None):
prints(prompt, end=' ') prints(prompt, end=' ')
return raw_input().decode(enc) return raw_input().decode(enc)
def choice(question=_('What do you want to do?'), choices=(), default=None, banner=''): def choice(
question=_('What do you want to do?'), choices=(), default=None, banner=''):
prints(banner) prints(banner)
for i, choice in enumerate(choices): for i, choice in enumerate(choices):
prints('%d)' % (i + 1), choice) prints('%d)' % (i + 1), choice)
@ -94,7 +102,8 @@ def manage_users(path=None):
while True: while True:
prompt = question + ' [1-%d]:' % len(choices) prompt = question + ' [1-%d]:' % len(choices)
if default is not None: if default is not None:
prompt = question + ' [1-%d %s: %d]' % (len(choices), _('default'), default+1) prompt = question + ' [1-%d %s: %d]' % (
len(choices), _('default'), default + 1)
reply = get_input(prompt) reply = get_input(prompt)
if not reply and default is not None: if not reply and default is not None:
reply = str(default + 1) reply = str(default + 1)
@ -128,16 +137,20 @@ def manage_users(path=None):
def validate(username): def validate(username):
if not m.has_user(username): if not m.has_user(username):
return _('The username %s does not exist') % username return _('The username %s does not exist') % username
return get_valid(_('Enter the username'), validate) return get_valid(_('Enter the username'), validate)
def get_pass(username): def get_pass(username):
while True: while True:
from getpass import getpass from getpass import getpass
one = getpass(_('Enter the new password for %s: ') % username).decode(enc) one = getpass(
_('Enter the new password for %s: ') % username).decode(enc)
if not one: if not one:
prints(_('Empty passwords are not allowed')) prints(_('Empty passwords are not allowed'))
continue continue
two = getpass(_('Re-enter the new password for %s, to verify: ') % username).decode(enc) two = getpass(
_('Re-enter the new password for %s, to verify: ') % username
).decode(enc)
if one != two: if one != two:
prints(_('Passwords do not match')) prints(_('Passwords do not match'))
continue continue
@ -154,7 +167,8 @@ def manage_users(path=None):
def remove_user(): def remove_user():
un = get_valid_user() un = get_valid_user()
if get_input((_('Are you sure you want to remove the user %s?') % un) + ' [y/n]:') != 'y': if get_input((_('Are you sure you want to remove the user %s?') % un) +
' [y/n]:') != 'y':
raise SystemExit(0) raise SystemExit(0)
m.remove_user(un) m.remove_user(un)
prints(_('User %s successfully removed!') % un) prints(_('User %s successfully removed!') % un)
@ -182,20 +196,27 @@ def manage_users(path=None):
if r is None: if r is None:
raise SystemExit('The user {} does not exist'.format(username)) raise SystemExit('The user {} does not exist'.format(username))
if r['allowed_library_names']: if r['allowed_library_names']:
prints(_('{} is currently only allowed to access the libraries named: {}').format( prints(
username, ', '.join(r['allowed_library_names']))) _('{} is currently only allowed to access the libraries named: {}')
.format(username, ', '.join(r['allowed_library_names'])))
if r['blocked_library_names']: if r['blocked_library_names']:
prints(_('{} is currently not allowed to access the libraries named: {}').format( prints(
username, ', '.join(r['blocked_library_names']))) _('{} is currently not allowed to access the libraries named: {}')
.format(username, ', '.join(r['blocked_library_names'])))
if r['library_restrictions']: if r['library_restrictions']:
prints(_('{} has the following additional per-library restrictions:').format(username)) prints(
_('{} has the following additional per-library restrictions:')
.format(username))
for k, v in r['library_restrictions'].iteritems(): for k, v in r['library_restrictions'].iteritems():
prints(k + ':', v) prints(k + ':', v)
else: else:
prints(_('{} has the no additional per-library restrictions')) prints(_('{} has the no additional per-library restrictions'))
c = choice(choices=[ c = choice(
_('Allow access to all libraries'), _('Allow access to only specified libraries'), choices=[
_('Allow access to all, except specified libraries'), _('Change per-library restrictions'), _('Allow access to all libraries'),
_('Allow access to only specified libraries'),
_('Allow access to all, except specified libraries'),
_('Change per-library restrictions'),
_('Cancel')]) _('Cancel')])
if c == 0: if c == 0:
m.update_user_restrictions(username, {}) m.update_user_restrictions(username, {})
@ -204,7 +225,9 @@ def manage_users(path=None):
library = get_input(_('Enter the name of the library:')) library = get_input(_('Enter the name of the library:'))
if not library: if not library:
break break
prints(_('Enter a search expression, access will be granted only to books matching this expression.' prints(
_(
'Enter a search expression, access will be granted only to books matching this expression.'
' An empty expression will grant access to all books.')) ' An empty expression will grant access to all books.'))
plr = get_input(_('Search expression:')) plr = get_input(_('Search expression:'))
if plr: if plr:
@ -227,25 +250,37 @@ def manage_users(path=None):
def edit_user(username=None): def edit_user(username=None):
username = username or get_valid_user() username = username or get_valid_user()
c = choice(choices=[ c = choice(
choices=[
_('Show password for {}').format(username), _('Show password for {}').format(username),
_('Change password for {}').format(username), _('Change password for {}').format(username),
_('Change read/write permission for {}').format(username), _('Change read/write permission for {}').format(username),
_('Change the libraries {} is allowed to access').format(username), _('Change the libraries {} is allowed to access').format(username),
_('Cancel'), _('Cancel'), ],
], banner='\n' + _('{} has {} access').format( banner='\n' + _('{} has {} access').format(
username, _('readonly') if m.is_readonly(username) else _('read-write')) username,
) _('readonly') if m.is_readonly(username) else _('read-write')))
print() print()
if c > 3: if c > 3:
actions.append(toplevel) actions.append(toplevel)
return return
{0: show_password, 1: change_password, 2: change_readonly, 3: change_restriction}[c](username) {
0: show_password,
1: change_password,
2: change_readonly,
3: change_restriction}[c](username)
actions.append(partial(edit_user, username=username)) actions.append(partial(edit_user, username=username))
def toplevel(): def toplevel():
{0:add_user, 1:edit_user, 2:remove_user, 3:lambda: None}[choice(choices=[ {
_('Add a new user'), _('Edit an existing user'), _('Remove a user'), 0: add_user,
1: edit_user,
2: remove_user,
3: lambda: None}[choice(
choices=[
_('Add a new user'),
_('Edit an existing user'),
_('Remove a user'),
_('Cancel')])]() _('Cancel')])]()
actions = [toplevel] actions = [toplevel]
@ -256,46 +291,63 @@ def manage_users(path=None):
# }}} # }}}
def create_option_parser(): def create_option_parser():
parser=opts_to_parser('%prog '+ _( parser = opts_to_parser(
'%prog ' + _(
'''[options] [path to library folder...] '''[options] [path to library folder...]
Start the calibre Content server. The calibre Content server exposes your Start the calibre Content server. The calibre Content server exposes your
calibre libraries over the internet. You can specify the path to the library calibre libraries over the internet. You can specify the path to the library
folders as arguments to %prog. If you do not specify any paths, all the folders as arguments to %prog. If you do not specify any paths, all the
libraries that the main calibre program knows about will be used. libraries that the main calibre program knows about will be used.
''' '''))
parser.add_option(
'--log',
default=None,
help=_(
'Path to log file for server log. This log contains server information and errors, not access logs. By default it is written to stdout.'
)) ))
parser.add_option( parser.add_option(
'--log', default=None, '--access-log',
help=_('Path to log file for server log. This log contains server information and errors, not access logs. By default it is written to stdout.')) default=None,
parser.add_option( help=_(
'--access-log', default=None, 'Path to the access log file. This log contains information'
help=_('Path to the access log file. This log contains information'
' about clients connecting to the server and making requests. By' ' about clients connecting to the server and making requests. By'
' default no access logging is done.')) ' default no access logging is done.'))
if not iswindows and not isosx: if not iswindows and not isosx:
# Does not work on macOS because if we fork() we cannot connect to Core # Does not work on macOS because if we fork() we cannot connect to Core
# Serives which is needed by the QApplication() constructor, which in # Serives which is needed by the QApplication() constructor, which in
# turn is needed by ensure_app() # turn is needed by ensure_app()
parser.add_option('--daemonize', default=False, action='store_true',
help=_('Run process in background as a daemon.'))
parser.add_option('--pidfile', default=None,
help=_('Write process PID to the specified file'))
parser.add_option( parser.add_option(
'--auto-reload', default=False, action='store_true', '--daemonize',
help=_('Automatically reload server when source code changes. Useful' default=False,
action='store_true',
help=_('Run process in background as a daemon.'))
parser.add_option(
'--pidfile', default=None, help=_('Write process PID to the specified file'))
parser.add_option(
'--auto-reload',
default=False,
action='store_true',
help=_(
'Automatically reload server when source code changes. Useful'
' for development. You should also specify a small value for the' ' for development. You should also specify a small value for the'
' shutdown timeout.')) ' shutdown timeout.'))
parser.add_option( parser.add_option(
'--manage-users', default=False, action='store_true', '--manage-users',
help=_('Manage the database of users allowed to connect to this server.' default=False,
action='store_true',
help=_(
'Manage the database of users allowed to connect to this server.'
' See also the %s option.') % '--userdb') ' See also the %s option.') % '--userdb')
parser.get_option('--userdb').help = _( parser.get_option('--userdb').help = _(
'Path to the user database to use for authentication. The database' 'Path to the user database to use for authentication. The database'
' is a SQLite file. To create it use {0}. You can read more' ' is a SQLite file. To create it use {0}. You can read more'
' about managing users at: {1}').format( ' about managing users at: {1}'
'--manage-users', localize_user_manual_link( ).format(
'--manage-users',
localize_user_manual_link(
'https://manual.calibre-ebook.com/server.html#managing-user-accounts-from-the-command-line-only' 'https://manual.calibre-ebook.com/server.html#managing-user-accounts-from-the-command-line-only'
)) ))
@ -308,12 +360,12 @@ option_parser = create_option_parser
def ensure_single_instance(): def ensure_single_instance():
if b'CALIBRE_NO_SI_DANGER_DANGER' not in os.environ and not singleinstance('db'): if b'CALIBRE_NO_SI_DANGER_DANGER' not in os.environ and not singleinstance('db'):
ext = '.exe' if iswindows else '' ext = '.exe' if iswindows else ''
raise SystemExit(_( raise SystemExit(
_(
'Another calibre program such as another instance of {} or the main' 'Another calibre program such as another instance of {} or the main'
' calibre program is running. Having multiple programs that can make' ' calibre program is running. Having multiple programs that can make'
' changes to a calibre library running at the same time is not supported.' ' changes to a calibre library running at the same time is not supported.'
).format('calibre-server' + ext) ).format('calibre-server' + ext))
)
def main(args=sys.argv): def main(args=sys.argv):
@ -338,7 +390,8 @@ def main(args=sys.argv):
if opts.auto_reload: if opts.auto_reload:
if getattr(opts, 'daemonize', False): if getattr(opts, 'daemonize', False):
raise SystemExit('Cannot specify --auto-reload and --daemonize at the same time') raise SystemExit(
'Cannot specify --auto-reload and --daemonize at the same time')
from calibre.srv.auto_reload import auto_reload, NoAutoReload from calibre.srv.auto_reload import auto_reload, NoAutoReload
try: try:
from calibre.utils.logging import default_log from calibre.utils.logging import default_log
@ -350,7 +403,9 @@ def main(args=sys.argv):
server = Server(libraries, opts) server = Server(libraries, opts)
if getattr(opts, 'daemonize', False): if getattr(opts, 'daemonize', False):
if not opts.log and not iswindows: if not opts.log and not iswindows:
raise SystemExit('In order to daemonize you must specify a log file, you can use /dev/stdout to log to screen even as a daemon') raise SystemExit(
'In order to daemonize you must specify a log file, you can use /dev/stdout to log to screen even as a daemon'
)
daemonize() daemonize()
if opts.pidfile: if opts.pidfile:
with lopen(opts.pidfile, 'wb') as f: with lopen(opts.pidfile, 'wb') as f: