# -*- test-case-name: twisted.vfs.test.test_sftp -*-

import os, time

import zope.interface

from twisted.python import components, log, util
from twisted.conch.avatar import ConchUser
from twisted.conch.interfaces import ISession, ISFTPFile
from twisted.conch.ssh.filetransfer import ISFTPServer, FileTransferServer
from twisted.conch.ssh.filetransfer import FXF_READ, FXF_WRITE, FXF_APPEND, FXF_CREAT, FXF_TRUNC, FXF_EXCL
from twisted.conch.ssh.filetransfer import SFTPError
from twisted.conch.ssh.filetransfer import FX_PERMISSION_DENIED, FX_FAILURE
from twisted.conch.ssh.filetransfer import FX_NO_SUCH_FILE, FX_OP_UNSUPPORTED
from twisted.conch.ssh.filetransfer import FX_NOT_A_DIRECTORY
from twisted.conch.ssh.filetransfer import FX_FILE_IS_A_DIRECTORY
from twisted.conch.ssh.filetransfer import FX_FILE_ALREADY_EXISTS
from twisted.conch.ssh import session
from twisted.conch.ls import lsLine
from twisted.internet import defer

from twisted.vfs import ivfs, pathutils

def translateErrors(function):
    """Decorator that catches VFSErrors and re-raises them as the corresponding
    SFTPErrors."""
    
    def f(*args, **kwargs):
        try:
            result = function(*args, **kwargs)
            if isinstance(result, defer.Deferred):
                result.addErrback(_ebtranslateErrors)
            return result
        except ivfs.PermissionError, e:
            raise SFTPError(FX_PERMISSION_DENIED, str(e))
        except ivfs.NotFoundError, e:
            raise SFTPError(FX_NO_SUCH_FILE, e.args[0])
        except ivfs.AlreadyExistsError, e:
            raise SFTPError(FX_FILE_ALREADY_EXISTS, e.args[0])
        except ivfs.VFSError, e:
            raise SFTPError(FX_FAILURE, str(e))
        except NotImplementedError, e:
            raise SFTPError(FX_OP_UNSUPPORTED, str(e))

    util.mergeFunctionMetadata(function, f)
    return f


def _ebtranslateErrors(failure):
    """This just re-raises the failure so that the translateErrors decorator
    around this errback can intercept it if it wants to."""
    failure.raiseException()
_ebtranslateErrors = translateErrors(_ebtranslateErrors)


class AdaptFileSystemUserToISFTP:

    zope.interface.implements(ISFTPServer)

    def __init__(self, avatar):
        self.avatar = avatar
        self.openFiles = {}
        self.openDirs = {}
        self.filesystem = avatar.filesystem

    def _setAttrs(self, path, attrs):
        """
        NOTE: this function assumes it runs as the logged-in user:
        i.e. under _runAsUser()
        """
        if attrs.has_key("uid") and attrs.has_key("gid"):
            os.lchown(path, attrs["uid"], attrs["gid"])
        if attrs.has_key("permissions"):
            os.chmod(path, attrs["permissions"])
        if attrs.has_key("atime") and attrs.has_key("mtime"):
            os.utime(path, (attrs["atime"]. attrs["mtime"]))

    def gotVersion(self, otherVersion, extData):
        return {}

    def openFile(self, filename, flags, attrs):
        createPlease = False
        exclusive = False
        openFlags = 0
        if flags & FXF_READ == FXF_READ and flags & FXF_WRITE == 0:
            openFlags = os.O_RDONLY
        if flags & FXF_WRITE == FXF_WRITE and flags & FXF_READ == 0:
            createPlease = True
            openFlags = os.O_WRONLY
        if flags & FXF_WRITE == FXF_WRITE and flags & FXF_READ == FXF_READ:
            createPlease = True
            openFlags = os.O_RDWR
        if flags & FXF_APPEND == FXF_APPEND:
            createPlease = True
            openFlags |= os.O_APPEND
        if flags & FXF_CREAT == FXF_CREAT:
            createPlease = True
            openFlags |= os.O_CREAT
        if flags & FXF_TRUNC == FXF_TRUNC:
            openFlags |= os.O_TRUNC
        if flags & FXF_EXCL == FXF_EXCL:
            exclusive = True

        # XXX Once we change readChunk/writeChunk we'll have to wrap
        # child in something that implements those.

        pathSegments = self.filesystem.splitPath(filename)
        dirname, basename = pathSegments[:-1], pathSegments[-1]
        parentNode = self.filesystem.fetch('/'.join(dirname))
        if createPlease:
            child = parentNode.createFile(basename, exclusive)
        elif parentNode.exists(basename):
            child = parentNode.child(basename)
            if not ivfs.IFileSystemLeaf.providedBy(child):
                raise SFTPError(FX_FILE_IS_A_DIRECTORY, filename)
        else:
            raise SFTPError(FX_NO_SUCH_FILE, filename)
        child.open(openFlags)
        return AdaptFileSystemLeafToISFTPFile(child)
    openFile = translateErrors(openFile)

    def removeFile(self, filename):
        node = self.filesystem.fetch(filename)
        if not ivfs.IFileSystemLeaf.providedBy(node):
            raise SFTPError(FX_FILE_IS_A_DIRECTORY, filename)
        node.remove()
    removeFile = translateErrors(removeFile)

    def renameFile(self, oldpath, newpath):
        try:
            targetNode = self.filesystem.fetch(newpath)
        except (ivfs.NotFoundError, KeyError):
            # Something with the new name already exists.
            pass
        else:
            if ivfs.IFileSystemContainer(targetNode, None):
                # The target node is a container.  We assume the caller means to
                # move the source node into the container rather than replace
                # it, and adjust newpath accordingly.
                newpath = self.filesystem.joinPath(
                    newpath, self.filesystem.basename(oldpath))
        old = self.filesystem.fetch(oldpath)
        old.rename(newpath)
    renameFile = translateErrors(renameFile)

    def makeDirectory(self, path, attrs):
        dirname  = self.filesystem.dirname(path)
        basename = self.filesystem.basename(path)
        return self.filesystem.fetch(dirname).createDirectory(basename)
    makeDirectory = translateErrors(makeDirectory)

    def removeDirectory(self, path):
        self.filesystem.fetch(path).remove()
    removeDirectory = translateErrors(removeDirectory)

    def openDirectory(self, path):
        directory = self.filesystem.fetch(path)
        if not ivfs.IFileSystemContainer.providedBy(directory):
            raise SFTPError(FX_NOT_A_DIRECTORY, path)
        class DirList:
            def __init__(self, iter):
                self.iter = iter
            def __iter__(self):
                return self

            def next(self):

                (name, attrs) = self.iter.next()

                class st:
                    pass

                s = st()
                s.st_mode   = attrs["permissions"]
                s.st_uid    = attrs["uid"]
                s.st_gid    = attrs["gid"]
                s.st_size   = attrs["size"]
                s.st_mtime  = attrs["mtime"]
                s.st_nlink  = attrs["nlink"]
                return ( name, lsLine(name, s), attrs )

            def close(self):
                return

        return DirList(
            iter([(name, _attrify(file))
                  for (name, file) in self.filesystem.fetch(path).children()]))
    openDirectory = translateErrors(openDirectory)

    def getAttrs(self, path, followLinks):
        node = self.filesystem.fetch(path)
        return _attrify(node)
    getAttrs = translateErrors(getAttrs)

    def setAttrs(self, path, attrs):
        node = self.filesystem.fetch(path)
        try:
            # XXX: setMetadata isn't yet part of the IFileSystemNode interface
            # (but it should be).  So we catch AttributeError, and translate it
            # to NotImplementedError because it's slightly nicer for clients.
            node.setMetadata(attrs)
        except AttributeError:
            raise NotImplementedError("NO SETATTR")
    setAttrs = translateErrors(setAttrs)

    def readLink(self, path):
        raise NotImplementedError("NO LINK")

    def makeLink(self, linkPath, targetPath):
        raise NotImplementedError("NO LINK")

    def realPath(self, path):
        return self.filesystem.absPath(path)


class AdaptFileSystemLeafToISFTPFile:
    zope.interface.implements(ISFTPFile)

    def __init__(self, original):
        self.original = original

    def close(self):
        return self.original.close()

    def readChunk(self, offset, length):
        return self.original.readChunk(offset, length)

    def writeChunk(self, offset, data):
        return self.original.writeChunk(offset, data)

    def getAttrs(self):
        return _attrify(self.original)
        
    def setAttrs(self, attrs):
        try:
            # XXX: setMetadata isn't yet part of the IFileSystemNode interface
            # (but it should be).  So we catch AttributeError, and translate it
            # to NotImplementedError because it's slightly nicer for clients.
            self.original.setMetadata(attrs)
        except AttributeError:
            raise NotImplementedError("NO SETATTR")


class VFSConchSession:
    zope.interface.implements(ISession)
    def __init__(self, avatar):
        self.avatar = avatar
    def openShell(self, proto):
        self.avatar.conn.transport.transport.loseConnection()
    def getPty(self, term, windowSize, modes):
        pass
    def closed(self):
        log.msg('shell closed')


class VFSConchUser(ConchUser):
    def __init__(self, username, root):
        ConchUser.__init__(self)
        self.username = username
        self.filesystem = pathutils.FileSystem(root)

        self.listeners = {}  # dict mapping (interface, port) -> listener
        self.channelLookup.update(
                {"session": session.SSHSession})
        self.subsystemLookup.update(
                {"sftp": FileTransferServer})

    def logout(self):
        # XXX - this may be broken
        log.msg('avatar %s logging out (%i)' % (self.username, len(self.listeners)))


def _attrify(node):
    meta = node.getMetadata()
    permissions = meta.get('permissions', None)
    if permissions is None:
        if ivfs.IFileSystemContainer.providedBy(node):
            permissions = 16877
        else:
            permissions = 33188

    return {'permissions': permissions,
            'size': meta.get('size', 0),
            'uid': meta.get('uid', 0),
            'gid': meta.get('gid', 0),
            'atime': meta.get('atime', time.time()),
            'mtime': meta.get('mtime', time.time()),
            'nlink': meta.get('nlink', 1)
            }



components.registerAdapter(AdaptFileSystemUserToISFTP, VFSConchUser, ISFTPServer)
components.registerAdapter(VFSConchSession, VFSConchUser, ISession)

