mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-06-23 15:30:45 -04:00
Content server: Allow managing the data files associated with a book by clicking the three dots in the top right corner of the book's page and choosing "Manage data files". Fixes #2091646 [Download the Data Files from the web browser](https://bugs.launchpad.net/calibre/+bug/2091646)
This commit is contained in:
parent
89e88b9678
commit
0177afafe6
@ -2058,6 +2058,33 @@ class DB:
|
|||||||
with src:
|
with src:
|
||||||
yield relpath, src, stat_result
|
yield relpath, src, stat_result
|
||||||
|
|
||||||
|
def remove_extra_files(self, book_path, relpaths, permanent):
|
||||||
|
bookdir = os.path.join(self.library_path, book_path)
|
||||||
|
errors = {}
|
||||||
|
for relpath in relpaths:
|
||||||
|
path = os.path.abspath(os.path.join(bookdir, relpath))
|
||||||
|
if not self.normpath(path).startswith(self.normpath(bookdir)):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
if permanent:
|
||||||
|
try:
|
||||||
|
os.remove(make_long_path_useable(path))
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
if not iswindows:
|
||||||
|
raise
|
||||||
|
time.sleep(1)
|
||||||
|
os.remove(make_long_path_useable(path))
|
||||||
|
else:
|
||||||
|
from calibre.utils.recycle_bin import recycle
|
||||||
|
recycle(make_long_path_useable(path))
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
errors[relpath] = e
|
||||||
|
return errors
|
||||||
|
|
||||||
def rename_extra_file(self, relpath, newrelpath, book_path, replace=True):
|
def rename_extra_file(self, relpath, newrelpath, book_path, replace=True):
|
||||||
bookdir = os.path.join(self.library_path, book_path)
|
bookdir = os.path.join(self.library_path, book_path)
|
||||||
src = os.path.abspath(os.path.join(bookdir, relpath))
|
src = os.path.abspath(os.path.join(bookdir, relpath))
|
||||||
@ -2077,6 +2104,8 @@ class DB:
|
|||||||
def add_extra_file(self, relpath, stream, book_path, replace=True, auto_rename=False):
|
def add_extra_file(self, relpath, stream, book_path, replace=True, auto_rename=False):
|
||||||
bookdir = os.path.join(self.library_path, book_path)
|
bookdir = os.path.join(self.library_path, book_path)
|
||||||
dest = os.path.abspath(os.path.join(bookdir, relpath))
|
dest = os.path.abspath(os.path.join(bookdir, relpath))
|
||||||
|
if not self.normpath(dest).startswith(self.normpath(bookdir)):
|
||||||
|
return None
|
||||||
if not replace and os.path.exists(make_long_path_useable(dest)):
|
if not replace and os.path.exists(make_long_path_useable(dest)):
|
||||||
if not auto_rename:
|
if not auto_rename:
|
||||||
return None
|
return None
|
||||||
|
@ -20,7 +20,7 @@ from io import DEFAULT_BUFFER_SIZE, BytesIO
|
|||||||
from queue import Queue
|
from queue import Queue
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
from time import mktime, monotonic, sleep, time
|
from time import mktime, monotonic, sleep, time
|
||||||
from typing import NamedTuple, Optional, Tuple
|
from typing import Iterable, NamedTuple, Optional, Tuple
|
||||||
|
|
||||||
from calibre import as_unicode, detect_ncpus, isbytestring
|
from calibre import as_unicode, detect_ncpus, isbytestring
|
||||||
from calibre.constants import iswindows, preferred_encoding
|
from calibre.constants import iswindows, preferred_encoding
|
||||||
@ -3357,6 +3357,17 @@ class Cache:
|
|||||||
self._clear_extra_files_cache(dest_id)
|
self._clear_extra_files_cache(dest_id)
|
||||||
return added
|
return added
|
||||||
|
|
||||||
|
@write_api
|
||||||
|
def remove_extra_files(self, book_id: int, relpaths: Iterable[str], permanent=False) -> dict[str, Exception | None]:
|
||||||
|
'''
|
||||||
|
Delete the specified extra files, either to Recycle Bin or permanently.
|
||||||
|
'''
|
||||||
|
path = self._field_for('path', book_id)
|
||||||
|
if path:
|
||||||
|
self._clear_extra_files_cache(book_id)
|
||||||
|
return self.backend.remove_extra_files(path, relpaths, permanent)
|
||||||
|
return dict.fromkeys(relpaths)
|
||||||
|
|
||||||
@read_api
|
@read_api
|
||||||
def list_extra_files(self, book_id, use_cache=False, pattern='') -> Tuple[ExtraFile, ...]:
|
def list_extra_files(self, book_id, use_cache=False, pattern='') -> Tuple[ExtraFile, ...]:
|
||||||
'''
|
'''
|
||||||
|
@ -16,7 +16,7 @@ from threading import Lock
|
|||||||
|
|
||||||
from calibre import fit_image, guess_type, sanitize_file_name
|
from calibre import fit_image, guess_type, sanitize_file_name
|
||||||
from calibre.constants import config_dir, iswindows
|
from calibre.constants import config_dir, iswindows
|
||||||
from calibre.db.constants import RESOURCE_URL_SCHEME
|
from calibre.db.constants import DATA_DIR_NAME, DATA_FILE_PATTERN, RESOURCE_URL_SCHEME
|
||||||
from calibre.db.errors import NoSuchFormat
|
from calibre.db.errors import NoSuchFormat
|
||||||
from calibre.ebooks.covers import cprefs, generate_cover, override_prefs, scale_cover, set_use_roman
|
from calibre.ebooks.covers import cprefs, generate_cover, override_prefs, scale_cover, set_use_roman
|
||||||
from calibre.ebooks.metadata import authors_to_string
|
from calibre.ebooks.metadata import authors_to_string
|
||||||
@ -24,6 +24,7 @@ from calibre.ebooks.metadata.meta import set_metadata
|
|||||||
from calibre.ebooks.metadata.opf2 import metadata_to_opf
|
from calibre.ebooks.metadata.opf2 import metadata_to_opf
|
||||||
from calibre.library.save_to_disk import find_plugboard
|
from calibre.library.save_to_disk import find_plugboard
|
||||||
from calibre.srv.errors import BookNotFound, HTTPBadRequest, HTTPNotFound
|
from calibre.srv.errors import BookNotFound, HTTPBadRequest, HTTPNotFound
|
||||||
|
from calibre.srv.metadata import encode_stat_result
|
||||||
from calibre.srv.routes import endpoint, json
|
from calibre.srv.routes import endpoint, json
|
||||||
from calibre.srv.utils import get_db, get_use_roman, http_date
|
from calibre.srv.utils import get_db, get_use_roman, http_date
|
||||||
from calibre.utils.config_base import tweaks
|
from calibre.utils.config_base import tweaks
|
||||||
@ -34,7 +35,8 @@ from calibre.utils.localization import _
|
|||||||
from calibre.utils.resources import get_image_path as I
|
from calibre.utils.resources import get_image_path as I
|
||||||
from calibre.utils.resources import get_path as P
|
from calibre.utils.resources import get_path as P
|
||||||
from calibre.utils.shared_file import share_open
|
from calibre.utils.shared_file import share_open
|
||||||
from polyglot.binary import as_hex_unicode
|
from calibre.utils.speedups import ReadOnlyFileBuffer
|
||||||
|
from polyglot.binary import as_hex_unicode, from_base64_bytes
|
||||||
from polyglot.urllib import quote
|
from polyglot.urllib import quote
|
||||||
|
|
||||||
plugboard_content_server_value = 'content_server'
|
plugboard_content_server_value = 'content_server'
|
||||||
@ -518,3 +520,72 @@ def set_note(ctx, rd, field, item_id, library_id):
|
|||||||
db.set_notes_for(field, item_id, db_html, searchable_text, resources)
|
db.set_notes_for(field, item_id, db_html, searchable_text, resources)
|
||||||
rd.outheaders['Content-Type'] = 'text/html; charset=UTF-8'
|
rd.outheaders['Content-Type'] = 'text/html; charset=UTF-8'
|
||||||
return srv_html
|
return srv_html
|
||||||
|
|
||||||
|
|
||||||
|
def data_file(rd, fname, path, stat_result):
|
||||||
|
cd = rd.query.get('content_disposition', 'attachment')
|
||||||
|
rd.outheaders['Content-Disposition'] = '''{}; filename="{}"; filename*=utf-8''{}'''.format(
|
||||||
|
cd, fname_for_content_disposition(fname), fname_for_content_disposition(fname, as_encoded_unicode=True))
|
||||||
|
return rd.filesystem_file_with_custom_etag(share_open(path, 'rb'), stat_result.st_dev, stat_result.st_ino, stat_result.st_size, stat_result.st_mtime)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@endpoint('/data-files/get/{book_id}/{relpath}/{library_id=None}', types={'book_id': int})
|
||||||
|
def get_data_file(ctx, rd, book_id, relpath, library_id):
|
||||||
|
db = get_db(ctx, rd, library_id)
|
||||||
|
if db is None:
|
||||||
|
raise HTTPNotFound(f'Library {library_id} not found')
|
||||||
|
for ef in db.list_extra_files(book_id, pattern=DATA_FILE_PATTERN):
|
||||||
|
if ef.relpath == relpath:
|
||||||
|
return data_file(rd, relpath.rpartition('/')[2], ef.file_path, ef.stat_result)
|
||||||
|
raise HTTPNotFound(f'No data file {relpath} in book {book_id} in library {library_id}')
|
||||||
|
|
||||||
|
|
||||||
|
def strerr(e: Exception):
|
||||||
|
# Dont leak the filepath in the error response
|
||||||
|
if isinstance(e, OSError):
|
||||||
|
return e.strerror or str(e)
|
||||||
|
return str(e)
|
||||||
|
|
||||||
|
|
||||||
|
@endpoint('/data-files/upload/{book_id}/{library_id=None}', needs_db_write=True, methods={'POST'}, types={'book_id': int}, postprocess=json)
|
||||||
|
def upload_data_files(ctx, rd, book_id, library_id):
|
||||||
|
db = get_db(ctx, rd, library_id)
|
||||||
|
if db is None:
|
||||||
|
raise HTTPNotFound(f'Library {library_id} not found')
|
||||||
|
files = {}
|
||||||
|
try:
|
||||||
|
recvd = load_json_file(rd.request_body_file)
|
||||||
|
for x in recvd:
|
||||||
|
data = from_base64_bytes(x['data_url'].split(',', 1)[-1])
|
||||||
|
relpath = f'{DATA_DIR_NAME}/{x["name"]}'
|
||||||
|
files[relpath] = ReadOnlyFileBuffer(data, x['name'])
|
||||||
|
except Exception as err:
|
||||||
|
raise HTTPBadRequest(f'Invalid query: {err}')
|
||||||
|
err = ''
|
||||||
|
try:
|
||||||
|
db.add_extra_files(book_id, files)
|
||||||
|
except Exception as e:
|
||||||
|
err = strerr(e)
|
||||||
|
data_files = db.list_extra_files(book_id, use_cache=False, pattern=DATA_FILE_PATTERN)
|
||||||
|
return {'error': err, 'data_files': {e.relpath: encode_stat_result(e.stat_result) for e in data_files}}
|
||||||
|
|
||||||
|
|
||||||
|
@endpoint('/data-files/remove/{book_id}/{library_id=None}', needs_db_write=True, methods={'POST'}, types={'book_id': int}, postprocess=json)
|
||||||
|
def remove_data_files(ctx, rd, book_id, library_id):
|
||||||
|
db = get_db(ctx, rd, library_id)
|
||||||
|
if db is None:
|
||||||
|
raise HTTPNotFound(f'Library {library_id} not found')
|
||||||
|
try:
|
||||||
|
relpaths = load_json_file(rd.request_body_file)
|
||||||
|
if not isinstance(relpaths, list):
|
||||||
|
raise Exception('files to remove must be a list')
|
||||||
|
except Exception as err:
|
||||||
|
raise HTTPBadRequest(f'Invalid query: {err}')
|
||||||
|
|
||||||
|
errors = db.remove_extra_files(book_id, relpaths, permanent=True)
|
||||||
|
data_files = db.list_extra_files(book_id, use_cache=False, pattern=DATA_FILE_PATTERN)
|
||||||
|
ans = {'data_files': {e.relpath: encode_stat_result(e.stat_result) for e in data_files}}
|
||||||
|
if errors:
|
||||||
|
ans['errors'] = {k: strerr(v) for k, v in errors.items() if v is not None}
|
||||||
|
return ans
|
||||||
|
@ -11,6 +11,7 @@ from threading import Lock
|
|||||||
|
|
||||||
from calibre.constants import config_dir
|
from calibre.constants import config_dir
|
||||||
from calibre.db.categories import Tag, category_display_order
|
from calibre.db.categories import Tag, category_display_order
|
||||||
|
from calibre.db.constants import DATA_FILE_PATTERN
|
||||||
from calibre.ebooks.metadata.sources.identify import urls_from_identifiers
|
from calibre.ebooks.metadata.sources.identify import urls_from_identifiers
|
||||||
from calibre.library.comments import comments_to_html, markdown
|
from calibre.library.comments import comments_to_html, markdown
|
||||||
from calibre.library.field_metadata import category_icon_map
|
from calibre.library.field_metadata import category_icon_map
|
||||||
@ -64,6 +65,12 @@ def add_field(field, db, book_id, ans, field_metadata):
|
|||||||
ans[field] = val
|
ans[field] = val
|
||||||
|
|
||||||
|
|
||||||
|
def encode_stat_result(s: os.stat_result) -> dict[str, int]:
|
||||||
|
return {
|
||||||
|
'size': s.st_size, 'mtime_ns': s.st_mtime_ns,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def book_as_json(db, book_id):
|
def book_as_json(db, book_id):
|
||||||
db = db.new_api
|
db = db.new_api
|
||||||
with db.safe_read_lock:
|
with db.safe_read_lock:
|
||||||
@ -94,6 +101,9 @@ def book_as_json(db, book_id):
|
|||||||
x = db.items_with_notes_in_book(book_id)
|
x = db.items_with_notes_in_book(book_id)
|
||||||
if x:
|
if x:
|
||||||
ans['items_with_notes'] = {field: {v: k for k, v in items.items()} for field, items in x.items()}
|
ans['items_with_notes'] = {field: {v: k for k, v in items.items()} for field, items in x.items()}
|
||||||
|
data_files = db.list_extra_files(book_id, use_cache=True, pattern=DATA_FILE_PATTERN)
|
||||||
|
if data_files:
|
||||||
|
ans['data_files'] = {e.relpath: encode_stat_result(e.stat_result) for e in data_files}
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
|
||||||
|
@ -8,11 +8,11 @@ import traceback
|
|||||||
from ajax import ajax, ajax_send, encode_query_component
|
from ajax import ajax, ajax_send, encode_query_component
|
||||||
from book_list.delete_book import refresh_after_delete, start_delete_book
|
from book_list.delete_book import refresh_after_delete, start_delete_book
|
||||||
from book_list.globals import get_session_data
|
from book_list.globals import get_session_data
|
||||||
from book_list.item_list import create_item, create_item_list
|
from book_list.item_list import create_item, create_item_list, create_side_action
|
||||||
from book_list.library_data import (
|
from book_list.library_data import (
|
||||||
all_libraries, book_after, book_metadata, cover_url, current_library_id,
|
all_libraries, book_after, book_metadata, cover_url, current_library_id,
|
||||||
current_virtual_library, download_url, library_data, load_status,
|
current_virtual_library, download_url, library_data, load_status,
|
||||||
set_book_metadata
|
set_book_metadata, download_data_file_url
|
||||||
)
|
)
|
||||||
from book_list.router import back, home, open_book, report_a_load_failure, show_note
|
from book_list.router import back, home, open_book, report_a_load_failure, show_note
|
||||||
from book_list.theme import (
|
from book_list.theme import (
|
||||||
@ -23,8 +23,9 @@ from book_list.ui import query_as_href, set_panel_handler, show_panel
|
|||||||
from book_list.views import search_query_for
|
from book_list.views import search_query_for
|
||||||
from date import format_date
|
from date import format_date
|
||||||
from dom import add_extra_css, build_rule, clear, ensure_id, svgicon, unique_id
|
from dom import add_extra_css, build_rule, clear, ensure_id, svgicon, unique_id
|
||||||
|
from file_uploads import update_status_widget, upload_files_widget, upload_status_widget
|
||||||
from gettext import gettext as _
|
from gettext import gettext as _
|
||||||
from modals import create_custom_dialog, error_dialog, warning_dialog
|
from modals import create_custom_dialog, error_dialog, warning_dialog, question_dialog
|
||||||
from read_book.touch import (
|
from read_book.touch import (
|
||||||
copy_touch, install_handlers, interpret_single_gesture, touch_id, update_touch
|
copy_touch, install_handlers, interpret_single_gesture, touch_id, update_touch
|
||||||
)
|
)
|
||||||
@ -40,6 +41,7 @@ bd_counter = 0
|
|||||||
CLASS_NAME = 'book-details-panel'
|
CLASS_NAME = 'book-details-panel'
|
||||||
SEARCH_INTERNET_CLASS = 'book-details-search-internet'
|
SEARCH_INTERNET_CLASS = 'book-details-search-internet'
|
||||||
COPY_TO_LIBRARY_CLASS = 'book-details-copy-to-library'
|
COPY_TO_LIBRARY_CLASS = 'book-details-copy-to-library'
|
||||||
|
DATA_FILES_CLASS = 'book-details-data-files'
|
||||||
FORMAT_PRIORITIES = [
|
FORMAT_PRIORITIES = [
|
||||||
'EPUB', 'AZW3', 'DOCX', 'LIT', 'MOBI', 'ODT', 'RTF', 'MD', 'MARKDOWN', 'TXT', 'PDF'
|
'EPUB', 'AZW3', 'DOCX', 'LIT', 'MOBI', 'ODT', 'RTF', 'MD', 'MARKDOWN', 'TXT', 'PDF'
|
||||||
]
|
]
|
||||||
@ -469,6 +471,9 @@ add_extra_css(def():
|
|||||||
|
|
||||||
sel = '.' + COPY_TO_LIBRARY_CLASS
|
sel = '.' + COPY_TO_LIBRARY_CLASS
|
||||||
style += build_rule(sel, margin='1ex 1em')
|
style += build_rule(sel, margin='1ex 1em')
|
||||||
|
|
||||||
|
sel = '.' + DATA_FILES_CLASS
|
||||||
|
style += build_rule(sel, margin='1ex 1em')
|
||||||
return style
|
return style
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -827,6 +832,11 @@ def create_more_actions_panel(container_id):
|
|||||||
action=def():
|
action=def():
|
||||||
show_subsequent_panel('copy_to_library', replace=True)
|
show_subsequent_panel('copy_to_library', replace=True)
|
||||||
),
|
),
|
||||||
|
create_item(_('Manage data files'), subtitle=_('Upload or download additional data files to this book'),
|
||||||
|
action=def():
|
||||||
|
show_subsequent_panel('data_files', replace=True)
|
||||||
|
),
|
||||||
|
|
||||||
]
|
]
|
||||||
container.appendChild(E.div())
|
container.appendChild(E.div())
|
||||||
create_item_list(container.lastChild, items)
|
create_item_list(container.lastChild, items)
|
||||||
@ -849,6 +859,196 @@ def url_for(template, data):
|
|||||||
return template.format(title=eqc(data.title), author=eqc(data.author))
|
return template.format(title=eqc(data.title), author=eqc(data.author))
|
||||||
|
|
||||||
|
|
||||||
|
EXTENSIONS_TO_OPEN_IN_BROWSER = {
|
||||||
|
'pdf': True,
|
||||||
|
'jpeg': True,
|
||||||
|
'jpg': True,
|
||||||
|
'png': True,
|
||||||
|
'gif': True,
|
||||||
|
'webp': True,
|
||||||
|
'txt': True,
|
||||||
|
'html': True,
|
||||||
|
'htm': True,
|
||||||
|
'xhtml': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
def download_data_file(book_id, relpath):
|
||||||
|
ext = relpath.toLowerCase().rpartition('.')[2]
|
||||||
|
open_inline = bool(EXTENSIONS_TO_OPEN_IN_BROWSER[ext])
|
||||||
|
cd = 'inline' if open_inline else 'attachment'
|
||||||
|
url = download_data_file_url(book_id, relpath, cd)
|
||||||
|
if open_inline:
|
||||||
|
window.open(url)
|
||||||
|
else:
|
||||||
|
window.location = url
|
||||||
|
|
||||||
|
|
||||||
|
def on_progress(container_id, book_id, loaded, total, xhr):
|
||||||
|
container = document.getElementById(container_id)
|
||||||
|
if container and total:
|
||||||
|
update_status_widget(container, loaded, total)
|
||||||
|
|
||||||
|
|
||||||
|
def data_files_submitted(container_id, book_id, end_type, xhr, ev):
|
||||||
|
if end_type is 'abort':
|
||||||
|
back()
|
||||||
|
return
|
||||||
|
if end_type is not 'load':
|
||||||
|
error_dialog(_('Failed to upload files to server'), _(
|
||||||
|
'Uploading data files for book: {} failed.').format(book_id), xhr.error_html)
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
res = JSON.parse(xhr.responseText)
|
||||||
|
except Exception as err:
|
||||||
|
error_dialog(_('Could not upload data files for book'), _('Server returned an invalid response'), err.toString())
|
||||||
|
return
|
||||||
|
mi = book_metadata(book_id)
|
||||||
|
if mi:
|
||||||
|
mi.data_files = res.data_files
|
||||||
|
if res.error:
|
||||||
|
error_dialog(_('Could not upload data files for book'), _('There was an error: ') + res.error)
|
||||||
|
else:
|
||||||
|
back()
|
||||||
|
|
||||||
|
|
||||||
|
def submit_data_files(top_container_id, container_id, book_id, added):
|
||||||
|
c = document.getElementById(container_id)
|
||||||
|
clear(c)
|
||||||
|
c.appendChild(E.div(style='margin: 1ex 1rem', _('Uploading changes to server, please wait...')))
|
||||||
|
w = upload_status_widget()
|
||||||
|
c.appendChild(w)
|
||||||
|
ajax_send(
|
||||||
|
f'data-files/upload/{book_id}/{current_library_id()}', added,
|
||||||
|
data_files_submitted.bind(None, container_id, book_id), on_progress.bind(None, container_id, book_id))
|
||||||
|
|
||||||
|
|
||||||
|
def files_added(check_existing, top_container_id, book_id, container_id, files):
|
||||||
|
container = document.getElementById(container_id)
|
||||||
|
mi = book_metadata(book_id)
|
||||||
|
if not container or not mi or not files[0]:
|
||||||
|
return
|
||||||
|
added = v'[]'
|
||||||
|
|
||||||
|
def exists(fname):
|
||||||
|
for relpath in mi.data_files:
|
||||||
|
q = relpath.partition('/')[2]
|
||||||
|
if q is fname:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
if check_existing:
|
||||||
|
existing = [file.name for file in files if exists(file.name)]
|
||||||
|
if existing.length:
|
||||||
|
return question_dialog(_('Replace existing data files?'), _('The following data files already exist, are you sure you want to replace them?') + ' ' + existing.join(', '), def (yes):
|
||||||
|
if yes:
|
||||||
|
files_added(False, top_container_id, book_id, container_id, files)
|
||||||
|
else:
|
||||||
|
back()
|
||||||
|
)
|
||||||
|
|
||||||
|
for file in files:
|
||||||
|
data = {'name': file.name, 'size': file.size, 'type': file.type, 'data_url': None}
|
||||||
|
added.push(data)
|
||||||
|
r = FileReader()
|
||||||
|
r.onload = def(evt):
|
||||||
|
data.data_url = evt.target.result
|
||||||
|
for entry in added:
|
||||||
|
if not entry.data_url:
|
||||||
|
return
|
||||||
|
submit_data_files(top_container_id, container_id, book_id, added)
|
||||||
|
r.readAsDataURL(file)
|
||||||
|
|
||||||
|
|
||||||
|
def upload_data_file(container_id):
|
||||||
|
if not render_book.book_id or not book_metadata(render_book.book_id):
|
||||||
|
return return_to_book_details()
|
||||||
|
book_id = render_book.book_id
|
||||||
|
container = document.getElementById(container_id)
|
||||||
|
create_top_bar(container, title=_('Upload data files'), action=back, icon='close')
|
||||||
|
div = E.div(id=unique_id())
|
||||||
|
container.appendChild(div)
|
||||||
|
upload_files_widget(div, files_added.bind(None, True, container_id, book_id), _(
|
||||||
|
'Upload files by <a>selecting the files</a> or drag and drop of the files here.'),
|
||||||
|
single_file=False)
|
||||||
|
|
||||||
|
|
||||||
|
def rebuild_data_files_list(container_id, book_id, relpath_being_deleted):
|
||||||
|
|
||||||
|
def delete_data_file(relpath, fname, ev):
|
||||||
|
question_dialog(_('Are you sure?'), _('Do you want to permanently delete the data file {0} from the book {1}?').format(
|
||||||
|
fname, mi.title), def(yes):
|
||||||
|
if yes:
|
||||||
|
data = v'[relpath]'
|
||||||
|
ajax_send(f'data-files/remove/{book_id}/{current_library_id()}',
|
||||||
|
data, data_file_deleted.bind(None, container_id, book_id, relpath))
|
||||||
|
rebuild_data_files_list(container_id, book_id, relpath_being_deleted)
|
||||||
|
)
|
||||||
|
|
||||||
|
def ddf(relpath, fname, ev):
|
||||||
|
download_data_file(book_id, relpath)
|
||||||
|
|
||||||
|
def upload_data_file(ev):
|
||||||
|
show_subsequent_panel('upload_data_file', replace=False)
|
||||||
|
|
||||||
|
container = document.getElementById(container_id)
|
||||||
|
mi = book_metadata(book_id)
|
||||||
|
if not container or not mi:
|
||||||
|
return
|
||||||
|
container = container.querySelector('[data-component=data-files-list-container]')
|
||||||
|
clear(container)
|
||||||
|
items = [create_item(_('Upload new data file'), icon='plus', action=upload_data_file)]
|
||||||
|
if mi.data_files:
|
||||||
|
fname_map = {relpath: relpath.partition('/')[2] for relpath in mi.data_files}
|
||||||
|
df = sorted(Object.keys(fname_map), key=def(x): return fname_map[x].toLowerCase();)
|
||||||
|
for relpath in df:
|
||||||
|
fname = fname_map[relpath]
|
||||||
|
being_deleted = relpath is relpath_being_deleted
|
||||||
|
subtitle = None
|
||||||
|
side_actions = []
|
||||||
|
if being_deleted:
|
||||||
|
subtitle = _('This file is being deleted')
|
||||||
|
else:
|
||||||
|
side_actions.push(create_side_action(
|
||||||
|
'trash', tooltip=_('Delete the file: {}').format(fname), action=delete_data_file.bind(None, relpath, fname)
|
||||||
|
))
|
||||||
|
items.push(create_item(fname, icon='cloud-download', subtitle=subtitle, side_actions=side_actions, action=ddf.bind(None, relpath, fname)))
|
||||||
|
create_item_list(container, items)
|
||||||
|
|
||||||
|
|
||||||
|
def data_file_deleted(container_id, book_id, relpath, end_type, xhr, ev):
|
||||||
|
if end_type is 'abort':
|
||||||
|
back()
|
||||||
|
return
|
||||||
|
if end_type is not 'load':
|
||||||
|
error_dialog(_('Failed to delete data file from server'), _(
|
||||||
|
'Deleting data file for book: {} failed.').format(book_id), xhr.error_html)
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
res = JSON.parse(xhr.responseText)
|
||||||
|
except Exception as err:
|
||||||
|
error_dialog(_('Could not delete data file for book'), _('Server returned an invalid response'), err.toString())
|
||||||
|
return
|
||||||
|
mi = book_metadata(book_id)
|
||||||
|
if mi:
|
||||||
|
mi.data_files = res.data_files
|
||||||
|
if res.errors:
|
||||||
|
error_dialog(_('Failed to delete data file'), _('Failed to delete data file: {}').format(relpath), res.errors[relpath])
|
||||||
|
rebuild_data_files_list(container_id, book_id)
|
||||||
|
|
||||||
|
|
||||||
|
def data_files(container_id):
|
||||||
|
if not render_book.book_id or not book_metadata(render_book.book_id):
|
||||||
|
return return_to_book_details()
|
||||||
|
book_id = render_book.book_id
|
||||||
|
|
||||||
|
container = document.getElementById(container_id)
|
||||||
|
create_top_bar(container, title=_('Manage data files'), action=back, icon='close')
|
||||||
|
container.appendChild(E.div(class_=DATA_FILES_CLASS))
|
||||||
|
container = container.lastChild
|
||||||
|
container.appendChild(E.div(data_component='data-files-list-container'))
|
||||||
|
rebuild_data_files_list(container_id, book_id)
|
||||||
|
|
||||||
|
|
||||||
def search_internet(container_id):
|
def search_internet(container_id):
|
||||||
if not render_book.book_id or not book_metadata(render_book.book_id):
|
if not render_book.book_id or not book_metadata(render_book.book_id):
|
||||||
return return_to_book_details()
|
return return_to_book_details()
|
||||||
@ -1013,3 +1213,5 @@ set_panel_handler('book_details', init)
|
|||||||
set_panel_handler('book_details^more_actions', create_more_actions_panel)
|
set_panel_handler('book_details^more_actions', create_more_actions_panel)
|
||||||
set_panel_handler('book_details^search_internet', search_internet)
|
set_panel_handler('book_details^search_internet', search_internet)
|
||||||
set_panel_handler('book_details^copy_to_library', copy_to_library)
|
set_panel_handler('book_details^copy_to_library', copy_to_library)
|
||||||
|
set_panel_handler('book_details^data_files', data_files)
|
||||||
|
set_panel_handler('book_details^upload_data_file', upload_data_file)
|
||||||
|
@ -224,6 +224,15 @@ def download_url(book_id, fmt, content_disposition):
|
|||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
|
||||||
|
def download_data_file_url(book_id, relpath, content_disposition):
|
||||||
|
lid = current_library_id()
|
||||||
|
rpath = encodeURIComponent(relpath)
|
||||||
|
ans = absolute_path(f'data-files/get/{book_id}/{rpath}/{lid}')
|
||||||
|
if content_disposition:
|
||||||
|
ans += f'?content_disposition={content_disposition}'
|
||||||
|
return ans
|
||||||
|
|
||||||
|
|
||||||
def book_metadata(book_id):
|
def book_metadata(book_id):
|
||||||
return library_data.metadata[book_id]
|
return library_data.metadata[book_id]
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user