9 from apiclient import errors as apiclient_errors
13 from fusefile import StringFile, ObjectFile, FuncToJSONFile, FuseArvadosFile
14 from fresh import FreshBase, convertTime, use_counter, check_update
16 import arvados.collection
17 from arvados.util import portable_data_hash_pattern, uuid_pattern, collection_uuid_pattern, group_uuid_pattern, user_uuid_pattern, link_uuid_pattern
19 _logger = logging.getLogger('arvados.arvados_fuse')
22 # Match any character which FUSE or Linux cannot accommodate as part
23 # of a filename. (If present in a collection filename, they will
24 # appear as underscores in the fuse mount.)
25 _disallowed_filename_characters = re.compile('[\x00/]')
27 # '.' and '..' are not reachable if API server is newer than #6277
28 def sanitize_filename(dirty):
29 """Replace disallowed filename characters with harmless "_"."""
39 return _disallowed_filename_characters.sub('_', dirty)
42 class Directory(FreshBase):
43 """Generic directory object, backed by a dict.
45 Consists of a set of entries with the key representing the filename
46 and the value referencing a File or Directory object.
49 def __init__(self, parent_inode, inodes):
50 """parent_inode is the integer inode number"""
52 super(Directory, self).__init__()
55 if not isinstance(parent_inode, int):
56 raise Exception("parent_inode should be an int")
57 self.parent_inode = parent_inode
60 self._mtime = time.time()
62 # Overriden by subclasses to implement logic to update the entries dict
63 # when the directory is stale
68 # Only used when computing the size of the disk footprint of the directory
76 def checkupdate(self):
80 except apiclient.errors.HttpError as e:
85 def __getitem__(self, item):
86 return self._entries[item]
91 return list(self._entries.items())
95 def __contains__(self, k):
96 return k in self._entries
101 return len(self._entries)
104 self.inodes.touch(self)
105 super(Directory, self).fresh()
107 def merge(self, items, fn, same, new_entry):
108 """Helper method for updating the contents of the directory.
110 Takes a list describing the new contents of the directory, reuse
111 entries that are the same in both the old and new lists, create new
112 entries, and delete old entries missing from the new list.
114 :items: iterable with new directory contents
116 :fn: function to take an entry in 'items' and return the desired file or
117 directory name, or None if this entry should be skipped
119 :same: function to compare an existing entry (a File or Directory
120 object) with an entry in the items list to determine whether to keep
123 :new_entry: function to create a new directory entry (File or Directory
124 object) from an entry in the items list.
128 oldentries = self._entries
132 name = sanitize_filename(fn(i))
134 if name in oldentries and same(oldentries[name], i):
135 # move existing directory entry over
136 self._entries[name] = oldentries[name]
139 _logger.debug("Adding entry '%s' to inode %i", name, self.inode)
140 # create new directory entry
143 self._entries[name] = self.inodes.add_entry(ent)
146 # delete any other directory entries that were not in found in 'items'
148 _logger.debug("Forgetting about entry '%s' on inode %i", i, self.inode)
149 self.inodes.invalidate_entry(self.inode, i.encode(self.inodes.encoding))
150 self.inodes.del_entry(oldentries[i])
154 self.inodes.invalidate_inode(self.inode)
155 self._mtime = time.time()
159 def clear(self, force=False):
160 """Delete all entries"""
162 if not self.in_use() or force:
163 oldentries = self._entries
166 if not oldentries[n].clear(force):
167 self._entries = oldentries
170 self.inodes.invalidate_entry(self.inode, n.encode(self.inodes.encoding))
171 self.inodes.del_entry(oldentries[n])
172 self.inodes.invalidate_inode(self.inode)
187 def create(self, name):
188 raise NotImplementedError()
190 def mkdir(self, name):
191 raise NotImplementedError()
193 def unlink(self, name):
194 raise NotImplementedError()
196 def rmdir(self, name):
197 raise NotImplementedError()
199 def rename(self, name_old, name_new, src):
200 raise NotImplementedError()
203 class CollectionDirectoryBase(Directory):
204 """Represent an Arvados Collection as a directory.
206 This class is used for Subcollections, and is also the base class for
207 CollectionDirectory, which implements collection loading/saving on
210 Most operations act only the underlying Arvados `Collection` object. The
211 `Collection` object signals via a notify callback to
212 `CollectionDirectoryBase.on_event` that an item was added, removed or
213 modified. FUSE inodes and directory entries are created, deleted or
214 invalidated in response to these events.
218 def __init__(self, parent_inode, inodes, collection):
219 super(CollectionDirectoryBase, self).__init__(parent_inode, inodes)
220 self.collection = collection
222 def new_entry(self, name, item, mtime):
223 name = sanitize_filename(name)
224 if hasattr(item, "fuse_entry") and item.fuse_entry is not None:
225 if item.fuse_entry.dead is not True:
226 raise Exception("Can only reparent dead inode entry")
227 if item.fuse_entry.inode is None:
228 raise Exception("Reparented entry must still have valid inode")
229 item.fuse_entry.dead = False
230 self._entries[name] = item.fuse_entry
231 elif isinstance(item, arvados.collection.RichCollectionBase):
232 self._entries[name] = self.inodes.add_entry(CollectionDirectoryBase(self.inode, self.inodes, item))
233 self._entries[name].populate(mtime)
235 self._entries[name] = self.inodes.add_entry(FuseArvadosFile(self.inode, item, mtime))
236 item.fuse_entry = self._entries[name]
238 def on_event(self, event, collection, name, item):
239 if collection == self.collection:
240 name = sanitize_filename(name)
241 _logger.debug("collection notify %s %s %s %s", event, collection, name, item)
243 if event == arvados.collection.ADD:
244 self.new_entry(name, item, self.mtime())
245 elif event == arvados.collection.DEL:
246 ent = self._entries[name]
247 del self._entries[name]
248 self.inodes.invalidate_entry(self.inode, name.encode(self.inodes.encoding))
249 self.inodes.del_entry(ent)
250 elif event == arvados.collection.MOD:
251 if hasattr(item, "fuse_entry") and item.fuse_entry is not None:
252 self.inodes.invalidate_inode(item.fuse_entry.inode)
253 elif name in self._entries:
254 self.inodes.invalidate_inode(self._entries[name].inode)
256 def populate(self, mtime):
258 self.collection.subscribe(self.on_event)
259 for entry, item in self.collection.items():
260 self.new_entry(entry, item, self.mtime())
263 return self.collection.writable()
267 with llfuse.lock_released:
268 self.collection.root_collection().save()
272 def create(self, name):
273 with llfuse.lock_released:
274 self.collection.open(name, "w").close()
278 def mkdir(self, name):
279 with llfuse.lock_released:
280 self.collection.mkdirs(name)
284 def unlink(self, name):
285 with llfuse.lock_released:
286 self.collection.remove(name)
291 def rmdir(self, name):
292 with llfuse.lock_released:
293 self.collection.remove(name)
298 def rename(self, name_old, name_new, src):
299 if not isinstance(src, CollectionDirectoryBase):
300 raise llfuse.FUSEError(errno.EPERM)
305 if isinstance(ent, FuseArvadosFile) and isinstance(tgt, FuseArvadosFile):
307 elif isinstance(ent, CollectionDirectoryBase) and isinstance(tgt, CollectionDirectoryBase):
309 raise llfuse.FUSEError(errno.ENOTEMPTY)
310 elif isinstance(ent, CollectionDirectoryBase) and isinstance(tgt, FuseArvadosFile):
311 raise llfuse.FUSEError(errno.ENOTDIR)
312 elif isinstance(ent, FuseArvadosFile) and isinstance(tgt, CollectionDirectoryBase):
313 raise llfuse.FUSEError(errno.EISDIR)
315 with llfuse.lock_released:
316 self.collection.rename(name_old, name_new, source_collection=src.collection, overwrite=True)
321 class CollectionDirectory(CollectionDirectoryBase):
322 """Represents the root of a directory tree representing a collection."""
324 def __init__(self, parent_inode, inodes, api, num_retries, collection_record=None, explicit_collection=None):
325 super(CollectionDirectory, self).__init__(parent_inode, inodes, None)
327 self.num_retries = num_retries
328 self.collection_record_file = None
329 self.collection_record = None
332 self._poll_time = (api._rootDesc.get('blobSignatureTtl', 60*60*2)/2)
334 _logger.debug("Error getting blobSignatureTtl from discovery document: %s", sys.exc_info()[0])
335 self._poll_time = 60*60
337 if isinstance(collection_record, dict):
338 self.collection_locator = collection_record['uuid']
339 self._mtime = convertTime(collection_record.get('modified_at'))
341 self.collection_locator = collection_record
343 self._manifest_size = 0
344 if self.collection_locator:
345 self._writable = (uuid_pattern.match(self.collection_locator) is not None)
346 self._updating_lock = threading.Lock()
349 return i['uuid'] == self.collection_locator or i['portable_data_hash'] == self.collection_locator
352 return self.collection.writable() if self.collection is not None else self._writable
354 # Used by arv-web.py to switch the contents of the CollectionDirectory
355 def change_collection(self, new_locator):
356 """Switch the contents of the CollectionDirectory.
358 Must be called with llfuse.lock held.
361 self.collection_locator = new_locator
362 self.collection_record = None
365 def new_collection(self, new_collection_record, coll_reader):
367 self.clear(force=True)
369 self.collection_record = new_collection_record
371 if self.collection_record:
372 self._mtime = convertTime(self.collection_record.get('modified_at'))
373 self.collection_locator = self.collection_record["uuid"]
374 if self.collection_record_file is not None:
375 self.collection_record_file.update(self.collection_record)
377 self.collection = coll_reader
378 self.populate(self.mtime())
381 return self.collection_locator
384 def update(self, to_record_version=None):
386 if self.collection_record is not None and portable_data_hash_pattern.match(self.collection_locator):
389 if self.collection_locator is None:
394 with llfuse.lock_released:
395 self._updating_lock.acquire()
399 _logger.debug("Updating %s", to_record_version)
400 if self.collection is not None:
401 if self.collection.known_past_version(to_record_version):
402 _logger.debug("%s already processed %s", self.collection_locator, to_record_version)
404 self.collection.update()
406 if uuid_pattern.match(self.collection_locator):
407 coll_reader = arvados.collection.Collection(
408 self.collection_locator, self.api, self.api.keep,
409 num_retries=self.num_retries)
411 coll_reader = arvados.collection.CollectionReader(
412 self.collection_locator, self.api, self.api.keep,
413 num_retries=self.num_retries)
414 new_collection_record = coll_reader.api_response() or {}
415 # If the Collection only exists in Keep, there will be no API
416 # response. Fill in the fields we need.
417 if 'uuid' not in new_collection_record:
418 new_collection_record['uuid'] = self.collection_locator
419 if "portable_data_hash" not in new_collection_record:
420 new_collection_record["portable_data_hash"] = new_collection_record["uuid"]
421 if 'manifest_text' not in new_collection_record:
422 new_collection_record['manifest_text'] = coll_reader.manifest_text()
424 if self.collection_record is None or self.collection_record["portable_data_hash"] != new_collection_record.get("portable_data_hash"):
425 self.new_collection(new_collection_record, coll_reader)
427 self._manifest_size = len(coll_reader.manifest_text())
428 _logger.debug("%s manifest_size %i", self, self._manifest_size)
429 # end with llfuse.lock_released, re-acquire lock
434 self._updating_lock.release()
435 except arvados.errors.NotFoundError as e:
436 _logger.error("Error fetching collection '%s': %s", self.collection_locator, e)
437 except arvados.errors.ArgumentError as detail:
438 _logger.warning("arv-mount %s: error %s", self.collection_locator, detail)
439 if self.collection_record is not None and "manifest_text" in self.collection_record:
440 _logger.warning("arv-mount manifest_text is: %s", self.collection_record["manifest_text"])
442 _logger.exception("arv-mount %s: error", self.collection_locator)
443 if self.collection_record is not None and "manifest_text" in self.collection_record:
444 _logger.error("arv-mount manifest_text is: %s", self.collection_record["manifest_text"])
450 def __getitem__(self, item):
451 if item == '.arvados#collection':
452 if self.collection_record_file is None:
453 self.collection_record_file = ObjectFile(self.inode, self.collection_record)
454 self.inodes.add_entry(self.collection_record_file)
455 return self.collection_record_file
457 return super(CollectionDirectory, self).__getitem__(item)
459 def __contains__(self, k):
460 if k == '.arvados#collection':
463 return super(CollectionDirectory, self).__contains__(k)
465 def invalidate(self):
466 self.collection_record = None
467 self.collection_record_file = None
468 super(CollectionDirectory, self).invalidate()
471 return (self.collection_locator is not None)
474 # This is an empirically-derived heuristic to estimate the memory used
475 # to store this collection's metadata. Calculating the memory
476 # footprint directly would be more accurate, but also more complicated.
477 return self._manifest_size * 128
480 if self.collection is not None:
482 self.collection.save()
483 self.collection.stop_threads()
486 class TmpCollectionDirectory(CollectionDirectoryBase):
487 """A directory backed by an Arvados collection that never gets saved.
489 This supports using Keep as scratch space. A userspace program can
490 read the .arvados#collection file to get a current manifest in
491 order to save a snapshot of the scratch data or use it as a crunch
495 class UnsaveableCollection(arvados.collection.Collection):
501 def __init__(self, parent_inode, inodes, api_client, num_retries):
502 collection = self.UnsaveableCollection(
503 api_client=api_client,
504 keep_client=api_client.keep,
505 num_retries=num_retries)
506 super(TmpCollectionDirectory, self).__init__(
507 parent_inode, inodes, collection)
508 self.collection_record_file = None
509 self.populate(self.mtime())
511 def on_event(self, *args, **kwargs):
512 super(TmpCollectionDirectory, self).on_event(*args, **kwargs)
513 if self.collection_record_file:
515 self.collection_record_file.invalidate()
516 self.inodes.invalidate_inode(self.collection_record_file.inode)
517 _logger.debug("%s invalidated collection record", self)
519 def collection_record(self):
520 with llfuse.lock_released:
523 "manifest_text": self.collection.manifest_text(),
524 "portable_data_hash": self.collection.portable_data_hash(),
527 def __contains__(self, k):
528 return (k == '.arvados#collection' or
529 super(TmpCollectionDirectory, self).__contains__(k))
532 def __getitem__(self, item):
533 if item == '.arvados#collection':
534 if self.collection_record_file is None:
535 self.collection_record_file = FuncToJSONFile(
536 self.inode, self.collection_record)
537 self.inodes.add_entry(self.collection_record_file)
538 return self.collection_record_file
539 return super(TmpCollectionDirectory, self).__getitem__(item)
548 self.collection.stop_threads()
550 def invalidate(self):
551 if self.collection_record_file:
552 self.collection_record_file.invalidate()
553 super(TmpCollectionDirectory, self).invalidate()
556 class MagicDirectory(Directory):
557 """A special directory that logically contains the set of all extant keep locators.
559 When a file is referenced by lookup(), it is tested to see if it is a valid
560 keep locator to a manifest, and if so, loads the manifest contents as a
561 subdirectory of this directory with the locator as the directory name.
562 Since querying a list of all extant keep locators is impractical, only
563 collections that have already been accessed are visible to readdir().
568 This directory provides access to Arvados collections as subdirectories listed
569 by uuid (in the form 'zzzzz-4zz18-1234567890abcde') or portable data hash (in
570 the form '1234567890abcdef0123456789abcdef+123').
572 Note that this directory will appear empty until you attempt to access a
573 specific collection subdirectory (such as trying to 'cd' into it), at which
574 point the collection will actually be looked up on the server and the directory
575 will appear if it exists.
579 def __init__(self, parent_inode, inodes, api, num_retries, pdh_only=False):
580 super(MagicDirectory, self).__init__(parent_inode, inodes)
582 self.num_retries = num_retries
583 self.pdh_only = pdh_only
585 def __setattr__(self, name, value):
586 super(MagicDirectory, self).__setattr__(name, value)
587 # When we're assigned an inode, add a README.
588 if ((name == 'inode') and (self.inode is not None) and
589 (not self._entries)):
590 self._entries['README'] = self.inodes.add_entry(
591 StringFile(self.inode, self.README_TEXT, time.time()))
592 # If we're the root directory, add an identical by_id subdirectory.
593 if self.inode == llfuse.ROOT_INODE:
594 self._entries['by_id'] = self.inodes.add_entry(MagicDirectory(
595 self.inode, self.inodes, self.api, self.num_retries, self.pdh_only))
597 def __contains__(self, k):
598 if k in self._entries:
601 if not portable_data_hash_pattern.match(k) and (self.pdh_only or not uuid_pattern.match(k)):
605 e = self.inodes.add_entry(CollectionDirectory(
606 self.inode, self.inodes, self.api, self.num_retries, k))
609 if k not in self._entries:
612 self.inodes.del_entry(e)
615 self.inodes.invalidate_entry(self.inode, k)
616 self.inodes.del_entry(e)
618 except Exception as ex:
619 _logger.debug('arv-mount exception keep %s', ex)
620 self.inodes.del_entry(e)
623 def __getitem__(self, item):
625 return self._entries[item]
627 raise KeyError("No collection with id " + item)
629 def clear(self, force=False):
633 class RecursiveInvalidateDirectory(Directory):
634 def invalidate(self):
636 super(RecursiveInvalidateDirectory, self).invalidate()
637 for a in self._entries:
638 self._entries[a].invalidate()
643 class TagsDirectory(RecursiveInvalidateDirectory):
644 """A special directory that contains as subdirectories all tags visible to the user."""
646 def __init__(self, parent_inode, inodes, api, num_retries, poll_time=60):
647 super(TagsDirectory, self).__init__(parent_inode, inodes)
649 self.num_retries = num_retries
651 self._poll_time = poll_time
655 with llfuse.lock_released:
656 tags = self.api.links().list(
657 filters=[['link_class', '=', 'tag']],
658 select=['name'], distinct=True
659 ).execute(num_retries=self.num_retries)
661 self.merge(tags['items'],
663 lambda a, i: a.tag == i['name'],
664 lambda i: TagDirectory(self.inode, self.inodes, self.api, self.num_retries, i['name'], poll=self._poll, poll_time=self._poll_time))
667 class TagDirectory(Directory):
668 """A special directory that contains as subdirectories all collections visible
669 to the user that are tagged with a particular tag.
672 def __init__(self, parent_inode, inodes, api, num_retries, tag,
673 poll=False, poll_time=60):
674 super(TagDirectory, self).__init__(parent_inode, inodes)
676 self.num_retries = num_retries
679 self._poll_time = poll_time
683 with llfuse.lock_released:
684 taggedcollections = self.api.links().list(
685 filters=[['link_class', '=', 'tag'],
686 ['name', '=', self.tag],
687 ['head_uuid', 'is_a', 'arvados#collection']],
689 ).execute(num_retries=self.num_retries)
690 self.merge(taggedcollections['items'],
691 lambda i: i['head_uuid'],
692 lambda a, i: a.collection_locator == i['head_uuid'],
693 lambda i: CollectionDirectory(self.inode, self.inodes, self.api, self.num_retries, i['head_uuid']))
696 class ProjectDirectory(Directory):
697 """A special directory that contains the contents of a project."""
699 def __init__(self, parent_inode, inodes, api, num_retries, project_object,
700 poll=False, poll_time=60):
701 super(ProjectDirectory, self).__init__(parent_inode, inodes)
703 self.num_retries = num_retries
704 self.project_object = project_object
705 self.project_object_file = None
706 self.project_uuid = project_object['uuid']
708 self._poll_time = poll_time
709 self._updating_lock = threading.Lock()
710 self._current_user = None
712 def createDirectory(self, i):
713 if collection_uuid_pattern.match(i['uuid']):
714 return CollectionDirectory(self.inode, self.inodes, self.api, self.num_retries, i)
715 elif group_uuid_pattern.match(i['uuid']):
716 return ProjectDirectory(self.inode, self.inodes, self.api, self.num_retries, i, self._poll, self._poll_time)
717 elif link_uuid_pattern.match(i['uuid']):
718 if i['head_kind'] == 'arvados#collection' or portable_data_hash_pattern.match(i['head_uuid']):
719 return CollectionDirectory(self.inode, self.inodes, self.api, self.num_retries, i['head_uuid'])
722 elif uuid_pattern.match(i['uuid']):
723 return ObjectFile(self.parent_inode, i)
728 return self.project_uuid
732 if self.project_object_file == None:
733 self.project_object_file = ObjectFile(self.inode, self.project_object)
734 self.inodes.add_entry(self.project_object_file)
738 if i['name'] is None or len(i['name']) == 0:
740 elif collection_uuid_pattern.match(i['uuid']) or group_uuid_pattern.match(i['uuid']):
741 # collection or subproject
743 elif link_uuid_pattern.match(i['uuid']) and i['head_kind'] == 'arvados#collection':
746 elif 'kind' in i and i['kind'].startswith('arvados#'):
748 return "{}.{}".format(i['name'], i['kind'][8:])
753 if isinstance(a, CollectionDirectory) or isinstance(a, ProjectDirectory):
754 return a.uuid() == i['uuid']
755 elif isinstance(a, ObjectFile):
756 return a.uuid() == i['uuid'] and not a.stale()
760 with llfuse.lock_released:
761 self._updating_lock.acquire()
765 if group_uuid_pattern.match(self.project_uuid):
766 self.project_object = self.api.groups().get(
767 uuid=self.project_uuid).execute(num_retries=self.num_retries)
768 elif user_uuid_pattern.match(self.project_uuid):
769 self.project_object = self.api.users().get(
770 uuid=self.project_uuid).execute(num_retries=self.num_retries)
772 contents = arvados.util.list_all(self.api.groups().contents,
773 self.num_retries, uuid=self.project_uuid)
775 # end with llfuse.lock_released, re-acquire lock
780 self.createDirectory)
782 self._updating_lock.release()
786 def __getitem__(self, item):
787 if item == '.arvados#project':
788 return self.project_object_file
790 return super(ProjectDirectory, self).__getitem__(item)
792 def __contains__(self, k):
793 if k == '.arvados#project':
796 return super(ProjectDirectory, self).__contains__(k)
801 with llfuse.lock_released:
802 if not self._current_user:
803 self._current_user = self.api.users().current().execute(num_retries=self.num_retries)
804 return self._current_user["uuid"] in self.project_object["writable_by"]
811 def mkdir(self, name):
813 with llfuse.lock_released:
814 self.api.collections().create(body={"owner_uuid": self.project_uuid,
816 "manifest_text": ""}).execute(num_retries=self.num_retries)
818 except apiclient_errors.Error as error:
820 raise llfuse.FUSEError(errno.EEXIST)
824 def rmdir(self, name):
826 raise llfuse.FUSEError(errno.ENOENT)
827 if not isinstance(self[name], CollectionDirectory):
828 raise llfuse.FUSEError(errno.EPERM)
829 if len(self[name]) > 0:
830 raise llfuse.FUSEError(errno.ENOTEMPTY)
831 with llfuse.lock_released:
832 self.api.collections().delete(uuid=self[name].uuid()).execute(num_retries=self.num_retries)
837 def rename(self, name_old, name_new, src):
838 if not isinstance(src, ProjectDirectory):
839 raise llfuse.FUSEError(errno.EPERM)
843 if not isinstance(ent, CollectionDirectory):
844 raise llfuse.FUSEError(errno.EPERM)
847 # POSIX semantics for replacing one directory with another is
848 # tricky (the target directory must be empty, the operation must be
849 # atomic which isn't possible with the Arvados API as of this
850 # writing) so don't support that.
851 raise llfuse.FUSEError(errno.EPERM)
853 self.api.collections().update(uuid=ent.uuid(),
854 body={"owner_uuid": self.uuid(),
855 "name": name_new}).execute(num_retries=self.num_retries)
857 # Acually move the entry from source directory to this directory.
858 del src._entries[name_old]
859 self._entries[name_new] = ent
860 self.inodes.invalidate_entry(src.inode, name_old.encode(self.inodes.encoding))
863 class SharedDirectory(Directory):
864 """A special directory that represents users or groups who have shared projects with me."""
866 def __init__(self, parent_inode, inodes, api, num_retries, exclude,
867 poll=False, poll_time=60):
868 super(SharedDirectory, self).__init__(parent_inode, inodes)
870 self.num_retries = num_retries
871 self.current_user = api.users().current().execute(num_retries=num_retries)
873 self._poll_time = poll_time
877 with llfuse.lock_released:
878 all_projects = arvados.util.list_all(
879 self.api.groups().list, self.num_retries,
880 filters=[['group_class','=','project']])
882 for ob in all_projects:
883 objects[ob['uuid']] = ob
887 for ob in all_projects:
888 if ob['owner_uuid'] != self.current_user['uuid'] and ob['owner_uuid'] not in objects:
890 root_owners[ob['owner_uuid']] = True
892 lusers = arvados.util.list_all(
893 self.api.users().list, self.num_retries,
894 filters=[['uuid','in', list(root_owners)]])
895 lgroups = arvados.util.list_all(
896 self.api.groups().list, self.num_retries,
897 filters=[['uuid','in', list(root_owners)]])
903 objects[l["uuid"]] = l
905 objects[l["uuid"]] = l
908 for r in root_owners:
912 contents[obr["name"]] = obr
913 #elif obr.get("username"):
914 # contents[obr["username"]] = obr
915 elif "first_name" in obr:
916 contents[u"{} {}".format(obr["first_name"], obr["last_name"])] = obr
920 if r['owner_uuid'] not in objects:
921 contents[r['name']] = r
923 # end with llfuse.lock_released, re-acquire lock
926 self.merge(contents.items(),
928 lambda a, i: a.uuid() == i[1]['uuid'],
929 lambda i: ProjectDirectory(self.inode, self.inodes, self.api, self.num_retries, i[1], poll=self._poll, poll_time=self._poll_time))