mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Implement serving of static resources
This commit is contained in:
parent
f620eb0279
commit
f2469eefdc
11
resources/content-server/empty.html
Normal file
11
resources/content-server/empty.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>empty</title>
|
||||||
|
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
31
src/calibre/srv/content.py
Normal file
31
src/calibre/srv/content.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env python2
|
||||||
|
# vim:fileencoding=utf-8
|
||||||
|
from __future__ import (unicode_literals, division, absolute_import,
|
||||||
|
print_function)
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
|
||||||
|
import os, errno
|
||||||
|
|
||||||
|
from calibre.srv.errors import HTTPNotFound
|
||||||
|
from calibre.srv.routes import endpoint
|
||||||
|
|
||||||
|
@endpoint('/static/{+what}', auth_required=False, cache_control=24)
|
||||||
|
def static(ctx, rd, what):
|
||||||
|
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:
|
||||||
|
raise HTTPNotFound('Naughty, naughty!')
|
||||||
|
path = os.path.relpath(path, base).replace(os.sep, '/')
|
||||||
|
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')
|
||||||
|
raise HTTPNotFound()
|
||||||
|
|
||||||
|
@endpoint('/favicon.png', auth_required=False, cache_control=24)
|
||||||
|
def favicon(ctx, rd):
|
||||||
|
return lopen(I('lt.png'), 'rb')
|
@ -6,6 +6,8 @@ from __future__ import (unicode_literals, division, absolute_import,
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
|
||||||
|
from importlib import import_module
|
||||||
|
|
||||||
from calibre.srv.routes import Router
|
from calibre.srv.routes import Router
|
||||||
|
|
||||||
class LibraryBroker(object):
|
class LibraryBroker(object):
|
||||||
@ -32,6 +34,10 @@ class Handler(object):
|
|||||||
|
|
||||||
def __init__(self, libraries, opts):
|
def __init__(self, libraries, opts):
|
||||||
self.router = Router(ctx=Context(libraries, opts), url_prefix=opts.url_prefix)
|
self.router = Router(ctx=Context(libraries, opts), url_prefix=opts.url_prefix)
|
||||||
|
for module in ('content',):
|
||||||
|
module = import_module('calibre.srv.' + module)
|
||||||
|
self.router.load_routes(vars(module).itervalues())
|
||||||
|
self.router.finalize()
|
||||||
self.router.ctx.url_for = self.router.url_for
|
self.router.ctx.url_for = self.router.url_for
|
||||||
self.dispatch = self.router.dispatch
|
self.dispatch = self.router.dispatch
|
||||||
|
|
||||||
|
@ -522,7 +522,7 @@ class HTTPConnection(HTTPRequest):
|
|||||||
mt = guess_type(output.name)[0]
|
mt = guess_type(output.name)[0]
|
||||||
if mt:
|
if mt:
|
||||||
if mt in {'text/plain', 'text/html', 'application/javascript', 'text/css'}:
|
if mt in {'text/plain', 'text/html', 'application/javascript', 'text/css'}:
|
||||||
mt =+ '; charset=UTF-8'
|
mt += '; charset=UTF-8'
|
||||||
outheaders['Content-Type'] = mt
|
outheaders['Content-Type'] = mt
|
||||||
elif isinstance(output, (bytes, type(''))):
|
elif isinstance(output, (bytes, type(''))):
|
||||||
output = dynamic_output(output, outheaders)
|
output = dynamic_output(output, outheaders)
|
||||||
|
@ -6,24 +6,38 @@ from __future__ import (unicode_literals, division, absolute_import,
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
|
||||||
import httplib, sys, inspect, re
|
import httplib, sys, inspect, re, time, numbers
|
||||||
from itertools import izip
|
from itertools import izip
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
|
||||||
from calibre.srv.errors import HTTPSimpleResponse, HTTPNotFound, RouteError
|
from calibre.srv.errors import HTTPSimpleResponse, HTTPNotFound, RouteError
|
||||||
|
from calibre.srv.utils import http_date
|
||||||
|
|
||||||
default_methods = frozenset(('HEAD', 'GET'))
|
default_methods = frozenset(('HEAD', 'GET'))
|
||||||
|
|
||||||
def route_key(route):
|
def route_key(route):
|
||||||
return route.partition('{')[0].rstrip('/')
|
return route.partition('{')[0].rstrip('/')
|
||||||
|
|
||||||
def endpoint(route, methods=default_methods, types=None, auth_required=True, android_workaround=False):
|
def endpoint(route,
|
||||||
|
methods=default_methods,
|
||||||
|
types=None,
|
||||||
|
auth_required=True,
|
||||||
|
android_workaround=False,
|
||||||
|
|
||||||
|
# Manage the HTTP caching
|
||||||
|
# Set to None or 'no-cache' to prevent caching of this endpoint
|
||||||
|
# Set to a number to cache for at most number hours
|
||||||
|
# Set to a tuple (cache_type, max_age) to explicitly set the
|
||||||
|
# Cache-Control header
|
||||||
|
cache_control=False
|
||||||
|
):
|
||||||
def annotate(f):
|
def annotate(f):
|
||||||
f.route = route.rstrip('/') or '/'
|
f.route = route.rstrip('/') or '/'
|
||||||
f.types = types or {}
|
f.types = types or {}
|
||||||
f.methods = methods
|
f.methods = methods
|
||||||
f.auth_required = auth_required
|
f.auth_required = auth_required
|
||||||
f.android_workaround = android_workaround
|
f.android_workaround = android_workaround
|
||||||
|
f.cache_control = cache_control
|
||||||
f.is_endpoint = True
|
f.is_endpoint = True
|
||||||
return f
|
return f
|
||||||
return annotate
|
return annotate
|
||||||
@ -207,6 +221,27 @@ class Router(object):
|
|||||||
self.init_session(endpoint_, data)
|
self.init_session(endpoint_, 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
|
||||||
|
|
||||||
|
cc = endpoint_.cache_control
|
||||||
|
if cc is not False and 'Cache-Control' not in data.outheaders:
|
||||||
|
if cc is None or cc == 'no-cache':
|
||||||
|
outheaders['Expires'] = http_date(10000.0) # A date in the past
|
||||||
|
outheaders['Cache-Control'] = 'no-cache, must-revalidate'
|
||||||
|
outheaders['Pragma'] = 'no-cache'
|
||||||
|
elif isinstance(cc, numbers.Number):
|
||||||
|
cc = int(60 * 60 * cc)
|
||||||
|
outheaders['Cache-Control'] = 'public, max-age=%d' % cc
|
||||||
|
if cc == 0:
|
||||||
|
cc -= 100000
|
||||||
|
outheaders['Expires'] = http_date(cc + time.time())
|
||||||
|
else:
|
||||||
|
ctype, max_age = cc
|
||||||
|
max_age = int(60 * 60 * max_age)
|
||||||
|
outheaders['Cache-Control'] = '%s, max-age=%d' % (ctype, max_age)
|
||||||
|
if max_age == 0:
|
||||||
|
max_age -= 100000
|
||||||
|
outheaders['Expires'] = http_date(max_age + time.time())
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
def url_for(self, route, **kwargs):
|
def url_for(self, route, **kwargs):
|
||||||
|
@ -7,11 +7,15 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import unittest, time, httplib
|
import unittest, time, httplib, shutil, gc, tempfile, atexit, os
|
||||||
|
from io import BytesIO
|
||||||
|
from functools import partial
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
|
||||||
from calibre.srv.utils import ServerLog
|
from calibre.srv.utils import ServerLog
|
||||||
|
|
||||||
|
rmtree = partial(shutil.rmtree, ignore_errors=True)
|
||||||
|
|
||||||
class BaseTest(unittest.TestCase):
|
class BaseTest(unittest.TestCase):
|
||||||
|
|
||||||
longMessage = True
|
longMessage = True
|
||||||
@ -19,6 +23,51 @@ class BaseTest(unittest.TestCase):
|
|||||||
|
|
||||||
ae = unittest.TestCase.assertEqual
|
ae = unittest.TestCase.assertEqual
|
||||||
|
|
||||||
|
class LibraryBaseTest(BaseTest):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
from calibre.utils.recycle_bin import nuke_recycle
|
||||||
|
nuke_recycle()
|
||||||
|
self.library_path = self.mkdtemp()
|
||||||
|
self.create_db(self.library_path)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
from calibre.utils.recycle_bin import restore_recyle
|
||||||
|
restore_recyle()
|
||||||
|
gc.collect(), gc.collect()
|
||||||
|
try:
|
||||||
|
shutil.rmtree(self.library_path)
|
||||||
|
except EnvironmentError:
|
||||||
|
# Try again in case something transient has a file lock on windows
|
||||||
|
gc.collect(), gc.collect()
|
||||||
|
time.sleep(2)
|
||||||
|
shutil.rmtree(self.library_path)
|
||||||
|
|
||||||
|
def mkdtemp(self):
|
||||||
|
ans = tempfile.mkdtemp(prefix='db_test_')
|
||||||
|
atexit.register(rmtree, ans)
|
||||||
|
return ans
|
||||||
|
|
||||||
|
def create_db(self, library_path):
|
||||||
|
from calibre.db.legacy import create_backend
|
||||||
|
from calibre.db.cache import Cache
|
||||||
|
d = os.path.dirname
|
||||||
|
src = os.path.join(d(d(d(os.path.abspath(__file__)))), 'db', 'tests', 'metadata.db')
|
||||||
|
dest = os.path.join(library_path, 'metadata.db')
|
||||||
|
shutil.copy2(src, dest)
|
||||||
|
db = Cache(create_backend(library_path))
|
||||||
|
db.init()
|
||||||
|
db.set_cover({1:I('lt.png', data=True), 2:I('polish.png', data=True)})
|
||||||
|
db.add_format(1, 'FMT1', BytesIO(b'book1fmt1'), run_hooks=False)
|
||||||
|
db.add_format(1, 'FMT2', BytesIO(b'book1fmt2'), run_hooks=False)
|
||||||
|
db.add_format(2, 'FMT1', BytesIO(b'book2fmt1'), run_hooks=False)
|
||||||
|
db.backend.conn.close()
|
||||||
|
return dest
|
||||||
|
|
||||||
|
def create_server(self, *args, **kwargs):
|
||||||
|
args = (self.library_path ,) + args
|
||||||
|
return LibraryServer(*args, **kwargs)
|
||||||
|
|
||||||
class TestServer(Thread):
|
class TestServer(Thread):
|
||||||
|
|
||||||
daemon = True
|
daemon = True
|
||||||
@ -64,3 +113,22 @@ class TestServer(Thread):
|
|||||||
def change_handler(self, handler):
|
def change_handler(self, handler):
|
||||||
from calibre.srv.http_response import create_http_handler
|
from calibre.srv.http_response import create_http_handler
|
||||||
self.loop.handler = create_http_handler(handler)
|
self.loop.handler = create_http_handler(handler)
|
||||||
|
|
||||||
|
class LibraryServer(TestServer):
|
||||||
|
|
||||||
|
def __init__(self, library_path, libraries=(), plugins=(), specialize=lambda x:None, **kwargs):
|
||||||
|
Thread.__init__(self, name='ServerMain')
|
||||||
|
from calibre.srv.opts import Options
|
||||||
|
from calibre.srv.loop import ServerLoop
|
||||||
|
from calibre.srv.handler import Handler
|
||||||
|
from calibre.srv.http_response import create_http_handler
|
||||||
|
opts = Options(**kwargs)
|
||||||
|
self.libraries = libraries or (library_path,)
|
||||||
|
self.handler = Handler(libraries, opts)
|
||||||
|
self.loop = ServerLoop(
|
||||||
|
create_http_handler(self.handler.dispatch),
|
||||||
|
opts=opts,
|
||||||
|
plugins=plugins,
|
||||||
|
log=ServerLog(level=ServerLog.WARN),
|
||||||
|
)
|
||||||
|
specialize(self)
|
||||||
|
50
src/calibre/srv/tests/content.py
Normal file
50
src/calibre/srv/tests/content.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env python2
|
||||||
|
# vim:fileencoding=utf-8
|
||||||
|
from __future__ import (unicode_literals, division, absolute_import,
|
||||||
|
print_function)
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
|
||||||
|
import httplib
|
||||||
|
|
||||||
|
from calibre.srv.tests.base import LibraryBaseTest
|
||||||
|
|
||||||
|
class ContentTest(LibraryBaseTest):
|
||||||
|
|
||||||
|
def test_static(self):
|
||||||
|
'Test serving of static content'
|
||||||
|
with self.create_server() as server:
|
||||||
|
conn = server.connect()
|
||||||
|
|
||||||
|
def missing(url, body=b''):
|
||||||
|
conn.request('GET', url)
|
||||||
|
r = conn.getresponse()
|
||||||
|
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!')
|
||||||
|
|
||||||
|
def test_response(r):
|
||||||
|
self.assertIn(b'max-age=', r.getheader('Cache-Control'))
|
||||||
|
self.assertIn(b'public', r.getheader('Cache-Control'))
|
||||||
|
self.assertIsNotNone(r.getheader('Expires'))
|
||||||
|
self.assertIsNotNone(r.getheader('ETag'))
|
||||||
|
self.assertIsNotNone(r.getheader('Content-Type'))
|
||||||
|
|
||||||
|
def test(src, url):
|
||||||
|
raw = P(src, data=True)
|
||||||
|
conn.request('GET', url)
|
||||||
|
r = conn.getresponse()
|
||||||
|
self.ae(r.status, httplib.OK)
|
||||||
|
self.ae(r.read(), raw)
|
||||||
|
test_response(r)
|
||||||
|
conn.request('GET', url, headers={'If-None-Match':r.getheader('ETag')})
|
||||||
|
r = conn.getresponse()
|
||||||
|
self.ae(r.status, httplib.NOT_MODIFIED)
|
||||||
|
self.ae(b'', r.read())
|
||||||
|
|
||||||
|
test('content-server/empty.html', '/static/empty.html')
|
||||||
|
test('images/lt.png', '/favicon.png')
|
Loading…
x
Reference in New Issue
Block a user