1 # Copyright (C) The Arvados Authors. All rights reserved.
3 # SPDX-License-Identifier: AGPL-3.0
13 from apiclient import errors as apiclient_errors
17 from fusefile import StringFile, ObjectFile, FuncToJSONFile, FuseArvadosFile
18 from fresh import FreshBase, convertTime, use_counter, check_update
20 import arvados.collection
21 from arvados.util import portable_data_hash_pattern, uuid_pattern, collection_uuid_pattern, group_uuid_pattern, user_uuid_pattern, link_uuid_pattern
23 _logger = logging.getLogger('arvados.arvados_fuse')
26 # Match any character which FUSE or Linux cannot accommodate as part
27 # of a filename. (If present in a collection filename, they will
28 # appear as underscores in the fuse mount.)
29 _disallowed_filename_characters = re.compile('[\x00/]')
31 # '.' and '..' are not reachable if API server is newer than #6277
32 def sanitize_filename(dirty):
33 """Replace disallowed filename characters with harmless "_"."""
43 return _disallowed_filename_characters.sub('_', dirty)
46 class Directory(FreshBase):
47 """Generic directory object, backed by a dict.
49 Consists of a set of entries with the key representing the filename
50 and the value referencing a File or Directory object.
53 def __init__(self, parent_inode, inodes):
54 """parent_inode is the integer inode number"""
56 super(Directory, self).__init__()
59 if not isinstance(parent_inode, int):
60 raise Exception("parent_inode should be an int")
61 self.parent_inode = parent_inode
64 self._mtime = time.time()
66 # Overriden by subclasses to implement logic to update the entries dict
67 # when the directory is stale
72 # Only used when computing the size of the disk footprint of the directory
80 def checkupdate(self):
84 except apiclient.errors.HttpError as e:
89 def __getitem__(self, item):
90 return self._entries[item]
95 return list(self._entries.items())
99 def __contains__(self, k):
100 return k in self._entries
105 return len(self._entries)
108 self.inodes.touch(self)
109 super(Directory, self).fresh()
111 def merge(self, items, fn, same, new_entry):
112 """Helper method for updating the contents of the directory.
114 Takes a list describing the new contents of the directory, reuse
115 entries that are the same in both the old and new lists, create new
116 entries, and delete old entries missing from the new list.
118 :items: iterable with new directory contents
120 :fn: function to take an entry in 'items' and return the desired file or
121 directory name, or None if this entry should be skipped
123 :same: function to compare an existing entry (a File or Directory
124 object) with an entry in the items list to determine whether to keep
127 :new_entry: function to create a new directory entry (File or Directory
128 object) from an entry in the items list.
132 oldentries = self._entries
136 name = sanitize_filename(fn(i))
138 if name in oldentries and same(oldentries[name], i):
139 # move existing directory entry over
140 self._entries[name] = oldentries[name]
143 _logger.debug("Adding entry '%s' to inode %i", name, self.inode)
144 # create new directory entry
147 self._entries[name] = self.inodes.add_entry(ent)
150 # delete any other directory entries that were not in found in 'items'
152 _logger.debug("Forgetting about entry '%s' on inode %i", i, self.inode)
153 self.inodes.invalidate_entry(self.inode, i.encode(self.inodes.encoding))
154 self.inodes.del_entry(oldentries[i])
158 self.inodes.invalidate_inode(self.inode)
159 self._mtime = time.time()
164 if super(Directory, self).in_use():
166 for v in self._entries.itervalues():
171 def has_ref(self, only_children):
172 if super(Directory, self).has_ref(only_children):
174 for v in self._entries.itervalues():
180 """Delete all entries"""
181 oldentries = self._entries
184 oldentries[n].clear()
185 self.inodes.invalidate_entry(self.inode, n.encode(self.inodes.encoding))
186 self.inodes.del_entry(oldentries[n])
187 self.inodes.invalidate_inode(self.inode)
190 def kernel_invalidate(self):
191 for n, e in self._entries.iteritems():
192 self.inodes.invalidate_entry(self.inode, n.encode(self.inodes.encoding))
193 e.kernel_invalidate()
194 self.inodes.invalidate_inode(self.inode)
205 def want_event_subscribe(self):
206 raise NotImplementedError()
208 def create(self, name):
209 raise NotImplementedError()
211 def mkdir(self, name):
212 raise NotImplementedError()
214 def unlink(self, name):
215 raise NotImplementedError()
217 def rmdir(self, name):
218 raise NotImplementedError()
220 def rename(self, name_old, name_new, src):
221 raise NotImplementedError()
224 class CollectionDirectoryBase(Directory):
225 """Represent an Arvados Collection as a directory.
227 This class is used for Subcollections, and is also the base class for
228 CollectionDirectory, which implements collection loading/saving on
231 Most operations act only the underlying Arvados `Collection` object. The
232 `Collection` object signals via a notify callback to
233 `CollectionDirectoryBase.on_event` that an item was added, removed or
234 modified. FUSE inodes and directory entries are created, deleted or
235 invalidated in response to these events.
239 def __init__(self, parent_inode, inodes, collection):
240 super(CollectionDirectoryBase, self).__init__(parent_inode, inodes)
241 self.collection = collection
243 def new_entry(self, name, item, mtime):
244 name = sanitize_filename(name)
245 if hasattr(item, "fuse_entry") and item.fuse_entry is not None:
246 if item.fuse_entry.dead is not True:
247 raise Exception("Can only reparent dead inode entry")
248 if item.fuse_entry.inode is None:
249 raise Exception("Reparented entry must still have valid inode")
250 item.fuse_entry.dead = False
251 self._entries[name] = item.fuse_entry
252 elif isinstance(item, arvados.collection.RichCollectionBase):
253 self._entries[name] = self.inodes.add_entry(CollectionDirectoryBase(self.inode, self.inodes, item))
254 self._entries[name].populate(mtime)
256 self._entries[name] = self.inodes.add_entry(FuseArvadosFile(self.inode, item, mtime))
257 item.fuse_entry = self._entries[name]
259 def on_event(self, event, collection, name, item):
260 if collection == self.collection:
261 name = sanitize_filename(name)
262 _logger.debug("collection notify %s %s %s %s", event, collection, name, item)
264 if event == arvados.collection.ADD:
265 self.new_entry(name, item, self.mtime())
266 elif event == arvados.collection.DEL:
267 ent = self._entries[name]
268 del self._entries[name]
269 self.inodes.invalidate_entry(self.inode, name.encode(self.inodes.encoding))
270 self.inodes.del_entry(ent)
271 elif event == arvados.collection.MOD:
272 if hasattr(item, "fuse_entry") and item.fuse_entry is not None:
273 self.inodes.invalidate_inode(item.fuse_entry.inode)
274 elif name in self._entries:
275 self.inodes.invalidate_inode(self._entries[name].inode)
277 def populate(self, mtime):
279 self.collection.subscribe(self.on_event)
280 for entry, item in self.collection.items():
281 self.new_entry(entry, item, self.mtime())
284 return self.collection.writable()
288 with llfuse.lock_released:
289 self.collection.root_collection().save()
293 def create(self, name):
294 with llfuse.lock_released:
295 self.collection.open(name, "w").close()
299 def mkdir(self, name):
300 with llfuse.lock_released:
301 self.collection.mkdirs(name)
305 def unlink(self, name):
306 with llfuse.lock_released:
307 self.collection.remove(name)
312 def rmdir(self, name):
313 with llfuse.lock_released:
314 self.collection.remove(name)
319 def rename(self, name_old, name_new, src):
320 if not isinstance(src, CollectionDirectoryBase):
321 raise llfuse.FUSEError(errno.EPERM)
326 if isinstance(ent, FuseArvadosFile) and isinstance(tgt, FuseArvadosFile):
328 elif isinstance(ent, CollectionDirectoryBase) and isinstance(tgt, CollectionDirectoryBase):
330 raise llfuse.FUSEError(errno.ENOTEMPTY)
331 elif isinstance(ent, CollectionDirectoryBase) and isinstance(tgt, FuseArvadosFile):
332 raise llfuse.FUSEError(errno.ENOTDIR)
333 elif isinstance(ent, FuseArvadosFile) and isinstance(tgt, CollectionDirectoryBase):
334 raise llfuse.FUSEError(errno.EISDIR)
336 with llfuse.lock_released:
337 self.collection.rename(name_old, name_new, source_collection=src.collection, overwrite=True)
342 super(CollectionDirectoryBase, self).clear()
343 self.collection = None
346 class CollectionDirectory(CollectionDirectoryBase):
347 """Represents the root of a directory tree representing a collection."""
349 def __init__(self, parent_inode, inodes, api, num_retries, collection_record=None, explicit_collection=None):
350 super(CollectionDirectory, self).__init__(parent_inode, inodes, None)
352 self.num_retries = num_retries
353 self.collection_record_file = None
354 self.collection_record = None
357 self._poll_time = (api._rootDesc.get('blobSignatureTtl', 60*60*2)/2)
359 _logger.debug("Error getting blobSignatureTtl from discovery document: %s", sys.exc_info()[0])
360 self._poll_time = 60*60
362 if isinstance(collection_record, dict):
363 self.collection_locator = collection_record['uuid']
364 self._mtime = convertTime(collection_record.get('modified_at'))
366 self.collection_locator = collection_record
368 self._manifest_size = 0
369 if self.collection_locator:
370 self._writable = (uuid_pattern.match(self.collection_locator) is not None)
371 self._updating_lock = threading.Lock()
374 return i['uuid'] == self.collection_locator or i['portable_data_hash'] == self.collection_locator
377 return self.collection.writable() if self.collection is not None else self._writable
379 def want_event_subscribe(self):
380 return (uuid_pattern.match(self.collection_locator) is not None)
382 # Used by arv-web.py to switch the contents of the CollectionDirectory
383 def change_collection(self, new_locator):
384 """Switch the contents of the CollectionDirectory.
386 Must be called with llfuse.lock held.
389 self.collection_locator = new_locator
390 self.collection_record = None
393 def new_collection(self, new_collection_record, coll_reader):
397 self.collection_record = new_collection_record
399 if self.collection_record:
400 self._mtime = convertTime(self.collection_record.get('modified_at'))
401 self.collection_locator = self.collection_record["uuid"]
402 if self.collection_record_file is not None:
403 self.collection_record_file.update(self.collection_record)
405 self.collection = coll_reader
406 self.populate(self.mtime())
409 return self.collection_locator
412 def update(self, to_record_version=None):
414 if self.collection_record is not None and portable_data_hash_pattern.match(self.collection_locator):
417 if self.collection_locator is None:
422 with llfuse.lock_released:
423 self._updating_lock.acquire()
427 _logger.debug("Updating collection %s inode %s to record version %s", self.collection_locator, self.inode, to_record_version)
428 if self.collection is not None:
429 if self.collection.known_past_version(to_record_version):
430 _logger.debug("%s already processed %s", self.collection_locator, to_record_version)
432 self.collection.update()
434 if uuid_pattern.match(self.collection_locator):
435 coll_reader = arvados.collection.Collection(
436 self.collection_locator, self.api, self.api.keep,
437 num_retries=self.num_retries)
439 coll_reader = arvados.collection.CollectionReader(
440 self.collection_locator, self.api, self.api.keep,
441 num_retries=self.num_retries)
442 new_collection_record = coll_reader.api_response() or {}
443 # If the Collection only exists in Keep, there will be no API
444 # response. Fill in the fields we need.
445 if 'uuid' not in new_collection_record:
446 new_collection_record['uuid'] = self.collection_locator
447 if "portable_data_hash" not in new_collection_record:
448 new_collection_record["portable_data_hash"] = new_collection_record["uuid"]
449 if 'manifest_text' not in new_collection_record:
450 new_collection_record['manifest_text'] = coll_reader.manifest_text()
452 if self.collection_record is None or self.collection_record["portable_data_hash"] != new_collection_record.get("portable_data_hash"):
453 self.new_collection(new_collection_record, coll_reader)
455 self._manifest_size = len(coll_reader.manifest_text())
456 _logger.debug("%s manifest_size %i", self, self._manifest_size)
457 # end with llfuse.lock_released, re-acquire lock
462 self._updating_lock.release()
463 except arvados.errors.NotFoundError as e:
464 _logger.error("Error fetching collection '%s': %s", self.collection_locator, e)
465 except arvados.errors.ArgumentError as detail:
466 _logger.warning("arv-mount %s: error %s", self.collection_locator, detail)
467 if self.collection_record is not None and "manifest_text" in self.collection_record:
468 _logger.warning("arv-mount manifest_text is: %s", self.collection_record["manifest_text"])
470 _logger.exception("arv-mount %s: error", self.collection_locator)
471 if self.collection_record is not None and "manifest_text" in self.collection_record:
472 _logger.error("arv-mount manifest_text is: %s", self.collection_record["manifest_text"])
478 def __getitem__(self, item):
479 if item == '.arvados#collection':
480 if self.collection_record_file is None:
481 self.collection_record_file = ObjectFile(self.inode, self.collection_record)
482 self.inodes.add_entry(self.collection_record_file)
483 return self.collection_record_file
485 return super(CollectionDirectory, self).__getitem__(item)
487 def __contains__(self, k):
488 if k == '.arvados#collection':
491 return super(CollectionDirectory, self).__contains__(k)
493 def invalidate(self):
494 self.collection_record = None
495 self.collection_record_file = None
496 super(CollectionDirectory, self).invalidate()
499 return (self.collection_locator is not None)
502 # This is an empirically-derived heuristic to estimate the memory used
503 # to store this collection's metadata. Calculating the memory
504 # footprint directly would be more accurate, but also more complicated.
505 return self._manifest_size * 128
508 if self.collection is not None:
510 self.collection.save()
511 self.collection.stop_threads()
514 super(CollectionDirectory, self).clear()
515 self._manifest_size = 0
518 class TmpCollectionDirectory(CollectionDirectoryBase):
519 """A directory backed by an Arvados collection that never gets saved.
521 This supports using Keep as scratch space. A userspace program can
522 read the .arvados#collection file to get a current manifest in
523 order to save a snapshot of the scratch data or use it as a crunch
527 class UnsaveableCollection(arvados.collection.Collection):
533 def __init__(self, parent_inode, inodes, api_client, num_retries):
534 collection = self.UnsaveableCollection(
535 api_client=api_client,
536 keep_client=api_client.keep,
537 num_retries=num_retries)
538 super(TmpCollectionDirectory, self).__init__(
539 parent_inode, inodes, collection)
540 self.collection_record_file = None
541 self.populate(self.mtime())
543 def on_event(self, *args, **kwargs):
544 super(TmpCollectionDirectory, self).on_event(*args, **kwargs)
545 if self.collection_record_file:
547 self.collection_record_file.invalidate()
548 self.inodes.invalidate_inode(self.collection_record_file.inode)
549 _logger.debug("%s invalidated collection record", self)
551 def collection_record(self):
552 with llfuse.lock_released:
555 "manifest_text": self.collection.manifest_text(),
556 "portable_data_hash": self.collection.portable_data_hash(),
559 def __contains__(self, k):
560 return (k == '.arvados#collection' or
561 super(TmpCollectionDirectory, self).__contains__(k))
564 def __getitem__(self, item):
565 if item == '.arvados#collection':
566 if self.collection_record_file is None:
567 self.collection_record_file = FuncToJSONFile(
568 self.inode, self.collection_record)
569 self.inodes.add_entry(self.collection_record_file)
570 return self.collection_record_file
571 return super(TmpCollectionDirectory, self).__getitem__(item)
579 def want_event_subscribe(self):
583 self.collection.stop_threads()
585 def invalidate(self):
586 if self.collection_record_file:
587 self.collection_record_file.invalidate()
588 super(TmpCollectionDirectory, self).invalidate()
591 class MagicDirectory(Directory):
592 """A special directory that logically contains the set of all extant keep locators.
594 When a file is referenced by lookup(), it is tested to see if it is a valid
595 keep locator to a manifest, and if so, loads the manifest contents as a
596 subdirectory of this directory with the locator as the directory name.
597 Since querying a list of all extant keep locators is impractical, only
598 collections that have already been accessed are visible to readdir().
603 This directory provides access to Arvados collections as subdirectories listed
604 by uuid (in the form 'zzzzz-4zz18-1234567890abcde') or portable data hash (in
605 the form '1234567890abcdef0123456789abcdef+123').
607 Note that this directory will appear empty until you attempt to access a
608 specific collection subdirectory (such as trying to 'cd' into it), at which
609 point the collection will actually be looked up on the server and the directory
610 will appear if it exists.
614 def __init__(self, parent_inode, inodes, api, num_retries, pdh_only=False):
615 super(MagicDirectory, self).__init__(parent_inode, inodes)
617 self.num_retries = num_retries
618 self.pdh_only = pdh_only
620 def __setattr__(self, name, value):
621 super(MagicDirectory, self).__setattr__(name, value)
622 # When we're assigned an inode, add a README.
623 if ((name == 'inode') and (self.inode is not None) and
624 (not self._entries)):
625 self._entries['README'] = self.inodes.add_entry(
626 StringFile(self.inode, self.README_TEXT, time.time()))
627 # If we're the root directory, add an identical by_id subdirectory.
628 if self.inode == llfuse.ROOT_INODE:
629 self._entries['by_id'] = self.inodes.add_entry(MagicDirectory(
630 self.inode, self.inodes, self.api, self.num_retries, self.pdh_only))
632 def __contains__(self, k):
633 if k in self._entries:
636 if not portable_data_hash_pattern.match(k) and (self.pdh_only or not uuid_pattern.match(k)):
640 e = self.inodes.add_entry(CollectionDirectory(
641 self.inode, self.inodes, self.api, self.num_retries, k))
644 if k not in self._entries:
647 self.inodes.del_entry(e)
650 self.inodes.invalidate_entry(self.inode, k)
651 self.inodes.del_entry(e)
653 except Exception as ex:
654 _logger.debug('arv-mount exception keep %s', ex)
655 self.inodes.del_entry(e)
658 def __getitem__(self, item):
660 return self._entries[item]
662 raise KeyError("No collection with id " + item)
667 def want_event_subscribe(self):
668 return not self.pdh_only
671 class TagsDirectory(Directory):
672 """A special directory that contains as subdirectories all tags visible to the user."""
674 def __init__(self, parent_inode, inodes, api, num_retries, poll_time=60):
675 super(TagsDirectory, self).__init__(parent_inode, inodes)
677 self.num_retries = num_retries
679 self._poll_time = poll_time
681 def want_event_subscribe(self):
686 with llfuse.lock_released:
687 tags = self.api.links().list(
688 filters=[['link_class', '=', 'tag']],
689 select=['name'], distinct=True
690 ).execute(num_retries=self.num_retries)
692 self.merge(tags['items'],
694 lambda a, i: a.tag == i['name'],
695 lambda i: TagDirectory(self.inode, self.inodes, self.api, self.num_retries, i['name'], poll=self._poll, poll_time=self._poll_time))
698 class TagDirectory(Directory):
699 """A special directory that contains as subdirectories all collections visible
700 to the user that are tagged with a particular tag.
703 def __init__(self, parent_inode, inodes, api, num_retries, tag,
704 poll=False, poll_time=60):
705 super(TagDirectory, self).__init__(parent_inode, inodes)
707 self.num_retries = num_retries
710 self._poll_time = poll_time
712 def want_event_subscribe(self):
717 with llfuse.lock_released:
718 taggedcollections = self.api.links().list(
719 filters=[['link_class', '=', 'tag'],
720 ['name', '=', self.tag],
721 ['head_uuid', 'is_a', 'arvados#collection']],
723 ).execute(num_retries=self.num_retries)
724 self.merge(taggedcollections['items'],
725 lambda i: i['head_uuid'],
726 lambda a, i: a.collection_locator == i['head_uuid'],
727 lambda i: CollectionDirectory(self.inode, self.inodes, self.api, self.num_retries, i['head_uuid']))
730 class ProjectDirectory(Directory):
731 """A special directory that contains the contents of a project."""
733 def __init__(self, parent_inode, inodes, api, num_retries, project_object,
734 poll=False, poll_time=60):
735 super(ProjectDirectory, self).__init__(parent_inode, inodes)
737 self.num_retries = num_retries
738 self.project_object = project_object
739 self.project_object_file = None
740 self.project_uuid = project_object['uuid']
742 self._poll_time = poll_time
743 self._updating_lock = threading.Lock()
744 self._current_user = None
746 def want_event_subscribe(self):
749 def createDirectory(self, i):
750 if collection_uuid_pattern.match(i['uuid']):
751 return CollectionDirectory(self.inode, self.inodes, self.api, self.num_retries, i)
752 elif group_uuid_pattern.match(i['uuid']):
753 return ProjectDirectory(self.inode, self.inodes, self.api, self.num_retries, i, self._poll, self._poll_time)
754 elif link_uuid_pattern.match(i['uuid']):
755 if i['head_kind'] == 'arvados#collection' or portable_data_hash_pattern.match(i['head_uuid']):
756 return CollectionDirectory(self.inode, self.inodes, self.api, self.num_retries, i['head_uuid'])
759 elif uuid_pattern.match(i['uuid']):
760 return ObjectFile(self.parent_inode, i)
765 return self.project_uuid
769 if self.project_object_file == None:
770 self.project_object_file = ObjectFile(self.inode, self.project_object)
771 self.inodes.add_entry(self.project_object_file)
775 if i['name'] is None or len(i['name']) == 0:
777 elif collection_uuid_pattern.match(i['uuid']) or group_uuid_pattern.match(i['uuid']):
778 # collection or subproject
780 elif link_uuid_pattern.match(i['uuid']) and i['head_kind'] == 'arvados#collection':
783 elif 'kind' in i and i['kind'].startswith('arvados#'):
785 return "{}.{}".format(i['name'], i['kind'][8:])
790 if isinstance(a, CollectionDirectory) or isinstance(a, ProjectDirectory):
791 return a.uuid() == i['uuid']
792 elif isinstance(a, ObjectFile):
793 return a.uuid() == i['uuid'] and not a.stale()
797 with llfuse.lock_released:
798 self._updating_lock.acquire()
802 if group_uuid_pattern.match(self.project_uuid):
803 self.project_object = self.api.groups().get(
804 uuid=self.project_uuid).execute(num_retries=self.num_retries)
805 elif user_uuid_pattern.match(self.project_uuid):
806 self.project_object = self.api.users().get(
807 uuid=self.project_uuid).execute(num_retries=self.num_retries)
809 contents = arvados.util.list_all(self.api.groups().contents,
810 self.num_retries, uuid=self.project_uuid)
812 # end with llfuse.lock_released, re-acquire lock
817 self.createDirectory)
819 self._updating_lock.release()
823 def __getitem__(self, item):
824 if item == '.arvados#project':
825 return self.project_object_file
827 return super(ProjectDirectory, self).__getitem__(item)
829 def __contains__(self, k):
830 if k == '.arvados#project':
833 return super(ProjectDirectory, self).__contains__(k)
838 with llfuse.lock_released:
839 if not self._current_user:
840 self._current_user = self.api.users().current().execute(num_retries=self.num_retries)
841 return self._current_user["uuid"] in self.project_object["writable_by"]
848 def mkdir(self, name):
850 with llfuse.lock_released:
851 self.api.collections().create(body={"owner_uuid": self.project_uuid,
853 "manifest_text": ""}).execute(num_retries=self.num_retries)
855 except apiclient_errors.Error as error:
857 raise llfuse.FUSEError(errno.EEXIST)
861 def rmdir(self, name):
863 raise llfuse.FUSEError(errno.ENOENT)
864 if not isinstance(self[name], CollectionDirectory):
865 raise llfuse.FUSEError(errno.EPERM)
866 if len(self[name]) > 0:
867 raise llfuse.FUSEError(errno.ENOTEMPTY)
868 with llfuse.lock_released:
869 self.api.collections().delete(uuid=self[name].uuid()).execute(num_retries=self.num_retries)
874 def rename(self, name_old, name_new, src):
875 if not isinstance(src, ProjectDirectory):
876 raise llfuse.FUSEError(errno.EPERM)
880 if not isinstance(ent, CollectionDirectory):
881 raise llfuse.FUSEError(errno.EPERM)
884 # POSIX semantics for replacing one directory with another is
885 # tricky (the target directory must be empty, the operation must be
886 # atomic which isn't possible with the Arvados API as of this
887 # writing) so don't support that.
888 raise llfuse.FUSEError(errno.EPERM)
890 self.api.collections().update(uuid=ent.uuid(),
891 body={"owner_uuid": self.uuid(),
892 "name": name_new}).execute(num_retries=self.num_retries)
894 # Acually move the entry from source directory to this directory.
895 del src._entries[name_old]
896 self._entries[name_new] = ent
897 self.inodes.invalidate_entry(src.inode, name_old.encode(self.inodes.encoding))
900 class SharedDirectory(Directory):
901 """A special directory that represents users or groups who have shared projects with me."""
903 def __init__(self, parent_inode, inodes, api, num_retries, exclude,
904 poll=False, poll_time=60):
905 super(SharedDirectory, self).__init__(parent_inode, inodes)
907 self.num_retries = num_retries
908 self.current_user = api.users().current().execute(num_retries=num_retries)
910 self._poll_time = poll_time
914 with llfuse.lock_released:
915 all_projects = arvados.util.list_all(
916 self.api.groups().list, self.num_retries,
917 filters=[['group_class','=','project']])
919 for ob in all_projects:
920 objects[ob['uuid']] = ob
924 for ob in all_projects:
925 if ob['owner_uuid'] != self.current_user['uuid'] and ob['owner_uuid'] not in objects:
927 root_owners[ob['owner_uuid']] = True
929 lusers = arvados.util.list_all(
930 self.api.users().list, self.num_retries,
931 filters=[['uuid','in', list(root_owners)]])
932 lgroups = arvados.util.list_all(
933 self.api.groups().list, self.num_retries,
934 filters=[['uuid','in', list(root_owners)]])
940 objects[l["uuid"]] = l
942 objects[l["uuid"]] = l
945 for r in root_owners:
949 contents[obr["name"]] = obr
950 #elif obr.get("username"):
951 # contents[obr["username"]] = obr
952 elif "first_name" in obr:
953 contents[u"{} {}".format(obr["first_name"], obr["last_name"])] = obr
957 if r['owner_uuid'] not in objects:
958 contents[r['name']] = r
960 # end with llfuse.lock_released, re-acquire lock
963 self.merge(contents.items(),
965 lambda a, i: a.uuid() == i[1]['uuid'],
966 lambda i: ProjectDirectory(self.inode, self.inodes, self.api, self.num_retries, i[1], poll=self._poll, poll_time=self._poll_time))
970 def want_event_subscribe(self):