Switch to using localStorage instead of cookies for sessions

This commit is contained in:
Kovid Goyal 2015-11-04 14:58:46 +05:30
parent 9431a6e3ee
commit e2bfb32dc9
11 changed files with 171 additions and 98 deletions

View File

@ -6,7 +6,7 @@
<meta name="robots" content="noindex"> <meta name="robots" content="noindex">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/png" href="favicon.png"> <link rel="icon" type="image/png" href="favicon.png">
<script>window.calibre_entry_point = 'ENTRY_POINT';</script> <script>window.calibre_entry_point = 'ENTRY_POINT'; window.calibre_username = USERNAME;</script>
<script type="text/javascript" src="static/main.js"></script> <script type="text/javascript" src="static/main.js"></script>
<link rel="stylesheet" href="static/reset.css"></link> <link rel="stylesheet" href="static/reset.css"></link>
<link rel="stylesheet" href="static/font-awesome/fa.css"></link> <link rel="stylesheet" href="static/font-awesome/fa.css"></link>

View File

@ -18,7 +18,6 @@ from calibre.ebooks.metadata.book.json_codec import JsonCodec
from calibre.srv.errors import HTTPNotFound from calibre.srv.errors import HTTPNotFound
from calibre.srv.metadata import book_as_json from calibre.srv.metadata import book_as_json
from calibre.srv.routes import endpoint, 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.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.srv.utils import http_date, custom_fields_to_display, encode_name, decode_name
from calibre.utils.config import prefs, tweaks 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')) 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 Return the data needed to create the server main UI
Optional: ?num=50 Optional: ?num=50&sort=date.desc&library_id=<default library>
''' '''
session = rd.session ans = {'username':rd.username}
ans = {'session_data': {k:session[k] for k in defaults.iterkeys()}}
ans['library_map'], ans['default_library'] = ctx.library_map ans['library_map'], ans['default_library'] = ctx.library_map
ans['library_id'] = library_id or ans['default_library'] ans['library_id'], db, sorts, orders = get_basic_query_data(ctx, rd.query)
sorts, orders = [], []
for x in ans['session_data']['sort'].split(','):
s, o = x.partition(':')[::2]
sorts.append(s.strip()), orders.append(o.strip())
try: try:
num = int(rd.query.get('num', 50)) num = int(rd.query.get('num', 50))
except Exception: except Exception:
raise HTTPNotFound('Invalid number of books: %r' % rd.query.get('num')) raise HTTPNotFound('Invalid number of books: %r' % rd.query.get('num'))
db = get_db(ctx, library_id)
with db.safe_read_lock: with db.safe_read_lock:
ans['search_result'] = _search(ctx, rd, db, '', num, 0, ','.join(sorts), ','.join(orders)) ans['search_result'] = _search(ctx, rd, db, '', num, 0, ','.join(sorts), ','.join(orders))
ans['field_metadata'] = db.field_metadata.all_metadata() ans['field_metadata'] = db.field_metadata.all_metadata()

View File

@ -4,10 +4,11 @@
from __future__ import (unicode_literals, division, absolute_import, from __future__ import (unicode_literals, division, absolute_import,
print_function) print_function)
import re import re, json
from functools import partial from functools import partial
from threading import Lock from threading import Lock
from calibre import prepare_string_for_xml
from calibre.srv.routes import endpoint from calibre.srv.routes import endpoint
html_cache = {} html_cache = {}
@ -43,4 +44,5 @@ def get_html(name, auto_reload_port, **replacements):
def index(ctx, rd): def index(ctx, rd):
return rd.generate_static_output('/', partial( return rd.generate_static_output('/', partial(
get_html, 'content-server/index.html', getattr(rd.opts, 'auto_reload_port', 0), 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'))))

View File

@ -15,7 +15,6 @@ from threading import Lock
from calibre.db.cache import Cache from calibre.db.cache import Cache
from calibre.db.legacy import create_backend, LibraryDatabase from calibre.db.legacy import create_backend, LibraryDatabase
from calibre.srv.routes import Router from calibre.srv.routes import Router
from calibre.srv.session import Sessions
from calibre.utils.date import utcnow from calibre.utils.date import utcnow
def init_library(library_path): def init_library(library_path):
@ -80,21 +79,18 @@ class Context(object):
url_for = None url_for = None
CATEGORY_CACHE_SIZE = 25 CATEGORY_CACHE_SIZE = 25
SEARCH_CACHE_SIZE = 100 SEARCH_CACHE_SIZE = 100
SESSION_COOKIE = 'calibre_session'
def __init__(self, libraries, opts, testing=False): def __init__(self, libraries, opts, testing=False):
self.opts = opts self.opts = opts
self.library_broker = LibraryBroker(libraries) self.library_broker = LibraryBroker(libraries)
self.testing = testing self.testing = testing
self.lock = Lock() self.lock = Lock()
self.sessions = Sessions()
def init_session(self, endpoint, data): 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): def finalize_session(self, endpoint, data, output):
data.outcookie[self.SESSION_COOKIE] = data.session.key pass
data.outcookie[self.SESSION_COOKIE]['path'] = self.url_for(None)
def get_library(self, library_id=None): def get_library(self, library_id=None):
return self.library_broker.get(library_id) return self.library_broker.get(library_id)

View File

@ -1,66 +0,0 @@
#!/usr/bin/env python2
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net>
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)

View File

@ -1,10 +1,21 @@
# vim:fileencoding=utf-8 # vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net> # License: GPL v3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net>
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() 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: if bypass_cache:
path += ('&' if '?' in path else '?') + Date().getTime() path += ('&' if has_query else '?') + Date().getTime()
xhr.request_path = path xhr.request_path = path
def progress_callback(ev): def progress_callback(ev):

View File

@ -2,11 +2,20 @@
# License: GPL v3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net> # License: GPL v3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net>
boss = None boss = None
session_data = None
def get_boss(): def get_boss():
nonlocal boss
return boss return boss
def set_boss(obj): def set_boss(obj):
nonlocal boss nonlocal boss
boss = obj 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

View File

@ -70,8 +70,9 @@ class ItemsView:
a = ul.lastChild.firstChild a = ul.lastChild.firstChild
if item.subtitle: if item.subtitle:
a.appendChild(E.div(item.subtitle, class_='subtitle')) a.appendChild(E.div(item.subtitle, class_='subtitle'))
a.addEventListener('click', def(event): event.preventDefault();)
if item.action: 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): def create_item(title, action=None, subtitle=None, icon_name=None):

View File

@ -4,11 +4,13 @@
from dom import set_css, build_rule from dom import set_css, build_rule
from elementmaker import E from elementmaker import E
from gettext import gettext as _ from gettext import gettext as _
from book_list.globals import get_session_data
bv_counter = 0
THUMBNAIL_MAX_WIDTH = 300 THUMBNAIL_MAX_WIDTH = 300
THUMBNAIL_MAX_HEIGHT = 400 THUMBNAIL_MAX_HEIGHT = 400
bv_counter = 0
class BooksView: class BooksView:
def __init__(self, interface_data): def __init__(self, interface_data):
@ -27,7 +29,7 @@ class BooksView:
E.div(id='get-more-books') E.div(id='get-more-books')
) )
document.body.appendChild(div) 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'): def set_view_mode(self, mode='cover_grid'):
if self.mode == mode: if self.mode == mode:

96
src/pyj/session.pyj Normal file
View File

@ -0,0 +1,96 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net>
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)

View File

@ -4,17 +4,19 @@
from ajax import ajax from ajax import ajax
from elementmaker import E from elementmaker import E
from gettext import gettext as _ from gettext import gettext as _
from session import UserSessionData
from book_list.boss import Boss 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): def on_library_loaded(end_type, xhr, ev):
p = document.getElementById('page_load_progress') p = document.getElementById('page_load_progress')
p.parentNode.removeChild(p) p.parentNode.removeChild(p)
if end_type == 'load': if end_type == 'load':
boss = Boss(JSON.parse(xhr.responseText)) interface_data = JSON.parse(xhr.responseText)
boss = Boss(interface_data)
set_boss(boss) set_boss(boss)
else: 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: [{}] {}'), 'Failed to download library data from "{}", with status: [{}] {}'),
xhr.request_path, xhr.status, xhr.statusText))) xhr.request_path, xhr.status, xhr.statusText)))
@ -23,11 +25,16 @@ def on_library_load_progress(loaded, total):
p.max = total p.max = total
p.value = loaded 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(): def on_load():
if window.calibre_entry_point == 'book list': 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 # 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 # with a largely empty starting document, we can use this to preload any resources
# we know are going to be needed immediately. # we know are going to be needed immediately.
window.addEventListener("load", on_load) window.addEventListener('load', on_load)