mirror of
https://framagit.org/medoc92/recollwebui.git
synced 2026-10-19 19:17:27 +00:00
687 lines
24 KiB
Python
Executable file
687 lines
24 KiB
Python
Executable file
#{{{ imports
|
|
import os
|
|
import bottle
|
|
import time
|
|
import sys
|
|
import datetime
|
|
import glob
|
|
import hashlib
|
|
import csv
|
|
import io
|
|
import string
|
|
import shlex
|
|
from urllib.parse import quote as urlquote
|
|
from recoll import recoll, rclextract, rclconfig
|
|
|
|
def msg(s):
|
|
print("%s" % s, file=sys.stderr)
|
|
|
|
# use ujson if avalaible (faster than built in json)
|
|
try:
|
|
import ujson as json
|
|
except ImportError:
|
|
import json
|
|
#msg("ujson module not found, using (slower) built-in json module instead")
|
|
|
|
g_fscharset=sys.getfilesystemencoding()
|
|
|
|
g_tmpdir = os.getenv("RECOLL_TMPDIR")
|
|
if not g_tmpdir:
|
|
g_tmpdir = os.getenv("TMPDIR")
|
|
|
|
#}}}
|
|
#{{{ settings
|
|
# settings defaults
|
|
DEFAULTS = {
|
|
'context': 30,
|
|
'stem': 1,
|
|
'timefmt': '%c',
|
|
'dirdepth': 2,
|
|
'maxchars': 500,
|
|
'maxresults': 0,
|
|
'perpage': 25,
|
|
'csvfields': 'filename title author size time mtype url',
|
|
'title_link': 'download',
|
|
'collapsedups': 0,
|
|
'synonyms': "",
|
|
'mounts': {
|
|
# Override default links for directories.
|
|
# Useful for rewriting links to access the files on a server.
|
|
# If not specified, the url will be dir path prefixed with 'file://'.
|
|
#
|
|
# Path in recoll.conf : Remote url
|
|
# '/media/data/docs' : 'https://media.server.com/docs',
|
|
# '/var/www/data' : 'file:///192.168.1.2/data',
|
|
},
|
|
"noresultlinks": 0,
|
|
"logquery": 0,
|
|
"shortenpaths": 1,
|
|
"permlinks": 0,
|
|
"res_permlink": 0,
|
|
}
|
|
|
|
# sort fields/labels
|
|
SORTS = [
|
|
("relevancyrating", "Relevancy"),
|
|
("mtime", "Date",),
|
|
("url", "Path"),
|
|
("filename", "Filename"),
|
|
("fbytes", "Size"),
|
|
("author", "Author"),
|
|
]
|
|
|
|
# doc fields
|
|
FIELDS = [
|
|
# exposed by python api
|
|
'abstract',
|
|
'author',
|
|
'collapsecount',
|
|
'dbytes',
|
|
'dmtime',
|
|
'fbytes',
|
|
'filename',
|
|
'fmtime',
|
|
'ipath',
|
|
'keywords',
|
|
'mtime',
|
|
'mtype',
|
|
'origcharset',
|
|
'relevancyrating',
|
|
'sig',
|
|
'size',
|
|
'title',
|
|
'url',
|
|
# calculated
|
|
'label',
|
|
'snippet',
|
|
'time',
|
|
]
|
|
#}}}
|
|
#{{{ functions
|
|
#{{{ helpers
|
|
def select(ls, invalid=[None]):
|
|
for value in ls:
|
|
if value not in invalid:
|
|
return value
|
|
|
|
def timestr(secs, fmt):
|
|
# Just in case: we had a bug at some point inserting commas in the dmtime field.
|
|
secs = secs.strip(',')
|
|
if secs == '' or secs is None:
|
|
secs = '0'
|
|
t = time.localtime(int(secs))
|
|
return time.strftime(fmt, t)
|
|
|
|
# Compute a file name used for an attachment 'filename' attribute. We don't know what the remote
|
|
# system would accept, so play it safe
|
|
_g_valid_filename_chars = "_-%s%s" % (string.ascii_letters, string.digits)
|
|
def normalise_filename(fn):
|
|
out = ''.join(c if c in _g_valid_filename_chars else "_" for c in fn)
|
|
return out
|
|
|
|
# We may need to get the "topdirs" value from other directories than our main one.
|
|
def get_topdirs(confdir):
|
|
rclconf = rclconfig.RclConfig(confdir)
|
|
return rclconf.getConfParam('topdirs')
|
|
|
|
# Environment fetch for the cases where we don't care if unset or null
|
|
def safe_envget(varnm):
|
|
try:
|
|
return os.environ[varnm]
|
|
except Exception as ex:
|
|
return None
|
|
|
|
# Get the database directory from recoll.conf, defaults to confdir/xapiandb. Note
|
|
# that this is available as getDbDir() from recoll 1.27 (2020)
|
|
def get_dbdir(confdir):
|
|
confdir = os.path.expanduser(confdir)
|
|
rclconf = rclconfig.RclConfig(confdir)
|
|
try:
|
|
dbdir = rclconf.getDbDir()
|
|
except:
|
|
dbdir = rclconf.getConfParam('dbdir')
|
|
if not dbdir:
|
|
dbdir = 'xapiandb'
|
|
if not os.path.isabs(dbdir):
|
|
cachedir = rclconf.getConfParam('cachedir')
|
|
if not cachedir:
|
|
cachedir = confdir
|
|
dbdir = os.path.join(cachedir, dbdir)
|
|
# recoll API expects bytes, not strings
|
|
return os.path.normpath(dbdir).encode(g_fscharset)
|
|
|
|
#}}}
|
|
def commonpathprefix(paths):
|
|
if len(paths) == 0:
|
|
return ""
|
|
common = [p for p in paths[0].split("/") if len(p)]
|
|
for path in paths[1:]:
|
|
# Keep only the paths elements at the start which are common with the currently
|
|
# calculated common part
|
|
np = [p for p in path.split("/") if len(p)]
|
|
nc = []
|
|
for i in range(len(np)):
|
|
if i >= len(common) or np[i] != common[i]:
|
|
break
|
|
nc.append(np[i])
|
|
if len(nc) == 0:
|
|
return ""
|
|
common = nc
|
|
return "/" + "/".join(common) + "/"
|
|
|
|
#{{{ get_config
|
|
def get_config():
|
|
# Arrange for apache wsgi SetEnv values to be reflected in the os environment.
|
|
# This allows people to use either method
|
|
for k in ("RECOLL_CONFDIR", "RECOLL_EXTRACONFDIRS"):
|
|
if k in bottle.request.environ:
|
|
os.environ[k] = bottle.request.environ[k]
|
|
config = {}
|
|
envdir = safe_envget('RECOLL_CONFDIR')
|
|
# get useful things from recoll.conf
|
|
rclconf = rclconfig.RclConfig(envdir)
|
|
config['confdir'] = rclconf.getConfDir()
|
|
topdirs = [os.path.expanduser(d) for d in shlex.split(rclconf.getConfParam('topdirs'))]
|
|
config['dirs'] = dict.fromkeys(topdirs, config['confdir'])
|
|
config['commonprefix'] = commonpathprefix(topdirs)
|
|
# add topdirs from extra config dirs
|
|
extraconfdirs = safe_envget('RECOLL_EXTRACONFDIRS')
|
|
if extraconfdirs:
|
|
config['extraconfdirs'] = shlex.split(extraconfdirs)
|
|
for e in config['extraconfdirs']:
|
|
config['dirs'].update(dict.fromkeys([os.path.expanduser(d) for d in
|
|
shlex.split(get_topdirs(e))],e))
|
|
config['extradbs'] = list(map(get_dbdir, config['extraconfdirs']))
|
|
else:
|
|
config['extraconfdirs'] = None
|
|
config['extradbs'] = None
|
|
config['stemlang'] = rclconf.getConfParam('indexstemminglanguages')
|
|
|
|
# Possibly adjust user config defaults with data from recoll.conf. Some defaults which are
|
|
# generally suitable like dirdepth=2 can be unworkable on big data sets (causing init errors so
|
|
# that they can't even be adjusted from the UI). The 2nd parameter asks for an int conversion
|
|
fetches = [("context", 1), ("stem", 1),("timefmt", 0),("dirdepth", 1),("maxchars", 1),
|
|
("maxresults", 1), ("perpage", 1), ("csvfields", 0), ("title_link", 0),
|
|
("collapsedups", 1), ("synonyms", 0), ("noresultlinks", 1), ("logquery", 1),
|
|
("shortenpaths", 1), ("permlinks", 1), ("res_permlink", 1), ("queryfrag", 0),
|
|
]
|
|
for k, isint in fetches:
|
|
value = rclconf.getConfParam("webui_" + k)
|
|
if value is not None:
|
|
DEFAULTS[k] = int(value) if isint else value
|
|
# get config from cookies or defaults
|
|
for k, v in DEFAULTS.items():
|
|
value = select([bottle.request.get_cookie(k), v], invalid=["None", None])
|
|
config[k] = type(v)(value)
|
|
# Fix csvfields: get rid of invalid ones to avoid needing tests in the dump function
|
|
cf = config['csvfields'].split()
|
|
ncf = [f for f in cf if f in FIELDS]
|
|
config['csvfields'] = ' '.join(ncf)
|
|
config['fields'] = ' '.join(FIELDS)
|
|
# get mountpoints
|
|
config['mounts'] = {}
|
|
for d in config['dirs']:
|
|
name = 'mount_%s' % urlquote(d,'')
|
|
config['mounts'][d] = select([bottle.request.get_cookie(name),
|
|
rclconf.getConfParam(f"webui_mount_{d}"),
|
|
f"file://{d}"],
|
|
[None, ''])
|
|
|
|
# Parameters set by the admin in the recoll configuration
|
|
# file. These override anything else, so read them last
|
|
val = rclconf.getConfParam('webui_nojsoncsv')
|
|
val = 0 if val is None else int(val)
|
|
config['rclc_nojsoncsv'] = val
|
|
|
|
val = rclconf.getConfParam('webui_maxperpage')
|
|
val = 0 if val is None else int(val)
|
|
if val:
|
|
if config['perpage'] == 0 or config['perpage'] > val:
|
|
config['perpage'] = val
|
|
|
|
val = rclconf.getConfParam('webui_nosettings')
|
|
val = 0 if val is None else int(val)
|
|
config['rclc_nosettings'] = val
|
|
|
|
val = rclconf.getConfParam('webui_pdfposition')
|
|
val = 0 if val is None else int(val)
|
|
config['rclc_pdfposition'] = val
|
|
|
|
val = str(rclconf.getConfParam('webui_defaultsort'))
|
|
config['defsortidx'] = 0
|
|
for i in range(len(SORTS)):
|
|
if SORTS[i][0] == val or SORTS[i][1] == val:
|
|
config['defsortidx'] = i
|
|
break
|
|
return config
|
|
#}}}
|
|
#{{{ get_dirs
|
|
def get_dirs(tops, depth):
|
|
v = []
|
|
for top in tops:
|
|
# We do the conversion to bytes here, because Python versions
|
|
# before 3.7 won't do the right thing if the locale is C,
|
|
# which would be the case with a default apache install
|
|
top = top.encode('utf-8', 'surrogateescape')
|
|
dirs = [top]
|
|
for d in range(1, depth+1):
|
|
dirs = dirs + glob.glob(top + b'/*' * d)
|
|
dirs = filter(lambda f: os.path.isdir(f), dirs)
|
|
top_path = top.rsplit(b'/', 1)[0]
|
|
dirs = [w.replace(top_path+b'/', b'', 1) for w in dirs]
|
|
v = v + dirs
|
|
for i in range(len(v)):
|
|
v[i] = v[i].decode('utf-8', 'surrogateescape')
|
|
return ['<all>'] + v
|
|
#}}}
|
|
#{{{ get_query
|
|
def get_query(config=None):
|
|
defsortidx = config['defsortidx'] if config and 'defsortidx' in config else 0
|
|
query = {
|
|
'query': select([bottle.request.query.query, '']),
|
|
'before': select([bottle.request.query.before, '']),
|
|
'after': select([bottle.request.query.after, '']),
|
|
'dir': select([bottle.request.query.dir, '', '<all>'], [None, '']),
|
|
'sort': select([bottle.request.query.sort, SORTS[defsortidx][0]], [None, '']),
|
|
'ascending': int(select([bottle.request.query.ascending, 0], [None, ''])),
|
|
'page': int(select([bottle.request.query.page, 0], [None, ''])),
|
|
'highlight': int(select([bottle.request.query.highlight, 1], [None, ''])),
|
|
'snippets': int(select([bottle.request.query.snippets, 1], [None, ''])),
|
|
}
|
|
if bottle.request.query.rcludi:
|
|
query['rcludi'] = bottle.request.query.rcludi
|
|
#msg("query['query'] : %s" % query['query'])
|
|
return query
|
|
#}}}
|
|
#{{{ query_to_recoll_string
|
|
def query_to_recoll_string(q):
|
|
qs = q['query']
|
|
if len(q['after']) > 0 or len(q['before']) > 0:
|
|
qs += " date:%s/%s" % (q['after'], q['before'])
|
|
qdir = q['dir']
|
|
if qdir != '<all>':
|
|
qs += " dir:\"%s\" " % qdir
|
|
return qs
|
|
#}}}
|
|
#{{{ recoll_initsearch
|
|
def recoll_initsearch(q):
|
|
config = get_config()
|
|
confdir = config['confdir']
|
|
dbs = []
|
|
""" The reason for this somewhat elaborate scheme is to keep the
|
|
set size as small as possible by searching only those databases
|
|
with matching topdirs """
|
|
if q['dir'] == '<all>':
|
|
if config['extraconfdirs']:
|
|
dbs.extend(map(get_dbdir,config['extraconfdirs']))
|
|
else:
|
|
confdirs = []
|
|
for d,conf in config['dirs'].items():
|
|
tdbasename = os.path.basename(d)
|
|
if os.path.commonprefix([tdbasename, q['dir']]) == tdbasename:
|
|
confdirs.append(conf)
|
|
if len(confdirs) == 0:
|
|
# should not happen, using non-existing q['dir']?
|
|
bottle.abort(400, 'no matching database for search directory ' + q['dir'])
|
|
elif len(confdirs) == 1:
|
|
# only one config (most common situation)
|
|
confdir = confdirs[0]
|
|
else:
|
|
# more than one config with matching topdir, use 'm all
|
|
confdir = confdirs[0]
|
|
dbs.extend(map(get_dbdir, confdirs[1:]))
|
|
|
|
if config['extradbs']:
|
|
dbs.extend(config['extradbs'])
|
|
|
|
db = recoll.connect(confdir, extra_dbs=dbs)
|
|
|
|
# Compare to "None" because of the conv. to str done while setting from cookies
|
|
if config["synonyms"] and config["synonyms"] != "None":
|
|
try:
|
|
db.setSynonymsFile(config["synonyms"])
|
|
except:
|
|
# Only supported from recoll 1.40.3, just ignore the error for now
|
|
msg(f"Setting synonyms to [{config['synonyms']}] failed")
|
|
pass
|
|
|
|
db.setAbstractParams(config['maxchars'], config['context'])
|
|
query = db.query()
|
|
query.sortby(q['sort'], q['ascending'])
|
|
try:
|
|
qs = query_to_recoll_string(q)
|
|
if "queryfrag" in config and config["queryfrag"]:
|
|
qs += " " + config["queryfrag"]
|
|
if "logquery" in config and config["logquery"]:
|
|
msg(f"Query: {qs}")
|
|
query.execute(qs, config['stem'], config['stemlang'],
|
|
collapseduplicates=config['collapsedups'])
|
|
except Exception as ex:
|
|
msg("Query execute failed: %s" % ex)
|
|
pass
|
|
return query, db
|
|
#}}}
|
|
#{{{ HlMeths
|
|
class HlMeths:
|
|
def startMatch(self, idx):
|
|
return '<span class="search-result-highlight">'
|
|
def endMatch(self):
|
|
return '</span>'
|
|
#}}}
|
|
#{{{ recoll_search
|
|
def recoll_search(q):
|
|
config = get_config()
|
|
tstart = datetime.datetime.now()
|
|
results = []
|
|
query,_ = recoll_initsearch(q)
|
|
nres = query.rowcount
|
|
if "rcludi" in q and q["rcludi"]:
|
|
rcludi = q["rcludi"]
|
|
nres = 1
|
|
q['page'] = 1
|
|
else:
|
|
rcludi = None
|
|
if config['maxresults'] == 0:
|
|
config['maxresults'] = nres
|
|
if nres > config['maxresults']:
|
|
nres = config['maxresults']
|
|
if config['perpage'] == 0 or q['page'] == 0:
|
|
config['perpage'] = nres
|
|
q['page'] = 1
|
|
offset = (q['page'] - 1) * config['perpage']
|
|
|
|
if query.rowcount > 0:
|
|
if type(query.next) == int:
|
|
query.next = offset
|
|
else:
|
|
query.scroll(offset, mode='absolute')
|
|
|
|
if 'highlight' in q and q['highlight']:
|
|
highlighter = HlMeths()
|
|
else:
|
|
highlighter = None
|
|
|
|
udibreak = False
|
|
while len(results) < config['perpage']:
|
|
try:
|
|
doc = query.fetchone()
|
|
# Later Recoll versions return None at EOL instead of
|
|
# exception This change restores conformance to PEP 249
|
|
# Python Database API Specification
|
|
if not doc:
|
|
break
|
|
if rcludi:
|
|
if doc['rcludi'] == rcludi:
|
|
udibreak = True
|
|
else:
|
|
continue
|
|
except:
|
|
break
|
|
d = {}
|
|
for f in FIELDS:
|
|
v = getattr(doc, f)
|
|
if v is not None:
|
|
d[f] = v
|
|
else:
|
|
d[f] = ''
|
|
if doc['mtype'] == "application/pdf" and doc['url'].startswith("file://"):
|
|
try:
|
|
# Note: getfirstmatchpage is only available from recoll 1.44
|
|
pagenum, term = query.getfirstmatchpage(doc)
|
|
d['url'] += f"#page={pagenum}&search={urlquote(term)}"
|
|
except:
|
|
pass
|
|
d['label'] = select([d['title'], d['filename'], '?'], [None, ''])
|
|
d['sha'] = hashlib.sha1((d['url']+d['ipath']).encode('utf-8')).hexdigest()
|
|
d['time'] = timestr(d['mtime'], config['timefmt'])
|
|
d['rcludi'] = doc['rcludi']
|
|
if 'snippets' in q and q['snippets']:
|
|
if highlighter:
|
|
d['snippet'] = query.makedocabstract(doc, methods=highlighter)
|
|
else:
|
|
d['snippet'] = query.makedocabstract(doc)
|
|
if not d['snippet']:
|
|
try:
|
|
d['snippet'] = doc['abstract']
|
|
except:
|
|
pass
|
|
#for n,v in d.items():
|
|
# print("type(%s) is %s" % (n,type(v)))
|
|
results.append(d)
|
|
if udibreak:
|
|
break
|
|
tend = datetime.datetime.now()
|
|
return results, nres, tend - tstart
|
|
#}}}
|
|
#}}}
|
|
#{{{ routes
|
|
#{{{ static
|
|
@bottle.route('/static/:path#.+#')
|
|
def server_static(path):
|
|
return bottle.static_file(path, root='./static')
|
|
#}}}
|
|
@bottle.route('/staticdoc/:path#.+#')
|
|
def server_staticdoc(path):
|
|
if not g_tmpdir:
|
|
return ""
|
|
fn = os.path.basename(path)
|
|
if not fn.startswith("rcltmp"):
|
|
bottle.abort(401, 'Bad temporary file name')
|
|
return bottle.static_file(path, root=g_tmpdir)
|
|
#}}}
|
|
#{{{ main
|
|
@bottle.route('/')
|
|
@bottle.view('main')
|
|
def main():
|
|
config = get_config()
|
|
bottle.response.headers['Vary'] = 'Cookie'
|
|
return { 'dirs': get_dirs(config['dirs'], config['dirdepth']),
|
|
'query': get_query(config), 'sorts': SORTS, 'config': config}
|
|
#}}}
|
|
#{{{ results
|
|
@bottle.route('/results')
|
|
@bottle.view('results')
|
|
def results():
|
|
config = get_config()
|
|
query = get_query(config)
|
|
qs = query_to_recoll_string(query)
|
|
res, nres, timer = recoll_search(query)
|
|
if config['maxresults'] == 0:
|
|
config['maxresults'] = nres
|
|
if config['perpage'] == 0:
|
|
config['perpage'] = nres
|
|
bottle.response.headers['Vary'] = 'Cookie'
|
|
bottle.response.headers['No-Vary-Search'] = 'key-order'
|
|
return { 'res': res, 'time': timer, 'query': query, 'dirs':
|
|
get_dirs(config['dirs'], config['dirdepth']),
|
|
'qs': qs, 'sorts': SORTS, 'config': config,
|
|
'query_string': bottle.request.query_string, 'nres': nres,
|
|
'config': config}
|
|
#}}}
|
|
#{{{ preview
|
|
@bottle.route('/preview/<resnum:int>')
|
|
def preview(resnum):
|
|
config = get_config()
|
|
query = get_query(config)
|
|
rclq,db = recoll_initsearch(query)
|
|
if "rcludi" in query and query["rcludi"]:
|
|
# Permlinks active
|
|
# Notes: if the initial path had non-utf8 chars, they would have \xnn encoded and we should
|
|
# decode them with codecs.escape_decode(query['rcludi']. Howvever, this is not foolproof
|
|
# because the original path could have contained litteral \xnn (4 ascii chars) sequences:
|
|
# implausible, but not impossible. So for now let well enough alone.
|
|
# Also: this currently does not work with additional indexes because we'd need to pass the
|
|
# idxi, but we can't access it from Python. This will be fixed in recoll versions from
|
|
# 1.43.13, and we will have to add the idxi to the urls along with rcludi
|
|
doc = db.getDoc(query['rcludi'])
|
|
else:
|
|
if resnum > rclq.rowcount - 1:
|
|
return 'Bad result index %d' % resnum
|
|
rclq.scroll(resnum)
|
|
doc = rclq.fetchone()
|
|
xt = rclextract.Extractor(doc)
|
|
tdoc = xt.textextract(doc.ipath)
|
|
if tdoc.mimetype == 'text/html':
|
|
ishtml = 1
|
|
bottle.response.content_type = 'text/html; charset=utf-8'
|
|
else:
|
|
ishtml = 0
|
|
bottle.response.content_type = 'text/plain; charset=utf-8'
|
|
if 'highlight' in query and query['highlight']:
|
|
hl = HlMeths()
|
|
txt = rclq.highlight(tdoc.text, ishtml=ishtml, methods=hl)
|
|
pos = txt.find('<head>')
|
|
ssref = '<link rel="stylesheet" type="text/css" href="../static/style.css">'
|
|
if pos >= 0:
|
|
txt = txt[0:pos+6] + ssref + txt[pos+6:]
|
|
else:
|
|
txt = '<html><head>' + ssref + \
|
|
'<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"></head><body>'+ \
|
|
txt
|
|
bottle.response.content_type = 'text/html; charset=utf-8'
|
|
return txt
|
|
bottle.response.headers['Vary'] = 'Cookie'
|
|
bottle.response.headers['No-Vary-Search'] = 'key-order'
|
|
return tdoc.text
|
|
#}}}
|
|
#{{{ download
|
|
@bottle.route('/download/<resnum:int>')
|
|
def edit(resnum):
|
|
config = get_config()
|
|
query = get_query(config)
|
|
rclq,db = recoll_initsearch(query)
|
|
if "rcludi" in query and query["rcludi"]:
|
|
# See comment in preview
|
|
doc = db.getDoc(query['rcludi'])
|
|
else:
|
|
if resnum > rclq.rowcount - 1:
|
|
return 'Bad result index %d' % resnum
|
|
rclq.scroll(resnum)
|
|
doc = rclq.fetchone()
|
|
xt = rclextract.Extractor(doc)
|
|
path = xt.idoctofile(doc.ipath, doc.mimetype)
|
|
if "filename" in doc.keys():
|
|
filename = doc.filename
|
|
else:
|
|
filename = os.path.basename(path)
|
|
pagenum = -1
|
|
try:
|
|
# Note: getfirstmatchpage is only available from recoll 1.44
|
|
pagenum, term = rclq.getfirstmatchpage(doc)
|
|
except:
|
|
pass
|
|
if config['rclc_pdfposition'] and pagenum != -1 and doc.mimetype == "application/pdf":
|
|
# Note that the path is a temp file generated by recoll, so no decoding issues.
|
|
#
|
|
# Tried to redirect to the same path with
|
|
# an additional parameter (dodownload=1) and then after redirection, test the additional
|
|
# param and proceed with the normal processing (return a descriptor to a deleted file), but
|
|
# this does not seem to work, it appears that the browser wants a file-looking url to
|
|
# process the search term and page number ???
|
|
#
|
|
# Does not work (download ok but no search term or pagenum processed):
|
|
# query_string = bottle.request.query_string
|
|
# url=f"/download/{resnum}?{query_string}&dodownload=1" + f"#page={pagenum}&search={term}"
|
|
# So we have to use an actual file, and we can't delete it. We check
|
|
# the name in staticdoc() to mitigate possible security issues about temp files.
|
|
fn = os.path.basename(path).decode()
|
|
url = "/staticdoc/" + fn + f"#page={pagenum}&search={term}"
|
|
return "<html><head></head><body><script>" \
|
|
f"window.location.replace(\"{url}\");" + \
|
|
"</script></body></html>"
|
|
else:
|
|
bottle.response.content_type = doc.mimetype
|
|
bottle.response.headers['Content-Disposition'] = f'attachment; filename="{filename}"'
|
|
bottle.response.headers['Content-Length'] = os.stat(path).st_size
|
|
f = open(path, 'rb')
|
|
try:
|
|
os.unlink(path)
|
|
except:
|
|
pass
|
|
bottle.response.headers['Vary'] = 'Cookie'
|
|
bottle.response.headers['No-Vary-Search'] = 'key-order'
|
|
return f
|
|
|
|
|
|
#}}}
|
|
#{{{ json
|
|
@bottle.route('/json')
|
|
def get_json():
|
|
config = get_config()
|
|
query = get_query(config)
|
|
qs = query_to_recoll_string(query)
|
|
bottle.response.headers['Vary'] = 'Cookie'
|
|
bottle.response.headers['No-Vary-Search'] = 'key-order'
|
|
bottle.response.headers['Content-Type'] = 'application/json'
|
|
bottle.response.headers['Content-Disposition'] = \
|
|
'attachment; filename=recoll-%s.json' % normalise_filename(qs)
|
|
res, nres, timer = recoll_search(query)
|
|
ures = []
|
|
for d in res:
|
|
ud={}
|
|
for f,v in d.items():
|
|
ud[f] = v
|
|
ures.append(ud)
|
|
res = ures
|
|
return json.dumps({ 'query': query, 'results': res })
|
|
#}}}
|
|
#{{{ csv
|
|
@bottle.route('/csv')
|
|
def get_csv():
|
|
config = get_config()
|
|
query = get_query(config)
|
|
query['page'] = 0
|
|
query['snippets'] = 0
|
|
qs = query_to_recoll_string(query)
|
|
bottle.response.headers['Vary'] = 'Cookie'
|
|
bottle.response.headers['No-Vary-Search'] = 'key-order'
|
|
bottle.response.headers['Content-Type'] = 'text/csv'
|
|
bottle.response.headers['Content-Disposition'] = \
|
|
'attachment; filename=recoll-%s.csv' % normalise_filename(qs)
|
|
res, nres, timer = recoll_search(query)
|
|
si = io.StringIO()
|
|
cw = csv.writer(si)
|
|
fields = config['csvfields'].split()
|
|
cw.writerow(fields)
|
|
for doc in res:
|
|
row = []
|
|
for f in fields:
|
|
if f in doc:
|
|
row.append(doc[f])
|
|
else:
|
|
row.append('')
|
|
cw.writerow(row)
|
|
return si.getvalue().strip("\r\n")
|
|
#}}}
|
|
#{{{ settings/set
|
|
@bottle.route('/settings')
|
|
@bottle.view('settings')
|
|
def settings():
|
|
return get_config()
|
|
|
|
@bottle.route('/set')
|
|
def set():
|
|
config = get_config()
|
|
for k, v in DEFAULTS.items():
|
|
bottle.response.set_cookie(k, str(bottle.request.query.get(k)),
|
|
max_age=3153600000, expires=315360000)
|
|
for d in config['dirs']:
|
|
# We should not set the cookie if the value is the default (identical path). This would
|
|
# allow the server configuration setting to be used if set. This would also show a wrong
|
|
# value in the settings screen (default instead of server config value), so not too sure
|
|
# what the right thing would be here.
|
|
cookie_name = 'mount_%s' % urlquote(d, '')
|
|
bottle.response.set_cookie(cookie_name, str(bottle.request.query.get('mount_%s' % d)),
|
|
max_age=3153600000, expires=315360000)
|
|
bottle.redirect('./')
|
|
#}}}
|
|
#{{{ osd
|
|
@bottle.route('/osd.xml')
|
|
@bottle.view('osd')
|
|
def main():
|
|
#config = get_config()
|
|
url = bottle.request.urlparts
|
|
url = '%s://%s' % (url.scheme, url.netloc)
|
|
return {'url': url}
|
|
#}}}
|
|
# vim: fdm=marker:tw=80:ts=4:sw=4:sts=4:et
|