1
0
mirror of https://github.com/janeczku/calibre-web synced 2025-01-17 20:52:57 +00:00

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
This commit is contained in:
Ozzie Isaacs 2024-12-02 15:31:46 +01:00
parent 14e57e3714
commit 42924d9508
11 changed files with 154 additions and 127 deletions

View File

@ -55,13 +55,13 @@ mimetypes.init()
mimetypes.add_type('application/xhtml+xml', '.xhtml') mimetypes.add_type('application/xhtml+xml', '.xhtml')
mimetypes.add_type('application/epub+zip', '.epub') mimetypes.add_type('application/epub+zip', '.epub')
mimetypes.add_type('application/epub+zip', '.kepub') 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', '.mobi')
mimetypes.add_type('application/octet-stream', '.prc') mimetypes.add_type('application/octet-stream', '.prc')
mimetypes.add_type('application/vnd.amazon.ebook', '.azw') mimetypes.add_type('application/vnd.amazon.ebook', '.azw')
mimetypes.add_type('application/x-mobi8-ebook', '.azw3') mimetypes.add_type('application/x-mobi8-ebook', '.azw3')
mimetypes.add_type('application/x-rar', '.cbr') mimetypes.add_type('application/x-cbr', '.cbr')
mimetypes.add_type('application/zip', '.cbz') mimetypes.add_type('application/x-cbz', '.cbz')
mimetypes.add_type('application/x-tar', '.cbt') mimetypes.add_type('application/x-tar', '.cbt')
mimetypes.add_type('application/x-7z-compressed', '.cb7') mimetypes.add_type('application/x-7z-compressed', '.cb7')
mimetypes.add_type('image/vnd.djv', '.djv') mimetypes.add_type('image/vnd.djv', '.djv')

View File

@ -32,7 +32,8 @@ from datetime import time as datetime_time
from functools import wraps from functools import wraps
from urllib.parse import urlparse 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 markupsafe import Markup
from .cw_login import current_user from .cw_login import current_user
from flask_babel import gettext as _ from flask_babel import gettext as _
@ -378,10 +379,7 @@ def list_users():
user.default = get_user_locale_language(user.default_language) user.default = get_user_locale_language(user.default_language)
table_entries = {'totalNotFiltered': total_count, 'total': filtered_count, "rows": users} table_entries = {'totalNotFiltered': total_count, 'total': filtered_count, "rows": users}
js_list = json.dumps(table_entries, cls=db.AlchemyEncoder) return make_response(json.dumps(table_entries, cls=db.AlchemyEncoder))
response = make_response(js_list)
response.headers["Content-Type"] = "application/json; charset=utf-8"
return response
@admi.route("/ajax/deleteuser", methods=['POST']) @admi.route("/ajax/deleteuser", methods=['POST'])
@ -400,7 +398,7 @@ def delete_user():
success = list() success = list()
if not users: if not users:
log.error("User not found") 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: for user in users:
try: try:
message = _delete_user(user) message = _delete_user(user)
@ -416,7 +414,7 @@ def delete_user():
log.info("Users {} deleted".format(user_ids)) log.info("Users {} deleted".format(user_ids))
success = [{'type': "success", 'message': _("{} users deleted successfully").format(count)}] success = [{'type': "success", 'message': _("{} users deleted successfully").format(count)}]
success.extend(errors) success.extend(errors)
return Response(json.dumps(success), mimetype='application/json') return make_response(jsonify(success))
@admi.route("/ajax/getlocale") @admi.route("/ajax/getlocale")
@ -498,10 +496,10 @@ def edit_list_user(param):
if not ub.session.query(ub.User). \ if not ub.session.query(ub.User). \
filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN, filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN,
ub.User.id != user.id).count(): ub.User.id != user.id).count():
return Response( return make_response(
json.dumps([{'type': "danger", jsonify([{'type': "danger",
'message': _("No admin user remaining, can't remove admin role", 'message': _("No admin user remaining, can't remove admin role",
nick=user.name)}]), mimetype='application/json') nick=user.name)}]))
user.role &= ~value user.role &= ~value
else: else:
raise Exception(_("Value has to be true or false")) 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() count = ub.session.query(ub.KoboSyncedBooks).filter(userid == ub.KoboSyncedBooks.user_id).delete()
message = _("{} sync entries deleted").format(count) message = _("{} sync entries deleted").format(count)
ub.session_commit(message) 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): def check_valid_read_column(column):
@ -1264,7 +1262,7 @@ def _configuration_ldap_helper(to_save):
@admin_required @admin_required
def simulatedbchange(): def simulatedbchange():
db_change, db_valid = _db_simulate_change() 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"]) @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['result'] = [{'type': "success", 'message': _("Calibre-Web configuration updated")}]
resp['reboot'] = reboot resp['reboot'] = reboot
resp['config_upload'] = config.config_upload_formats 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): def _db_configuration_result(error_flash=None, gdrive_error=None):

View File

@ -28,11 +28,11 @@ from shutil import copyfile
from markupsafe import escape, Markup # dependency of flask from markupsafe import escape, Markup # dependency of flask
from functools import wraps 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 gettext as _
from flask_babel import lazy_gettext as N_ from flask_babel import lazy_gettext as N_
from flask_babel import get_locale 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.exc import OperationalError, IntegrityError, InterfaceError
from sqlalchemy.orm.exc import StaleDataError from sqlalchemy.orm.exc import StaleDataError
from sqlalchemy.sql.expression import func from sqlalchemy.sql.expression import func
@ -76,7 +76,7 @@ def edit_required(f):
@editbook.route("/ajax/delete/<int:book_id>", methods=["POST"]) @editbook.route("/ajax/delete/<int:book_id>", methods=["POST"])
@user_login_required @user_login_required
def delete_book_from_details(book_id): 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"]) @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 len(request.files.getlist("btn-upload")) < 2:
if current_user.role_edit() or current_user.role_admin(): if current_user.role_edit() or current_user.role_admin():
resp = {"location": url_for('edit-book.show_edit_book', book_id=book_id)} 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: else:
resp = {"location": url_for('web.show_book', book_id=book_id)} 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: except (OperationalError, IntegrityError, StaleDataError) as e:
calibre_db.session.rollback() calibre_db.session.rollback()
log.error_or_exception("Database error: {}".format(e)) log.error_or_exception("Database error: {}".format(e))
flash(_("Oops! Database Error: %(error)s.", error=e.orig if hasattr(e, "orig") else e), flash(_("Oops! Database Error: %(error)s.", error=e.orig if hasattr(e, "orig") else e),
category="error") 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) abort(404)
@ -206,7 +206,7 @@ def table_get_custom_enum(c_id):
ret.append({'value': "", 'text': ""}) ret.append({'value': "", 'text': ""})
for idx, en in enumerate(cc.get_display_dict()['enum_values']): for idx, en in enumerate(cc.get_display_dict()['enum_values']):
ret.append({'value': en, 'text': en}) ret.append({'value': en, 'text': en})
return json.dumps(ret) return make_response(jsonify(ret))
@editbook.route("/ajax/editbooks/<param>", methods=['POST']) @editbook.route("/ajax/editbooks/<param>", methods=['POST'])
@ -221,68 +221,54 @@ def edit_list_book(param):
try: try:
if param == 'series_index': if param == 'series_index':
edit_book_series_index(vals['value'], book) 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': elif param == 'tags':
edit_book_tags(vals['value'], book) edit_book_tags(vals['value'], book)
ret = Response(json.dumps({'success': True, 'newValue': ', '.join([tag.name for tag in book.tags])}), ret = make_response(jsonify(success=True, newValue=', '.join([tag.name for tag in book.tags])))
mimetype='application/json')
elif param == 'series': elif param == 'series':
edit_book_series(vals['value'], book) edit_book_series(vals['value'], book)
ret = Response(json.dumps({'success': True, 'newValue': ', '.join([serie.name for serie in book.series])}), ret = make_response(jsonify(success=True, newValue=', '.join([serie.name for serie in book.series])))
mimetype='application/json')
elif param == 'publishers': elif param == 'publishers':
edit_book_publisher(vals['value'], book) edit_book_publisher(vals['value'], book)
ret = Response(json.dumps({'success': True, ret = make_response(jsonify(success=True,
'newValue': ', '.join([publisher.name for publisher in book.publishers])}), newValue=', '.join([publisher.name for publisher in book.publishers])))
mimetype='application/json')
elif param == 'languages': elif param == 'languages':
invalid = list() invalid = list()
edit_book_languages(vals['value'], book, invalid=invalid) edit_book_languages(vals['value'], book, invalid=invalid)
if invalid: if invalid:
ret = Response(json.dumps({'success': False, ret = make_response(jsonify(success=False,
'msg': 'Invalid languages in request: {}'.format(','.join(invalid))}), msg='Invalid languages in request: {}'.format(','.join(invalid))))
mimetype='application/json')
else: else:
lang_names = list() lang_names = list()
for lang in book.languages: for lang in book.languages:
lang_names.append(isoLanguages.get_language_name(get_locale(), lang.lang_code)) lang_names.append(isoLanguages.get_language_name(get_locale(), lang.lang_code))
ret = Response(json.dumps({'success': True, 'newValue': ', '.join(lang_names)}), ret = make_response(jsonify(success=True, newValue=', '.join(lang_names)))
mimetype='application/json')
elif param == 'author_sort': elif param == 'author_sort':
book.author_sort = vals['value'] book.author_sort = vals['value']
ret = Response(json.dumps({'success': True, 'newValue': book.author_sort}), ret = make_response(jsonify(success=True, newValue=book.author_sort))
mimetype='application/json')
elif param == 'title': elif param == 'title':
sort_param = book.sort sort_param = book.sort
if handle_title_on_edit(book, vals.get('value', "")): if handle_title_on_edit(book, vals.get('value', "")):
rename_error = helper.update_dir_structure(book.id, config.get_book_path()) rename_error = helper.update_dir_structure(book.id, config.get_book_path())
if not rename_error: if not rename_error:
ret = Response(json.dumps({'success': True, 'newValue': book.title}), ret = make_response(jsonify(success=True, newValue=book.title))
mimetype='application/json')
else: else:
ret = Response(json.dumps({'success': False, ret = make_response(jsonify(success=False, msg=rename_error))
'msg': rename_error}),
mimetype='application/json')
elif param == 'sort': elif param == 'sort':
book.sort = vals['value'] book.sort = vals['value']
ret = Response(json.dumps({'success': True, 'newValue': book.sort}), ret = make_response(jsonify(success=True,newValue=book.sort))
mimetype='application/json')
elif param == 'comments': elif param == 'comments':
edit_book_comments(vals['value'], book) edit_book_comments(vals['value'], book)
ret = Response(json.dumps({'success': True, 'newValue': book.comments[0].text}), ret = make_response(jsonify(success=True, newValue=book.comments[0].text))
mimetype='application/json')
elif param == 'authors': elif param == 'authors':
input_authors, __ = handle_author_on_edit(book, vals['value'], vals.get('checkA', None) == "true") 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]) rename_error = helper.update_dir_structure(book.id, config.get_book_path(), input_authors[0])
if not rename_error: if not rename_error:
ret = Response(json.dumps({ ret = make_response(jsonify(
'success': True, success=True,
'newValue': ' & '.join([author.replace('|', ',') for author in input_authors])}), newValue=' & '.join([author.replace('|', ',') for author in input_authors])))
mimetype='application/json')
else: else:
ret = Response(json.dumps({'success': False, ret = make_response(jsonify(success=False, msg=rename_error))
'msg': rename_error}),
mimetype='application/json')
elif param == 'is_archived': elif param == 'is_archived':
is_archived = change_archived_books(book.id, vals['value'] == "True", is_archived = change_archived_books(book.id, vals['value'] == "True",
message="Book {} archive bit set to: {}".format(book.id, vals['value'])) 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"]: if vals['value'] in ["True", "False"]:
ret = "" ret = ""
else: else:
ret = Response(json.dumps({'success': True, 'newValue': vals['value']}), ret = make_response(jsonify(success=True, newValue=vals['value']))
mimetype='application/json')
else: else:
return _("Parameter not found"), 400 return _("Parameter not found"), 400
book.last_modified = datetime.now(timezone.utc) book.last_modified = datetime.now(timezone.utc)
@ -315,9 +300,8 @@ def edit_list_book(param):
except (OperationalError, IntegrityError, StaleDataError) as e: except (OperationalError, IntegrityError, StaleDataError) as e:
calibre_db.session.rollback() calibre_db.session.rollback()
log.error_or_exception("Database error: {}".format(e)) log.error_or_exception("Database error: {}".format(e))
ret = Response(json.dumps({'success': False, ret = make_response(jsonify(success=False,
'msg': 'Database error: {}'.format(e.orig if hasattr(e, "orig") else e)}), msg='Database error: {}'.format(e.orig if hasattr(e, "orig") else e)))
mimetype='application/json')
return ret return ret
@ -328,13 +312,13 @@ def get_sorted_entry(field, bookid):
book = calibre_db.get_filtered_book(bookid) book = calibre_db.get_filtered_book(bookid)
if book: if book:
if field == 'title': if field == 'title':
return json.dumps({'sort': book.sort}) return make_response(jsonify(sort=book.sort))
elif field == 'authors': elif field == 'authors':
return json.dumps({'author_sort': book.author_sort}) return make_response(jsonify(author_sort=book.author_sort))
if field == 'sort': if field == 'sort':
return json.dumps({'sort': book.title}) return make_response(jsonify(sort=book.title))
if field == 'author_sort': 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 "" return ""
@ -350,7 +334,7 @@ def simulate_merge_list_book():
from_book = [] from_book = []
for book_id in vals: for book_id in vals:
from_book.append(calibre_db.get_book(book_id).title) 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 "" return ""
@ -388,7 +372,7 @@ def merge_list_book():
element.uncompressed_size, element.uncompressed_size,
to_name)) to_name))
delete_book_from_table(from_book.id, "", True) delete_book_from_table(from_book.id, "", True)
return json.dumps({'success': True}) return make_response(jsonify(success=True))
return "" return ""
@ -428,11 +412,11 @@ def table_xchange_author_title():
except (OperationalError, IntegrityError, StaleDataError) as e: except (OperationalError, IntegrityError, StaleDataError) as e:
calibre_db.session.rollback() calibre_db.session.rollback()
log.error_or_exception("Database error: {}".format(e)) 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: if config.config_use_google_drive:
gdriveutils.updateGdriveCalibreFromLocal() gdriveutils.updateGdriveCalibreFromLocal()
return json.dumps({'success': True}) return make_response(jsonify(success=True))
return "" return ""
@ -560,7 +544,7 @@ def do_edit_book(book_id, upload_formats=None):
if upload_formats: if upload_formats:
resp = {"location": url_for('edit-book.show_edit_book', book_id=book_id)} 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: if "detail_view" in to_save:
return redirect(url_for('web.show_book', book_id=book.id)) 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 config.config_check_extensions and allowed_extensions != ['']:
if not validate_mime_type(requested_file, 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") 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: if '.' in requested_file.filename:
file_ext = requested_file.filename.rsplit('.', 1)[-1].lower() file_ext = requested_file.filename.rsplit('.', 1)[-1].lower()
if file_ext not in allowed_extensions and '' not in allowed_extensions: if file_ext not in allowed_extensions and '' not in allowed_extensions:
flash( flash(
_("File extension '%(ext)s' is not allowed to be uploaded to this server", _("File extension '%(ext)s' is not allowed to be uploaded to this server",
ext=file_ext), category="error") 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: else:
flash(_('File to be uploaded must have an extension'), category="error") 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 # extract metadata from file
try: 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) log.error("File %s could not saved to temp dir", requested_file.filename)
flash(_("File %(filename)s could not saved to temp dir", flash(_("File %(filename)s could not saved to temp dir",
filename=requested_file.filename), category="error") 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 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=""): def render_delete_book_result(book_format, json_response, warning, book_id, location=""):
if book_format: if book_format:
if json_response: 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", "type": "success",
"format": book_format, "format": book_format,
"message": _('Book Format Successfully Deleted')}]) "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)) return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
else: else:
if json_response: 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", "type": "success",
"format": book_format, "format": book_format,
"message": _('Book Successfully Deleted')}]) "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()) result, error = helper.delete_book(book, config.get_book_path(), book_format=book_format.upper())
if not result: if not result:
if json_response: 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", "type": "danger",
"format": "", "format": "",
"message": error}]) "message": error}])
@ -914,7 +898,7 @@ def delete_book_from_table(book_id, book_format, json_response, location=""):
log.error_or_exception(ex) log.error_or_exception(ex)
calibre_db.session.rollback() calibre_db.session.rollback()
if json_response: 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", "type": "danger",
"format": "", "format": "",
"message": ex}]) "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) return render_delete_book_result(book_format, json_response, warning, book_id, location)
message = _("You are missing permissions to delete books") message = _("You are missing permissions to delete books")
if json_response: 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", "type": "danger",
"format": "", "format": "",
"message": message}) "message": message})

View File

@ -34,6 +34,15 @@ except ImportError as e:
error = "Cannot import python-magic, checking uploaded file metadata will not work: {}".format(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(): def get_temp_dir():
tmp_dir = os.path.join(gettempdir(), 'calibre_web') tmp_dir = os.path.join(gettempdir(), 'calibre_web')
if not os.path.isdir(tmp_dir): if not os.path.isdir(tmp_dir):
@ -54,7 +63,7 @@ def validate_mime_type(file_buffer, allowed_extensions):
allowed_mimetypes = list() allowed_mimetypes = list()
for x in allowed_extensions: for x in allowed_extensions:
try: try:
allowed_mimetypes.append(mimetypes.types_map["." + x]) allowed_mimetypes.append(get_mimetype("." + x))
except KeyError: except KeyError:
log.error("Unkown mimetype for Extension: {}".format(x)) log.error("Unkown mimetype for Extension: {}".format(x))
tmp_mime_type = mime.from_buffer(file_buffer.read()) tmp_mime_type = mime.from_buffer(file_buffer.read())

View File

@ -30,7 +30,7 @@ import requests
import unidecode import unidecode
from uuid import uuid4 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 gettext as _
from flask_babel import lazy_gettext as N_ from flask_babel import lazy_gettext as N_
from flask_babel import get_locale from flask_babel import get_locale
@ -974,7 +974,8 @@ def do_download_file(book, book_format, client, data, headers):
# ToDo Check headers parameter # ToDo Check headers parameter
for element in headers: for element in headers:
response.headers[element[0]] = element[1] 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 return response

View File

@ -21,10 +21,10 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import datetime import datetime
import json # import json
from urllib.parse import unquote_plus 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 get_locale
from flask_babel import gettext as _ from flask_babel import gettext as _
@ -451,7 +451,7 @@ def get_database_stats():
stat['authors'] = calibre_db.session.query(db.Authors).count() stat['authors'] = calibre_db.session.query(db.Authors).count()
stat['categories'] = calibre_db.session.query(db.Tags).count() stat['categories'] = calibre_db.session.query(db.Tags).count()
stat['series'] = calibre_db.session.query(db.Series).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>") @opds.route("/opds/thumb_240_240/<book_id>")

View File

@ -23,7 +23,7 @@ import json
import os import os
import sys 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 .cw_login import current_user
from flask_babel import get_locale from flask_babel import get_locale
from sqlalchemy.exc import InvalidRequestError, OperationalError from sqlalchemy.exc import InvalidRequestError, OperationalError
@ -89,7 +89,7 @@ def metadata_provider():
provider.append( provider.append(
{"name": c.__name__, "active": ac, "initial": ac, "id": c.__id__} {"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"]) @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) provider = next((c for c in cl if c.__id__ == prov_name), None)
if provider is not None: if provider is not None:
data = provider.search(new_state.get("query", "")) data = provider.search(new_state.get("query", ""))
return Response( return make_response(jsonify([asdict(x) for x in data]))
json.dumps([asdict(x) for x in data]), mimetype="application/json"
)
return "" return ""
@ -138,4 +136,4 @@ def metadata_search():
} }
for future in concurrent.futures.as_completed(meta): for future in concurrent.futures.as_completed(meta):
data.extend([asdict(x) for x in future.result() if x]) data.extend([asdict(x) for x in future.result() if x])
return Response(json.dumps(data), mimetype="application/json") return make_response(jsonify(data))

View File

@ -210,13 +210,13 @@ class TaskConvert(CalibreTask):
else: else:
error_message = N_('%(format)s format not found on disk', format=format_new_ext.upper()) error_message = N_('%(format)s format not found on disk', format=format_new_ext.upper())
local_db.session.close() local_db.session.close()
log.info("ebook converter failed with error while converting book") log.info("ebook converter failed with error while converting book")
if not error_message: if not error_message:
error_message = N_('Ebook converter failed with unknown error') error_message = N_('Ebook converter failed with unknown error')
else: else:
log.error(error_message) log.error(error_message)
self._handleError(error_message) self._handleError(error_message)
return return
def _convert_kepubify(self, file_path, format_old_ext, format_new_ext): def _convert_kepubify(self, file_path, format_old_ext, format_new_ext):
if config.config_embed_metadata and config.config_binariesdir: if config.config_embed_metadata and config.config_binariesdir:

View File

@ -25,8 +25,7 @@ import chardet # dependency of requests
import copy import copy
from importlib.metadata import metadata from importlib.metadata import metadata
from flask import Blueprint, jsonify from flask import Blueprint, jsonify, request, redirect, send_from_directory, make_response, flash, abort, url_for
from flask import request, redirect, send_from_directory, make_response, flash, abort, url_for, Response
from flask import session as flask_session from flask import session as flask_session
from flask_babel import gettext as _ from flask_babel import gettext as _
from flask_babel import get_locale from flask_babel import get_locale
@ -1198,13 +1197,14 @@ def serve_book(book_id, book_format, anyname):
if not data: if not data:
return "File not in Database" return "File not in Database"
range_header = request.headers.get('Range', None) 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: if config.config_use_google_drive:
try: try:
headers = Headers() headers = Headers()
headers["Content-Type"] = mimetypes.types_map.get('.' + book_format, "application/octet-stream") headers["Content-Type"] = mimetypes.types_map.get('.' + book_format, "application/octet-stream")
if not range_header: if not range_header:
log.info('Serving book: %s', data.name)
headers['Accept-Ranges'] = 'bytes' headers['Accept-Ranges'] = 'bytes'
df = getFileFromEbooksFolder(book.path, data.name + "." + book_format) df = getFileFromEbooksFolder(book.path, data.name + "." + book_format)
return do_gdrive_download(df, headers, (book_format.upper() == 'TXT')) 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" return "File Not Found"
else: else:
if book_format.upper() == 'TXT': if book_format.upper() == 'TXT':
log.info('Serving book: %s', data.name)
try: try:
rawdata = open(os.path.join(config.get_book_path(), book.path, data.name + "." + book_format), rawdata = open(os.path.join(config.get_book_path(), book.path, data.name + "." + book_format),
"rb").read() "rb").read()
@ -1234,7 +1233,6 @@ def serve_book(book_id, book_format, anyname):
response = make_response( response = make_response(
send_from_directory(os.path.join(config.get_book_path(), book.path), data.name + "." + book_format)) send_from_directory(os.path.join(config.get_book_path(), book.path), data.name + "." + book_format))
if not range_header: if not range_header:
log.info('Serving book: %s', data.name)
response.headers['Accept-Ranges'] = 'bytes' response.headers['Accept-Ranges'] = 'bytes'
return response return response
@ -1253,8 +1251,7 @@ def download_link(book_id, book_format, anyname):
@download_required @download_required
def send_to_ereader(book_id, book_format, convert): def send_to_ereader(book_id, book_format, convert):
if not config.get_mail_server_configured(): if not config.get_mail_server_configured():
response = [{'type': "danger", 'message': _("Please configure the SMTP mail settings first...")}] return make_response(jsonify(type="danger", message=_("Please configure the SMTP mail settings first...")))
return Response(json.dumps(response), mimetype='application/json')
elif current_user.kindle_mail: elif current_user.kindle_mail:
result = send_mail(book_id, book_format, convert, current_user.kindle_mail, config.get_book_path(), result = send_mail(book_id, book_format, convert, current_user.kindle_mail, config.get_book_path(),
current_user.name) 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)}] response = [{'type': "danger", 'message': _("Oops! There was an error sending book: %(res)s", res=result)}]
else: else:
response = [{'type': "danger", 'message': _("Oops! Please update your profile with a valid eReader Email.")}] 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 ################################################################## # ################################### Login Logout ##################################################################

View File

@ -1,13 +1,13 @@
# GDrive Integration # GDrive Integration
google-api-python-client>=1.7.11,<2.200.0 google-api-python-client>=1.7.11,<2.200.0
gevent>20.6.0,<24.3.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 httplib2>=0.9.2,<0.23.0
oauth2client>=4.0.0,<4.1.4 oauth2client>=4.0.0,<4.1.4
uritemplate>=3.0.0,<4.2.0 uritemplate>=3.0.0,<4.2.0
pyasn1-modules>=0.0.8,<0.5.0 pyasn1-modules>=0.0.8,<0.5.0
pyasn1>=0.1.9,<0.7.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 PyYAML>=3.12,<6.1
rsa>=3.4.2,<4.10.0 rsa>=3.4.2,<4.10.0

View File

@ -37,20 +37,20 @@
<div class="row"> <div class="row">
<div class="col-xs-6 col-md-6 col-sm-offset-3" style="margin-top:50px;"> <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> </div>
<div class="row"> <div class="row">
<div class="col-xs-6 col-md-6 col-sm-offset-3"> <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> </div>
<div class="row"> <div class="row">
<div class="col-xs-6 col-md-6 col-sm-offset-3"> <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> </div>
</div> </div>
@ -1023,13 +1023,13 @@
<tr id="su" class="skipClass"> <tr id="su" class="passClass">
<td>TestEditAdditionalBooks</td> <td>TestEditAdditionalBooks</td>
<td class="text-center">18</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">0</td> <td class="text-center">0</td>
<td class="text-center">1</td>
<td class="text-center"> <td class="text-center">
<a onclick="showClassDetail('c12', 18)">Detail</a> <a onclick="showClassDetail('c12', 18)">Detail</a>
</td> </td>
@ -1172,11 +1172,11 @@
<tr id='st12.16' class='none bg-warning'> <tr id='pt12.16' class='hiddenRow bg-success'>
<td> <td>
<div class='testcase'>TestEditAdditionalBooks - test_xss_author_edit</div> <div class='testcase'>TestEditAdditionalBooks - test_xss_author_edit</div>
</td> </td>
<td colspan='6' align='center'>SKIP</td> <td colspan='6' align='center'>PASS</td>
</tr> </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>TestEditBooksOnGdrive</td>
<td class="text-center">18</td> <td class="text-center">18</td>
<td class="text-center">18</td> <td class="text-center">16</td>
<td class="text-center">0</td> <td class="text-center">2</td>
<td class="text-center">0</td> <td class="text-center">0</td>
<td class="text-center">0</td> <td class="text-center">0</td>
<td class="text-center"> <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> <td>
<div class='testcase'>TestEditBooksOnGdrive - test_edit_rating</div> <div class='testcase'>TestEditBooksOnGdrive - test_edit_rating</div>
</td> </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">&times;</span></button>
</div>
<div class="text-left pull-left">
<pre class="text-left">Traceback (most recent call last):
File &#34;/home/ozzie/Development/calibre-web-test/test/test_edit_ebooks_gdrive.py&#34;, line 632, in test_edit_rating
self.assertEqual(4, values[&#39;rating&#39;])
AssertionError: 4 != 0</pre>
</div>
<div class="clearfix"></div>
</div>
<!--css div popup end-->
</td>
</tr> </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> <td>
<div class='testcase'>TestEditBooksOnGdrive - test_watch_metadata</div> <div class='testcase'>TestEditBooksOnGdrive - test_watch_metadata</div>
</td> </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">&times;</span></button>
</div>
<div class="text-left pull-left">
<pre class="text-left">Traceback (most recent call last):
File &#34;/home/ozzie/Development/calibre-web-test/test/test_edit_ebooks_gdrive.py&#34;, line 976, in test_watch_metadata
self.assertNotIn(&#39;series&#39;, book)
AssertionError: &#39;series&#39; unexpectedly found in {&#39;id&#39;: 5, &#39;reader&#39;: [], &#39;title&#39;: &#39;testbook&#39;, &#39;author&#39;: [&#39;John Döe&#39;], &#39;rating&#39;: 0, &#39;languages&#39;: [&#39;English&#39;], &#39;identifier&#39;: [], &#39;cover&#39;: &#39;/cover/5/og?c=1733511155&#39;, &#39;tag&#39;: [], &#39;publisher&#39;: [&#39;Randomhäus&#39;], &#39;pubdate&#39;: &#39;Jan 19, 2017&#39;, &#39;comment&#39;: &#39;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&#39;, &#39;add_shelf&#39;: [], &#39;del_shelf&#39;: [], &#39;edit_enable&#39;: True, &#39;kindle&#39;: None, &#39;kindlebtn&#39;: None, &#39;download&#39;: [&#39;EPUB\n (6.7 kB)&#39;], &#39;read&#39;: False, &#39;archived&#39;: False, &#39;series_all&#39;: &#39;Book 1 of test&#39;, &#39;series_index&#39;: &#39;1&#39;, &#39;series&#39;: &#39;test&#39;, &#39;cust_columns&#39;: []}</pre>
</div>
<div class="clearfix"></div>
</div>
<!--css div popup end-->
</td>
</tr> </tr>
@ -5801,10 +5841,10 @@ IndexError: list index out of range</pre>
<tr id='total_row' class="text-center bg-grey"> <tr id='total_row' class="text-center bg-grey">
<td>Total</td> <td>Total</td>
<td>523</td> <td>523</td>
<td>514</td> <td>513</td>
<td>0</td> <td>2</td>
<td>1</td> <td>1</td>
<td>8</td> <td>7</td>
<td>&nbsp;</td> <td>&nbsp;</td>
</tr> </tr>
</table> </table>
@ -6348,7 +6388,7 @@ IndexError: list index out of range</pre>
</div> </div>
<script> <script>
drawCircle(514, 0, 1, 8); drawCircle(513, 2, 1, 7);
showCase(5); showCase(5);
</script> </script>