"Fossies" - the Fresh Open Source Software Archive

Member "Tardis-1.2.1/src/Tardis/TardisFS.py" (9 Jun 2021, 27357 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 "TardisFS.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 #! /usr/bin/python
    2 # vim: set et sw=4 sts=4 fileencoding=utf-8:
    3 #
    4 # Tardis: A Backup System
    5 # Copyright 2013-2020, Eric Koldinger, All Rights Reserved.
    6 # kolding@washington.edu
    7 #
    8 # Redistribution and use in source and binary forms, with or without
    9 # modification, are permitted provided that the following conditions are met:
   10 #
   11 #     * Redistributions of source code must retain the above copyright
   12 #       notice, this list of conditions and the following disclaimer.
   13 #     * Redistributions in binary form must reproduce the above copyright
   14 #       notice, this list of conditions and the following disclaimer in the
   15 #       documentation and/or other materials provided with the distribution.
   16 #     * Neither the name of the copyright holder nor the
   17 #       names of its contributors may be used to endorse or promote products
   18 #       derived from this software without specific prior written permission.
   19 #
   20 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   21 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   22 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
   23 # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
   24 # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
   25 # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
   26 # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
   27 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
   28 # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
   29 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
   30 # POSSIBILITY OF SUCH DAMAGE.
   31 
   32 
   33 import os      # for filesystem modes (O_RDONLY, etc)
   34 import os.path
   35 import errno   # for error number codes (ENOENT, etc)
   36                # - note: these must be returned as negatives
   37 import sys
   38 import logging
   39 import logging.handlers
   40 import argparse
   41 import tempfile
   42 import json
   43 import base64
   44 import time
   45 import stat    # for file properties
   46 import functools
   47 
   48 #import fuse
   49 from fuse import FUSE, FuseOSError, Operations, LoggingMixIn
   50 
   51 import Tardis
   52 import Tardis.CacheDir as CacheDir
   53 import Tardis.Regenerator as Regenerator
   54 import Tardis.Util as Util
   55 import Tardis.Cache as Cache
   56 import Tardis.Defaults as Defaults
   57 import Tardis.TardisDB as TardisDB
   58 import Tardis.Config as Config
   59 
   60 _BackupSetInfo = 0
   61 _LastBackupSet = 1
   62 _DirInfo       = 2
   63 _DirContents   = 3
   64 _FileDetails   = 4
   65 _LinkContents  = 5
   66 
   67 _infoEnabled    = True
   68 
   69 logger = None
   70 
   71 logLevels = [logging.WARNING, logging.INFO, logging.DEBUG]
   72 
   73 def tracer(func):
   74     @functools.wraps(func)
   75     def trace(*args, **kwargs):
   76         if _infoEnabled:
   77             logger.info("CALL %s:(%s %s)", func.__name__, str(args)[1:-1], str(kwargs)[1:-1])
   78         try:
   79             x = func(*args, **kwargs)
   80             logger.info("COMPLETE %s:(%s %s) => %s", func.__name__, str(args)[1:-1], str(kwargs)[1:-1], str(x)[:32])
   81             return x
   82         except Exception as e:
   83             logger.error("CALL %s:(%s %s)", func.__name__, str(args)[1:-1], str(kwargs)[1:-1])
   84             logger.error("%s raised exception %s: %s", func.__name__, e.__class__.__name__, str(e))
   85             #logger.exception(e)
   86             raise e
   87     return trace
   88 
   89 def getDepth(path):
   90     """
   91     Return the depth of a given path, zero-based from root ('/')
   92     """
   93     logger.debug("getDepth: %s", path)
   94     if path ==  '/':
   95         return 0
   96 
   97     return path.count('/')
   98 
   99 def getParts(path):
  100     """
  101     Return the slash-separated parts of a given path as a list
  102     Namely, the backupset and the path within the set
  103     """
  104     if path == '/':
  105         return [['/']]
  106     return path.strip("/").split('/', 1)
  107 
  108 class TardisFS(LoggingMixIn, Operations):
  109     """
  110     FUSE filesystem to read data from a Tardis Backup Database
  111     """
  112     # Disable pylint complaints about "could me a function" and "unused argument" as lots of required FUSE functions
  113     # just return "read-only FS" status
  114     # pragma pylint: disable=no-self-use,unused-argument
  115     backupsets = {}
  116     dirInfo = {}
  117     fsencoding = sys.getfilesystemencoding()
  118     name = "TardisFS"
  119 
  120     client   = Defaults.getDefault('TARDIS_CLIENT')
  121     database = Defaults.getDefault('TARDIS_DB')
  122     dbdir    = Defaults.getDefault('TARDIS_DBDIR') % { 'TARDIS_DB': database }          # HACK
  123     dbname   = Defaults.getDefault('TARDIS_DBNAME')
  124     current  = Defaults.getDefault('TARDIS_RECENT_SET')
  125 
  126 
  127     def __init__(self, db, cache, crypto, args):
  128         self.cacheDir = cache
  129         self.crypt = crypto
  130         self.tardis = db
  131 
  132         # Create a regenerator.
  133         self.regenerator = Regenerator.Regenerator(self.cacheDir, self.tardis, crypt=self.crypt)
  134         self.files = {}
  135 
  136         # Set up some caches.
  137         self.cachetime  = 60
  138 
  139         self.cache      = Cache.Cache(0, float(self.cachetime))
  140         self.fileCache  = Cache.Cache(0, float(self.cachetime), 'FileCache')
  141 
  142         self.authenticate = True
  143 
  144 
  145     def __del__(self):
  146         if self.tardis:
  147             self.tardis.close()
  148 
  149     def __repr__(self):
  150         return self.name
  151 
  152     def fsEncodeName(self, name):
  153         return name
  154 
  155     def getBackupSetInfo(self, b):
  156         key = (_BackupSetInfo, b)
  157         info = self.cache.retrieve(key)
  158         if info:
  159             return info
  160         info = self.tardis.getBackupSetInfo(b)
  161         self.cache.insert(key, info)
  162         return info
  163 
  164     def lastBackupSet(self, completed):
  165         key = (_LastBackupSet, completed)
  166         backupset = self.cache.retrieve(key)
  167         if backupset:
  168             return backupset
  169         backupset = self.tardis.lastBackupSet(completed=completed)
  170         self.cache.insert(key, backupset)
  171         return backupset
  172 
  173     def getDirInfo(self, path):
  174         """ Return the inode and backupset of a directory """
  175         #self.log.info("getDirInfo: %s", path)
  176         key = (_DirInfo, path)
  177         info = self.cache.retrieve(key)
  178         if info:
  179             return info
  180 
  181         #self.log.debug("No cache info available for %s", path)
  182         parts = getParts(path)
  183         bsInfo = self.getBackupSetInfo(parts[0])
  184         if len(parts) == 2:
  185             subpath = parts[1]
  186             if self.crypt:
  187                 subpath = self.crypt.encryptPath(subpath)
  188             #fInfo = self.getFileInfoByPath(subpath, bsInfo['backupset'])
  189             fInfo = self.getFileInfoByPath(path)
  190             #self.log.info("fInfo %s %s %s", parts[1], "**", str(fInfo))
  191             info = (bsInfo, fInfo)
  192         else:
  193             fInfo = {'inode': 0, 'device': 0, 'dir': 1}
  194             info = (bsInfo, fInfo)
  195 
  196         if info:
  197             self.cache.insert(key, info)
  198         return info
  199 
  200     def getFileInfoByPath(self, path):
  201         #self.log.info("getFileInfoByPath: %s", path)
  202 
  203         # First, check the cache
  204         f = self.fileCache.retrieve(path)
  205         if f:
  206             #self.log.debug("getFileInfoByPath: %s found in cache", path)
  207             return f
  208 
  209         # Not in the cache, look things up
  210         #self.log.debug("File info for %s not in cache", path)
  211         (head, tail) = os.path.split(path)
  212         data = self.getDirInfo(head)
  213         if data:
  214             bsInfo, dInfo = data
  215         else:
  216             return None
  217 
  218         if bsInfo:
  219             if self.crypt:
  220                 tail = self.crypt.encryptPath(tail)
  221             #self.log.debug(str(dInfo))
  222             f = self.tardis.getFileInfoByName(tail, (dInfo['inode'], dInfo['device']), bsInfo['backupset'])
  223         else:
  224             parts = getParts(path)
  225             b = self.getBackupSetInfo(parts[0])
  226             subpath = parts[1]
  227             if self.crypt:
  228                 subpath = self.crypt.encryptPath(subpath)
  229             #self.log.debug("getFileInfoByPath: %s=>%s", parts[1], subpath)
  230             f = self.tardis.getFileInfoByPath(subpath, b['backupset'])
  231         # Cache it.
  232         self.fileCache.insert(path, f)
  233         # Return it
  234         return f
  235 
  236     #@tracer
  237     def getattr(self, path, fh=None):
  238         """
  239         - st_mode (protection bits)
  240         - st_ino (inode number)
  241         - st_dev (device)
  242         - st_nlink (number of hard links)
  243         - st_uid (user ID of owner)
  244         - st_gid (group ID of owner)
  245         - st_size (size of file, in bytes)
  246         - st_atime (time of most recent access)
  247         - st_mtime (time of most recent content modification)
  248         - st_ctime (platform dependent; time of most recent metadata change on Unix,
  249                     or the time of creation on Windows).
  250         """
  251 
  252         #self.log.info("CALL getattr: %s",  path)
  253         path = self.fsEncodeName(path)
  254 
  255         depth = getDepth(path) # depth of path, zero-based from root
  256         if depth == 0:
  257             # Fake the root
  258             target = self.lastBackupSet(False)
  259             timestamp = float(target['starttime'])
  260             st = {
  261                 'st_mode': stat.S_IFDIR | 0o555,
  262                 'st_ino': 0,
  263                 'st_dev': 0,
  264                 'st_nlink': 32,
  265                 'st_uid': 0,
  266                 'st_gid': 0,
  267                 'st_size': 4096,
  268                 'st_atime': timestamp,
  269                 'st_mtime': timestamp,
  270                 'st_ctime': timestamp,
  271             }
  272             return st
  273         elif depth == 1:
  274             # Root directory contents
  275             lead = getParts(path)
  276             if lead[0] == self.current:
  277                 target = self.lastBackupSet(True)
  278                 timestamp = float(target['endtime'])
  279                 st = {
  280                     'st_mode': stat.S_IFLNK | 0o755,
  281                     'st_ino': 1,
  282                     'st_dev': 0,
  283                     'st_nlink': 1,
  284                     'st_uid': 0,
  285                     'st_gid': 0,
  286                     'st_size': 4096,
  287                     'st_atime': timestamp,
  288                     'st_mtime': timestamp,
  289                     'st_ctime': timestamp
  290                 }
  291                 return st
  292             else:
  293                 f = self.getBackupSetInfo(lead[0])
  294                 #self.log.debug("Got backupset info for %s: %s", lead[0], str(f))
  295                 if f:
  296                     timestamp = float(f['starttime'])
  297                     st = {
  298                         'st_mode': stat.S_IFDIR | 0o555,
  299                         'st_ino': int(float(f['starttime'])),
  300                         'st_dev': 0,
  301                         'st_nlink': 2,
  302                         'st_uid': 0,
  303                         'st_gid': 0,
  304                         'st_size': 4096,
  305                         'st_atime': timestamp,
  306                         'st_mtime': timestamp,
  307                         'st_ctime': timestamp
  308                     }
  309                     return st
  310         else:
  311             f = self.getFileInfoByPath(path)
  312             if f:
  313                 st = {
  314                     'st_mode': f["mode"],
  315                     'st_ino': f["inode"],
  316                     'st_dev': 0,
  317                     'st_nlink': f["nlinks"],
  318                     'st_uid': f["uid"],
  319                     'st_gid': f["gid"],
  320                     'st_atime': f["mtime"],
  321                     'st_mtime': f["mtime"],
  322                     'st_ctime': f["ctime"]
  323                 }
  324                 if f["size"] is not None:
  325                     st['st_size'] = int(f["size"])
  326                 elif f["dir"]:
  327                     st['st_size'] = 4096       # Arbitrary number
  328                 else:
  329                     st['st_size'] = 0
  330                 return st
  331         logger.debug("File not found: %s", path)
  332         raise FuseOSError(errno.ENOENT)
  333 
  334     #@tracer
  335     #def getdir(self, _, fh):
  336         #"""
  337         #return: [[('file1', 0), ('file2', 0), ... ]]
  338         #"""
  339         ##self.log.info('CALL getdir {}'.format(path))
  340         #raise FuseOSError(errno.ENOSYS)
  341 
  342     #@tracer
  343     def readdir(self, path, offset):
  344         #self.log.info("CALL readdir %s Offset: %d", path, offset)
  345         parent = None
  346 
  347         path = self.fsEncodeName(path)
  348 
  349         key = (_DirContents, path)
  350         dirents = self.cache.retrieve(key)
  351         if not dirents:
  352             dirents = ['.', '..']
  353             depth = getDepth(path)
  354             if depth == 0:
  355                 dirents.append(self.current)
  356                 entries = self.tardis.listBackupSets()
  357                 dirents.extend([y['name'] for y in entries])
  358             else:
  359                 parts = getParts(path)
  360                 if depth == 1:
  361                     b = self.getBackupSetInfo(parts[0])
  362                     entries = self.tardis.readDirectory((0, 0), b['backupset'])
  363                 else:
  364                     (b, parent) = self.getDirInfo(path)
  365                     entries = self.tardis.readDirectory((parent["inode"], parent["device"]), b['backupset'])
  366                 #if self.crypt:
  367                     #entries = self.decryptNames(entries)
  368 
  369                 # For each entry, cache it, so a later getattr() call can use it.
  370                 # Get attr will typically be called promptly after a call to
  371                 now = time.time()
  372                 for e in entries:
  373                     name  = e['name']
  374                     if self.crypt:
  375                         name = self.crypt.decryptFilename(name)
  376                     name = self.fsEncodeName(name)
  377                     p = os.path.join(path, name)
  378                     self.fileCache.insert(p, e, now=now)
  379                     dirents.append(name)
  380             self.cache.insert(key, dirents)
  381 
  382         #self.log.debug("Direntries: %s", str(dirents))
  383 
  384         # Now, return each entry in the list.
  385         for e in dirents:
  386             name = e
  387             #self.log.debug("readdir %s yielding dir entry for %s.  Mode: %s. Type: %s ", path, e, mode, type(mode))
  388             yield name
  389 
  390     #@tracer
  391     def mythread ( self ):
  392         #self.log.info('mythread')
  393         raise FuseOSError(errno.ENOSYS)
  394 
  395     #@tracer
  396     def chmod ( self, path, mode ):
  397         #self.log.info('CALL chmod {} {}'.format(path, oct(mode)))
  398         raise FuseOSError(errno.EROFS)
  399 
  400     #@tracer
  401     def chown ( self, path, uid, gid ):
  402         #self.log.info( 'CALL chown {} {} {}'.format(path, uid, gid))
  403         raise FuseOSError(errno.EROFS)
  404 
  405     #@tracer
  406     def fsync ( self, path, isFsyncFile ):
  407         #self.log.info( 'CALL fsync {} {}'.format(path, isFsyncFile))
  408         raise FuseOSError(errno.EROFS)
  409 
  410     #@tracer
  411     def link ( self, targetPath, linkPath ):
  412         #self.log.info( 'CALL link {} {}'.format(targetPath, linkPath))
  413         raise FuseOSError(errno.EROFS)
  414 
  415     #@tracer
  416     def mkdir ( self, path, mode ):
  417         #self.log.info( 'CALL mkdir {} {}'.format(path, oct(mode)))
  418         raise FuseOSError(errno.EROFS)
  419 
  420     #@tracer
  421     def mknod ( self, path, mode, dev ):
  422         #self.log.info( 'CALL mknod {} {} {}'.format(path, oct(mode), dev))
  423         raise FuseOSError(errno.EROFS)
  424 
  425     #@tracer
  426     def open ( self, path, flags ):
  427         #self.log.info('CALL open {} {})'.format(path, flags))
  428         path = self.fsEncodeName(path)
  429 
  430         depth = getDepth(path) # depth of path, zero-based from root
  431 
  432         if depth < 2:
  433             raise FuseOSError(errno.ENOENT)
  434 
  435         # TODO: Lock this
  436         if path in self.files:
  437             self.files[path]["opens"] += 1
  438             return 0
  439 
  440         parts = getParts(path)
  441         b = self.getBackupSetInfo(parts[0])
  442         if b:
  443             subpath = parts[1]
  444             if self.crypt:
  445                 subpath = self.crypt.encryptPath(subpath)
  446             f = self.regenerator.recoverFile(subpath, b['backupset'], nameEncrypted=True, authenticate=self.authenticate)
  447             if f:
  448                 logger.debug("Opened file %s", path)
  449                 try:
  450                     f.flush()
  451                     f.seek(0)
  452                 except (AttributeError, IOError) as e:
  453                     logger.exception(e)
  454                     bytesCopied = 0
  455                     logger.debug("Copying file to tempfile")
  456                     temp = tempfile.TemporaryFile()
  457                     chunk = f.read(65536)
  458                     while chunk:
  459                         bytesCopied = bytesCopied + len(chunk)
  460                         temp.write(chunk)
  461                         chunk = f.read(65536)
  462                     f.close()
  463                     logger.debug("Copied %d bytes to tempfile", bytesCopied)
  464                     temp.flush()
  465                     temp.seek(0)
  466                     f = temp
  467 
  468                 self.files[path] = {"file": f, "opens": 1}
  469                 logger.debug("Set files[%s] => %s", path, str(self.files[path]))
  470                 return 0
  471         # Otherwise.....
  472         raise FuseOSError(errno.ENOENT)
  473 
  474 
  475     #@tracer
  476     def read ( self, path, length, offset, fh ):
  477         #self.log.info('CALL read {} {} {}'.format(path, length, offset))
  478         path = self.fsEncodeName(path)
  479         f = self.files[path]["file"]
  480         if f:
  481             f.seek(offset)
  482             data = f.read(length)
  483             logger.debug("Actually read %d bytes of %s", len(data), type(data))
  484             return data
  485         logger.warning("No file for path %s", path)
  486         raise FuseOSError(errno.EINVAL)
  487 
  488     #@tracer
  489     def readlink ( self, path ):
  490         #self.log.info('CALL readlink {}'.format(path))
  491         path = self.fsEncodeName(path)
  492 
  493         key = (_LinkContents, path)
  494         link = self.cache.retrieve(key)
  495         if link:
  496             return link
  497         if path == '/' + self.current:
  498             target = self.lastBackupSet(True)
  499             logger.debug("Path: %s Target: %s %s", path, target['name'], target['backupset'])
  500             link = str(target['name'])
  501             self.cache.insert(key, link)
  502             return link
  503         elif getDepth(path) > 1:
  504             parts = getParts(path)
  505             b = self.getBackupSetInfo(parts[0])
  506             if b:
  507                 subpath = parts[1]
  508                 if self.crypt:
  509                     subpath = self.crypt.encryptPath(subpath)
  510                 f = self.regenerator.recoverFile(subpath, b['backupset'], nameEncrypted=True, authenticate=self.authenticate)
  511                 f.flush()
  512                 link = f.readline().decode(self.fsencoding, errors='backslashreplace')
  513                 f.close()
  514                 #if self.repoint:
  515                 #    if os.path.isabs(link):
  516                 #        link = os.path.join(self.mountpoint, parts[0], os.path.relpath(link, "/"))
  517                 self.cache.insert(key, link)
  518                 return link
  519         raise FuseOSError(errno.ENOENT)
  520 
  521     #@tracer
  522     def release ( self, path, flags ):
  523         path = self.fsEncodeName(path)
  524 
  525         if self.files[path]:
  526             self.files[path]["opens"] -= 1
  527             if self.files[path]["opens"] == 0:
  528                 self.files[path]["file"].close()
  529                 del self.files[path]
  530             return 0
  531         raise FuseOSError(errno.EINVAL)
  532 
  533     #@tracer
  534     def rename ( self, oldPath, newPath ):
  535         #self.log.info('CALL rename {} {}'.format(oldPath, newPath))
  536         raise FuseOSError(errno.EROFS)
  537 
  538     #@tracer
  539     def rmdir ( self, path ):
  540         #self.log.info('CALL rmdir {}'.format(path))
  541         raise FuseOSError(errno.EROFS)
  542 
  543     #@tracer
  544     def statfs ( self, path ):
  545         #self.log.info('CALL statfs: %s', path)
  546         if isinstance(self.cacheDir, CacheDir.CacheDir):
  547             fs = os.statvfs(self.cacheDir.root)
  548 
  549             return dict((key, getattr(fs, key)) for key in (
  550                 'f_bavail', 'f_bfree', 'f_blocks', 'f_bsize', 'f_favail',
  551                 'f_ffree', 'f_files', 'f_flag', 'f_frsize', 'f_namemax'))
  552         raise FuseOSError(errno.EINVAL)
  553 
  554     def symlink ( self, targetPath, linkPath ):
  555         #self.log.info('CALL symlink {} {}'.format(path, linkPath))
  556         raise FuseOSError(errno.EROFS)
  557 
  558     def truncate ( self, path, size ):
  559         #self.log.info('CALL truncate {} {}'.format(path, size))
  560         raise FuseOSError(errno.EROFS)
  561 
  562     def unlink ( self, path ):
  563         #self.log.info('CALL unlink {}'.format(path))
  564         raise FuseOSError(errno.EROFS)
  565 
  566     def write ( self, path, buf, offset ):
  567         #self.log.info('CALL write {} {} {}'.format(path, offset, len(buf)))
  568         raise FuseOSError(errno.EROFS)
  569 
  570     # Map extrenal attribute names for the top level directories to backupset info names
  571     attrMap = {
  572         'user.priority' : 'priority',
  573         'user.complete' : 'completed',
  574         'user.backupset': 'backupset',
  575         'user.session'  : 'session'
  576     }
  577 
  578     #@tracer
  579     #def listxattr ( self, path, size ):
  580     def listxattr(self, path):
  581         path = self.fsEncodeName(path)
  582         #self.log.info('CALL listxattr %s %d', path, size)
  583         if getDepth(path) == 1:
  584             parts = getParts(path)
  585             b = self.getBackupSetInfo(parts[0])
  586             if b:
  587                 return list(self.attrMap.keys())
  588 
  589         if getDepth(path) > 1:
  590             parts = getParts(path)
  591             b = self.getBackupSetInfo(parts[0])
  592             if b:
  593                 subpath = parts[1]
  594                 if self.crypt:
  595                     subpath = self.crypt.encryptPath(subpath)
  596                 info = self.tardis.getFileInfoByPath(subpath, b['backupset'])
  597                 if info:
  598                     attrs = ['user.tardis_checksum', 'user.tardis_since', 'user.tardis_chain']
  599                     logger.info("xattrs: %s", info['xattrs'])
  600                     if info['xattrs']:
  601                         f = self.regenerator.recoverChecksum(info['xattrs'], authenticate=self.authenticate)
  602                         xattrs = json.loads(f.read())
  603                         logger.debug("Xattrs: %s", str(xattrs))
  604                         attrs += list(map(str, list(xattrs.keys())))
  605                         logger.debug("Adding xattrs: %s", list(xattrs.keys()))
  606                         logger.info("Xattrs: %s", str(attrs))
  607                         logger.info("Returning: %s", str(attrs))
  608 
  609                     return attrs
  610 
  611         return None
  612 
  613     #@tracer
  614     #def getxattr (self, path, attr, size, *args):
  615     def getxattr(self, path, attr, position=0):
  616         path = self.fsEncodeName(path)
  617         #logger.info('CALL getxattr: %s %s', path, attr)
  618         attr = str(attr)
  619 
  620         depth = getDepth(path)
  621         #logger.info("Got depth of path %s -> %s", path, depth)
  622 
  623         if depth == 1:
  624             if attr in self.attrMap:
  625                 parts = getParts(path)
  626                 b = self.getBackupSetInfo(parts[0])
  627                 if self.attrMap[attr] in list(b.keys()):
  628                     return bytes(str(b[self.attrMap[attr]]), 'utf-8')
  629 
  630         if depth > 1:
  631             parts = getParts(path)
  632             b = self.getBackupSetInfo(parts[0])
  633 
  634             subpath = parts[1]
  635             if self.crypt:
  636                 subpath = self.crypt.encryptPath(subpath)
  637             #logger.debug("-----> Asking for attribute %s", attr)
  638             if attr == 'user.tardis_checksum':
  639                 if b:
  640                     checksum = self.tardis.getChecksumByPath(subpath, b['backupset'])
  641                     #logger.debug("Got checksum {}", str(checksum))
  642                     if checksum:
  643                         return bytes(str(checksum), 'utf-8')
  644             elif attr == 'user.tardis_since':
  645                 if b:
  646                     since = self.tardis.getFirstBackupSet(subpath, b['backupset'])
  647                     #self.log.debug(str(since))
  648                     if since:
  649                         return bytes(str(since), 'utf-8')
  650             elif attr == 'user.tardis_chain':
  651                 info = self.tardis.getChecksumInfoByPath(subpath, b['backupset'])
  652                 #self.log.debug(str(checksum))
  653                 if info:
  654                     chain = info['chainlength']
  655                     return bytes(str(chain), 'utf-8')
  656             else:
  657                 # Must be an imported value.  Let's generate it.
  658                 info = self.getFileInfoByPath(path)
  659                 if info['xattrs']:
  660                     f = self.regenerator.recoverChecksum(info['xattrs'], authenticate=self.authenticate)
  661                     xattrs = json.loads(f.read())
  662                     if attr in xattrs:
  663                         value = base64.b64decode(xattrs[attr])
  664                         return bytes(str(value), 'utf-8')
  665 
  666         #self.log.debug("Getxattr -- default return value")
  667         return bytes('', 'utf-8')
  668 
  669 def processMountOpts(mountopts):
  670     kwargs = {}
  671     if mountopts:
  672         for i in mountopts:
  673             opts = i.split(',')
  674             for j in opts:
  675                 x = j.split('=', 1)
  676                 if len(x) == 1:
  677                     kwargs[x[0]] = True
  678                 else:
  679                     kwargs[x[0]] = x[1]
  680     return kwargs
  681 
  682 def processArgs():
  683     parser = argparse.ArgumentParser(description='Mount a FUSE filesystem containing tardis backup data', add_help = False, fromfile_prefix_chars='@')
  684 
  685     (_, remaining) = Config.parseConfigOptions(parser)
  686     Config.addCommonOptions(parser)
  687     Config.addPasswordOptions(parser)
  688 
  689     parser.add_argument('-o',               dest='mountopts', action='append',help='Standard mount -o options')
  690     parser.add_argument('-d',               dest='debug', action='store_true', default=False, help='Run in FUSE debug mode')
  691     parser.add_argument('-f',               dest='foreground', action='store_true', default=False, help='Remain in foreground')
  692 
  693     parser.add_argument('--verbose', '-v',  dest='verbose', action='count', default=0, help="Increase verbosity")
  694     parser.add_argument('--version',            action='version', version='%(prog)s ' + Tardis.__versionstring__,    help='Show the version')
  695     parser.add_argument('--help', '-h',     action='help')
  696 
  697     parser.add_argument('mountpoint',       nargs=1, help="List of directories to sync")
  698 
  699     Util.addGenCompletions(parser)
  700 
  701     args = parser.parse_args(remaining)
  702 
  703     return args
  704 
  705 def delTardisKeys(kwargs):
  706     keys = ['password', 'pwfile', 'pwprog', 'database', 'client', 'keys', 'dbname', 'dbdir']
  707     for i in keys:
  708         kwargs.pop(i, None)
  709 
  710 def main():
  711     global logger
  712     args = processArgs()
  713     kwargs = processMountOpts(args.mountopts)
  714     logger = Util.setupLogging(args.verbose)
  715 
  716     try:
  717         argsDict = vars(args)
  718         def getarg(name):
  719             """ Extract a value from either the kwargs, or the regular args """
  720             return kwargs.get(name) or argsDict.get(name)
  721 
  722         # Extract the password file and program, if they exist.  Names differ, so getarg doesn't work.
  723         pwfile = kwargs.get('pwfile') or argsDict.get('passwordfile')
  724         pwprog = kwargs.get('pwprog') or argsDict.get('passwordprog')
  725 
  726         password = Util.getPassword(getarg('password'), pwfile, pwprog, prompt="Password for %s: " % (getarg('client')))
  727         args.password = None
  728         (tardis, cache, crypt) = Util.setupDataConnection(getarg('database'), getarg('client'), password, getarg('keys'), getarg('dbname'), getarg('dbdir'))
  729     except TardisDB.AuthenticationException as e:
  730         logger.error("Authentication failed.  Bad password")
  731         #if args.exceptions:
  732             #logger.exception(e)
  733         sys.exit(1)
  734     except Exception as e:
  735         logger.error("DB Connection failed: %s", e)
  736         sys.exit(1)
  737 
  738     delTardisKeys(kwargs)
  739 
  740     fs = TardisFS(tardis, cache, crypt, args)
  741     FUSE(fs, args.mountpoint[0], debug=args.debug, nothreads=True, foreground=args.foreground, **kwargs)
  742 
  743 if __name__ == "__main__":
  744     main()