def cdb_add_book(ctx, rd, job_id, add_duplicates, filename, library_id): ''' Add a file as a new book. The file contents must be in the body of the request. The response will also have the title/authors/languages read from the metadata of the file/filename. It will contain a `book_id` field specifying the id of the newly added book, or if add_duplicates is not specified and a duplicate was found, no book_id will be present. It will also return the value of `job_id` as the `id` field and `filename` as the `filename` field. ''' db = get_db(ctx, rd, library_id) if ctx.restriction_for(rd, db): raise HTTPForbidden('Cannot use the add book interface with a user who has per library restrictions') if not filename: raise HTTPBadRequest('An empty filename is not allowed') sfilename = sanitize_file_name_unicode(filename) fmt = os.path.splitext(sfilename)[1] fmt = fmt[1:] if fmt else None if not fmt: raise HTTPBadRequest('An filename with no extension is not allowed') if isinstance(rd.request_body_file, BytesIO): raise HTTPBadRequest('A request body containing the file data must be specified') add_duplicates = add_duplicates in ('y', '1') path = os.path.join(rd.tdir, sfilename) rd.request_body_file.name = path rd.request_body_file.seek(0) mi = get_metadata(rd.request_body_file, stream_type=fmt, use_libprs_metadata=True) rd.request_body_file.seek(0) ids, duplicates = db.add_books([(mi, {fmt: rd.request_body_file})], add_duplicates=add_duplicates) ans = {'title': mi.title, 'authors': mi.authors, 'languages': mi.languages, 'filename': filename, 'id': job_id} if ids: ans['book_id'] = ids[0] books_added(ids) return ans
def cdb_set_fields(ctx, rd, book_id, library_id): db = get_db(ctx, rd, library_id) if ctx.restriction_for(rd, db): raise HTTPForbidden('Cannot use the set fields interface with a user who has per library restrictions') data = load_payload_data(rd) try: changes, loaded_book_ids = data['changes'], frozenset(map(int, data.get('loaded_book_ids', ()))) all_dirtied = bool(data.get('all_dirtied')) if not isinstance(changes, dict): raise TypeError('changes must be a dict') except Exception: raise HTTPBadRequest( '''Data must be of the form {'changes': {'title': 'New Title', ...}, 'loaded_book_ids':[book_id1, book_id2, ...]'}''') dirtied = set() cdata = changes.pop('cover', False) if cdata is not False: if cdata is not None: try: cdata = standard_b64decode(cdata.split(',', 1)[-1].encode('ascii')) except Exception: raise HTTPBadRequest('Cover data is not valid base64 encoded data') try: fmt = what(None, cdata) except Exception: fmt = None if fmt not in ('jpeg', 'png'): raise HTTPBadRequest('Cover data must be either JPEG or PNG') dirtied |= db.set_cover({book_id: cdata}) for field, value in iteritems(changes): dirtied |= db.set_field(field, {book_id: value}) ctx.notify_changes(db.backend.library_path, metadata(dirtied)) all_ids = dirtied if all_dirtied else (dirtied & loaded_book_ids) all_ids |= {book_id} return {bid: book_as_json(db, bid) for bid in all_ids}
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 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.query)[: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 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 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 cdb_copy_to_library(ctx, rd, target_library_id, library_id): db_src = get_db(ctx, rd, library_id) db_dest = get_db(ctx, rd, target_library_id) if ctx.restriction_for(rd, db_src) or ctx.restriction_for(rd, db_dest): raise HTTPForbidden( 'Cannot use the copy to library interface with a user who has per library restrictions' ) data = load_payload_data(rd) try: book_ids = {int(x) for x in data['book_ids']} move_books = bool(data.get('move', False)) preserve_date = bool(data.get('preserve_date', True)) duplicate_action = data.get('duplicate_action') or 'add' automerge_action = data.get('automerge_action') or 'overwrite' except Exception: raise HTTPBadRequest( 'Invalid encoded data, must be of the form: {book_ids: [id1, id2, ..]}' ) if duplicate_action not in ('add', 'add_formats_to_existing', 'ignore'): raise HTTPBadRequest( 'duplicate_action must be one of: add, add_formats_to_existing, ignore' ) if automerge_action not in ('overwrite', 'ignore', 'new record'): raise HTTPBadRequest( 'automerge_action must be one of: overwrite, ignore, new record') response = {} identical_books_data = None if duplicate_action != 'add': identical_books_data = db_dest.data_for_find_identical_books() to_remove = set() from calibre.db.copy_to_library import copy_one_book for book_id in book_ids: try: rdata = copy_one_book(book_id, db_src, db_dest, duplicate_action=duplicate_action, automerge_action=automerge_action, preserve_uuid=move_books, preserve_date=preserve_date, identical_books_data=identical_books_data) if move_books: to_remove.add(book_id) response[book_id] = {'ok': True, 'payload': rdata} except Exception: import traceback response[book_id] = { 'ok': False, 'payload': traceback.format_exc() } if to_remove: db_src.remove_books(to_remove, permanent=True) return response
def cdb_set_fields(ctx, rd, book_id, library_id): db = get_db(ctx, rd, library_id) if ctx.restriction_for(rd, db): raise HTTPForbidden( 'Cannot use the set fields 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: data = msgpack_loads(raw) elif 'application/json' in ct: data = json_loads(raw) else: raise HTTPBadRequest('Only JSON or msgpack requests are supported') except Exception: raise HTTPBadRequest('Invalid encoded data') try: changes, loaded_book_ids = data['changes'], frozenset( map(int, data.get('loaded_book_ids', ()))) all_dirtied = bool(data.get('all_dirtied')) if not isinstance(changes, dict): raise TypeError('changes must be a dict') except Exception: raise HTTPBadRequest( '''Data must be of the form {'changes': {'title': 'New Title', ...}, 'loaded_book_ids':[book_id1, book_id2, ...]'}''' ) dirtied = set() cdata = changes.pop('cover', False) if cdata is not False: if cdata is not None: try: cdata = standard_b64decode( cdata.split(',', 1)[-1].encode('ascii')) except Exception: raise HTTPBadRequest( 'Cover data is not valid base64 encoded data') try: fmt = what(None, cdata) except Exception: fmt = None if fmt not in ('jpeg', 'png'): raise HTTPBadRequest('Cover data must be either JPEG or PNG') dirtied |= db.set_cover({book_id: cdata}) for field, value in changes.iteritems(): dirtied |= db.set_field(field, {book_id: value}) ctx.notify_changes(db.backend.library_path, metadata(dirtied)) all_ids = dirtied if all_dirtied else (dirtied & loaded_book_ids) all_ids |= {book_id} return {bid: book_as_json(db, book_id) for bid in all_ids}
def load_payload_data(rd): 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: return msgpack_loads(raw) elif 'application/json' in ct: return json_loads(raw) else: raise HTTPBadRequest('Only JSON or msgpack requests are supported') except Exception: raise HTTPBadRequest('Invalid encoded data')
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.query) 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.query)[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 cdb_delete_book(ctx, rd, book_ids, library_id): db = get_db(ctx, rd, library_id) if ctx.restriction_for(rd, db): raise HTTPForbidden('Cannot use the delete book interface with a user who has per library restrictions') try: ids = {int(x) for x in book_ids.split(',')} except Exception: raise HTTPBadRequest('invalid book_ids: {}'.format(book_ids)) db.remove_books(ids) books_deleted(ids) return {}
def change_pw(ctx, rd): user = rd.username or None if user is None: raise HTTPForbidden( 'Anonymous users are not allowed to change passwords') try: pw = json.loads(rd.request_body_file.read()) oldpw, newpw = pw['oldpw'], pw['newpw'] except Exception: raise HTTPBadRequest('No decodable password found') if oldpw != ctx.user_manager.get(user): raise HTTPBadRequest(_('Existing password is incorrect')) err = validate_password(newpw) if err: raise HTTPBadRequest(err) try: ctx.user_manager.change_password(user, newpw) except Exception as err: raise HTTPBadRequest(as_unicode(err)) ctx.log.warn('Changed password for user', user) return 'password for {} changed'.format(user)
def cdb_set_fields(ctx, rd, book_id, library_id): db = get_db(ctx, rd, library_id) if ctx.restriction_for(rd, db): raise HTTPForbidden('Cannot use the set fields 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: data = msgpack_loads(raw) elif 'application/json' in ct: data = json_loads(raw) else: raise HTTPBadRequest('Only JSON or msgpack requests are supported') changes, loaded_book_ids = data['changes'], frozenset(map(int, data['loaded_book_ids'])) except Exception: raise HTTPBadRequest('Invalid encoded data') dirtied = set() for field, value in changes.iteritems(): dirtied |= db.set_field(field, {book_id: value}) metadata(dirtied) return {bid: book_as_json(db, book_id) for bid in (dirtied & loaded_book_ids) | {book_id}}
def set_session_data(ctx, rd): ''' Store session data persistently so that it is propagated automatically to new logged in clients ''' if rd.username: try: new_data = load_json_file(rd.request_body_file) if not isinstance(new_data, dict): raise Exception('session data must be a dict') except Exception as err: raise HTTPBadRequest('Invalid data: %s' % as_unicode(err)) ud = ctx.user_manager.get_session_data(rd.username) ud.update(new_data) ctx.user_manager.set_session_data(rd.username, ud)