299 lines
14 KiB
Python
Executable File
299 lines
14 KiB
Python
Executable File
#!/usr/bin/env python
|
|
# -*- coding: utf-8 mode: python -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
|
|
|
|
"""Ce script est prévu pour être utilisé dans un script CGI.
|
|
Il permet de lister le contenu d'un répertoire au format XML, et de télécharger les fichiers trouvés.
|
|
"""
|
|
|
|
import os, sys, re, shutil, mimetypes
|
|
from os import path
|
|
from time import time, localtime
|
|
from types import UnicodeType, StringTypes
|
|
import xml.etree.ElementTree as ET
|
|
|
|
# _u() et datef() sont des fonctions de pyulib, recopiées ici pour ne pas devoir
|
|
# dépendre d'une librairie extérieure
|
|
def _u(u):
|
|
if type(u) not in StringTypes: u = unicode(str(u), 'utf-8', 'strict')
|
|
elif type(u) is not UnicodeType: u = unicode(u, 'utf-8', 'strict')
|
|
return u
|
|
DATEF_MAP = {'%Y': '%(y)04i', '%m': '%(m)02i', '%d': '%(d)02i', '%H': '%(H)02i', '%M': '%(M)02i', '%S': '%(S)02i'}
|
|
DATE_DATEF = '%d/%m/%Y'
|
|
TIME_DATEF = '%H:%M:%S'
|
|
def datef(format=None, t=None):
|
|
if format is None: format = DATE_DATEF
|
|
if t is None: t = time()
|
|
y, m, d, H, M, S, W, J, dst = localtime(t)
|
|
for fr, to in DATEF_MAP.items(): format = format.replace(fr, to)
|
|
return format % locals()
|
|
|
|
RE_COMMA = re.compile(r'\s*,\s*')
|
|
RE_VAR_NAME_EXPR = re.compile(r'([_a-zA-Z][-_a-zA-Z0-9]*)=(.*)')
|
|
RE_SORT_FIELD_TYPE = re.compile(r'([_a-zA-Z][-_a-zA-Z0-9]*)(?::([aAdDcCnN]+))?')
|
|
|
|
class Filter:
|
|
re_spec = None
|
|
ex_vars = None
|
|
ex_group = None
|
|
|
|
def __init__(self, re_spec):
|
|
self.re_spec = re.compile(re_spec)
|
|
self.ex_vars = []
|
|
self.ex_group = None
|
|
|
|
def add_var(self, ex_var):
|
|
mo = RE_VAR_NAME_EXPR.match(ex_var)
|
|
if mo is None: raise ValueError('Invalid VAR_EXPR: %s' % ex_var)
|
|
name, expr = mo.groups()
|
|
self.ex_vars.append((name, expr))
|
|
|
|
def set_group(self, ex_group):
|
|
self.ex_group = ex_group
|
|
|
|
def __repr__(self):
|
|
return 'Filter<%s,%s,%s>' % (self.re_spec, repr(self.ex_vars), self.ex_group)
|
|
|
|
def match_fill(self, file):
|
|
mo = self.re_spec.match(file.name)
|
|
if mo is None: return None
|
|
for name, expr in self.ex_vars:
|
|
value = mo.expand(expr)
|
|
if '%' in value: value = value % file.vars
|
|
file[name] = value
|
|
if self.ex_group is not None:
|
|
group = mo.expand(self.ex_group)
|
|
if '%' in group: group = group % file.vars
|
|
file.group = group
|
|
return True
|
|
|
|
class File:
|
|
pf = None
|
|
group = None
|
|
vars = None
|
|
|
|
def __init__(self, dir, name):
|
|
dir = path.abspath(dir)
|
|
pf = path.join(dir, name)
|
|
self.__dict__['pf'] = pf
|
|
self.__dict__['group'] = None
|
|
self.__dict__['vars'] = {}
|
|
|
|
stat = os.stat(pf)
|
|
mtime = int(stat.st_mtime)
|
|
|
|
self.dir = dir
|
|
self.name = name
|
|
self.mtime = mtime
|
|
self.date = datef(DATE_DATEF, mtime)
|
|
self.time = datef(TIME_DATEF, mtime)
|
|
|
|
def isfile(self):
|
|
return path.isfile(self.pf)
|
|
|
|
def __getitem__(self, name): return self.vars[name]
|
|
def __setitem__(self, name, value): self.vars[name] = value
|
|
def __getattr__(self, name): return self.vars[name]
|
|
def __setattr__(self, name, value):
|
|
if self.__dict__.has_key(name):
|
|
self.__dict__[name] = value
|
|
else:
|
|
self.vars[name] = value
|
|
def __repr__(self): return 'File<%s>' % (self.pf)
|
|
|
|
def find_files(basedir, filters):
|
|
files = []
|
|
for name in os.listdir(basedir):
|
|
file = File(basedir, name)
|
|
if not file.isfile(): continue
|
|
matched = False
|
|
for filter in filters:
|
|
matched = filter.match_fill(file) or matched
|
|
if not matched: continue
|
|
files.append(file)
|
|
return files
|
|
|
|
def build_sortfunc(sortby):
|
|
SORTS = []
|
|
for term in RE_COMMA.split(sortby.strip()):
|
|
mo = RE_SORT_FIELD_TYPE.match(term)
|
|
if mo is None: raise ValueError('Invalid SORT_EXPR: %s' % term)
|
|
field, type = mo.groups()
|
|
if type is None: type = ''
|
|
type = type.upper()
|
|
if 'A' in type: order = 'A'
|
|
elif 'D' in type: order = 'D'
|
|
else: order = 'A'
|
|
if 'C' in type: method = 'C'
|
|
elif 'N' in type: method = 'N'
|
|
else: method = 'C'
|
|
SORTS.append((field, method, order))
|
|
def sortfunc(a, b):
|
|
for field, method, order in SORTS:
|
|
av = getattr(a, field)
|
|
bv = getattr(b, field)
|
|
if method == 'C':
|
|
av = str(av)
|
|
bv = str(bv)
|
|
elif method == 'N':
|
|
av = int(av)
|
|
bv = int(bv)
|
|
if av < bv: outcome = -1
|
|
elif av > bv: outcome = 1
|
|
else: outcome = 0
|
|
if order == 'A': pass
|
|
elif order == 'D': outcome = -outcome
|
|
if outcome != 0: return outcome
|
|
return 0
|
|
return sortfunc
|
|
|
|
def sort_files(files, sortfunc):
|
|
files.sort(sortfunc)
|
|
return files
|
|
|
|
def build_fgroups(files):
|
|
fgroups = set()
|
|
for file in files:
|
|
if file.group is not None:
|
|
fgroups.add(file.group)
|
|
fgroups = list(fgroups)
|
|
fgroups.sort()
|
|
return fgroups
|
|
|
|
def filter_files(files, group):
|
|
if group: return filter(lambda file: file.group == group, files)
|
|
else: return filter(lambda file: not file.group, files)
|
|
|
|
def select_file(files, name):
|
|
matches = filter(lambda file: file.name == name, files)
|
|
if matches: return matches[0]
|
|
else: return None
|
|
|
|
def print_files(files, fgroups=None, select_group=None, script_name=None, query_string=None, xslt=None):
|
|
xresult = ET.Element("result")
|
|
xenv = ET.SubElement(xresult, "env")
|
|
if script_name is not None:
|
|
ET.SubElement(xenv, "script_name").text = _u(script_name)
|
|
if query_string is not None:
|
|
ET.SubElement(xenv, "query_string").text = _u(query_string)
|
|
xfgroups = ET.SubElement(xresult, "fgroups")
|
|
if fgroups is not None:
|
|
for fgroup in fgroups:
|
|
xfgroup = ET.SubElement(xfgroups, 'fgroup')
|
|
xfgroup.text = _u(fgroup)
|
|
if fgroup == select_group:
|
|
xfgroup.set('selected', 'selected')
|
|
xfiles = ET.SubElement(xresult, "files")
|
|
for file in files:
|
|
xfile = ET.SubElement(xfiles, "file")
|
|
if file.group is not None: xfile.set('group', file.group)
|
|
for name, value in file.vars.items():
|
|
ET.SubElement(xfile, name).text = _u(value)
|
|
|
|
sys.stdout.write('<?xml version="1.0" encoding="UTF-8"?>\n')
|
|
if xslt is not None:
|
|
sys.stdout.write('<?xml-stylesheet version="1.0" type="text/xsl" href="%s"?>\n' % xslt)
|
|
ET.ElementTree(xresult).write(sys.stdout, "utf-8")
|
|
|
|
def run_cgilsxml():
|
|
default_filter = Filter(r'(.*)')
|
|
def add_spec(option, opt, value, parser, *args, **kw):
|
|
if env['filter'] is not None: env['filters'].append(env['filter'])
|
|
env['filter'] = Filter(value)
|
|
def add_var(option, opt_str, value, parser, *args, **kw):
|
|
if env['filter'] is None: env['filter'] = default_filter
|
|
env['filter'].add_var(value)
|
|
def set_group(option, opt_str, value, parser, *args, **kw):
|
|
if env['filter'] is None: env['filter'] = default_filter
|
|
env['filter'].set_group(value)
|
|
|
|
from optparse import OptionParser
|
|
OP = OptionParser(usage=u"\n\t%prog [options] /path/to/dir", description=__doc__)
|
|
OP.add_option('-t', '--xslt', dest='xslt',
|
|
help=u"Ajouter le chemin vers la feuille de style XSLT dans le flux XML généré")
|
|
OP.add_option('-e', '--spec', dest='spec', action='callback', callback=add_spec, type="string",
|
|
help=u"Spécifier l'expression régulière permettant de sélectionner les fichiers à lister. L'expression régulière peut définir des groupes qui sont utilisées pour l'extraction des variables.\nIl est possible de spécifier cette option plusieurs fois.")
|
|
OP.add_option('-v', '--var', dest='var', action='callback', callback=add_var, type="string",
|
|
help=u"Définir la variable NAME à la valeur de l'expression VAR_EXPR. Dans cette expression, il est possible d'utiliser des expressions de la forme %%(var)s pour inclure des variables déjà définies, ou \\N et \\g<NAME> pour inclure respective le groupe numéro N et le groupe nommé NAME de l'expression régulière --spec.\nCette option peut être spécifiée plusieurs fois. Elle s'applique à l'expression régulière définie par la dernière option --spec")
|
|
OP.add_option('-g', '--group', dest='group', action='callback', callback=set_group, type="string",
|
|
help=u"Spécifier l'expression qui permet de construire des ensembles de fichiers sur la base des groupes définis dans l'expression régulière de l'option --spec. Dans cette expression, il est possible d'utiliser des expressions de la forme %%(var)s pour inclure des variables déjà définies, ou \\N ou \\g<NAME> pour inclure respective le groupe numéro N et le groupe nommé NAME de l'expression régulière --spec.\nCette option ne peut être spécifiée qu'une seule fois par option --spec")
|
|
OP.add_option('-s', '--sort', dest='sortby',
|
|
help=u"Spécifier le champ sur lequel trier ainsi que le type de tri à utiliser. SORT_EXPR est de la forme FIELD:TYPE où FIELD est le nom du champ et TYPE est le type de tri: A, D, C et/ou N pour (A)scendant, (D)escendant, (C)haine, (N)numérique. Si un champ est spécifié mais que le type de tri ne l'est pas, la valeur par défaut est AC. Si cette option n'est pas spécifiée, le tri par défaut est 'mtime:DN'.\nIl est possible de spécifier plusieurs champs pour le tri en les séparant par des virgules.")
|
|
OP.add_option('--cgi', dest='cgi_mode', action='store_true',
|
|
help=u"Activer le mode CGI. Ce mode est automatiquement activé si la variable d'environnement REQUEST_METHOD existe.")
|
|
OP.add_option('--cgi-path-info', dest='path_info',
|
|
help=u"Spécifier un chemin d'un fichier à télécharger. En mode CGI, cette valeur est prise dans la variable d'environnement PATH_INFO")
|
|
OP.add_option('--cgi-script-name', dest='script_name',
|
|
help=u"Spécifier la valeur de SCRIPT_NAME pour provisionner l'environnement du fichier résultat. En mode CGI, cette valeur est prise dans la variable d'environnement du même nom.")
|
|
OP.add_option('--cgi-query-string', dest='query_string',
|
|
help=u"Spécifier la valeur de QUERY_STRING pour provisionner l'environnement du fichier résultat. En mode CGI, cette valeur est prise dans la variable d'environnement du même nom.")
|
|
OP.add_option('--cgi-param-group', dest='select_group',
|
|
help=u"Sélectionner le groupe spécifié. Seuls les fichiers du groupe sont affichés. En mode CGI, cette option est automatiquement activée si QUERY_STRING contient le paramètre group=GROUP.\nNote: pour le calcul de cette valeur, l'API cgi.FieldStorage() est utilisé, ce qui fait que la valeur éventuellement fournie par l'option --cgi-query-string est ignorée.")
|
|
env = dict(filters=[], filter=None)
|
|
o, args = OP.parse_args()
|
|
filters = env['filters']
|
|
filter = env['filter']
|
|
xslt, sortby, cgi_mode = o.xslt, o.sortby, o.cgi_mode
|
|
cgi_path_info = o.path_info
|
|
cgi_script_name = o.script_name
|
|
cgi_query_string = o.query_string
|
|
cgi_select_group = o.select_group
|
|
|
|
environ = os.environ
|
|
if cgi_mode is None: cgi_mode = 'REQUEST_METHOD' in environ
|
|
if cgi_mode:
|
|
import cgi
|
|
form = cgi.FieldStorage()
|
|
if 'group' in form and cgi_select_group is None:
|
|
cgi_select_group = form.getfirst("group")
|
|
if cgi_path_info is None and 'PATH_INFO' in environ :
|
|
cgi_path_info = environ.get('PATH_INFO')
|
|
if cgi_script_name is None and 'SCRIPT_NAME' in environ :
|
|
cgi_script_name = environ.get('SCRIPT_NAME')
|
|
if cgi_query_string is None and 'QUERY_STRING' in environ :
|
|
cgi_query_string = environ.get('QUERY_STRING')
|
|
if cgi_path_info is not None: cgi_path_info = path.split(cgi_path_info)[1]
|
|
|
|
if filter is not None: filters.append(filter)
|
|
if filter is None and not filters: filters.append(default_filter)
|
|
if len(args) == 0: args = ['.']
|
|
if sortby is None: sortby = 'mtime:DN'
|
|
|
|
files = []
|
|
for basedir in args:
|
|
files.extend(find_files(basedir, filters))
|
|
sortfunc = build_sortfunc(sortby)
|
|
files = sort_files(files, sortfunc)
|
|
fgroups = build_fgroups(files)
|
|
if cgi_mode and fgroups and cgi_select_group is None:
|
|
# En mode CGI, s'il y a plusieurs groupes, ne pas afficher la liste
|
|
# complète, mais requérir la sélection d'un groupe
|
|
cgi_select_group = ''
|
|
if cgi_select_group is not None:
|
|
files = filter_files(files, cgi_select_group)
|
|
if cgi_path_info:
|
|
file = select_file(files, cgi_path_info)
|
|
if file is None:
|
|
if cgi_mode:
|
|
print "Content-Type: text/plain; charset=UTF-8"
|
|
print
|
|
print "Impossible de trouver le fichier %s" % cgi_path_info
|
|
else:
|
|
if cgi_mode:
|
|
ctype, encoding = mimetypes.guess_type(file.name, False)
|
|
if ctype is None: ctype = "application/octet-stream"
|
|
print "Content-Type: %s; charset=utf-8" % ctype
|
|
print "Content-Disposition: attachment; filename=\"%s\"" % file.name
|
|
print
|
|
inf = open(file.pf, 'rb')
|
|
try: shutil.copyfileobj(inf, sys.stdout)
|
|
finally: inf.close()
|
|
sys.exit()
|
|
|
|
if cgi_mode:
|
|
print "Content-Type: text/xml; charset=UTF-8"
|
|
print
|
|
print_files(files, fgroups, cgi_select_group, cgi_script_name, cgi_query_string, xslt)
|
|
|
|
if __name__ == '__main__':
|
|
run_cgilsxml()
|