3644: Default mount mode now includes home, shared, by_hash, and by_tag.
[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             if re.match(r'^[a-f0-9]{32}', self.collection_locator):
261                 return
262             #with llfuse.lock_released:
263             new_collection_object = self.api.collections().get(uuid=self.collection_locator).execute()
264             if "portable_data_hash" not in new_collection_object:
265                 new_collection_object["portable_data_hash"] = new_collection_object["uuid"]
266
267             if self.collection_object is None or self.collection_object["portable_data_hash"] != new_collection_object["portable_data_hash"]:
268                 self.collection_object = new_collection_object
269
270                 if self.manifest_text_file is not None:
271                     self.manifest_text_file.contents = self.collection_object["manifest_text"]
272                     self.manifest_text_file._ctime = self.ctime()
273                     self.manifest_text_file._mtime = self.mtime()
274                 if self.pdh_file is not None:
275                     self.pdh_file.contents = self.collection_object["portable_data_hash"]
276                     self.pdh_file._ctime = self.ctime()
277                     self.pdh_file._mtime = self.mtime()
278
279                 self.clear()
280                 collection = arvados.CollectionReader(self.collection_object["manifest_text"], self.api)
281                 for s in collection.all_streams():
282                     cwd = self
283                     for part in s.name().split('/'):
284                         if part != '' and part != '.':
285                             partname = sanitize_filename(part)
286                             if partname not in cwd._entries:
287                                 cwd._entries[partname] = self.inodes.add_entry(Directory(cwd.inode))
288                             cwd = cwd._entries[partname]
289                     for k, v in s.files().items():
290                         cwd._entries[sanitize_filename(k)] = self.inodes.add_entry(StreamReaderFile(cwd.inode, v, self.ctime(), self.mtime()))
291             self.fresh()
292         except Exception as detail:
293             _logger.error("arv-mount %s: error", self.collection_locator)
294             _logger.exception(detail)
295             return False
296
297     def __getitem__(self, item):
298         self.checkupdate()
299         if item == '.manifest_text':
300             if self.manifest_text_file is None:
301                 self.manifest_text_file = StringFile(self.inode, self.collection_object["manifest_text"], self.ctime(), self.mtime())
302                 self.inodes.add_entry(self.manifest_text_file)
303             return self.manifest_text_file
304         elif item == '.portable_data_hash':
305             if self.pdh_file is None:
306                 self.pdh_file = StringFile(self.inode, self.collection_object["portable_data_hash"], self.ctime(), self.mtime())
307                 self.inodes.add_entry(self.pdh_file)
308             return self.pdh_file
309         else:
310             return super(CollectionDirectory, self).__getitem__(item)
311
312     def __contains__(self, k):
313         if k in ('.manifest_text', '.portable_data_hash'):
314             return True
315         else:
316             return super(CollectionDirectory, self).__contains__(k)
317
318     def ctime(self):
319         self.checkupdate()
320         return convertTime(self.collection_object["created_at"])
321
322     def mtime(self):
323         self.checkupdate()
324         return convertTime(self.collection_object["modified_at"])
325
326 class MagicDirectory(Directory):
327     '''A special directory that logically contains the set of all extant keep
328     locators.  When a file is referenced by lookup(), it is tested to see if it
329     is a valid keep locator to a manifest, and if so, loads the manifest
330     contents as a subdirectory of this directory with the locator as the
331     directory name.  Since querying a list of all extant keep locators is
332     impractical, only collections that have already been accessed are visible
333     to readdir().
334     '''
335
336     def __init__(self, parent_inode, inodes, api):
337         super(MagicDirectory, self).__init__(parent_inode)
338         self.inodes = inodes
339         self.api = api
340
341     def __contains__(self, k):
342         if k in self._entries:
343             return True
344         try:
345             e = self.inodes.add_entry(CollectionDirectory(self.inode, self.inodes, self.api, k))
346             if e.update():
347                 self._entries[k] = e
348                 return True
349             else:
350                 return False
351         except Exception as e:
352             _logger.debug('arv-mount exception keep %s', e)
353             return False
354
355     def __getitem__(self, item):
356         if item in self:
357             return self._entries[item]
358         else:
359             raise KeyError("No collection with id " + item)
360
361 class RecursiveInvalidateDirectory(Directory):
362     def invalidate(self):
363         if self.inode == llfuse.ROOT_INODE:
364             llfuse.lock.acquire()
365         try:
366             super(RecursiveInvalidateDirectory, self).invalidate()
367             for a in self._entries:
368                 self._entries[a].invalidate()
369         except Exception as e:
370             _logger.exception(e)
371         finally:
372             if self.inode == llfuse.ROOT_INODE:
373                 llfuse.lock.release()
374
375 class TagsDirectory(RecursiveInvalidateDirectory):
376     '''A special directory that contains as subdirectories all tags visible to the user.'''
377
378     def __init__(self, parent_inode, inodes, api, poll_time=60):
379         super(TagsDirectory, self).__init__(parent_inode)
380         self.inodes = inodes
381         self.api = api
382         #try:
383         #    arvados.events.subscribe(self.api, [['object_uuid', 'is_a', 'arvados#link']], lambda ev: self.invalidate())
384         #except:
385         self._poll = True
386         self._poll_time = poll_time
387
388     def update(self):
389         tags = self.api.links().list(filters=[['link_class', '=', 'tag']], select=['name'], distinct = True).execute()
390         if "items" in tags:
391             self.merge(tags['items'],
392                        lambda i: i['name'] if 'name' in i else i['uuid'],
393                        lambda a, i: a.tag == i,
394                        lambda i: TagDirectory(self.inode, self.inodes, self.api, i['name'], poll=self._poll, poll_time=self._poll_time))
395
396 class TagDirectory(Directory):
397     '''A special directory that contains as subdirectories all collections visible
398     to the user that are tagged with a particular tag.
399     '''
400
401     def __init__(self, parent_inode, inodes, api, tag, poll=False, poll_time=60):
402         super(TagDirectory, self).__init__(parent_inode)
403         self.inodes = inodes
404         self.api = api
405         self.tag = tag
406         self._poll = poll
407         self._poll_time = poll_time
408
409     def update(self):
410         taggedcollections = self.api.links().list(filters=[['link_class', '=', 'tag'],
411                                                ['name', '=', self.tag],
412                                                ['head_uuid', 'is_a', 'arvados#collection']],
413                                       select=['head_uuid']).execute()
414         self.merge(taggedcollections['items'],
415                    lambda i: i['head_uuid'],
416                    lambda a, i: a.collection_locator == i['head_uuid'],
417                    lambda i: CollectionDirectory(self.inode, self.inodes, self.api, i['head_uuid']))
418
419
420 class ProjectDirectory(RecursiveInvalidateDirectory):
421     '''A special directory that contains the contents of a project.'''
422
423     def __init__(self, parent_inode, inodes, api, project_object, poll=False, poll_time=60):
424         super(ProjectDirectory, self).__init__(parent_inode)
425         self.inodes = inodes
426         self.api = api
427         self.project_object = project_object
428         self.uuid = project_object['uuid']
429
430     def createDirectory(self, i):
431         if re.match(r'[a-z0-9]{5}-4zz18-[a-z0-9]{15}', i['uuid']):
432             return CollectionDirectory(self.inode, self.inodes, self.api, i['uuid'])
433         elif re.match(r'[a-z0-9]{5}-j7d0g-[a-z0-9]{15}', i['uuid']):
434             return ProjectDirectory(self.inode, self.inodes, self.api, i, self._poll, self._poll_time)
435         elif re.match(r'[a-z0-9]{5}-o0j2j-[a-z0-9]{15}', i['uuid']) and i['head_kind'] == 'arvados#collection':
436             return CollectionDirectory(self.inode, self.inodes, self.api, i['head_uuid'])
437         #elif re.match(r'[a-z0-9]{5}-8i9sb-[a-z0-9]{15}', i['uuid']):
438         #    return None
439         elif re.match(r'[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}', i['uuid']):
440             return ObjectFile(self.parent_inode, i)
441         else:
442             return None
443
444     def update(self):
445         def namefn(i):
446             if 'name' in i:
447                 if i['name'] is None:
448                     return None
449                 elif re.match(r'[a-z0-9]{5}-(4zz18|j7d0g)-[a-z0-9]{15}', i['uuid']):
450                     # collection or subproject
451                     return i['name']
452                 elif re.match(r'[a-z0-9]{5}-o0j2j-[a-z0-9]{15}', i['uuid']) and i['head_kind'] == 'arvados#collection':
453                     # name link
454                     return i['name']
455                 elif 'kind' in i and i['kind'].startswith('arvados#'):
456                     # something else
457                     return "{}.{}".format(i['name'], i['kind'][8:])                    
458             else:
459                 return None
460
461         def samefn(a, i):
462             if isinstance(a, CollectionDirectory):
463                 return a.collection_locator == i['uuid']
464             elif isinstance(a, ProjectDirectory):
465                 return a.uuid == i['uuid']
466             elif isinstance(a, ObjectFile):
467                 return a.uuid == i['uuid'] and not a.stale()
468             return False
469
470         #with llfuse.lock_released:
471         if re.match(r'[a-z0-9]{5}-j7d0g-[a-z0-9]{15}', self.uuid):
472             self.project_object = self.api.groups().get(uuid=self.uuid).execute()
473         elif re.match(r'[a-z0-9]{5}-tpzed-[a-z0-9]{15}', self.uuid):
474             self.project_object = self.api.users().get(uuid=self.uuid).execute()
475
476         contents = arvados.util.list_all(self.api.groups().contents, uuid=self.uuid)
477         # Name links will be obsolete soon, take this out when there are no more pre-#3036 in use.
478         contents += arvados.util.list_all(self.api.links().list, filters=[['tail_uuid', '=', self.uuid], ['link_class', '=', 'name']])
479
480         #print contents
481
482         self.merge(contents,
483                    namefn,
484                    samefn,
485                    self.createDirectory)
486
487     def ctime(self):
488         return convertTime(self.project_object["created_at"]) if "created_at" in self.project_object else 0
489
490     def mtime(self):
491         return convertTime(self.project_object["modified_at"]) if "modified_at" in self.project_object  else 0
492
493
494
495 class SharedDirectory(RecursiveInvalidateDirectory):
496     '''A special directory that represents users or groups who have shared projects with me.'''
497
498     def __init__(self, parent_inode, inodes, api, exclude, poll=False, poll_time=60):
499         super(SharedDirectory, self).__init__(parent_inode)
500         self.current_user = api.users().current().execute()
501         self.inodes = inodes
502         self.api = api
503
504         # try:
505         #     arvados.events.subscribe(self.api, [], lambda ev: self.invalidate())
506         # except:
507         self._poll = True
508         self._poll_time = poll_time
509
510     def update(self):
511         #with llfuse.lock_released:
512         all_projects = arvados.util.list_all(self.api.groups().list, filters=[['group_class','=','project']])
513         objects = {}
514         for ob in all_projects:
515             objects[ob['uuid']] = ob
516
517         roots = []
518         root_owners = {}
519         for ob in all_projects:
520             if ob['owner_uuid'] != self.current_user['uuid'] and ob['owner_uuid'] not in objects:
521                 roots.append(ob)
522                 root_owners[ob['owner_uuid']] = True
523
524         #with llfuse.lock_released:
525         lusers = arvados.util.list_all(self.api.users().list, filters=[['uuid','in', list(root_owners)]])
526         lgroups = arvados.util.list_all(self.api.groups().list, filters=[['uuid','in', list(root_owners)]])
527
528         users = {}
529         groups = {}
530
531         for l in lusers:
532             objects[l["uuid"]] = l
533         for l in lgroups:
534             objects[l["uuid"]] = l
535
536         contents = {}
537         for r in root_owners:
538             if r in objects:
539                 obr = objects[r]
540                 if "name" in obr:
541                     contents[obr["name"]] = obr
542                 if "first_name" in obr:
543                     contents[u"{} {}".format(obr["first_name"], obr["last_name"])] = obr
544
545         for r in roots:
546             if r['owner_uuid'] not in objects:
547                 contents[r['name']] = r
548         
549         try:
550             self.merge(contents.items(),
551                        lambda i: i[0],
552                        lambda a, i: a.uuid == i[1]['uuid'],
553                        lambda i: ProjectDirectory(self.inode, self.inodes, self.api, i[1], poll=self._poll, poll_time=self._poll_time))
554         except Exception as e:
555             _logger.exception(e)
556
557
558 class FileHandle(object):
559     '''Connects a numeric file handle to a File or Directory object that has
560     been opened by the client.'''
561
562     def __init__(self, fh, entry):
563         self.fh = fh
564         self.entry = entry
565
566
567 class Inodes(object):
568     '''Manage the set of inodes.  This is the mapping from a numeric id
569     to a concrete File or Directory object'''
570
571     def __init__(self):
572         self._entries = {}
573         self._counter = llfuse.ROOT_INODE
574
575     def __getitem__(self, item):
576         return self._entries[item]
577
578     def __setitem__(self, key, item):
579         self._entries[key] = item
580
581     def __iter__(self):
582         return self._entries.iterkeys()
583
584     def items(self):
585         return self._entries.items()
586
587     def __contains__(self, k):
588         return k in self._entries
589
590     def add_entry(self, entry):
591         entry.inode = self._counter
592         self._entries[entry.inode] = entry
593         self._counter += 1
594         return entry
595
596     def del_entry(self, entry):
597         llfuse.invalidate_inode(entry.inode)
598         del self._entries[entry.inode]
599
600 class Operations(llfuse.Operations):
601     '''This is the main interface with llfuse.  The methods on this object are
602     called by llfuse threads to service FUSE events to query and read from
603     the file system.
604
605     llfuse has its own global lock which is acquired before calling a request handler,
606     so request handlers do not run concurrently unless the lock is explicitly released
607     with llfuse.lock_released.'''
608
609     def __init__(self, uid, gid):
610         super(Operations, self).__init__()
611
612         self.inodes = Inodes()
613         self.uid = uid
614         self.gid = gid
615
616         # dict of inode to filehandle
617         self._filehandles = {}
618         self._filehandles_counter = 1
619
620         # Other threads that need to wait until the fuse driver
621         # is fully initialized should wait() on this event object.
622         self.initlock = threading.Event()
623
624     def init(self):
625         # Allow threads that are waiting for the driver to be finished
626         # initializing to continue
627         self.initlock.set()
628
629     def access(self, inode, mode, ctx):
630         return True
631
632     def getattr(self, inode):
633         if inode not in self.inodes:
634             raise llfuse.FUSEError(errno.ENOENT)
635
636         e = self.inodes[inode]
637
638         entry = llfuse.EntryAttributes()
639         entry.st_ino = inode
640         entry.generation = 0
641         entry.entry_timeout = 300
642         entry.attr_timeout = 300
643
644         entry.st_mode = stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH
645         if isinstance(e, Directory):
646             entry.st_mode |= stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH | stat.S_IFDIR
647         else:
648             entry.st_mode |= stat.S_IFREG
649
650         entry.st_nlink = 1
651         entry.st_uid = self.uid
652         entry.st_gid = self.gid
653         entry.st_rdev = 0
654
655         entry.st_size = e.size()
656
657         entry.st_blksize = 512
658         entry.st_blocks = (e.size()/512)
659         if e.size()/512 != 0:
660             entry.st_blocks += 1
661         entry.st_atime = 0
662         entry.st_mtime = e.mtime()
663         entry.st_ctime = e.ctime()
664
665         return entry
666
667     def lookup(self, parent_inode, name):
668         _logger.debug("arv-mount lookup: parent_inode %i name %s",
669                       parent_inode, name)
670         inode = None
671
672         if name == '.':
673             inode = parent_inode
674         else:
675             if parent_inode in self.inodes:
676                 p = self.inodes[parent_inode]
677                 if name == '..':
678                     inode = p.parent_inode
679                 elif name in p:
680                     inode = p[name].inode
681
682         if inode != None:
683             return self.getattr(inode)
684         else:
685             raise llfuse.FUSEError(errno.ENOENT)
686
687     def open(self, inode, flags):
688         if inode in self.inodes:
689             p = self.inodes[inode]
690         else:
691             raise llfuse.FUSEError(errno.ENOENT)
692
693         if (flags & os.O_WRONLY) or (flags & os.O_RDWR):
694             raise llfuse.FUSEError(errno.EROFS)
695
696         if isinstance(p, Directory):
697             raise llfuse.FUSEError(errno.EISDIR)
698
699         fh = self._filehandles_counter
700         self._filehandles_counter += 1
701         self._filehandles[fh] = FileHandle(fh, p)
702         return fh
703
704     def read(self, fh, off, size):
705         _logger.debug("arv-mount read %i %i %i", fh, off, size)
706         if fh in self._filehandles:
707             handle = self._filehandles[fh]
708         else:
709             raise llfuse.FUSEError(errno.EBADF)
710
711         try:
712             with llfuse.lock_released:
713                 return handle.entry.readfrom(off, size)
714         except:
715             raise llfuse.FUSEError(errno.EIO)
716
717     def release(self, fh):
718         if fh in self._filehandles:
719             del self._filehandles[fh]
720
721     def opendir(self, inode):
722         _logger.debug("arv-mount opendir: inode %i", inode)
723
724         if inode in self.inodes:
725             p = self.inodes[inode]
726         else:
727             raise llfuse.FUSEError(errno.ENOENT)
728
729         if not isinstance(p, Directory):
730             raise llfuse.FUSEError(errno.ENOTDIR)
731
732         fh = self._filehandles_counter
733         self._filehandles_counter += 1
734         if p.parent_inode in self.inodes:
735             parent = self.inodes[p.parent_inode]
736         else:
737             raise llfuse.FUSEError(errno.EIO)
738
739         self._filehandles[fh] = FileHandle(fh, [('.', p), ('..', parent)] + list(p.items()))
740         return fh
741
742     def readdir(self, fh, off):
743         _logger.debug("arv-mount readdir: fh %i off %i", fh, off)
744
745         if fh in self._filehandles:
746             handle = self._filehandles[fh]
747         else:
748             raise llfuse.FUSEError(errno.EBADF)
749
750         _logger.debug("arv-mount handle.entry %s", handle.entry)
751
752         e = off
753         while e < len(handle.entry):
754             if handle.entry[e][1].inode in self.inodes:
755                 yield (handle.entry[e][0], self.getattr(handle.entry[e][1].inode), e+1)
756             e += 1
757
758     def releasedir(self, fh):
759         del self._filehandles[fh]
760
761     def statfs(self):
762         st = llfuse.StatvfsData()
763         st.f_bsize = 64 * 1024
764         st.f_blocks = 0
765         st.f_files = 0
766
767         st.f_bfree = 0
768         st.f_bavail = 0
769
770         st.f_ffree = 0
771         st.f_favail = 0
772
773         st.f_frsize = 0
774         return st
775
776     # The llfuse documentation recommends only overloading functions that
777     # are actually implemented, as the default implementation will raise ENOSYS.
778     # However, there is a bug in the llfuse default implementation of create()
779     # "create() takes exactly 5 positional arguments (6 given)" which will crash
780     # arv-mount.
781     # The workaround is to implement it with the proper number of parameters,
782     # and then everything works out.
783     def create(self, p1, p2, p3, p4, p5):
784         raise llfuse.FUSEError(errno.EROFS)