#!/usr/bin/env python
import os
import json
import urllib
import urllib2
import sys
import pwd
import errno
import fcntl
import logging
import time

from threading import Lock
from collections import deque

import fuse

log = logging.getLogger(__name__)

logging.basicConfig()

fuse.fuse_python_api = (0, 2)
fuse.feature_assert('stateful_files', 'has_init')

class check_access(object):

    def __init__(self, *args, **kwargs):
        self._args = args
        self._kwargs = kwargs

    def __call__(self, func):
        def wrapper(inst, *args, **kwargs):
            new_args = list(args)
            new_kwargs = dict(kwargs)
            for i, (mode, path) in enumerate(zip(self._args, args)):
                new_args[i] = self.check(inst, path, mode)
            for name, mode in self._kwargs.iteritems():
                new_kwargs[name] = self.check(inst, kwargs.get(name), mode)
            return func(inst, *new_args, **new_kwargs)
        return wrapper

    def check(self, inst, path, mode):
        if mode is None: return
        rc = inst.access(path, mode)
        if rc:
            raise OSError(errno.EPERM, path,'Permission denied')

class check_and_translate(check_access):

    def check(self, inst, path, mode):
        super(check_and_translate, self).check(inst, path, mode)
        return inst._to_global(path)

def flag2mode(flags):
    md = {os.O_RDONLY: 'r', os.O_WRONLY: 'w', os.O_RDWR: 'w+'}
    m = md[flags & (os.O_RDONLY | os.O_WRONLY | os.O_RDWR)]
    if flags | os.O_APPEND:
        m = m.replace('w', 'a', 1)
    return m

class AccessFS(fuse.Fuse):

    def __init__(self, *args, **kw):
        super(AccessFS, self).__init__(*args, **kw)
        self.root = '/'
        self.auth_method = 'unix'
        self.permission_host = 'http://localhost:8080'
        self.permission_cache_timeout = 30
        self.permission_cache_size = 1024
        self.file_class = self.make_file_class()
        self.perm_cache = None

    def getattr(self, path):
        return os.lstat("." + path)

    def readlink(self, path):
        self._assert_access(path, os.R_OK)
        return os.readlink("." + path)

    def readdir(self, path, offset):
        print 'Readdir!'
        for e in os.listdir("." + path):
            yield fuse.Direntry(e)

    def unlink(self, path):
        self._assert_access(path, os.W_OK)
        os.unlink("." + path)

    def rmdir(self, path):
        self._assert_access(path, os.W_OK)
        os.rmdir("." + path)

    def symlink(self, path, path1):
        self._assert_access(path, os.W_OK)
        os.symlink(path, "." + path1)

    def rename(self, path, path1):
        self._assert_access(path, os.R_OK | os.W_OK)
        self._assert_access(path1, os.R_OK | os.W_OK)
        os.rename("." + path, "." + path1)

    def link(self, path, path1):
        self._assert_access(path, os.R_OK)
        self._assert_access(path1, os.W_OK)
        os.link("." + path, "." + path1)

    def chmod(self, path, mode):
        self._assert_access(path, os.W_OK)
        os.chmod("." + path, mode)

    def chown(self, path, user, group):
        self._assert_access(path, os.W_OK)
        os.chown("." + path, user, group)

    def truncate(self, path, len):
        self._assert_access(path, os.W_OK)
        f = open("." + path, "a")
        f.truncate(len)
        f.close()

    def mknod(self, path, mode, dev):
        self._assert_access(path, os.W_OK)
        os.mknod("." + path, mode, dev)

    def mkdir(self, path, mode):
        self._assert_access(path, os.W_OK)
        os.mkdir("." + path, mode)

    def utime(self, path, times):
        os.utime("." + path, times)

    def access(self, path, mode):
        if mode & (os.R_OK|os.W_OK) == 0: return
        ctx = fuse.FuseGetContext()
        entry = self.perm_cache.get(ctx['uid'], path)
        if (mode & entry) != mode:
            return -errno.EACCES

    def _assert_access(self, path, mode):
        rc = self.access(path, mode)
        if rc:
            raise OSError(errno.EPERM, path,'Permission denied')

    def statfs(self):
        """
        Should return an object with statvfs attributes (f_bsize, f_frsize...).
        Eg., the return value of os.statvfs() is such a thing (since py 2.2).
        If you are not reusing an existing statvfs object, start with
        fuse.StatVFS(), and define the attributes.

        To provide usable information (ie., you want sensible df(1)
        output, you are suggested to specify the following attributes:

            - f_bsize - preferred size of file blocks, in bytes
            - f_frsize - fundamental size of file blcoks, in bytes
                [if you have no idea, use the same as blocksize]
            - f_blocks - total number of blocks in the filesystem
            - f_bfree - number of free blocks
            - f_files - total number of file inodes
            - f_ffree - nunber of free file inodes
        """

        return os.statvfs(".")

    def fsinit(self):
        uid_cache = UnixUsernameCache()
        self.perm_cache = PermissionCache(
            uid_cache,
            self.permission_host,
            self.permission_cache_timeout,
            self.permission_cache_size)
        os.chdir(self.root)

    def make_file_class(self):
        class FSAccessFile(AccessFile):
            filesystem=self
        return FSAccessFile

class AccessFile(fuse.FuseFileInfo):
    direct_io=False
    keep_cache = False
    needs_write = (
        os.O_WRONLY
        | os.O_RDWR
        | os.O_APPEND
        | os.O_CREAT
        | os.O_TRUNC )

    def __init__(self, path, flags, *mode):
        access_mode = os.R_OK
        if flags & self.needs_write:
            access_mode |= os.W_OK
        self.filesystem._assert_access(path, access_mode)
        self.file = os.fdopen(os.open("." + path, flags, *mode),
                              flag2mode(flags))
        self.fd = self.file.fileno()

    def read(self, length, offset):
        self.file.seek(offset)
        return self.file.read(length)

    def write(self, buf, offset):
        self.file.seek(offset)
        self.file.write(buf)
        return len(buf)

    def release(self, flags):
        self.file.close()

    def _fflush(self):
        if 'w' in self.file.mode or 'a' in self.file.mode:
            self.file.flush()

    def fsync(self, isfsyncfile):
        self._fflush()
        if isfsyncfile and hasattr(os, 'fdatasync'):
            os.fdatasync(self.fd)
        else:
            os.fsync(self.fd)

    def flush(self):
        self._fflush()
        # cf. xmp_flush() in fusexmp_fh.c
        os.close(os.dup(self.fd))

    def fgetattr(self):
        return os.fstat(self.fd)

    def ftruncate(self, len):
        self.file.truncate(len)

    def lock(self, cmd, owner, **kw):
        # The code here is much rather just a demonstration of the locking
        # API than something which actually was seen to be useful.

        # Advisory file locking is pretty messy in Unix, and the Python
        # interface to this doesn't make it better.
        # We can't do fcntl(2)/F_GETLK from Python in a platfrom independent
        # way. The following implementation *might* work under Linux.
        #
        # if cmd == fcntl.F_GETLK:
        #     import struct
        #
        #     lockdata = struct.pack('hhQQi', kw['l_type'], os.SEEK_SET,
        #                            kw['l_start'], kw['l_len'], kw['l_pid'])
        #     ld2 = fcntl.fcntl(self.fd, fcntl.F_GETLK, lockdata)
        #     flockfields = ('l_type', 'l_whence', 'l_start', 'l_len', 'l_pid')
        #     uld2 = struct.unpack('hhQQi', ld2)
        #     res = {}
        #     for i in xrange(len(uld2)):
        #          res[flockfields[i]] = uld2[i]
        #
        #     return fuse.Flock(**res)

        # Convert fcntl-ish lock parameters to Python's weird
        # lockf(3)/flock(2) medley locking API...
        op = { fcntl.F_UNLCK : fcntl.LOCK_UN,
               fcntl.F_RDLCK : fcntl.LOCK_SH,
               fcntl.F_WRLCK : fcntl.LOCK_EX }[kw['l_type']]
        if cmd == fcntl.F_GETLK:
            return -errno.EOPNOTSUPP
        elif cmd == fcntl.F_SETLK:
            if op != fcntl.LOCK_UN:
                op |= fcntl.LOCK_NB
        elif cmd == fcntl.F_SETLKW:
            pass
        else:
            return -errno.EINVAL

        fcntl.lockf(self.fd, op, kw['l_start'], kw['l_len'])

class PermissionCache(object):

    def __init__(self, uid_cache, host, timeout=30, size=1024):
        self._host = host
        self._timeout = timeout
        self._size = size
        self._data = {}
        self._entries = deque()
        self._lock = Lock()
        self._uid_cache = uid_cache

    def get(self, uid, path):
        try:
            entry, timestamp = self._data[uid, path]
            elapsed = time.time() - timestamp
            if elapsed > self._timeout:
                print 'Timeout!', elapsed
                uname = self._uid_cache.get(uid)
                entry = self._refresh_result(uid, path, self._api_lookup(uname, path))
                return entry
            return entry
        except KeyError:
            pass
        uname = self._uid_cache.get(uid)
        try:
            entry = self._api_lookup(uname, path)
        except:
            entry = 0
            log.exception('Error checking access for %s', path)
        self._save_result(uid, path, entry)
        return entry

    def _api_lookup(self, uname, path):
        if path.count('/') < 3:
            return os.R_OK
        path = self._mangle(path)
        url = (
            self._host
            + '/auth/repo_permissions?'
            + urllib.urlencode(dict(
                    repo_path=path,
                    username=uname)))
        print 'Checking access for %s at %s (%s)' % (uname, url, path)
        fp = urllib2.urlopen(url)
        result = json.load(fp)
        print result
        entry = 0
        if result['allow_read']: entry |= os.R_OK
        if result['allow_write']: entry |= os.W_OK
        return entry

    def _refresh_result(self, uid, path, value):
        with self._lock:
            if (uid,path) in self._data:
                self._data[uid, path] = (value, time.time())
            else:
                if len(self._data) >= self._size:
                    k = self._entries.popleft()
                    del self._data[k]
                self._data[uid, path] = (value, time.time())
                self._entries.append((uid, path))
        return value

    def _save_result(self, uid, path, value):
        with self._lock:
            if len(self._data) >= self._size:
                k = self._entries.popleft()
                del self._data[k]
            self._data[uid, path] = (value, time.time())
            self._entries.append((uid, path))

    def _mangle(self, path):
        '''Convert paths from the form /SCM/neighborhood/project/a/b/c to
        /SCM/project.neighborhood/a/b/c
        '''
        parts = [ p for p in path.split(os.path.sep) if p ]
        scm, nbhd, proj, rest = parts[0], parts[1], parts[2], parts[3:]
        parts = ['/SCM/%s.%s' % (proj, nbhd) ] + rest
        return '/'.join(parts)

class UnixUsernameCache(object):

    def __init__(self):
        self._cache = {}

    def get(self, uid):
        try:
            return self._cache[uid]
        except KeyError:
            pass
        uname = pwd.getpwuid(uid).pw_name
        self._cache[uid] = uname
        return uname

def main():

    usage = """
Userspace nullfs-alike: mirror the filesystem tree from some point on.

""" + fuse.Fuse.fusage

    server = AccessFS(version="%prog " + fuse.__version__,
                 usage=usage,
                 dash_s_do='setsingle')

    server.parser.add_option(mountopt="root", metavar="PATH", default='/',
                             help="mirror filesystem from under PATH [default: %default]")
    server.parse(values=server, errex=1)

    try:
        if server.fuse_args.mount_expected():
            os.chdir(server.root)
    except OSError:
        print >> sys.stderr, "can't enter root of underlying filesystem"
        sys.exit(1)

    server.main()


if __name__ == '__main__':
    main()
