Content server: Initial implementation of --url-prefix option to ease use of reverse proxying. /browse and /mobile ported, OPDS remains

This commit is contained in:
Kovid Goyal 2010-10-27 23:10:17 -06:00
parent 6681fe6ede
commit 5efad88b63
8 changed files with 98 additions and 69 deletions

View File

@ -8,20 +8,20 @@
<meta http-equiv="X-UA-Compatible" content="IE=100" />
<link rel="icon" type="image/x-icon" href="http://calibre-ebook.com/favicon.ico" />
<link rel="stylesheet" type="text/css" href="/static/browse/browse.css" />
<link type="text/css" href="/static/jquery_ui/css/humanity-custom/jquery-ui-1.8.5.custom.css" rel="stylesheet" />
<link rel="stylesheet" type="text/css" href="/static/jquery.multiselect.css" />
<link rel="stylesheet" type="text/css" href="{prefix}/static/browse/browse.css" />
<link type="text/css" href="{prefix}/static/jquery_ui/css/humanity-custom/jquery-ui-1.8.5.custom.css" rel="stylesheet" />
<link rel="stylesheet" type="text/css" href="{prefix}/static/jquery.multiselect.css" />
<script type="text/javascript" src="/static/jquery.js"></script>
<script type="text/javascript" src="/static/jquery.corner.js"></script>
<script type="text/javascript" src="{prefix}/static/jquery.js"></script>
<script type="text/javascript" src="{prefix}/static/jquery.corner.js"></script>
<script type="text/javascript"
src="/static/jquery_ui/js/jquery-ui-1.8.5.custom.min.js"></script>
src="{prefix}/static/jquery_ui/js/jquery-ui-1.8.5.custom.min.js"></script>
<script type="text/javascript"
src="/static/jquery.multiselect.min.js"></script>
src="{prefix}/static/jquery.multiselect.min.js"></script>
<script type="text/javascript" src="/static/browse/browse.js"></script>
<script type="text/javascript" src="{prefix}/static/browse/browse.js"></script>
<script type="text/javascript">
var sort_cookie_name = "{sort_cookie_name}";
@ -39,16 +39,16 @@
<div id="header">
<div class="area">
<div class="bubble">
<p><a href="/browse" title="Return to top level"
<p><a href="{prefix}/browse" title="Return to top level"
>&rarr;&nbsp;home&nbsp;&larr;</a></p>
</div>
</div>
<div id="nav-container">&nbsp;
<ul id="primary-nav">
<li><a id="nav-mobile" href="/mobile" title="A version of this website suited for mobile browsers">Mobile</a></li>
<li><a id="nav-mobile" href="{prefix}/mobile" title="A version of this website suited for mobile browsers">Mobile</a></li>
<li><a id="nav-demo" href="/old" title="The old version of this webiste">Old</a></li>
<li><a id="nav-download" href="/opds" title="An OPDS feed based version of this website, used in special purpose applications">Feed</a></li>
<li><a id="nav-demo" href="{prefix}/old" title="The old version of this webiste">Old</a></li>
<li><a id="nav-download" href="{prefix}/opds" title="An OPDS feed based version of this website, used in special purpose applications">Feed</a></li>
</ul>
</div>
@ -58,7 +58,7 @@
<input type="hidden" name="cmd" value="_s-xclick"></input>
<input type="hidden" name="hosted_button_id" value="3028915"></input>
<input type="image"
src="/static/button-donate.png"
src="{prefix}/static/button-donate.png"
name="submit"></input>
<img alt="" src="https://www.paypal.com/en_US/i/scr/pixel.gif"
width="1" height="1"></img>
@ -76,7 +76,7 @@
</select>
</div>
<div id="search_box">
<form name="search_form" action="/browse/search" method="get" accept-charset="UTF-8">
<form name="search_form" action="{prefix}/browse/search" method="get" accept-charset="UTF-8">
<input value="{initial_search}" type="text" title="Search" name="query"
class="search_input" />&nbsp;
<input type="submit" value="Search" title="Search" alt="Search" />

View File

@ -1,6 +1,6 @@
<div id="details_{id}" class="details">
<div class="left">
<img alt="Cover of {title}" src="/get/cover/{id}" />
<img alt="Cover of {title}" src="{prefix}/get/cover/{id}" />
</div>
<div class="right">
<div class="field formats">{formats}</div>

View File

@ -1,6 +1,6 @@
<div id="summary_{id}" class="summary">
<div class="left">
<img alt="Cover of {title}" src="/get/thumb_90_120/{id}" />
<img alt="Cover of {title}" src="{prefix}/get/thumb_90_120/{id}" />
{get_button}
</div>
<div class="right">
@ -8,7 +8,7 @@
<span class="rating_container">{stars}</span>
<span class="series">{series}</span>
<a href="#" onclick="show_details(this); return false;" title="{details_tt}">{details}</a>
<a href="/browse/book/{id}" title="{permalink_tt}">{permalink}</a>
<a href="{prefix}/browse/book/{id}" title="{permalink_tt}">{permalink}</a>
</div>
<div class="title"><strong>{title}</strong></div>
<div class="authors">{authors}</div>

View File

@ -44,6 +44,10 @@ def server_config(defaults=None):
'by first letter when there are more than this number '
'of items. Default: %default. Set to a large number '
'to disable grouping.'))
c.add_opt('url_prefix', ['--url-prefix'], default='',
help=_('Prefix to prepend to all URLs. Useful for reverse'
'proxying to this server from Apache/nginx/etc.'))
return c
def custom_fields_to_display(db):

View File

@ -28,16 +28,19 @@ from calibre.library.server.browse import BrowseServer
class DispatchController(object): # {{{
def __init__(self):
def __init__(self, prefix):
self.dispatcher = cherrypy.dispatch.RoutesDispatcher()
self.funcs = []
self.seen = set([])
self.prefix = prefix if prefix else ''
def __call__(self, name, route, func, **kwargs):
if name in self.seen:
raise NameError('Route name: '+ repr(name) + ' already used')
self.seen.add(name)
kwargs['action'] = 'f_%d'%len(self.funcs)
if route != '/':
route = self.prefix + route
self.dispatcher.connect(name, route, self, **kwargs)
self.funcs.append(expose(func))
@ -55,7 +58,7 @@ class DispatchController(object): # {{{
# }}}
class BonJour(SimplePlugin):
class BonJour(SimplePlugin): # {{{
def __init__(self, engine, port=8080):
SimplePlugin.__init__(self, engine)
@ -85,6 +88,7 @@ class BonJour(SimplePlugin):
cherrypy.engine.bonjour = BonJour(cherrypy.engine)
# }}}
class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache,
BrowseServer):
@ -177,7 +181,7 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache,
def start(self):
self.is_running = False
d = DispatchController()
d = DispatchController(self.opts.url_prefix)
for x in self.__class__.__bases__:
if hasattr(x, 'add_routes'):
x.add_routes(self, d)

View File

@ -22,7 +22,7 @@ from calibre.library.comments import comments_to_html
from calibre.library.server import custom_fields_to_display
from calibre.library.field_metadata import category_icon_map
def render_book_list(ids, suffix=''): # {{{
def render_book_list(ids, prefix, suffix=''): # {{{
pages = []
num = len(ids)
pos = 0
@ -35,11 +35,11 @@ def render_book_list(ids, suffix=''): # {{{
page_template = u'''\
<div class="page" id="page{0}">
<div class="load_data" title="{1}">
<span class="url" title="/browse/booklist_page"></span>
<span class="url" title="{prefix}/browse/booklist_page"></span>
<span class="start" title="{start}"></span>
<span class="end" title="{end}"></span>
</div>
<div class="loading"><img src="/static/loading.gif" /> {2}</div>
<div class="loading"><img src="{prefix}/static/loading.gif" /> {2}</div>
<div class="loaded"></div>
</div>
'''
@ -49,7 +49,7 @@ def render_book_list(ids, suffix=''): # {{{
ld = xml(json.dumps(pg), True)
rpages.append(page_template.format(i, ld,
xml(_('Loading, please wait')) + '&hellip;',
start=pos+1, end=pos+len(pg)))
start=pos+1, end=pos+len(pg), prefix=prefix))
rpages = u'\n\n'.join(rpages)
templ = u'''\
@ -91,7 +91,7 @@ def utf8(x): # {{{
return x
# }}}
def render_rating(rating, container='span', prefix=None): # {{{
def render_rating(rating, url_prefix, container='span', prefix=None): # {{{
if rating < 0.1:
return '', ''
added = 0
@ -108,15 +108,15 @@ def render_rating(rating, container='span', prefix=None): # {{{
elif n >= 0.9:
x = 'on'
ans.append(
u'<img alt="{0}" title="{0}" src="/static/star-{1}.png" />'.format(
rstring, x))
u'<img alt="{0}" title="{0}" src="{2}/static/star-{1}.png" />'.format(
rstring, x, url_prefix))
added += 1
ans.append('</%s>'%container)
return u''.join(ans), rstring
# }}}
def get_category_items(category, items, restriction, datatype): # {{{
def get_category_items(category, items, restriction, datatype, prefix): # {{{
if category == 'search':
items = [x for x in items if x.name != restriction]
@ -125,8 +125,8 @@ def get_category_items(category, items, restriction, datatype): # {{{
templ = (u'<div title="{4}" class="category-item">'
'<div class="category-name">{0}</div><div>{1}</div>'
'<div>{2}'
'<span class="href">{3}</span></div></div>')
rating, rstring = render_rating(i.avg_rating)
'<span class="href">{5}{3}</span></div></div>')
rating, rstring = render_rating(i.avg_rating, prefix)
name = xml(i.name)
if datatype == 'rating':
name = xml(_('%d stars')%int(i.avg_rating))
@ -142,7 +142,7 @@ def get_category_items(category, items, restriction, datatype): # {{{
q = category
href = '/browse/matches/%s/%s'%(quote(q), quote(id_))
return templ.format(xml(name), rating,
xml(desc), xml(href), rstring)
xml(desc), xml(href), rstring, prefix)
items = list(map(item, items))
return '\n'.join(['<div class="category-container">'] + items + ['</div>'])
@ -243,6 +243,7 @@ class BrowseServer(object):
ans = ans.replace('{sort_select_label}', xml(_('Sort by')+':'))
ans = ans.replace('{sort_cookie_name}', scn)
ans = ans.replace('{prefix}', self.opts.url_prefix)
opts = ['<option %svalue="%s">%s</option>' % (
'selected="selected" ' if k==sort else '',
xml(k), xml(n), ) for k, n in
@ -258,15 +259,14 @@ class BrowseServer(object):
ans = ans.replace('{initial_search}', initial_search)
return ans
return self.__browse_template__
@property
def browse_summary_template(self):
if not hasattr(self, '__browse_summary_template__') or \
self.opts.develop:
self.__browse_summary_template__ = \
P('content_server/browse/summary.html', data=True).decode('utf-8')
return self.__browse_summary_template__
return self.__browse_summary_template__.replace('{prefix}',
self.opts.url_prefix)
@property
def browse_details_template(self):
@ -274,7 +274,8 @@ class BrowseServer(object):
self.opts.develop:
self.__browse_details_template__ = \
P('content_server/browse/details.html', data=True).decode('utf-8')
return self.__browse_details_template__
return self.__browse_details_template__.replace('{prefix}',
self.opts.url_prefix)
# }}}
@ -334,11 +335,11 @@ class BrowseServer(object):
icon = 'blank.png'
cats.append((meta['name'], category, icon))
cats = [('<li title="{2} {0}"><img src="{src}" alt="{0}" />'
cats = [('<li title="{2} {0}"><img src="{3}{src}" alt="{0}" />'
'<span class="label">{0}</span>'
'<span class="url">/browse/category/{1}</span></li>')
'<span class="url">{3}/browse/category/{1}</span></li>')
.format(xml(x, True), xml(quote(y)), xml(_('Browse books by')),
src='/browse/icon/'+z)
self.opts.url_prefix, src='/browse/icon/'+z)
for x, y, z in cats]
main = '<div class="toplevel"><h3>{0}</h3><ul>{1}</ul></div>'\
@ -378,7 +379,8 @@ class BrowseServer(object):
if len(items) <= self.opts.max_opds_ungrouped_items:
script = 'false'
items = get_category_items(category, items,
self.search_restriction_name, datatype)
self.search_restriction_name, datatype,
self.opts.url_prefix)
else:
getter = lambda x: unicode(getattr(x, 'sort', x.name))
starts = set([])
@ -393,12 +395,13 @@ class BrowseServer(object):
getter(y).upper().startswith(x)])
items = [(u'<h3 title="{0}">{0} <span>[{2}]</span></h3><div>'
u'<div class="loaded" style="display:none"></div>'
u'<div class="loading"><img alt="{1}" src="/static/loading.gif" /><em>{1}</em></div>'
u'<span class="load_href">{3}</span></div>').format(
u'<div class="loading"><img alt="{1}" src="{4}/static/loading.gif" /><em>{1}</em></div>'
u'<span class="load_href">{4}{3}</span></div>').format(
xml(s, True),
xml(_('Loading, please wait'))+'&hellip;',
unicode(c),
xml(u'/browse/category_group/%s/%s'%(category, s)))
xml(u'/browse/category_group/%s/%s'%(category, s)),
self.opts.url_prefix)
for s, c in category_groups.items()]
items = '\n\n'.join(items)
items = u'<div id="groups">\n{0}</div>'.format(items)
@ -410,13 +413,13 @@ class BrowseServer(object):
main = u'''
<div class="category">
<h3>{0}</h3>
<a class="navlink" href="/browse"
<a class="navlink" href="{3}/browse"
title="{2}">{2}&nbsp;&uarr;</a>
{1}
</div>
'''.format(
xml(_('Browsing by')+': ' + category_name), items,
xml(_('Up'), True))
xml(_('Up'), True), self.opts.url_prefix)
return self.browse_template(sort).format(title=category_name,
script=script, main=main)
@ -449,7 +452,8 @@ class BrowseServer(object):
sort = self.browse_sort_categories(entries, sort)
entries = get_category_items(category, entries,
self.search_restriction_name, datatype)
self.search_restriction_name, datatype,
self.opts.url_prefix)
return json.dumps(entries, ensure_ascii=False)
@ -459,9 +463,11 @@ class BrowseServer(object):
if category == None:
ans = self.browse_toplevel()
elif category == 'newest':
raise cherrypy.InternalRedirect('/browse/matches/newest/dummy')
raise cherrypy.InternalRedirect(self.opts.url_prefix +
'/browse/matches/newest/dummy')
elif category == 'allbooks':
raise cherrypy.InternalRedirect('/browse/matches/allbooks/dummy')
raise cherrypy.InternalRedirect(self.opts.url_prefix +
'/browse/matches/allbooks/dummy')
else:
ans = self.browse_category(category, category_sort)
@ -532,7 +538,8 @@ class BrowseServer(object):
list_sort = category
sort = self.browse_sort_book_list(items, list_sort)
ids = [x[0] for x in items]
html = render_book_list(ids, suffix=_('in') + ' ' + category_name)
html = render_book_list(ids, self.opts.url_prefix,
suffix=_('in') + ' ' + category_name)
return self.browse_template(sort, category=False).format(
title=_('Books in') + " " +category_name,
@ -580,17 +587,18 @@ class BrowseServer(object):
if fmts and fmt:
other_fmts = [x for x in fmts if x.lower() != fmt.lower()]
if other_fmts:
ofmts = [u'<a href="/get/{0}/{1}_{2}.{0}" title="{3}">{3}</a>'\
.format(f, fname, id_, f.upper()) for f in
ofmts = [u'<a href="{4}/get/{0}/{1}_{2}.{0}" title="{3}">{3}</a>'\
.format(f, fname, id_, f.upper(),
self.opts.url_prefix) for f in
other_fmts]
ofmts = ', '.join(ofmts)
args['other_formats'] = u'<strong>%s: </strong>' % \
_('Other formats') + ofmts
args['details_href'] = '/browse/details/'+str(id_)
args['details_href'] = self.opts.url_prefix + '/browse/details/'+str(id_)
if fmt:
href = '/get/%s/%s_%d.%s'%(
href = self.opts.url_prefix + '/get/%s/%s_%d.%s'%(
fmt, fname, id_, fmt)
rt = xml(_('Read %s in the %s format')%(args['title'],
fmt.upper()), True)
@ -603,7 +611,8 @@ class BrowseServer(object):
args['comments'] = comments_to_html(mi.comments)
args['stars'] = ''
if mi.rating:
args['stars'] = render_rating(mi.rating/2.0, prefix=_('Rating'))[0]
args['stars'] = render_rating(mi.rating/2.0,
self.opts.url_prefix, prefix=_('Rating'))[0]
if args['tags']:
args['tags'] = u'<strong>%s: </strong>'%xml(_('Tags')) + \
args['tags']
@ -628,8 +637,9 @@ class BrowseServer(object):
args, fmt, fmts, fname = self.browse_get_book_args(mi, id_)
args['formats'] = ''
if fmts:
ofmts = [u'<a href="/get/{0}/{1}_{2}.{0}" title="{3}">{3}</a>'\
.format(fmt, fname, id_, fmt.upper()) for fmt in
ofmts = [u'<a href="{4}/get/{0}/{1}_{2}.{0}" title="{3}">{3}</a>'\
.format(fmt, fname, id_, fmt.upper(),
self.opts.url_prefix) for fmt in
fmts]
ofmts = ', '.join(ofmts)
args['formats'] = ofmts
@ -648,7 +658,8 @@ class BrowseServer(object):
continue
if m['datatype'] == 'rating':
r = u'<strong>%s: </strong>'%xml(m['name']) + \
render_rating(mi.rating/2.0, prefix=m['name'])[0]
render_rating(mi.rating/2.0, self.opts.url_prefix,
prefix=m['name'])[0]
else:
r = u'<strong>%s: </strong>'%xml(m['name']) + \
args[field]
@ -704,7 +715,8 @@ class BrowseServer(object):
items = [self.db.data._data[x] for x in ids]
sort = self.browse_sort_book_list(items, list_sort)
ids = [x[0] for x in items]
html = render_book_list(ids, suffix=_('in search')+': '+query)
html = render_book_list(ids, self.opts.url_prefix,
suffix=_('in search')+': '+query)
return self.browse_template(sort, category=False, initial_search=query).format(
title=_('Matching books'),
script='booklist();', main=html)

View File

@ -103,7 +103,11 @@ class ContentServer(object):
if self.opts.develop:
lm = fromtimestamp(os.stat(path).st_mtime)
cherrypy.response.headers['Last-Modified'] = self.last_modified(lm)
return open(path, 'rb').read()
with open(path, 'rb') as f:
ans = f.read()
if path.endswith('.css'):
ans = ans.replace('/static/', self.opts.url_prefix + '/static/')
return ans
def index(self, **kwargs):
'The / URL'

View File

@ -26,9 +26,9 @@ def CLASS(*args, **kwargs): # class is a reserved word in Python
return kwargs
def build_search_box(num, search, sort, order): # {{{
def build_search_box(num, search, sort, order, prefix): # {{{
div = DIV(id='search_box')
form = FORM('Show ', method='get', action='mobile')
form = FORM('Show ', method='get', action=prefix+'/mobile')
form.set('accept-charset', 'UTF-8')
div.append(form)
@ -89,11 +89,12 @@ def build_navigation(start, num, total, url_base): # {{{
# }}}
def build_index(books, num, search, sort, order, start, total, url_base, CKEYS):
logo = DIV(IMG(src='/static/calibre.png', alt=__appname__), id='logo')
def build_index(books, num, search, sort, order, start, total, url_base, CKEYS,
prefix):
logo = DIV(IMG(src=prefix+'/static/calibre.png', alt=__appname__), id='logo')
search_box = build_search_box(num, search, sort, order)
navigation = build_navigation(start, num, total, url_base)
search_box = build_search_box(num, search, sort, order, prefix)
navigation = build_navigation(start, num, total, prefix+url_base)
bookt = TABLE(id='listing')
body = BODY(
@ -107,7 +108,8 @@ def build_index(books, num, search, sort, order, start, total, url_base, CKEYS):
# Book list {{{
for book in books:
thumbnail = TD(
IMG(type='image/jpeg', border='0', src='/get/thumb/%s' %
IMG(type='image/jpeg', border='0',
src=prefix+'/get/thumb/%s' %
book['id']),
CLASS('thumbnail'))
@ -118,7 +120,7 @@ def build_index(books, num, search, sort, order, start, total, url_base, CKEYS):
s = SPAN(
A(
fmt.lower(),
href='/get/%s/%s-%s_%d.%s' % (fmt, a, t,
href=prefix+'/get/%s/%s-%s_%d.%s' % (fmt, a, t,
book['id'], fmt)
),
CLASS('button'))
@ -154,7 +156,7 @@ def build_index(books, num, search, sort, order, start, total, url_base, CKEYS):
TITLE(__appname__ + ' Library'),
LINK(rel='icon', href='http://calibre-ebook.com/favicon.ico',
type='image/x-icon'),
LINK(rel='stylesheet', type='text/css', href='/mobile/style.css')
LINK(rel='stylesheet', type='text/css', href=prefix+'/mobile/style.css')
), # End head
body
) # End html
@ -174,7 +176,9 @@ class MobileServer(object):
cherrypy.response.headers['Content-Type'] = 'text/css; charset=utf-8'
updated = utcfromtimestamp(os.stat(path).st_mtime)
cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
return open(path, 'rb').read()
with open(path, 'rb') as f:
ans = f.read()
return ans.replace('{prefix}', self.opts.url_prefix)
def mobile(self, start='1', num='25', sort='date', search='',
_=None, order='descending'):
@ -259,7 +263,8 @@ class MobileServer(object):
url_base = "/mobile?search=" + search+";order="+order+";sort="+sort+";num="+str(num)
return html.tostring(build_index(books, num, search, sort, order,
start, len(ids), url_base, CKEYS),
start, len(ids), url_base, CKEYS,
self.opts.url_prefix),
encoding='utf-8', include_meta_content_type=True,
pretty_print=True)