def _delete(self, path: FsPath, expect=None) -> None: if path.is_root(): raise PermissionError(13, "Permission denied", str(path)) parent_access, parent_manifest = self._retrieve_entry(path.parent) if not is_folderish_manifest(parent_manifest): raise NotADirectoryError(20, "Not a directory", str(path.parent)) try: item_access = parent_manifest.children[path.name] except KeyError: raise FileNotFoundError(2, "No such file or directory", str(path)) item_manifest = self.get_manifest(item_access) if is_folderish_manifest(item_manifest): if expect == "file": raise IsADirectoryError(21, "Is a directory", str(path)) if item_manifest.children: raise OSError(39, "Directory not empty", str(path)) elif expect == "folder": raise NotADirectoryError(20, "Not a directory", str(path)) parent_manifest = parent_manifest.evolve_children_and_mark_updated( {path.name: None}) self.set_manifest(parent_access, parent_manifest) self.event_bus.send("fs.entry.updated", id=parent_access.id)
async def _lock_parent_manifest_from_path( self, path: FsPath ) -> AsyncIterator[Tuple[LocalFolderishManifests, Optional[BaseLocalManifest]]]: # This is the most complicated locking scenario. # It requires locking the parent of the given entry and the entry itself # if it exists. # This is done in a two step process: # - 1. Lock the parent (it must exist). While the parent is locked, no # children can be added, renamed or removed. # - 2. Lock the children if exists. It it doesn't, there is nothing to lock # since the parent lock guarentees that it is not going to be added while # using the context. # This double locking is only required for a single use case: the overwriting # of empty directory during a move. We have to make sure that no one adds # something to the directory while it is being overwritten. # If read/write locks were to be implemented, the parent would be write locked # and the child read locked. This means that despite locking two entries, only # a single entry is modified at a time. # Source is root if path.is_root(): raise FSPermissionError(filename=str(path)) # Loop over attempts while True: # Lock parent first async with self._lock_manifest_from_path(path.parent) as parent: # Parent is not a directory if not isinstance( parent, (LocalFolderManifest, LocalWorkspaceManifest)): raise FSNotADirectoryError(filename=path.parent) # Child doesn't exist if path.name not in parent.children: yield parent, None return # Child exists entry_id = parent.children[path.name] try: async with self.local_storage.lock_manifest( entry_id) as manifest: yield parent, manifest return # Child is not available except FSLocalMissError as exc: assert exc.id == entry_id # Release the lock and download the child manifest await self._load_manifest(entry_id)
async def _resolve_placeholders_in_path(self, path: FsPath, access: Access, manifest: LocalManifest) -> bool: """ Returns: If an additional sync is needed """ # Notes we sync recursively from children to parents, this is more # efficient given otherwise we would have to do: # 1) sync the parent # 2) sync the child # 3) re-sync the parent with the child is_placeholder = is_placeholder_manifest(manifest) if not is_placeholder: # Cannot have a non-placeholder with a placeholder parent, hence # we don't have to go any further. return manifest.need_sync else: if is_file_manifest(manifest): need_more_sync = await self._minimal_sync_file( path, access, manifest) else: need_more_sync = await self._minimal_sync_folder( path, access, manifest) # Once the entry is synced, we must sync it parent as well to have # the entry visible for other clients if not path.is_root(): try: parent_access, parent_manifest = self.local_folder_fs.get_entry( path.parent) except FSManifestLocalMiss: # Nothing to do if entry is no present locally return False if is_placeholder_manifest(parent_manifest): await self._resolve_placeholders_in_path( path.parent, parent_access, parent_manifest) else: await self._sync_folder(path.parent, parent_access, parent_manifest, recursive=False) if not need_more_sync: self.event_bus.send("fs.entry.synced", path=str(path), id=access.id) return need_more_sync
def stat(self, path: FsPath) -> dict: access, manifest = self._retrieve_entry_read_only(path) if is_file_manifest(manifest): return { "type": "file", "is_folder": False, "created": manifest.created, "updated": manifest.updated, "base_version": manifest.base_version, "is_placeholder": manifest.is_placeholder, "need_sync": manifest.need_sync, "size": manifest.size, } elif is_workspace_manifest(manifest): return { "type": "workspace", "is_folder": True, "created": manifest.created, "updated": manifest.updated, "base_version": manifest.base_version, "is_placeholder": manifest.is_placeholder, "need_sync": manifest.need_sync, "children": list(sorted(manifest.children.keys())), "creator": manifest.creator, "participants": list(manifest.participants), } else: return { "type": "root" if path.is_root() else "folder", "is_folder": True, "created": manifest.created, "updated": manifest.updated, "base_version": manifest.base_version, "is_placeholder": manifest.is_placeholder, "need_sync": manifest.need_sync, "children": list(sorted(manifest.children.keys())), }
def touch(self, path: FsPath) -> None: if path.is_root(): raise FileExistsError(17, "File exists", str(path)) if path.parent.is_root(): raise PermissionError( 13, "Permission denied (only workpace allowed at root level)", str(path)) access, manifest = self._retrieve_entry(path.parent) if not is_folderish_manifest(manifest): raise NotADirectoryError(20, "Not a directory", str(path.parent)) if path.name in manifest.children: raise FileExistsError(17, "File exists", str(path)) child_access = ManifestAccess() child_manifest = LocalFileManifest(self.local_author) manifest = manifest.evolve_children_and_mark_updated( {path.name: child_access}) self.set_manifest(access, manifest) self.set_manifest(child_access, child_manifest) self.event_bus.send("fs.entry.updated", id=access.id) self.event_bus.send("fs.entry.updated", id=child_access.id)
async def entry_rename(self, source: FsPath, destination: FsPath, overwrite: bool = True) -> Optional[EntryID]: # Check write rights self.check_write_rights(source) # Source is root if source.is_root(): raise FSPermissionError(filename=source) # Destination is root if destination.is_root(): raise FSPermissionError(filename=destination) # Cross-directory renaming is not supported if source.parent != destination.parent: raise FSCrossDeviceError(filename=source, filename2=destination) # Pre-fetch the source if necessary if overwrite: await self._get_manifest_from_path(source) # Fetch and lock async with self._lock_parent_manifest_from_path(destination) as ( parent, child): # Source does not exist if source.name not in parent.children: raise FSFileNotFoundError(filename=source) source_entry_id = parent.children[source.name] # Source and destination are the same if source.name == destination.name: return None # Destination already exists if not overwrite and child is not None: raise FSFileExistsError(filename=destination) # Overwrite logic if overwrite and child is not None: source_manifest = await self._get_manifest(source_entry_id) # Overwrite a file if isinstance(source_manifest, LocalFileManifest): # Destination is a folder if isinstance(child, LocalFolderManifest): raise FSIsADirectoryError(filename=destination) # Overwrite a folder if isinstance(source_manifest, LocalFolderManifest): # Destination is not a folder if not isinstance(child, LocalFolderManifest): raise FSNotADirectoryError(filename=destination) # Destination is not empty if child.children: raise FSDirectoryNotEmptyError(filename=destination) # Create new manifest new_parent = parent.evolve_children_and_mark_updated( { destination.name: source_entry_id, source.name: None }, prevent_sync_pattern=self.local_storage. get_prevent_sync_pattern(), ) # Atomic change await self.local_storage.set_manifest(parent.id, new_parent) # Send event self._send_event(CoreEvent.FS_ENTRY_UPDATED, id=parent.id) # Return the entry id of the renamed entry return parent.children[source.name]
def test_root(path, is_root): obj = FsPath(path) assert obj.is_root() is is_root assert "//" not in str(obj)
def _copy(self, src: FsPath, dst: FsPath, delete_src: bool) -> None: # The idea here is to consider a manifest never move around the fs # (i.e. a given access always points to the same path). This simplify # sync notifications handling and avoid ending up with two path # (possibly from different workspaces !) pointing to the same manifest # which would be weird for user experience. # Long story short, when moving/copying manifest, we must recursively # copy the manifests and create new accesses for them. parent_src = src.parent parent_dst = dst.parent # No matter what, cannot move or overwrite root if src.is_root(): # Raise FileNotFoundError if parent_dst doesn't exists _, parent_dst_manifest = self._retrieve_entry(parent_dst) if not is_folderish_manifest(parent_dst_manifest): raise NotADirectoryError(20, "Not a directory", str(parent_dst)) else: raise PermissionError(13, "Permission denied", str(src), str(dst)) elif dst.is_root(): # Raise FileNotFoundError if parent_src doesn't exists _, parent_src_manifest = self._retrieve_entry(src.parent) if not is_folderish_manifest(parent_src_manifest): raise NotADirectoryError(20, "Not a directory", str(src.parent)) else: raise PermissionError(13, "Permission denied", str(src), str(dst)) if src == dst: # Raise FileNotFoundError if doesn't exist src_access, src_ro_manifest = self._retrieve_entry_read_only(src) if is_workspace_manifest(src_ro_manifest): raise PermissionError( 13, "Permission denied (cannot move/copy workpace, must rename it)", str(src), str(dst), ) return if parent_src == parent_dst: parent_access, parent_manifest = self._retrieve_entry(parent_src) if not is_folderish_manifest(parent_manifest): raise NotADirectoryError(20, "Not a directory", str(parent_src)) try: dst.relative_to(src) except ValueError: pass else: raise OSError(22, "Invalid argument", str(src), None, str(dst)) try: src_access = parent_manifest.children[src.name] except KeyError: raise FileNotFoundError(2, "No such file or directory", str(src)) existing_dst_access = parent_manifest.children.get(dst.name) src_manifest = self.get_manifest(src_access) if is_workspace_manifest(src_manifest): raise PermissionError( 13, "Permission denied (cannot move/copy workpace, must rename it)", str(src), str(dst), ) if existing_dst_access: existing_dst_manifest = self.get_manifest(existing_dst_access) if is_folderish_manifest(src_manifest): if is_file_manifest(existing_dst_manifest): raise NotADirectoryError(20, "Not a directory", str(dst)) elif existing_dst_manifest.children: raise OSError(39, "Directory not empty", str(dst)) else: if is_folderish_manifest(existing_dst_manifest): raise IsADirectoryError(21, "Is a directory", str(dst)) moved_access = self._recursive_manifest_copy( src_access, src_manifest) if not delete_src: parent_manifest = parent_manifest.evolve_children( {dst.name: moved_access}) else: parent_manifest = parent_manifest.evolve_children({ dst.name: moved_access, src.name: None }) self.set_manifest(parent_access, parent_manifest) self.event_bus.send("fs.entry.updated", id=parent_access.id) else: parent_src_access, parent_src_manifest = self._retrieve_entry( parent_src) if not is_folderish_manifest(parent_src_manifest): raise NotADirectoryError(20, "Not a directory", str(parent_src)) parent_dst_access, parent_dst_manifest = self._retrieve_entry( parent_dst) if not is_folderish_manifest(parent_dst_manifest): raise NotADirectoryError(20, "Not a directory", str(parent_dst)) try: dst.relative_to(src) except ValueError: pass else: raise OSError(22, "Invalid argument", str(src), None, str(dst)) try: src_access = parent_src_manifest.children[src.name] except KeyError: raise FileNotFoundError(2, "No such file or directory", str(src)) existing_dst_access = parent_dst_manifest.children.get(dst.name) src_manifest = self.get_manifest(src_access) if is_workspace_manifest(src_manifest): raise PermissionError( 13, "Permission denied (cannot move/copy workpace, must rename it)", str(src), str(dst), ) if existing_dst_access: existing_entry_manifest = self.get_manifest( existing_dst_access) if is_folderish_manifest(src_manifest): if is_file_manifest(existing_entry_manifest): raise NotADirectoryError(20, "Not a directory", str(dst)) elif existing_entry_manifest.children: raise OSError(39, "Directory not empty", str(dst)) else: if is_folderish_manifest(existing_entry_manifest): raise IsADirectoryError(21, "Is a directory", str(dst)) moved_access = self._recursive_manifest_copy( src_access, src_manifest) parent_dst_manifest = parent_dst_manifest.evolve_children_and_mark_updated( {dst.name: moved_access}) self.set_manifest(parent_dst_access, parent_dst_manifest) self.event_bus.send("fs.entry.updated", id=parent_dst_access.id) if delete_src: parent_src_manifest = parent_src_manifest.evolve_children_and_mark_updated( {src.name: None}) self.set_manifest(parent_src_access, parent_src_manifest) self.event_bus.send("fs.entry.updated", id=parent_src_access.id)