%PDF- %PDF-
| Direktori : /lib/calibre/calibre/srv/ |
| Current File : //lib/calibre/calibre/srv/library_broker.py |
#!/usr/bin/env python3
# License: GPLv3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
import os
from collections import OrderedDict, defaultdict
from threading import RLock as Lock
from calibre import filesystem_encoding
from calibre.db.cache import Cache
from calibre.db.legacy import LibraryDatabase, create_backend, set_global_state
from calibre.utils.filenames import samefile as _samefile
from calibre.utils.monotonic import monotonic
from polyglot.builtins import iteritems, itervalues
def gui_on_db_event(event_type, library_id, event_data):
from calibre.gui2.ui import get_gui
gui = get_gui()
if gui is not None:
gui.library_broker.on_db_event(event_type, library_id, event_data)
def canonicalize_path(p):
if isinstance(p, bytes):
p = p.decode(filesystem_encoding)
p = os.path.abspath(p).replace(os.sep, '/').rstrip('/')
return os.path.normcase(p)
def samefile(a, b):
a, b = canonicalize_path(a), canonicalize_path(b)
if a == b:
return True
return _samefile(a, b)
def basename(path):
while path and path[-1] in ('/' + os.sep):
path = path[:-1]
ans = os.path.basename(path)
if not ans:
# Can happen for a path like D:\ on windows
if len(path) == 2 and path[1] == ':':
ans = path[0]
return ans or 'Library'
def init_library(library_path, is_default_library):
db = Cache(
create_backend(
library_path, load_user_formatter_functions=is_default_library))
db.init()
return db
def make_library_id_unique(library_id, existing):
bname = library_id
c = 0
while library_id in existing:
c += 1
library_id = bname + ('%d' % c)
return library_id
def library_id_from_path(path, existing=frozenset()):
library_id = basename(path).replace(' ', '_')
return make_library_id_unique(library_id, existing)
def correct_case_of_last_path_component(original_path):
original_path = os.path.abspath(original_path)
prefix, basename = os.path.split(original_path)
q = basename.lower()
try:
equals = tuple(x for x in os.listdir(prefix) if x.lower() == q)
except OSError:
equals = ()
if len(equals) > 1:
if basename not in equals:
basename = equals[0]
elif equals:
basename = equals[0]
return os.path.join(prefix, basename)
def db_matches(db, library_id, library_path):
db = db.new_api
if getattr(db, 'server_library_id', object()) == library_id:
return True
dbpath = db.dbpath
return samefile(dbpath, os.path.join(library_path, os.path.basename(dbpath)))
class LibraryBroker:
def __init__(self, libraries):
self.lock = Lock()
self.lmap = OrderedDict()
self.library_name_map = {}
self.original_path_map = {}
seen = set()
for original_path in libraries:
path = canonicalize_path(original_path)
if path in seen:
continue
is_samefile = False
for s in seen:
if samefile(s, path):
is_samefile = True
break
seen.add(path)
if is_samefile or not LibraryDatabase.exists_at(path):
continue
corrected_path = correct_case_of_last_path_component(original_path)
library_id = library_id_from_path(corrected_path, self.lmap)
self.lmap[library_id] = path
self.library_name_map[library_id] = basename(corrected_path)
self.original_path_map[path] = original_path
self.loaded_dbs = {}
self.category_caches, self.search_caches, self.tag_browser_caches = (
defaultdict(OrderedDict), defaultdict(OrderedDict),
defaultdict(OrderedDict))
def get(self, library_id=None):
with self:
library_id = library_id or self.default_library
if library_id in self.loaded_dbs:
return self.loaded_dbs[library_id]
path = self.lmap.get(library_id)
if path is None:
return
try:
self.loaded_dbs[library_id] = ans = self.init_library(
path, library_id == self.default_library)
ans.new_api.server_library_id = library_id
except Exception:
self.loaded_dbs[library_id] = None
raise
return ans
def init_library(self, library_path, is_default_library):
library_path = self.original_path_map.get(library_path, library_path)
return init_library(library_path, is_default_library)
def close(self):
with self:
for db in itervalues(self.loaded_dbs):
getattr(db, 'close', lambda: None)()
self.lmap, self.loaded_dbs = OrderedDict(), {}
@property
def default_library(self):
return next(iter(self.lmap))
@property
def library_map(self):
with self:
return self.library_name_map.copy()
def allowed_libraries(self, filter_func):
with self:
allowed_names = filter_func(
basename(l) for l in itervalues(self.lmap))
return OrderedDict(((lid, self.library_map[lid])
for lid, path in iteritems(self.lmap)
if basename(path) in allowed_names))
def path_for_library_id(self, library_id):
with self:
lpath = self.lmap.get(library_id)
if lpath is None:
q = library_id.lower()
for k, v in self.lmap.items():
if k.lower() == q:
lpath = v
break
else:
return
return self.original_path_map.get(lpath)
def __enter__(self):
self.lock.acquire()
def __exit__(self, *a):
self.lock.release()
EXPIRED_AGE = 300 # seconds
def load_gui_libraries(gprefs=None):
if gprefs is None:
from calibre.utils.config import JSONConfig
gprefs = JSONConfig('gui')
stats = gprefs.get('library_usage_stats', {})
return sorted(stats, key=stats.get, reverse=True)
def path_for_db(db):
return db.new_api.backend.library_path
class GuiLibraryBroker(LibraryBroker):
def __init__(self, db):
from calibre.gui2 import gprefs
self.last_used_times = defaultdict(lambda: -EXPIRED_AGE)
self.gui_library_id = None
self.listening_for_db_events = False
LibraryBroker.__init__(self, load_gui_libraries(gprefs))
self.gui_library_changed(db)
def init_library(self, library_path, is_default_library):
library_path = self.original_path_map.get(library_path, library_path)
db = LibraryDatabase(library_path, is_second_db=True)
if self.listening_for_db_events:
db.new_api.add_listener(gui_on_db_event)
return db
def get(self, library_id=None):
try:
return getattr(LibraryBroker.get(self, library_id), 'new_api', None)
finally:
self.last_used_times[library_id or self.default_library] = monotonic()
def start_listening_for_db_events(self):
with self:
self.listening_for_db_events = True
for db in self.loaded_dbs.values():
db.new_api.add_listener(gui_on_db_event)
def on_db_event(self, event_type, library_id, event_data):
from calibre.gui2.ui import get_gui
gui = get_gui()
if gui is not None:
with self:
db = self.loaded_dbs.get(library_id)
if db is not None:
gui.event_in_db.emit(db, event_type, event_data)
def get_library(self, original_library_path):
library_path = canonicalize_path(original_library_path)
with self:
for library_id, path in iteritems(self.lmap):
if samefile(library_path, path):
db = self.loaded_dbs.get(library_id)
if db is None:
db = self.loaded_dbs[library_id] = self.init_library(
path, False)
db.new_api.server_library_id = library_id
return db
# A new library
if library_path not in self.original_path_map:
self.original_path_map[library_path] = original_library_path
db = self.init_library(library_path, False)
corrected_path = correct_case_of_last_path_component(original_library_path)
library_id = library_id_from_path(corrected_path, self.lmap)
db.new_api.server_library_id = library_id
self.lmap[library_id] = library_path
self.library_name_map[library_id] = basename(corrected_path)
self.loaded_dbs[library_id] = db
return db
def prepare_for_gui_library_change(self, newloc):
# Must be called with lock held
for library_id, path in iteritems(self.lmap):
db = self.loaded_dbs.get(library_id)
if db is not None and samefile(newloc, path):
if library_id == self.gui_library_id:
# Have to reload db
self.loaded_dbs.pop(library_id, None)
return
set_global_state(db)
return db
def gui_library_changed(self, db, olddb=None):
# Must be called with lock held
original_path = path_for_db(db)
newloc = canonicalize_path(original_path)
for library_id, path in iteritems(self.lmap):
if samefile(newloc, path):
self.loaded_dbs[library_id] = db
self.gui_library_id = library_id
break
else:
# A new library
corrected_path = correct_case_of_last_path_component(original_path)
library_id = self.gui_library_id = library_id_from_path(corrected_path, self.lmap)
self.lmap[library_id] = newloc
self.library_name_map[library_id] = basename(corrected_path)
self.original_path_map[newloc] = original_path
self.loaded_dbs[library_id] = db
db.new_api.server_library_id = library_id
if self.listening_for_db_events:
db.new_api.add_listener(gui_on_db_event)
if olddb is not None and samefile(path_for_db(olddb), path_for_db(db)):
# This happens after a restore database, for example
olddb.close(), olddb.break_cycles()
self._prune_loaded_dbs()
def is_gui_library(self, library_path):
with self:
if self.gui_library_id and self.gui_library_id in self.lmap:
return samefile(library_path, self.lmap[self.gui_library_id])
return False
def _prune_loaded_dbs(self):
now = monotonic()
for library_id in tuple(self.loaded_dbs):
if library_id != self.gui_library_id and now - self.last_used_times[
library_id] > EXPIRED_AGE:
db = self.loaded_dbs.pop(library_id, None)
if db is not None:
db.close()
db.break_cycles()
def prune_loaded_dbs(self):
with self:
self._prune_loaded_dbs()
def unload_library(self, library_path):
with self:
path = canonicalize_path(library_path)
for library_id, q in iteritems(self.lmap):
if samefile(path, q):
break
else:
return
db = self.loaded_dbs.pop(library_id, None)
if db is not None:
db.close()
db.break_cycles()
def remove_library(self, path):
with self:
path = canonicalize_path(path)
for library_id, q in iteritems(self.lmap):
if samefile(path, q):
break
else:
return
self.lmap.pop(library_id, None), self.library_name_map.pop(
library_id, None), self.original_path_map.pop(path, None)
db = self.loaded_dbs.pop(library_id, None)
if db is not None:
db.close()
db.break_cycles()