Implement serving of static resources

This commit is contained in:
Kovid Goyal 2015-06-13 15:12:06 +05:30
parent f620eb0279
commit f2469eefdc
7 changed files with 205 additions and 4 deletions

View 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>

View 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')

View File

@ -6,6 +6,8 @@ from __future__ import (unicode_literals, division, absolute_import,
__license__ = 'GPL v3'
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
from importlib import import_module
from calibre.srv.routes import Router
class LibraryBroker(object):
@ -32,6 +34,10 @@ class Handler(object):
def __init__(self, libraries, opts):
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.dispatch = self.router.dispatch

View File

@ -522,7 +522,7 @@ class HTTPConnection(HTTPRequest):
mt = guess_type(output.name)[0]
if mt:
if mt in {'text/plain', 'text/html', 'application/javascript', 'text/css'}:
mt =+ '; charset=UTF-8'
mt += '; charset=UTF-8'
outheaders['Content-Type'] = mt
elif isinstance(output, (bytes, type(''))):
output = dynamic_output(output, outheaders)

View File

@ -6,24 +6,38 @@ from __future__ import (unicode_literals, division, absolute_import,
__license__ = 'GPL v3'
__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 operator import attrgetter
from calibre.srv.errors import HTTPSimpleResponse, HTTPNotFound, RouteError
from calibre.srv.utils import http_date
default_methods = frozenset(('HEAD', 'GET'))
def route_key(route):
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):
f.route = route.rstrip('/') or '/'
f.types = types or {}
f.methods = methods
f.auth_required = auth_required
f.android_workaround = android_workaround
f.cache_control = cache_control
f.is_endpoint = True
return f
return annotate
@ -207,6 +221,27 @@ class Router(object):
self.init_session(endpoint_, data)
ans = endpoint_(self.ctx, data, *args)
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
def url_for(self, route, **kwargs):

View File

@ -7,11 +7,15 @@ __license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__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 calibre.srv.utils import ServerLog
rmtree = partial(shutil.rmtree, ignore_errors=True)
class BaseTest(unittest.TestCase):
longMessage = True
@ -19,6 +23,51 @@ class BaseTest(unittest.TestCase):
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):
daemon = True
@ -64,3 +113,22 @@ class TestServer(Thread):
def change_handler(self, handler):
from calibre.srv.http_response import create_http_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)

View 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')