text: #!/usr/bin/env python
  
  #       Licensed to the Apache Software Foundation (ASF) under one
  #       or more contributor license agreements.  See the NOTICE file
  #       distributed with this work for additional information
  #       regarding copyright ownership.  The ASF licenses this file
  #       to you under the Apache License, Version 2.0 (the
  #       "License"); you may not use this file except in compliance
  #       with the License.  You may obtain a copy of the License at
  #
  #         http://www.apache.org/licenses/LICENSE-2.0
  #
  #       Unless required by applicable law or agreed to in writing,
  #       software distributed under the License is distributed on an
  #       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  #       KIND, either express or implied.  See the License for the
  #       specific language governing permissions and limitations
  #       under the License.
  
  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()
  