Merge branch '3187-pipeline-instance-page' into 3605-improved-dashboard
[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.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, self.token, self.insecure)
42         return self.local.api
43
44     def localkeep(self):
45         if 'keep' not in self.local.__dict__:
46             self.local.keep = arvados.KeepClient(api_client=self.localapi(), block_cache=self.block_cache)
47         return self.local.keep
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, self.api.localkeep())
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 arvados.errors.ArgumentError as detail:
351             _logger.warning("arv-mount %s: error %s", self.collection_locator, detail)
352             if self.collection_object is not None and "manifest_text" in self.collection_object:
353                 _logger.warning("arv-mount manifest_text is: %s", self.collection_object["manifest_text"])
354         except Exception as detail:
355             _logger.error("arv-mount %s: error", self.collection_locator)
356             if self.collection_object is not None and "manifest_text" in self.collection_object:
357                 _logger.error("arv-mount manifest_text is: %s", self.collection_object["manifest_text"])
358             _logger.exception(detail)
359         return False
360
361     def __getitem__(self, item):
362         self.checkupdate()
363         if item == '.arvados#collection':
364             if self.collection_object_file is None:
365                 self.collection_object_file = ObjectFile(self.inode, self.collection_object)
366                 self.inodes.add_entry(self.collection_object_file)
367             return self.collection_object_file
368         else:
369             return super(CollectionDirectory, self).__getitem__(item)
370
371     def __contains__(self, k):
372         if k == '.arvados#collection':
373             return True
374         else:
375             return super(CollectionDirectory, self).__contains__(k)
376
377     def mtime(self):
378         self.checkupdate()
379         return convertTime(self.collection_object["modified_at"]) if self.collection_object is not None and 'modified_at' in self.collection_object else 0
380
381
382 class MagicDirectory(Directory):
383     '''A special directory that logically contains the set of all extant keep
384     locators.  When a file is referenced by lookup(), it is tested to see if it
385     is a valid keep locator to a manifest, and if so, loads the manifest
386     contents as a subdirectory of this directory with the locator as the
387     directory name.  Since querying a list of all extant keep locators is
388     impractical, only collections that have already been accessed are visible
389     to readdir().
390     '''
391
392     def __init__(self, parent_inode, inodes, api):
393         super(MagicDirectory, self).__init__(parent_inode)
394         self.inodes = inodes
395         self.api = api
396         # Have to defer creating readme_file because at this point we don't
397         # yet have an inode assigned.
398         self.readme_file = None
399
400     def create_readme(self):
401         if self.readme_file is None:
402             text = '''This directory provides access to Arvados collections as subdirectories listed
403 by uuid (in the form 'zzzzz-4zz18-1234567890abcde') or portable data hash (in
404 the form '1234567890abcdefghijklmnopqrstuv+123').
405
406 Note that this directory will appear empty until you attempt to access a
407 specific collection subdirectory (such as trying to 'cd' into it), at which
408 point the collection will actually be looked up on the server and the directory
409 will appear if it exists.
410 '''
411             self.readme_file = self.inodes.add_entry(StringFile(self.inode, text, time.time()))
412             self._entries["README"] = self.readme_file
413
414     def __contains__(self, k):
415         self.create_readme()
416
417         if k in self._entries:
418             return True
419
420         if not portable_data_hash_pattern.match(k) and not uuid_pattern.match(k):
421             return False
422
423         try:
424             e = self.inodes.add_entry(CollectionDirectory(self.inode, self.inodes, self.api, k))
425             if e.update():
426                 self._entries[k] = e
427                 return True
428             else:
429                 return False
430         except Exception as e:
431             _logger.debug('arv-mount exception keep %s', e)
432             return False
433
434     def items(self):
435         self.create_readme()
436         return self._entries.items()
437
438     def __getitem__(self, item):
439         if item in self:
440             return self._entries[item]
441         else:
442             raise KeyError("No collection with id " + item)
443
444
445 class RecursiveInvalidateDirectory(Directory):
446     def invalidate(self):
447         if self.inode == llfuse.ROOT_INODE:
448             llfuse.lock.acquire()
449         try:
450             super(RecursiveInvalidateDirectory, self).invalidate()
451             for a in self._entries:
452                 self._entries[a].invalidate()
453         except Exception as e:
454             _logger.exception(e)
455         finally:
456             if self.inode == llfuse.ROOT_INODE:
457                 llfuse.lock.release()
458
459
460 class TagsDirectory(RecursiveInvalidateDirectory):
461     '''A special directory that contains as subdirectories all tags visible to the user.'''
462
463     def __init__(self, parent_inode, inodes, api, poll_time=60):
464         super(TagsDirectory, self).__init__(parent_inode)
465         self.inodes = inodes
466         self.api = api
467         self._poll = True
468         self._poll_time = poll_time
469
470     def update(self):
471         with llfuse.lock_released:
472             tags = self.api.links().list(filters=[['link_class', '=', 'tag']], select=['name'], distinct = True).execute()
473         if "items" in tags:
474             self.merge(tags['items'],
475                        lambda i: i['name'] if 'name' in i else i['uuid'],
476                        lambda a, i: a.tag == i,
477                        lambda i: TagDirectory(self.inode, self.inodes, self.api, i['name'], poll=self._poll, poll_time=self._poll_time))
478
479
480 class TagDirectory(Directory):
481     '''A special directory that contains as subdirectories all collections visible
482     to the user that are tagged with a particular tag.
483     '''
484
485     def __init__(self, parent_inode, inodes, api, tag, poll=False, poll_time=60):
486         super(TagDirectory, self).__init__(parent_inode)
487         self.inodes = inodes
488         self.api = api
489         self.tag = tag
490         self._poll = poll
491         self._poll_time = poll_time
492
493     def update(self):
494         with llfuse.lock_released:
495             taggedcollections = self.api.links().list(filters=[['link_class', '=', 'tag'],
496                                                    ['name', '=', self.tag],
497                                                    ['head_uuid', 'is_a', 'arvados#collection']],
498                                           select=['head_uuid']).execute()
499         self.merge(taggedcollections['items'],
500                    lambda i: i['head_uuid'],
501                    lambda a, i: a.collection_locator == i['head_uuid'],
502                    lambda i: CollectionDirectory(self.inode, self.inodes, self.api, i['head_uuid']))
503
504
505 class ProjectDirectory(Directory):
506     '''A special directory that contains the contents of a project.'''
507
508     def __init__(self, parent_inode, inodes, api, project_object, poll=False, poll_time=60):
509         super(ProjectDirectory, self).__init__(parent_inode)
510         self.inodes = inodes
511         self.api = api
512         self.project_object = project_object
513         self.project_object_file = None
514         self.uuid = project_object['uuid']
515
516     def createDirectory(self, i):
517         if collection_uuid_pattern.match(i['uuid']):
518             return CollectionDirectory(self.inode, self.inodes, self.api, i)
519         elif group_uuid_pattern.match(i['uuid']):
520             return ProjectDirectory(self.inode, self.inodes, self.api, i, self._poll, self._poll_time)
521         elif link_uuid_pattern.match(i['uuid']):
522             if i['head_kind'] == 'arvados#collection' or portable_data_hash_pattern.match(i['head_uuid']):
523                 return CollectionDirectory(self.inode, self.inodes, self.api, i['head_uuid'])
524             else:
525                 return None
526         elif uuid_pattern.match(i['uuid']):
527             return ObjectFile(self.parent_inode, i)
528         else:
529             return None
530
531     def update(self):
532         if self.project_object_file == None:
533             self.project_object_file = ObjectFile(self.inode, self.project_object)
534             self.inodes.add_entry(self.project_object_file)
535
536         def namefn(i):
537             if 'name' in i:
538                 if i['name'] is None or len(i['name']) == 0:
539                     return None
540                 elif collection_uuid_pattern.match(i['uuid']) or group_uuid_pattern.match(i['uuid']):
541                     # collection or subproject
542                     return i['name']
543                 elif link_uuid_pattern.match(i['uuid']) and i['head_kind'] == 'arvados#collection':
544                     # name link
545                     return i['name']
546                 elif 'kind' in i and i['kind'].startswith('arvados#'):
547                     # something else
548                     return "{}.{}".format(i['name'], i['kind'][8:])
549             else:
550                 return None
551
552         def samefn(a, i):
553             if isinstance(a, CollectionDirectory):
554                 return a.collection_locator == i['uuid']
555             elif isinstance(a, ProjectDirectory):
556                 return a.uuid == i['uuid']
557             elif isinstance(a, ObjectFile):
558                 return a.uuid == i['uuid'] and not a.stale()
559             return False
560
561         with llfuse.lock_released:
562             if group_uuid_pattern.match(self.uuid):
563                 self.project_object = self.api.groups().get(uuid=self.uuid).execute()
564             elif user_uuid_pattern.match(self.uuid):
565                 self.project_object = self.api.users().get(uuid=self.uuid).execute()
566
567             contents = arvados.util.list_all(self.api.groups().contents, uuid=self.uuid)
568             # Name links will be obsolete soon, take this out when there are no more pre-#3036 in use.
569             contents += arvados.util.list_all(self.api.links().list, filters=[['tail_uuid', '=', self.uuid], ['link_class', '=', 'name']])
570
571         # end with llfuse.lock_released, re-acquire lock
572
573         self.merge(contents,
574                    namefn,
575                    samefn,
576                    self.createDirectory)
577
578     def __getitem__(self, item):
579         self.checkupdate()
580         if item == '.arvados#project':
581             return self.project_object_file
582         else:
583             return super(ProjectDirectory, self).__getitem__(item)
584
585     def __contains__(self, k):
586         if k == '.arvados#project':
587             return True
588         else:
589             return super(ProjectDirectory, self).__contains__(k)
590
591
592 class SharedDirectory(Directory):
593     '''A special directory that represents users or groups who have shared projects with me.'''
594
595     def __init__(self, parent_inode, inodes, api, exclude, poll=False, poll_time=60):
596         super(SharedDirectory, self).__init__(parent_inode)
597         self.current_user = api.users().current().execute()
598         self.inodes = inodes
599         self.api = api
600         self._poll = True
601         self._poll_time = poll_time
602
603     def update(self):
604         with llfuse.lock_released:
605             all_projects = arvados.util.list_all(self.api.groups().list, filters=[['group_class','=','project']])
606             objects = {}
607             for ob in all_projects:
608                 objects[ob['uuid']] = ob
609
610             roots = []
611             root_owners = {}
612             for ob in all_projects:
613                 if ob['owner_uuid'] != self.current_user['uuid'] and ob['owner_uuid'] not in objects:
614                     roots.append(ob)
615                     root_owners[ob['owner_uuid']] = True
616
617             lusers = arvados.util.list_all(self.api.users().list, filters=[['uuid','in', list(root_owners)]])
618             lgroups = arvados.util.list_all(self.api.groups().list, filters=[['uuid','in', list(root_owners)]])
619
620             users = {}
621             groups = {}
622
623             for l in lusers:
624                 objects[l["uuid"]] = l
625             for l in lgroups:
626                 objects[l["uuid"]] = l
627
628             contents = {}
629             for r in root_owners:
630                 if r in objects:
631                     obr = objects[r]
632                     if "name" in obr:
633                         contents[obr["name"]] = obr
634                     if "first_name" in obr:
635                         contents[u"{} {}".format(obr["first_name"], obr["last_name"])] = obr
636
637             for r in roots:
638                 if r['owner_uuid'] not in objects:
639                     contents[r['name']] = r
640
641         # end with llfuse.lock_released, re-acquire lock
642
643         try:
644             self.merge(contents.items(),
645                        lambda i: i[0],
646                        lambda a, i: a.uuid == i[1]['uuid'],
647                        lambda i: ProjectDirectory(self.inode, self.inodes, self.api, i[1], poll=self._poll, poll_time=self._poll_time))
648         except Exception as e:
649             _logger.exception(e)
650
651
652 class FileHandle(object):
653     '''Connects a numeric file handle to a File or Directory object that has
654     been opened by the client.'''
655
656     def __init__(self, fh, entry):
657         self.fh = fh
658         self.entry = entry
659
660
661 class Inodes(object):
662     '''Manage the set of inodes.  This is the mapping from a numeric id
663     to a concrete File or Directory object'''
664
665     def __init__(self):
666         self._entries = {}
667         self._counter = llfuse.ROOT_INODE
668
669     def __getitem__(self, item):
670         return self._entries[item]
671
672     def __setitem__(self, key, item):
673         self._entries[key] = item
674
675     def __iter__(self):
676         return self._entries.iterkeys()
677
678     def items(self):
679         return self._entries.items()
680
681     def __contains__(self, k):
682         return k in self._entries
683
684     def add_entry(self, entry):
685         entry.inode = self._counter
686         self._entries[entry.inode] = entry
687         self._counter += 1
688         return entry
689
690     def del_entry(self, entry):
691         llfuse.invalidate_inode(entry.inode)
692         del self._entries[entry.inode]
693
694 class Operations(llfuse.Operations):
695     '''This is the main interface with llfuse.  The methods on this object are
696     called by llfuse threads to service FUSE events to query and read from
697     the file system.
698
699     llfuse has its own global lock which is acquired before calling a request handler,
700     so request handlers do not run concurrently unless the lock is explicitly released
701     using "with llfuse.lock_released:"'''
702
703     def __init__(self, uid, gid):
704         super(Operations, self).__init__()
705
706         self.inodes = Inodes()
707         self.uid = uid
708         self.gid = gid
709
710         # dict of inode to filehandle
711         self._filehandles = {}
712         self._filehandles_counter = 1
713
714         # Other threads that need to wait until the fuse driver
715         # is fully initialized should wait() on this event object.
716         self.initlock = threading.Event()
717
718     def init(self):
719         # Allow threads that are waiting for the driver to be finished
720         # initializing to continue
721         self.initlock.set()
722
723     def access(self, inode, mode, ctx):
724         return True
725
726     def getattr(self, inode):
727         if inode not in self.inodes:
728             raise llfuse.FUSEError(errno.ENOENT)
729
730         e = self.inodes[inode]
731
732         entry = llfuse.EntryAttributes()
733         entry.st_ino = inode
734         entry.generation = 0
735         entry.entry_timeout = 300
736         entry.attr_timeout = 300
737
738         entry.st_mode = stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH
739         if isinstance(e, Directory):
740             entry.st_mode |= stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH | stat.S_IFDIR
741         else:
742             entry.st_mode |= stat.S_IFREG
743
744         entry.st_nlink = 1
745         entry.st_uid = self.uid
746         entry.st_gid = self.gid
747         entry.st_rdev = 0
748
749         entry.st_size = e.size()
750
751         entry.st_blksize = 512
752         entry.st_blocks = (e.size()/512)+1
753         entry.st_atime = int(e.atime())
754         entry.st_mtime = int(e.mtime())
755         entry.st_ctime = int(e.mtime())
756
757         return entry
758
759     def lookup(self, parent_inode, name):
760         _logger.debug("arv-mount lookup: parent_inode %i name %s",
761                       parent_inode, name)
762         inode = None
763
764         if name == '.':
765             inode = parent_inode
766         else:
767             if parent_inode in self.inodes:
768                 p = self.inodes[parent_inode]
769                 if name == '..':
770                     inode = p.parent_inode
771                 elif isinstance(p, Directory) and name in p:
772                     inode = p[name].inode
773
774         if inode != None:
775             return self.getattr(inode)
776         else:
777             raise llfuse.FUSEError(errno.ENOENT)
778
779     def open(self, inode, flags):
780         if inode in self.inodes:
781             p = self.inodes[inode]
782         else:
783             raise llfuse.FUSEError(errno.ENOENT)
784
785         if (flags & os.O_WRONLY) or (flags & os.O_RDWR):
786             raise llfuse.FUSEError(errno.EROFS)
787
788         if isinstance(p, Directory):
789             raise llfuse.FUSEError(errno.EISDIR)
790
791         fh = self._filehandles_counter
792         self._filehandles_counter += 1
793         self._filehandles[fh] = FileHandle(fh, p)
794         return fh
795
796     def read(self, fh, off, size):
797         _logger.debug("arv-mount read %i %i %i", fh, off, size)
798         if fh in self._filehandles:
799             handle = self._filehandles[fh]
800         else:
801             raise llfuse.FUSEError(errno.EBADF)
802
803         # update atime
804         handle.entry._atime = time.time()
805
806         try:
807             with llfuse.lock_released:
808                 return handle.entry.readfrom(off, size)
809         except arvados.errors.NotFoundError as e:
810             _logger.warning("Block not found: " + str(e))
811             raise llfuse.FUSEError(errno.EIO)
812         except Exception as e:
813             _logger.exception(e)
814             raise llfuse.FUSEError(errno.EIO)
815
816     def release(self, fh):
817         if fh in self._filehandles:
818             del self._filehandles[fh]
819
820     def opendir(self, inode):
821         _logger.debug("arv-mount opendir: inode %i", inode)
822
823         if inode in self.inodes:
824             p = self.inodes[inode]
825         else:
826             raise llfuse.FUSEError(errno.ENOENT)
827
828         if not isinstance(p, Directory):
829             raise llfuse.FUSEError(errno.ENOTDIR)
830
831         fh = self._filehandles_counter
832         self._filehandles_counter += 1
833         if p.parent_inode in self.inodes:
834             parent = self.inodes[p.parent_inode]
835         else:
836             raise llfuse.FUSEError(errno.EIO)
837
838         # update atime
839         p._atime = time.time()
840
841         self._filehandles[fh] = FileHandle(fh, [('.', p), ('..', parent)] + list(p.items()))
842         return fh
843
844     def readdir(self, fh, off):
845         _logger.debug("arv-mount readdir: fh %i off %i", fh, off)
846
847         if fh in self._filehandles:
848             handle = self._filehandles[fh]
849         else:
850             raise llfuse.FUSEError(errno.EBADF)
851
852         _logger.debug("arv-mount handle.entry %s", handle.entry)
853
854         e = off
855         while e < len(handle.entry):
856             if handle.entry[e][1].inode in self.inodes:
857                 yield (handle.entry[e][0], self.getattr(handle.entry[e][1].inode), e+1)
858             e += 1
859
860     def releasedir(self, fh):
861         del self._filehandles[fh]
862
863     def statfs(self):
864         st = llfuse.StatvfsData()
865         st.f_bsize = 64 * 1024
866         st.f_blocks = 0
867         st.f_files = 0
868
869         st.f_bfree = 0
870         st.f_bavail = 0
871
872         st.f_ffree = 0
873         st.f_favail = 0
874
875         st.f_frsize = 0
876         return st
877
878     # The llfuse documentation recommends only overloading functions that
879     # are actually implemented, as the default implementation will raise ENOSYS.
880     # However, there is a bug in the llfuse default implementation of create()
881     # "create() takes exactly 5 positional arguments (6 given)" which will crash
882     # arv-mount.
883     # The workaround is to implement it with the proper number of parameters,
884     # and then everything works out.
885     def create(self, p1, p2, p3, p4, p5):
886         raise llfuse.FUSEError(errno.EROFS)