#!/usr/bin/env python
# -*- coding: utf-8 mode: python -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8

u"""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, urlparse, urllib, fnmatch, glob
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 u is None: return None
    elif 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
    glob = None
    caret_anchor = None
    deep_scan = None
    re_allows = None
    ex_vars = None
    ex_group = None
    ex_break_on = None
    force_disposition = None
    force_type = None
    force_charset = None

    def __init__(self, spec=None, glob=None):
        if spec is not None:
            self.re_spec = re.compile(spec)
            self.caret_anchor = spec.startswith('^')
            self.deep_scan = '/' in spec
        elif glob is not None:
            self.glob = glob
            self.deep_scan = '/' in glob
        else: raise ValueError("either spec or glob must be specified")
        self.re_allows = []
        self.ex_vars = []
        self.ex_group = None
        self.ex_break_on = None

    def allow_spec(self, re_allow):
        self.re_allows.append(re.compile(re_allow))

    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 set_break_on(self, ex_break_on):
        self.ex_break_on = ex_break_on

    def __repr__(self):
        return 'Filter<%s||%s,%s,%s,%s,%s>' % \
            (self.re_spec, self.glob, repr(self.re_allows),
             repr(self.ex_vars),
             self.ex_group, self.ex_break_on,
            )

    def match_fill(self, file, basedir=None):
        if self.glob:
            if self.deep_scan:
                # la correspondance a déjà été faite avant avec glob.glob(). on
                # se contente d'initialiser l'objet
                pass
            elif not fnmatch.fnmatch(file.name, self.glob):
                return None
            file.group = None
            file.break_on = None
            if self.force_disposition: file.content_disposition = self.force_disposition
            if self.force_type: file.content_type = self.force_type
            if self.force_charset: file.content_charset = self.force_charset
            return True

        if self.deep_scan:
            pf = file.pf
            if not self.caret_anchor:
                blen = len(basedir)
                if pf[:blen] != basedir: return None
                pf = pf[blen:]
                if not pf.startswith('/'): return None
                pf = pf[1:]
            mo = self.re_spec.match(pf)
        else: 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
        else:
            group = None
        file.group = group
        if self.ex_break_on is not None:
            break_on = mo.expand(self.ex_break_on)
            if '%' in break_on: break_on = break_on % file.vars
        else:
            break_on = None
        file.break_on = break_on
        if self.force_disposition: file.content_disposition = self.force_disposition
        if self.force_type: file.content_type = self.force_type
        if self.force_charset: file.content_charset = self.force_charset
        return True

    def match_allow(self, file):
        for re_allow in self.re_allows:
            if re_allow.match(file.name) is not None:
                return True
        return False

class File:
    pf = None
    group = None
    vars = None
    dontlist = None

    def __init__(self, dir=None, name=None, pf=None, dontlist=False):
        if pf is None:
            if dir is None or name is None: raise ValueError("either (dir,name) or pf must be specified")
            pf = path.join(dir, name)
        dir, name =  path.split(pf) # il est possible que name soit de la forme subdir/name
        dir = path.abspath(dir)
        pf = path.join(dir, name)
        self.__dict__['pf'] = pf
        self.__dict__['group'] = None
        self.__dict__['vars'] = {}
        self.__dict__['dontlist'] = dontlist

        stat = os.stat(pf)
        mtime = int(stat.st_mtime)

        self.dir = dir
        self.name = name
        self.dlname = name
        self.size = stat.st_size
        self.mtime = mtime
        self.date = datef(DATE_DATEF, mtime)
        self.time = datef(TIME_DATEF, mtime)
        self.Y, self.m, self.d, self.H, self.M, self.S = datef('%Y %m %d %H %M %S', mtime).split(' ')

    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 has_key(self, name): return self.vars.has_key(name)
    def __getattr__(self, name):
        try: return self.vars[name]
        except KeyError: raise AttributeError(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, spec_method='all'):
    basedir = path.abspath(basedir)
    files = []

    deep_filters = [filter for filter in filters if filter.re_spec is not None and filter.deep_scan]
    if deep_filters:
        for dirpath, dirnames, filenames in os.walk(basedir):
            for filename in filenames:
                # race condition: si le fichier disparait entre-temps, une
                # exception se produit
                try: file = File(dirpath, filename)
                except: continue
                matched1 = matched = False
                allowed = False
                for filter in deep_filters:
                    if spec_method == 'all' or \
                       (spec_method == 'first' and not matched):
                        matched1 = filter.match_fill(file, basedir)
                    else:
                        matched1 = False
                    matched = matched1 or matched

                    allowed = filter.match_allow(file) or allowed

                if matched: pass
                elif allowed: file.dontlist = True
                else: continue
                files.append(file)

    simple_filters = [filter for filter in filters if filter.re_spec is not None and not filter.deep_scan]
    if simple_filters:
        for name in os.listdir(basedir):
            # race condition: si le fichier disparait entre-temps, une
            # exception se produit
            try: file = File(basedir, name)
            except: continue
            if not file.isfile(): continue
            matched1 = matched = False
            allowed = False
            for filter in simple_filters:
                if spec_method == 'all' or \
                   (spec_method == 'first' and not matched):
                    matched1 = filter.match_fill(file, basedir)
                else:
                    matched1 = False
                matched = matched1 or matched

                allowed = filter.match_allow(file) or allowed

            if matched: pass
            elif allowed: file.dontlist = True
            else: continue
            files.append(file)

    glob_filters = [filter for filter in filters if filter.glob is not None]
    if glob_filters:
        for filter in glob_filters:
            candidates = []
            if filter.deep_scan:
                for pf in glob.glob(path.join(basedir, filter.glob)):
                    # race condition: si le fichier disparait entre-temps, une
                    # exception se produit
                    try: file = File(pf=pf)
                    except: continue
                    candidates.append(file)
            else:
                for name in os.listdir(basedir):
                    # race condition: si le fichier disparait entre-temps, une
                    # exception se produit
                    try: file = File(basedir, name)
                    except: continue
                    candidates.append(file)
            for file in candidates:
                if not file.isfile(): continue
                elif filter.match_fill(file): pass
                elif filter.match_allow(file): file.dontlist = True
                else: 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, None)
            bv = getattr(b, field, None)
            if av is None:
                if bv is None: outcome = 0
                else: outcome = 1
            elif bv is None:
                outcome = -1
            else:
                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: func = lambda file: file.dontlist or file.group == group
    else: func = lambda file: file.dontlist or not file.group
    return filter(func, files)

def select_file(files, name):
    matches = filter(lambda file: file.name == name, files)
    if matches: return matches[0]
    else: return None

def cgi_nocache():
    print "Cache-Control: private, no-cache, no-store, must-revalidate, max-age=0"
    print "Pragma: no-cache"
    print "Expires: Thu, 01 Jan 1970 00:00:00 GMT"

RE_COMMA = re.compile(',')
def lfix(values):
    if values is None: return None
    fvalues = []
    for parts in values:
        parts = RE_COMMA.split(parts)
        fvalues.extend(parts)
    return fvalues

def filter_query_string(query_string, includes=None, excludes=None, prefix=None):
    params = urlparse.parse_qsl(query_string, keep_blank_values=True)
    includes = lfix(includes)
    if includes is None: names = dict(params).keys()
    else: names = includes[:]
    excludes = lfix(excludes)
    if excludes is not None:
        for name in excludes:
            if name in names: names.remove(name)
    params = [(name, value) for (name, value) in params if name in names]
    query_string = urllib.urlencode(params)
    if prefix:
        if query_string != "": query_string = "%s&%s" % (prefix, query_string)
        elif prefix != "": query_string = prefix
    return query_string

def print_files(files, fgroups=None, select_group=None, script_name=None, xslt=None,
                query_string=None, includes=None, excludes=None, prefix=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)
        ET.SubElement(xenv, "script_base").text = _u(re.sub(r'[^/]+$', '', script_name))

    orig_query_string = query_string
    if orig_query_string is not None:
        query_string = orig_query_string
        params = []
        if query_string is not None:
            params = urlparse.parse_qsl(query_string, keep_blank_values=True)
            query_string = _u(query_string)
        fquery_string = orig_query_string
        fparams = []
        if fquery_string is not None:
            fquery_string = filter_query_string(fquery_string, includes, excludes, prefix)
            fparams = urlparse.parse_qsl(fquery_string, keep_blank_values=True)
            fquery_string = _u(fquery_string)

        if includes or excludes or prefix:
            xorig = ET.SubElement(xenv, "orig")
            if query_string:
                ET.SubElement(xorig, "query_string").text = u'?%s' % query_string
                ET.SubElement(xorig, "squery_string").text = u'&%s' % query_string
            xvars = ET.SubElement(xorig, "query_vars")
            for name, value in params:
                ET.SubElement(xvars, name).text = value
            query_string = fquery_string
            params = fparams

        if query_string:
            ET.SubElement(xenv, "query_string").text = u'?%s' % query_string
            ET.SubElement(xenv, "squery_string").text = u'&%s' % query_string

        xvars = ET.SubElement(xenv, "query_vars")
        for name, value in params:
            ET.SubElement(xvars, name).text = value

    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")
    first = True
    last_classifier = (None, None)
    for file in files:
        if file.dontlist: continue
        current_classifier = (file.group, file.break_on)
        if first or current_classifier != last_classifier:
            file.break_here = None
        last_classifier = current_classifier
        first = False
        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():
    class ContentInfos:
        disposition = 'attachment'
        type = None
        charset = 'utf-8'
    default_content = ContentInfos()
    default_filter = Filter(r'(.*)')
    env = dict(filters=[], filter=None)
    def add_spec(option, opt, value, parser, *args, **kw):
        if env['filter'] is not None: env['filters'].append(env['filter'])
        env['filter'] = Filter(spec=value)
    def add_glob(option, opt, value, parser, *args, **kw):
        if env['filter'] is not None: env['filters'].append(env['filter'])
        env['filter'] = Filter(glob=value)
    def allow_spec(option, opt, value, parser, *args, **kw):
        if env['filter'] is None: env['filter'] = default_filter
        env['filter'].allow_spec(value)
    def add_var(option, opt, value, parser, *args, **kw):
        if env['filter'] is None: env['filter'] = default_filter
        env['filter'].add_var(value)
    def set_group(option, opt, value, parser, *args, **kw):
        if env['filter'] is None: env['filter'] = default_filter
        env['filter'].set_group(value)
    def set_break_on(option, opt, value, parser, *args, **kw):
        if env['filter'] is None: env['filter'] = default_filter
        env['filter'].set_break_on(value)
    def set_content_disposition(option, opt, value, parser, *args, **kw):
        if value in ('attachment', 'inline'): pass
        elif value == 'none': value = None
        else:
            if cgi_mode:
                if not cgi_cache: cgi_nocache()
                print "Content-Type: text/plain; charset=utf-8"
                print
            print "%s: Disposition de contenu invalide. Ce soit être attachment, inline ou none" % cgi_content_disposition
            sys.exit(1)
        if env['filter'] is None: default_content.disposition = value
        else: env['filter'].force_disposition = value
    def set_content_type(option, opt, value, parser, *args, **kw):
        if env['filter'] is None: default_content.type = value
        else: env['filter'].force_type = value
    def set_content_charset(option, opt, value, parser, *args, **kw):
        if env['filter'] is None: default_content.charset = value
        else: env['filter'].force_charset = value

    from optparse import OptionParser
    OP = OptionParser(usage=u"\n\t%prog [options] /path/to/dir", description=__doc__)
    OP.set_defaults(spec_method='all')
    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. "
                  + u"La correspondance est tentée sur le nom du fichier avec la fonction match(). "
                  + u"Penser à inclure l'ancre $ pour matcher le nom entier du fichier. "
                  + u"Si l'expression régulière contient un caractère '/', la correspondance peut se faire sur le chemin complet du fichier. "
                  + u"Pour simplifier, par défaut seule la partie située après le chemin de base est matchée, sauf si l'expression commence par '^', auquel cas la correspondance se fait sur le chemin complet du fichier. "
                  + u"L'expression régulière peut définir des groupes qui sont utilisées pour l'extraction des variables."
                  + u"\n  Il est possible de spécifier cette option plusieurs fois."
                  + u"\n  Note: TOUTES les expressions régulières sont testées par rapport au nom du fichier, et pour celles qui correspondent, les variables correspondantes sont définies. "
                  + u"Il faut donc ordonner les expressions régulières de la plus générale à la plus spécifique, contrairement à ce qui se fait d'habitude.")
    OP.add_option('--all-specs', dest='spec_method', action='store_const', const='all',
                  help=u"Avec l'option -e, tester toutes les expressions régulières dans l'ordre de définition. C'est le comportement par défaut.")
    OP.add_option('--first-spec', dest='spec_method', action='store_const', const='first',
                  help=u"Avec l'option -e, arrêter à la première expression régulière qui correspond")
    OP.add_option('--glob', dest='pattern', action='callback', callback=add_glob, type='string',
                  help=u"Comme --spec, mais en utilisant les patterns du shell *, ?, [seq], [!seq]. "
                  + u"Si l'expression contient un caractère '/', la correspondance se fait sur le chemin complet du fichier avec le module glob. "
                  + u"Cette précision est importante notamment pour la recherche de fichiers dont le nom commence par un point. "
                  + u"Si l'expression ne contient pas le caractère '/', la correspondance est faite uniquement sur le nom de fichier avec le module fnmatch, et le point n'a plus de signification particulière. "
                  + u"Cette méthode de correspondance ne permet pas de définir des groupes et ne permet donc pas d'extraire des variables.")
    OP.add_option('-E', '--allow-spec', dest='spec', action='callback', callback=allow_spec, type='string',
                  help=u"Ajouter une spécification de fichier qui peut être demandé avec --cgi-path-info. "
                  + u"Ces fichiers ne sont pas inclus dans la liste.")
    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. "
                  + u"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 respectivement le groupe numéro N et le groupe nommé NAME de l'expression régulière --spec."
                  + u"\n  Cette option peut être spécifiée plusieurs fois. "
                  + u"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. "
                  + u"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 respectivement le groupe numéro N et le groupe nommé NAME de l'expression régulière --spec."
                  + u"\n  Cette option ne peut être spécifiée qu'une seule fois par option --spec")
    OP.add_option('-b', '--break-on', dest='break_on', action='callback', callback=set_break_on, type='string',
                  help=u"Spécifier une expression qui permet de partitionner la liste des fichiers au sein d'un même groupe. "
                  + u"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 respectivement le groupe numéro N et le groupe nommé NAME de l'expression régulière --spec."
                  + u"\n  Bien que ce ne soit pas une obligation, il est logique de trier la liste sur cette expression pour que les groupes de fichiers soient ensembles dans la liste."
                  + u"\n  Cette 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. "
                  + u"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. "
                  + u"Si un champ est spécifié mais que le type de tri ne l'est pas, la valeur par défaut est AC. "
                  + u"Si cette option n'est pas spécifiée, le tri par défaut est 'mtime:DN'."
                  + u"\n  Il 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. "
                  + u"Ce mode est automatiquement activé si la variable d'environnement REQUEST_METHOD existe.")
    OP.add_option('--cgi-allow-cache', dest='cgi_allow_cache', action='store_true',
                  help=u"En mode CGI, ne pas rajouter les en-tête désactivant la mise en cache du résultat.")
    OP.add_option('--cgi-content-disposition', '--cgicd', dest='content-disposition', action='callback', callback=set_content_disposition, type='string',
                  help=u"En mode CGI, forcer la disposition de contenu avec lequel servir le fichier. "
                  + u"Les valeurs valides sont attachment (par défaut), inline, none. "
                  + u"Si cette option est spécifiée après une option --spec ou --glob, elle ne s'applique qu'aux fichiers qui correspondent à ce modèle")
    OP.add_option('--cgi-content-type', '--cgict', dest='content-type', action='callback', callback=set_content_type, type='string',
                  help=u"En mode CGI, forcer le type de contenu avec lequel servir le fichier. "
                  + u"Si cette option est spécifiée après une option --spec ou --glob, elle ne s'applique qu'aux fichiers qui correspondent à ce modèle")
    OP.add_option('--cgi-content-charset', '--cgicc', dest='content-charset', action='callback', callback=set_content_charset, type='string',
                  help=u"En mode CGI, forcer l'encoding de contenu avec lequel servir le fichier. "
                  + u"Si cette option est spécifiée après une option --spec ou --glob, elle ne s'applique qu'aux fichiers qui correspondent à ce modèle")
    OP.add_option('-P', '--cgi-path-info', dest='path_info',
                  help=u"Spécifier un chemin d'un fichier à télécharger. "
                  + u"En mode CGI, cette valeur est prise dans la variable d'environnement PATH_INFO")
    OP.add_option('-Q', '--cgi-query-string', dest='query_string',
                  help=u"Spécifier la valeur de QUERY_STRING pour provisionner l'environnement du fichier résultat. "
                  + u"En mode CGI, cette valeur est prise dans la variable d'environnement QUERY_STRING.")
    OP.add_option('-N', '--cgi-script-name', dest='script_name',
                  help=u"Spécifier la valeur de SCRIPT_NAME pour provisionner l'environnement du fichier résultat. "
                  + u"En mode CGI, cette option est automatiquement activée si QUERY_STRING contient le paramètre script_name=SCRIPT_NAME, la valeur par défaut étant la valeur de la variable d'environnement SCRIPT_NAME."
                  + u"\n  L'ordre de priorité pour le calcul de cette valeur est: d'abord le paramètre script_name dans QUERY_STRING, puis l'option de la ligne de commande, enfin la valeur de la variable d'environnement")
    OP.add_option('-G', '--cgi-param-group', dest='select_group',
                  help=u"Sélectionner le groupe spécifié. "
                  + u"Seuls les fichiers du groupe sont affichés. "
                  + u"En mode CGI, cette option est automatiquement activée si QUERY_STRING contient le paramètre group=GROUP."
                  + u"\n  S'il n'y a qu'un seul groupe, il est automatiquement sélectionné."
                  + u"\n  L'ordre de priorité pour le calcul de cette valeur est: d'abord le paramètre group dans QUERY_STRING, puis l'option de la ligne de commande")
    OP.add_option('-t', '--cgi-param-xslt', dest='xslt',
                  help=u"Ajouter le chemin vers la feuille de style XSLT dans le flux XML généré. "
                  + u"En mode CGI, cette option est automatiquement activée si QUERY_STRING contient le paramètre xslt=XSLT."
                  + u"\n  L'ordre de priorité pour le calcul de cette valeur est: d'abord le paramètre xslt dans QUERY_STRING, puis l'option de la ligne de commande")
    OP.add_option('-i', '--include', dest='includes', action='append',
                  help=u"Spécifier un paramètre à inclure pour construire la valeur du chemin xpath /result/env/query_string dans le résultat. "
                  + u"Il est possible de spécifier plusieurs paramètres en les séparant par des virgules. "
                  + u"Par défaut, prendre tous les paramètres de la requête.")
    OP.add_option('-x', '--exclude', dest='excludes', action='append',
                  help=u"Spécifier un paramètre à exclure pour construire la valeur du chemin xpath /result/env/query_string dans le résultat. "
                  + u"Il est possible de spécifier plusieurs paramètres en les séparant par des virgules.")
    OP.add_option('-p', '--prefix', dest="prefix",
                  help=u"Ajouter les paramètres supplémentaires spécifiés à /result/env/query_string.")
    o, args = OP.parse_args()
    filters = env['filters']
    filter = env['filter']
    sortby = o.sortby

    environ = os.environ
    cgi_mode = o.cgi_mode
    cgi_cache = o.cgi_allow_cache and True or False
    cgi_query_string = o.query_string
    if cgi_query_string is not None: environ['QUERY_STRING'] = cgi_query_string
    if cgi_mode is None:
        cgi_mode = 'REQUEST_METHOD' in environ
    if cgi_mode:
        import cgi; form = cgi.FieldStorage()
        cgi_path_info = o.path_info
        if cgi_path_info is None and 'PATH_INFO' in environ : cgi_path_info = environ.get('PATH_INFO')
        if cgi_query_string is None and 'QUERY_STRING' in environ: cgi_query_string = environ.get('QUERY_STRING')
        cgi_script_name = None
        if 'script_name' in form and cgi_script_name is None: cgi_script_name = form.getfirst("script_name")
        if cgi_script_name is None: cgi_script_name = o.script_name
        if cgi_script_name is None and 'SCRIPT_NAME' in environ : cgi_script_name = environ.get('SCRIPT_NAME')
        cgi_select_group = None
        if 'group' in form and cgi_select_group is None: cgi_select_group = form.getfirst("group")
        if cgi_select_group is None: cgi_select_group = o.select_group
        cgi_xslt = None
        if 'xslt' in form and cgi_xslt is None: cgi_xslt = form.getfirst("xslt")
        if cgi_xslt is None: cgi_xslt = o.xslt
    else:
        cgi_path_info = o.path_info
        cgi_query_string = o.query_string
        cgi_script_name = o.script_name
        cgi_select_group = o.select_group
        cgi_xslt =  o.xslt

    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, o.spec_method))
    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, sauf s'il n'y a
        # qu'une seul groupe défini
        if len(fgroups) == 1: cgi_select_group = fgroups[0]
        else: 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:
                if not cgi_cache: cgi_nocache()
                print "Content-Type: text/plain; charset=utf-8"
                print
            print "Impossible de trouver le fichier %s" % cgi_path_info
            sys.exit(1)
        else:
            if cgi_mode:
                if not cgi_cache: cgi_nocache()
                dlname = file.dlname or file.name
                if file.has_key('content_disposition'): content_disposition = file.content_disposition
                else: content_disposition = default_content.disposition
                if file.has_key('content_type'): content_type = file.content_type
                else: content_type = default_content.type
                if content_type is None: content_type, encoding = mimetypes.guess_type(dlname, False)
                if content_type is None: content_type = "application/octet-stream"
                if file.has_key('content_charset'): content_charset = file.content_charset
                else: content_charset = default_content.charset

                print "Content-Type: %s; charset=%s" % (content_type, content_charset)
                if content_disposition is not None:
                    print "Content-Disposition: %s; filename=\"%s\"" % (content_disposition, dlname)
                print
            inf = open(file.pf, 'rb')
            try: shutil.copyfileobj(inf, sys.stdout)
            finally: inf.close()
            sys.exit(0)

    if cgi_mode:
        if not cgi_cache: cgi_nocache()
        print "Content-Type: text/xml; charset=utf-8"
        print
    print_files(files, fgroups, cgi_select_group, cgi_script_name, cgi_xslt,
                cgi_query_string, o.includes, o.excludes, o.prefix)

if __name__ == '__main__':
    run_cgilsxml()
