Server endpoint for adding books

This commit is contained in:
Kovid Goyal 2018-01-24 15:16:05 +05:30
parent d48c8e7a2b
commit f97c680386
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
7 changed files with 105 additions and 20 deletions

View File

@ -4,19 +4,23 @@
from __future__ import absolute_import, division, print_function, unicode_literals from __future__ import absolute_import, division, print_function, unicode_literals
import os
from functools import partial from functools import partial
from io import BytesIO
from calibre import as_unicode from calibre import as_unicode, sanitize_file_name_unicode
from calibre.db.cli import module_for_cmd from calibre.db.cli import module_for_cmd
from calibre.srv.errors import HTTPBadRequest, HTTPNotFound, HTTPForbidden from calibre.ebooks.metadata.meta import get_metadata
from calibre.srv.routes import endpoint, msgpack_or_json from calibre.srv.changes import books_added
from calibre.srv.utils import get_library_data from calibre.srv.errors import HTTPBadRequest, HTTPForbidden, HTTPNotFound
from calibre.srv.routes import endpoint, json, msgpack_or_json
from calibre.srv.utils import get_db, get_library_data
from calibre.utils.serialize import MSGPACK_MIME, json_loads, msgpack_loads from calibre.utils.serialize import MSGPACK_MIME, json_loads, msgpack_loads
receive_data_methods = {'GET', 'POST'} receive_data_methods = {'GET', 'POST'}
@endpoint('/cdb/cmd/{which}/{version=0}', postprocess=msgpack_or_json, methods=receive_data_methods) @endpoint('/cdb/cmd/{which}/{version=0}', postprocess=msgpack_or_json, methods=receive_data_methods, cache_control='no-cache')
def cdb_run(ctx, rd, which, version): def cdb_run(ctx, rd, which, version):
try: try:
m = module_for_cmd(which) m = module_for_cmd(which)
@ -50,3 +54,39 @@ def cdb_run(ctx, rd, which, version):
import traceback import traceback
return {'err': as_unicode(err), 'tb': traceback.format_exc()} return {'err': as_unicode(err), 'tb': traceback.format_exc()}
return {'result': result} return {'result': result}
@endpoint('/cdb/add-book/{job_id}/{add_duplicates}/{filename}/{library_id=None}',
needs_db_write=True, postprocess=json, methods=receive_data_methods, cache_control='no-cache')
def cdb_add_book(ctx, rd, job_id, add_duplicates, filename, library_id):
'''
Add a file as a new book. The file contents must be in the body of the request.
The response will also have the title/authors/languages read from the
metadata of the file/filename. It will contain a `book_id` field specifying the id of the newly added book,
or if add_duplicates is not specified and a duplicate was found, no book_id will be present. It will also
return the value of `job_id` as the `id` field and `filename` as the `filename` field.
'''
db = get_db(ctx, rd, library_id)
if ctx.restriction_for(rd, db):
raise HTTPForbidden('Cannot use the add book interface with a user who has per library restrictions')
if not filename:
raise HTTPBadRequest('An empty filename is not allowed')
sfilename = sanitize_file_name_unicode(filename)
fmt = os.path.splitext(sfilename)[1]
fmt = fmt[1:] if fmt else None
if not fmt:
raise HTTPBadRequest('An filename with no extension is not allowed')
if isinstance(rd.request_body_file, BytesIO):
raise HTTPBadRequest('A request body containing the file data must be specified')
add_duplicates = add_duplicates in ('y', '1')
path = os.path.join(rd.tdir, sfilename)
rd.request_body_file.name = path
mi = get_metadata(rd.request_body_file, stream_type=fmt, use_libprs_metadata=True)
rd.request_body_file.seek(0)
ids, duplicates = db.add_books([(mi, {fmt: rd.request_body_file})], add_duplicates=add_duplicates)
ans = {'title': mi.title, 'authors': mi.authors, 'languages': mi.languages, 'filename': filename, 'id': job_id}
if ids:
ans['book_id'] = ids[0]
books_added(ids)
return ans

View File

@ -66,7 +66,11 @@ def endpoint(route,
# 200 for GET and HEAD and 201 for POST # 200 for GET and HEAD and 201 for POST
ok_code=None, ok_code=None,
postprocess=None postprocess=None,
# Needs write access to the calibre database
needs_db_write=False
): ):
from calibre.srv.handler import Context from calibre.srv.handler import Context
from calibre.srv.http_response import RequestData from calibre.srv.http_response import RequestData
@ -82,6 +86,7 @@ def endpoint(route,
f.postprocess = postprocess f.postprocess = postprocess
f.ok_code = ok_code f.ok_code = ok_code
f.is_endpoint = True f.is_endpoint = True
f.needs_db_write = needs_db_write
argspec = inspect.getargspec(f) argspec = inspect.getargspec(f)
if len(argspec.args) < 2: if len(argspec.args) < 2:
raise TypeError('The endpoint %r must take at least two arguments' % f.route) raise TypeError('The endpoint %r must take at least two arguments' % f.route)
@ -303,6 +308,8 @@ class Router(object):
data.status_code = endpoint_.ok_code data.status_code = endpoint_.ok_code
self.init_session(endpoint_, data) self.init_session(endpoint_, data)
if endpoint_.needs_db_write:
self.ctx.check_for_write_access(data)
ans = endpoint_(self.ctx, data, *args) ans = endpoint_(self.ctx, data, *args)
self.finalize_session(endpoint_, data, ans) self.finalize_session(endpoint_, data, ans)
outheaders = data.outheaders outheaders = data.outheaders

View File

@ -7,17 +7,19 @@ __license__ = 'GPL v3'
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
import httplib, zlib, json, base64, os import httplib, zlib, json, base64, os
from io import BytesIO
from functools import partial from functools import partial
from urllib import urlencode from urllib import urlencode, quote
from httplib import OK, NOT_FOUND, FORBIDDEN from httplib import OK, NOT_FOUND, FORBIDDEN
from calibre.ebooks.metadata.meta import get_metadata
from calibre.srv.tests.base import LibraryBaseTest from calibre.srv.tests.base import LibraryBaseTest
def make_request(conn, url, headers={}, prefix='/ajax', username=None, password=None, method='GET'): def make_request(conn, url, headers={}, prefix='/ajax', username=None, password=None, method='GET', data=None):
if username and password: if username and password:
headers[b'Authorization'] = b'Basic ' + base64.standard_b64encode((username + ':' + password).encode('utf-8')) headers[b'Authorization'] = b'Basic ' + base64.standard_b64encode((username + ':' + password).encode('utf-8'))
conn.request(method, prefix + url, headers=headers) conn.request(method, prefix + url, headers=headers, body=data)
r = conn.getresponse() r = conn.getresponse()
data = r.read() data = r.read()
if r.status == httplib.OK and data and data[0] in b'{[': if r.status == httplib.OK and data and data[0] in b'{[':
@ -82,7 +84,7 @@ class ContentTest(LibraryBaseTest):
self.ae(set(data['book_ids']), {2}) self.ae(set(data['book_ids']), {2})
# }}} # }}}
def test_srv_restrictions(self): def test_srv_restrictions(self): # {{{
' Test that virtual lib. + search restriction works on all end points' ' Test that virtual lib. + search restriction works on all end points'
with self.create_server(auth=True, auth_mode='basic') as server: with self.create_server(auth=True, auth_mode='basic') as server:
db = server.handler.router.ctx.library_broker.get(None) db = server.handler.router.ctx.library_broker.get(None)
@ -136,6 +138,7 @@ class ContentTest(LibraryBaseTest):
# cdb.py # cdb.py
r(url_for('/cdb/cmd', which='list'), status=FORBIDDEN) r(url_for('/cdb/cmd', which='list'), status=FORBIDDEN)
r(url_for('/cdb/add-book', filename='test.epub'), status=FORBIDDEN)
# code.py # code.py
def sr(path, **k): def sr(path, **k):
@ -151,4 +154,33 @@ class ContentTest(LibraryBaseTest):
ok(url_for('/get', what='thumb', book_id=1)) ok(url_for('/get', what='thumb', book_id=1))
nf(url_for('/get', what='thumb', book_id=3)) nf(url_for('/get', what='thumb', book_id=3))
# Not going test legacy and opds as they are to painful # Not going test legacy and opds as they are too painful
# }}}
def test_srv_add_book(self): # {{{
with self.create_server(auth=True, auth_mode='basic') as server:
server.handler.ctx.user_manager.add_user('12', 'test')
server.handler.ctx.user_manager.add_user('ro', 'test', readonly=True)
conn = server.connect()
ae = self.assertEqual
def r(filename, data=None, status=OK, method='POST', username='12', add_duplicates='n', job_id=1):
r, data = make_request(conn, '/cdb/add-book/{}/{}/{}'.format(job_id, add_duplicates, quote(filename.encode('utf-8')).decode('ascii')),
username=username, password='test', prefix='', method=method, data=data)
ae(status, r.status)
return data
r('test.epub', None, username='ro', status=FORBIDDEN)
content = b'content'
filename = 'test add - XXX.txt'
data = r(filename, content)
s = BytesIO(content)
s.name = filename
mi = get_metadata(s, stream_type='txt')
ae(data, {'title': mi.title, 'book_id': data['book_id'], 'authors': mi.authors, 'languages': mi.languages, 'id': '1', 'filename': filename})
r, q = make_request(conn, '/get/txt/{}'.format(data['book_id']), username='12', password='test', prefix='')
ae(r.status, OK)
ae(q, content)
# }}}

View File

@ -122,10 +122,8 @@ def ajax_send(path, data, on_complete, on_progress=None, query=None, timeout=Non
return xhr return xhr
def ajax_send_file(path, file, on_complete, on_progress, query=None, timeout=None, ok_code=200): def ajax_send_file(path, file, on_complete, on_progress, timeout=None, ok_code=200):
xhr = ajax(path, on_complete, on_progress, False, 'POST', query, timeout, ok_code) xhr = ajax(path, on_complete, on_progress, False, 'POST', timeout, ok_code)
if file.name:
xhr.setRequestHeader('Calibre-Filename', file.name)
if file.type: if file.type:
xhr.overrideMimeType(file.type) xhr.overrideMimeType(file.type)
r = FileReader() r = FileReader()

View File

@ -5,6 +5,7 @@ from __python__ import bound_methods, hash_literals
from gettext import gettext as _ from gettext import gettext as _
from ajax import ajax_send_file from ajax import ajax_send_file
from book_list.library_data import loaded_books_query
from book_list.router import back from book_list.router import back
from book_list.top_bar import create_top_bar from book_list.top_bar import create_top_bar
from dom import ensure_id from dom import ensure_id
@ -51,6 +52,14 @@ def fake_send(container_id, job_id):
setTimeout(fake_send.bind(None, container_id, job_id), 1000) setTimeout(fake_send.bind(None, container_id, job_id), 1000)
def send_file(file, container_id, job_id, add_duplicates):
lid = loaded_books_query().library_id
ad = 'y' if add_duplicates else 'n'
return ajax_send_file(
f'/cdb/add-book/{job_id}/{ad}/{encodeURIComponent(file.name)}/{lid}',
file, on_complete.bind(None, container_id, job_id), on_progress.bind(None, container_id, job_id))
def files_chosen(container_id, files): def files_chosen(container_id, files):
container = document.getElementById(container_id) container = document.getElementById(container_id)
if not container: if not container:
@ -64,7 +73,7 @@ def files_chosen(container_id, files):
if state.fake_send: if state.fake_send:
setTimeout(fake_send.bind(None, container_id, job_id), 100) setTimeout(fake_send.bind(None, container_id, job_id), 100)
else: else:
ajax_send_file('/add-book', file, on_complete.bind(None, container_id, job_id), on_progress.bind(None, container_id, job_id), query={'id': '' + job_id}) send_file(file, container_id, job_id)
def add_books_panel(container_id): def add_books_panel(container_id):

View File

@ -456,6 +456,9 @@ def add_books(container_id):
if not library_data.sortable_fields: if not library_data.sortable_fields:
show_panel('book_list', replace=True) show_panel('book_list', replace=True)
return return
if not get_interface_data().username:
error_dialog(_('Not logged in'), _('You must be logged in to add books'))
return
add_books_panel(container_id) add_books_panel(container_id)
# }}} # }}}

View File

@ -62,7 +62,3 @@ def upload_status_widget(name, job_id):
ans.appendChild(E.progress()) ans.appendChild(E.progress())
ans.appendChild(E.span()) ans.appendChild(E.span())
return ans return ans
def start_send(container, files):
pass