diff --git a/swh/fuse/fs/artifact.py b/swh/fuse/fs/artifact.py --- a/swh/fuse/fs/artifact.py +++ b/swh/fuse/fs/artifact.py @@ -7,8 +7,13 @@ from pathlib import Path from typing import Any, AsyncIterator, List -from swh.fuse.fs.entry import EntryMode, FuseEntry -from swh.fuse.fs.symlink import SymlinkEntry +from swh.fuse.fs.entry import ( + EntryMode, + FuseDirEntry, + FuseEntry, + FuseFileEntry, + FuseSymlinkEntry, +) from swh.model.from_disk import DentryPerms from swh.model.identifiers import CONTENT, DIRECTORY, RELEASE, REVISION, SWHID @@ -26,7 +31,7 @@ prefetch: Any = None -class Content(ArtifactEntry): +class Content(ArtifactEntry, FuseFileEntry): """ Software Heritage content artifact. Content leaves (AKA blobs) are represented on disks as regular files, @@ -49,11 +54,8 @@ else: return len(await self.get_content()) - async def __aiter__(self): - raise ValueError("Cannot iterate over a content type artifact") - -class Directory(ArtifactEntry): +class Directory(ArtifactEntry, FuseDirEntry): """ Software Heritage directory artifact. Directory nodes are represented as directories on the file-system, @@ -81,7 +83,7 @@ # 1. Symlinks if mode == DentryPerms.symlink: yield self.create_child( - SymlinkEntry, + FuseSymlinkEntry, name=name, # Symlink target is stored in the blob content target=await self.fuse.get_blob(swhid), @@ -92,7 +94,7 @@ # symlink to distinguish it with regular directories await self.fuse.get_metadata(swhid) yield self.create_child( - SymlinkEntry, + FuseSymlinkEntry, name=name, target=Path(self.get_relative_root_path(), f"archive/{swhid}"), ) @@ -109,7 +111,7 @@ ) -class Revision(ArtifactEntry): +class Revision(ArtifactEntry, FuseDirEntry): """ Software Heritage revision artifact. Revision (AKA commit) nodes are represented on the file-system as @@ -140,10 +142,12 @@ root_path = self.get_relative_root_path() yield self.create_child( - SymlinkEntry, name="root", target=Path(root_path, f"archive/{directory}"), + FuseSymlinkEntry, + name="root", + target=Path(root_path, f"archive/{directory}"), ) yield self.create_child( - SymlinkEntry, + FuseSymlinkEntry, name="meta.json", target=Path(root_path, f"meta/{self.swhid}.json"), ) @@ -156,12 +160,12 @@ if len(parents) >= 1: yield self.create_child( - SymlinkEntry, name="parent", target="parents/1/", + FuseSymlinkEntry, name="parent", target="parents/1/", ) @dataclass -class RevisionParents(FuseEntry): +class RevisionParents(FuseDirEntry): """ Revision virtual `parents/` directory """ parents: List[SWHID] @@ -170,13 +174,13 @@ root_path = self.get_relative_root_path() for i, parent in enumerate(self.parents): yield self.create_child( - SymlinkEntry, + FuseSymlinkEntry, name=str(i + 1), target=Path(root_path, f"archive/{parent}"), ) -class Release(ArtifactEntry): +class Release(ArtifactEntry, FuseDirEntry): """ Software Heritage release artifact. Release nodes are represented on the file-system as directories with the @@ -207,14 +211,14 @@ root_path = self.get_relative_root_path() yield self.create_child( - SymlinkEntry, + FuseSymlinkEntry, name="meta.json", target=Path(root_path, f"meta/{self.swhid}.json"), ) target = metadata["target"] yield self.create_child( - SymlinkEntry, name="target", target=Path(root_path, f"archive/{target}") + FuseSymlinkEntry, name="target", target=Path(root_path, f"archive/{target}") ) yield self.create_child( ReleaseType, @@ -226,14 +230,14 @@ target_dir = await self.find_root_directory(target) if target_dir is not None: yield self.create_child( - SymlinkEntry, + FuseSymlinkEntry, name="root", target=Path(root_path, f"archive/{target_dir}"), ) @dataclass -class ReleaseType(FuseEntry): +class ReleaseType(FuseFileEntry): """ Release type virtual file """ target_type: str diff --git a/swh/fuse/fs/entry.py b/swh/fuse/fs/entry.py --- a/swh/fuse/fs/entry.py +++ b/swh/fuse/fs/entry.py @@ -51,33 +51,86 @@ async def get_content(self) -> bytes: """ Return the content of a file entry """ - return None + if isinstance(self, FuseFileEntry): + return await self.get_content() + else: + raise ValueError("Cannot get raw content from a non-file entry") async def size(self) -> int: """ Return the size of a file entry """ - return 0 + if isinstance(self, FuseFileEntry) or isinstance(self, FuseSymlinkEntry): + return await self.size() + else: + return 0 async def __aiter__(self) -> AsyncIterator[FuseEntry]: """ Return the child entries of a directory entry """ - yield None + if isinstance(self, FuseDirEntry): + yield await self.__aiter__() + else: + raise ValueError("Cannot iterate over a non-directory entry") async def lookup(self, name: str) -> FuseEntry: """ Look up a FUSE entry by name """ - async for entry in self: - if entry.name == name: - return entry - return None + if isinstance(self, FuseDirEntry): + return await self.lookup() + else: + raise ValueError("Cannot lookup a non-directory entry") def get_target(self) -> Union[str, bytes, Path]: """ Return the path target of a symlink entry """ - return None + if isinstance(self, FuseSymlinkEntry): + return self.get_target() + else: + raise ValueError("Cannot get target from a non-symlink entry") def get_relative_root_path(self) -> str: return "../" * (self.depth - 1) def create_child(self, constructor: Any, **kwargs) -> FuseEntry: return constructor(depth=self.depth + 1, fuse=self.fuse, **kwargs) + + +class FuseFileEntry(FuseEntry): + """ FUSE virtual file entry """ + + async def get_content(self) -> bytes: + raise NotImplementedError + + async def size(self) -> int: + raise NotImplementedError + + +class FuseDirEntry(FuseEntry): + """ FUSE virtual directory entry """ + + async def __aiter__(self) -> AsyncIterator[FuseEntry]: + raise NotImplementedError + + async def lookup(self, name: str) -> FuseEntry: + async for entry in self: + if entry.name == name: + return entry + return None + + +@dataclass +class FuseSymlinkEntry(FuseEntry): + """ FUSE virtual symlink entry + + Attributes: + target: path to symlink target + """ + + mode: int = field(init=False, default=int(EntryMode.SYMLINK)) + target: Union[str, bytes, Path] + + async def size(self) -> int: + return len(str(self.target)) + + def get_target(self) -> Union[str, bytes, Path]: + return self.target diff --git a/swh/fuse/fs/mountpoint.py b/swh/fuse/fs/mountpoint.py --- a/swh/fuse/fs/mountpoint.py +++ b/swh/fuse/fs/mountpoint.py @@ -8,13 +8,13 @@ from typing import AsyncIterator from swh.fuse.fs.artifact import OBJTYPE_GETTERS -from swh.fuse.fs.entry import EntryMode, FuseEntry +from swh.fuse.fs.entry import EntryMode, FuseDirEntry, FuseEntry, FuseFileEntry from swh.model.exceptions import ValidationError from swh.model.identifiers import CONTENT, SWHID, parse_swhid @dataclass -class Root(FuseEntry): +class Root(FuseDirEntry): """ The FUSE mountpoint, consisting of the archive/ and meta/ directories """ name: str = field(init=False, default=None) @@ -27,7 +27,7 @@ @dataclass -class ArchiveDir(FuseEntry): +class ArchiveDir(FuseDirEntry): """ The archive/ directory is lazily populated with one entry per accessed SWHID, having actual SWHIDs as names """ @@ -65,7 +65,7 @@ @dataclass -class MetaDir(FuseEntry): +class MetaDir(FuseDirEntry): """ The meta/ directory contains one SWHID.json file for each SWHID entry under archive/. The JSON file contain all available meta information about the given SWHID, as returned by the Software Heritage Web API for that @@ -87,7 +87,7 @@ @dataclass -class MetaEntry(FuseEntry): +class MetaEntry(FuseFileEntry): """ An entry from the meta/ directory, containing for each accessed SWHID a corresponding SWHID.json file with all the metadata from the Software Heritage archive. """ diff --git a/swh/fuse/fs/symlink.py b/swh/fuse/fs/symlink.py deleted file mode 100644 --- a/swh/fuse/fs/symlink.py +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright (C) 2020 The Software Heritage developers -# See the AUTHORS file at the top-level directory of this distribution -# License: GNU General Public License version 3, or any later version -# See top-level LICENSE file for more information - -from dataclasses import dataclass, field -from pathlib import Path -from typing import Union - -from swh.fuse.fs.entry import EntryMode, FuseEntry - - -@dataclass -class SymlinkEntry(FuseEntry): - """ FUSE virtual entry for symlinks - - Attributes: - target: path to symlink target - """ - - mode: int = field(init=False, default=int(EntryMode.SYMLINK)) - target: Union[str, bytes, Path] - - async def size(self) -> int: - return len(str(self.target)) - - def get_target(self) -> Union[str, bytes, Path]: - return self.target