mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-07 18:24:30 -04:00
Switch to using localStorage instead of cookies for sessions
This commit is contained in:
parent
9431a6e3ee
commit
e2bfb32dc9
@ -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>
|
||||
|
@ -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()
|
||||
|
@ -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'))))
|
||||
|
@ -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)
|
||||
|
@ -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)
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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
96
src/pyj/session.pyj
Normal 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)
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user