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