#!/usr/bin/python

# Copyright (c) 2009 Peter Palfrader
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

import errno
import fuse
import os
import psycopg2
import psycopg2.extras
import stat
import sys
import yaml
sys.path.append(os.path.abspath(os.path.dirname(sys.argv[0]))+'/../lib')
from dbhelper import DBHelper

fuse.fuse_python_api = (0, 2)

class StorageBackend:
   def store(self, source, digest):
      raise "Not implemented"

   def retreive(self, digest):
      raise "Not implemented"

class FileBackend(StorageBackend):
   def __init__(self, config):
      for kw in ('farmpath',):
         if not kw in config:
            sys.stderr.write("Snapshot config file does not define %s keywords\n"%kw)
            sys.exit(1)
      self.farmpath = config['farmpath']

   def make_symlink(self, digest):
      prefix1 = digest[0:2]
      prefix2 = digest[2:4]
      return os.path.join(self.farmpath, prefix1, prefix2, digest)


class MyStat(fuse.Stat):
   def __init__(self):
      self.st_ino = 0
      self.st_dev = 0
      self.st_uid = 0
      self.st_gid = 0
      self.st_atime = 0
      self.st_mtime = 0
      self.st_ctime = 0

      self.st_mode = 0
      self.st_size = 0
      self.st_nlink = 1

class DirStat(MyStat):
   def __init__(self, subdirs=None, ctime=0, mtime=0, atime=0):
      MyStat.__init__(self)
      self.st_mode = stat.S_IFDIR | 0755
      if subdirs is None:
         self.st_nlink = 1 # unknown
      else:
         self.st_nlink = 2+subdirs
      self.st_size = 4096
      self.st_ctime = ctime
      self.st_mtime = mtime
      self.st_atime = atime

class SymlinkStat(MyStat):
   def __init__(self, len, ctime=0, atime=0):
      MyStat.__init__(self)
      self.st_mode = stat.S_IFLNK | 0777
      self.st_size = len
      self.st_ctime = ctime
      self.st_mtime = ctime
      self.st_atime = atime

class FileStat(MyStat):
   def __init__(self):
      MyStat.__init__(self)
      self.st_mode = stat.S_IFREG | 0644


def split_path(path):
   path_elements = path.split('/',2)[1:]
   if len(path_elements) == 1:
      subpath = '/'
   else:
      subpath = '/'+path_elements[1]
   return(path_elements[0], subpath)

class SnapshotFile:
   def __init__(self, db, parent_dir, timestamp, name):
      self.db = db
      self.parent_dir = parent_dir
      self.timestamp = timestamp
      self.name = name

      list = self.db.query("""SELECT hash,
                                     extract(epoch from first_run) as ctime,
                                     extract(epoch from last_run) as atime
                              FROM file NATURAL JOIN node_with_ts
                              WHERE parent=%(parent_dir)s
                                AND first_run <= %(timestamp)s
                                AND last_run  >= %(timestamp)s
                                AND name = %(name)s
                           """,
                           {'parent_dir': self.parent_dir,
                            'timestamp': self.timestamp,
                            'name': name});
      if len(list) == 0:
         raise IOError(errno.ENOENT, 'No such directory')
      elif len(list) > 1:
         raise IOError(errno.EIO, 'Multiple hits for regular file - FS corrupt?')

      self.hash = list[0]['hash']
      self.ctime = list[0]['ctime']
      self.atime = list[0]['atime']
      self.target = storage.make_symlink(self.hash)

   def getattr(self):
      return SymlinkStat(len(self.target), self.ctime, self.atime)
   def readlink(self):
      return self.target


class SnapshotSymlink:
   def __init__(self, db, parent_dir, timestamp, name):
      self.db = db
      self.parent_dir = parent_dir
      self.timestamp = timestamp
      self.name = name

      list = self.db.query("""SELECT target,
                                     extract(epoch from first_run) as ctime,
                                     extract(epoch from last_run) as atime
                              FROM symlink NATURAL JOIN node_with_ts
                              WHERE parent=%(parent_dir)s
                                AND first_run <= %(timestamp)s
                                AND last_run  >= %(timestamp)s
                                AND name = %(name)s
                           """,
                           {'parent_dir': self.parent_dir,
                            'timestamp': self.timestamp,
                            'name': name});
      if len(list) == 0:
         raise IOError(errno.ENOENT, 'No such directory')
      elif len(list) > 1:
         raise IOError(errno.EIO, 'Multiple hits for symlink - FS corrupt?')

      self.target = list[0]['target']
      self.ctime = list[0]['ctime']
      self.atime = list[0]['atime']

   def getattr(self):
      return SymlinkStat(len(self.target), self.ctime, self.atime)
   def readlink(self):
      return self.target


class SnapshotDirectory:
   def __init__(self, db, archive_id, timestamp, path):
      self.db = db
      self.archive_id = archive_id
      self.timestamp = timestamp
      self.path = path

      list = self.db.query("""SELECT directory_id,
                                     extract(epoch from first_run) as ctime,
                                     extract(epoch from timestamp without time zone %(timestamp)s) as mtime,
                                     extract(epoch from last_run) as atime
                              FROM directory NATURAL JOIN node_with_ts
                              WHERE path=%(path)s
                                AND archive_id = %(archive_id)s
                                AND first_run <= %(timestamp)s
                                AND last_run  >= %(timestamp)s""",
                           {'archive_id': self.archive_id,
                            'path': self.path,
                            'timestamp': self.timestamp})
      if len(list) == 0:
         raise IOError(errno.ENOENT, 'No such directory')
      elif len(list) > 1:
         raise IOError(errno.EIO, 'Multiple hits for directory - FS corrupt?')

      self.directory_id = list[0]['directory_id']
      self.ctime = list[0]['ctime']
      self.mtime = list[0]['mtime']
      self.atime = list[0]['atime']

   def getattr(self, name=None):
      if name is None:
         # the getattr is about the directory itself
         st = DirStat(None, self.ctime, self.mtime, self.atime)
         return st
      else:
         # not this directory but a element (not a subdir) in it

         # so maybe it's a regular file
         try:
            return SnapshotFile(self.db, self.directory_id, self.timestamp, name).getattr()
         except IOError, e:
            if e.errno != errno.ENOENT: raise

         # or maybe it's a symlink:
         try:
            return SnapshotSymlink(self.db, self.directory_id, self.timestamp, name).getattr()
         except IOError, e:
            if e.errno != errno.ENOENT: raise

         return -errno.ENOENT

   def readdir(self, offset):
      # all subdirs:
      subdirs = self.db.query_firsts("""SELECT path
                                        FROM directory NATURAL JOIN node_with_ts
                                        WHERE parent=%(parent_dir)s
                                          AND directory_id <> parent
                                          AND first_run <= %(timestamp)s
                                          AND last_run  >= %(timestamp)s
                                     """,
                                    # the / directory has itself listed as parent - we never want to
                                    # get that returned
                                     {'parent_dir': self.directory_id,
                                      'timestamp': self.timestamp});
      others = self.db.query_firsts("""  SELECT name FROM file NATURAL JOIN node_with_ts
                                         WHERE parent=%(parent_dir)s
                                           AND first_run <= %(timestamp)s
                                           AND last_run  >= %(timestamp)s
                                       UNION ALL
                                         SELECT name FROM symlink NATURAL JOIN node_with_ts
                                         WHERE parent=%(parent_dir)s
                                           AND first_run <= %(timestamp)s
                                           AND last_run  >= %(timestamp)s
                                    """,
                                    {'parent_dir': self.directory_id,
                                     'timestamp': self.timestamp});
      subdirs = map(lambda x: os.path.basename(x), subdirs)
      subdirs.extend(others)
      return subdirs

   def readlink(self, name):
         try:
            return SnapshotFile(self.db, self.directory_id, self.timestamp, name).readlink()
         except IOError, e:
            if e.errno != errno.ENOENT: raise

         try:
            return SnapshotSymlink(self.db, self.directory_id, self.timestamp, name).readlink()
         except IOError, e:
            if e.errno != errno.ENOENT: raise

         return -errno.ENOENT




class DynamicElement:
   def getattr(self, path):
      (first, subpath) = split_path(path)
      if first == '':
         return DirStat()
      try:
         return self._make_child(first).getattr(subpath)
      except IOError, e:
         if e.errno != errno.ENOENT: raise
      return -errno.ENOENT

   def readdir(self, path, offset):
      (first, subpath) = split_path(path)
      if first == '':
        return self.list_elements()

      try:
         return self._make_child(first).readdir(subpath, offset)
      except IOError, e:
         if e.errno != errno.ENOENT: raise
      return -errno.ENOENT

   def readlink(self, path):
      (first, subpath) = split_path(path)
      if first == '':
         return -errno.EINVAL

      try:
         return self._make_child(first).readlink(subpath)
      except IOError, e:
         if e.errno != errno.ENOENT: raise
      return -errno.ENOENT


class FSStaticElement:
   def __init__(self):
      self.elements = {}
      self.ctime = 0
      self.mtime = 0
      self.atime = 0

   def getattr(self, path):
      (first, subpath) = split_path(path)
      if first == '':
         return DirStat(len(self.elements), self.ctime, self.mtime, self.atime)
      elif first in self.elements:
         return self.elements[ first ].getattr(subpath)
      return -errno.ENOENT

   def readdir(self, path, offset):
      (first, subpath) = split_path(path)
      if first == '':
         return self.elements.keys()
      elif first in self.elements:
         return self.elements[ first ].readdir(subpath, offset)
      return -errno.ENOENT

   def readlink(self, path):
      (first, subpath) = split_path(path)
      if first == '':
         return -errno.EINVAL
      elif first in self.elements:
         return self.elements[ first ].readlink(subpath)
      return -errno.ENOENT

class MirrorRun:
   def __init__(self, db, archive_id, timestamp):
      self.db = db
      self.archive_id = archive_id
      self.timestamp = timestamp

   def getattr(self, path):
      try:
         return SnapshotDirectory(self.db, self.archive_id, self.timestamp, path).getattr()
      except IOError, e:
         if e.errno != errno.ENOENT: raise

      parentpath = os.path.dirname(path)
      basename = os.path.basename(path)
      return SnapshotDirectory(self.db, self.archive_id, self.timestamp, parentpath).getattr(basename)

   def readdir(self, path, offset):
      return SnapshotDirectory(self.db, self.archive_id, self.timestamp, path).readdir(offset)

   def readlink(self, path):
      parentpath = os.path.dirname(path)
      basename = os.path.basename(path)
      return SnapshotDirectory(self.db, self.archive_id, self.timestamp, parentpath).readlink(basename)


class Archive(FSStaticElement):
   def __init__(self, db, id, first_run, last_run):
      FSStaticElement.__init__(self)
      self.db = db
      self.id = id
      self.ctime = first_run
      self.mtime = last_run
      self.atime = last_run
      self.load_elements()

   def load_elements(self):
      list = self.db.query("""SELECT run, to_char(run, 'YYYYMMDD"T"HH24MISS') AS timestamp
                              FROM mirrorrun
                              WHERE archive_id=%(id)s""", {'id': self.id})
      for e in list:
         timestamp = e['run']
         pretty_timestamp = e['timestamp']
         self.elements[pretty_timestamp] = MirrorRun(self.db, self.id, timestamp)




class PackageFile:
   def __init__(self, db, hash):
      self.target = storage.make_symlink(hash)

   def getattr(self, path):
      if path == '':
         return -errno.ENOENT
      return SymlinkStat(len(self.target))

   def readlink(self, path):
      if path == '':
         return -errno.ENOENT
      return self.target


class BinaryPackage(DynamicElement):
   def __init__(self, db, binpkgid):
      self.db = db
      self.binpkgid = binpkgid

   def list_elements(self):
      return self.db.query_firsts(""" SELECT max(name) AS name
                                       FROM file JOIN file_binpkg_mapping
                                         ON file.hash = file_binpkg_mapping.hash
                                       WHERE binpkg_id=%(binpkgid)s
                                       GROUP BY file.hash""", {'binpkgid': self.binpkgid})

   def _make_child(self, child):
      row = self.db.query_one("""SELECT file.hash
                                   FROM file JOIN file_binpkg_mapping
                                     ON file.hash = file_binpkg_mapping.hash
                                   WHERE binpkg_id=%(binpkgid)s
                                     AND file.name=%(child)s
                                   LIMIT 1""", {'binpkgid': self.binpkgid, 'child': child})
      if not row is None:
         return PackageFile(self.db, row['hash'])

      raise IOError(errno.ENOENT, 'No such directory')

class SourcePackageVersion(DynamicElement):
   def __init__(self, db, name, version):
      self.db = db
      r = self.db.query_one("""SELECT srcpkg_id FROM srcpkg WHERE name=%(name)s AND version=%(version)s""", {'name': name, 'version': version})
      if r is None:
         raise IOError(errno.ENOENT, 'No such directory')
      self.srcpkgid = r['srcpkg_id']

   def list_elements(self):
      return self.db.query_firsts("""SELECT name FROM binpkg WHERE srcpkg_id=%(srcpkgid)s
                                     UNION ALL
                                     SELECT max(name) AS name
                                       FROM file JOIN file_srcpkg_mapping
                                         ON file.hash = file_srcpkg_mapping.hash
                                       WHERE srcpkg_id=%(srcpkgid)s
                                       GROUP BY file.hash""", {'srcpkgid': self.srcpkgid})

   def _make_child(self, child):
      row = self.db.query_one("""SELECT binpkg_id FROM binpkg WHERE srcpkg_id=%(srcpkgid)s AND name=%(child)s""", {'srcpkgid': self.srcpkgid, 'child': child})
      if not row is None:
         return BinaryPackage(self.db, row['binpkg_id'])

      row = self.db.query_one("""SELECT file.hash
                                   FROM file JOIN file_srcpkg_mapping
                                     ON file.hash = file_srcpkg_mapping.hash
                                   WHERE srcpkg_id=%(srcpkgid)s
                                     AND file.name=%(child)s
                                   LIMIT 1""", {'srcpkgid': self.srcpkgid, 'child': child})
      if not row is None:
         return PackageFile(self.db, row['hash'])

      raise IOError(errno.ENOENT, 'No such directory')

class SourceVersionsBySourceName(DynamicElement):
   def __init__(self, db, name):
      self.db = db
      self.name = name
      r = self.db.query_one("""SELECT count(*) AS count FROM srcpkg WHERE name=%(name)s""", {'name': name})
      if r['count'] == 0:
         raise IOError(errno.ENOENT, 'No such directory')

   def list_elements(self):
      return self.db.query_firsts("""SELECT version FROM srcpkg WHERE name=%(name)s""", {'name': self.name})

   def _make_child(self, version):
      return SourcePackageVersion(self.db, self.name, version)


class SourcePackagesByLetter(DynamicElement):
   def __init__(self, db, firstletter):
      self.db = db
      self.firstletter = firstletter

   def list_elements(self):
      return self.db.query_firsts(""" SELECT DISTINCT name FROM srcpkg WHERE substring(name from 1 for 1) = %(firstletter)s """,
                                  {'firstletter': self.firstletter})

   def _make_child(self, child):
      return SourceVersionsBySourceName(self.db, child)


class SourceFirstLetters(FSStaticElement):
   def __init__(self, db):
      FSStaticElement.__init__(self)
      self.db = db
      self.load_elements()

   def load_elements(self):
      for name in self.db.query_firsts("""SELECT DISTINCT substring(name from 1 for 1) FROM srcpkg"""):
         self.elements[name] = SourcePackagesByLetter(self.db, name)


class Root(FSStaticElement):
   def __init__(self, db):
      FSStaticElement.__init__(self)
      self.db = db
      result = self.db.query("""SELECT coalesce(extract(epoch FROM min(run)), 0) AS first_run,
                                       coalesce(extract(epoch FROM max(run)), 0) AS last_run
                                FROM mirrorrun""")
      self.ctime = result[0]['first_run']
      self.mtime = result[0]['last_run']
      self.atime = result[0]['last_run']
      self.load_elements()

   def load_elements(self):
      list = self.db.query("""SELECT archive_id, name,
                                     coalesce(extract(epoch FROM (SELECT min(run) FROM mirrorrun WHERE mirrorrun.archive_id=archive.archive_id)), 0) AS first_run,
                                     coalesce(extract(epoch FROM (SELECT max(run) FROM mirrorrun WHERE mirrorrun.archive_id=archive.archive_id)), 0) AS last_run
                              FROM archive""")
      for e in list:
         id = e['archive_id']
         name = e['name']
         first_run = e['first_run']
         last_run = e['last_run']
         self.elements[name] = Archive(self.db, id, first_run, last_run)
      self.elements['packages'] = SourceFirstLetters(self.db)


class SnapshotFS(fuse.Fuse):
   def __init__(self, db, *args, **kw):
      fuse.Fuse.__init__(self, *args, **kw)
      self.db = db
      self.reload_database()

   def reload_database(self):
      print "Reloading cached entries"
      self.root = Root(self.db)

   def getattr(self, path):
      if path == '/.ctrl-reload-database':
         self.reload_database()
         return FileStat()
      return self.root.getattr(path)

   def readdir(self, path, offset):
      dirents = [ '.', '..' ]
      dirents.extend( self.root.readdir(path, offset) )
      for r in dirents:
         yield fuse.Direntry(r)

   def readlink(self, path):
      return self.root.readlink(path)

def usage(err = False):
    if err:
        f = sys.stderr
        exit = 1
    else:
        f = sys.stdout
        exit = 0
    f.write("Usage: %s <config.yaml> [fuse options] <mountpoint>\n" % (sys.argv[0]))
    sys.exit(exit)

def readConfig(conffile):
    return yaml.load(open(conffile).read())

def main():
   if len(sys.argv) <= 1:
      usage(True)
   opt1 = sys.argv[1]
   del sys.argv[1]
   if opt1 == "-h" or opt1 == "--help":
      usage(False)

   config = readConfig(opt1)

   for kw in 'snapshot', 'db':
      if not kw in config:
         sys.stderr.write("Config file does not define %s keywords\n"%kw)
         sys.exit(1)

   if not 'connectstring' in config['db']:
      sys.stderr.write("Database config file does not define db->connectstring keywords\n")
      sys.exit(1)

   db = DBHelper(config['db']['connectstring'])
   db.execute('ROLLBACK')
   global storage
   storage = FileBackend(config['snapshot'])
   server = SnapshotFS(db, dash_s_do='setsingle')
   server.parse(errex=1)
   server.main()

if __name__ == '__main__':
   main()

# vim:set et:
# vim:set ts=3:
# vim:set shiftwidth=3:
