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="viewport" content="width=device-width, initial-scale=1">
<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>
<link rel="stylesheet" href="static/reset.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.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=<default library>
'''
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()

View File

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

View File

@ -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)

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
# 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()
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):

View File

@ -2,11 +2,20 @@
# License: GPL v3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net>
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

View File

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

View File

@ -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:

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