9 from apiclient import errors as apiclient_errors
12 from fusefile import StringFile, ObjectFile, FuseArvadosFile
13 from fresh import FreshBase, convertTime, use_counter, check_update
15 import arvados.collection
16 from arvados.util import portable_data_hash_pattern, uuid_pattern, collection_uuid_pattern, group_uuid_pattern, user_uuid_pattern, link_uuid_pattern
18 _logger = logging.getLogger('arvados.arvados_fuse')
21 # Match any character which FUSE or Linux cannot accommodate as part
22 # of a filename. (If present in a collection filename, they will
23 # appear as underscores in the fuse mount.)
24 _disallowed_filename_characters = re.compile('[\x00/]')
26 # '.' and '..' are not reachable if API server is newer than #6277
27 def sanitize_filename(dirty):
28 """Replace disallowed filename characters with harmless "_"."""
38 return _disallowed_filename_characters.sub('_', dirty)
41 class Directory(FreshBase):
42 """Generic directory object, backed by a dict.
44 Consists of a set of entries with the key representing the filename
45 and the value referencing a File or Directory object.
48 def __init__(self, parent_inode, inodes):
49 """parent_inode is the integer inode number"""
51 super(Directory, self).__init__()
54 if not isinstance(parent_inode, int):
55 raise Exception("parent_inode should be an int")
56 self.parent_inode = parent_inode
59 self._mtime = time.time()
61 # Overriden by subclasses to implement logic to update the entries dict
62 # when the directory is stale
67 # Only used when computing the size of the disk footprint of the directory
75 def checkupdate(self):
79 except apiclient.errors.HttpError as e:
84 def __getitem__(self, item):
85 return self._entries[item]
90 return list(self._entries.items())
94 def __contains__(self, k):
95 return k in self._entries
100 return len(self._entries)
103 self.inodes.touch(self)
104 super(Directory, self).fresh()
106 def merge(self, items, fn, same, new_entry):
107 """Helper method for updating the contents of the directory.
109 Takes a list describing the new contents of the directory, reuse
110 entries that are the same in both the old and new lists, create new
111 entries, and delete old entries missing from the new list.
113 :items: iterable with new directory contents
115 :fn: function to take an entry in 'items' and return the desired file or
116 directory name, or None if this entry should be skipped
118 :same: function to compare an existing entry (a File or Directory
119 object) with an entry in the items list to determine whether to keep
122 :new_entry: function to create a new directory entry (File or Directory
123 object) from an entry in the items list.
127 oldentries = self._entries
131 name = sanitize_filename(fn(i))
133 if name in oldentries and same(oldentries[name], i):
134 # move existing directory entry over
135 self._entries[name] = oldentries[name]
138 _logger.debug("Adding entry '%s' to inode %i", name, self.inode)
139 # create new directory entry
142 self._entries[name] = self.inodes.add_entry(ent)
145 # delete any other directory entries that were not in found in 'items'
147 _logger.debug("Forgetting about entry '%s' on inode %i", i, self.inode)
148 self.inodes.invalidate_entry(self.inode, i.encode(self.inodes.encoding))
149 self.inodes.del_entry(oldentries[i])
153 self.inodes.invalidate_inode(self.inode)
154 self._mtime = time.time()
158 def clear(self, force=False):
159 """Delete all entries"""
161 if not self.in_use() or force:
162 oldentries = self._entries
165 if not oldentries[n].clear(force):
166 self._entries = oldentries
169 self.inodes.invalidate_entry(self.inode, n.encode(self.inodes.encoding))
170 self.inodes.del_entry(oldentries[n])
171 self.inodes.invalidate_inode(self.inode)
186 def create(self, name):
187 raise NotImplementedError()
189 def mkdir(self, name):
190 raise NotImplementedError()
192 def unlink(self, name):
193 raise NotImplementedError()
195 def rmdir(self, name):
196 raise NotImplementedError()
198 def rename(self, name_old, name_new, src):
199 raise NotImplementedError()
202 class CollectionDirectoryBase(Directory):
203 """Represent an Arvados Collection as a directory.
205 This class is used for Subcollections, and is also the base class for
206 CollectionDirectory, which implements collection loading/saving on
209 Most operations act only the underlying Arvados `Collection` object. The
210 `Collection` object signals via a notify callback to
211 `CollectionDirectoryBase.on_event` that an item was added, removed or
212 modified. FUSE inodes and directory entries are created, deleted or
213 invalidated in response to these events.
217 def __init__(self, parent_inode, inodes, collection):
218 super(CollectionDirectoryBase, self).__init__(parent_inode, inodes)
219 self.collection = collection
221 def new_entry(self, name, item, mtime):
222 name = sanitize_filename(name)
223 if hasattr(item, "fuse_entry") and item.fuse_entry is not None:
224 if item.fuse_entry.dead is not True:
225 raise Exception("Can only reparent dead inode entry")
226 if item.fuse_entry.inode is None:
227 raise Exception("Reparented entry must still have valid inode")
228 item.fuse_entry.dead = False
229 self._entries[name] = item.fuse_entry
230 elif isinstance(item, arvados.collection.RichCollectionBase):
231 self._entries[name] = self.inodes.add_entry(CollectionDirectoryBase(self.inode, self.inodes, item))
232 self._entries[name].populate(mtime)
234 self._entries[name] = self.inodes.add_entry(FuseArvadosFile(self.inode, item, mtime))
235 item.fuse_entry = self._entries[name]
237 def on_event(self, event, collection, name, item):
238 if collection == self.collection:
239 name = sanitize_filename(name)
240 _logger.debug("collection notify %s %s %s %s", event, collection, name, item)
242 if event == arvados.collection.ADD:
243 self.new_entry(name, item, self.mtime())
244 elif event == arvados.collection.DEL:
245 ent = self._entries[name]
246 del self._entries[name]
247 self.inodes.invalidate_entry(self.inode, name.encode(self.inodes.encoding))
248 self.inodes.del_entry(ent)
249 elif event == arvados.collection.MOD:
250 if hasattr(item, "fuse_entry") and item.fuse_entry is not None:
251 self.inodes.invalidate_inode(item.fuse_entry.inode)
252 elif name in self._entries:
253 self.inodes.invalidate_inode(self._entries[name].inode)
255 def populate(self, mtime):
257 self.collection.subscribe(self.on_event)
258 for entry, item in self.collection.items():
259 self.new_entry(entry, item, self.mtime())
262 return self.collection.writable()
266 with llfuse.lock_released:
267 self.collection.root_collection().save()
271 def create(self, name):
272 with llfuse.lock_released:
273 self.collection.open(name, "w").close()
277 def mkdir(self, name):
278 with llfuse.lock_released:
279 self.collection.mkdirs(name)
283 def unlink(self, name):
284 with llfuse.lock_released:
285 self.collection.remove(name)
290 def rmdir(self, name):
291 with llfuse.lock_released:
292 self.collection.remove(name)
297 def rename(self, name_old, name_new, src):
298 if not isinstance(src, CollectionDirectoryBase):
299 raise llfuse.FUSEError(errno.EPERM)
304 if isinstance(ent, FuseArvadosFile) and isinstance(tgt, FuseArvadosFile):
306 elif isinstance(ent, CollectionDirectoryBase) and isinstance(tgt, CollectionDirectoryBase):
308 raise llfuse.FUSEError(errno.ENOTEMPTY)
309 elif isinstance(ent, CollectionDirectoryBase) and isinstance(tgt, FuseArvadosFile):
310 raise llfuse.FUSEError(errno.ENOTDIR)
311 elif isinstance(ent, FuseArvadosFile) and isinstance(tgt, CollectionDirectoryBase):
312 raise llfuse.FUSEError(errno.EISDIR)
314 with llfuse.lock_released:
315 self.collection.rename(name_old, name_new, source_collection=src.collection, overwrite=True)
320 class CollectionDirectory(CollectionDirectoryBase):
321 """Represents the root of a directory tree representing a collection."""
323 def __init__(self, parent_inode, inodes, api, num_retries, collection_record=None, explicit_collection=None):
324 super(CollectionDirectory, self).__init__(parent_inode, inodes, None)
326 self.num_retries = num_retries
327 self.collection_record_file = None
328 self.collection_record = None
329 if isinstance(collection_record, dict):
330 self.collection_locator = collection_record['uuid']
331 self._mtime = convertTime(collection_record.get('modified_at'))
333 self.collection_locator = collection_record
335 self._manifest_size = 0
336 if self.collection_locator:
337 self._writable = (uuid_pattern.match(self.collection_locator) is not None)
338 self._updating_lock = threading.Lock()
341 return i['uuid'] == self.collection_locator or i['portable_data_hash'] == self.collection_locator
344 return self.collection.writable() if self.collection is not None else self._writable
346 # Used by arv-web.py to switch the contents of the CollectionDirectory
347 def change_collection(self, new_locator):
348 """Switch the contents of the CollectionDirectory.
350 Must be called with llfuse.lock held.
353 self.collection_locator = new_locator
354 self.collection_record = None
357 def new_collection(self, new_collection_record, coll_reader):
359 self.clear(force=True)
361 self.collection_record = new_collection_record
363 if self.collection_record:
364 self._mtime = convertTime(self.collection_record.get('modified_at'))
365 self.collection_locator = self.collection_record["uuid"]
366 if self.collection_record_file is not None:
367 self.collection_record_file.update(self.collection_record)
369 self.collection = coll_reader
370 self.populate(self.mtime())
373 return self.collection_locator
376 def update(self, to_record_version=None):
378 if self.collection_record is not None and portable_data_hash_pattern.match(self.collection_locator):
381 if self.collection_locator is None:
386 with llfuse.lock_released:
387 self._updating_lock.acquire()
391 _logger.debug("Updating %s", to_record_version)
392 if self.collection is not None:
393 if self.collection.known_past_version(to_record_version):
394 _logger.debug("%s already processed %s", self.collection_locator, to_record_version)
396 self.collection.update()
398 if uuid_pattern.match(self.collection_locator):
399 coll_reader = arvados.collection.Collection(
400 self.collection_locator, self.api, self.api.keep,
401 num_retries=self.num_retries)
403 coll_reader = arvados.collection.CollectionReader(
404 self.collection_locator, self.api, self.api.keep,
405 num_retries=self.num_retries)
406 new_collection_record = coll_reader.api_response() or {}
407 # If the Collection only exists in Keep, there will be no API
408 # response. Fill in the fields we need.
409 if 'uuid' not in new_collection_record:
410 new_collection_record['uuid'] = self.collection_locator
411 if "portable_data_hash" not in new_collection_record:
412 new_collection_record["portable_data_hash"] = new_collection_record["uuid"]
413 if 'manifest_text' not in new_collection_record:
414 new_collection_record['manifest_text'] = coll_reader.manifest_text()
416 if self.collection_record is None or self.collection_record["portable_data_hash"] != new_collection_record.get("portable_data_hash"):
417 self.new_collection(new_collection_record, coll_reader)
419 self._manifest_size = len(coll_reader.manifest_text())
420 _logger.debug("%s manifest_size %i", self, self._manifest_size)
421 # end with llfuse.lock_released, re-acquire lock
426 self._updating_lock.release()
427 except arvados.errors.NotFoundError as e:
428 _logger.error("Error fetching collection '%s': %s", self.collection_locator, e)
429 except arvados.errors.ArgumentError as detail:
430 _logger.warning("arv-mount %s: error %s", self.collection_locator, detail)
431 if self.collection_record is not None and "manifest_text" in self.collection_record:
432 _logger.warning("arv-mount manifest_text is: %s", self.collection_record["manifest_text"])
434 _logger.exception("arv-mount %s: error", self.collection_locator)
435 if self.collection_record is not None and "manifest_text" in self.collection_record:
436 _logger.error("arv-mount manifest_text is: %s", self.collection_record["manifest_text"])
441 def __getitem__(self, item):
442 if item == '.arvados#collection':
443 if self.collection_record_file is None:
444 self.collection_record_file = ObjectFile(self.inode, self.collection_record)
445 self.inodes.add_entry(self.collection_record_file)
446 return self.collection_record_file
448 return super(CollectionDirectory, self).__getitem__(item)
450 def __contains__(self, k):
451 if k == '.arvados#collection':
454 return super(CollectionDirectory, self).__contains__(k)
456 def invalidate(self):
457 self.collection_record = None
458 self.collection_record_file = None
459 super(CollectionDirectory, self).invalidate()
462 return (self.collection_locator is not None)
465 # This is an empirically-derived heuristic to estimate the memory used
466 # to store this collection's metadata. Calculating the memory
467 # footprint directly would be more accurate, but also more complicated.
468 return self._manifest_size * 128
471 if self.collection is not None:
473 self.collection.save()
474 self.collection.stop_threads()
477 class TmpCollectionDirectory(CollectionDirectoryBase):
478 """A directory backed by an Arvados collection that never gets saved.
480 This supports using Keep as scratch space. A userspace program can
481 read the .arvados#collection file to get a current manifest in
482 order to save a snapshot of the scratch data or use it as a crunch
486 def __init__(self, parent_inode, inodes, api_client, num_retries):
487 collection = arvados.collection.Collection(
488 api_client=api_client,
489 keep_client=api_client.keep)
490 collection.save = self._commit_collection
491 collection.save_new = self._commit_collection
492 super(TmpCollectionDirectory, self).__init__(
493 parent_inode, inodes, collection)
494 self.collection_record_file = None
495 self._subscribed = False
496 self._update_collection_record()
498 def update(self, *args, **kwargs):
499 if not self._subscribed:
500 with llfuse.lock_released:
501 self.populate(self.mtime())
502 self._subscribed = True
505 def _commit_collection(self):
506 """Commit the data blocks, but don't save the collection to API.
508 Update the content of the special .arvados#collection file, if
509 it has been instantiated.
511 self.collection.flush()
512 self._update_collection_record()
513 if self.collection_record_file is not None:
514 self.collection_record_file.update(self.collection_record)
515 self.inodes.invalidate_inode(self.collection_record_file.inode)
517 def _update_collection_record(self):
518 self.collection_record = {
520 "manifest_text": self.collection.manifest_text(),
521 "portable_data_hash": self.collection.portable_data_hash(),
524 def __contains__(self, k):
525 return (k == '.arvados#collection' or
526 super(TmpCollectionDirectory, self).__contains__(k))
529 def __getitem__(self, item):
530 if item == '.arvados#collection':
531 if self.collection_record_file is None:
532 self.collection_record_file = ObjectFile(
533 self.inode, self.collection_record)
534 self.inodes.add_entry(self.collection_record_file)
535 return self.collection_record_file
536 return super(TmpCollectionDirectory, self).__getitem__(item)
542 self.collection.stop_threads()
545 class MagicDirectory(Directory):
546 """A special directory that logically contains the set of all extant keep locators.
548 When a file is referenced by lookup(), it is tested to see if it is a valid
549 keep locator to a manifest, and if so, loads the manifest contents as a
550 subdirectory of this directory with the locator as the directory name.
551 Since querying a list of all extant keep locators is impractical, only
552 collections that have already been accessed are visible to readdir().
557 This directory provides access to Arvados collections as subdirectories listed
558 by uuid (in the form 'zzzzz-4zz18-1234567890abcde') or portable data hash (in
559 the form '1234567890abcdef0123456789abcdef+123').
561 Note that this directory will appear empty until you attempt to access a
562 specific collection subdirectory (such as trying to 'cd' into it), at which
563 point the collection will actually be looked up on the server and the directory
564 will appear if it exists.
568 def __init__(self, parent_inode, inodes, api, num_retries, pdh_only=False):
569 super(MagicDirectory, self).__init__(parent_inode, inodes)
571 self.num_retries = num_retries
572 self.pdh_only = pdh_only
574 def __setattr__(self, name, value):
575 super(MagicDirectory, self).__setattr__(name, value)
576 # When we're assigned an inode, add a README.
577 if ((name == 'inode') and (self.inode is not None) and
578 (not self._entries)):
579 self._entries['README'] = self.inodes.add_entry(
580 StringFile(self.inode, self.README_TEXT, time.time()))
581 # If we're the root directory, add an identical by_id subdirectory.
582 if self.inode == llfuse.ROOT_INODE:
583 self._entries['by_id'] = self.inodes.add_entry(MagicDirectory(
584 self.inode, self.inodes, self.api, self.num_retries, self.pdh_only))
586 def __contains__(self, k):
587 if k in self._entries:
590 if not portable_data_hash_pattern.match(k) and (self.pdh_only or not uuid_pattern.match(k)):
594 e = self.inodes.add_entry(CollectionDirectory(
595 self.inode, self.inodes, self.api, self.num_retries, k))
598 if k not in self._entries:
601 self.inodes.del_entry(e)
604 self.inodes.del_entry(e)
606 except Exception as e:
607 _logger.debug('arv-mount exception keep %s', e)
608 self.inodes.del_entry(e)
611 def __getitem__(self, item):
613 return self._entries[item]
615 raise KeyError("No collection with id " + item)
617 def clear(self, force=False):
621 class RecursiveInvalidateDirectory(Directory):
622 def invalidate(self):
624 super(RecursiveInvalidateDirectory, self).invalidate()
625 for a in self._entries:
626 self._entries[a].invalidate()
631 class TagsDirectory(RecursiveInvalidateDirectory):
632 """A special directory that contains as subdirectories all tags visible to the user."""
634 def __init__(self, parent_inode, inodes, api, num_retries, poll_time=60):
635 super(TagsDirectory, self).__init__(parent_inode, inodes)
637 self.num_retries = num_retries
639 self._poll_time = poll_time
643 with llfuse.lock_released:
644 tags = self.api.links().list(
645 filters=[['link_class', '=', 'tag']],
646 select=['name'], distinct=True
647 ).execute(num_retries=self.num_retries)
649 self.merge(tags['items'],
651 lambda a, i: a.tag == i['name'],
652 lambda i: TagDirectory(self.inode, self.inodes, self.api, self.num_retries, i['name'], poll=self._poll, poll_time=self._poll_time))
655 class TagDirectory(Directory):
656 """A special directory that contains as subdirectories all collections visible
657 to the user that are tagged with a particular tag.
660 def __init__(self, parent_inode, inodes, api, num_retries, tag,
661 poll=False, poll_time=60):
662 super(TagDirectory, self).__init__(parent_inode, inodes)
664 self.num_retries = num_retries
667 self._poll_time = poll_time
671 with llfuse.lock_released:
672 taggedcollections = self.api.links().list(
673 filters=[['link_class', '=', 'tag'],
674 ['name', '=', self.tag],
675 ['head_uuid', 'is_a', 'arvados#collection']],
677 ).execute(num_retries=self.num_retries)
678 self.merge(taggedcollections['items'],
679 lambda i: i['head_uuid'],
680 lambda a, i: a.collection_locator == i['head_uuid'],
681 lambda i: CollectionDirectory(self.inode, self.inodes, self.api, self.num_retries, i['head_uuid']))
684 class ProjectDirectory(Directory):
685 """A special directory that contains the contents of a project."""
687 def __init__(self, parent_inode, inodes, api, num_retries, project_object,
688 poll=False, poll_time=60):
689 super(ProjectDirectory, self).__init__(parent_inode, inodes)
691 self.num_retries = num_retries
692 self.project_object = project_object
693 self.project_object_file = None
694 self.project_uuid = project_object['uuid']
696 self._poll_time = poll_time
697 self._updating_lock = threading.Lock()
698 self._current_user = None
700 def createDirectory(self, i):
701 if collection_uuid_pattern.match(i['uuid']):
702 return CollectionDirectory(self.inode, self.inodes, self.api, self.num_retries, i)
703 elif group_uuid_pattern.match(i['uuid']):
704 return ProjectDirectory(self.inode, self.inodes, self.api, self.num_retries, i, self._poll, self._poll_time)
705 elif link_uuid_pattern.match(i['uuid']):
706 if i['head_kind'] == 'arvados#collection' or portable_data_hash_pattern.match(i['head_uuid']):
707 return CollectionDirectory(self.inode, self.inodes, self.api, self.num_retries, i['head_uuid'])
710 elif uuid_pattern.match(i['uuid']):
711 return ObjectFile(self.parent_inode, i)
716 return self.project_uuid
720 if self.project_object_file == None:
721 self.project_object_file = ObjectFile(self.inode, self.project_object)
722 self.inodes.add_entry(self.project_object_file)
726 if i['name'] is None or len(i['name']) == 0:
728 elif collection_uuid_pattern.match(i['uuid']) or group_uuid_pattern.match(i['uuid']):
729 # collection or subproject
731 elif link_uuid_pattern.match(i['uuid']) and i['head_kind'] == 'arvados#collection':
734 elif 'kind' in i and i['kind'].startswith('arvados#'):
736 return "{}.{}".format(i['name'], i['kind'][8:])
741 if isinstance(a, CollectionDirectory) or isinstance(a, ProjectDirectory):
742 return a.uuid() == i['uuid']
743 elif isinstance(a, ObjectFile):
744 return a.uuid() == i['uuid'] and not a.stale()
748 with llfuse.lock_released:
749 self._updating_lock.acquire()
753 if group_uuid_pattern.match(self.project_uuid):
754 self.project_object = self.api.groups().get(
755 uuid=self.project_uuid).execute(num_retries=self.num_retries)
756 elif user_uuid_pattern.match(self.project_uuid):
757 self.project_object = self.api.users().get(
758 uuid=self.project_uuid).execute(num_retries=self.num_retries)
760 contents = arvados.util.list_all(self.api.groups().contents,
761 self.num_retries, uuid=self.project_uuid)
763 # end with llfuse.lock_released, re-acquire lock
768 self.createDirectory)
770 self._updating_lock.release()
774 def __getitem__(self, item):
775 if item == '.arvados#project':
776 return self.project_object_file
778 return super(ProjectDirectory, self).__getitem__(item)
780 def __contains__(self, k):
781 if k == '.arvados#project':
784 return super(ProjectDirectory, self).__contains__(k)
789 with llfuse.lock_released:
790 if not self._current_user:
791 self._current_user = self.api.users().current().execute(num_retries=self.num_retries)
792 return self._current_user["uuid"] in self.project_object["writable_by"]
799 def mkdir(self, name):
801 with llfuse.lock_released:
802 self.api.collections().create(body={"owner_uuid": self.project_uuid,
804 "manifest_text": ""}).execute(num_retries=self.num_retries)
806 except apiclient_errors.Error as error:
808 raise llfuse.FUSEError(errno.EEXIST)
812 def rmdir(self, name):
814 raise llfuse.FUSEError(errno.ENOENT)
815 if not isinstance(self[name], CollectionDirectory):
816 raise llfuse.FUSEError(errno.EPERM)
817 if len(self[name]) > 0:
818 raise llfuse.FUSEError(errno.ENOTEMPTY)
819 with llfuse.lock_released:
820 self.api.collections().delete(uuid=self[name].uuid()).execute(num_retries=self.num_retries)
825 def rename(self, name_old, name_new, src):
826 if not isinstance(src, ProjectDirectory):
827 raise llfuse.FUSEError(errno.EPERM)
831 if not isinstance(ent, CollectionDirectory):
832 raise llfuse.FUSEError(errno.EPERM)
835 # POSIX semantics for replacing one directory with another is
836 # tricky (the target directory must be empty, the operation must be
837 # atomic which isn't possible with the Arvados API as of this
838 # writing) so don't support that.
839 raise llfuse.FUSEError(errno.EPERM)
841 self.api.collections().update(uuid=ent.uuid(),
842 body={"owner_uuid": self.uuid(),
843 "name": name_new}).execute(num_retries=self.num_retries)
845 # Acually move the entry from source directory to this directory.
846 del src._entries[name_old]
847 self._entries[name_new] = ent
848 self.inodes.invalidate_entry(src.inode, name_old.encode(self.inodes.encoding))
851 class SharedDirectory(Directory):
852 """A special directory that represents users or groups who have shared projects with me."""
854 def __init__(self, parent_inode, inodes, api, num_retries, exclude,
855 poll=False, poll_time=60):
856 super(SharedDirectory, self).__init__(parent_inode, inodes)
858 self.num_retries = num_retries
859 self.current_user = api.users().current().execute(num_retries=num_retries)
861 self._poll_time = poll_time
865 with llfuse.lock_released:
866 all_projects = arvados.util.list_all(
867 self.api.groups().list, self.num_retries,
868 filters=[['group_class','=','project']])
870 for ob in all_projects:
871 objects[ob['uuid']] = ob
875 for ob in all_projects:
876 if ob['owner_uuid'] != self.current_user['uuid'] and ob['owner_uuid'] not in objects:
878 root_owners[ob['owner_uuid']] = True
880 lusers = arvados.util.list_all(
881 self.api.users().list, self.num_retries,
882 filters=[['uuid','in', list(root_owners)]])
883 lgroups = arvados.util.list_all(
884 self.api.groups().list, self.num_retries,
885 filters=[['uuid','in', list(root_owners)]])
891 objects[l["uuid"]] = l
893 objects[l["uuid"]] = l
896 for r in root_owners:
900 contents[obr["name"]] = obr
901 #elif obr.get("username"):
902 # contents[obr["username"]] = obr
903 elif "first_name" in obr:
904 contents[u"{} {}".format(obr["first_name"], obr["last_name"])] = obr
908 if r['owner_uuid'] not in objects:
909 contents[r['name']] = r
911 # end with llfuse.lock_released, re-acquire lock
914 self.merge(contents.items(),
916 lambda a, i: a.uuid() == i[1]['uuid'],
917 lambda i: ProjectDirectory(self.inode, self.inodes, self.api, self.num_retries, i[1], poll=self._poll, poll_time=self._poll_time))