"Fossies" - the Fresh Open Source Software Archive

Member "Tardis-1.2.1/src/Tardis/List.py" (9 Jun 2021, 31493 Bytes) of package /linux/privat/Tardis-1.2.1.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "List.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 1.1.5_vs_1.2.1.

    1 # vim: set et sw=4 sts=4 fileencoding=utf-8:
    2 #
    3 # Tardis: A Backup System
    4 # Copyright 2013-2020, Eric Koldinger, All Rights Reserved.
    5 # kolding@washington.edu
    6 #
    7 # Redistribution and use in source and binary forms, with or without
    8 # modification, are permitted provided that the following conditions are met:
    9 #
   10 #     * Redistributions of source code must retain the above copyright
   11 #       notice, this list of conditions and the following disclaimer.
   12 #     * Redistributions in binary form must reproduce the above copyright
   13 #       notice, this list of conditions and the following disclaimer in the
   14 #       documentation and/or other materials provided with the distribution.
   15 #     * Neither the name of the copyright holder nor the
   16 #       names of its contributors may be used to endorse or promote products
   17 #       derived from this software without specific prior written permission.
   18 #
   19 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   20 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   21 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
   22 # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
   23 # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
   24 # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
   25 # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
   26 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
   27 # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
   28 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
   29 # POSSIBILITY OF SUCH DAMAGE.
   30 
   31 import sys
   32 import logging
   33 import os
   34 import os.path
   35 import stat
   36 import argparse
   37 import fnmatch
   38 import parsedatetime
   39 import time
   40 
   41 import termcolor
   42 
   43 import Tardis
   44 import Tardis.Util as Util
   45 import Tardis.Defaults as Defaults
   46 import Tardis.Config as Config
   47 import Tardis.TardisDB as TardisDB
   48 
   49 columns = None
   50 columnfmt = None
   51 args = None
   52 curcolor = None
   53 logger = None
   54 backupSets = []
   55 
   56 line = ''
   57 
   58 colors = {
   59     'gone'      :  'red',
   60     'changed'   :  'cyan',
   61     'moved'     :  'blue',
   62     'full'      :  'cyan,,bold',
   63     'header'    :  'green',
   64     'name'      :  None,
   65     'error'     :  'red,,bold',
   66     'default'   :  None
   67 }
   68 
   69 fsEncoding = sys.getfilesystemencoding()
   70 
   71 def setColors(s):
   72     groups = s.split(':')
   73     groups = list(map(str.strip, groups))
   74     for g in groups:
   75         x = g.split('=')
   76         name = str(x[0])
   77         c = list(map(str.strip, x[1].split(',')))
   78         #c = map(lambda x: None if x.lower() == 'none' else x, c)
   79         c = [None if x.lower() == 'none' else x for x in c]
   80         if len(c) == 1:
   81             colors[name] = c[0]
   82         else:
   83             c = [None if i == '' else i for i in c]
   84             colors[name] = tuple(c)
   85 
   86 def doprint(text='', color=None, eol=False):
   87     """
   88     Add some characters to a line to be printed.  If eol is True, print the line and restart.
   89     Color can either be a color (red, blue, green, etc), or a tuple.
   90     If it's a tuple, the first element is the color, the second is a background color ('on_white', 'on_blue', etc),
   91     and any remaining values are attributes ('blink', 'underline') etc.
   92     See the termcolor package for lists of colors
   93     """
   94     global line
   95     if args.colors and color:
   96         if isinstance(color, str):
   97             line += termcolor.colored(str(text), color)
   98         else:
   99             line += termcolor.colored(str(text), color[0], color[1], attrs=list(color[2:]))
  100     else:
  101         line += str(text)
  102 
  103     #print(line)
  104     if eol:
  105         print(line.rstrip())
  106         line=''
  107 
  108 def flushLine():
  109     """
  110     Flush the line out, if there is one being built.
  111     """
  112     global line
  113     if line:
  114         print(line.rstrip())     # clear out any trailing spaces
  115         line=''
  116 
  117 def makeFakeRootInfo():
  118     fInfos = {}
  119     fSet = backupSets[0]
  120     lSet = backupSets[-1]
  121     for bset in backupSets:
  122         fInfos[bset['backupset']] = {
  123             "name"          : '',
  124             "inode"         : 0,
  125             "device"        : 0,
  126             "dir"           : 1,
  127             "link"          : 0,
  128             "parent"        : 0,
  129             "parentdev"     : 0,
  130             "size"          : 0,
  131             "mtime"         : 0,
  132             "ctime"         : 0,
  133             "atime"         : 0,
  134             "mode"          : 0o755,
  135             "uid"           : 0,
  136             "gid"           : 0,
  137             "nlinks"        : 1,
  138             "firstset"      : fSet['backupset'],
  139             "lastset"       : lSet['backupset'],
  140             "checksum"      : None,
  141             "chainlength"   : 0,
  142             "xattrs"        : None,
  143             "acl"           : None
  144         }
  145     return fInfos
  146 
  147 def collectFileInfo(filename, tardis, crypt):
  148     """
  149     Collect information about a file in all the backupsets
  150     Note that we sometimes need to reduce the pathlength.  It's done here, on a directory
  151     by directory basis.
  152     """
  153     lookup = crypt.encryptPath(filename) if crypt else filename
  154 
  155     fInfos = {}
  156     lInfo = {}
  157     if filename == '/':
  158         fInfos = makeFakeRootInfo()
  159     elif args.reduce:
  160         for bset in backupSets:
  161             temp = lookup
  162             temp = Util.reducePath(tardis, bset['backupset'], temp, args.reduce)     # No crypt, as we've already run that to get to lookup
  163 
  164             if lInfo and lInfo['firstset'] <= bset['backupset'] <= lInfo['lastset']:
  165                 fInfos[bset['backupset']] = lInfo
  166             else:
  167                 lInfo = tardis.getFileInfoByPath(temp, bset['backupset'])
  168                 fInfos[bset['backupset']] = lInfo
  169     else:
  170         fSet = backupSets[0]['backupset']
  171         lSet = backupSets[-1]['backupset']
  172         for (bset, info) in tardis.getFileInfoByPathForRange(lookup, fSet, lSet):
  173             logger.debug("Bset: %s, info: %s", bset, info)
  174             fInfos[bset] = info
  175 
  176     return fInfos
  177 
  178 def collectDirContents(tardis, dirlist, crypt):
  179     """
  180     Build a hash of hashes.  Outer hash is indexed by backupset, inner by filename
  181     Note: This is very inefficient.  You basically query for the same information over and over.
  182     Because of this, we use collectDirContents2 instead.  This function is left here for documentation
  183     purposes primarily.
  184     """
  185     contents = {}
  186     names = set()
  187     for (bset, finfo) in dirlist:
  188         x = tardis.readDirectory((finfo['inode'], finfo['device']), bset['backupset'])
  189         dirInfo = {}
  190         for y in x:
  191             name = str(crypt.decryptFilename(y['name']) if crypt else y['name'])
  192             dirInfo[name] = y
  193             names.add(name)
  194         contents[bset['backupset']] = dirInfo
  195     return contents, names
  196 
  197 def collectDirContents2(tardis, dirList, crypt):
  198     """
  199     Do the same thing as collectDirContents, just a lot faster, relying on the structure of the DB.
  200     Create a set of directory "ranges", a range being a set of entries in the dirlist that a: all have
  201     the same inode, and b: span a contiguous range of backupsets in the backupsets list (ie, if there are 3
  202     backupsets in the range in backupsets, there also must be the same three entries in the dirlist).  Then
  203     query any directory entries that exist in here, and span each one over the approriate portions of the
  204     range.  Repeat for each range.
  205     """
  206 
  207     contents = {}
  208     for (x, y) in dirList:
  209         contents[x['backupset']] = {}
  210     names = set()
  211     ranges = []
  212     dirRange = []
  213     prev = {}
  214     dirHash = dict([(x['backupset'], y) for (x,y) in dirList])
  215     # Detect the ranges
  216     for bset in backupSets:
  217         d = dirHash.setdefault(bset['backupset'])
  218         # If we don't have an entry here, the range ends.
  219         # OR if the inode is different from the previous
  220         if prev and ((not d) or (prev['inode'] != d['inode']) or (prev['device'] != d['device'])):
  221             if len(dirRange):
  222                 ranges.append(dirRange)
  223                 dirRange = []
  224         if d:
  225             dirRange.append(bset)
  226         prev = d
  227     if len(dirRange):
  228         ranges.append(dirRange)
  229 
  230     # Now, for each range, populate
  231     for r in ranges:
  232         first = r[0]['backupset']
  233         last  = r[-1]['backupset']
  234         dinfo = dirHash[first]
  235         #print "Reading for (%d, %d) : %d => %d" %(dinfo['inode'], dinfo['device'], first, last)
  236         x = tardis.readDirectoryForRange((dinfo['inode'], dinfo['device']), first, last)
  237         for y in x:
  238             logger.debug("Processing %s", y['name'])
  239             name = Util.asString(crypt.decryptFilename(y['name'])) if crypt else Util.asString(y['name'])
  240             names.add(name)
  241             for bset in r:
  242                 if y['firstset'] <= bset['backupset'] <= y['lastset']:
  243                     contents[bset['backupset']][name] = y
  244 
  245     # and return what we've discovered
  246     return (contents, names)
  247 
  248 
  249 def getFileNames(contents):
  250     """
  251     Extract a list of file names from file contents.  Names will contain a single entry
  252     for each name encountered.
  253     """
  254     names = set()
  255     for bset in backupSets:
  256         if bset['backupset'] in contents:
  257             lnames = set(contents[bset['backupset']].keys())
  258             names = names.union(lnames)
  259     return names
  260 
  261 def getInfoByName(contents, name):
  262     """
  263     Extract a list of fInfos corresponding to each backupset, based on the name list.
  264     """
  265     fInfo = {}
  266     for bset in backupSets:
  267         if bset['backupset'] in contents:
  268             d = contents[bset['backupset']]
  269             f = d.setdefault(name, None)
  270             fInfo[bset['backupset']] = f
  271         else:
  272             fInfo[bset['backupset']] = None
  273 
  274     return fInfo
  275 
  276 
  277 column = 0
  278 
  279 """
  280 The actual work of printing the data.
  281 """
  282 def printit(info, name, color, gone):
  283     global column
  284     annotation = ''
  285     if args.annotate and info is not None:
  286         if info['dir']:
  287             annotation = '/'
  288         elif info['link']:
  289             annotation = '@'
  290         elif info['mode'] & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH):
  291             annotation = '*'
  292     name = name + annotation
  293     if gone:
  294         name = '(' + name + ')'
  295 
  296     if column == 0:
  297         doprint('  ')
  298 
  299     if args.cksums:
  300         if info and info['checksum']:
  301             cksum = info['checksum']
  302         else:
  303             cksum = ''
  304     if args.chnlen:
  305         if info and info['chainlength'] is not None:
  306             chnlen = "%-3d" % int(info['chainlength'])
  307         else:
  308             chnlen = ''
  309     if args.inode:
  310         if info and info['inode'] is not None:
  311             inode = "%8d" % int(info['inode'])
  312         else:
  313             inode = ''
  314     if args.size:
  315         if info and info['size'] is not None:
  316             if args.human:
  317                 fsize = "%8s" % Util.fmtSize(info['size'], formats=['','KB','MB','GB', 'TB', 'PB'])
  318             else:
  319                 fsize = "%8d" % int(info['size'])
  320         else:
  321             fsize = ''
  322 
  323     if args.long:
  324         if gone:
  325             doprint('  %s' % (name), color, eol=True)
  326         else:
  327             mode = Util.filemode(info['mode'])
  328             group = Util.getGroupName(info['gid'])
  329             owner = Util.getUserId(info['uid'])
  330             mtime = Util.formatTime(info['mtime'])
  331             nlinks = info['nlinks']
  332             if info['size'] is not None:
  333                 if args.human:
  334                     size = Util.fmtSize(info['size'], formats=['','KB','MB','GB', 'TB', 'PB'])
  335                 else:
  336                     size = "%8d" % info['size']
  337             else:
  338                 size = ''
  339             doprint('  %9s %3d %-8s %-8s %8s %12s ' % (mode, nlinks, owner, group, size, mtime), color=colors['name'])
  340             if args.size:
  341                 doprint(' %8s ' % (fsize))
  342             if args.inode:
  343                 doprint(' %8s ' % (inode))
  344             if args.cksums:
  345                 doprint(' %32s ' % (cksum))
  346             if args.chnlen:
  347                 doprint(' %-3s ' % (chnlen))
  348             doprint('%s' % (name), color, eol=True)
  349     elif args.cksums or args.chnlen or args.inode or args.size:
  350         doprint(columnfmt % name, color)
  351         if args.size:
  352             doprint(' ' + fsize, color=colors['name'])
  353         if args.inode:
  354             doprint(' ' + inode, color=colors['name'])
  355         if args.cksums:
  356             doprint(' ' + cksum, color=colors['name'])
  357         if args.chnlen:
  358             doprint(' ' + chnlen, color=colors['name'])
  359         doprint(' ', eol=True)
  360     else:
  361         column += 1
  362         if column == columns:
  363             eol = True
  364             column = 0
  365         else:
  366             eol = False
  367         doprint(columnfmt % name, color, eol=eol)
  368 
  369 def printVersions(fInfos):
  370     """
  371     Print info about each version of the file that exists
  372     Doesn't actually do the printing, but calls printit to do it.
  373     """
  374     global column
  375     prevInfo = None        # Previous version's info
  376     lSet     = None
  377     column = 0
  378 
  379     for bset in backupSets:
  380         info = fInfos[bset['backupset']]
  381         color = None
  382         new = False
  383         gone = False
  384         broken = False
  385 
  386         # If there was no previous version, or the checksum has changed, we're new
  387         if (info is None) and (prevInfo is None):
  388             # file didn't exist here or previously.  Just skip
  389             continue
  390 
  391         if (info is None) and prevInfo is not None:
  392             # file disappeared.
  393             color = colors['gone']
  394             gone = True
  395         elif info['checksum'] is None:
  396             # Check for the error case where a file isn't connected to a checksum.  Not good.
  397             color = colors['error']
  398             broken = True
  399         elif (prevInfo is None) or (info['checksum'] != prevInfo['checksum']) or \
  400              ((args.checktimes or args.checkmeta) and (info['mtime'] != prevInfo['mtime'] or info['ctime'] != prevInfo['ctime'])) or \
  401              (args.checkmeta and (info['uid'] != prevInfo['uid'] or info['gid'] != prevInfo['gid'])):
  402             if info['chainlength'] == 0 and not info['dir']:
  403                 color = colors['full']
  404             else:
  405                 color = colors['changed']
  406             new = True
  407         elif info['inode'] != prevInfo['inode']:
  408             color = colors['moved']
  409             new = True
  410         else:
  411             pass
  412 
  413         prevInfo = info
  414         if new:
  415             lSet = bset
  416 
  417         # Skip out if we're not printing something here
  418         # Bascially we stay if we're print everything or it's a new file
  419         # OR if we're printing deletions and we disappered
  420         if args.versions == 'last' or args.versions == 'none' or (args.versions == 'change' and not (new or gone or broken)) or (gone and not args.deletions) or (broken and not args.broken):
  421             continue
  422 
  423         logger.debug("Bset: %s", bset)
  424         printit(info, bset['name'], color, gone)
  425 
  426     if args.versions == 'last':
  427         printit(fInfos[lSet['backupset']], lSet['name'], colors['changed'], False)
  428 
  429     flushLine()
  430 
  431 def processFile(filename, fInfos, tardis, crypt, printContents=True, recurse=0, first=True, fmt='%s:', eol=True):
  432     """
  433     Collect information about a file, across all the backup sets
  434     Print a header for the file.
  435     """
  436     logger.debug("Processing file %s", filename)
  437 
  438     # Count the number of non-null entries
  439     numFound = len([i for i in fInfos if fInfos[i] is not None])
  440 
  441     # Print the header
  442     if args.headers or (numFound == 0) or args.recent or not first:
  443         color = colors['header'] if first else colors['name']
  444         doprint(fmt % filename, color)
  445         if numFound == 0:
  446             doprint(' Not found', colors['error'])
  447         if (numFound == 0) or args.versions != 'none' or eol:
  448             flushLine()
  449 
  450     if args.versions != 'none':
  451         printVersions(fInfos)
  452 
  453     # Figure out which versions of the file are directories
  454 
  455     if printContents:
  456         # Create the list of directories
  457         dirs = [(x, fInfos[x['backupset']]) for x in backupSets if fInfos[x['backupset']] and fInfos[x['backupset']]['dir'] == 1]
  458         if len(dirs):
  459             (contents, names) = collectDirContents2(tardis, dirs, crypt)
  460             if not args.hidden:
  461                 names = [n for n in names if not n.startswith('.')]
  462             (numCols, fmt) = computeColumnWidth(names)
  463             col = 0
  464 
  465             for name in sorted(names, key=lambda x: x.lower().lstrip('.'), reverse=args.reverse):
  466                 fInfo = getInfoByName(contents, name)
  467                 col += 1
  468                 eol = True if ((col % numCols) == 0) else False
  469                 processFile(name, fInfo, tardis, crypt, printContents=False, recurse=0, first=False, fmt=fmt, eol=eol)
  470             flushLine()
  471 
  472     if recurse:
  473         # This is inefficient.  We're recalculating info we grabbed above.  But recursion should be minimal
  474         dirs = [(x, fInfos[x['backupset']]) for x in backupSets if fInfos[x['backupset']] and fInfos[x['backupset']]['dir'] == 1]
  475         if len(dirs):
  476             (contents, names) = collectDirContents2(tardis, dirs, crypt)
  477             if not args.hidden:
  478                 names = [n for n in names if not n.startswith('.')]
  479             (numCols, fmt) = computeColumnWidth(names)
  480             col = 0
  481 
  482             for name in sorted(names, key=lambda x: x.lower().lstrip('.'), reverse=args.reverse):
  483                 fInfos = getInfoByName(contents, name)
  484                 dirs = [(x, fInfos[x['backupset']]) for x in backupSets if fInfos[x['backupset']] and fInfos[x['backupset']]['dir'] == 1]
  485                 if len(dirs):
  486                     print()
  487                     processFile(os.path.join(filename, name), fInfos, tardis, crypt, printContents=printContents, recurse=recurse-1, first=True, eol=True)
  488                 flushLine()
  489 
  490 def findSet(name):
  491     for i in backupSets:
  492         if i['name'] == name:
  493             return i['backupset']
  494     doprint("Could not find backupset %s" % name, color=colors['error'], eol=True)
  495     return -1
  496 
  497 def pruneBackupSets(startRange, endRange):
  498     """
  499     Prune backupsets to only those in the specified range.
  500     """
  501     global backupSets
  502     newsets = backupSets[:]
  503     for i in backupSets:
  504         if not startRange <= i['backupset'] <= endRange:
  505             newsets.remove(i)
  506     backupSets = newsets
  507 
  508 def pruneBackupSetsByRange():
  509     """
  510     Parse and check the range varables, and prune the set appopriately.
  511     """
  512     setRange = args.range.split(':')
  513     if len(setRange) > 2:
  514         doprint("Invalid range '%s'" % args.range, color=colors['error'], eol=True)
  515         sys.exit(1)
  516     elif len(setRange) == 1:
  517         setRange.append(setRange[0])
  518 
  519     if setRange[0]:
  520         try:
  521             startRange = int(setRange[0])
  522         except ValueError:
  523             startRange = findSet(setRange[0])
  524             if startRange == -1:
  525                 sys.exit(1)
  526     else:
  527         startRange = 0
  528 
  529     if setRange[1]:
  530         try:
  531             endRange = int(setRange[1])
  532         except ValueError:
  533             endRange = findSet(setRange[1])
  534             if endRange == -1:
  535                 sys.exit(1)
  536     else:
  537         endRange = sys.maxsize
  538 
  539     if endRange < startRange:
  540         doprint("Invalid range.  Start must be before end", color=colors['error'], eol=True)
  541         sys.exit(1)
  542 
  543     pruneBackupSets(startRange, endRange)
  544 
  545 def pruneBackupSetsByDateRange(tardis):
  546     """
  547     Parse and check the date range variable, and prune the range appropriately.
  548     """
  549     cal = parsedatetime.Calendar()
  550     daterange = args.daterange.split(':')
  551     if len(daterange) > 2:
  552         doprint("Invalid range '%s'" % args.daterange, color=colors['error'], eol=True)
  553         sys.exit(1)
  554     elif len(daterange) == 1:
  555         daterange.append('')
  556 
  557     if daterange[0]:
  558         (then, success) = cal.parse(daterange[0])
  559         if success:
  560             startTime = time.mktime(then)
  561             startSet = tardis.getBackupSetInfoForTime(startTime)
  562 
  563             if startSet:
  564                 # Get the backupset, then add 1.  Backupset will be the LAST backupset before
  565                 # the start time, so 1 larger should be the first backupset after that.
  566                 # I think
  567                 startRange=startSet['backupset'] + 1
  568             else:
  569                 startRange = 0
  570         else:
  571             doprint("Invalid time: %s" % daterange[0], color=colors['error'], eol=True)
  572             sys.exit(1)
  573     else:
  574         startRange = 0
  575         startTime = time.mktime(time.gmtime(0))
  576 
  577     if daterange[1]:
  578         (then, success) = cal.parse(daterange[1])
  579         if success:
  580             endTime = time.mktime(then)
  581             endSet = tardis.getBackupSetInfoForTime(endTime)
  582             if endSet:
  583                 endRange = endSet['backupset']
  584             else:
  585                 endRange = sys.maxsize
  586         else:
  587             doprint("Invalid time: %s" % daterange[1], color=colors['error'], eol=True)
  588             sys.exit(1)
  589     else:
  590         endRange = sys.maxsize
  591         endTime = time.time()
  592 
  593     doprint("Starttime: " + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(startTime)), color=colors['header'], eol=True)
  594     doprint("EndTime:   " + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(endTime)), color=colors['header'], eol=True)
  595 
  596     if startTime > endTime:
  597         doprint("Invalid time range: end before start", color=colors['error'], eol='True')
  598         sys.exit(1)
  599 
  600     pruneBackupSets(startRange, endRange)
  601 
  602 
  603 def computeColumnWidth(names):
  604     """
  605     Given a list of names, compute the columns widths
  606     """
  607     if len(list(names)) == 0:
  608         return (1, '%s')
  609     longestName = max(list(map(len, names)), default=0)
  610 
  611     if args.columns:
  612         columns = args.columns
  613     else:
  614         if os.isatty(sys.stdout.fileno()):
  615             (_, width) = Util.getTerminalSize()
  616             logger.debug("Setting width to %d", width)
  617             width -= 2          # lop a couple characters off the end to avoid annoying wraps in some cases.
  618             columns = int(width / (longestName + 4))
  619             if columns == 0:
  620                 columns = 1
  621         else:
  622             columns = 1
  623 
  624     fmt = "%%-%ds  " % (longestName + 2)
  625     logger.debug("Setting columns to %d", columns)
  626 
  627     return (columns, fmt)
  628 
  629 def setupDisplay(tardis):
  630     """
  631     Calculate display parameters, including creating the list of backupsets that we want to process
  632     """
  633     global columns, columnfmt
  634     global backupSets
  635 
  636     backupSets = list(tardis.listBackupSets())
  637     if args.range:
  638         pruneBackupSetsByRange()
  639     elif args.daterange:
  640         pruneBackupSetsByDateRange(tardis)
  641 
  642     bsetNames = [x['name'] for x in backupSets]
  643 
  644     (columns, columnfmt) = computeColumnWidth(bsetNames)
  645 
  646 def globPath(path, tardis, crypt, first=0):
  647     """
  648     Glob a path.  Only globbs the first
  649     """
  650     logger.debug("Globbing %s", path)
  651     if not Util.isMagic(path):
  652         return [path]
  653     comps = path.split(os.sep)
  654     results = []
  655     for i in range(first, len(comps)):
  656         if Util.isMagic(comps[i]):
  657             currentPath = os.path.join('/', *comps[:i])
  658             pattern = comps[i]
  659             logger.debug("Globbing in component %d of %s: %s %s", i, path, currentPath, pattern)
  660 
  661             # Collect info about the current path (without the globb pattern)
  662             fInfos = collectFileInfo(currentPath, tardis, crypt)
  663 
  664             # Collect any directories in that poth
  665             dirs = [(x, fInfos[x['backupset']]) for x in backupSets if fInfos[x['backupset']] and fInfos[x['backupset']]['dir'] == 1]
  666 
  667             # And cons up the names which are in those directories
  668             (_, names) = collectDirContents2(tardis, dirs, crypt)
  669 
  670             # Filter down any that match
  671             matches = fnmatch.filter(names, pattern)
  672 
  673             # Put the paths back together
  674             globbed = sorted([os.path.join('/', currentPath, match, *comps[i+1:]) for match in matches])
  675             logger.debug("Globbed %s: %s", path, globbed)
  676 
  677             # And repeat.
  678             for j in globbed:
  679                 results += globPath(j, tardis, crypt, i + 1)
  680             break
  681     return  results
  682 
  683 def processArgs():
  684     isatty = os.isatty(sys.stdout.fileno())
  685 
  686     parser = argparse.ArgumentParser(description='List Tardis File Versions', fromfile_prefix_chars='@', formatter_class=Util.HelpFormatter, add_help=False)
  687 
  688     (_, remaining) = Config.parseConfigOptions(parser)
  689 
  690     Config.addCommonOptions(parser)
  691     Config.addPasswordOptions(parser)
  692 
  693     parser.add_argument('--long', '-l',     dest='long',        default=False, action='store_true',         help='Use long listing format.')
  694     parser.add_argument('--hidden', '-a',   dest='hidden',      default=False, action='store_true',         help='Show hidden files.')
  695     parser.add_argument('--reverse', '-r',  dest='reverse',     default=False, action='store_true',         help='Reverse the sort order')
  696     parser.add_argument('--annotate', '-f', dest='annotate',    default=False, action='store_true',         help='Annotate files based on type.')
  697     parser.add_argument('--size', '-s',     dest='size',        default=False, action='store_true',         help='Show file sizes')
  698     parser.add_argument('--human', '-H',    dest='human',       default=False, action='store_true',         help='Format sizes for easy reading')
  699     parser.add_argument('--dirinfo', '-d',  dest='dirinfo',     default=False, action='store_true',         help='List directories, but not their contents')
  700     parser.add_argument('--checksums', '-c',dest='cksums',      default=False, action='store_true',         help='Print checksums.')
  701     parser.add_argument('--chainlen', '-L', dest='chnlen',      default=False, action='store_true',         help='Print chainlengths.')
  702     parser.add_argument('--inode', '-i',    dest='inode',       default=False, action='store_true',         help='Print inode numbers')
  703     parser.add_argument('--versions', '-V', dest='versions',    default='change', choices=['none', 'change', 'all', 'last'],   help='Display all, changed, last, or no versions of files.  Default: %(default)s')
  704     parser.add_argument('--deletions',      dest='deletions',   default=True,  action=Util.StoreBoolean,    help='Show deletions. Default: %(default)s')
  705     parser.add_argument('--broken',         dest='broken',      default=True,  action=Util.StoreBoolean,    help='Show broken files (missing data). Default: %(default)s')
  706     parser.add_argument('--oneline', '-O',  dest='oneline',     default=False, action=Util.StoreBoolean,    help='Display versions on one line with the name.  Default: %(default)s')
  707     parser.add_argument('--times', '-T',    dest='checktimes',  default=False, action=Util.StoreBoolean,    help='Use file time changes when determining diffs. Default: %(default)s')
  708     parser.add_argument('--metadata', '-M', dest='checkmeta',   default=False, action=Util.StoreBoolean,    help='Use any metadata changes when determining diffs.  Default: %(default)s')
  709     parser.add_argument('--headers',        dest='headers',     default=True,  action=Util.StoreBoolean,    help='Show headers. Default: %(default)s')
  710     parser.add_argument('--colors',         dest='colors',      default=isatty, action=Util.StoreBoolean,   help='Use colors. Default: %(default)s')
  711     parser.add_argument('--columns',        dest='columns',     type=int, default=None ,                    help='Number of columns to display')
  712 
  713     parser.add_argument('--recurse', '-R',  dest='recurse',     default=False, action='store_true',         help='List Directories Recurively')
  714     parser.add_argument('--maxdepth',       dest='maxdepth',    default=sys.maxsize, type=int,              help='Maximum depth to recurse directories')
  715     #parser.add_argument('--path',           dest='path',        default=False, action='store_true',         help='Print the full path of files')
  716 
  717     parser.add_argument('--glob',           dest='glob',        default=False, action=Util.StoreBoolean,    help='Glob filenames')
  718 
  719     parser.add_argument('--reduce',         dest='reduce',      default=0, type=int, const=sys.maxsize, nargs='?',
  720                         help='Reduce paths by N directories.  No value for smart reduction')
  721     parser.add_argument('--realpath',       dest='realpath',    default=True, action=Util.StoreBoolean,     help='Use the full path, expanding symlinks to their actual path components')
  722 
  723     rangegrp = parser.add_mutually_exclusive_group()
  724     rangegrp.add_argument('--range',        dest='range',   default=None,                                   help="Use a range of backupsets.  Format: 'Start:End' Start and End can be names or backupset numbers.  Either value can be left off to indicate the first or last set respectively")
  725     rangegrp.add_argument('--dates',        dest='daterange', default=None,                                 help="Use a range of dates for the backupsets.  Format: 'Start:End'.  Start and End are names which can be intepreted liberally.  Either can be left off to indicate the first or last set respectively")
  726 
  727     parser.add_argument('--exceptions',     default=False, action=Util.StoreBoolean, dest='exceptions', help="Log full exception data");
  728 
  729     parser.add_argument('--verbose', '-v',  action='count', default=0, dest='verbose',                  help='Increase the verbosity')
  730     parser.add_argument('--version',        action='version', version='%(prog)s ' + Tardis.__versionstring__,    help='Show the version')
  731     parser.add_argument('--help', '-h',     action='help')
  732 
  733     parser.add_argument('directories', nargs='*', default='.',                                              help='List of directories/files to list')
  734 
  735     Util.addGenCompletions(parser)
  736 
  737     return parser.parse_args(remaining)
  738 
  739 def main():
  740     global args, logger
  741     tardis = None
  742     try:
  743         args = processArgs()
  744         logger = Util.setupLogging(args.verbose)
  745 
  746         setColors(Defaults.getDefault('TARDIS_LS_COLORS'))
  747 
  748         # Load any password info
  749         password = Util.getPassword(args.password, args.passwordfile, args.passwordprog, prompt="Password for %s: " % (args.client))
  750         args.password = None
  751 
  752         (tardis, _, crypt) = Util.setupDataConnection(args.database, args.client, password, args.keys, args.dbname, args.dbdir)
  753 
  754         setupDisplay(tardis)
  755 
  756         if args.headers:
  757             doprint("Client: %s    DB: %s" %(args.client, args.database), color=colors['name'], eol=True)
  758 
  759         if args.glob:
  760             directories = []
  761             for d in args.directories:
  762                 if not Util.isMagic(d):
  763                     directories.append(d)
  764                 else:
  765                     directories += globPath(os.path.abspath(d), tardis, crypt)
  766         else:
  767             directories = args.directories
  768 
  769         for d in directories:
  770             d = os.path.abspath(d)
  771             if args.realpath:
  772                 d = os.path.realpath(d)
  773             fInfos = collectFileInfo(d, tardis, crypt)
  774             recurse = args.maxdepth if args.recurse else 0
  775             processFile(d, fInfos, tardis, crypt, printContents=(not args.dirinfo), recurse=recurse)
  776     except KeyboardInterrupt:
  777         pass
  778     except TardisDB.AuthenticationException as e:
  779         logger.error("Authentication failed.  Bad password")
  780         if args.exceptions:
  781             logger.exception(e)
  782     except Exception as e:
  783         logger.error("Caught exception: %s", str(e))
  784         if args.exceptions:
  785             logger.exception(e)
  786     finally:
  787         if tardis:
  788             tardis.close()
  789 
  790 if __name__ == "__main__":
  791     main()