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