root/trunk/source/pyhesiodfs/pyHesiodFS.py

Revision 176, 8.3 kB (checked in by broder, 15 years ago)

Add a fix so that unlink(2) and symlink(2) work on OS X.

  • 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         else:
178             try:
179                 filsys = hesiod.FilsysLookup(name)
180             except IOError, e:
181                 if e.errno in (errno.ENOENT, errno.EMSGSIZE):
182                     raise IOError(errno.ENOENT, os.strerror(errno.ENOENT))
183                 else:
184                     raise IOError(errno.EIO, os.strerror(errno.EIO))
185             # FIXME check if the first locker is valid
186             if len(filsys.filsys) >= 1:
187                 pointers = filsys.filsys
188                 pointer = pointers[0]
189                 if pointer['type'] != 'AFS' and pointer['type'] != 'LOC':
190                     syslog(LOG_NOTICE, "Unknown locker type "+pointer['type']+" for locker "+name+" ("+repr(pointer)+" )")
191                     return None
192                 else:
193                     self.mounts[self._uid()][name] = pointer['location']
194                     syslog(LOG_INFO, "Mounting "+name+" on "+pointer['location'])
195                     return pointer['location']
196             else:
197                 syslog(LOG_WARNING, "Couldn't find filsys for "+name)
198                 return None
199
200     def getdir(self, path):
201         return [(i, 0) for i in (['.', '..', hello_path[1:]] + self.getCachedLockers())]
202
203     def readdir(self, path, offset):
204         for (r, zero) in self.getdir(path):
205             yield fuse.Direntry(r)
206            
207     def readlink(self, path):
208         return self.findLocker(path[1:])
209
210     def open(self, path, flags):
211         if path != hello_path:
212             return -errno.ENOENT
213         accmode = os.O_RDONLY | os.O_WRONLY | os.O_RDWR
214         if (flags & accmode) != os.O_RDONLY:
215             return -errno.EACCES
216
217     def read(self, path, size, offset):
218         if path != hello_path:
219             return -errno.ENOENT
220         slen = len(hello_str)
221         if offset < slen:
222             if offset + size > slen:
223                 size = slen - offset
224             buf = hello_str[offset:offset+size]
225         else:
226             buf = ''
227         return buf
228    
229     def symlink(self, src, path):
230         if path == '/' or path == hello_path:
231             return -errno.EPERM
232         elif '/' not in path[1:]:
233             self.mounts[self._uid()][path[1:]] = src
234             self.negcache[self._uid()].remove(path[1:])
235         else:
236             return -errno.EPERM
237    
238     def unlink(self, path):
239         if path == '/' or path == hello_path:
240             return -errno.EPERM
241         elif '/' not in path[1:]:
242             del self.mounts[self._uid()][path[1:]]
243             self.negcache[self._uid()].add(path[1:])
244         else:
245             return -errno.EPERM
246
247 def main():
248     global hello_str
249     try:
250         usage = Fuse.fusage
251         server = PyHesiodFS(version="%prog " + fuse.__version__,
252                             usage=usage,
253                             dash_s_do='setsingle')
254         server.parse(errex=1)
255     except AttributeError:
256         usage="""
257 pyHesiodFS [mountpath] [options]
258
259 """
260         if sys.argv[1] == '-f':
261             sys.argv.pop(1)
262         server = PyHesiodFS()
263
264     hello_str = hello_str % {'mountpoint': server.parse(errex=1).mountpoint}
265     server.main()
266
267 if __name__ == '__main__':
268     main()
Note: See TracBrowser for help on using the browser.