]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - mac/tahoefuse.py
103fd04c9de7c6b2bcf6a8549d674e4c9625b61e
[tahoe-lafs/tahoe-lafs.git] / mac / tahoefuse.py
1 #!/usr/bin/env python
2
3 #-----------------------------------------------------------------------------------------------
4 from allmydata.uri import CHKFileURI, NewDirectoryURI, LiteralFileURI
5 from allmydata.scripts.common_http import do_http as do_http_req
6 from allmydata.util.hashutil import tagged_hash
7 from allmydata.util.assertutil import precondition
8 from allmydata.util import base32
9 from allmydata.scripts.common import get_aliases
10
11 from twisted.python import usage
12
13 import base64
14 import sha
15 import sys
16 import os
17 #import pprint
18 import errno
19 import stat
20 # pull in some spaghetti to make this stuff work without fuse-py being installed
21 try:
22     import _find_fuse_parts
23     junk = _find_fuse_parts
24     del junk
25 except ImportError:
26     pass
27 import fuse
28
29 import time
30 import traceback
31 import simplejson
32 import urllib
33
34 VERSIONSTR="0.6"
35
36 USAGE = 'usage: tahoe fuse [dir_cap_name] [fuse_options] mountpoint'
37 DEFAULT_DIRECTORY_VALIDITY=26
38
39 if not hasattr(fuse, '__version__'):
40     raise RuntimeError, \
41         "your fuse-py doesn't know of fuse.__version__, probably it's too old."
42
43 fuse.fuse_python_api = (0, 2)
44 fuse.feature_assert('stateful_files', 'has_init')
45
46 class TahoeFuseOptions(usage.Options):
47     optParameters = [
48         ["node-directory", None, "~/.tahoe",
49          "Look here to find out which Tahoe node should be used for all "
50          "operations. The directory should either contain a full Tahoe node, "
51          "or a file named node.url which points to some other Tahoe node. "
52          "It should also contain a file named private/aliases which contains "
53          "the mapping from alias name to root dirnode URI."
54          ],
55         ["node-url", None, None,
56          "URL of the tahoe node to use, a URL like \"http://127.0.0.1:8123\". "
57          "This overrides the URL found in the --node-directory ."],
58         ["alias", None, None,
59          "Which alias should be mounted."],
60         ["root-uri", None, None,
61          "Which root directory uri should be mounted."],
62         ["cache-timeout", None, 20,
63          "Time, in seconds, to cache directory data."],
64         ]
65
66     def __init__(self):
67         usage.Options.__init__(self)
68         self.fuse_options = []
69         self.mountpoint = None
70
71     def opt_option(self, fuse_option):
72         """
73         Pass mount options directly to fuse.  See below.
74         """
75         self.fuse_options.append(fuse_option)
76         
77     opt_o = opt_option
78
79     def parseArgs(self, mountpoint=''):
80         self.mountpoint = mountpoint
81
82     def getSynopsis(self):
83         return "%s [options] mountpoint" % (os.path.basename(sys.argv[0]),)
84
85 logfile = file('tfuse.log', 'ab')
86
87 def log(msg):
88     logfile.write("%s: %s\n" % (time.asctime(), msg))
89     #time.sleep(0.1)
90     logfile.flush()
91
92 fuse.flog = log
93
94 def unicode_to_utf8(u):
95     precondition(isinstance(u, unicode), repr(u))
96     return u.encode('utf-8')
97
98 def do_http(method, url, body=''):
99     resp = do_http_req(method, url, body)
100     log('do_http(%s, %s) -> %s, %s' % (method, url, resp.status, resp.reason))
101     if resp.status not in (200, 201):
102         raise RuntimeError('http response (%s, %s)' % (resp.status, resp.reason))
103     else:
104         return resp.read()
105
106 def flag2mode(flags):
107     log('flag2mode(%r)' % (flags,))
108     #md = {os.O_RDONLY: 'r', os.O_WRONLY: 'w', os.O_RDWR: 'w+'}
109     md = {os.O_RDONLY: 'rb', os.O_WRONLY: 'wb', os.O_RDWR: 'w+b'}
110     m = md[flags & (os.O_RDONLY | os.O_WRONLY | os.O_RDWR)]
111
112     if flags & os.O_APPEND:
113         m = m.replace('w', 'a', 1)
114
115     return m
116
117 class TFSIOError(IOError):
118     pass
119
120 class ENOENT(TFSIOError):
121     def __init__(self, msg):
122         TFSIOError.__init__(self, errno.ENOENT, msg)
123
124 class EINVAL(TFSIOError):
125     def __init__(self, msg):
126         TFSIOError.__init__(self, errno.EINVAL, msg)
127
128 class EACCESS(TFSIOError):
129     def __init__(self, msg):
130         TFSIOError.__init__(self, errno.EACCESS, msg)
131
132 class EEXIST(TFSIOError):
133     def __init__(self, msg):
134         TFSIOError.__init__(self, errno.EEXIST, msg)
135
136 def logargsretexc(meth):
137     def inner(self, *args, **kwargs):
138         log("%s(%r, %r)" % (meth, args, kwargs))
139         try:
140             ret = meth(self, *args, **kwargs)
141         except:
142             log('exception:\n%s' % (traceback.format_exc(),))
143             raise
144         log("ret: %r" % (ret, ))
145         return ret
146     inner.__name__ = '<logwrap(%s)>' % (meth,)
147     return inner
148
149 def logexc(meth):
150     def inner(self, *args, **kwargs):
151         try:
152             ret = meth(self, *args, **kwargs)
153         except TFSIOError, tie:
154             log('error: %s' % (tie,))
155             raise
156         except:
157             log('exception:\n%s' % (traceback.format_exc(),))
158             raise
159         return ret
160     inner.__name__ = '<logwrap(%s)>' % (meth,)
161     return inner
162
163 def log_exc():
164     log('exception:\n%s' % (traceback.format_exc(),))
165
166 class TahoeFuseFile(object):
167
168     def __init__(self, path, flags, *mode):
169         log("TFF: __init__(%r, %r, %r)" % (path, flags, mode))
170
171         self._path = path # for tahoe put
172         try:
173             self.parent, self.name, self.fnode = self.tfs.get_parent_name_and_child(path)
174             m = flag2mode(flags)
175             log('TFF: flags2(mode) -> %s' % (m,))
176             if m[0] in 'wa':
177                 # write
178                 self.fname = self.tfs.cache.tmp_file(os.urandom(20))
179                 if self.fnode is None:
180                     log('TFF: [%s] open() for write: no file node, creating new File %s' % (self.name, self.fname, ))
181                     self.fnode = File(0, 'URI:LIT:')
182                     self.fnode.tmp_fname = self.fname # XXX kill this
183                     self.parent.add_child(self.name, self.fnode, {})
184                 elif hasattr(self.fnode, 'tmp_fname'):
185                     self.fname = self.fnode.tmp_fname
186                     log('TFF: [%s] open() for write: existing file node lists %s' % (self.name, self.fname, ))
187                 else:
188                     log('TFF: [%s] open() for write: existing file node lists no tmp_file, using new %s' % (self.name, self.fname, ))
189                 self.file = os.fdopen(os.open(self.fname, flags|os.O_CREAT, *mode), m)
190                 self.fd = self.file.fileno()
191                 log('TFF: opened(%s) for write' % self.fname)
192                 self.open_for_write = True
193             else:
194                 # read
195                 assert self.fnode is not None
196                 uri = self.fnode.get_uri()
197
198                 # XXX make this go away
199                 if hasattr(self.fnode, 'tmp_fname'):
200                     self.fname = self.fnode.tmp_fname
201                     log('TFF: reopening(%s) for reading' % self.fname)
202                 else:
203                     log('TFF: fetching file from cache for reading')
204                     self.fname = self.tfs.cache.get_file(uri)
205
206                 self.file = os.fdopen(os.open(self.fname, flags, *mode), m)
207                 self.fd = self.file.fileno()
208                 self.open_for_write = False
209                 log('TFF: opened(%s) for read' % self.fname)
210         except:
211             log_exc()
212             raise
213
214     def log(self, msg):
215         log("<TFF(%s:%s)> %s" % (os.path.basename(self.fname), self.name, msg))
216
217     @logexc
218     def read(self, size, offset):
219         self.log('read(%r, %r)' % (size, offset, ))
220         self.file.seek(offset)
221         return self.file.read(size)
222
223     @logexc
224     def write(self, buf, offset):
225         self.log("write(-%s-, %r)" % (len(buf), offset))
226         if not self.open_for_write:
227             return -errno.EACCES
228         self.file.seek(offset)
229         self.file.write(buf)
230         return len(buf)
231
232     @logexc
233     def release(self, flags):
234         self.log("release(%r)" % (flags,))
235         self.file.close()
236         if self.open_for_write:
237             size = os.path.getsize(self.fname)
238             self.fnode.size = size
239             file_cap = self.tfs.upload(self.fname)
240             self.fnode.ro_uri = file_cap
241             # XXX [ ] TODO: set metadata
242             # write new uri into parent dir entry
243             self.parent.add_child(self.name, self.fnode, {})
244             self.log("uploaded: %s" % (file_cap,))
245
246         # dbg
247         print_tree()
248
249     def _fflush(self):
250         if 'w' in self.file.mode or 'a' in self.file.mode:
251             self.file.flush()
252
253     @logexc
254     def fsync(self, isfsyncfile):
255         self.log("fsync(%r)" % (isfsyncfile,))
256         self._fflush()
257         if isfsyncfile and hasattr(os, 'fdatasync'):
258             os.fdatasync(self.fd)
259         else:
260             os.fsync(self.fd)
261
262     @logexc
263     def flush(self):
264         self.log("flush()")
265         self._fflush()
266         # cf. xmp_flush() in fusexmp_fh.c
267         os.close(os.dup(self.fd))
268
269     @logexc
270     def fgetattr(self):
271         self.log("fgetattr()")
272         s = os.fstat(self.fd)
273         self.log("fgetattr() -> %r" % (s,))
274         return s
275
276     @logexc
277     def ftruncate(self, len):
278         self.log("ftruncate(%r)" % (len,))
279         self.file.truncate(len)
280
281 class TahoeFuse(fuse.Fuse):
282
283     def __init__(self, tfs, *args, **kw):
284         log("TF: __init__(%r, %r)" % (args, kw))
285
286         self.tfs = tfs
287         _tfs_ = tfs
288         class MyFuseFile(TahoeFuseFile):
289             tfs = _tfs_
290         self.file_class = MyFuseFile
291         log("TF: file_class: %r" % (self.file_class,))
292
293         fuse.Fuse.__init__(self, *args, **kw)
294
295         #import thread
296         #thread.start_new_thread(self.launch_reactor, ())
297
298     def log(self, msg):
299         log("<TF> %s" % (msg, ))
300
301     @logexc
302     def readlink(self, path):
303         self.log("readlink(%r)" % (path,))
304         node = self.tfs.get_path(path)
305         if node:
306             raise EINVAL('Not a symlink') # nothing in tahoe is a symlink
307         else:
308             raise ENOENT('Invalid argument')
309
310     @logexc
311     def unlink(self, path):
312         self.log("unlink(%r)" % (path,))
313         self.tfs.unlink(path)
314
315     @logexc
316     def rmdir(self, path):
317         self.log("rmdir(%r)" % (path,))
318         self.tfs.unlink(path)
319
320     @logexc
321     def symlink(self, path, path1):
322         self.log("symlink(%r, %r)" % (path, path1))
323         self.tfs.link(path, path1)
324
325     @logexc
326     def rename(self, path, path1):
327         self.log("rename(%r, %r)" % (path, path1))
328         self.tfs.rename(path, path1)
329
330     @logexc
331     def link(self, path, path1):
332         self.log("link(%r, %r)" % (path, path1))
333         self.tfs.link(path, path1)
334
335     @logexc
336     def chmod(self, path, mode):
337         self.log("XX chmod(%r, %r)" % (path, mode))
338         #return -errno.EOPNOTSUPP
339
340     @logexc
341     def chown(self, path, user, group):
342         self.log("XX chown(%r, %r, %r)" % (path, user, group))
343         #return -errno.EOPNOTSUPP
344
345     @logexc
346     def truncate(self, path, len):
347         self.log("XX truncate(%r, %r)" % (path, len))
348         #return -errno.EOPNOTSUPP
349
350     @logexc
351     def utime(self, path, times):
352         self.log("XX utime(%r, %r)" % (path, times))
353         #return -errno.EOPNOTSUPP
354
355     @logexc
356     def statfs(self):
357         self.log("statfs()")
358         """
359         Should return an object with statvfs attributes (f_bsize, f_frsize...).
360         Eg., the return value of os.statvfs() is such a thing (since py 2.2).
361         If you are not reusing an existing statvfs object, start with
362         fuse.StatVFS(), and define the attributes.
363
364         To provide usable information (ie., you want sensible df(1)
365         output, you are suggested to specify the following attributes:
366
367             - f_bsize - preferred size of file blocks, in bytes
368             - f_frsize - fundamental size of file blcoks, in bytes
369                 [if you have no idea, use the same as blocksize]
370             - f_blocks - total number of blocks in the filesystem
371             - f_bfree - number of free blocks
372             - f_files - total number of file inodes
373             - f_ffree - nunber of free file inodes
374         """
375
376         block_size = 4096 # 4k
377         preferred_block_size = 131072 # 128k, c.f. seg_size
378         fs_size = 8*2**40 # 8Tb
379         fs_free = 2*2**40 # 2Tb
380
381         s = fuse.StatVfs(f_bsize = preferred_block_size,
382                          f_frsize = block_size,
383                          f_blocks = fs_size / block_size,
384                          f_bfree = fs_free / block_size,
385                          f_bavail = fs_free / block_size,
386                          f_files = 2**30, # total files
387                          f_ffree = 2**20, # available files
388                          f_favail = 2**20, # available files (root)
389                          f_flag = 2, # no suid
390                          f_namemax = 255) # max name length
391         return s
392
393     def fsinit(self):
394         self.log("fsinit()")
395
396     def main(self, *a, **kw):
397         self.log("main(%r, %r)" % (a, kw))
398
399         return fuse.Fuse.main(self, *a, **kw)
400
401     ##################################################################
402
403     @logexc
404     def readdir(self, path, offset):
405         log('readdir(%r, %r)' % (path, offset))
406         node = self.tfs.get_path(path)
407         if node is None:
408             return -errno.ENOENT
409         dirlist = ['.', '..'] + node.children.keys()
410         log('dirlist = %r' % (dirlist,))
411         return [fuse.Direntry(d) for d in dirlist]
412
413     @logexc
414     def getattr(self, path):
415         log('getattr(%r)' % (path,))
416
417         if path == '/':
418             # we don't have any metadata for the root (no edge leading to it)
419             mode = (stat.S_IFDIR | 755)
420             mtime = self.tfs.root.mtime
421             s = TStat({}, st_mode=mode, st_nlink=1, st_mtime=mtime)
422             log('getattr(%r) -> %r' % (path, s))
423             return s
424             
425         parent, name, child = self.tfs.get_parent_name_and_child(path)
426         if not child: # implicitly 'or not parent'
427             raise ENOENT('No such file or directory')
428         return parent.get_stat(name)
429
430     @logexc
431     def access(self, path, mode):
432         self.log("access(%r, %r)" % (path, mode))
433         node = self.tfs.get_path(path)
434         if not node:
435             return -errno.ENOENT
436         accmode = os.O_RDONLY | os.O_WRONLY | os.O_RDWR
437         if (mode & 0222):
438             if not node.writable():
439                 log('write access denied for %s (req:%o)' % (path, mode, ))
440                 return -errno.EACCES
441         #else:
442             #log('access granted for %s' % (path, ))
443
444     @logexc
445     def mkdir(self, path, mode):
446         self.log("mkdir(%r, %r)" % (path, mode))
447         self.tfs.mkdir(path)
448
449 def launch_tahoe_fuse(tfs, argv):
450     sys.argv = ['tahoe fuse'] + list(argv)
451     log('setting sys.argv=%r' % (sys.argv,))
452     config = TahoeFuseOptions()
453     server = TahoeFuse(tfs, version="%prog " +VERSIONSTR+", fuse "+ fuse.__version__,
454                        usage=config.getSynopsis(),
455                        dash_s_do='setsingle')
456     server.parse(errex=1)
457     server.main()
458
459 def getnodeurl(nodedir):
460     f = file(os.path.expanduser(os.path.join(nodedir, "node.url")), 'rb')
461     nu = f.read().strip()
462     f.close()
463     if nu[-1] != "/":
464         nu += "/"
465     return nu
466
467 def fingerprint(uri):
468     if uri is None:
469         return None
470     return base64.b32encode(sha.new(uri).digest()).lower()[:6]
471
472 class TStat(fuse.Stat):
473     # in fuse 0.2, these are set by fuse.Stat.__init__
474     # in fuse 0.2-pre3 (hardy) they are not. badness unsues if they're missing
475     st_mode  = None
476     st_ino   = 0
477     st_dev   = 0
478     st_nlink = None
479     st_uid   = 0
480     st_gid   = 0
481     st_size  = 0
482     st_atime = 0
483     st_mtime = 0
484     st_ctime = 0
485
486     fields = [ 'st_mode', 'st_ino', 'st_dev', 'st_nlink', 'st_uid', 'st_gid', 'st_size',
487                'st_atime', 'st_mtime', 'st_ctime', ]
488     def __init__(self, metadata, **kwargs):
489         # first load any stat fields present in 'metadata'
490         for st in [ 'mtime', 'ctime' ]:
491             if st in metadata:
492                 setattr(self, "st_%s" % st, metadata[st])
493         for st in self.fields:
494             if st in metadata:
495                 setattr(self, st, metadata[st])
496
497         # then set any values passed in as kwargs
498         fuse.Stat.__init__(self, **kwargs)
499
500     def __repr__(self):
501         d = {}
502         for f in self.fields:
503             d[f] = getattr(self, f, None)
504         return "<Stat%r>" % (d,)
505
506 class Directory(object):
507     def __init__(self, tfs, ro_uri, rw_uri):
508         self.tfs = tfs
509         self.ro_uri = ro_uri
510         self.rw_uri = rw_uri
511         assert (rw_uri or ro_uri)
512         self.children = {}
513         self.last_load = None
514         self.last_data = None
515         self.mtime = 0
516
517     def __repr__(self):
518         return "<Directory %s>" % (fingerprint(self.get_uri()),)
519
520     def maybe_refresh(self, name=None):
521         """
522         if the previously cached data was retrieved within the cache
523         validity period, does nothing. otherwise refetches the data
524         for this directory and reloads itself
525         """
526         now = time.time()
527         if self.last_load is None or (now - self.last_load) > self.tfs.cache_validity:
528             self.load(name)
529
530     def load(self, name=None):
531         now = time.time()
532         print 'loading', name or self
533         log('%s.loading(%s)' % (self, name))
534         url = self.tfs.compose_url("uri/%s?t=json", self.get_uri())
535         data = urllib.urlopen(url).read()
536         h = tagged_hash('cache_hash', data)
537         if h == self.last_data:
538             self.last_load = now
539             log('%s.load() : no change h(data)=%s' % (self, base32.b2a(h), ))
540             return
541         try:
542             parsed = simplejson.loads(data)
543         except ValueError:
544             log('%s.load(): unable to parse json data for dir:\n%r' % (self, data))
545             return
546         nodetype, d = parsed
547         assert nodetype == 'dirnode'
548         self.children.clear()
549         for cname,details in d['children'].items():
550             cname = unicode_to_utf8(cname)
551             ctype, cattrs = details
552             metadata = cattrs.get('metadata', {})
553             if ctype == 'dirnode':
554                 cobj = self.tfs.dir_for(cname, cattrs.get('ro_uri'), cattrs.get('rw_uri'))
555             else:
556                 assert ctype == "filenode"
557                 cobj = File(cattrs.get('size'), cattrs.get('ro_uri'))
558             self.children[cname] = cobj, metadata
559         self.last_load = now
560         self.last_data = h
561         self.mtime = now
562         log('%s.load() loaded: \n%s' % (self, self.pprint(),))
563
564     def get_children(self):
565         return self.children.keys()
566
567     def get_child(self, name):
568         return self.children[name][0]
569
570     def add_child(self, name, child, metadata):
571         log('%s.add_child(%r, %r, %r)' % (self, name, child, metadata, ))
572         self.children[name] = child, metadata
573         url = self.tfs.compose_url("uri/%s/%s?t=uri", self.get_uri(), name)
574         child_cap = do_http('PUT', url, child.get_uri())
575         # XXX [ ] TODO: push metadata to tahoe node
576         assert child_cap == child.get_uri()
577         self.mtime = time.time()
578         log('added child %r with %r to %r' % (name, child_cap, self))
579
580     def remove_child(self, name):
581         log('%s.remove_child(%r)' % (self, name, ))
582         del self.children[name]
583         url = self.tfs.compose_url("uri/%s/%s", self.get_uri(), name)
584         resp = do_http('DELETE', url)
585         self.mtime = time.time()
586         log('child (%s) removal yielded %r' % (name, resp,))
587
588     def get_uri(self):
589         return self.rw_uri or self.ro_uri
590
591     def writable(self):
592         return self.rw_uri and self.rw_uri != self.ro_uri
593
594     def pprint(self, prefix='', printed=None, suffix=''):
595         ret = []
596         if printed is None:
597             printed = set()
598         writable = self.writable() and '+' or ' '
599         if self in printed:
600             ret.append("         %s/%s ... <%s> : %s" % (prefix, writable, fingerprint(self.get_uri()), suffix, ))
601         else:
602             ret.append("[%s] %s/%s : %s" % (fingerprint(self.get_uri()), prefix, writable, suffix, ))
603             printed.add(self)
604             for name,(child,metadata) in sorted(self.children.items()):
605                 ret.append(child.pprint(' ' * (len(prefix)+1)+name, printed, repr(metadata)))
606         return '\n'.join(ret)
607
608     def get_metadata(self, name):
609         return self.children[name][1]
610
611     def get_stat(self, name):
612         child,metadata = self.children[name]
613         log("%s.get_stat(%s) md: %r" % (self, name, metadata))
614
615         if isinstance(child, Directory):
616             child.maybe_refresh(name)
617             mode = metadata.get('st_mode') or (stat.S_IFDIR | 0755)
618             s = TStat(metadata, st_mode=mode, st_nlink=1, st_mtime=child.mtime)
619         else:
620             if hasattr(child, 'tmp_fname'):
621                 s = os.stat(child.tmp_fname)
622                 log("%s.get_stat(%s) returning local stat of tmp file" % (self, name, ))
623             else:
624                 s = TStat(metadata,
625                           st_nlink = 1,
626                           st_size = child.size,
627                           st_mode = metadata.get('st_mode') or (stat.S_IFREG | 0444),
628                           st_mtime = metadata.get('mtime') or self.mtime,
629                           )
630             return s
631
632         log("%s.get_stat(%s)->%s" % (self, name, s))
633         return s
634
635 class File(object):
636     def __init__(self, size, ro_uri):
637         self.size = size
638         if ro_uri:
639             ro_uri = str(ro_uri)
640         self.ro_uri = ro_uri
641
642     def __repr__(self):
643         return "<File %s>" % (fingerprint(self.ro_uri) or [self.tmp_fname],)
644
645     def pprint(self, prefix='', printed=None, suffix=''):
646         return "         %s (%s) : %s" % (prefix, self.size, suffix, )
647
648     def get_uri(self):
649         return self.ro_uri
650
651     def writable(self):
652         return True
653
654 class TFS(object):
655     def __init__(self, nodedir, nodeurl, root_uri, 
656                        cache_validity_period=DEFAULT_DIRECTORY_VALIDITY):
657         self.cache_validity = cache_validity_period
658         self.nodeurl = nodeurl
659         self.root_uri = root_uri
660         self.dirs = {}
661
662         cachedir = os.path.expanduser(os.path.join(nodedir, '_cache'))
663         self.cache = FileCache(nodeurl, cachedir)
664         ro_uri = NewDirectoryURI.init_from_string(self.root_uri).get_readonly()
665         self.root = Directory(self, ro_uri, self.root_uri)
666         self.root.maybe_refresh('<root>')
667
668     def log(self, msg):
669         log("<TFS> %s" % (msg, ))
670
671     def pprint(self):
672         return self.root.pprint()
673
674     def compose_url(self, fmt, *args):
675         return self.nodeurl + (fmt % tuple(map(urllib.quote, args)))
676
677     def get_parent_name_and_child(self, path):
678         """
679         find the parent dir node, name of child relative to that parent, and
680         child node within the TFS object space.
681         @returns: (parent, name, child) if the child is found
682                   (parent, name, None) if the child is missing from the parent
683                   (None, name, None) if the parent is not found
684         """
685         if path == '/':
686             return 
687         dirname, name = os.path.split(path)
688         parent = self.get_path(dirname)
689         if parent:
690             try:
691                 child = parent.get_child(name)
692                 return parent, name, child
693             except KeyError:
694                 return parent, name, None
695         else:
696             return None, name, None
697
698     def get_path(self, path):
699         comps = path.strip('/').split('/')
700         if comps == ['']:
701             comps = []
702         cursor = self.root
703         c_name = '<root>'
704         for comp in comps:
705             if not isinstance(cursor, Directory):
706                 self.log('path "%s" is not a dir' % (path,))
707                 return None
708             cursor.maybe_refresh(c_name)
709             try:
710                 cursor = cursor.get_child(comp)
711                 c_name = comp
712             except KeyError:
713                 self.log('path "%s" not found' % (path,))
714                 return None
715         if isinstance(cursor, Directory):
716             cursor.maybe_refresh(c_name)
717         return cursor
718
719     def dir_for(self, name, ro_uri, rw_uri):
720         #self.log('dir_for(%s) [%s/%s]' % (name, fingerprint(ro_uri), fingerprint(rw_uri)))
721         if ro_uri:
722             ro_uri = str(ro_uri)
723         if rw_uri:
724             rw_uri = str(rw_uri)
725         uri = rw_uri or ro_uri
726         assert uri
727         dirobj = self.dirs.get(uri)
728         if not dirobj:
729             self.log('dir_for(%s) creating new Directory' % (name, ))
730             dirobj = Directory(self, ro_uri, rw_uri)
731             self.dirs[uri] = dirobj
732         return dirobj
733
734     def upload(self, fname):
735         self.log('upload(%r)' % (fname,))
736         fh = file(fname, 'rb')
737         url = self.compose_url("uri")
738         file_cap = do_http('PUT', url, fh)
739         self.log('uploaded to: %r' % (file_cap,))
740         return file_cap
741
742     def mkdir(self, path):
743         self.log('mkdir(%r)' % (path,))
744         parent, name, child = self.get_parent_name_and_child(path)
745
746         if child:
747             raise EEXIST('File exists: %s' % (name,))
748         if not parent:
749             raise ENOENT('No such file or directory: %s' % (path,))
750
751         url = self.compose_url("uri?t=mkdir")
752         new_dir_cap = do_http('PUT', url)
753
754         ro_uri = NewDirectoryURI.init_from_string(new_dir_cap).get_readonly()
755         child = Directory(self, ro_uri, new_dir_cap)
756         parent.add_child(name, child, {})
757
758     def rename(self, path, path1):
759         self.log('rename(%s, %s)' % (path, path1))
760         src_parent, src_name, src_child = self.get_parent_name_and_child(path)
761         dst_parent, dst_name, dst_child = self.get_parent_name_and_child(path1)
762
763         if not src_child or not dst_parent:
764             raise ENOENT('No such file or directory')
765
766         dst_parent.add_child(dst_name, src_child, {})
767         src_parent.remove_child(src_name)
768
769     def unlink(self, path):
770         parent, name, child = self.get_parent_name_and_child(path)
771
772         if child is None: # parent or child is missing
773             raise ENOENT('No such file or directory')
774         if not parent.writable():
775             raise EACCESS('Permission denied')
776
777         parent.remove_child(name)
778
779     def link(self, path, path1):
780         src = self.get_path(path)
781         dst_parent, dst_name, dst_child = self.get_parent_name_and_child(path1)
782
783         if not src:
784             raise ENOENT('No such file or directory')
785         if dst_parent is None:
786             raise ENOENT('No such file or directory')
787         if not dst_parent.writable():
788             raise EACCESS('Permission denied')
789
790         dst_parent.add_child(dst_name, src, {})
791
792 class FileCache(object):
793     def __init__(self, nodeurl, cachedir):
794         self.nodeurl = nodeurl
795         self.cachedir = cachedir
796         if not os.path.exists(self.cachedir):
797             os.makedirs(self.cachedir)
798         self.tmpdir = os.path.join(self.cachedir, 'tmp')
799         if not os.path.exists(self.tmpdir):
800             os.makedirs(self.tmpdir)
801
802     def log(self, msg):
803         log("<FC> %s" % (msg, ))
804
805     def get_file(self, uri):
806         self.log('get_file(%s)' % (uri,))
807         if uri.startswith("URI:LIT"):
808             return self.get_literal(uri)
809         else:
810             return self.get_chk(uri)
811
812     def get_literal(self, uri):
813         h = sha.new(uri).digest()
814         u = LiteralFileURI.init_from_string(uri)
815         fname = os.path.join(self.cachedir, '__'+base64.b32encode(h).lower())
816         size = len(u.data)
817         self.log('writing literal file %s (%s)' % (fname, size, ))
818         fh = open(fname, 'wb')
819         fh.write(u.data)
820         fh.close()
821         return fname
822
823     def get_chk(self, uri):
824         u = CHKFileURI.init_from_string(str(uri))
825         storage_index = u.storage_index
826         size = u.size
827         fname = os.path.join(self.cachedir, base64.b32encode(storage_index).lower())
828         if os.path.exists(fname):
829             fsize = os.path.getsize(fname)
830             if fsize == size:
831                 return fname
832             else:
833                 self.log('warning file "%s" is too short %s < %s' % (fname, fsize, size))
834         self.log('downloading file %s (%s)' % (fname, size, ))
835         fh = open(fname, 'wb')
836         url = "%suri/%s" % (self.nodeurl, uri)
837         download = urllib.urlopen(''.join([ self.nodeurl, "uri/", uri ]))
838         while True:
839             chunk = download.read(4096)
840             if not chunk:
841                 break
842             fh.write(chunk)
843         fh.close()
844         return fname
845
846     def tmp_file(self, id):
847         fname = os.path.join(self.tmpdir, base64.b32encode(id).lower())
848         return fname
849
850 _tfs = None # to appease pyflakes; is set in main()
851 def print_tree():
852     log('tree:\n' + _tfs.pprint())
853
854 def main(argv):
855     log("\n\nmain(%s)" % (argv,))
856
857     if not argv:
858         argv = ['--help']
859     if len(argv) == 1 and argv[0] in ['-h', '--help', '--version']:
860         config = TahoeFuseOptions()
861         print >> sys.stderr, config
862         print >> sys.stderr, 'fuse usage follows:'
863         launch_tahoe_fuse(None, argv)
864         return -2
865
866     config = TahoeFuseOptions()
867     try:
868         #print 'parsing', argv
869         config.parseOptions(argv)
870     except usage.error, e:
871         print config
872         print e
873         return -1
874
875     if config['alias']:
876         alias = config['alias']
877         #print 'looking for aliases in', config['node-directory']
878         aliases = get_aliases(os.path.expanduser(config['node-directory']))
879         if alias not in aliases:
880             raise usage.error('Alias %r not found' % (alias,))
881         root_uri = aliases[alias]
882     elif config['root-uri']:
883         root_uri = config['root-uri']
884         alias = 'root-uri'
885         # test the uri for structural validity:
886         try:
887             NewDirectoryURI.init_from_string(root_uri)
888         except:
889             raise usage.error('root-uri must be a valid directory uri (not %r)' % (root_uri,))
890     else:
891         raise usage.error('At least one of --alias or --root-uri must be specified')
892
893
894     nodedir = config['node-directory']
895     nodeurl = config['node-url']
896     if not nodeurl:
897         nodeurl = getnodeurl(nodedir)
898
899     # switch to named log file.
900     global logfile
901     fname = 'tfuse.%s.log' % (alias,)
902     log('switching to %s' % (fname,))
903     logfile.close()
904     logfile = file(fname, 'ab')
905     log('\n'+(24*'_')+'init'+(24*'_')+'\n')
906
907     if not os.path.exists(config.mountpoint):
908         raise OSError(2, 'No such file or directory: "%s"' % (config.mountpoint,))
909
910     cache_timeout = float(config['cache-timeout'])
911     tfs = TFS(nodedir, nodeurl, root_uri, cache_timeout)
912     print tfs.pprint()
913
914     # make tfs instance accesible to print_tree() for dbg
915     global _tfs
916     _tfs = tfs
917
918     args = [ '-o'+opt for opt in config.fuse_options ] + [config.mountpoint]
919     launch_tahoe_fuse(tfs, args)
920
921 if __name__ == '__main__':
922     sys.exit(main(sys.argv[1:]))