mirror of
https://framagit.org/medoc92/recollwebui.git
synced 2026-10-19 03:00:17 +00:00
430 lines
14 KiB
Python
430 lines
14 KiB
Python
# Copyright (C) 2016-2022 J.F.Dockes
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the
|
|
# Free Software Foundation, Inc.,
|
|
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
|
|
from __future__ import print_function
|
|
|
|
import collections
|
|
import locale
|
|
import re
|
|
import os
|
|
import sys
|
|
import base64
|
|
import platform
|
|
import shlex
|
|
|
|
|
|
def _debug(s):
|
|
print("%s" % s, file=sys.stderr)
|
|
|
|
|
|
class ConfSimple(object):
|
|
"""A ConfSimple class reads a recoll configuration file, which is
|
|
a typical ini file (see the Recoll manual). It's a dictionary of
|
|
dictionaries which lets you retrieve named values from the top
|
|
level or a subsection"""
|
|
|
|
def __init__(self, confname, tildexp=False, readonly=True, casesensitive=False):
|
|
self.casesens = casesensitive
|
|
if not hasattr(self, "submapcasesens"):
|
|
self.submapcasesens = self.casesens
|
|
self.submaps = self._makedict(True)
|
|
self.submaps[b""] = self._makedict()
|
|
self.subkeys_unsorted = []
|
|
self.dotildexpand = tildexp
|
|
self.readonly = readonly
|
|
self.confname = confname
|
|
|
|
try:
|
|
f = open(confname, "rb")
|
|
except Exception as exc:
|
|
# _debug("Open Exception: %s" % exc)
|
|
# File does not exist -> empty config, not an error.
|
|
return
|
|
|
|
self._parseinput(f)
|
|
f.close()
|
|
|
|
def _makedict(self, issubmaps = False):
|
|
casesens = self.casesens
|
|
if issubmaps:
|
|
casesens = self.submapcasesens
|
|
if casesens:
|
|
return dict()
|
|
else:
|
|
return CaseInsensitiveDict()
|
|
|
|
def _parseinput(self, f):
|
|
appending = False
|
|
line = b""
|
|
submapkey = b""
|
|
for cline in f:
|
|
cline = cline.rstrip(b"\r\n")
|
|
if appending:
|
|
line = line + cline
|
|
else:
|
|
line = cline
|
|
line = line.strip()
|
|
if not line or line.startswith(b"#"):
|
|
continue
|
|
|
|
if line.endswith(b"\\"):
|
|
line = line[:-1]
|
|
appending = True
|
|
continue
|
|
|
|
appending = False
|
|
# _debug(line)
|
|
if line.startswith(b"["):
|
|
line = line.strip(b"[]")
|
|
if self.dotildexpand:
|
|
submapkey = os.path.expanduser(line)
|
|
if type(submapkey) == type(""):
|
|
submapkey = submapkey.encode("utf-8")
|
|
else:
|
|
submapkey = line
|
|
# _debug("Submapkey: [%s]" % submapkey)
|
|
self.subkeys_unsorted.append(submapkey)
|
|
continue
|
|
|
|
nm, sep, value = line.partition(b"=")
|
|
if not sep:
|
|
# No equal sign in line -> considered comment
|
|
continue
|
|
|
|
nm = nm.strip()
|
|
value = value.strip()
|
|
# _debug("sk [%s] nm: [%s] value: [%s]" % (submapkey, nm, value))
|
|
if not submapkey in self.submaps:
|
|
self.submaps[submapkey] = self._makedict()
|
|
self.submaps[submapkey][nm] = value
|
|
|
|
def getSubKeys_unsorted(self):
|
|
return [k.decode("utf-8") for k in self.subkeys_unsorted]
|
|
|
|
def getbin(self, nm, sk=b""):
|
|
"""Returns None if not found, empty string if found empty"""
|
|
if type(nm) != type(b"") or type(sk) != type(b""):
|
|
raise TypeError("getbin: parameters must be binary not unicode")
|
|
# _debug("ConfSimple::getbin nm [%s] sk [%s]" % (nm, sk))
|
|
if not sk in self.submaps:
|
|
return None
|
|
if not nm in self.submaps[sk]:
|
|
return None
|
|
return self.submaps[sk][nm]
|
|
|
|
def get(self, nm, sk=b"", dflt=None):
|
|
dodecode = False
|
|
if type(nm) == type(""):
|
|
dodecode = True
|
|
nm = nm.encode("utf-8")
|
|
if type(sk) == type(""):
|
|
sk = sk.encode("utf-8")
|
|
# v = ConfSimple.getbin(self, nm, sk)
|
|
v = self.getbin(nm, sk)
|
|
if v is not None and dodecode:
|
|
v = v.decode("utf-8")
|
|
if v is None:
|
|
return dflt
|
|
return v
|
|
|
|
def getNamesbin(self, sk=b""):
|
|
if not sk in self.submaps:
|
|
return None
|
|
return list(self.submaps[sk].keys())
|
|
|
|
def getNames(self, sk=""):
|
|
dodecode = False
|
|
if type(sk) == type(""):
|
|
dodecode = True
|
|
sk = sk.encode("utf-8")
|
|
if not sk in self.submaps:
|
|
return None
|
|
names = self.getNamesbin(sk)
|
|
if names and dodecode:
|
|
names = [nm.decode("utf-8") for nm in names]
|
|
return names
|
|
|
|
def _rewrite(self):
|
|
if self.readonly:
|
|
raise Exception("ConfSimple is readonly")
|
|
|
|
tname = self.confname + "-"
|
|
f = open(tname, "wb")
|
|
# First output null subkey submap
|
|
if b"" in self.submaps:
|
|
for nm, value in self.submaps[b""].items():
|
|
f.write(nm + b"=" + value + b"\n")
|
|
for sk, mp in self.submaps.items():
|
|
if sk == b"":
|
|
continue
|
|
f.write(b"[" + sk + b"]\n")
|
|
for nm, value in mp.items():
|
|
f.write(nm + b"=" + value + b"\n")
|
|
f.close()
|
|
try:
|
|
# os.replace works on Windows even if dst exists, but py3 only
|
|
os.replace(tname, self.confname)
|
|
except:
|
|
try:
|
|
os.rename(tname, self.confname)
|
|
except:
|
|
import shutil
|
|
|
|
shutil.move(tname, self.confname)
|
|
|
|
def setbin(self, nm, value, sk=b""):
|
|
if self.readonly:
|
|
raise Exception("ConfSimple is readonly")
|
|
if sk not in self.submaps:
|
|
self.submaps[sk] = self._makedict()
|
|
self.submaps[sk][nm] = value
|
|
self._rewrite()
|
|
return True
|
|
|
|
def set(self, nm, value, sk=b""):
|
|
if self.readonly:
|
|
raise Exception("ConfSimple is readonly")
|
|
if type(nm) == type(""):
|
|
nm = nm.encode("utf-8")
|
|
if type(value) == type(""):
|
|
value = value.encode("utf-8")
|
|
if type(sk) == type(""):
|
|
sk = sk.encode("utf-8")
|
|
return self.setbin(nm, value, sk)
|
|
|
|
|
|
class ConfTree(ConfSimple):
|
|
"""A ConfTree adds path-hierarchical interpretation of the section keys,
|
|
which should be '/'-separated values. When a value is requested for a
|
|
given path, it will also be searched in the sections corresponding to
|
|
the ancestors. E.g. get(name, '/a/b') will also look in sections '/a' and
|
|
'/' or '' (the last 2 are equivalent)"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
if platform.system() in ("Windows", "Darwin"):
|
|
self.submapcasesens = False
|
|
else:
|
|
self.submapcasesens = True
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def getbin(self, nm, sk=b""):
|
|
if type(nm) != type(b"") or type(sk) != type(b""):
|
|
raise TypeError("getbin: parameters must be binary not unicode")
|
|
# _debug("ConfTree::getbin: nm [%s] sk [%s]" % (nm, sk))
|
|
|
|
# Note the test for root. There does not seem to be a direct
|
|
# way to do this in os.path
|
|
if not sk:
|
|
return ConfSimple.getbin(self, nm, sk)
|
|
|
|
# Try all sk ancestors as submaps (/a/b/c-> /a/b/c, /a/b, /a, b'')
|
|
while True:
|
|
val = ConfSimple.getbin(self, nm, sk)
|
|
if val is not None:
|
|
return val
|
|
val = ConfSimple.getbin(self, nm, sk + b"/")
|
|
if val is not None:
|
|
return val
|
|
nsk = os.path.dirname(sk)
|
|
if nsk == sk:
|
|
# sk was already root, we're done.
|
|
break
|
|
sk = nsk
|
|
|
|
return ConfSimple.getbin(self, nm)
|
|
|
|
|
|
class ConfStack(object):
|
|
"""A ConfStack manages the superposition of a list of Configuration
|
|
objects. Values are looked for in each object from the list until found.
|
|
This typically provides for defaults overridden by sparse values in the
|
|
topmost file."""
|
|
|
|
def __init__(self, nm, dirs, tp="simple", casesensitive=True):
|
|
self.casesens = casesensitive
|
|
fnames = []
|
|
for dir in dirs:
|
|
fnm = os.path.join(dir, nm)
|
|
fnames.append(fnm)
|
|
self._construct(tp, fnames)
|
|
|
|
def _construct(self, tp, fnames):
|
|
self.confs = []
|
|
for fname in fnames:
|
|
if tp.lower() == "simple":
|
|
conf = ConfSimple(fname, casesensitive = self.casesens)
|
|
else:
|
|
conf = ConfTree(fname, casesensitive = self.casesens)
|
|
self.confs.append(conf)
|
|
|
|
# Accepts / returns binary strings (non-unicode)
|
|
def getbin(self, nm, sk=b""):
|
|
if type(nm) != type(b"") or type(sk) != type(b""):
|
|
raise TypeError("getbin: parameters must be binary not unicode")
|
|
for conf in self.confs:
|
|
value = conf.getbin(nm, sk)
|
|
if value is not None:
|
|
return value
|
|
return None
|
|
|
|
def get(self, nm, sk=b""):
|
|
dodecode = False
|
|
if type(nm) == type(""):
|
|
dodecode = True
|
|
nm = nm.encode("utf-8")
|
|
if type(sk) == type(""):
|
|
sk = sk.encode("utf-8")
|
|
# v = ConfSimple.getbin(self, nm, sk)
|
|
v = self.getbin(nm, sk)
|
|
if v is not None and dodecode:
|
|
v = v.decode("utf-8")
|
|
return v
|
|
|
|
|
|
# Split string of strings, with possible quoting and escaping.
|
|
# The default is do do Recoll stringToStrings emulation: whitespace
|
|
# separated, and doublequotes only (C-style). E.G.:
|
|
# word1 word2 "compound \\"quoted\\" string" ->
|
|
# ['word1', 'word2', 'compound "quoted string']
|
|
#
|
|
# This is not the shlex default and can be changed by setting the
|
|
# parameters
|
|
def stringToStrings(s, quotes='"', escape="\\", escapedquotes='"', whitespace=None):
|
|
lex = shlex.shlex(s, posix=True)
|
|
lex.whitespace_split = True
|
|
if quotes is not None:
|
|
lex.quotes = quotes
|
|
if escape is not None:
|
|
lex.escape = escape
|
|
if escapedquotes is not None:
|
|
lex.escapedquotes = escapedquotes
|
|
if whitespace is not None:
|
|
lex.whitespace = whitespace
|
|
lex.commenters = ""
|
|
l = []
|
|
while True:
|
|
tok = lex.get_token()
|
|
if not tok:
|
|
break
|
|
l.append(tok)
|
|
return l
|
|
|
|
|
|
def stringsToString(vs):
|
|
out = []
|
|
for s in vs:
|
|
if (
|
|
s.find(" ") != -1
|
|
or s.find("\t") != -1
|
|
or s.find("\\") != -1
|
|
or s.find('"') != -1
|
|
):
|
|
out.append('"' + s.replace("\\", "\\\\").replace('"', '\\"') + '"')
|
|
else:
|
|
out.append(s)
|
|
return " ".join(out)
|
|
|
|
|
|
def valToBool(s):
|
|
if not s:
|
|
return False
|
|
try:
|
|
val = int(s)
|
|
return val != 0
|
|
except:
|
|
pass
|
|
if type(s) == type(b""):
|
|
s = s.decode("UTF-8")
|
|
return s[0] in "tTyY"
|
|
|
|
|
|
# CaseInsensitiveDict was copied from:
|
|
# Requests - https://github.com/psf/requests
|
|
# Copyright 2019 Kenneth Reitz
|
|
# Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses
|
|
|
|
from collections.abc import MutableMapping
|
|
|
|
class CaseInsensitiveDict(MutableMapping):
|
|
"""
|
|
A case-insensitive ``dict``-like object.
|
|
|
|
Implements all methods and operations of
|
|
``collections.MutableMapping`` as well as dict's ``copy``. Also
|
|
provides ``lower_items``.
|
|
|
|
All keys are expected to be strings. The structure remembers the
|
|
case of the last key to be set, and ``iter(instance)``,
|
|
``keys()``, ``items()``, ``iterkeys()``, and ``iteritems()``
|
|
will contain case-sensitive keys. However, querying and contains
|
|
testing is case insensitive:
|
|
|
|
cid = CaseInsensitiveDict()
|
|
cid['Accept'] = 'application/json'
|
|
cid['aCCEPT'] == 'application/json' # True
|
|
list(cid) == ['Accept'] # True
|
|
|
|
For example, ``headers['content-encoding']`` will return the
|
|
value of a ``'Content-Encoding'`` response header, regardless
|
|
of how the header name was originally stored.
|
|
|
|
If the constructor, ``.update``, or equality comparison
|
|
operations are given keys that have equal ``.lower()``s, the
|
|
behavior is undefined.
|
|
|
|
"""
|
|
|
|
def __init__(self, data=None, **kwargs):
|
|
self._store = dict()
|
|
if data is None:
|
|
data = {}
|
|
self.update(data, **kwargs)
|
|
|
|
def __setitem__(self, key, value):
|
|
# Use the lowercased key for lookups, but store the actual
|
|
# key alongside the value.
|
|
self._store[key.lower()] = (key, value)
|
|
|
|
def __getitem__(self, key):
|
|
return self._store[key.lower()][1]
|
|
|
|
def __delitem__(self, key):
|
|
del self._store[key.lower()]
|
|
|
|
def __iter__(self):
|
|
return (casedkey for casedkey, mappedvalue in self._store.values())
|
|
|
|
def __len__(self):
|
|
return len(self._store)
|
|
|
|
def lower_items(self):
|
|
"""Like iteritems(), but with all lowercase keys."""
|
|
return ((lowerkey, keyval[1]) for (lowerkey, keyval) in self._store.items())
|
|
|
|
def __eq__(self, other):
|
|
if isinstance(other, collections.Mapping):
|
|
other = CaseInsensitiveDict(other)
|
|
else:
|
|
return NotImplemented
|
|
# Compare insensitively
|
|
return dict(self.lower_items()) == dict(other.lower_items())
|
|
|
|
# Copy is required
|
|
def copy(self):
|
|
return CaseInsensitiveDict(self._store.values())
|
|
|
|
def __repr__(self):
|
|
return "%s(%r)" % (self.__class__.__name__, dict(self.items()))
|