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
import os
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.srv.errors import HTTPBadRequest, HTTPNotFound, HTTPForbidden
from calibre.srv.routes import endpoint, msgpack_or_json
from calibre.srv.utils import get_library_data
from calibre.ebooks.metadata.meta import get_metadata
from calibre.srv.changes import books_added
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
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):
try:
m = module_for_cmd(which)
@ -50,3 +54,39 @@ def cdb_run(ctx, rd, which, version):
import traceback
return {'err': as_unicode(err), 'tb': traceback.format_exc()}
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
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.http_response import RequestData
@ -82,6 +86,7 @@ def endpoint(route,
f.postprocess = postprocess
f.ok_code = ok_code
f.is_endpoint = True
f.needs_db_write = needs_db_write
argspec = inspect.getargspec(f)
if len(argspec.args) < 2:
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
self.init_session(endpoint_, data)
if endpoint_.needs_db_write:
self.ctx.check_for_write_access(data)
ans = endpoint_(self.ctx, data, *args)
self.finalize_session(endpoint_, data, ans)
outheaders = data.outheaders

View File

@ -7,17 +7,19 @@ __license__ = 'GPL v3'
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
import httplib, zlib, json, base64, os
from io import BytesIO
from functools import partial
from urllib import urlencode
from urllib import urlencode, quote
from httplib import OK, NOT_FOUND, FORBIDDEN
from calibre.ebooks.metadata.meta import get_metadata
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:
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()
data = r.read()
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})
# }}}
def test_srv_restrictions(self):
def test_srv_restrictions(self): # {{{
' Test that virtual lib. + search restriction works on all end points'
with self.create_server(auth=True, auth_mode='basic') as server:
db = server.handler.router.ctx.library_broker.get(None)
@ -136,6 +138,7 @@ class ContentTest(LibraryBaseTest):
# cdb.py
r(url_for('/cdb/cmd', which='list'), status=FORBIDDEN)
r(url_for('/cdb/add-book', filename='test.epub'), status=FORBIDDEN)
# code.py
def sr(path, **k):
@ -151,4 +154,33 @@ class ContentTest(LibraryBaseTest):
ok(url_for('/get', what='thumb', book_id=1))
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
def ajax_send_file(path, file, on_complete, on_progress, query=None, timeout=None, ok_code=200):
xhr = ajax(path, on_complete, on_progress, False, 'POST', query, timeout, ok_code)
if file.name:
xhr.setRequestHeader('Calibre-Filename', file.name)
def ajax_send_file(path, file, on_complete, on_progress, timeout=None, ok_code=200):
xhr = ajax(path, on_complete, on_progress, False, 'POST', timeout, ok_code)
if file.type:
xhr.overrideMimeType(file.type)
r = FileReader()

View File

@ -5,6 +5,7 @@ from __python__ import bound_methods, hash_literals
from gettext import gettext as _
from ajax import ajax_send_file
from book_list.library_data import loaded_books_query
from book_list.router import back
from book_list.top_bar import create_top_bar
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)
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):
container = document.getElementById(container_id)
if not container:
@ -64,7 +73,7 @@ def files_chosen(container_id, files):
if state.fake_send:
setTimeout(fake_send.bind(None, container_id, job_id), 100)
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):

View File

@ -456,6 +456,9 @@ def add_books(container_id):
if not library_data.sortable_fields:
show_panel('book_list', replace=True)
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)
# }}}

View File

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