diff --git a/resources/content-server/index.html b/resources/content-server/index.html index 5460c8cc33..f93c9522db 100644 --- a/resources/content-server/index.html +++ b/resources/content-server/index.html @@ -6,7 +6,7 @@ - + diff --git a/src/calibre/srv/ajax.py b/src/calibre/srv/ajax.py index 95bb66d488..701899206c 100644 --- a/src/calibre/srv/ajax.py +++ b/src/calibre/srv/ajax.py @@ -18,7 +18,6 @@ from calibre.ebooks.metadata.book.json_codec import JsonCodec from calibre.srv.errors import HTTPNotFound from calibre.srv.metadata import book_as_json from calibre.srv.routes import endpoint, json -from calibre.srv.session import defaults from calibre.srv.content import get as get_content, icon as get_icon from calibre.srv.utils import http_date, custom_fields_to_display, encode_name, decode_name from calibre.utils.config import prefs, tweaks @@ -562,27 +561,43 @@ def search(ctx, rd, library_id): return _search(ctx, rd, db, query, num, offset, rd.query.get('sort', 'title'), rd.query.get('sort_order', 'asc')) # }}} +def get_basic_query_data(ctx, query): + library_id = query.get('library_id') + library_map, default_library = ctx.library_map + if library_id not in library_map: + library_id = default_library + db = get_db(ctx, library_id) + skeys = db.field_metadata.sortable_field_keys() + sorts, orders = [], [] + for x in query.get('sort', '').split(','): + if x: + s, o = x.partition('.')[::2] + if o not in ('asc', 'desc'): + o = 'asc' + if s.startswith('_'): + s = '#' + s[1:] + s = sanitize_sort_field_name(db.field_metadata, s) + if s in skeys: + sorts.append(s), orders.append(o) + if not sorts: + sorts, orders = ['date'], ['desc'] + return library_id, db, sorts, orders -@endpoint('/ajax/interface-data/{library_id=None}', postprocess=json) -def interface_data(ctx, rd, library_id): + +@endpoint('/ajax/interface-data', postprocess=json) +def interface_data(ctx, rd): ''' Return the data needed to create the server main UI - Optional: ?num=50 + Optional: ?num=50&sort=date.desc&library_id= ''' - session = rd.session - ans = {'session_data': {k:session[k] for k in defaults.iterkeys()}} + ans = {'username':rd.username} ans['library_map'], ans['default_library'] = ctx.library_map - ans['library_id'] = library_id or ans['default_library'] - sorts, orders = [], [] - for x in ans['session_data']['sort'].split(','): - s, o = x.partition(':')[::2] - sorts.append(s.strip()), orders.append(o.strip()) + ans['library_id'], db, sorts, orders = get_basic_query_data(ctx, rd.query) try: num = int(rd.query.get('num', 50)) except Exception: raise HTTPNotFound('Invalid number of books: %r' % rd.query.get('num')) - db = get_db(ctx, library_id) with db.safe_read_lock: ans['search_result'] = _search(ctx, rd, db, '', num, 0, ','.join(sorts), ','.join(orders)) ans['field_metadata'] = db.field_metadata.all_metadata() diff --git a/src/calibre/srv/code.py b/src/calibre/srv/code.py index 1a39d2fc9d..3cf0389fa4 100644 --- a/src/calibre/srv/code.py +++ b/src/calibre/srv/code.py @@ -4,10 +4,11 @@ from __future__ import (unicode_literals, division, absolute_import, print_function) -import re +import re, json from functools import partial from threading import Lock +from calibre import prepare_string_for_xml from calibre.srv.routes import endpoint html_cache = {} @@ -43,4 +44,5 @@ def get_html(name, auto_reload_port, **replacements): def index(ctx, rd): return rd.generate_static_output('/', partial( get_html, 'content-server/index.html', getattr(rd.opts, 'auto_reload_port', 0), - ENTRY_POINT='book list', LOADING_MSG=_('Loading library, please wait'))) + USERNAME=json.dumps(rd.username), ENTRY_POINT='book list', + LOADING_MSG=prepare_string_for_xml(_('Loading library, please wait')))) diff --git a/src/calibre/srv/handler.py b/src/calibre/srv/handler.py index 5ad89d5e69..38a3187693 100644 --- a/src/calibre/srv/handler.py +++ b/src/calibre/srv/handler.py @@ -15,7 +15,6 @@ from threading import Lock from calibre.db.cache import Cache from calibre.db.legacy import create_backend, LibraryDatabase from calibre.srv.routes import Router -from calibre.srv.session import Sessions from calibre.utils.date import utcnow def init_library(library_path): @@ -80,21 +79,18 @@ class Context(object): url_for = None CATEGORY_CACHE_SIZE = 25 SEARCH_CACHE_SIZE = 100 - SESSION_COOKIE = 'calibre_session' def __init__(self, libraries, opts, testing=False): self.opts = opts self.library_broker = LibraryBroker(libraries) self.testing = testing self.lock = Lock() - self.sessions = Sessions() def init_session(self, endpoint, data): - data.session = self.sessions.get_or_create(key=data.cookies.get(self.SESSION_COOKIE), username=data.username) + pass def finalize_session(self, endpoint, data, output): - data.outcookie[self.SESSION_COOKIE] = data.session.key - data.outcookie[self.SESSION_COOKIE]['path'] = self.url_for(None) + pass def get_library(self, library_id=None): return self.library_broker.get(library_id) diff --git a/src/calibre/srv/session.py b/src/calibre/srv/session.py deleted file mode 100644 index f6109861a3..0000000000 --- a/src/calibre/srv/session.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python2 -# vim:fileencoding=utf-8 -# License: GPLv3 Copyright: 2015, Kovid Goyal - -from __future__ import (unicode_literals, division, absolute_import, - print_function) -from copy import deepcopy -from uuid import uuid4 -from threading import Lock - -from calibre.utils.lru_cache import lru_cache - -defaults = { - 'sort': 'date:desc', - 'library_id': None, - 'view_mode': 'cover_grid', -} - -class Session(object): - - def __init__(self): - self._data = deepcopy(defaults) - - def __getitem__(self, key): - return self._data[key] - - def __setitem__(self, key, val): - self._data[key] = val - - -class SessionProxy(object): - - ''' Prevent the creation of a long-lived session object for every new - request without a session cookie. Instead, this object lives only as long - an individual request, and unless some setting is changed from the default - simply returns values from the global defaults object. ''' - - def __init__(self, sessions, key): - self.sessions = sessions - self.key = key - self.actual_session = None - - def __getitem__(self, key): - if self.actual_session is None: - return defaults[key] - return self.actual_session[key] - - def __setitem__(self, key, val): - with self.sessions.lock: - if self.actual_session is None: - self.actual_session = self.sessions.cache[self.key] = Session() - self.actual_session[key] = val - -class Sessions(object): - - def __init__(self): - self.cache = lru_cache(size=2000) - self.lock = Lock() - - def get_or_create(self, key=None, username=None): - key = key or str(uuid4()).replace('-', '') - try: - with self.lock: - return self.cache[key] - except KeyError: - return SessionProxy(self, key) diff --git a/src/pyj/ajax.pyj b/src/pyj/ajax.pyj index 7ade844f37..924a1ce6f1 100644 --- a/src/pyj/ajax.pyj +++ b/src/pyj/ajax.pyj @@ -1,10 +1,21 @@ # vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2015, Kovid Goyal -def ajax(path, on_complete, on_progress=None, bypass_cache=True, method='GET'): +def ajax(path, on_complete, on_progress=None, bypass_cache=True, method='GET', query=None): + query = query or {} xhr = XMLHttpRequest() + keys = Object.keys(query) + has_query = False + if keys.length: + for i, k in enumerate(keys): + val = query[k] + if val is undefined or val is None: + continue + path += ('?' if i == 0 else '&') + window.encodeURIComponent(k) + '=' + window.encodeURIComponent(val.toString()) + has_query = True if bypass_cache: - path += ('&' if '?' in path else '?') + Date().getTime() + path += ('&' if has_query else '?') + Date().getTime() + xhr.request_path = path def progress_callback(ev): diff --git a/src/pyj/book_list/globals.pyj b/src/pyj/book_list/globals.pyj index 34681493e8..ebc1b77bb9 100644 --- a/src/pyj/book_list/globals.pyj +++ b/src/pyj/book_list/globals.pyj @@ -2,11 +2,20 @@ # License: GPL v3 Copyright: 2015, Kovid Goyal boss = None +session_data = None def get_boss(): - nonlocal boss return boss def set_boss(obj): nonlocal boss boss = obj + return boss + +def set_session_data(sd): + nonlocal session_data + session_data = sd + return session_data + +def get_session_data(): + return session_data diff --git a/src/pyj/book_list/item_list.pyj b/src/pyj/book_list/item_list.pyj index f835f271f3..3eb35283f6 100644 --- a/src/pyj/book_list/item_list.pyj +++ b/src/pyj/book_list/item_list.pyj @@ -70,8 +70,9 @@ class ItemsView: a = ul.lastChild.firstChild if item.subtitle: a.appendChild(E.div(item.subtitle, class_='subtitle')) + a.addEventListener('click', def(event): event.preventDefault();) if item.action: - a.addEventListener('click', def(event): event.preventDefault(), item.action();) + ul.lastChild.addEventListener('click', item.action) def create_item(title, action=None, subtitle=None, icon_name=None): diff --git a/src/pyj/book_list/views.pyj b/src/pyj/book_list/views.pyj index a423a3a89b..3dbeae6512 100644 --- a/src/pyj/book_list/views.pyj +++ b/src/pyj/book_list/views.pyj @@ -4,11 +4,13 @@ from dom import set_css, build_rule from elementmaker import E from gettext import gettext as _ +from book_list.globals import get_session_data -bv_counter = 0 THUMBNAIL_MAX_WIDTH = 300 THUMBNAIL_MAX_HEIGHT = 400 +bv_counter = 0 + class BooksView: def __init__(self, interface_data): @@ -27,7 +29,7 @@ class BooksView: E.div(id='get-more-books') ) document.body.appendChild(div) - self.set_view_mode(interface_data['session_data']['view_mode']) + self.set_view_mode(get_session_data().get('view_mode')) def set_view_mode(self, mode='cover_grid'): if self.mode == mode: diff --git a/src/pyj/session.pyj b/src/pyj/session.pyj new file mode 100644 index 0000000000..2200a5d9d7 --- /dev/null +++ b/src/pyj/session.pyj @@ -0,0 +1,96 @@ +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2015, Kovid Goyal + +def storage_available(which): + which = which or 'localStorage' + try: + storage = window[which] + x = '__storage__test__' + storage.setItem(x, x) + storage.removeItem(x) + return True + except: + return False + +session_storage = None + +class FakeStorage: + + def __init__(self): + self.data = {} + + def getItem(self, key): + return self.data[key] + + def setItem(self, key, value): + if type(value) != 'string': + value = JSON.stringify(value) + self.data[key] = value + + def clear(self): + self.data = {} + +def get_session_storage(): + nonlocal session_storage + if session_storage is None: + if storage_available('localStorage'): + session_storage = window.localStorage + elif storage_available('sessionStorage'): + session_storage = window.sessionStorage + console.error('localStorage not available using sessionStorage instead') + else: + session_storage = FakeStorage() + console.error('sessionStorage and localStorage not available using a temp cache instead') + return session_storage + +class SessionData: + + def __init__(self): + self.storage = get_session_storage() + self.overflow_storage = {} + self.has_overflow = False + + def get(self, key, defval): + if defval is undefined: + defval = None + if self.has_overflow: + ans = self.overflow_storage[key] + if ans is undefined: + ans = self.storage.getItem(key) + else: + ans = self.storage.getItem(key) + if ans is undefined: + return defval + return JSON.parse(ans) + + def set(self, key, value): + value = JSON.stringify(value) + try: + self.storage.setItem(key, value) + v'delete self.overflow_storage[key]' + return True + except: + self.overflow_storage[key] = value + self.has_overflow = True + console.error('session storage has overflowed, using a temp cache instead') + return False + + def clear(self): + self.storage.clear() + self.overflow_storage = {} + self.has_overflow = False + +class UserSessionData(SessionData): + + def __init__(self, username): + self.prefix = (username or '') + ':' + self.has_user = bool(username) + self.username = username + SessionData.__init__(self) + + def get(self, key, defval): + return SessionData.get(self, (self.prefix + key), defval) + + def set(self, key, value): + return SessionData.set(self, (self.prefix + key), value) + diff --git a/src/pyj/srv.pyj b/src/pyj/srv.pyj index d81764d019..a77be229e1 100644 --- a/src/pyj/srv.pyj +++ b/src/pyj/srv.pyj @@ -4,17 +4,19 @@ from ajax import ajax from elementmaker import E from gettext import gettext as _ +from session import UserSessionData from book_list.boss import Boss -from book_list.globals import set_boss +from book_list.globals import set_boss, set_session_data def on_library_loaded(end_type, xhr, ev): p = document.getElementById('page_load_progress') p.parentNode.removeChild(p) if end_type == 'load': - boss = Boss(JSON.parse(xhr.responseText)) + interface_data = JSON.parse(xhr.responseText) + boss = Boss(interface_data) set_boss(boss) else: - document.body.appendChild(E.p(style="color:red", str.format(_( + document.body.appendChild(E.p(style='color:red', str.format(_( 'Failed to download library data from "{}", with status: [{}] {}'), xhr.request_path, xhr.status, xhr.statusText))) @@ -23,11 +25,16 @@ def on_library_load_progress(loaded, total): p.max = total p.value = loaded +def load_book_list(): + sd = set_session_data(UserSessionData(window.calibre_username)) + ajax('ajax/interface-data', on_library_loaded, on_library_load_progress, query={ + 'library_id':sd.get('library_id'), 'sort':sd.get('sort')}).send() + def on_load(): if window.calibre_entry_point == 'book list': - ajax('ajax/interface-data', on_library_loaded, on_library_load_progress).send() + load_book_list() # We wait for all page elements to load, since this is a single page app # with a largely empty starting document, we can use this to preload any resources # we know are going to be needed immediately. -window.addEventListener("load", on_load) +window.addEventListener('load', on_load)