mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Server endpoint for adding books
This commit is contained in:
parent
d48c8e7a2b
commit
f97c680386
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
@ -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()
|
||||||
|
@ -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):
|
||||||
|
@ -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)
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
@ -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
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user