diff --git a/src/calibre/srv/content.py b/src/calibre/srv/content.py index fea7f0eaac..235a175538 100644 --- a/src/calibre/srv/content.py +++ b/src/calibre/srv/content.py @@ -6,9 +6,11 @@ from __future__ import (unicode_literals, division, absolute_import, __license__ = 'GPL v3' __copyright__ = '2015, Kovid Goyal ' -import os, errno +import os from io import BytesIO +from calibre import fit_image +from calibre.constants import config_dir from calibre.db.errors import NoSuchFormat from calibre.ebooks.metadata import authors_to_string from calibre.ebooks.metadata.meta import set_metadata @@ -21,7 +23,7 @@ from calibre.srv.utils import http_date from calibre.utils.config_base import tweaks from calibre.utils.date import timestampfromdt from calibre.utils.filenames import ascii_filename -from calibre.utils.magick.draw import thumbnail +from calibre.utils.magick.draw import thumbnail, Image plugboard_content_server_value = 'content_server' plugboard_content_server_formats = ['epub', 'mobi', 'azw3'] @@ -123,6 +125,8 @@ def book_fmt(ctx, rd, library_id, db, book_id, fmt): @endpoint('/static/{+what}', auth_required=False, cache_control=24) def static(ctx, rd, what): + if not what: + raise HTTPNotFound() base = P('content-server', allow_user_override=False) path = os.path.abspath(os.path.join(base, *what.split('/'))) if not path.startswith(base) or ':' in what: @@ -131,15 +135,70 @@ def static(ctx, rd, what): path = P('content-server/' + path) try: return lopen(path, 'rb') - except EnvironmentError as e: - if e.errno == errno.EISDIR or os.path.isdir(path): - raise HTTPNotFound('Cannot get a directory') + except EnvironmentError: raise HTTPNotFound() @endpoint('/favicon.png', auth_required=False, cache_control=24) def favicon(ctx, rd): return lopen(I('lt.png'), 'rb') +@endpoint('/icon/{+which}', auth_required=False, cache_control=24) +def icon(ctx, rd, which): + sz = rd.query.get('sz') + if sz != 'full': + try: + sz = int(rd.query.get('sz', 48)) + except Exception: + sz = 48 + if which in {'', '_'}: + raise HTTPNotFound() + if which.startswith('_'): + base = os.path.join(config_dir, 'tb_icons') + path = os.path.abspath(os.path.join(base, *which[1:].split('/'))) + if not path.startswith(base) or ':' in which: + raise HTTPNotFound('Naughty, naughty!') + else: + base = P('images', allow_user_override=False) + path = os.path.abspath(os.path.join(base, *which.split('/'))) + if not path.startswith(base) or ':' in which: + raise HTTPNotFound('Naughty, naughty!') + path = os.path.relpath(path, base).replace(os.sep, '/') + path = P('images/' + path) + if sz == 'full': + try: + return lopen(path, 'rb') + except EnvironmentError: + raise HTTPNotFound() + tdir = os.path.join(rd.tdir, 'icons') + cached = os.path.join(tdir, '%d-%s.png' % (sz, which)) + try: + return lopen(cached, 'rb') + except EnvironmentError: + pass + try: + src = lopen(path, 'rb') + except EnvironmentError: + raise HTTPNotFound() + with src: + img = Image() + img.load(src.read()) + width, height = img.size + scaled, width, height = fit_image(width, height, sz, sz) + if scaled: + img.size = (width, height) + try: + ans = lopen(cached, 'w+b') + except EnvironmentError: + try: + os.mkdir(tdir) + except EnvironmentError: + pass + ans = lopen(cached, 'w+b') + ans.write(img.export('png')) + ans.seek(0) + return ans + + @endpoint('/get/{what}/{book_id}/{library_id=None}', types={'book_id':int}) def get(ctx, rd, what, book_id, library_id): db = ctx.get_library(library_id) @@ -149,11 +208,23 @@ def get(ctx, rd, what, book_id, library_id): if not db.has_id(book_id): raise HTTPNotFound('Book with id %r does not exist' % book_id) library_id = db.server_library_id # in case library_id was None - if what == 'thumb' or what.startswith('thumb_'): - try: - w, h = map(int, what.partition('_')[2].partition('x')[::2]) - except Exception: - w, h = 60, 80 + if what == 'thumb': + sz = rd.query.get('sz') + w, h = 60, 80 + if sz is None: + pass + elif sz == 'full': + w = h = None + elif 'x' in sz: + try: + w, h = map(int, sz.partition('x')[::2]) + except Exception: + pass + else: + try: + w = h = int(sz) + except Exception: + pass return cover(ctx, rd, library_id, db, book_id, width=w, height=h) elif what == 'cover': return cover(ctx, rd, library_id, db, book_id) diff --git a/src/calibre/srv/tests/content.py b/src/calibre/srv/tests/content.py index b6a54840ef..572b3443af 100644 --- a/src/calibre/srv/tests/content.py +++ b/src/calibre/srv/tests/content.py @@ -27,9 +27,10 @@ class ContentTest(LibraryBaseTest): self.ae(r.status, httplib.NOT_FOUND) self.ae(r.read(), body) - missing('/static/missing.xxx') - missing('/static/../out.html', b'Naughty, naughty!') - missing('/static/C:/out.html', b'Naughty, naughty!') + for prefix in ('static', 'icon'): + missing('/%s/missing.xxx' % prefix) + missing('/%s/../out.html' % prefix, b'Naughty, naughty!') + missing('/%s/C:/out.html' % prefix, b'Naughty, naughty!') def test_response(r): self.assertIn(b'max-age=', r.getheader('Cache-Control')) @@ -38,12 +39,16 @@ class ContentTest(LibraryBaseTest): self.assertIsNotNone(r.getheader('ETag')) self.assertIsNotNone(r.getheader('Content-Type')) - def test(src, url): + def test(src, url, sz=None): raw = P(src, data=True) conn.request('GET', url) r = conn.getresponse() self.ae(r.status, httplib.OK) - self.ae(r.read(), raw) + data = r.read() + if sz is None: + self.ae(data, raw) + else: + self.ae(sz, identify_data(data)[0]) test_response(r) conn.request('GET', url, headers={'If-None-Match':r.getheader('ETag')}) r = conn.getresponse() @@ -52,6 +57,9 @@ class ContentTest(LibraryBaseTest): test('content-server/empty.html', '/static/empty.html') test('images/lt.png', '/favicon.png') + test('images/lt.png', '/icon/lt.png?sz=full') + test('images/lt.png', '/icon/lt.png', sz=48) + test('images/lt.png', '/icon/lt.png?sz=16', sz=16) # }}} def test_get(self): # {{{ @@ -60,8 +68,9 @@ class ContentTest(LibraryBaseTest): db = server.handler.router.ctx.get_library() conn = server.connect() - def get(what, book_id, library_id=None): - conn.request('GET', '/get/%s/%s' % (what, book_id) + (('/' + library_id) if library_id else '')) + def get(what, book_id, library_id=None, q=''): + q = ('?' + q) if q else q + conn.request('GET', '/get/%s/%s' % (what, book_id) + (('/' + library_id) if library_id else '') + q) r = conn.getresponse() return r, r.read() @@ -137,15 +146,15 @@ class ContentTest(LibraryBaseTest): r, data = get('thumb', 1) self.ae(r.status, httplib.OK) self.ae(r.getheader('Used-Cache'), 'yes') - r, data = get('thumb_100x100', 1) + r, data = get('thumb', 1, q='sz=100') self.ae(r.status, httplib.OK) self.ae(identify_data(data), (100, 100, 'jpeg')) self.ae(r.getheader('Used-Cache'), 'no') - r, data = get('thumb_100x100', 1) + r, data = get('thumb', 1, q='sz=100x100') self.ae(r.status, httplib.OK) self.ae(r.getheader('Used-Cache'), 'yes') db.set_cover({1:I('lt.png', data=True)}) - r, data = get('thumb_100x100', 1) + r, data = get('thumb', 1, q='sz=100') self.ae(r.status, httplib.OK) self.ae(identify_data(data), (100, 100, 'jpeg')) self.ae(r.getheader('Used-Cache'), 'no')