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
682 def want_event_subscribe(self):
687 with llfuse.lock_released:
688 tags = self.api.links().list(
689 filters=[['link_class', '=', 'tag'], ["name", "!=", ""]],
690 select=['name'], distinct=True, limit=1000
691 ).execute(num_retries=self.num_retries)
693 self.merge(tags['items']+[{"name": n} for n in self._extra],
695 lambda a, i: a.tag == i['name'],
696 lambda i: TagDirectory(self.inode, self.inodes, self.api, self.num_retries, i['name'], poll=self._poll, poll_time=self._poll_time))
700 def __getitem__(self, item):
701 if super(TagsDirectory, self).__contains__(item):
702 return super(TagsDirectory, self).__getitem__(item)
703 with llfuse.lock_released:
704 tags = self.api.links().list(
705 filters=[['link_class', '=', 'tag'], ['name', '=', item]], limit=1
706 ).execute(num_retries=self.num_retries)
708 self._extra.add(item)
710 return super(TagsDirectory, self).__getitem__(item)
714 def __contains__(self, k):
715 if super(TagsDirectory, self).__contains__(k):
725 class TagDirectory(Directory):
726 """A special directory that contains as subdirectories all collections visible
727 to the user that are tagged with a particular tag.
730 def __init__(self, parent_inode, inodes, api, num_retries, tag,
731 poll=False, poll_time=60):
732 super(TagDirectory, self).__init__(parent_inode, inodes)
734 self.num_retries = num_retries
737 self._poll_time = poll_time
739 def want_event_subscribe(self):
744 with llfuse.lock_released:
745 taggedcollections = self.api.links().list(
746 filters=[['link_class', '=', 'tag'],
747 ['name', '=', self.tag],
748 ['head_uuid', 'is_a', 'arvados#collection']],
750 ).execute(num_retries=self.num_retries)
751 self.merge(taggedcollections['items'],
752 lambda i: i['head_uuid'],
753 lambda a, i: a.collection_locator == i['head_uuid'],
754 lambda i: CollectionDirectory(self.inode, self.inodes, self.api, self.num_retries, i['head_uuid']))
757 class ProjectDirectory(Directory):
758 """A special directory that contains the contents of a project."""
760 def __init__(self, parent_inode, inodes, api, num_retries, project_object,
761 poll=False, poll_time=60):
762 super(ProjectDirectory, self).__init__(parent_inode, inodes)
764 self.num_retries = num_retries
765 self.project_object = project_object
766 self.project_object_file = None
767 self.project_uuid = project_object['uuid']
769 self._poll_time = poll_time
770 self._updating_lock = threading.Lock()
771 self._current_user = None
773 def want_event_subscribe(self):
776 def createDirectory(self, i):
777 if collection_uuid_pattern.match(i['uuid']):
778 return CollectionDirectory(self.inode, self.inodes, self.api, self.num_retries, i)
779 elif group_uuid_pattern.match(i['uuid']):
780 return ProjectDirectory(self.inode, self.inodes, self.api, self.num_retries, i, self._poll, self._poll_time)
781 elif link_uuid_pattern.match(i['uuid']):
782 if i['head_kind'] == 'arvados#collection' or portable_data_hash_pattern.match(i['head_uuid']):
783 return CollectionDirectory(self.inode, self.inodes, self.api, self.num_retries, i['head_uuid'])
786 elif uuid_pattern.match(i['uuid']):
787 return ObjectFile(self.parent_inode, i)
792 return self.project_uuid
796 if self.project_object_file == None:
797 self.project_object_file = ObjectFile(self.inode, self.project_object)
798 self.inodes.add_entry(self.project_object_file)
802 if i['name'] is None or len(i['name']) == 0:
804 elif collection_uuid_pattern.match(i['uuid']) or group_uuid_pattern.match(i['uuid']):
805 # collection or subproject
807 elif link_uuid_pattern.match(i['uuid']) and i['head_kind'] == 'arvados#collection':
810 elif 'kind' in i and i['kind'].startswith('arvados#'):
812 return "{}.{}".format(i['name'], i['kind'][8:])
817 if isinstance(a, CollectionDirectory) or isinstance(a, ProjectDirectory):
818 return a.uuid() == i['uuid']
819 elif isinstance(a, ObjectFile):
820 return a.uuid() == i['uuid'] and not a.stale()
824 with llfuse.lock_released:
825 self._updating_lock.acquire()
829 if group_uuid_pattern.match(self.project_uuid):
830 self.project_object = self.api.groups().get(
831 uuid=self.project_uuid).execute(num_retries=self.num_retries)
832 elif user_uuid_pattern.match(self.project_uuid):
833 self.project_object = self.api.users().get(
834 uuid=self.project_uuid).execute(num_retries=self.num_retries)
836 contents = arvados.util.list_all(self.api.groups().contents,
837 self.num_retries, uuid=self.project_uuid)
839 # end with llfuse.lock_released, re-acquire lock
844 self.createDirectory)
846 self._updating_lock.release()
850 def __getitem__(self, item):
851 if item == '.arvados#project':
852 return self.project_object_file
854 return super(ProjectDirectory, self).__getitem__(item)
856 def __contains__(self, k):
857 if k == '.arvados#project':
860 return super(ProjectDirectory, self).__contains__(k)
865 with llfuse.lock_released:
866 if not self._current_user:
867 self._current_user = self.api.users().current().execute(num_retries=self.num_retries)
868 return self._current_user["uuid"] in self.project_object["writable_by"]
875 def mkdir(self, name):
877 with llfuse.lock_released:
878 self.api.collections().create(body={"owner_uuid": self.project_uuid,
880 "manifest_text": ""}).execute(num_retries=self.num_retries)
882 except apiclient_errors.Error as error:
884 raise llfuse.FUSEError(errno.EEXIST)
888 def rmdir(self, name):
890 raise llfuse.FUSEError(errno.ENOENT)
891 if not isinstance(self[name], CollectionDirectory):
892 raise llfuse.FUSEError(errno.EPERM)
893 if len(self[name]) > 0:
894 raise llfuse.FUSEError(errno.ENOTEMPTY)
895 with llfuse.lock_released:
896 self.api.collections().delete(uuid=self[name].uuid()).execute(num_retries=self.num_retries)
901 def rename(self, name_old, name_new, src):
902 if not isinstance(src, ProjectDirectory):
903 raise llfuse.FUSEError(errno.EPERM)
907 if not isinstance(ent, CollectionDirectory):
908 raise llfuse.FUSEError(errno.EPERM)
911 # POSIX semantics for replacing one directory with another is
912 # tricky (the target directory must be empty, the operation must be
913 # atomic which isn't possible with the Arvados API as of this
914 # writing) so don't support that.
915 raise llfuse.FUSEError(errno.EPERM)
917 self.api.collections().update(uuid=ent.uuid(),
918 body={"owner_uuid": self.uuid(),
919 "name": name_new}).execute(num_retries=self.num_retries)
921 # Acually move the entry from source directory to this directory.
922 del src._entries[name_old]
923 self._entries[name_new] = ent
924 self.inodes.invalidate_entry(src.inode, name_old.encode(self.inodes.encoding))
927 class SharedDirectory(Directory):
928 """A special directory that represents users or groups who have shared projects with me."""
930 def __init__(self, parent_inode, inodes, api, num_retries, exclude,
931 poll=False, poll_time=60):
932 super(SharedDirectory, self).__init__(parent_inode, inodes)
934 self.num_retries = num_retries
935 self.current_user = api.users().current().execute(num_retries=num_retries)
937 self._poll_time = poll_time
941 with llfuse.lock_released:
942 all_projects = arvados.util.list_all(
943 self.api.groups().list, self.num_retries,
944 filters=[['group_class','=','project']])
946 for ob in all_projects:
947 objects[ob['uuid']] = ob
951 for ob in all_projects:
952 if ob['owner_uuid'] != self.current_user['uuid'] and ob['owner_uuid'] not in objects:
954 root_owners[ob['owner_uuid']] = True
956 lusers = arvados.util.list_all(
957 self.api.users().list, self.num_retries,
958 filters=[['uuid','in', list(root_owners)]])
959 lgroups = arvados.util.list_all(
960 self.api.groups().list, self.num_retries,
961 filters=[['uuid','in', list(root_owners)]])
967 objects[l["uuid"]] = l
969 objects[l["uuid"]] = l
972 for r in root_owners:
976 contents[obr["name"]] = obr
977 #elif obr.get("username"):
978 # contents[obr["username"]] = obr
979 elif "first_name" in obr:
980 contents[u"{} {}".format(obr["first_name"], obr["last_name"])] = obr
984 if r['owner_uuid'] not in objects:
985 contents[r['name']] = r
987 # end with llfuse.lock_released, re-acquire lock
990 self.merge(contents.items(),
992 lambda a, i: a.uuid() == i[1]['uuid'],
993 lambda i: ProjectDirectory(self.inode, self.inodes, self.api, self.num_retries, i[1], poll=self._poll, poll_time=self._poll_time))
997 def want_event_subscribe(self):