%PDF- %PDF-
| Direktori : /lib/calibre/calibre/srv/ |
| Current File : //lib/calibre/calibre/srv/utils.py |
#!/usr/bin/env python3
__license__ = 'GPL v3'
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
import errno, socket, os
from email.utils import formatdate
from operator import itemgetter
from calibre import prints
from calibre.constants import iswindows
from calibre.srv.errors import HTTPNotFound
from calibre.utils.localization import get_translator
from calibre.utils.socket_inheritance import set_socket_inherit
from calibre.utils.logging import ThreadSafeLog
from calibre.utils.shared_file import share_open
from polyglot.builtins import iteritems
from polyglot import reprlib
from polyglot.http_cookie import SimpleCookie
from polyglot.builtins import as_unicode
from polyglot.urllib import parse_qs, quote as urlquote
from polyglot.binary import as_hex_unicode as encode_name, from_hex_unicode as decode_name
HTTP1 = 'HTTP/1.0'
HTTP11 = 'HTTP/1.1'
DESIRED_SEND_BUFFER_SIZE = 16 * 1024 # windows 7 uses an 8KB sndbuf
encode_name, decode_name
def http_date(timeval=None):
return str(formatdate(timeval=timeval, usegmt=True))
class MultiDict(dict): # {{{
def __setitem__(self, key, val):
vals = dict.get(self, key, [])
vals.append(val)
dict.__setitem__(self, key, vals)
def __getitem__(self, key):
return dict.__getitem__(self, key)[-1]
@staticmethod
def create_from_query_string(qs):
ans = MultiDict()
qs = as_unicode(qs)
for k, v in iteritems(parse_qs(qs, keep_blank_values=True)):
dict.__setitem__(ans, as_unicode(k), [as_unicode(x) for x in v])
return ans
def update_from_listdict(self, ld):
for key, values in iteritems(ld):
for val in values:
self[key] = val
def items(self, duplicates=True):
f = dict.items
for k, v in f(self):
if duplicates:
for x in v:
yield k, x
else:
yield k, v[-1]
iteritems = items
def values(self, duplicates=True):
f = dict.values
for v in f(self):
if duplicates:
yield from v
else:
yield v[-1]
itervalues = values
def set(self, key, val, replace_all=False):
if replace_all:
dict.__setitem__(self, key, [val])
else:
self[key] = val
def get(self, key, default=None, all=False):
if all:
try:
return dict.__getitem__(self, key)
except KeyError:
return []
try:
return self.__getitem__(key)
except KeyError:
return default
def pop(self, key, default=None, all=False):
ans = dict.pop(self, key, default)
if ans is default:
return [] if all else default
return ans if all else ans[-1]
def __repr__(self):
return '{' + ', '.join(f'{reprlib.repr(k)}: {reprlib.repr(v)}' for k, v in iteritems(self)) + '}'
__str__ = __unicode__ = __repr__
def pretty(self, leading_whitespace=''):
return leading_whitespace + ('\n' + leading_whitespace).join(
f'{k}: {(repr(v) if isinstance(v, bytes) else v)}' for k, v in sorted(self.items(), key=itemgetter(0)))
# }}}
def error_codes(*errnames):
''' Return error numbers for error names, ignoring non-existent names '''
ans = {getattr(errno, x, None) for x in errnames}
ans.discard(None)
return ans
socket_errors_eintr = error_codes("EINTR", "WSAEINTR")
socket_errors_socket_closed = error_codes( # errors indicating a disconnected connection
"EPIPE",
"EBADF", "WSAEBADF",
"ENOTSOCK", "WSAENOTSOCK",
"ENOTCONN", "WSAENOTCONN",
"ESHUTDOWN", "WSAESHUTDOWN",
"ETIMEDOUT", "WSAETIMEDOUT",
"ECONNREFUSED", "WSAECONNREFUSED",
"ECONNRESET", "WSAECONNRESET",
"ECONNABORTED", "WSAECONNABORTED",
"ENETRESET", "WSAENETRESET",
"EHOSTDOWN", "EHOSTUNREACH",
)
socket_errors_nonblocking = error_codes(
'EAGAIN', 'EWOULDBLOCK', 'WSAEWOULDBLOCK')
def start_cork(sock):
if hasattr(socket, 'TCP_CORK'):
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_CORK, 1)
def stop_cork(sock):
if hasattr(socket, 'TCP_CORK'):
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_CORK, 0)
def create_sock_pair():
'''Create socket pair. '''
client_sock, srv_sock = socket.socketpair()
set_socket_inherit(client_sock, False), set_socket_inherit(srv_sock, False)
return client_sock, srv_sock
def parse_http_list(header_val):
"""Parse lists as described by RFC 2068 Section 2.
In particular, parse comma-separated lists where the elements of
the list may include quoted-strings. A quoted-string could
contain a comma. A non-quoted string could have quotes in the
middle. Neither commas nor quotes count if they are escaped.
Only double-quotes count, not single-quotes.
"""
if isinstance(header_val, bytes):
slash, dquote, comma = b'\\",'
empty = b''
else:
slash, dquote, comma = '\\",'
empty = ''
part = empty
escape = quote = False
for cur in header_val:
if escape:
part += cur
escape = False
continue
if quote:
if cur == slash:
escape = True
continue
elif cur == dquote:
quote = False
part += cur
continue
if cur == comma:
yield part.strip()
part = empty
continue
if cur == dquote:
quote = True
part += cur
if part:
yield part.strip()
def parse_http_dict(header_val):
'Parse an HTTP comma separated header with items of the form a=1, b="xxx" into a dictionary'
if not header_val:
return {}
ans = {}
sep, dquote = b'="' if isinstance(header_val, bytes) else '="'
for item in parse_http_list(header_val):
k, v = item.partition(sep)[::2]
if k:
if v.startswith(dquote) and v.endswith(dquote):
v = v[1:-1]
ans[k] = v
return ans
def sort_q_values(header_val):
'Get sorted items from an HTTP header of type: a;q=0.5, b;q=0.7...'
if not header_val:
return []
def item(x):
e, r = x.partition(';')[::2]
p, v = r.partition('=')[::2]
q = 1.0
if p == 'q' and v:
try:
q = max(0.0, min(1.0, float(v.strip())))
except Exception:
pass
return e.strip(), q
return tuple(map(itemgetter(0), sorted(map(item, parse_http_list(header_val)), key=itemgetter(1), reverse=True)))
def eintr_retry_call(func, *args, **kwargs):
while True:
try:
return func(*args, **kwargs)
except OSError as e:
if getattr(e, 'errno', None) in socket_errors_eintr:
continue
raise
def get_translator_for_lang(cache, bcp_47_code):
try:
return cache[bcp_47_code]
except KeyError:
pass
cache[bcp_47_code] = ans = get_translator(bcp_47_code)
return ans
def encode_path(*components):
'Encode the path specified as a list of path components using URL encoding'
return '/' + '/'.join(urlquote(x.encode('utf-8'), '') for x in components)
class Cookie(SimpleCookie):
def _BaseCookie__set(self, key, real_value, coded_value):
return SimpleCookie._BaseCookie__set(self, key, real_value, coded_value)
def custom_fields_to_display(db):
return frozenset(db.field_metadata.ignorable_field_keys())
# Logging {{{
class ServerLog(ThreadSafeLog):
exception_traceback_level = ThreadSafeLog.WARN
class RotatingStream:
def __init__(self, filename, max_size=None, history=5):
self.filename, self.history, self.max_size = filename, history, max_size
if iswindows:
self.filename = '\\\\?\\' + os.path.abspath(self.filename)
self.set_output()
def set_output(self):
if iswindows:
self.stream = share_open(self.filename, 'a', newline='')
else:
# see https://bugs.python.org/issue27805
self.stream = open(os.open(self.filename, os.O_WRONLY|os.O_APPEND|os.O_CREAT|os.O_CLOEXEC), 'w')
try:
self.stream.tell()
except OSError:
# Happens if filename is /dev/stdout for example
self.max_size = None
def flush(self):
self.stream.flush()
def prints(self, level, *args, **kwargs):
kwargs['file'] = self.stream
prints(*args, **kwargs)
self.rollover()
def rename(self, src, dest):
try:
if iswindows:
from calibre_extensions import winutil
winutil.move_file(src, dest)
else:
os.rename(src, dest)
except OSError as e:
if e.errno != errno.ENOENT: # the source of the rename does not exist
raise
def rollover(self):
if not self.max_size or self.stream.tell() <= self.max_size:
return
self.stream.close()
for i in range(self.history - 1, 0, -1):
src, dest = '%s.%d' % (self.filename, i), '%s.%d' % (self.filename, i+1)
self.rename(src, dest)
self.rename(self.filename, '%s.%d' % (self.filename, 1))
self.set_output()
def clear(self):
if self.filename in ('/dev/stdout', '/dev/stderr'):
return
self.stream.close()
failed = {}
try:
os.remove(self.filename)
except OSError as e:
failed[self.filename] = e
import glob
for f in glob.glob(self.filename + '.*'):
try:
os.remove(f)
except OSError as e:
failed[f] = e
self.set_output()
return failed
class RotatingLog(ServerLog):
def __init__(self, filename, max_size=None, history=5):
ServerLog.__init__(self)
self.outputs = [RotatingStream(filename, max_size, history)]
def flush(self):
for o in self.outputs:
o.flush()
# }}}
class HandleInterrupt: # {{{
# On windows socket functions like accept(), recv(), send() are not
# interrupted by a Ctrl-C in the console. So to make Ctrl-C work we have to
# use this special context manager. See the echo server example at the
# bottom of srv/loop.py for how to use it.
def __init__(self, action):
if not iswindows:
return # Interrupts work fine on POSIX
self.action = action
from ctypes import WINFUNCTYPE, windll
from ctypes.wintypes import BOOL, DWORD
kernel32 = windll.LoadLibrary('kernel32')
# <http://msdn.microsoft.com/en-us/library/ms686016.aspx>
PHANDLER_ROUTINE = WINFUNCTYPE(BOOL, DWORD)
self.SetConsoleCtrlHandler = kernel32.SetConsoleCtrlHandler
self.SetConsoleCtrlHandler.argtypes = (PHANDLER_ROUTINE, BOOL)
self.SetConsoleCtrlHandler.restype = BOOL
@PHANDLER_ROUTINE
def handle(event):
if event == 0: # CTRL_C_EVENT
if self.action is not None:
self.action()
self.action = None
return 1
return 0
self.handle = handle
def __enter__(self):
if iswindows:
if self.SetConsoleCtrlHandler(self.handle, 1) == 0:
import ctypes
raise ctypes.WinError()
def __exit__(self, *args):
if iswindows:
if self.SetConsoleCtrlHandler(self.handle, 0) == 0:
import ctypes
raise ctypes.WinError()
# }}}
class Accumulator: # {{{
'Optimized replacement for BytesIO when the usage pattern is many writes followed by a single getvalue()'
def __init__(self):
self._buf = []
self.total_length = 0
def append(self, b):
self._buf.append(b)
self.total_length += len(b)
def getvalue(self):
ans = b''.join(self._buf)
self._buf = []
self.total_length = 0
return ans
# }}}
def get_db(ctx, rd, library_id):
db = ctx.get_library(rd, library_id)
if db is None:
raise HTTPNotFound('Library %r not found' % library_id)
return db
def get_library_data(ctx, rd, strict_library_id=False):
library_id = rd.query.get('library_id')
library_map, default_library = ctx.library_info(rd)
if library_id not in library_map:
if strict_library_id and library_id:
raise HTTPNotFound(f'No library with id: {library_id}')
library_id = default_library
db = get_db(ctx, rd, library_id)
return db, library_id, library_map, default_library
class Offsets:
'Calculate offsets for a paginated view'
def __init__(self, offset, delta, total):
if offset < 0:
offset = 0
if offset >= total:
raise HTTPNotFound('Invalid offset: %r'%offset)
last_allowed_index = total - 1
last_current_index = offset + delta - 1
self.slice_upper_bound = offset+delta
self.offset = offset
self.next_offset = last_current_index + 1
if self.next_offset > last_allowed_index:
self.next_offset = -1
self.previous_offset = self.offset - delta
if self.previous_offset < 0:
self.previous_offset = 0
self.last_offset = last_allowed_index - delta
if self.last_offset < 0:
self.last_offset = 0
_use_roman = None
def get_use_roman():
global _use_roman
if _use_roman is None:
from calibre.gui2 import config
_use_roman = config['use_roman_numerals_for_series_number']
return _use_roman