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
|
||||
|
||||
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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
# }}}
|
||||
|
@ -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()
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
||||
# }}}
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user