root/trunk/source/pyhesiodfs/pyHesiodFS.py

Revision 179, 8.7 kB (checked in by broder, 16 years ago)

Only replace "/afs/" at the beginning of the path.

  • Property svn:executable set to *
Line 
1 #!/usr/bin/python2.5
2
3 #    pyHesiodFS:
4 #    Copyright (C) 2007  Quentin Smith <quentin@mit.edu>
5 #    "Hello World" pyFUSE example:
6 #    Copyright (C) 2006  Andrew Straw  <strawman@astraw.com>
7 #
8 #    This program can be distributed under the terms of the GNU LGPL.
9 #    See the file COPYING.
10 #
11
12 import sys, os, stat, errno, time
13 from syslog import *
14 import fuse
15 from fuse import Fuse
16
17 import hesiod
18
19 try:
20     from collections import defaultdict
21 except ImportError:
22     class defaultdict(dict):
23         """
24         A dictionary that automatically will fill in keys that don't exist
25         with the result from some default value factory
26        
27         Based on the collections.defaultdict object in Python 2.5
28         """
29        
30         def __init__(self, default_factory):
31             self.default_factory = default_factory
32             super(defaultdict, self).__init__()
33        
34         def __getitem__(self, y):
35             if y not in self:
36                 self[y] = self.default_factory()
37             return super(defaultdict, self).__getitem__(y)
38        
39         def __str__(self):
40             return 'defaultdict(%s, %s)' % (self.default_factory,
41                                             super(defaultdict, self).__str__())
42
43 class negcache(dict):
44     """
45     A set-like object that automatically expunges entries after
46     they're been there for a certain amount of time.
47    
48     This only supports add, remove, and __contains__
49     """
50    
51     def __init__(self, cache_time=0.5):
52         self.cache_time = cache_time
53    
54     def add(self, obj):
55         self[obj] = time.time()
56    
57     def remove(self, obj):
58         try:
59             del self[obj]
60         except KeyError:
61             pass
62    
63     def __contains__(self, k):
64         if super(negcache, self).__contains__(k):
65             if self[k] + self.cache_time > time.time():
66                 return True
67             else:
68                 del self[k]
69         return False
70
71 new_fuse = hasattr(fuse, '__version__')
72
73 fuse.fuse_python_api = (0, 2)
74
75 hello_path = '/README.txt'
76 hello_str = """This is the pyhesiodfs FUSE autmounter. To access a Hesiod filsys, just access
77 %(mountpoint)s/name.
78
79 If you're using the Finder, try pressing Cmd+Shift+G and then entering
80 %(mountpoint)s/name"""
81
82 if not hasattr(fuse, 'Stat'):
83     fuse.Stat = object
84
85 class MyStat(fuse.Stat):
86     def __init__(self):
87         self.st_mode = 0
88         self.st_ino = 0
89         self.st_dev = 0
90         self.st_nlink = 0
91         self.st_uid = 0
92         self.st_gid = 0
93         self.st_size = 0
94         self.st_atime = 0
95         self.st_mtime = 0
96         self.st_ctime = 0
97
98     def toTuple(self):
99         return (self.st_mode, self.st_ino, self.st_dev, self.st_nlink,
100                 self.st_uid, self.st_gid, self.st_size, self.st_atime,
101                 self.st_mtime, self.st_ctime)
102
103 class PyHesiodFS(Fuse):
104
105     def __init__(self, *args, **kwargs):
106         Fuse.__init__(self, *args, **kwargs)
107        
108         openlog('pyhesiodfs', 0, LOG_DAEMON)
109        
110         try:
111             self.fuse_args.add("allow_other", True)
112         except AttributeError:
113             self.allow_other = 1
114
115         if sys.platform == 'darwin':
116             self.fuse_args.add("noappledouble", True)
117             self.fuse_args.add("noapplexattr", True)
118             self.fuse_args.add("volname", "MIT")
119             self.fuse_args.add("fsname", "pyHesiodFS")
120         self.mounts = defaultdict(dict)
121        
122         # Cache deletions for half a second - should give `ln -nsf`
123         # enough time to make a new symlink
124         self.negcache = defaultdict(negcache)
125    
126     def _uid(self):
127         return fuse.FuseGetContext()['uid']
128    
129     def _gid(self):
130         return fuse.FuseGetContext()['gid']
131    
132     def _pid(self):
133         return fuse.FuseGetContext()['pid']
134    
135     def getattr(self, path):
136         st = MyStat()
137         if path == '/':
138             # The old liblocker attach expects /mit to not be group-
139             # or other-writeable, but OS X's pre-operation access
140             # checks make it impossible to unlink or symlink if
141             # traditional UNIX permissions say you don't have bits
142             #
143             # This is a temporary work around that can be punted when
144             # I get around to writing the new attach
145             if sys.platform == 'darwin':
146                 st.st_mode = stat.S_IFDIR | 0777
147             else:
148                 st.st_mode = stat.S_IFDIR | 0755
149             st.st_gid = self._gid()
150             st.st_nlink = 2
151         elif path == hello_path:
152             st.st_mode = stat.S_IFREG | 0444
153             st.st_nlink = 1
154             st.st_size = len(hello_str)
155         elif '/' not in path[1:]:
156             if path[1:] not in self.negcache[self._uid()] and self.findLocker(path[1:]):
157                 st.st_mode = stat.S_IFLNK | 0777
158                 st.st_uid = self._uid()
159                 st.st_nlink = 1
160                 st.st_size = len(self.findLocker(path[1:]))
161             else:
162                 return -errno.ENOENT
163         else:
164             return -errno.ENOENT
165         if new_fuse:
166             return st
167         else:
168             return st.toTuple()
169
170     def getCachedLockers(self):
171         return self.mounts[self._uid()].keys()
172
173     def findLocker(self, name):
174         """Lookup a locker in hesiod and return its path"""
175         if name in self.mounts[self._uid()]:
176             return self.mounts[self._uid()][name]
177         elif name.startswith('.'):
178             ro = self.findLocker(name[1:])
179             if ro is None or ro.startswith('/afs/.'):
180                 return
181             else:
182                 rw = ro.replace('/afs/', '/afs/.', 1)
183                 self.mounts[self._uid()][name] = rw
184                 syslog(LOG_INFO, "Mounting "+name+" on "+rw)
185                 return rw
186         else:
187             try:
188                 filsys = hesiod.FilsysLookup(name)
189             except IOError, e:
190                 if e.errno in (errno.ENOENT, errno.EMSGSIZE):
191                     raise IOError(errno.ENOENT, os.strerror(errno.ENOENT))
192                 else:
193                     raise IOError(errno.EIO, os.strerror(errno.EIO))
194             # FIXME check if the first locker is valid
195             if len(filsys.filsys) >= 1:
196                 pointers = filsys.filsys
197                 pointer = pointers[0]
198                 if pointer['type'] != 'AFS' and pointer['type'] != 'LOC':
199                     syslog(LOG_NOTICE, "Unknown locker type "+pointer['type']+" for locker "+name+" ("+repr(pointer)+" )")
200                     return None
201                 else:
202                     self.mounts[self._uid()][name] = pointer['location']
203                     syslog(LOG_INFO, "Mounting "+name+" on "+pointer['location'])
204                     return pointer['location']
205             else:
206                 syslog(LOG_WARNING, "Couldn't find filsys for "+name)
207                 return None
208
209     def getdir(self, path):
210         return [(i, 0) for i in (['.', '..', hello_path[1:]] + self.getCachedLockers())]
211
212     def readdir(self, path, offset):
213         for (r, zero) in self.getdir(path):
214             yield fuse.Direntry(r)
215            
216     def readlink(self, path):
217         return self.findLocker(path[1:])
218
219     def open(self, path, flags):
220         if path != hello_path:
221             return -errno.ENOENT
222         accmode = os.O_RDONLY | os.O_WRONLY | os.O_RDWR
223         if (flags & accmode) != os.O_RDONLY:
224             return -errno.EACCES
225
226     def read(self, path, size, offset):
227         if path != hello_path:
228             return -errno.ENOENT
229         slen = len(hello_str)
230         if offset < slen:
231             if offset + size > slen:
232                 size = slen - offset
233             buf = hello_str[offset:offset+size]
234         else:
235             buf = ''
236         return buf
237    
238     def symlink(self, src, path):
239         if path == '/' or path == hello_path:
240             return -errno.EPERM
241         elif '/' not in path[1:]:
242             self.mounts[self._uid()][path[1:]] = src
243             self.negcache[self._uid()].remove(path[1:])
244         else:
245             return -errno.EPERM
246    
247     def unlink(self, path):
248         if path == '/' or path == hello_path:
249             return -errno.EPERM
250         elif '/' not in path[1:]:
251             del self.mounts[self._uid()][path[1:]]
252             self.negcache[self._uid()].add(path[1:])
253         else:
254             return -errno.EPERM
255
256 def main():
257     global hello_str
258     try:
259         usage = Fuse.fusage
260         server = PyHesiodFS(version="%prog " + fuse.__version__,
261                             usage=usage,
262                             dash_s_do='setsingle')
263         server.parse(errex=1)
264     except AttributeError:
265         usage="""
266 pyHesiodFS [mountpath] [options]
267
268 """
269         if sys.argv[1] == '-f':
270             sys.argv.pop(1)
271         server = PyHesiodFS()
272
273     hello_str = hello_str % {'mountpoint': server.parse(errex=1).mountpoint}
274     server.main()
275
276 if __name__ == '__main__':
277     main()
Note: See TracBrowser for help on using the browser.