From 42924d9508413be11199bcccb4158248e066c436 Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs <ozzie.fernandez.isaacs@googlemail.com> Date: Mon, 2 Dec 2024 15:31:46 +0100 Subject: [PATCH] Added logging of ip address (#3237) Refactored Response(json.dumps -> make_response(jsonify..) Update mimetypes - Allow different mimetypes for download and file upload check (#3245, #3243) Bugfixes from tests Updated optional-requirements --- cps/__init__.py | 6 +- cps/admin.py | 24 +++--- cps/editbooks.py | 106 ++++++++++-------------- cps/file_helper.py | 11 ++- cps/helper.py | 5 +- cps/opds.py | 6 +- cps/search_metadata.py | 10 +-- cps/tasks/convert.py | 14 ++-- cps/web.py | 17 ++-- optional-requirements.txt | 4 +- test/Calibre-Web TestSummary_Linux.html | 78 ++++++++++++----- 11 files changed, 154 insertions(+), 127 deletions(-) diff --git a/cps/__init__.py b/cps/__init__.py index 24267b1f..1b1fa191 100644 --- a/cps/__init__.py +++ b/cps/__init__.py @@ -55,13 +55,13 @@ mimetypes.init() mimetypes.add_type('application/xhtml+xml', '.xhtml') mimetypes.add_type('application/epub+zip', '.epub') mimetypes.add_type('application/epub+zip', '.kepub') -mimetypes.add_type('text/xml', '.fb2') +mimetypes.add_type('application/fb2+zip', '.fb2') mimetypes.add_type('application/octet-stream', '.mobi') mimetypes.add_type('application/octet-stream', '.prc') mimetypes.add_type('application/vnd.amazon.ebook', '.azw') mimetypes.add_type('application/x-mobi8-ebook', '.azw3') -mimetypes.add_type('application/x-rar', '.cbr') -mimetypes.add_type('application/zip', '.cbz') +mimetypes.add_type('application/x-cbr', '.cbr') +mimetypes.add_type('application/x-cbz', '.cbz') mimetypes.add_type('application/x-tar', '.cbt') mimetypes.add_type('application/x-7z-compressed', '.cb7') mimetypes.add_type('image/vnd.djv', '.djv') diff --git a/cps/admin.py b/cps/admin.py index 943abe92..71403e6c 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -32,7 +32,8 @@ from datetime import time as datetime_time from functools import wraps from urllib.parse import urlparse -from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory, g, Response +from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, \ + send_from_directory, g, jsonify from markupsafe import Markup from .cw_login import current_user from flask_babel import gettext as _ @@ -378,10 +379,7 @@ def list_users(): user.default = get_user_locale_language(user.default_language) table_entries = {'totalNotFiltered': total_count, 'total': filtered_count, "rows": users} - js_list = json.dumps(table_entries, cls=db.AlchemyEncoder) - response = make_response(js_list) - response.headers["Content-Type"] = "application/json; charset=utf-8" - return response + return make_response(json.dumps(table_entries, cls=db.AlchemyEncoder)) @admi.route("/ajax/deleteuser", methods=['POST']) @@ -400,7 +398,7 @@ def delete_user(): success = list() if not users: log.error("User not found") - return Response(json.dumps({'type': "danger", 'message': _("User not found")}), mimetype='application/json') + return make_response(jsonify(type="danger", message=_("User not found"))) for user in users: try: message = _delete_user(user) @@ -416,7 +414,7 @@ def delete_user(): log.info("Users {} deleted".format(user_ids)) success = [{'type': "success", 'message': _("{} users deleted successfully").format(count)}] success.extend(errors) - return Response(json.dumps(success), mimetype='application/json') + return make_response(jsonify(success)) @admi.route("/ajax/getlocale") @@ -498,10 +496,10 @@ def edit_list_user(param): if not ub.session.query(ub.User). \ filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN, ub.User.id != user.id).count(): - return Response( - json.dumps([{'type': "danger", + return make_response( + jsonify([{'type': "danger", 'message': _("No admin user remaining, can't remove admin role", - nick=user.name)}]), mimetype='application/json') + nick=user.name)}])) user.role &= ~value else: raise Exception(_("Value has to be true or false")) @@ -947,7 +945,7 @@ def do_full_kobo_sync(userid): count = ub.session.query(ub.KoboSyncedBooks).filter(userid == ub.KoboSyncedBooks.user_id).delete() message = _("{} sync entries deleted").format(count) ub.session_commit(message) - return Response(json.dumps([{"type": "success", "message": message}]), mimetype='application/json') + return make_response(jsonify(type="success", message=message)) def check_valid_read_column(column): @@ -1264,7 +1262,7 @@ def _configuration_ldap_helper(to_save): @admin_required def simulatedbchange(): db_change, db_valid = _db_simulate_change() - return Response(json.dumps({"change": db_change, "valid": db_valid}), mimetype='application/json') + return make_response(jsonify(change=db_change, valid=db_valid)) @admi.route("/admin/user/new", methods=["GET", "POST"]) @@ -1896,7 +1894,7 @@ def _configuration_result(error_flash=None, reboot=False): resp['result'] = [{'type': "success", 'message': _("Calibre-Web configuration updated")}] resp['reboot'] = reboot resp['config_upload'] = config.config_upload_formats - return Response(json.dumps(resp), mimetype='application/json') + return make_response(jsonify(resp)) def _db_configuration_result(error_flash=None, gdrive_error=None): diff --git a/cps/editbooks.py b/cps/editbooks.py index d5d92453..355a9bfd 100644 --- a/cps/editbooks.py +++ b/cps/editbooks.py @@ -28,11 +28,11 @@ from shutil import copyfile from markupsafe import escape, Markup # dependency of flask from functools import wraps -from flask import Blueprint, request, flash, redirect, url_for, abort, Response +from flask import Blueprint, request, flash, redirect, url_for, abort, jsonify, make_response, Response from flask_babel import gettext as _ from flask_babel import lazy_gettext as N_ from flask_babel import get_locale -from .cw_login import current_user, login_required +from .cw_login import current_user from sqlalchemy.exc import OperationalError, IntegrityError, InterfaceError from sqlalchemy.orm.exc import StaleDataError from sqlalchemy.sql.expression import func @@ -76,7 +76,7 @@ def edit_required(f): @editbook.route("/ajax/delete/<int:book_id>", methods=["POST"]) @user_login_required def delete_book_from_details(book_id): - return Response(delete_book_from_table(book_id, "", True), mimetype='application/json') + return delete_book_from_table(book_id, "", True) # , mimetype='application/json') @editbook.route("/delete/<int:book_id>", defaults={'book_format': ""}, methods=["POST"]) @@ -158,16 +158,16 @@ def upload(): if len(request.files.getlist("btn-upload")) < 2: if current_user.role_edit() or current_user.role_admin(): resp = {"location": url_for('edit-book.show_edit_book', book_id=book_id)} - return Response(json.dumps(resp), mimetype='application/json') + return make_response(jsonify(resp)) else: resp = {"location": url_for('web.show_book', book_id=book_id)} - return Response(json.dumps(resp), mimetype='application/json') + return make_response(jsonify(resp)) except (OperationalError, IntegrityError, StaleDataError) as e: calibre_db.session.rollback() log.error_or_exception("Database error: {}".format(e)) flash(_("Oops! Database Error: %(error)s.", error=e.orig if hasattr(e, "orig") else e), category="error") - return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json') + return make_response(jsonify(location=url_for("web.index"))) abort(404) @@ -206,7 +206,7 @@ def table_get_custom_enum(c_id): ret.append({'value': "", 'text': ""}) for idx, en in enumerate(cc.get_display_dict()['enum_values']): ret.append({'value': en, 'text': en}) - return json.dumps(ret) + return make_response(jsonify(ret)) @editbook.route("/ajax/editbooks/<param>", methods=['POST']) @@ -221,68 +221,54 @@ def edit_list_book(param): try: if param == 'series_index': edit_book_series_index(vals['value'], book) - ret = Response(json.dumps({'success': True, 'newValue': book.series_index}), mimetype='application/json') + ret = make_response(jsonify(success=True, newValue=book.series_index)) elif param == 'tags': edit_book_tags(vals['value'], book) - ret = Response(json.dumps({'success': True, 'newValue': ', '.join([tag.name for tag in book.tags])}), - mimetype='application/json') + ret = make_response(jsonify(success=True, newValue=', '.join([tag.name for tag in book.tags]))) elif param == 'series': edit_book_series(vals['value'], book) - ret = Response(json.dumps({'success': True, 'newValue': ', '.join([serie.name for serie in book.series])}), - mimetype='application/json') + ret = make_response(jsonify(success=True, newValue=', '.join([serie.name for serie in book.series]))) elif param == 'publishers': edit_book_publisher(vals['value'], book) - ret = Response(json.dumps({'success': True, - 'newValue': ', '.join([publisher.name for publisher in book.publishers])}), - mimetype='application/json') + ret = make_response(jsonify(success=True, + newValue=', '.join([publisher.name for publisher in book.publishers]))) elif param == 'languages': invalid = list() edit_book_languages(vals['value'], book, invalid=invalid) if invalid: - ret = Response(json.dumps({'success': False, - 'msg': 'Invalid languages in request: {}'.format(','.join(invalid))}), - mimetype='application/json') + ret = make_response(jsonify(success=False, + msg='Invalid languages in request: {}'.format(','.join(invalid)))) else: lang_names = list() for lang in book.languages: lang_names.append(isoLanguages.get_language_name(get_locale(), lang.lang_code)) - ret = Response(json.dumps({'success': True, 'newValue': ', '.join(lang_names)}), - mimetype='application/json') + ret = make_response(jsonify(success=True, newValue=', '.join(lang_names))) elif param == 'author_sort': book.author_sort = vals['value'] - ret = Response(json.dumps({'success': True, 'newValue': book.author_sort}), - mimetype='application/json') + ret = make_response(jsonify(success=True, newValue=book.author_sort)) elif param == 'title': sort_param = book.sort if handle_title_on_edit(book, vals.get('value', "")): rename_error = helper.update_dir_structure(book.id, config.get_book_path()) if not rename_error: - ret = Response(json.dumps({'success': True, 'newValue': book.title}), - mimetype='application/json') + ret = make_response(jsonify(success=True, newValue=book.title)) else: - ret = Response(json.dumps({'success': False, - 'msg': rename_error}), - mimetype='application/json') + ret = make_response(jsonify(success=False, msg=rename_error)) elif param == 'sort': book.sort = vals['value'] - ret = Response(json.dumps({'success': True, 'newValue': book.sort}), - mimetype='application/json') + ret = make_response(jsonify(success=True,newValue=book.sort)) elif param == 'comments': edit_book_comments(vals['value'], book) - ret = Response(json.dumps({'success': True, 'newValue': book.comments[0].text}), - mimetype='application/json') + ret = make_response(jsonify(success=True, newValue=book.comments[0].text)) elif param == 'authors': input_authors, __ = handle_author_on_edit(book, vals['value'], vals.get('checkA', None) == "true") rename_error = helper.update_dir_structure(book.id, config.get_book_path(), input_authors[0]) if not rename_error: - ret = Response(json.dumps({ - 'success': True, - 'newValue': ' & '.join([author.replace('|', ',') for author in input_authors])}), - mimetype='application/json') + ret = make_response(jsonify( + success=True, + newValue=' & '.join([author.replace('|', ',') for author in input_authors]))) else: - ret = Response(json.dumps({'success': False, - 'msg': rename_error}), - mimetype='application/json') + ret = make_response(jsonify(success=False, msg=rename_error)) elif param == 'is_archived': is_archived = change_archived_books(book.id, vals['value'] == "True", message="Book {} archive bit set to: {}".format(book.id, vals['value'])) @@ -301,8 +287,7 @@ def edit_list_book(param): if vals['value'] in ["True", "False"]: ret = "" else: - ret = Response(json.dumps({'success': True, 'newValue': vals['value']}), - mimetype='application/json') + ret = make_response(jsonify(success=True, newValue=vals['value'])) else: return _("Parameter not found"), 400 book.last_modified = datetime.now(timezone.utc) @@ -315,9 +300,8 @@ def edit_list_book(param): except (OperationalError, IntegrityError, StaleDataError) as e: calibre_db.session.rollback() log.error_or_exception("Database error: {}".format(e)) - ret = Response(json.dumps({'success': False, - 'msg': 'Database error: {}'.format(e.orig if hasattr(e, "orig") else e)}), - mimetype='application/json') + ret = make_response(jsonify(success=False, + msg='Database error: {}'.format(e.orig if hasattr(e, "orig") else e))) return ret @@ -328,13 +312,13 @@ def get_sorted_entry(field, bookid): book = calibre_db.get_filtered_book(bookid) if book: if field == 'title': - return json.dumps({'sort': book.sort}) + return make_response(jsonify(sort=book.sort)) elif field == 'authors': - return json.dumps({'author_sort': book.author_sort}) + return make_response(jsonify(author_sort=book.author_sort)) if field == 'sort': - return json.dumps({'sort': book.title}) + return make_response(jsonify(sort=book.title)) if field == 'author_sort': - return json.dumps({'authors': " & ".join([a.name for a in calibre_db.order_authors([book])])}) + return make_response(jsonify(authors=" & ".join([a.name for a in calibre_db.order_authors([book])]))) return "" @@ -350,7 +334,7 @@ def simulate_merge_list_book(): from_book = [] for book_id in vals: from_book.append(calibre_db.get_book(book_id).title) - return json.dumps({'to': to_book, 'from': from_book}) + return make_response(jsonify({'to': to_book, 'from': from_book})) return "" @@ -388,7 +372,7 @@ def merge_list_book(): element.uncompressed_size, to_name)) delete_book_from_table(from_book.id, "", True) - return json.dumps({'success': True}) + return make_response(jsonify(success=True)) return "" @@ -428,11 +412,11 @@ def table_xchange_author_title(): except (OperationalError, IntegrityError, StaleDataError) as e: calibre_db.session.rollback() log.error_or_exception("Database error: {}".format(e)) - return json.dumps({'success': False}) + return make_response(jsonify(success=False)) if config.config_use_google_drive: gdriveutils.updateGdriveCalibreFromLocal() - return json.dumps({'success': True}) + return make_response(jsonify(success=True)) return "" @@ -560,7 +544,7 @@ def do_edit_book(book_id, upload_formats=None): if upload_formats: resp = {"location": url_for('edit-book.show_edit_book', book_id=book_id)} - return Response(json.dumps(resp), mimetype='application/json') + return make_response(jsonify(resp)) if "detail_view" in to_save: return redirect(url_for('web.show_book', book_id=book.id)) @@ -769,17 +753,17 @@ def file_handling_on_upload(requested_file): if config.config_check_extensions and allowed_extensions != ['']: if not validate_mime_type(requested_file, allowed_extensions): flash(_("File type isn't allowed to be uploaded to this server"), category="error") - return None, Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json') + return None, make_response(jsonify(location=url_for("web.index"))) if '.' in requested_file.filename: file_ext = requested_file.filename.rsplit('.', 1)[-1].lower() if file_ext not in allowed_extensions and '' not in allowed_extensions: flash( _("File extension '%(ext)s' is not allowed to be uploaded to this server", ext=file_ext), category="error") - return None, Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json') + return None, make_response(jsonify(location=url_for("web.index"))) else: flash(_('File to be uploaded must have an extension'), category="error") - return None, Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json') + return None, make_response(jsonify(location=url_for("web.index"))) # extract metadata from file try: @@ -788,7 +772,7 @@ def file_handling_on_upload(requested_file): log.error("File %s could not saved to temp dir", requested_file.filename) flash(_("File %(filename)s could not saved to temp dir", filename=requested_file.filename), category="error") - return None, Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json') + return None, make_response(jsonify(location=url_for("web.index"))) return meta, None @@ -860,7 +844,7 @@ def delete_whole_book(book_id, book): def render_delete_book_result(book_format, json_response, warning, book_id, location=""): if book_format: if json_response: - return json.dumps([warning, {"location": url_for("edit-book.show_edit_book", book_id=book_id), + return jsonify([warning, {"location": url_for("edit-book.show_edit_book", book_id=book_id), "type": "success", "format": book_format, "message": _('Book Format Successfully Deleted')}]) @@ -869,7 +853,7 @@ def render_delete_book_result(book_format, json_response, warning, book_id, loca return redirect(url_for('edit-book.show_edit_book', book_id=book_id)) else: if json_response: - return json.dumps([warning, {"location": get_redirect_location(location, "web.index"), + return jsonify([warning, {"location": get_redirect_location(location, "web.index"), "type": "success", "format": book_format, "message": _('Book Successfully Deleted')}]) @@ -887,7 +871,7 @@ def delete_book_from_table(book_id, book_format, json_response, location=""): result, error = helper.delete_book(book, config.get_book_path(), book_format=book_format.upper()) if not result: if json_response: - return json.dumps([{"location": url_for("edit-book.show_edit_book", book_id=book_id), + return jsonify([{"location": url_for("edit-book.show_edit_book", book_id=book_id), "type": "danger", "format": "", "message": error}]) @@ -914,7 +898,7 @@ def delete_book_from_table(book_id, book_format, json_response, location=""): log.error_or_exception(ex) calibre_db.session.rollback() if json_response: - return json.dumps([{"location": url_for("edit-book.show_edit_book", book_id=book_id), + return jsonify([{"location": url_for("edit-book.show_edit_book", book_id=book_id), "type": "danger", "format": "", "message": ex}]) @@ -928,7 +912,7 @@ def delete_book_from_table(book_id, book_format, json_response, location=""): return render_delete_book_result(book_format, json_response, warning, book_id, location) message = _("You are missing permissions to delete books") if json_response: - return json.dumps({"location": url_for("edit-book.show_edit_book", book_id=book_id), + return jsonify({"location": url_for("edit-book.show_edit_book", book_id=book_id), "type": "danger", "format": "", "message": message}) diff --git a/cps/file_helper.py b/cps/file_helper.py index 9d5406d3..361709f8 100644 --- a/cps/file_helper.py +++ b/cps/file_helper.py @@ -34,6 +34,15 @@ except ImportError as e: error = "Cannot import python-magic, checking uploaded file metadata will not work: {}".format(e) +def get_mimetype(ext): + # overwrite some mimetypes for proper file detection + mimes = {".fb2": "text/xml", + ".cbz": "application/zip", + ".cbr": "application/x-rar" + } + return mimes.get(ext, mimetypes.types_map[ext]) + + def get_temp_dir(): tmp_dir = os.path.join(gettempdir(), 'calibre_web') if not os.path.isdir(tmp_dir): @@ -54,7 +63,7 @@ def validate_mime_type(file_buffer, allowed_extensions): allowed_mimetypes = list() for x in allowed_extensions: try: - allowed_mimetypes.append(mimetypes.types_map["." + x]) + allowed_mimetypes.append(get_mimetype("." + x)) except KeyError: log.error("Unkown mimetype for Extension: {}".format(x)) tmp_mime_type = mime.from_buffer(file_buffer.read()) diff --git a/cps/helper.py b/cps/helper.py index d6f4dd7d..0706d40b 100644 --- a/cps/helper.py +++ b/cps/helper.py @@ -30,7 +30,7 @@ import requests import unidecode from uuid import uuid4 -from flask import send_from_directory, make_response, abort, url_for, Response +from flask import send_from_directory, make_response, abort, url_for, Response, request from flask_babel import gettext as _ from flask_babel import lazy_gettext as N_ from flask_babel import get_locale @@ -974,7 +974,8 @@ def do_download_file(book, book_format, client, data, headers): # ToDo Check headers parameter for element in headers: response.headers[element[0]] = element[1] - log.info('Downloading file: {}'.format(os.path.join(filename, book_name + "." + book_format))) + log.info('Downloading file: \'%s\' by %s - %s', format(os.path.join(filename, book_name + "." + book_format)), + current_user.name, request.headers.get('X-Forwarded-For', request.remote_addr)) return response diff --git a/cps/opds.py b/cps/opds.py index 485d241a..6dea544a 100644 --- a/cps/opds.py +++ b/cps/opds.py @@ -21,10 +21,10 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import datetime -import json +# import json from urllib.parse import unquote_plus -from flask import Blueprint, request, render_template, make_response, abort, Response, g +from flask import Blueprint, request, render_template, make_response, abort, g, jsonify from flask_babel import get_locale from flask_babel import gettext as _ @@ -451,7 +451,7 @@ def get_database_stats(): stat['authors'] = calibre_db.session.query(db.Authors).count() stat['categories'] = calibre_db.session.query(db.Tags).count() stat['series'] = calibre_db.session.query(db.Series).count() - return Response(json.dumps(stat), mimetype="application/json") + return make_response(jsonify(stat)) @opds.route("/opds/thumb_240_240/<book_id>") diff --git a/cps/search_metadata.py b/cps/search_metadata.py index 719331f0..ae3c9faf 100644 --- a/cps/search_metadata.py +++ b/cps/search_metadata.py @@ -23,7 +23,7 @@ import json import os import sys -from flask import Blueprint, Response, request, url_for +from flask import Blueprint, request, url_for, make_response, jsonify from .cw_login import current_user from flask_babel import get_locale from sqlalchemy.exc import InvalidRequestError, OperationalError @@ -89,7 +89,7 @@ def metadata_provider(): provider.append( {"name": c.__name__, "active": ac, "initial": ac, "id": c.__id__} ) - return Response(json.dumps(provider), mimetype="application/json") + return make_response(jsonify(provider)) @meta.route("/metadata/provider", methods=["POST"]) @@ -114,9 +114,7 @@ def metadata_change_active_provider(prov_name): provider = next((c for c in cl if c.__id__ == prov_name), None) if provider is not None: data = provider.search(new_state.get("query", "")) - return Response( - json.dumps([asdict(x) for x in data]), mimetype="application/json" - ) + return make_response(jsonify([asdict(x) for x in data])) return "" @@ -138,4 +136,4 @@ def metadata_search(): } for future in concurrent.futures.as_completed(meta): data.extend([asdict(x) for x in future.result() if x]) - return Response(json.dumps(data), mimetype="application/json") + return make_response(jsonify(data)) diff --git a/cps/tasks/convert.py b/cps/tasks/convert.py index e0df3802..dc0af0c4 100644 --- a/cps/tasks/convert.py +++ b/cps/tasks/convert.py @@ -210,13 +210,13 @@ class TaskConvert(CalibreTask): else: error_message = N_('%(format)s format not found on disk', format=format_new_ext.upper()) local_db.session.close() - log.info("ebook converter failed with error while converting book") - if not error_message: - error_message = N_('Ebook converter failed with unknown error') - else: - log.error(error_message) - self._handleError(error_message) - return + log.info("ebook converter failed with error while converting book") + if not error_message: + error_message = N_('Ebook converter failed with unknown error') + else: + log.error(error_message) + self._handleError(error_message) + return def _convert_kepubify(self, file_path, format_old_ext, format_new_ext): if config.config_embed_metadata and config.config_binariesdir: diff --git a/cps/web.py b/cps/web.py index 36653a45..e1068c6f 100644 --- a/cps/web.py +++ b/cps/web.py @@ -25,8 +25,7 @@ import chardet # dependency of requests import copy from importlib.metadata import metadata -from flask import Blueprint, jsonify -from flask import request, redirect, send_from_directory, make_response, flash, abort, url_for, Response +from flask import Blueprint, jsonify, request, redirect, send_from_directory, make_response, flash, abort, url_for from flask import session as flask_session from flask_babel import gettext as _ from flask_babel import get_locale @@ -1198,13 +1197,14 @@ def serve_book(book_id, book_format, anyname): if not data: return "File not in Database" range_header = request.headers.get('Range', None) - + if not range_header: + log.info('Serving book: \'%s\' to %s - %s', data.name, current_user.name, + request.headers.get('X-Forwarded-For', request.remote_addr)) if config.config_use_google_drive: try: headers = Headers() headers["Content-Type"] = mimetypes.types_map.get('.' + book_format, "application/octet-stream") - if not range_header: - log.info('Serving book: %s', data.name) + if not range_header: headers['Accept-Ranges'] = 'bytes' df = getFileFromEbooksFolder(book.path, data.name + "." + book_format) return do_gdrive_download(df, headers, (book_format.upper() == 'TXT')) @@ -1213,7 +1213,6 @@ def serve_book(book_id, book_format, anyname): return "File Not Found" else: if book_format.upper() == 'TXT': - log.info('Serving book: %s', data.name) try: rawdata = open(os.path.join(config.get_book_path(), book.path, data.name + "." + book_format), "rb").read() @@ -1234,7 +1233,6 @@ def serve_book(book_id, book_format, anyname): response = make_response( send_from_directory(os.path.join(config.get_book_path(), book.path), data.name + "." + book_format)) if not range_header: - log.info('Serving book: %s', data.name) response.headers['Accept-Ranges'] = 'bytes' return response @@ -1253,8 +1251,7 @@ def download_link(book_id, book_format, anyname): @download_required def send_to_ereader(book_id, book_format, convert): if not config.get_mail_server_configured(): - response = [{'type': "danger", 'message': _("Please configure the SMTP mail settings first...")}] - return Response(json.dumps(response), mimetype='application/json') + return make_response(jsonify(type="danger", message=_("Please configure the SMTP mail settings first..."))) elif current_user.kindle_mail: result = send_mail(book_id, book_format, convert, current_user.kindle_mail, config.get_book_path(), current_user.name) @@ -1266,7 +1263,7 @@ def send_to_ereader(book_id, book_format, convert): response = [{'type': "danger", 'message': _("Oops! There was an error sending book: %(res)s", res=result)}] else: response = [{'type': "danger", 'message': _("Oops! Please update your profile with a valid eReader Email.")}] - return Response(json.dumps(response), mimetype='application/json') + return make_response(jsonify(response)) # ################################### Login Logout ################################################################## diff --git a/optional-requirements.txt b/optional-requirements.txt index b9d98544..1726847f 100644 --- a/optional-requirements.txt +++ b/optional-requirements.txt @@ -1,13 +1,13 @@ # GDrive Integration google-api-python-client>=1.7.11,<2.200.0 gevent>20.6.0,<24.3.0 -greenlet>=0.4.17,<3.1.0 +greenlet>=0.4.17,<3.2.0 httplib2>=0.9.2,<0.23.0 oauth2client>=4.0.0,<4.1.4 uritemplate>=3.0.0,<4.2.0 pyasn1-modules>=0.0.8,<0.5.0 pyasn1>=0.1.9,<0.7.0 -PyDrive2>=1.3.1,<1.20.0 +PyDrive2>=1.3.1,<1.22.0 PyYAML>=3.12,<6.1 rsa>=3.4.2,<4.10.0 diff --git a/test/Calibre-Web TestSummary_Linux.html b/test/Calibre-Web TestSummary_Linux.html index 039f2e7e..fc3b5acd 100644 --- a/test/Calibre-Web TestSummary_Linux.html +++ b/test/Calibre-Web TestSummary_Linux.html @@ -37,20 +37,20 @@ <div class="row"> <div class="col-xs-6 col-md-6 col-sm-offset-3" style="margin-top:50px;"> - <p class='text-justify attribute'><strong>Start Time: </strong>2024-11-29 20:17:45</p> + <p class='text-justify attribute'><strong>Start Time: </strong>2024-12-06 17:23:58</p> </div> </div> <div class="row"> <div class="col-xs-6 col-md-6 col-sm-offset-3"> - <p class='text-justify attribute'><strong>Stop Time: </strong>2024-11-30 03:36:53</p> + <p class='text-justify attribute'><strong>Stop Time: </strong>2024-12-07 00:45:19</p> </div> </div> <div class="row"> <div class="col-xs-6 col-md-6 col-sm-offset-3"> - <p class='text-justify attribute'><strong>Duration: </strong>6h 12 min</p> + <p class='text-justify attribute'><strong>Duration: </strong>6h 11 min</p> </div> </div> </div> @@ -1023,13 +1023,13 @@ - <tr id="su" class="skipClass"> + <tr id="su" class="passClass"> <td>TestEditAdditionalBooks</td> <td class="text-center">18</td> - <td class="text-center">17</td> + <td class="text-center">18</td> + <td class="text-center">0</td> <td class="text-center">0</td> <td class="text-center">0</td> - <td class="text-center">1</td> <td class="text-center"> <a onclick="showClassDetail('c12', 18)">Detail</a> </td> @@ -1172,11 +1172,11 @@ - <tr id='st12.16' class='none bg-warning'> + <tr id='pt12.16' class='hiddenRow bg-success'> <td> <div class='testcase'>TestEditAdditionalBooks - test_xss_author_edit</div> </td> - <td colspan='6' align='center'>SKIP</td> + <td colspan='6' align='center'>PASS</td> </tr> @@ -2074,11 +2074,11 @@ IndexError: list index out of range</pre> - <tr id="su" class="passClass"> + <tr id="su" class="failClass"> <td>TestEditBooksOnGdrive</td> <td class="text-center">18</td> - <td class="text-center">18</td> - <td class="text-center">0</td> + <td class="text-center">16</td> + <td class="text-center">2</td> <td class="text-center">0</td> <td class="text-center">0</td> <td class="text-center"> @@ -2205,11 +2205,31 @@ IndexError: list index out of range</pre> - <tr id='pt19.14' class='hiddenRow bg-success'> + <tr id="ft19.14" class="none bg-danger"> <td> <div class='testcase'>TestEditBooksOnGdrive - test_edit_rating</div> </td> - <td colspan='6' align='center'>PASS</td> + <td colspan='6'> + <div class="text-center"> + <a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_ft19.14')">FAIL</a> + </div> + <!--css div popup start--> + <div id="div_ft19.14" class="popup_window test_output" style="display:block;"> + <div class='close_button pull-right'> + <button type="button" class="close" aria-label="Close" onfocus="this.blur();" + onclick="document.getElementById('div_ft19.14').style.display='none'"><span + aria-hidden="true">×</span></button> + </div> + <div class="text-left pull-left"> + <pre class="text-left">Traceback (most recent call last): + File "/home/ozzie/Development/calibre-web-test/test/test_edit_ebooks_gdrive.py", line 632, in test_edit_rating + self.assertEqual(4, values['rating']) +AssertionError: 4 != 0</pre> + </div> + <div class="clearfix"></div> + </div> + <!--css div popup end--> + </td> </tr> @@ -2241,11 +2261,31 @@ IndexError: list index out of range</pre> - <tr id='pt19.18' class='hiddenRow bg-success'> + <tr id="ft19.18" class="none bg-danger"> <td> <div class='testcase'>TestEditBooksOnGdrive - test_watch_metadata</div> </td> - <td colspan='6' align='center'>PASS</td> + <td colspan='6'> + <div class="text-center"> + <a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_ft19.18')">FAIL</a> + </div> + <!--css div popup start--> + <div id="div_ft19.18" class="popup_window test_output" style="display:block;"> + <div class='close_button pull-right'> + <button type="button" class="close" aria-label="Close" onfocus="this.blur();" + onclick="document.getElementById('div_ft19.18').style.display='none'"><span + aria-hidden="true">×</span></button> + </div> + <div class="text-left pull-left"> + <pre class="text-left">Traceback (most recent call last): + File "/home/ozzie/Development/calibre-web-test/test/test_edit_ebooks_gdrive.py", line 976, in test_watch_metadata + self.assertNotIn('series', book) +AssertionError: 'series' unexpectedly found in {'id': 5, 'reader': [], 'title': 'testbook', 'author': ['John Döe'], 'rating': 0, 'languages': ['English'], 'identifier': [], 'cover': '/cover/5/og?c=1733511155', 'tag': [], 'publisher': ['Randomhäus'], 'pubdate': 'Jan 19, 2017', 'comment': 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit.Aenean commodo ligula eget dolor.Aenean massa.Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem.Nulla consequat massa quis enim.Donec pede justo, fringilla vel, aliquet nec, vulputate', 'add_shelf': [], 'del_shelf': [], 'edit_enable': True, 'kindle': None, 'kindlebtn': None, 'download': ['EPUB\n (6.7 kB)'], 'read': False, 'archived': False, 'series_all': 'Book 1 of test', 'series_index': '1', 'series': 'test', 'cust_columns': []}</pre> + </div> + <div class="clearfix"></div> + </div> + <!--css div popup end--> + </td> </tr> @@ -5801,10 +5841,10 @@ IndexError: list index out of range</pre> <tr id='total_row' class="text-center bg-grey"> <td>Total</td> <td>523</td> - <td>514</td> - <td>0</td> + <td>513</td> + <td>2</td> <td>1</td> - <td>8</td> + <td>7</td> <td> </td> </tr> </table> @@ -6348,7 +6388,7 @@ IndexError: list index out of range</pre> </div> <script> - drawCircle(514, 0, 1, 8); + drawCircle(513, 2, 1, 7); showCase(5); </script>