3644: Changed so ctime == mtime because ctime is not creation time (arg!).
[arvados.git] / services / fuse / arvados_fuse / __init__.py
1 #
2 # FUSE driver for Arvados Keep
3 #
4
5 import os
6 import sys
7 import llfuse
8 from llfuse import FUSEError
9 import errno
10 import stat
11 import threading
12 import arvados
13 import pprint
14 import arvados.events
15 import re
16 import apiclient
17 import json
18 import logging
19 import time
20 import calendar
21 import threading
22
23 _logger = logging.getLogger('arvados.arvados_fuse')
24
25 portable_data_hash_pattern = re.compile(r'[0-9a-f]{32}\+\d+')
26 uuid_pattern = re.compile(r'[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}')
27 collection_uuid_pattern = re.compile(r'[a-z0-9]{5}-4zz18-[a-z0-9]{15}')
28 group_uuid_pattern = re.compile(r'[a-z0-9]{5}-j7d0g-[a-z0-9]{15}')
29 user_uuid_pattern = re.compile(r'[a-z0-9]{5}-tpzed-[a-z0-9]{15}')
30 link_uuid_pattern = re.compile(r'[a-z0-9]{5}-o0j2j-[a-z0-9]{15}')
31
32 class SafeApi(object):
33     '''Threadsafe wrapper for API object.  This stores and returns a different api
34     object per thread, because httplib2 which underlies apiclient is not
35     threadsafe.
36     '''
37
38     def __init__(self, config):
39         self.host = config.get('ARVADOS_API_HOST')
40         self.token = config.get('ARVADOS_API_TOKEN')
41         self.insecure = config.flag_is_true('ARVADOS_API_HOST_INSECURE')
42         self.local = threading.local()
43
44     def localapi(self):
45         if 'api' not in self.local.__dict__:
46             self.local.api = arvados.api('v1', False, self.host, self.token, self.insecure)
47         return self.local.api
48
49     def collections(self):
50         return self.localapi().collections()
51
52     def links(self):
53         return self.localapi().links()
54
55     def groups(self):
56         return self.localapi().groups()
57
58     def users(self):
59         return self.localapi().users()
60         
61 def convertTime(t):
62     '''Parse Arvados timestamp to unix time.'''
63     return calendar.timegm(time.strptime(t, "%Y-%m-%dT%H:%M:%SZ"))
64
65 def sanitize_filename(dirty):
66     '''Remove troublesome characters from filenames.'''
67     # http://www.dwheeler.com/essays/fixing-unix-linux-filenames.html
68     if dirty is None:
69         return None
70
71     fn = ""
72     for c in dirty:
73         if (c >= '\x00' and c <= '\x1f') or c == '\x7f' or c == '/':
74             # skip control characters and /
75             continue
76         fn += c
77
78     # strip leading - or ~ and leading/trailing whitespace
79     stripped = fn.lstrip("-~ ").rstrip()
80     if len(stripped) > 0:
81         return stripped
82     else:
83         return None
84
85
86 class FreshBase(object):
87     '''Base class for maintaining fresh/stale state to determine when to update.'''
88     def __init__(self):
89         self._stale = True
90         self._poll = False
91         self._last_update = time.time()
92         self._atime = time.time()
93         self._poll_time = 60
94
95     # Mark the value as stale
96     def invalidate(self):
97         self._stale = True
98
99     # Test if the entries dict is stale.  Also updates atime.
100     def stale(self):
101         self._atime = time.time()
102         if self._stale:
103             return True
104         if self._poll:
105             return (self._last_update + self._poll_time) < self._atime
106         return False
107
108     def fresh(self):
109         self._stale = False
110         self._last_update = time.time()
111
112     def atime(self):
113         return self._atime
114
115 class File(FreshBase):
116     '''Base for file objects.'''
117
118     def __init__(self, parent_inode, _mtime=0):
119         super(File, self).__init__()
120         self.inode = None
121         self.parent_inode = parent_inode
122         self._mtime = _mtime
123
124     def size(self):
125         return 0
126
127     def readfrom(self, off, size):
128         return ''
129
130     def mtime(self):
131         return self._mtime
132
133
134 class StreamReaderFile(File):
135     '''Wraps a StreamFileReader as a file.'''
136
137     def __init__(self, parent_inode, reader, _mtime):
138         super(StreamReaderFile, self).__init__(parent_inode, _mtime)
139         self.reader = reader
140
141     def size(self):
142         return self.reader.size()
143
144     def readfrom(self, off, size):
145         return self.reader.readfrom(off, size)
146
147     def stale(self):
148         return False
149
150
151 class StringFile(File):
152     '''Wrap a simple string as a file'''
153     def __init__(self, parent_inode, contents, _mtime):
154         super(StringFile, self).__init__(parent_inode, _mtime)
155         self.contents = contents
156
157     def size(self):
158         return len(self.contents)
159
160     def readfrom(self, off, size):
161         return self.contents[off:(off+size)]    
162
163
164 class ObjectFile(StringFile):
165     '''Wrap a dict as a serialized json object.'''
166
167     def __init__(self, parent_inode, obj):
168         super(ObjectFile, self).__init__(parent_inode, "", 0)
169         self.uuid = obj['uuid']
170         self.update(obj)
171
172     def update(self, obj):
173         self._mtime = convertTime(obj['modified_at']) if 'modified_at' in obj else 0
174         self.contents = json.dumps(obj, indent=4, sort_keys=True) + "\n"
175
176
177 class Directory(FreshBase):
178     '''Generic directory object, backed by a dict.
179     Consists of a set of entries with the key representing the filename
180     and the value referencing a File or Directory object.
181     '''
182
183     def __init__(self, parent_inode):
184         super(Directory, self).__init__()
185
186         '''parent_inode is the integer inode number'''
187         self.inode = None
188         if not isinstance(parent_inode, int):
189             raise Exception("parent_inode should be an int")
190         self.parent_inode = parent_inode
191         self._entries = {}
192         self._mtime = time.time()
193
194     #  Overriden by subclasses to implement logic to update the entries dict
195     #  when the directory is stale
196     def update(self):
197         pass
198
199     # Only used when computing the size of the disk footprint of the directory
200     # (stub)
201     def size(self):
202         return 0
203
204     def checkupdate(self):
205         if self.stale():
206             try:
207                 self.update()
208             except apiclient.errors.HttpError as e:
209                 _logger.debug(e)
210
211     def __getitem__(self, item):
212         self.checkupdate()
213         return self._entries[item]
214
215     def items(self):
216         self.checkupdate()
217         return self._entries.items()
218
219     def __iter__(self):
220         self.checkupdate()
221         return self._entries.iterkeys()
222
223     def __contains__(self, k):
224         self.checkupdate()
225         return k in self._entries
226
227     def merge(self, items, fn, same, new_entry):
228         '''Helper method for updating the contents of the directory.  Takes a list
229         describing the new contents of the directory, reuse entries that are
230         the same in both the old and new lists, create new entries, and delete
231         old entries missing from the new list.
232
233         items: iterable with new directory contents
234
235         fn: function to take an entry in 'items' and return the desired file or
236         directory name, or None if this entry should be skipped
237
238         same: function to compare an existing entry (a File or Directory
239         object) with an entry in the items list to determine whether to keep
240         the existing entry.
241
242         new_entry: function to create a new directory entry (File or Directory
243         object) from an entry in the items list.
244
245         '''
246
247         oldentries = self._entries
248         self._entries = {}
249         changed = False
250         for i in items:
251             name = sanitize_filename(fn(i))
252             if name:
253                 if name in oldentries and same(oldentries[name], i):
254                     # move existing directory entry over
255                     self._entries[name] = oldentries[name]
256                     del oldentries[name]
257                 else:
258                     # create new directory entry
259                     ent = new_entry(i)
260                     if ent is not None:
261                         self._entries[name] = self.inodes.add_entry(ent)
262                     changed = True
263
264         # delete any other directory entries that were not in found in 'items'
265         for i in oldentries:            
266             llfuse.invalidate_entry(self.inode, str(i))
267             self.inodes.del_entry(oldentries[i])
268             changed = True
269
270         if changed:
271             self._mtime = time.time()
272
273         self.fresh()
274
275     def clear(self):
276         '''Delete all entries'''
277         oldentries = self._entries
278         self._entries = {}
279         for n in oldentries:
280             if isinstance(n, Directory):
281                 n.clear()
282             llfuse.invalidate_entry(self.inode, str(n))
283             self.inodes.del_entry(oldentries[n])
284         self.invalidate()
285
286     def mtime(self):
287         return self._mtime
288
289
290 class CollectionDirectory(Directory):
291     '''Represents the root of a directory tree holding a collection.'''
292
293     def __init__(self, parent_inode, inodes, api, collection):
294         super(CollectionDirectory, self).__init__(parent_inode)
295         self.inodes = inodes
296         self.api = api
297         self.collection_object_file = None
298         self.collection_object = None
299         if isinstance(collection, dict):
300             self.collection_locator = collection['uuid']
301         else:
302             self.collection_locator = collection
303
304     def same(self, i):
305         return i['uuid'] == self.collection_locator or i['portable_data_hash'] == self.collection_locator
306
307     def new_collection(self, new_collection_object):
308         self.collection_object = new_collection_object
309
310         if self.collection_object_file is not None:
311             self.collection_object_file.update(self.collection_object)
312
313         self.clear()
314         collection = arvados.CollectionReader(self.collection_object["manifest_text"], self.api)
315         for s in collection.all_streams():
316             cwd = self
317             for part in s.name().split('/'):
318                 if part != '' and part != '.':
319                     partname = sanitize_filename(part)
320                     if partname not in cwd._entries:
321                         cwd._entries[partname] = self.inodes.add_entry(Directory(cwd.inode))
322                     cwd = cwd._entries[partname]
323             for k, v in s.files().items():
324                 cwd._entries[sanitize_filename(k)] = self.inodes.add_entry(StreamReaderFile(cwd.inode, v, self.mtime()))        
325
326     def update(self):
327         try:
328             if self.collection_object is not None and portable_data_hash_pattern.match(self.collection_locator):
329                 return True
330
331             with llfuse.lock_released:
332                 new_collection_object = self.api.collections().get(uuid=self.collection_locator).execute()
333                 if "portable_data_hash" not in new_collection_object:
334                     new_collection_object["portable_data_hash"] = new_collection_object["uuid"]
335             # end with llfuse.lock_released, re-acquire lock
336
337             if self.collection_object is None or self.collection_object["portable_data_hash"] != new_collection_object["portable_data_hash"]:
338                 self.new_collection(new_collection_object)
339
340             self.fresh()
341             return True
342         except apiclient.errors.HttpError as e:
343             if e.resp.status == 404:
344                 _logger.warn("arv-mount %s: not found", self.collection_locator)
345             else:
346                 _logger.error("arv-mount %s: error", self.collection_locator)
347                 _logger.exception(detail)
348         except Exception as detail:
349             _logger.error("arv-mount %s: error", self.collection_locator)
350             if "manifest_text" in self.collection_object:
351                 _logger.error("arv-mount manifest_text is: %s", self.collection_object["manifest_text"])
352             _logger.exception(detail)                
353         return False
354
355     def __getitem__(self, item):
356         self.checkupdate()
357         if item == '.arvados#collection':
358             if self.collection_object_file is None:
359                 self.collection_object_file = ObjectFile(self.inode, self.collection_object)
360                 self.inodes.add_entry(self.collection_object_file)
361             return self.collection_object_file
362         else:
363             return super(CollectionDirectory, self).__getitem__(item)
364
365     def __contains__(self, k):
366         if k == '.arvados#collection':
367             return True
368         else:
369             return super(CollectionDirectory, self).__contains__(k)
370
371     def mtime(self):
372         self.checkupdate()
373         return convertTime(self.collection_object["modified_at"]) if self.collection_object is not None and 'modified_at' in self.collection_object else 0
374
375
376 class MagicDirectory(Directory):
377     '''A special directory that logically contains the set of all extant keep
378     locators.  When a file is referenced by lookup(), it is tested to see if it
379     is a valid keep locator to a manifest, and if so, loads the manifest
380     contents as a subdirectory of this directory with the locator as the
381     directory name.  Since querying a list of all extant keep locators is
382     impractical, only collections that have already been accessed are visible
383     to readdir().
384     '''
385
386     def __init__(self, parent_inode, inodes, api):
387         super(MagicDirectory, self).__init__(parent_inode)
388         self.inodes = inodes
389         self.api = api
390         self.readme_file = None
391
392     def __contains__(self, k):
393         if self.readme_file is None:
394             text = '''This directory provides access to Arvados collections as subdirectories listed
395 by uuid (in the form 'zzzzz-4zz18-1234567890abcde') or portable data hash (in
396 the form '1234567890abcdefghijklmnopqrstuv+123').
397
398 Note that this directory will appear empty until you attempt to access a
399 specific collection subdirectory (such as trying to 'cd' into it), at which
400 point the collection will actually be looked up on the server and the directory
401 will appear if it exists.
402 '''
403             self.readme_file = self.inodes.add_entry(StringFile(self.inode, text, time.time()))
404             self._entries["README"] = self.readme_file
405
406         if k in self._entries:
407             return True
408
409         if not portable_data_hash_pattern.match(k) and not uuid_pattern.match(k):
410             return False
411
412         try:
413             e = self.inodes.add_entry(CollectionDirectory(self.inode, self.inodes, self.api, k))
414             if e.update():
415                 self._entries[k] = e
416                 return True
417             else:
418                 return False
419         except Exception as e:
420             _logger.debug('arv-mount exception keep %s', e)
421             return False
422
423     def __getitem__(self, item):
424         if item in self:
425             return self._entries[item]
426         else:
427             raise KeyError("No collection with id " + item)
428
429
430 class RecursiveInvalidateDirectory(Directory):
431     def invalidate(self):
432         if self.inode == llfuse.ROOT_INODE:
433             llfuse.lock.acquire()
434         try:
435             super(RecursiveInvalidateDirectory, self).invalidate()
436             for a in self._entries:
437                 self._entries[a].invalidate()
438         except Exception as e:
439             _logger.exception(e)
440         finally:
441             if self.inode == llfuse.ROOT_INODE:
442                 llfuse.lock.release()
443
444
445 class TagsDirectory(RecursiveInvalidateDirectory):
446     '''A special directory that contains as subdirectories all tags visible to the user.'''
447
448     def __init__(self, parent_inode, inodes, api, poll_time=60):
449         super(TagsDirectory, self).__init__(parent_inode)
450         self.inodes = inodes
451         self.api = api
452         self._poll = True
453         self._poll_time = poll_time
454
455     def update(self):
456         with llfuse.lock_released:
457             tags = self.api.links().list(filters=[['link_class', '=', 'tag']], select=['name'], distinct = True).execute()
458         if "items" in tags:
459             self.merge(tags['items'],
460                        lambda i: i['name'] if 'name' in i else i['uuid'],
461                        lambda a, i: a.tag == i,
462                        lambda i: TagDirectory(self.inode, self.inodes, self.api, i['name'], poll=self._poll, poll_time=self._poll_time))
463
464
465 class TagDirectory(Directory):
466     '''A special directory that contains as subdirectories all collections visible
467     to the user that are tagged with a particular tag.
468     '''
469
470     def __init__(self, parent_inode, inodes, api, tag, poll=False, poll_time=60):
471         super(TagDirectory, self).__init__(parent_inode)
472         self.inodes = inodes
473         self.api = api
474         self.tag = tag
475         self._poll = poll
476         self._poll_time = poll_time
477
478     def update(self):
479         with llfuse.lock_released:
480             taggedcollections = self.api.links().list(filters=[['link_class', '=', 'tag'],
481                                                    ['name', '=', self.tag],
482                                                    ['head_uuid', 'is_a', 'arvados#collection']],
483                                           select=['head_uuid']).execute()
484         self.merge(taggedcollections['items'],
485                    lambda i: i['head_uuid'],
486                    lambda a, i: a.collection_locator == i['head_uuid'],
487                    lambda i: CollectionDirectory(self.inode, self.inodes, self.api, i['head_uuid']))
488
489
490 class ProjectDirectory(RecursiveInvalidateDirectory):
491     '''A special directory that contains the contents of a project.'''
492
493     def __init__(self, parent_inode, inodes, api, project_object, poll=False, poll_time=60):
494         super(ProjectDirectory, self).__init__(parent_inode)
495         self.inodes = inodes
496         self.api = api
497         self.project_object = project_object
498         self.project_object_file = ObjectFile(self.inode, self.project_object)
499         self.inodes.add_entry(self.project_object_file)
500         self.uuid = project_object['uuid']
501
502     def createDirectory(self, i):
503         if collection_uuid_pattern.match(i['uuid']):
504             return CollectionDirectory(self.inode, self.inodes, self.api, i)
505         elif group_uuid_pattern.match(i['uuid']):
506             return ProjectDirectory(self.inode, self.inodes, self.api, i, self._poll, self._poll_time)
507         elif link_uuid_pattern.match(i['uuid']):
508             if i['head_kind'] == 'arvados#collection' or portable_data_hash_pattern.match(i['head_uuid']):
509                 return CollectionDirectory(self.inode, self.inodes, self.api, i['head_uuid'])
510             else:
511                 return None
512         elif uuid_pattern.match(i['uuid']):
513             return ObjectFile(self.parent_inode, i)
514         else:
515             return None
516
517     def update(self):
518         def namefn(i):
519             if 'name' in i:
520                 if i['name'] is None or len(i['name']) == 0:
521                     return None
522                 elif collection_uuid_pattern.match(i['uuid']) or group_uuid_pattern.match(i['uuid']):
523                     # collection or subproject
524                     return i['name']
525                 elif link_uuid_pattern.match(i['uuid']) and i['head_kind'] == 'arvados#collection':
526                     # name link
527                     return i['name']
528                 elif 'kind' in i and i['kind'].startswith('arvados#'):
529                     # something else
530                     return "{}.{}".format(i['name'], i['kind'][8:])                    
531             else:
532                 return None
533
534         def samefn(a, i):
535             if isinstance(a, CollectionDirectory):
536                 return a.collection_locator == i['uuid']
537             elif isinstance(a, ProjectDirectory):
538                 return a.uuid == i['uuid']
539             elif isinstance(a, ObjectFile):
540                 return a.uuid == i['uuid'] and not a.stale()
541             return False
542
543         with llfuse.lock_released:
544             if group_uuid_pattern.match(self.uuid):
545                 self.project_object = self.api.groups().get(uuid=self.uuid).execute()
546             elif user_uuid_pattern.match(self.uuid):
547                 self.project_object = self.api.users().get(uuid=self.uuid).execute()
548
549             contents = arvados.util.list_all(self.api.groups().contents, uuid=self.uuid)
550             # Name links will be obsolete soon, take this out when there are no more pre-#3036 in use.
551             contents += arvados.util.list_all(self.api.links().list, filters=[['tail_uuid', '=', self.uuid], ['link_class', '=', 'name']])
552             
553         # end with llfuse.lock_released, re-acquire lock
554
555         self.merge(contents,
556                    namefn,
557                    samefn,
558                    self.createDirectory)
559
560     def __getitem__(self, item):
561         self.checkupdate()
562         if item == '.arvados#project':
563             return self.project_object_file
564         else:
565             return super(ProjectDirectory, self).__getitem__(item)
566
567     def __contains__(self, k):
568         if k == '.arvados#project':
569             return True
570         else:
571             return super(ProjectDirectory, self).__contains__(k)
572
573
574 class SharedDirectory(RecursiveInvalidateDirectory):
575     '''A special directory that represents users or groups who have shared projects with me.'''
576
577     def __init__(self, parent_inode, inodes, api, exclude, poll=False, poll_time=60):
578         super(SharedDirectory, self).__init__(parent_inode)
579         self.current_user = api.users().current().execute()
580         self.inodes = inodes
581         self.api = api
582         self._poll = True
583         self._poll_time = poll_time
584
585     def update(self):
586         with llfuse.lock_released:
587             all_projects = arvados.util.list_all(self.api.groups().list, filters=[['group_class','=','project']])
588             objects = {}
589             for ob in all_projects:
590                 objects[ob['uuid']] = ob
591
592             roots = []
593             root_owners = {}
594             for ob in all_projects:
595                 if ob['owner_uuid'] != self.current_user['uuid'] and ob['owner_uuid'] not in objects:
596                     roots.append(ob)
597                     root_owners[ob['owner_uuid']] = True
598
599             lusers = arvados.util.list_all(self.api.users().list, filters=[['uuid','in', list(root_owners)]])
600             lgroups = arvados.util.list_all(self.api.groups().list, filters=[['uuid','in', list(root_owners)]])
601
602             users = {}
603             groups = {}
604
605             for l in lusers:
606                 objects[l["uuid"]] = l
607             for l in lgroups:
608                 objects[l["uuid"]] = l
609
610             contents = {}
611             for r in root_owners:
612                 if r in objects:
613                     obr = objects[r]
614                     if "name" in obr:
615                         contents[obr["name"]] = obr
616                     if "first_name" in obr:
617                         contents[u"{} {}".format(obr["first_name"], obr["last_name"])] = obr
618
619             for r in roots:
620                 if r['owner_uuid'] not in objects:
621                     contents[r['name']] = r
622
623         # end with llfuse.lock_released, re-acquire lock
624
625         try:
626             self.merge(contents.items(),
627                        lambda i: i[0],
628                        lambda a, i: a.uuid == i[1]['uuid'],
629                        lambda i: ProjectDirectory(self.inode, self.inodes, self.api, i[1], poll=self._poll, poll_time=self._poll_time))
630         except Exception as e:
631             _logger.exception(e)
632
633
634 class FileHandle(object):
635     '''Connects a numeric file handle to a File or Directory object that has
636     been opened by the client.'''
637
638     def __init__(self, fh, entry):
639         self.fh = fh
640         self.entry = entry
641
642
643 class Inodes(object):
644     '''Manage the set of inodes.  This is the mapping from a numeric id
645     to a concrete File or Directory object'''
646
647     def __init__(self):
648         self._entries = {}
649         self._counter = llfuse.ROOT_INODE
650
651     def __getitem__(self, item):
652         return self._entries[item]
653
654     def __setitem__(self, key, item):
655         self._entries[key] = item
656
657     def __iter__(self):
658         return self._entries.iterkeys()
659
660     def items(self):
661         return self._entries.items()
662
663     def __contains__(self, k):
664         return k in self._entries
665
666     def add_entry(self, entry):
667         entry.inode = self._counter
668         self._entries[entry.inode] = entry
669         self._counter += 1
670         return entry
671
672     def del_entry(self, entry):
673         llfuse.invalidate_inode(entry.inode)
674         del self._entries[entry.inode]
675
676 class Operations(llfuse.Operations):
677     '''This is the main interface with llfuse.  The methods on this object are
678     called by llfuse threads to service FUSE events to query and read from
679     the file system.
680
681     llfuse has its own global lock which is acquired before calling a request handler,
682     so request handlers do not run concurrently unless the lock is explicitly released
683     using "with llfuse.lock_released:"'''
684
685     def __init__(self, uid, gid):
686         super(Operations, self).__init__()
687
688         self.inodes = Inodes()
689         self.uid = uid
690         self.gid = gid
691
692         # dict of inode to filehandle
693         self._filehandles = {}
694         self._filehandles_counter = 1
695
696         # Other threads that need to wait until the fuse driver
697         # is fully initialized should wait() on this event object.
698         self.initlock = threading.Event()
699
700     def init(self):
701         # Allow threads that are waiting for the driver to be finished
702         # initializing to continue
703         self.initlock.set()
704
705     def access(self, inode, mode, ctx):
706         return True
707
708     def getattr(self, inode):
709         if inode not in self.inodes:
710             raise llfuse.FUSEError(errno.ENOENT)
711
712         e = self.inodes[inode]
713
714         entry = llfuse.EntryAttributes()
715         entry.st_ino = inode
716         entry.generation = 0
717         entry.entry_timeout = 300
718         entry.attr_timeout = 300
719
720         entry.st_mode = stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH
721         if isinstance(e, Directory):
722             entry.st_mode |= stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH | stat.S_IFDIR
723         else:
724             entry.st_mode |= stat.S_IFREG
725
726         entry.st_nlink = 1
727         entry.st_uid = self.uid
728         entry.st_gid = self.gid
729         entry.st_rdev = 0
730
731         entry.st_size = e.size()
732
733         entry.st_blksize = 512
734         entry.st_blocks = (e.size()/512)+1
735         entry.st_atime = int(e.atime())
736         entry.st_mtime = int(e.mtime())
737         entry.st_ctime = int(e.mtime())
738
739         return entry
740
741     def lookup(self, parent_inode, name):
742         _logger.debug("arv-mount lookup: parent_inode %i name %s",
743                       parent_inode, name)
744         inode = None
745
746         if name == '.':
747             inode = parent_inode
748         else:
749             if parent_inode in self.inodes:
750                 p = self.inodes[parent_inode]
751                 if name == '..':
752                     inode = p.parent_inode
753                 elif name in p:
754                     inode = p[name].inode
755
756         if inode != None:
757             return self.getattr(inode)
758         else:
759             raise llfuse.FUSEError(errno.ENOENT)
760
761     def open(self, inode, flags):
762         if inode in self.inodes:
763             p = self.inodes[inode]
764         else:
765             raise llfuse.FUSEError(errno.ENOENT)
766
767         if (flags & os.O_WRONLY) or (flags & os.O_RDWR):
768             raise llfuse.FUSEError(errno.EROFS)
769
770         if isinstance(p, Directory):
771             raise llfuse.FUSEError(errno.EISDIR)
772
773         fh = self._filehandles_counter
774         self._filehandles_counter += 1
775         self._filehandles[fh] = FileHandle(fh, p)
776         return fh
777
778     def read(self, fh, off, size):
779         _logger.debug("arv-mount read %i %i %i", fh, off, size)
780         if fh in self._filehandles:
781             handle = self._filehandles[fh]
782         else:
783             raise llfuse.FUSEError(errno.EBADF)
784
785         try:
786             with llfuse.lock_released:
787                 return handle.entry.readfrom(off, size)
788         except:
789             raise llfuse.FUSEError(errno.EIO)
790
791     def release(self, fh):
792         if fh in self._filehandles:
793             del self._filehandles[fh]
794
795     def opendir(self, inode):
796         _logger.debug("arv-mount opendir: inode %i", inode)
797
798         if inode in self.inodes:
799             p = self.inodes[inode]
800         else:
801             raise llfuse.FUSEError(errno.ENOENT)
802
803         if not isinstance(p, Directory):
804             raise llfuse.FUSEError(errno.ENOTDIR)
805
806         fh = self._filehandles_counter
807         self._filehandles_counter += 1
808         if p.parent_inode in self.inodes:
809             parent = self.inodes[p.parent_inode]
810         else:
811             raise llfuse.FUSEError(errno.EIO)
812
813         self._filehandles[fh] = FileHandle(fh, [('.', p), ('..', parent)] + list(p.items()))
814         return fh
815
816     def readdir(self, fh, off):
817         _logger.debug("arv-mount readdir: fh %i off %i", fh, off)
818
819         if fh in self._filehandles:
820             handle = self._filehandles[fh]
821         else:
822             raise llfuse.FUSEError(errno.EBADF)
823
824         _logger.debug("arv-mount handle.entry %s", handle.entry)
825
826         e = off
827         while e < len(handle.entry):
828             if handle.entry[e][1].inode in self.inodes:
829                 yield (handle.entry[e][0], self.getattr(handle.entry[e][1].inode), e+1)
830             e += 1
831
832     def releasedir(self, fh):
833         del self._filehandles[fh]
834
835     def statfs(self):
836         st = llfuse.StatvfsData()
837         st.f_bsize = 64 * 1024
838         st.f_blocks = 0
839         st.f_files = 0
840
841         st.f_bfree = 0
842         st.f_bavail = 0
843
844         st.f_ffree = 0
845         st.f_favail = 0
846
847         st.f_frsize = 0
848         return st
849
850     # The llfuse documentation recommends only overloading functions that
851     # are actually implemented, as the default implementation will raise ENOSYS.
852     # However, there is a bug in the llfuse default implementation of create()
853     # "create() takes exactly 5 positional arguments (6 given)" which will crash
854     # arv-mount.
855     # The workaround is to implement it with the proper number of parameters,
856     # and then everything works out.
857     def create(self, p1, p2, p3, p4, p5):
858         raise llfuse.FUSEError(errno.EROFS)