possibilité de matcher des sous-répertoires dans l'expression --spec. Support du match de type --glob

This commit is contained in:
Jephté Clain 2016-06-10 03:57:07 +04:00
parent a49d6e2a24
commit b7600aa43b
1 changed files with 102 additions and 23 deletions

View File

@ -6,7 +6,7 @@ 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. 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 import os, sys, re, shutil, mimetypes, urlparse, urllib, fnmatch, glob
from os import path from os import path
from time import time, localtime from time import time, localtime
from types import UnicodeType, StringTypes from types import UnicodeType, StringTypes
@ -35,13 +35,23 @@ RE_SORT_FIELD_TYPE = re.compile(r'([_a-zA-Z][-_a-zA-Z0-9]*)(?::([aAdDcCnN]+))?')
class Filter: class Filter:
re_spec = None re_spec = None
glob = None
caret_anchor = None
deep_scan = None
re_allows = None re_allows = None
ex_vars = None ex_vars = None
ex_group = None ex_group = None
ex_break_on = None ex_break_on = None
def __init__(self, re_spec): def __init__(self, spec=None, glob=None):
self.re_spec = re.compile(re_spec) 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.re_allows = []
self.ex_vars = [] self.ex_vars = []
self.ex_group = None self.ex_group = None
@ -63,14 +73,34 @@ class Filter:
self.ex_break_on = ex_break_on self.ex_break_on = ex_break_on
def __repr__(self): def __repr__(self):
return 'Filter<%s,%s,%s,%s,%s>' % \ return 'Filter<%s||%s,%s,%s,%s,%s>' % \
(self.re_spec, repr(self.re_allows), (self.re_spec, self.glob, repr(self.re_allows),
repr(self.ex_vars), repr(self.ex_vars),
self.ex_group, self.ex_break_on, self.ex_group, self.ex_break_on,
) )
def match_fill(self, file): def match_fill(self, file, basedir=None):
mo = self.re_spec.match(file.name) 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
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 if mo is None: return None
for name, expr in self.ex_vars: for name, expr in self.ex_vars:
value = mo.expand(expr) value = mo.expand(expr)
@ -102,7 +132,11 @@ class File:
vars = None vars = None
dontlist = None dontlist = None
def __init__(self, dir, name, dontlist=False): 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) dir = path.abspath(dir)
pf = path.join(dir, name) pf = path.join(dir, name)
self.__dict__['pf'] = pf self.__dict__['pf'] = pf
@ -137,19 +171,49 @@ class File:
def __repr__(self): return 'File<%s>' % (self.pf) def __repr__(self): return 'File<%s>' % (self.pf)
def find_files(basedir, filters): def find_files(basedir, filters):
basedir = path.abspath(basedir)
files = [] files = []
for name in os.listdir(basedir): deep_filters = [filter for filter in filters if filter.re_spec is not None and filter.deep_scan]
file = File(basedir, name) if deep_filters:
if not file.isfile(): continue for dirpath, dirnames, filenames in os.walk(basedir):
matched = False for filename in filenames:
allowed = False file = File(dirpath, filename)
for filter in filters: matched = False
matched = filter.match_fill(file) or matched allowed = False
allowed = filter.match_allow(file) or allowed for filter in deep_filters:
if matched: pass matched = filter.match_fill(file, basedir) or matched
elif allowed: file.dontlist = True allowed = filter.match_allow(file) or allowed
else: continue if matched: pass
files.append(file) 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):
file = File(basedir, name)
if not file.isfile(): continue
matched = False
allowed = False
for filter in simple_filters:
matched = filter.match_fill(file) 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:
if filter.deep_scan:
candidates = [File(pf=pf) for pf in glob.glob(path.join(basedir, filter.glob))]
else:
candidates = [File(basedir, name) for name in os.listdir(basedir)]
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 return files
def build_sortfunc(sortby): def build_sortfunc(sortby):
@ -318,7 +382,10 @@ def run_cgilsxml():
default_filter = Filter(r'(.*)') default_filter = Filter(r'(.*)')
def add_spec(option, opt, value, parser, *args, **kw): def add_spec(option, opt, value, parser, *args, **kw):
if env['filter'] is not None: env['filters'].append(env['filter']) if env['filter'] is not None: env['filters'].append(env['filter'])
env['filter'] = Filter(value) 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): def allow_spec(option, opt, value, parser, *args, **kw):
if env['filter'] is None: env['filter'] = default_filter if env['filter'] is None: env['filter'] = default_filter
env['filter'].allow_spec(value) env['filter'].allow_spec(value)
@ -335,9 +402,21 @@ def run_cgilsxml():
from optparse import OptionParser from optparse import OptionParser
OP = OptionParser(usage=u"\n\t%prog [options] /path/to/dir", description=__doc__) OP = OptionParser(usage=u"\n\t%prog [options] /path/to/dir", description=__doc__)
OP.add_option('-e', '--spec', dest='spec', action='callback', callback=add_spec, type='string', 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." 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 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. 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.") + 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('--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', 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. Ces fichiers ne sont pas inclus dans la liste.") help=u"Ajouter une spécification de fichier qui peut être demandé avec --cgi-path-info. Ces fichiers ne sont pas inclus dans la liste.")
OP.add_option('-v', '--var', dest='var', action='callback', callback=add_var, type='string', OP.add_option('-v', '--var', dest='var', action='callback', callback=add_var, type='string',