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 | st.st_mode = stat.S_IFDIR | 0755 |
---|
139 | st.st_gid = self._gid() |
---|
140 | st.st_nlink = 2 |
---|
141 | elif path == hello_path: |
---|
142 | st.st_mode = stat.S_IFREG | 0444 |
---|
143 | st.st_nlink = 1 |
---|
144 | st.st_size = len(hello_str) |
---|
145 | elif '/' not in path[1:]: |
---|
146 | if path[1:] not in self.negcache[self._uid()] and self.findLocker(path[1:]): |
---|
147 | st.st_mode = stat.S_IFLNK | 0777 |
---|
148 | st.st_uid = self._uid() |
---|
149 | st.st_nlink = 1 |
---|
150 | st.st_size = len(self.findLocker(path[1:])) |
---|
151 | else: |
---|
152 | return -errno.ENOENT |
---|
153 | else: |
---|
154 | return -errno.ENOENT |
---|
155 | if new_fuse: |
---|
156 | return st |
---|
157 | else: |
---|
158 | return st.toTuple() |
---|
159 | |
---|
160 | def getCachedLockers(self): |
---|
161 | return self.mounts[self._uid()].keys() |
---|
162 | |
---|
163 | def findLocker(self, name): |
---|
164 | """Lookup a locker in hesiod and return its path""" |
---|
165 | if name in self.mounts[self._uid()]: |
---|
166 | return self.mounts[self._uid()][name] |
---|
167 | else: |
---|
168 | try: |
---|
169 | filsys = hesiod.FilsysLookup(name) |
---|
170 | except IOError, e: |
---|
171 | if e.errno in (errno.ENOENT, errno.EMSGSIZE): |
---|
172 | raise IOError(errno.ENOENT, os.strerror(errno.ENOENT)) |
---|
173 | else: |
---|
174 | raise IOError(errno.EIO, os.strerror(errno.EIO)) |
---|
175 | # FIXME check if the first locker is valid |
---|
176 | if len(filsys.filsys) >= 1: |
---|
177 | pointers = filsys.filsys |
---|
178 | pointer = pointers[0] |
---|
179 | if pointer['type'] != 'AFS' and pointer['type'] != 'LOC': |
---|
180 | syslog(LOG_NOTICE, "Unknown locker type "+pointer['type']+" for locker "+name+" ("+repr(pointer)+" )") |
---|
181 | return None |
---|
182 | else: |
---|
183 | self.mounts[self._uid()][name] = pointer['location'] |
---|
184 | syslog(LOG_INFO, "Mounting "+name+" on "+pointer['location']) |
---|
185 | return pointer['location'] |
---|
186 | else: |
---|
187 | syslog(LOG_WARNING, "Couldn't find filsys for "+name) |
---|
188 | return None |
---|
189 | |
---|
190 | def getdir(self, path): |
---|
191 | return [(i, 0) for i in (['.', '..', hello_path[1:]] + self.getCachedLockers())] |
---|
192 | |
---|
193 | def readdir(self, path, offset): |
---|
194 | for (r, zero) in self.getdir(path): |
---|
195 | yield fuse.Direntry(r) |
---|
196 | |
---|
197 | def readlink(self, path): |
---|
198 | return self.findLocker(path[1:]) |
---|
199 | |
---|
200 | def open(self, path, flags): |
---|
201 | if path != hello_path: |
---|
202 | return -errno.ENOENT |
---|
203 | accmode = os.O_RDONLY | os.O_WRONLY | os.O_RDWR |
---|
204 | if (flags & accmode) != os.O_RDONLY: |
---|
205 | return -errno.EACCES |
---|
206 | |
---|
207 | def read(self, path, size, offset): |
---|
208 | if path != hello_path: |
---|
209 | return -errno.ENOENT |
---|
210 | slen = len(hello_str) |
---|
211 | if offset < slen: |
---|
212 | if offset + size > slen: |
---|
213 | size = slen - offset |
---|
214 | buf = hello_str[offset:offset+size] |
---|
215 | else: |
---|
216 | buf = '' |
---|
217 | return buf |
---|
218 | |
---|
219 | def symlink(self, src, path): |
---|
220 | if path == '/' or path == hello_path: |
---|
221 | return -errno.EPERM |
---|
222 | elif '/' not in path[1:]: |
---|
223 | self.mounts[self._uid()][path[1:]] = src |
---|
224 | self.negcache[self._uid()].remove(path[1:]) |
---|
225 | else: |
---|
226 | return -errno.EPERM |
---|
227 | |
---|
228 | def unlink(self, path): |
---|
229 | if path == '/' or path == hello_path: |
---|
230 | return -errno.EPERM |
---|
231 | elif '/' not in path[1:]: |
---|
232 | del self.mounts[self._uid()][path[1:]] |
---|
233 | self.negcache[self._uid()].add(path[1:]) |
---|
234 | else: |
---|
235 | return -errno.EPERM |
---|
236 | |
---|
237 | def main(): |
---|
238 | global hello_str |
---|
239 | try: |
---|
240 | usage = Fuse.fusage |
---|
241 | server = PyHesiodFS(version="%prog " + fuse.__version__, |
---|
242 | usage=usage, |
---|
243 | dash_s_do='setsingle') |
---|
244 | server.parse(errex=1) |
---|
245 | except AttributeError: |
---|
246 | usage=""" |
---|
247 | pyHesiodFS [mountpath] [options] |
---|
248 | |
---|
249 | """ |
---|
250 | if sys.argv[1] == '-f': |
---|
251 | sys.argv.pop(1) |
---|
252 | server = PyHesiodFS() |
---|
253 | |
---|
254 | hello_str = hello_str % {'mountpoint': server.parse(errex=1).mountpoint} |
---|
255 | server.main() |
---|
256 | |
---|
257 | if __name__ == '__main__': |
---|
258 | main() |
---|