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'
|
||||
__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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
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