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