%PDF- %PDF-
| Direktori : /lib/calibre/calibre/srv/ |
| Current File : //lib/calibre/calibre/srv/handler.py |
#!/usr/bin/env python3
# License: GPLv3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net>
import json
from functools import partial
from importlib import import_module
from threading import Lock
from calibre.srv.auth import AuthController
from calibre.srv.errors import HTTPForbidden
from calibre.srv.library_broker import LibraryBroker, path_for_db
from calibre.srv.routes import Router
from calibre.srv.users import UserManager
from calibre.utils.date import utcnow
from calibre.utils.search_query_parser import ParseException
from polyglot.builtins import itervalues
class Context:
log = None
url_for = None
jobs_manager = None
CATEGORY_CACHE_SIZE = 25
SEARCH_CACHE_SIZE = 100
def __init__(self, libraries, opts, testing=False, notify_changes=None):
self.opts = opts
self.library_broker = libraries if isinstance(libraries, LibraryBroker) else LibraryBroker(libraries)
self.testing = testing
self.lock = Lock()
self.user_manager = UserManager(opts.userdb)
self.ignored_fields = frozenset(filter(None, (x.strip() for x in (opts.ignored_fields or '').split(','))))
self.displayed_fields = frozenset(filter(None, (x.strip() for x in (opts.displayed_fields or '').split(','))))
self._notify_changes = notify_changes
def notify_changes(self, library_path, change_event):
if self._notify_changes is not None:
self._notify_changes(library_path, change_event)
def start_job(self, name, module, func, args=(), kwargs=None, job_done_callback=None, job_data=None):
return self.jobs_manager.start_job(name, module, func, args, kwargs, job_done_callback, job_data)
def job_status(self, job_id):
return self.jobs_manager.job_status(job_id)
def abort_job(self, job_id):
return self.jobs_manager.abort_job(job_id)
def is_field_displayable(self, field):
if self.displayed_fields and field not in self.displayed_fields:
return False
return field not in self.ignored_fields
def init_session(self, endpoint, data):
pass
def finalize_session(self, endpoint, data, output):
pass
def get_library(self, request_data, library_id=None):
if not request_data.username:
return self.library_broker.get(library_id)
lf = partial(self.user_manager.allowed_library_names, request_data.username)
allowed_libraries = self.library_broker.allowed_libraries(lf)
if not allowed_libraries:
raise HTTPForbidden(f'The user {request_data.username} is not allowed to access any libraries on this server')
library_id = library_id or next(iter(allowed_libraries))
if library_id in allowed_libraries:
return self.library_broker.get(library_id)
raise HTTPForbidden(f'The user {request_data.username} is not allowed to access the library {library_id}')
def library_info(self, request_data):
if not request_data.username:
return self.library_broker.library_map, self.library_broker.default_library
lf = partial(self.user_manager.allowed_library_names, request_data.username)
allowed_libraries = self.library_broker.allowed_libraries(lf)
if not allowed_libraries:
raise HTTPForbidden(f'The user {request_data.username} is not allowed to access any libraries on this server')
return dict(allowed_libraries), next(iter(allowed_libraries))
def restriction_for(self, request_data, db):
return self.user_manager.library_restriction(request_data.username, path_for_db(db))
def has_id(self, request_data, db, book_id):
restriction = self.restriction_for(request_data, db)
if restriction:
try:
return book_id in db.search('', restriction=restriction)
except ParseException:
return False
return db.has_id(book_id)
def get_allowed_book_ids_from_restriction(self, request_data, db):
restriction = self.restriction_for(request_data, db)
return frozenset(db.search('', restriction=restriction)) if restriction else None
def allowed_book_ids(self, request_data, db):
try:
ans = self.get_allowed_book_ids_from_restriction(request_data, db)
except ParseException:
return frozenset()
if ans is None:
ans = db.all_book_ids()
return ans
def check_for_write_access(self, request_data):
if not request_data.username:
if request_data.is_trusted_ip:
return
raise HTTPForbidden('Anonymous users are not allowed to make changes')
if self.user_manager.is_readonly(request_data.username):
raise HTTPForbidden(f'The user {request_data.username} does not have permission to make changes')
def get_effective_book_ids(self, db, request_data, vl, report_parse_errors=False):
try:
return db.books_in_virtual_library(vl, self.restriction_for(request_data, db))
except ParseException:
if report_parse_errors:
raise
return frozenset()
def get_categories(self, request_data, db, sort='name', first_letter_sort=True,
vl='', report_parse_errors=False):
restrict_to_ids = self.get_effective_book_ids(db, request_data, vl,
report_parse_errors=report_parse_errors)
key = restrict_to_ids, sort, first_letter_sort
with self.lock:
cache = self.library_broker.category_caches[db.server_library_id]
old = cache.pop(key, None)
if old is None or old[0] <= db.last_modified():
categories = db.get_categories(book_ids=restrict_to_ids, sort=sort, first_letter_sort=first_letter_sort)
cache[key] = old = (utcnow(), categories)
if len(cache) > self.CATEGORY_CACHE_SIZE:
cache.popitem(last=False)
else:
cache[key] = old
return old[1]
def get_tag_browser(self, request_data, db, opts, render, vl=''):
restrict_to_ids = self.get_effective_book_ids(db, request_data, vl)
key = restrict_to_ids, opts
with self.lock:
cache = self.library_broker.category_caches[db.server_library_id]
old = cache.pop(key, None)
if old is None or old[0] <= db.last_modified():
categories = db.get_categories(book_ids=restrict_to_ids, sort=opts.sort_by, first_letter_sort=opts.collapse_model == 'first letter')
data = json.dumps(render(db, categories), ensure_ascii=False)
if isinstance(data, str):
data = data.encode('utf-8')
cache[key] = old = (utcnow(), data)
if len(cache) > self.CATEGORY_CACHE_SIZE:
cache.popitem(last=False)
else:
cache[key] = old
return old[1]
def search(self, request_data, db, query, vl='', report_restriction_errors=False):
try:
restrict_to_ids = self.get_effective_book_ids(db, request_data, vl, report_parse_errors=report_restriction_errors)
except ParseException:
try:
self.get_allowed_book_ids_from_restriction(request_data, db)
except ParseException as e:
return frozenset(), e
return frozenset(), None
query = query or ''
key = query, restrict_to_ids
with self.lock:
cache = self.library_broker.search_caches[db.server_library_id]
old = cache.pop(key, None)
if old is None or old[0] < db.clear_search_cache_count:
matches = db.search(query, book_ids=restrict_to_ids)
cache[key] = old = (db.clear_search_cache_count, matches)
if len(cache) > self.SEARCH_CACHE_SIZE:
cache.popitem(last=False)
else:
cache[key] = old
if report_restriction_errors:
return old[1], None
return old[1]
SRV_MODULES = ('ajax', 'books', 'cdb', 'code', 'content', 'legacy', 'opds', 'users_api', 'convert')
class Handler:
def __init__(self, libraries, opts, testing=False, notify_changes=None):
ctx = Context(libraries, opts, testing=testing, notify_changes=notify_changes)
self.auth_controller = None
if opts.auth:
has_ssl = opts.ssl_certfile is not None and opts.ssl_keyfile is not None
prefer_basic_auth = {'auto':has_ssl, 'basic':True}.get(opts.auth_mode, False)
self.auth_controller = AuthController(
user_credentials=ctx.user_manager, prefer_basic_auth=prefer_basic_auth, ban_time_in_minutes=opts.ban_for, ban_after=opts.ban_after)
self.router = Router(ctx=ctx, url_prefix=opts.url_prefix, auth_controller=self.auth_controller)
for module in SRV_MODULES:
module = import_module('calibre.srv.' + module)
self.router.load_routes(itervalues(vars(module)))
self.router.finalize()
self.router.ctx.url_for = self.router.url_for
self.dispatch = self.router.dispatch
def set_log(self, log):
self.router.ctx.log = log
if self.auth_controller is not None:
self.auth_controller.log = log
def set_jobs_manager(self, jobs_manager):
self.router.ctx.jobs_manager = jobs_manager
def close(self):
self.router.ctx.library_broker.close()
@property
def ctx(self):
return self.router.ctx