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