diff --git a/src/calibre/srv/cdb.py b/src/calibre/srv/cdb.py index 708c854742..49176f43b0 100644 --- a/src/calibre/srv/cdb.py +++ b/src/calibre/srv/cdb.py @@ -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 diff --git a/src/calibre/srv/routes.py b/src/calibre/srv/routes.py index 917b8caffd..75f2e434f5 100644 --- a/src/calibre/srv/routes.py +++ b/src/calibre/srv/routes.py @@ -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 diff --git a/src/calibre/srv/tests/ajax.py b/src/calibre/srv/tests/ajax.py index 43b8ceebe0..de51fffa41 100644 --- a/src/calibre/srv/tests/ajax.py +++ b/src/calibre/srv/tests/ajax.py @@ -7,17 +7,19 @@ __license__ = 'GPL v3' __copyright__ = '2015, Kovid Goyal ' 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) + + # }}} diff --git a/src/pyj/ajax.pyj b/src/pyj/ajax.pyj index 70cdbc4820..c4dbea43a2 100644 --- a/src/pyj/ajax.pyj +++ b/src/pyj/ajax.pyj @@ -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() diff --git a/src/pyj/book_list/add.pyj b/src/pyj/book_list/add.pyj index 2724ae0ab0..e819304c53 100644 --- a/src/pyj/book_list/add.pyj +++ b/src/pyj/book_list/add.pyj @@ -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): diff --git a/src/pyj/book_list/views.pyj b/src/pyj/book_list/views.pyj index 11e1eb463d..332fdb278a 100644 --- a/src/pyj/book_list/views.pyj +++ b/src/pyj/book_list/views.pyj @@ -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) # }}} diff --git a/src/pyj/file_uploads.pyj b/src/pyj/file_uploads.pyj index 762b1071a6..445d5e1d54 100644 --- a/src/pyj/file_uploads.pyj +++ b/src/pyj/file_uploads.pyj @@ -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