def cdb_run(ctx, rd, which, version): try: m = module_for_cmd(which) except ImportError: raise HTTPNotFound('No module named: {}'.format(which)) if not getattr(m, 'readonly', False): ctx.check_for_write_access(rd) if getattr(m, 'version', 0) != int(version): raise HTTPNotFound(('The module {} is not available in version: {}.' 'Make sure the version of calibre used for the' ' server and calibredb match').format(which, version)) db = get_library_data(ctx, rd, strict_library_id=True)[0] if ctx.restriction_for(rd, db): raise HTTPForbidden('Cannot use the command-line db interface with a user who has per library restrictions') raw = rd.read() ct = rd.inheaders.get('Content-Type', all=True) try: if MSGPACK_MIME in ct: args = msgpack_loads(raw) elif 'application/json' in ct: args = json_loads(raw) else: raise HTTPBadRequest('Only JSON or msgpack requests are supported') except Exception: raise HTTPBadRequest('args are not valid encoded data') if getattr(m, 'needs_srv_ctx', False): args = [ctx] + list(args) try: result = m.implementation(db, partial(ctx.notify_changes, db.backend.library_path), *args) except Exception as err: import traceback return {'err': as_unicode(err), 'tb': traceback.format_exc()} return {'result': result}
def field_names(ctx, rd, field): ''' Get a list of all names for the specified field Optional: ?library_id=<default library> ''' db, library_id = get_library_data(ctx, rd)[:2] return tuple(db.all_field_names(field))
def book_manifest(ctx, rd, book_id, fmt): db, library_id = get_library_data(ctx, rd)[:2] force_reload = rd.query.get('force_reload') == '1' if plugin_for_input_format(fmt) is None: raise HTTPNotFound('The format %s cannot be viewed' % fmt.upper()) if book_id not in ctx.allowed_book_ids(rd, db): raise HTTPNotFound('No book with id: %s in library: %s' % (book_id, library_id)) with db.safe_read_lock: fm = db.format_metadata(book_id, fmt) if not fm: raise HTTPNotFound('No %s format for the book (id:%s) in the library: %s' % (fmt, book_id, library_id)) size, mtime = map(int, (fm['size'], time.mktime(fm['mtime'].utctimetuple())*10)) bhash = book_hash(db.library_id, book_id, fmt, size, mtime) with cache_lock: mpath = abspath(os.path.join(books_cache_dir(), 'f', bhash, 'calibre-book-manifest.json')) if force_reload: safe_remove(mpath, True) try: os.utime(mpath, None) with lopen(mpath, 'rb') as f: ans = jsonlib.load(f) ans['metadata'] = book_as_json(db, book_id) return ans except EnvironmentError as e: if e.errno != errno.ENOENT: raise x = failed_jobs.pop(bhash, None) if x is not None: return {'aborted':x[0], 'traceback':x[1], 'job_status':'finished'} job_id = queued_jobs.get(bhash) if job_id is None: job_id = queue_job(ctx, partial(db.copy_format_to, book_id, fmt), bhash, fmt, book_id, size, mtime) status, result, tb, aborted = ctx.job_status(job_id) return {'aborted': aborted, 'traceback':tb, 'job_status':status, 'job_id':job_id}
def more_books(ctx, rd): ''' Get more results from the specified search-query, which must be specified as JSON in the request body. Optional: ?num=50&library_id=<default library> ''' db, library_id = get_library_data(ctx, rd)[:2] try: num = int(rd.query.get('num', DEFAULT_NUMBER_OF_BOOKS)) except Exception: raise HTTPNotFound('Invalid number of books: %r' % rd.query.get('num')) try: search_query = load_json_file(rd.request_body_file) query, offset, sorts, orders = search_query['query'], search_query[ 'offset'], search_query['sort'], search_query['sort_order'] except KeyError as err: raise HTTPBadRequest('Search query missing key: %s' % as_unicode(err)) except Exception as err: raise HTTPBadRequest('Invalid query: %s' % as_unicode(err)) ans = {} with db.safe_read_lock: ans['search_result'] = search_result(ctx, rd, db, query, num, offset, sorts, orders) mdata = ans['metadata'] = {} for book_id in ans['search_result']['book_ids']: data = book_as_json(db, book_id) if data is not None: mdata[book_id] = data return ans
def conversion_status(ctx, rd, job_id): with cache_lock: job_status = conversion_jobs.get(job_id) if job_status is None: raise HTTPNotFound('No job with id: {}'.format(job_id)) job_status.last_check_at = monotonic() if job_status.running: percent, msg = job_status.current_status if rd.query.get('abort_job'): ctx.abort_job(job_id) return {'running': True, 'percent': percent, 'msg': msg} del conversion_jobs[job_id] try: ans = {'running': False, 'ok': job_status.ok, 'was_aborted': job_status.was_aborted, 'traceback': job_status.traceback, 'log': job_status.log} if job_status.ok: db, library_id = get_library_data(ctx, rd)[:2] if library_id != job_status.library_id: raise HTTPNotFound('job library_id does not match') fmt = job_status.output_path.rpartition('.')[-1] try: db.add_format(job_status.book_id, fmt, job_status.output_path) except NoSuchBook: raise HTTPNotFound( 'book_id {} not found in library'.format(job_status.book_id)) formats_added({job_status.book_id: (fmt,)}) ans['size'] = os.path.getsize(job_status.output_path) ans['fmt'] = fmt return ans finally: job_status.cleanup()
def get_books(ctx, rd): ''' Get books for the specified query Optional: ?library_id=<default library>&num=50&sort=timestamp.desc&search=''&vl='' ''' library_id, db, sorts, orders, vl = get_basic_query_data(ctx, rd) try: num = int(rd.query.get('num', rd.opts.num_per_page)) except Exception: raise HTTPNotFound('Invalid number of books: %r' % rd.query.get('num')) searchq = rd.query.get('search', '') db = get_library_data(ctx, rd)[0] ans = {} mdata = ans['metadata'] = {} with db.safe_read_lock: try: ans['search_result'] = search_result( ctx, rd, db, searchq, num, 0, ','.join(sorts), ','.join(orders), vl ) except ParseException as err: # This must not be translated as it is used by the front end to # detect invalid search expressions raise HTTPBadRequest('Invalid search expression: %s' % as_unicode(err)) for book_id in ans['search_result']['book_ids']: data = book_as_json(db, book_id) if data is not None: mdata[book_id] = data return ans
def mobile(ctx, rd): db, library_id, library_map, default_library = get_library_data(ctx, rd) try: start = max(1, int(rd.query.get('start', 1))) except ValueError: raise HTTPBadRequest('start is not an integer') try: num = max(0, int(rd.query.get('num', 25))) except ValueError: raise HTTPBadRequest('num is not an integer') search = rd.query.get('search') or '' with db.safe_read_lock: book_ids = ctx.search(rd, db, search) total = len(book_ids) ascending = rd.query.get('order', '').lower().strip() == 'ascending' sort_by = sanitize_sort_field_name(db.field_metadata, rd.query.get('sort') or 'date') try: book_ids = db.multisort([(sort_by, ascending)], book_ids) except Exception: sort_by = 'date' book_ids = db.multisort([(sort_by, ascending)], book_ids) books = [db.get_metadata(book_id) for book_id in book_ids[(start-1):(start-1)+num]] rd.outheaders['Last-Modified'] = http_date(timestampfromdt(db.last_modified())) order = 'ascending' if ascending else 'descending' q = {b'search':search.encode('utf-8'), b'order':bytes(order), b'sort':sort_by.encode('utf-8'), b'num':bytes(num), 'library_id':library_id} url_base = ctx.url_for('/mobile') + '?' + urlencode(q) lm = {k:v for k, v in library_map.iteritems() if k != library_id} return build_index(books, num, search, sort_by, order, start, total, url_base, db.field_metadata, ctx, lm, library_id)
def conversion_data(ctx, rd, book_id): from calibre.ebooks.conversion.config import ( NoSupportedInputFormats, get_input_format_for_book, get_sorted_output_formats) db = get_library_data(ctx, rd)[0] if not ctx.has_id(rd, db, book_id): raise BookNotFound(book_id, db) try: input_format, input_formats = get_input_format_for_book(db, book_id) except NoSupportedInputFormats: input_formats = [] else: if rd.query.get('input_fmt') and rd.query.get('input_fmt').lower() in input_formats: input_format = rd.query.get('input_fmt').lower() if input_format in input_formats: input_formats.remove(input_format) input_formats.insert(0, input_format) input_fmt = input_formats[0] if input_formats else 'epub' output_formats = get_sorted_output_formats(rd.query.get('output_fmt')) ans = { 'input_formats': [x.upper() for x in input_formats], 'output_formats': output_formats, 'profiles': profiles(), 'conversion_options': get_conversion_options(input_fmt, output_formats[0], book_id, db), 'title': db.field_for('title', book_id), 'authors': db.field_for('authors', book_id), 'book_id': book_id } return ans
def conversion_data(ctx, rd, book_id): from calibre.ebooks.conversion.config import (NoSupportedInputFormats, get_input_format_for_book, get_sorted_output_formats) db = get_library_data(ctx, rd)[0] if not ctx.has_id(rd, db, book_id): raise BookNotFound(book_id, db) try: input_format, input_formats = get_input_format_for_book(db, book_id) except NoSupportedInputFormats: input_formats = [] else: if rd.query.get('input_fmt') and rd.query.get( 'input_fmt').lower() in input_formats: input_format = rd.query.get('input_fmt').lower() if input_format in input_formats: input_formats.remove(input_format) input_formats.insert(0, input_format) input_fmt = input_formats[0] if input_formats else 'epub' output_formats = get_sorted_output_formats(rd.query.get('output_fmt')) ans = { 'input_formats': [x.upper() for x in input_formats], 'output_formats': output_formats, 'conversion_options': get_conversion_options(input_fmt, output_formats[0], book_id, db), 'title': db.field_for('title', book_id), 'authors': db.field_for('authors', book_id), 'book_id': book_id } return ans
def get_books(ctx, rd): ''' Get books for the specified query Optional: ?library_id=<default library>&num=50&sort=timestamp.desc&search='' ''' library_id, db, sorts, orders = get_basic_query_data(ctx, rd) try: num = int(rd.query.get('num', DEFAULT_NUMBER_OF_BOOKS)) except Exception: raise HTTPNotFound('Invalid number of books: %r' % rd.query.get('num')) searchq = rd.query.get('search', '') db = get_library_data(ctx, rd)[0] ans = {} mdata = ans['metadata'] = {} with db.safe_read_lock: try: ans['search_result'] = search_result(ctx, rd, db, searchq, num, 0, ','.join(sorts), ','.join(orders)) except ParseException as err: # This must not be translated as it is used by the front end to # detect invalid search expressions raise HTTPBadRequest('Invalid search expression: %s' % as_unicode(err)) for book_id in ans['search_result']['book_ids']: data = book_as_json(db, book_id) if data is not None: mdata[book_id] = data return ans
def more_books(ctx, rd): ''' Get more results from the specified search-query, which must be specified as JSON in the request body. Optional: ?num=50&library_id=<default library> ''' db, library_id = get_library_data(ctx, rd)[:2] try: num = int(rd.query.get('num', rd.opts.num_per_page)) except Exception: raise HTTPNotFound('Invalid number of books: %r' % rd.query.get('num')) try: search_query = load_json_file(rd.request_body_file) query, offset, sorts, orders, vl = search_query['query'], search_query[ 'offset' ], search_query['sort'], search_query['sort_order'], search_query['vl'] except KeyError as err: raise HTTPBadRequest('Search query missing key: %s' % as_unicode(err)) except Exception as err: raise HTTPBadRequest('Invalid query: %s' % as_unicode(err)) ans = {} with db.safe_read_lock: ans['search_result'] = search_result( ctx, rd, db, query, num, offset, sorts, orders, vl ) mdata = ans['metadata'] = {} for book_id in ans['search_result']['book_ids']: data = book_as_json(db, book_id) if data is not None: mdata[book_id] = data return ans
def start_conversion(ctx, rd, book_id): db, library_id = get_library_data(ctx, rd)[:2] if not ctx.has_id(rd, db, book_id): raise BookNotFound(book_id, db) data = json.loads(rd.request_body_file.read()) input_fmt = data['input_fmt'] job_id = queue_job(ctx, rd, library_id, db, input_fmt, book_id, data) return job_id
def field_names(ctx, rd, field): ''' Get a list of all names for the specified field Optional: ?library_id=<default library> ''' if field == 'languages': ans = all_lang_names() else: db, library_id = get_library_data(ctx, rd)[:2] ans = tuple(sorted(db.all_field_names(field), key=numeric_sort_key)) return ans
def tag_browser(ctx, rd): ''' Get the Tag Browser serialized as JSON Optional: ?library_id=<default library>&sort_tags_by=name&partition_method=first letter &collapse_at=25&dont_collapse=&hide_empty_categories= ''' db, library_id = get_library_data(ctx, rd)[:2] etag = '%s||%s||%s' % (db.last_modified(), rd.username, library_id) etag = hashlib.sha1(etag.encode('utf-8')).hexdigest() def generate(): db, library_id = get_library_data(ctx, rd)[:2] return json(ctx, rd, tag_browser, categories_as_json(ctx, rd, db)) return rd.etagged_dynamic_response(etag, generate)
def book_file(ctx, rd, book_id, fmt, size, mtime, name): db, library_id = get_library_data(ctx, rd)[:2] if not ctx.has_id(rd, db, book_id): raise BookNotFound(book_id, db) bhash = book_hash(db.library_id, book_id, fmt, size, mtime) base = abspath(os.path.join(books_cache_dir(), 'f')) mpath = abspath(os.path.join(base, bhash, name)) if not mpath.startswith(base): raise HTTPNotFound('No book file with hash: %s and name: %s' % (bhash, name)) try: return rd.filesystem_file_with_custom_etag(lopen(mpath, 'rb'), bhash, name) except EnvironmentError as e: if e.errno != errno.ENOENT: raise raise HTTPNotFound('No book file with hash: %s and name: %s' % (bhash, name))
def tag_browser(ctx, rd): ''' Get the Tag Browser serialized as JSON Optional: ?library_id=<default library>&sort_tags_by=name&partition_method=first letter &collapse_at=25&dont_collapse=&hide_empty_categories=&vl='' ''' db, library_id = get_library_data(ctx, rd)[:2] opts = categories_settings(rd.query, db) vl = rd.query.get('vl') or '' etag = cPickle.dumps([db.last_modified().isoformat(), rd.username, library_id, vl, list(opts)], -1) etag = hashlib.sha1(etag).hexdigest() def generate(): return json(ctx, rd, tag_browser, categories_as_json(ctx, rd, db, opts, vl)) return rd.etagged_dynamic_response(etag, generate)
def get_basic_query_data(ctx, rd): db, library_id, library_map, default_library = get_library_data(ctx, rd) skeys = db.field_metadata.sortable_field_keys() sorts, orders = [], [] for x in rd.query.get('sort', '').split(','): if x: s, o = x.rpartition('.')[::2] if o and not s: s, o = o, '' if o not in ('asc', 'desc'): o = 'asc' if s.startswith('_'): s = '#' + s[1:] s = sanitize_sort_field_name(db.field_metadata, s) if s in skeys: sorts.append(s), orders.append(o) if not sorts: sorts, orders = ['timestamp'], ['desc'] return library_id, db, sorts, orders, rd.query.get('vl') or ''
def cdb_run(ctx, rd, which, version): try: m = module_for_cmd(which) except ImportError: raise HTTPNotFound('No module named: {}'.format(which)) if not getattr(m, 'readonly', False): ctx.check_for_write_access(rd) if getattr(m, 'version', 0) != int(version): raise HTTPNotFound( ('The module {} is not available in version: {}.' 'Make sure the version of calibre used for the' ' server and calibredb match').format(which, version)) db = get_library_data(ctx, rd, strict_library_id=True)[0] if ctx.restriction_for(rd, db): raise HTTPForbidden( 'Cannot use the command-line db interface with a user who has per library restrictions' ) raw = rd.read() ct = rd.inheaders.get('Content-Type', all=True) ct = {x.lower().partition(';')[0] for x in ct} try: if MSGPACK_MIME in ct: args = msgpack_loads(raw) elif 'application/json' in ct: args = json_loads(raw) else: raise HTTPBadRequest('Only JSON or msgpack requests are supported') except Exception: raise HTTPBadRequest('args are not valid encoded data') if getattr(m, 'needs_srv_ctx', False): args = [ctx] + list(args) try: result = m.implementation( db, partial(ctx.notify_changes, db.backend.library_path), *args) except Exception as err: tb = '' if not getattr(err, 'suppress_traceback', False): import traceback tb = traceback.format_exc() return {'err': as_unicode(err), 'tb': tb} return {'result': result}
def mobile(ctx, rd): db, library_id, library_map, default_library = get_library_data(ctx, rd) try: start = max(1, int(rd.query.get('start', 1))) except ValueError: raise HTTPBadRequest('start is not an integer') try: num = max(0, int(rd.query.get('num', 25))) except ValueError: raise HTTPBadRequest('num is not an integer') search = rd.query.get('search') or '' with db.safe_read_lock: book_ids = ctx.search(rd, db, search) total = len(book_ids) ascending = rd.query.get('order', '').lower().strip() == 'ascending' sort_by = sanitize_sort_field_name(db.field_metadata, rd.query.get('sort') or 'date') try: book_ids = db.multisort([(sort_by, ascending)], book_ids) except Exception: sort_by = 'date' book_ids = db.multisort([(sort_by, ascending)], book_ids) books = [ db.get_metadata(book_id) for book_id in book_ids[(start - 1):(start - 1) + num] ] rd.outheaders['Last-Modified'] = http_date( timestampfromdt(db.last_modified())) order = 'ascending' if ascending else 'descending' q = { b'search': search.encode('utf-8'), b'order': order.encode('ascii'), b'sort': sort_by.encode('utf-8'), b'num': as_bytes(num), 'library_id': library_id } url_base = ctx.url_for('/mobile') + '?' + urlencode(q) lm = {k: v for k, v in iteritems(library_map) if k != library_id} return build_index(rd, books, num, search, sort_by, order, start, total, url_base, db.field_metadata, ctx, lm, library_id)
def conversion_status(ctx, rd, job_id): with cache_lock: job_status = conversion_jobs.get(job_id) if job_status is None: raise HTTPNotFound(f'No job with id: {job_id}') job_status.last_check_at = monotonic() if job_status.running: percent, msg = job_status.current_status if rd.query.get('abort_job'): ctx.abort_job(job_id) return {'running': True, 'percent': percent, 'msg': msg} del conversion_jobs[job_id] try: ans = { 'running': False, 'ok': job_status.ok, 'was_aborted': job_status.was_aborted, 'traceback': job_status.traceback, 'log': job_status.log } if job_status.ok: db, library_id = get_library_data(ctx, rd)[:2] if library_id != job_status.library_id: raise HTTPNotFound('job library_id does not match') fmt = job_status.output_path.rpartition('.')[-1] try: db.add_format(job_status.book_id, fmt, job_status.output_path) except NoSuchBook: raise HTTPNotFound( f'book_id {job_status.book_id} not found in library') formats_added({job_status.book_id: (fmt, )}) ans['size'] = os.path.getsize(job_status.output_path) ans['fmt'] = fmt return ans finally: job_status.cleanup()
def book_manifest(ctx, rd, book_id, fmt): db, library_id = get_library_data(ctx, rd)[:2] force_reload = rd.query.get('force_reload') == '1' if plugin_for_input_format(fmt) is None: raise HTTPNotFound('The format %s cannot be viewed' % fmt.upper()) if not ctx.has_id(rd, db, book_id): raise BookNotFound(book_id, db) with db.safe_read_lock: fm = db.format_metadata(book_id, fmt, allow_cache=False) if not fm: raise HTTPNotFound(f'No {fmt} format for the book (id:{book_id}) in the library: {library_id}') size, mtime = map(int, (fm['size'], time.mktime(fm['mtime'].utctimetuple())*10)) bhash = book_hash(db.library_id, book_id, fmt, size, mtime) with cache_lock: mpath = abspath(os.path.join(books_cache_dir(), 'f', bhash, 'calibre-book-manifest.json')) if force_reload: safe_remove(mpath, True) try: os.utime(mpath, None) with lopen(mpath, 'rb') as f: ans = jsonlib.load(f) ans['metadata'] = book_as_json(db, book_id) user = rd.username or None ans['last_read_positions'] = db.get_last_read_positions(book_id, fmt, user) if user else [] ans['annotations_map'] = db.annotations_map_for_book(book_id, fmt, user_type='web', user=user or '*') return ans except OSError as e: if e.errno != errno.ENOENT: raise x = failed_jobs.pop(bhash, None) if x is not None: return {'aborted':x[0], 'traceback':x[1], 'job_status':'finished'} job_id = queued_jobs.get(bhash) if job_id is None: job_id = queue_job(ctx, partial(db.copy_format_to, book_id, fmt), bhash, fmt, book_id, size, mtime) status, result, tb, aborted = ctx.job_status(job_id) return {'aborted': aborted, 'traceback':tb, 'job_status':status, 'job_id':job_id}
def __init__(self, ctx, rd): self.db, self.library_id, self.library_map, self.default_library = get_library_data( ctx, rd) self.ctx, self.rd = ctx, rd
def generate(): db, library_id = get_library_data(ctx, rd)[:2] return json(ctx, rd, tag_browser, categories_as_json(ctx, rd, db))
def __init__(self, ctx, rd): self.db, self.library_id, self.library_map, self.default_library = get_library_data(ctx, rd) self.ctx, self.rd = ctx, rd