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 want_event_subscribe(self):
188 raise NotImplementedError()
190 def create(self, name):
191 raise NotImplementedError()
193 def mkdir(self, name):
194 raise NotImplementedError()
196 def unlink(self, name):
197 raise NotImplementedError()
199 def rmdir(self, name):
200 raise NotImplementedError()
202 def rename(self, name_old, name_new, src):
203 raise NotImplementedError()
206 class CollectionDirectoryBase(Directory):
207 """Represent an Arvados Collection as a directory.
209 This class is used for Subcollections, and is also the base class for
210 CollectionDirectory, which implements collection loading/saving on
213 Most operations act only the underlying Arvados `Collection` object. The
214 `Collection` object signals via a notify callback to
215 `CollectionDirectoryBase.on_event` that an item was added, removed or
216 modified. FUSE inodes and directory entries are created, deleted or
217 invalidated in response to these events.
221 def __init__(self, parent_inode, inodes, collection):
222 super(CollectionDirectoryBase, self).__init__(parent_inode, inodes)
223 self.collection = collection
225 def new_entry(self, name, item, mtime):
226 name = sanitize_filename(name)
227 if hasattr(item, "fuse_entry") and item.fuse_entry is not None:
228 if item.fuse_entry.dead is not True:
229 raise Exception("Can only reparent dead inode entry")
230 if item.fuse_entry.inode is None:
231 raise Exception("Reparented entry must still have valid inode")
232 item.fuse_entry.dead = False
233 self._entries[name] = item.fuse_entry
234 elif isinstance(item, arvados.collection.RichCollectionBase):
235 self._entries[name] = self.inodes.add_entry(CollectionDirectoryBase(self.inode, self.inodes, item))
236 self._entries[name].populate(mtime)
238 self._entries[name] = self.inodes.add_entry(FuseArvadosFile(self.inode, item, mtime))
239 item.fuse_entry = self._entries[name]
241 def on_event(self, event, collection, name, item):
242 if collection == self.collection:
243 name = sanitize_filename(name)
244 _logger.debug("collection notify %s %s %s %s", event, collection, name, item)
246 if event == arvados.collection.ADD:
247 self.new_entry(name, item, self.mtime())
248 elif event == arvados.collection.DEL:
249 ent = self._entries[name]
250 del self._entries[name]
251 self.inodes.invalidate_entry(self.inode, name.encode(self.inodes.encoding))
252 self.inodes.del_entry(ent)
253 elif event == arvados.collection.MOD:
254 if hasattr(item, "fuse_entry") and item.fuse_entry is not None:
255 self.inodes.invalidate_inode(item.fuse_entry.inode)
256 elif name in self._entries:
257 self.inodes.invalidate_inode(self._entries[name].inode)
259 def populate(self, mtime):
261 self.collection.subscribe(self.on_event)
262 for entry, item in self.collection.items():
263 self.new_entry(entry, item, self.mtime())
266 return self.collection.writable()
270 with llfuse.lock_released:
271 self.collection.root_collection().save()
275 def create(self, name):
276 with llfuse.lock_released:
277 self.collection.open(name, "w").close()
281 def mkdir(self, name):
282 with llfuse.lock_released:
283 self.collection.mkdirs(name)
287 def unlink(self, name):
288 with llfuse.lock_released:
289 self.collection.remove(name)
294 def rmdir(self, name):
295 with llfuse.lock_released:
296 self.collection.remove(name)
301 def rename(self, name_old, name_new, src):
302 if not isinstance(src, CollectionDirectoryBase):
303 raise llfuse.FUSEError(errno.EPERM)
308 if isinstance(ent, FuseArvadosFile) and isinstance(tgt, FuseArvadosFile):
310 elif isinstance(ent, CollectionDirectoryBase) and isinstance(tgt, CollectionDirectoryBase):
312 raise llfuse.FUSEError(errno.ENOTEMPTY)
313 elif isinstance(ent, CollectionDirectoryBase) and isinstance(tgt, FuseArvadosFile):
314 raise llfuse.FUSEError(errno.ENOTDIR)
315 elif isinstance(ent, FuseArvadosFile) and isinstance(tgt, CollectionDirectoryBase):
316 raise llfuse.FUSEError(errno.EISDIR)
318 with llfuse.lock_released:
319 self.collection.rename(name_old, name_new, source_collection=src.collection, overwrite=True)
324 class CollectionDirectory(CollectionDirectoryBase):
325 """Represents the root of a directory tree representing a collection."""
327 def __init__(self, parent_inode, inodes, api, num_retries, collection_record=None, explicit_collection=None):
328 super(CollectionDirectory, self).__init__(parent_inode, inodes, None)
330 self.num_retries = num_retries
331 self.collection_record_file = None
332 self.collection_record = None
335 self._poll_time = (api._rootDesc.get('blobSignatureTtl', 60*60*2)/2)
337 _logger.debug("Error getting blobSignatureTtl from discovery document: %s", sys.exc_info()[0])
338 self._poll_time = 60*60
340 if isinstance(collection_record, dict):
341 self.collection_locator = collection_record['uuid']
342 self._mtime = convertTime(collection_record.get('modified_at'))
344 self.collection_locator = collection_record
346 self._manifest_size = 0
347 if self.collection_locator:
348 self._writable = (uuid_pattern.match(self.collection_locator) is not None)
349 self._updating_lock = threading.Lock()
352 return i['uuid'] == self.collection_locator or i['portable_data_hash'] == self.collection_locator
355 return self.collection.writable() if self.collection is not None else self._writable
357 def want_event_subscribe(self):
358 return (uuid_pattern.match(self.collection_locator) is not None)
360 # Used by arv-web.py to switch the contents of the CollectionDirectory
361 def change_collection(self, new_locator):
362 """Switch the contents of the CollectionDirectory.
364 Must be called with llfuse.lock held.
367 self.collection_locator = new_locator
368 self.collection_record = None
371 def new_collection(self, new_collection_record, coll_reader):
373 self.clear(force=True)
375 self.collection_record = new_collection_record
377 if self.collection_record:
378 self._mtime = convertTime(self.collection_record.get('modified_at'))
379 self.collection_locator = self.collection_record["uuid"]
380 if self.collection_record_file is not None:
381 self.collection_record_file.update(self.collection_record)
383 self.collection = coll_reader
384 self.populate(self.mtime())
387 return self.collection_locator
390 def update(self, to_record_version=None):
392 if self.collection_record is not None and portable_data_hash_pattern.match(self.collection_locator):
395 if self.collection_locator is None:
400 with llfuse.lock_released:
401 self._updating_lock.acquire()
405 _logger.debug("Updating %s", to_record_version)
406 if self.collection is not None:
407 if self.collection.known_past_version(to_record_version):
408 _logger.debug("%s already processed %s", self.collection_locator, to_record_version)
410 self.collection.update()
412 if uuid_pattern.match(self.collection_locator):
413 coll_reader = arvados.collection.Collection(
414 self.collection_locator, self.api, self.api.keep,
415 num_retries=self.num_retries)
417 coll_reader = arvados.collection.CollectionReader(
418 self.collection_locator, self.api, self.api.keep,
419 num_retries=self.num_retries)
420 new_collection_record = coll_reader.api_response() or {}
421 # If the Collection only exists in Keep, there will be no API
422 # response. Fill in the fields we need.
423 if 'uuid' not in new_collection_record:
424 new_collection_record['uuid'] = self.collection_locator
425 if "portable_data_hash" not in new_collection_record:
426 new_collection_record["portable_data_hash"] = new_collection_record["uuid"]
427 if 'manifest_text' not in new_collection_record:
428 new_collection_record['manifest_text'] = coll_reader.manifest_text()
430 if self.collection_record is None or self.collection_record["portable_data_hash"] != new_collection_record.get("portable_data_hash"):
431 self.new_collection(new_collection_record, coll_reader)
433 self._manifest_size = len(coll_reader.manifest_text())
434 _logger.debug("%s manifest_size %i", self, self._manifest_size)
435 # end with llfuse.lock_released, re-acquire lock
440 self._updating_lock.release()
441 except arvados.errors.NotFoundError as e:
442 _logger.error("Error fetching collection '%s': %s", self.collection_locator, e)
443 except arvados.errors.ArgumentError as detail:
444 _logger.warning("arv-mount %s: error %s", self.collection_locator, detail)
445 if self.collection_record is not None and "manifest_text" in self.collection_record:
446 _logger.warning("arv-mount manifest_text is: %s", self.collection_record["manifest_text"])
448 _logger.exception("arv-mount %s: error", self.collection_locator)
449 if self.collection_record is not None and "manifest_text" in self.collection_record:
450 _logger.error("arv-mount manifest_text is: %s", self.collection_record["manifest_text"])
456 def __getitem__(self, item):
457 if item == '.arvados#collection':
458 if self.collection_record_file is None:
459 self.collection_record_file = ObjectFile(self.inode, self.collection_record)
460 self.inodes.add_entry(self.collection_record_file)
461 return self.collection_record_file
463 return super(CollectionDirectory, self).__getitem__(item)
465 def __contains__(self, k):
466 if k == '.arvados#collection':
469 return super(CollectionDirectory, self).__contains__(k)
471 def invalidate(self):
472 self.collection_record = None
473 self.collection_record_file = None
474 super(CollectionDirectory, self).invalidate()
477 return (self.collection_locator is not None)
480 # This is an empirically-derived heuristic to estimate the memory used
481 # to store this collection's metadata. Calculating the memory
482 # footprint directly would be more accurate, but also more complicated.
483 return self._manifest_size * 128
486 if self.collection is not None:
488 self.collection.save()
489 self.collection.stop_threads()
492 class TmpCollectionDirectory(CollectionDirectoryBase):
493 """A directory backed by an Arvados collection that never gets saved.
495 This supports using Keep as scratch space. A userspace program can
496 read the .arvados#collection file to get a current manifest in
497 order to save a snapshot of the scratch data or use it as a crunch
501 class UnsaveableCollection(arvados.collection.Collection):
507 def __init__(self, parent_inode, inodes, api_client, num_retries):
508 collection = self.UnsaveableCollection(
509 api_client=api_client,
510 keep_client=api_client.keep,
511 num_retries=num_retries)
512 super(TmpCollectionDirectory, self).__init__(
513 parent_inode, inodes, collection)
514 self.collection_record_file = None
515 self.populate(self.mtime())
517 def on_event(self, *args, **kwargs):
518 super(TmpCollectionDirectory, self).on_event(*args, **kwargs)
519 if self.collection_record_file:
521 self.collection_record_file.invalidate()
522 self.inodes.invalidate_inode(self.collection_record_file.inode)
523 _logger.debug("%s invalidated collection record", self)
525 def collection_record(self):
526 with llfuse.lock_released:
529 "manifest_text": self.collection.manifest_text(),
530 "portable_data_hash": self.collection.portable_data_hash(),
533 def __contains__(self, k):
534 return (k == '.arvados#collection' or
535 super(TmpCollectionDirectory, self).__contains__(k))
538 def __getitem__(self, item):
539 if item == '.arvados#collection':
540 if self.collection_record_file is None:
541 self.collection_record_file = FuncToJSONFile(
542 self.inode, self.collection_record)
543 self.inodes.add_entry(self.collection_record_file)
544 return self.collection_record_file
545 return super(TmpCollectionDirectory, self).__getitem__(item)
553 def want_event_subscribe(self):
557 self.collection.stop_threads()
559 def invalidate(self):
560 if self.collection_record_file:
561 self.collection_record_file.invalidate()
562 super(TmpCollectionDirectory, self).invalidate()
565 class MagicDirectory(Directory):
566 """A special directory that logically contains the set of all extant keep locators.
568 When a file is referenced by lookup(), it is tested to see if it is a valid
569 keep locator to a manifest, and if so, loads the manifest contents as a
570 subdirectory of this directory with the locator as the directory name.
571 Since querying a list of all extant keep locators is impractical, only
572 collections that have already been accessed are visible to readdir().
577 This directory provides access to Arvados collections as subdirectories listed
578 by uuid (in the form 'zzzzz-4zz18-1234567890abcde') or portable data hash (in
579 the form '1234567890abcdef0123456789abcdef+123').
581 Note that this directory will appear empty until you attempt to access a
582 specific collection subdirectory (such as trying to 'cd' into it), at which
583 point the collection will actually be looked up on the server and the directory
584 will appear if it exists.
588 def __init__(self, parent_inode, inodes, api, num_retries, pdh_only=False):
589 super(MagicDirectory, self).__init__(parent_inode, inodes)
591 self.num_retries = num_retries
592 self.pdh_only = pdh_only
594 def __setattr__(self, name, value):
595 super(MagicDirectory, self).__setattr__(name, value)
596 # When we're assigned an inode, add a README.
597 if ((name == 'inode') and (self.inode is not None) and
598 (not self._entries)):
599 self._entries['README'] = self.inodes.add_entry(
600 StringFile(self.inode, self.README_TEXT, time.time()))
601 # If we're the root directory, add an identical by_id subdirectory.
602 if self.inode == llfuse.ROOT_INODE:
603 self._entries['by_id'] = self.inodes.add_entry(MagicDirectory(
604 self.inode, self.inodes, self.api, self.num_retries, self.pdh_only))
606 def __contains__(self, k):
607 if k in self._entries:
610 if not portable_data_hash_pattern.match(k) and (self.pdh_only or not uuid_pattern.match(k)):
614 e = self.inodes.add_entry(CollectionDirectory(
615 self.inode, self.inodes, self.api, self.num_retries, k))
618 if k not in self._entries:
621 self.inodes.del_entry(e)
624 self.inodes.invalidate_entry(self.inode, k)
625 self.inodes.del_entry(e)
627 except Exception as ex:
628 _logger.debug('arv-mount exception keep %s', ex)
629 self.inodes.del_entry(e)
632 def __getitem__(self, item):
634 return self._entries[item]
636 raise KeyError("No collection with id " + item)
638 def clear(self, force=False):
641 def want_event_subscribe(self):
642 return not self.pdh_only
645 class RecursiveInvalidateDirectory(Directory):
646 def invalidate(self):
648 super(RecursiveInvalidateDirectory, self).invalidate()
649 for a in self._entries:
650 self._entries[a].invalidate()
655 class TagsDirectory(RecursiveInvalidateDirectory):
656 """A special directory that contains as subdirectories all tags visible to the user."""
658 def __init__(self, parent_inode, inodes, api, num_retries, poll_time=60):
659 super(TagsDirectory, self).__init__(parent_inode, inodes)
661 self.num_retries = num_retries
663 self._poll_time = poll_time
665 def want_event_subscribe(self):
670 with llfuse.lock_released:
671 tags = self.api.links().list(
672 filters=[['link_class', '=', 'tag']],
673 select=['name'], distinct=True
674 ).execute(num_retries=self.num_retries)
676 self.merge(tags['items'],
678 lambda a, i: a.tag == i['name'],
679 lambda i: TagDirectory(self.inode, self.inodes, self.api, self.num_retries, i['name'], poll=self._poll, poll_time=self._poll_time))
682 class TagDirectory(Directory):
683 """A special directory that contains as subdirectories all collections visible
684 to the user that are tagged with a particular tag.
687 def __init__(self, parent_inode, inodes, api, num_retries, tag,
688 poll=False, poll_time=60):
689 super(TagDirectory, self).__init__(parent_inode, inodes)
691 self.num_retries = num_retries
694 self._poll_time = poll_time
696 def want_event_subscribe(self):
701 with llfuse.lock_released:
702 taggedcollections = self.api.links().list(
703 filters=[['link_class', '=', 'tag'],
704 ['name', '=', self.tag],
705 ['head_uuid', 'is_a', 'arvados#collection']],
707 ).execute(num_retries=self.num_retries)
708 self.merge(taggedcollections['items'],
709 lambda i: i['head_uuid'],
710 lambda a, i: a.collection_locator == i['head_uuid'],
711 lambda i: CollectionDirectory(self.inode, self.inodes, self.api, self.num_retries, i['head_uuid']))
714 class ProjectDirectory(Directory):
715 """A special directory that contains the contents of a project."""
717 def __init__(self, parent_inode, inodes, api, num_retries, project_object,
718 poll=False, poll_time=60):
719 super(ProjectDirectory, self).__init__(parent_inode, inodes)
721 self.num_retries = num_retries
722 self.project_object = project_object
723 self.project_object_file = None
724 self.project_uuid = project_object['uuid']
726 self._poll_time = poll_time
727 self._updating_lock = threading.Lock()
728 self._current_user = None
730 def want_event_subscribe(self):
733 def createDirectory(self, i):
734 if collection_uuid_pattern.match(i['uuid']):
735 return CollectionDirectory(self.inode, self.inodes, self.api, self.num_retries, i)
736 elif group_uuid_pattern.match(i['uuid']):
737 return ProjectDirectory(self.inode, self.inodes, self.api, self.num_retries, i, self._poll, self._poll_time)
738 elif link_uuid_pattern.match(i['uuid']):
739 if i['head_kind'] == 'arvados#collection' or portable_data_hash_pattern.match(i['head_uuid']):
740 return CollectionDirectory(self.inode, self.inodes, self.api, self.num_retries, i['head_uuid'])
743 elif uuid_pattern.match(i['uuid']):
744 return ObjectFile(self.parent_inode, i)
749 return self.project_uuid
753 if self.project_object_file == None:
754 self.project_object_file = ObjectFile(self.inode, self.project_object)
755 self.inodes.add_entry(self.project_object_file)
759 if i['name'] is None or len(i['name']) == 0:
761 elif collection_uuid_pattern.match(i['uuid']) or group_uuid_pattern.match(i['uuid']):
762 # collection or subproject
764 elif link_uuid_pattern.match(i['uuid']) and i['head_kind'] == 'arvados#collection':
767 elif 'kind' in i and i['kind'].startswith('arvados#'):
769 return "{}.{}".format(i['name'], i['kind'][8:])
774 if isinstance(a, CollectionDirectory) or isinstance(a, ProjectDirectory):
775 return a.uuid() == i['uuid']
776 elif isinstance(a, ObjectFile):
777 return a.uuid() == i['uuid'] and not a.stale()
781 with llfuse.lock_released:
782 self._updating_lock.acquire()
786 if group_uuid_pattern.match(self.project_uuid):
787 self.project_object = self.api.groups().get(
788 uuid=self.project_uuid).execute(num_retries=self.num_retries)
789 elif user_uuid_pattern.match(self.project_uuid):
790 self.project_object = self.api.users().get(
791 uuid=self.project_uuid).execute(num_retries=self.num_retries)
793 contents = arvados.util.list_all(self.api.groups().contents,
794 self.num_retries, uuid=self.project_uuid)
796 # end with llfuse.lock_released, re-acquire lock
801 self.createDirectory)
803 self._updating_lock.release()
807 def __getitem__(self, item):
808 if item == '.arvados#project':
809 return self.project_object_file
811 return super(ProjectDirectory, self).__getitem__(item)
813 def __contains__(self, k):
814 if k == '.arvados#project':
817 return super(ProjectDirectory, self).__contains__(k)
822 with llfuse.lock_released:
823 if not self._current_user:
824 self._current_user = self.api.users().current().execute(num_retries=self.num_retries)
825 return self._current_user["uuid"] in self.project_object["writable_by"]
832 def mkdir(self, name):
834 with llfuse.lock_released:
835 self.api.collections().create(body={"owner_uuid": self.project_uuid,
837 "manifest_text": ""}).execute(num_retries=self.num_retries)
839 except apiclient_errors.Error as error:
841 raise llfuse.FUSEError(errno.EEXIST)
845 def rmdir(self, name):
847 raise llfuse.FUSEError(errno.ENOENT)
848 if not isinstance(self[name], CollectionDirectory):
849 raise llfuse.FUSEError(errno.EPERM)
850 if len(self[name]) > 0:
851 raise llfuse.FUSEError(errno.ENOTEMPTY)
852 with llfuse.lock_released:
853 self.api.collections().delete(uuid=self[name].uuid()).execute(num_retries=self.num_retries)
858 def rename(self, name_old, name_new, src):
859 if not isinstance(src, ProjectDirectory):
860 raise llfuse.FUSEError(errno.EPERM)
864 if not isinstance(ent, CollectionDirectory):
865 raise llfuse.FUSEError(errno.EPERM)
868 # POSIX semantics for replacing one directory with another is
869 # tricky (the target directory must be empty, the operation must be
870 # atomic which isn't possible with the Arvados API as of this
871 # writing) so don't support that.
872 raise llfuse.FUSEError(errno.EPERM)
874 self.api.collections().update(uuid=ent.uuid(),
875 body={"owner_uuid": self.uuid(),
876 "name": name_new}).execute(num_retries=self.num_retries)
878 # Acually move the entry from source directory to this directory.
879 del src._entries[name_old]
880 self._entries[name_new] = ent
881 self.inodes.invalidate_entry(src.inode, name_old.encode(self.inodes.encoding))
884 class SharedDirectory(Directory):
885 """A special directory that represents users or groups who have shared projects with me."""
887 def __init__(self, parent_inode, inodes, api, num_retries, exclude,
888 poll=False, poll_time=60):
889 super(SharedDirectory, self).__init__(parent_inode, inodes)
891 self.num_retries = num_retries
892 self.current_user = api.users().current().execute(num_retries=num_retries)
894 self._poll_time = poll_time
898 with llfuse.lock_released:
899 all_projects = arvados.util.list_all(
900 self.api.groups().list, self.num_retries,
901 filters=[['group_class','=','project']])
903 for ob in all_projects:
904 objects[ob['uuid']] = ob
908 for ob in all_projects:
909 if ob['owner_uuid'] != self.current_user['uuid'] and ob['owner_uuid'] not in objects:
911 root_owners[ob['owner_uuid']] = True
913 lusers = arvados.util.list_all(
914 self.api.users().list, self.num_retries,
915 filters=[['uuid','in', list(root_owners)]])
916 lgroups = arvados.util.list_all(
917 self.api.groups().list, self.num_retries,
918 filters=[['uuid','in', list(root_owners)]])
924 objects[l["uuid"]] = l
926 objects[l["uuid"]] = l
929 for r in root_owners:
933 contents[obr["name"]] = obr
934 #elif obr.get("username"):
935 # contents[obr["username"]] = obr
936 elif "first_name" in obr:
937 contents[u"{} {}".format(obr["first_name"], obr["last_name"])] = obr
941 if r['owner_uuid'] not in objects:
942 contents[r['name']] = r
944 # end with llfuse.lock_released, re-acquire lock
947 self.merge(contents.items(),
949 lambda a, i: a.uuid() == i[1]['uuid'],
950 lambda i: ProjectDirectory(self.inode, self.inodes, self.api, self.num_retries, i[1], poll=self._poll, poll_time=self._poll_time))
954 def want_event_subscribe(self):