Page Menu
Home
Software Heritage
Search
Configure Global Search
Log In
Files
F7124583
D4064.id14526.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
50 KB
Subscribers
None
D4064.id14526.diff
View Options
diff --git a/mypy.ini b/mypy.ini
--- a/mypy.ini
+++ b/mypy.ini
@@ -5,11 +5,17 @@
# 3rd party libraries without stubs (yet)
+[mypy-daemon.*]
+ignore_missing_imports = True
+
[mypy-pkg_resources.*]
ignore_missing_imports = True
[mypy-pytest.*]
ignore_missing_imports = True
-# [mypy-add_your_lib_here.*]
-# ignore_missing_imports = True
+[mypy-pyfuse3.*]
+ignore_missing_imports = True
+
+[mypy-pyfuse3_asyncio.*]
+ignore_missing_imports = True
diff --git a/requirements-test.txt b/requirements-test.txt
--- a/requirements-test.txt
+++ b/requirements-test.txt
@@ -1,2 +1,3 @@
pytest
requests # workaround for https://forge.softwareheritage.org/T2634
+requests-mock
diff --git a/requirements.txt b/requirements.txt
--- a/requirements.txt
+++ b/requirements.txt
@@ -1 +1,2 @@
pyfuse3
+python-daemon
diff --git a/swh/fuse/cache.py b/swh/fuse/cache.py
new file mode 100644
--- /dev/null
+++ b/swh/fuse/cache.py
@@ -0,0 +1,150 @@
+# 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 abc import ABC
+from contextlib import closing
+import json
+import sqlite3
+from typing import Any, Dict, Optional, Set
+
+from swh.model.identifiers import SWHID, parse_swhid
+from swh.web.client.client import typify
+
+
+class FuseCache:
+ """ SWH FUSE retrieves both metadata and file contents from the Software
+ Heritage archive via the network. In order to obtain reasonable performances
+ several caches are used to minimize network transfer.
+
+ Caches are stored on disk in SQLite databases located at
+ `$XDG_CACHE_HOME/swh/fuse/`.
+
+ All caches are persistent (i.e., they survive the restart of the SWH FUSE
+ process) and global (i.e., they are shared by concurrent SWH FUSE
+ processes).
+
+ We assume that no cache *invalidation* is necessary, due to intrinsic
+ properties of the Software Heritage archive, such as integrity verification
+ and append-only archive changes. To clean the caches one can just remove the
+ corresponding files from disk. """
+
+ def __init__(self, cache_conf: Dict[str, Any]):
+ self.cache_conf = cache_conf
+
+ def __enter__(self):
+ self.metadata = MetadataCache(self.cache_conf["metadata"])
+ self.metadata.__enter__()
+ self.blob = BlobCache(self.cache_conf["blob"])
+ self.blob.__enter__()
+ return self
+
+ def __exit__(self, type=None, val=None, tb=None) -> None:
+ self.metadata.__exit__()
+ self.blob.__exit__()
+
+ def get_cached_swhids(self) -> Set[SWHID]:
+ """ Return a list of all previously cached SWHID """
+
+ with closing(self.metadata.conn.cursor()) as metadata_cursor, (
+ closing(self.blob.conn.cursor())
+ ) as blob_cursor:
+ # Some entries can be in one cache but not in the other so create a
+ # set from all caches
+ metadata_cursor.execute("select swhid from metadata_cache")
+ blob_cursor.execute("select swhid from blob_cache")
+ swhids = metadata_cursor.fetchall() + blob_cursor.fetchall()
+ swhids = [parse_swhid(x[0]) for x in swhids]
+ return set(swhids)
+
+
+class AbstractCache(ABC):
+ """ Abstract cache implementation to share common behavior between cache
+ types (such as: YAML config parsing, SQLite context manager) """
+
+ def __init__(self, conf: Dict[str, Any]):
+ self.conf = conf
+
+ def __enter__(self):
+ # In-memory (thus temporary) caching is useful for testing purposes
+ if self.conf.get("in-memory", False):
+ path = ":memory:"
+ else:
+ path = self.conf["path"]
+ self.conn = sqlite3.connect(path)
+ return self
+
+ def __exit__(self, type=None, val=None, tb=None) -> None:
+ self.conn.close()
+
+
+class MetadataCache(AbstractCache):
+ """ The metadata cache map each SWHID to the complete metadata of the
+ referenced object. This is analogous to what is available in
+ `meta/<SWHID>.json` file (and generally used as data source for returning
+ the content of those files). """
+
+ def __enter__(self):
+ super().__enter__()
+ with self.conn as conn:
+ conn.execute("create table if not exists metadata_cache (swhid, metadata)")
+ return self
+
+ def __getitem__(self, swhid: SWHID) -> Any:
+ with self.conn as conn, closing(conn.cursor()) as cursor:
+ cursor.execute(
+ "select metadata from metadata_cache where swhid=?", (str(swhid),)
+ )
+ cache = cursor.fetchone()
+ if cache:
+ metadata = json.loads(cache[0])
+ return typify(metadata, swhid.object_type)
+ else:
+ return None
+
+ def __setitem__(self, swhid: SWHID, metadata: Any) -> None:
+ with self.conn as conn:
+ conn.execute(
+ "insert into metadata_cache values (?, ?)",
+ (
+ str(swhid),
+ json.dumps(
+ metadata,
+ # Converts the typified JSON to plain str version
+ default=lambda x: (
+ x.object_id if isinstance(x, SWHID) else x.__dict__
+ ),
+ ),
+ ),
+ )
+
+
+class BlobCache(AbstractCache):
+ """ The blob cache map SWHIDs of type `cnt` to the bytes of their archived
+ content.
+
+ The blob cache entry for a given content object is populated, at the latest,
+ the first time the object is `read()`-d. It might be populated earlier on
+ due to prefetching, e.g., when a directory pointing to the given content is
+ listed for the first time. """
+
+ def __enter__(self):
+ super().__enter__()
+ with self.conn as conn:
+ conn.execute("create table if not exists blob_cache (swhid, blob)")
+ return self
+
+ def __getitem__(self, swhid: SWHID) -> Optional[str]:
+ with self.conn as conn, closing(conn.cursor()) as cursor:
+ cursor.execute("select blob from blob_cache where swhid=?", (str(swhid),))
+ cache = cursor.fetchone()
+ if cache:
+ blob = cache[0]
+ return blob
+ else:
+ return None
+
+ def __setitem__(self, swhid: SWHID, blob: str) -> None:
+ with self.conn as conn:
+ conn.execute("insert into blob_cache values (?, ?)", (str(swhid), blob))
diff --git a/swh/fuse/cli.py b/swh/fuse/cli.py
--- a/swh/fuse/cli.py
+++ b/swh/fuse/cli.py
@@ -3,29 +3,65 @@
# License: GNU General Public License version 3, or any later version
# See top-level LICENSE file for more information
+from contextlib import ExitStack
+import os
+
# WARNING: do not import unnecessary things here to keep cli startup time under
# control
-import os
-from typing import Any, Dict
+from pathlib import Path
+from typing import Any, Dict, Tuple
import click
+from daemon import DaemonContext
# from swh.core import config
from swh.core.cli import CONTEXT_SETTINGS
+from swh.model.identifiers import SWHID
# All generic config code should reside in swh.core.config
DEFAULT_CONFIG_PATH = os.environ.get(
"SWH_CONFIG_FILE", os.path.join(click.get_app_dir("swh"), "global.yml")
)
-DEFAULT_CONFIG: Dict[str, Any] = {
- "web-api": {
- "url": "https://archive.softwareheritage.org/api/1",
- "auth-token": None,
- }
+CACHE_HOME_DIR: Path = (
+ Path(os.environ["XDG_CACHE_HOME"])
+ if "XDG_CACHE_HOME" in os.environ
+ else Path.home() / ".cache"
+)
+
+DEFAULT_CONFIG: Dict[str, Tuple[str, Any]] = {
+ "cache": (
+ "dict",
+ {
+ "metadata": {"path": CACHE_HOME_DIR / "swh/fuse/metadata.sqlite"},
+ "blob": {"path": CACHE_HOME_DIR / "swh/fuse/blob.sqlite"},
+ },
+ ),
+ "web-api": (
+ "dict",
+ {"url": "https://archive.softwareheritage.org/api/1", "auth-token": None,},
+ ),
}
+class SWHIDParamType(click.ParamType):
+ """Click argument that accepts SWHID and return them as
+ :class:`swh.model.identifiers.SWHID` instances
+
+ """
+
+ name = "SWHID"
+
+ def convert(self, value, param, ctx) -> SWHID:
+ from swh.model.exceptions import ValidationError
+ from swh.model.identifiers import parse_swhid
+
+ try:
+ return parse_swhid(value)
+ except ValidationError:
+ self.fail(f'"{value}" is not a valid SWHID', param, ctx)
+
+
@click.group(name="fuse", context_settings=CONTEXT_SETTINGS)
# XXX conffile logic temporarily commented out due to:
# XXX https://forge.softwareheritage.org/T2632
@@ -50,17 +86,36 @@
@cli.command()
+@click.argument(
+ "path",
+ required=True,
+ metavar="PATH",
+ type=click.Path(exists=True, dir_okay=True, file_okay=False),
+)
+@click.argument("swhids", nargs=-1, metavar="[SWHID]...", type=SWHIDParamType())
@click.option(
- "-u",
- "--api-url",
- default=DEFAULT_CONFIG["web-api"]["url"],
- metavar="API_URL",
+ "--config-file",
+ "-C",
+ default=None,
+ type=click.Path(exists=True, dir_okay=False,),
+ help="YAML configuration file",
+)
+@click.option(
+ "-f",
+ "--foreground",
+ is_flag=True,
show_default=True,
- help="base URL for Software Heritage Web API",
+ help="Run FUSE system in foreground instead of daemon",
)
@click.pass_context
-def mount(ctx, api_url):
- """Mount the Software Heritage archive at the given mount point"""
- from .fuse import fuse # XXX
+def mount(ctx, swhids, path, config_file, foreground):
+ """ Mount the Software Heritage archive at the given mount point """
+
+ from swh.core import config
+ from swh.fuse import fuse
- fuse()
+ conf = config.read(config_file, DEFAULT_CONFIG)
+ with ExitStack() as stack:
+ if not foreground:
+ stack.enter(DaemonContext())
+ fuse.main(swhids, path, conf)
diff --git a/swh/fuse/fs/artifact.py b/swh/fuse/fs/artifact.py
new file mode 100644
--- /dev/null
+++ b/swh/fuse/fs/artifact.py
@@ -0,0 +1,49 @@
+# 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 typing import Any, Dict, Iterator, List
+
+from swh.fuse.fs.entry import ArtifactEntry, VirtualEntry
+
+
+class Content:
+ """ Software Heritage content artifact.
+
+ Content leaves (AKA blobs) are represented on disks as regular files,
+ containing the corresponding bytes, as archived.
+
+ Note that permissions are associated to blobs only in the context of
+ directories. Hence, when accessing blobs from the top-level `archive/`
+ directory, the permissions of the `archive/SWHID` file will be arbitrary and
+ not meaningful (e.g., `0x644`). """
+
+ def __init__(self, json: Dict[str, Any]):
+ self.json = json
+
+
+class Directory:
+ """ Software Heritage directory artifact.
+
+ Directory nodes are represented as directories on the file-system,
+ containing one entry for each entry of the archived directory. Entry names
+ and other metadata, including permissions, will correspond to the archived
+ entry metadata.
+
+ Note that the FUSE mount is read-only, no matter what the permissions say.
+ So it is possible that, in the context of a directory, a file is presented
+ as writable, whereas actually writing to it will fail with `EPERM`. """
+
+ def __init__(self, json: List[Dict[str, Any]]):
+ self.json = json
+
+ def __iter__(self) -> Iterator[VirtualEntry]:
+ entries = []
+ for entry in self.json:
+ name, swhid = entry["name"], entry["target"]
+ # The directory API has extra info we can use to set attributes
+ # without additional Software Heritage API call
+ prefetch = entry
+ entries.append(ArtifactEntry(name, swhid, prefetch))
+ return iter(entries)
diff --git a/swh/fuse/fs/entry.py b/swh/fuse/fs/entry.py
new file mode 100644
--- /dev/null
+++ b/swh/fuse/fs/entry.py
@@ -0,0 +1,55 @@
+# 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 enum import IntEnum
+from stat import S_IFDIR, S_IFREG
+from typing import Any, Dict
+
+from swh.model.identifiers import SWHID
+
+
+class EntryMode(IntEnum):
+ """ Default entry mode and permissions for the FUSE.
+
+ The FUSE mount is always read-only, even if permissions contradict this
+ statement (in a context of a directory, entries are listed with permissions
+ taken from the archive).
+ """
+
+ RDONLY_FILE = S_IFREG | 0o444
+ RDONLY_DIR = S_IFDIR | 0o555
+
+
+class VirtualEntry:
+ """ Main wrapper class to manipulate virtual FUSE entries
+
+ Attributes:
+ name: entry filename
+ mode: entry permission mode
+ """
+
+ def __init__(self, name: str, mode: EntryMode):
+ self.name = name
+ self.mode = mode
+
+
+class ArtifactEntry(VirtualEntry):
+ """ FUSE virtual entry for a Software Heritage Artifact
+
+ Attributes:
+ name: entry filename
+ swhid: Software Heritage persistent identifier
+ prefetch: optional prefetched metadata used to set entry attributes
+ """
+
+ def __init__(self, name: str, swhid: SWHID, prefetch: Dict[str, Any] = None):
+ self.name = name
+ self.swhid = swhid
+ self.prefetch = prefetch
+
+
+ROOT_DIRENTRY = VirtualEntry("root", EntryMode.RDONLY_DIR)
+ARCHIVE_DIRENTRY = VirtualEntry("archive", EntryMode.RDONLY_DIR)
+META_DIRENTRY = VirtualEntry("meta", EntryMode.RDONLY_DIR)
diff --git a/swh/fuse/fs/mountpoint.py b/swh/fuse/fs/mountpoint.py
new file mode 100644
--- /dev/null
+++ b/swh/fuse/fs/mountpoint.py
@@ -0,0 +1,56 @@
+# 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 typing import Iterator
+
+from swh.fuse.cache import FuseCache
+from swh.fuse.fs.entry import (
+ ARCHIVE_DIRENTRY,
+ META_DIRENTRY,
+ ArtifactEntry,
+ EntryMode,
+ VirtualEntry,
+)
+
+
+class Root:
+ """ The FUSE mountpoint, consisting of the archive/ and meta/ directories """
+
+ def __iter__(self) -> Iterator[VirtualEntry]:
+ entries = [ARCHIVE_DIRENTRY, META_DIRENTRY]
+ return iter(entries)
+
+
+class Archive:
+ """ The archive/ directory is lazily populated with one entry per accessed
+ SWHID, having actual SWHIDs as names """
+
+ def __init__(self, cache: FuseCache):
+ self.cache = cache
+
+ def __iter__(self) -> Iterator[VirtualEntry]:
+ entries = []
+ for swhid in self.cache.get_cached_swhids():
+ entries.append(ArtifactEntry(str(swhid), swhid))
+ return iter(entries)
+
+
+class Meta:
+ """ 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
+ object. Note that, in case of pagination (e.g., snapshot objects with many
+ branches) the JSON file will contain a complete version with all pages
+ merged together. """
+
+ def __init__(self, cache: FuseCache):
+ self.cache = cache
+
+ def __iter__(self) -> Iterator[VirtualEntry]:
+ entries = []
+ for swhid in self.cache.get_cached_swhids():
+ filename = str(swhid) + ".json"
+ entries.append(VirtualEntry(filename, EntryMode.RDONLY_FILE))
+ return iter(entries)
diff --git a/swh/fuse/fuse.py b/swh/fuse/fuse.py
--- a/swh/fuse/fuse.py
+++ b/swh/fuse/fuse.py
@@ -3,11 +3,331 @@
# License: GNU General Public License version 3, or any later version
# See top-level LICENSE file for more information
-
+import asyncio
+import errno
+import itertools
import logging
-import sys
+import os
+from pathlib import Path
+import time
+from typing import Any, Dict, List
+
+import pyfuse3
+import pyfuse3_asyncio
+import requests
+
+from swh.fuse.cache import FuseCache
+from swh.fuse.fs.artifact import Content, Directory
+from swh.fuse.fs.entry import (
+ ARCHIVE_DIRENTRY,
+ META_DIRENTRY,
+ ROOT_DIRENTRY,
+ ArtifactEntry,
+ EntryMode,
+ VirtualEntry,
+)
+from swh.fuse.fs.mountpoint import Archive, Meta, Root
+from swh.model.identifiers import CONTENT, DIRECTORY, SWHID, parse_swhid
+from swh.web.client.client import WebAPIClient
+
+
+class Fuse(pyfuse3.Operations):
+ """ Software Heritage Filesystem in Userspace (FUSE). Locally mount parts of
+ the archive and naviguate it as a virtual file system. """
+
+ def __init__(
+ self,
+ swhids: List[SWHID],
+ root_path: Path,
+ cache: FuseCache,
+ conf: Dict[str, Any],
+ ):
+ super(Fuse, self).__init__()
+
+ self._next_inode: int = pyfuse3.ROOT_INODE
+ self._next_fd: int = 0
+
+ root_inode = self._next_inode
+ self._next_inode += 1
+
+ self._inode2entry: Dict[int, VirtualEntry] = {root_inode: ROOT_DIRENTRY}
+ self._entry2inode: Dict[VirtualEntry, int] = {ROOT_DIRENTRY: root_inode}
+ self._entry2fd: Dict[VirtualEntry, int] = {}
+ self._fd2entry: Dict[int, VirtualEntry] = {}
+ self._inode2path: Dict[int, Path] = {root_inode: root_path}
+
+ self.time_ns: int = time.time_ns() # start time, used as timestamp
+ self.gid = os.getgid()
+ self.uid = os.getuid()
+
+ self.web_api = WebAPIClient(
+ conf["web-api"]["url"], conf["web-api"]["auth-token"]
+ )
+ self.cache = cache
+
+ # Initially populate the cache
+ for swhid in swhids:
+ self.get_metadata(swhid)
+
+ def shutdown(self) -> None:
+ pass
+
+ def _alloc_inode(self, entry: VirtualEntry) -> int:
+ """ Return a unique inode integer for a given entry """
+
+ try:
+ return self._entry2inode[entry]
+ except KeyError:
+ inode = self._next_inode
+ self._next_inode += 1
+ self._entry2inode[entry] = inode
+ self._inode2entry[inode] = entry
+
+ # TODO add inode recycling with invocation to invalidate_inode when
+ # the dicts get too big
+
+ return inode
+
+ def _alloc_fd(self, entry: VirtualEntry) -> int:
+ """ Return a unique file descriptor integer for a given entry """
+
+ try:
+ return self._entry2fd[entry]
+ except KeyError:
+ fd = self._next_fd
+ self._next_fd += 1
+ self._entry2fd[entry] = fd
+ self._fd2entry[fd] = entry
+ return fd
+
+ def inode2entry(self, inode: int) -> VirtualEntry:
+ """ Return the entry matching a given inode """
+
+ try:
+ return self._inode2entry[inode]
+ except KeyError:
+ raise pyfuse3.FUSEError(errno.ENOENT)
+
+ def entry2inode(self, entry: VirtualEntry) -> int:
+ """ Return the inode matching a given entry """
+
+ try:
+ return self._entry2inode[entry]
+ except KeyError:
+ raise pyfuse3.FUSEError(errno.ENOENT)
+
+ def inode2path(self, inode: int) -> Path:
+ """ Return the path matching a given inode """
+
+ try:
+ return self._inode2path[inode]
+ except KeyError:
+ raise pyfuse3.FUSEError(errno.ENOENT)
+
+ def get_metadata(self, swhid: SWHID) -> Any:
+ """ Retrieve metadata for a given SWHID using Software Heritage API """
+
+ # TODO: swh-graph API
+ cache = self.cache.metadata[swhid]
+ if cache:
+ return cache
+
+ try:
+ metadata = self.web_api.get(swhid)
+ self.cache.metadata[swhid] = metadata
+ return metadata
+ except requests.HTTPError:
+ logging.error(f"Unknown SWHID: '{swhid}'")
+
+ def get_blob(self, swhid: SWHID) -> str:
+ """ Retrieve the blob bytes for a given content SWHID using Software
+ Heritage API """
+
+ if swhid.object_type != CONTENT:
+ raise pyfuse3.FUSEError(errno.EINVAL)
+
+ cache = self.cache.blob[swhid]
+ if cache:
+ return cache
+
+ resp = list(self.web_api.content_raw(swhid))
+ blob = "".join(map(bytes.decode, resp))
+ self.cache.blob[swhid] = blob
+ return blob
+
+ def get_direntries(self, entry: VirtualEntry) -> Any:
+ """ Return directory entries of a given entry """
+
+ if isinstance(entry, ArtifactEntry):
+ if entry.swhid.object_type == CONTENT:
+ raise pyfuse3.FUSEError(errno.ENOTDIR)
+
+ metadata = self.get_metadata(entry.swhid)
+ if entry.swhid.object_type == CONTENT:
+ return Content(metadata)
+ if entry.swhid.object_type == DIRECTORY:
+ return Directory(metadata)
+ # TODO: add other objects
+ else:
+ if entry == ROOT_DIRENTRY:
+ return Root()
+ elif entry == ARCHIVE_DIRENTRY:
+ return Archive(self.cache)
+ elif entry == META_DIRENTRY:
+ return Meta(self.cache)
+ # TODO: error handling
+
+ def get_attrs(self, entry: VirtualEntry) -> pyfuse3.EntryAttributes:
+ """ Return entry attributes """
+
+ attrs = pyfuse3.EntryAttributes()
+ attrs.st_size = 0
+ attrs.st_atime_ns = self.time_ns
+ attrs.st_ctime_ns = self.time_ns
+ attrs.st_mtime_ns = self.time_ns
+ attrs.st_gid = self.gid
+ attrs.st_uid = self.uid
+ attrs.st_ino = self._alloc_inode(entry)
+
+ if isinstance(entry, ArtifactEntry):
+ metadata = entry.prefetch or self.get_metadata(entry.swhid)
+ if entry.swhid.object_type == CONTENT:
+ # Only in the context of a directory entry do we have archived
+ # permissions. Otherwise, fallback to default read-only.
+ attrs.st_mode = metadata.get("perms", int(EntryMode.RDONLY_FILE))
+ attrs.st_size = metadata["length"]
+ else:
+ attrs.st_mode = int(EntryMode.RDONLY_DIR)
+ else:
+ attrs.st_mode = int(entry.mode)
+ # Meta JSON entries (under the root meta/ directory)
+ if entry.name.endswith(".json"):
+ swhid = parse_swhid(entry.name.replace(".json", ""))
+ metadata = self.get_metadata(swhid)
+ attrs.st_size = len(str(metadata))
+
+ return attrs
+
+ async def getattr(
+ self, inode: int, _ctx: pyfuse3.RequestContext
+ ) -> pyfuse3.EntryAttributes:
+ """ Get attributes for a given inode """
+
+ entry = self.inode2entry(inode)
+ return self.get_attrs(entry)
+
+ async def opendir(self, inode: int, _ctx: pyfuse3.RequestContext) -> int:
+ """ Open a directory referred by a given inode """
+
+ # Re-use inode as directory handle
+ return inode
+
+ async def readdir(
+ self, inode: int, offset: int, token: pyfuse3.ReaddirToken
+ ) -> None:
+ """ Read entries in an open directory """
+
+ direntry = self.inode2entry(inode)
+ path = self.inode2path(inode)
+
+ # TODO: add cache on direntry list?
+ entries = self.get_direntries(direntry)
+ next_id = offset + 1
+ for entry in itertools.islice(entries, offset, None):
+ name = os.fsencode(entry.name)
+ attrs = self.get_attrs(entry)
+ if not pyfuse3.readdir_reply(token, name, attrs, next_id):
+ break
+
+ next_id += 1
+ self._inode2entry[attrs.st_ino] = entry
+ self._inode2path[attrs.st_ino] = Path(path, entry.name)
+
+ async def open(
+ self, inode: int, _flags: int, _ctx: pyfuse3.RequestContext
+ ) -> pyfuse3.FileInfo:
+ """ Open an inode and return a unique file descriptor """
+
+ entry = self.inode2entry(inode)
+ fd = self._alloc_fd(entry)
+ return pyfuse3.FileInfo(fh=fd, keep_cache=True)
+
+ async def read(self, fd: int, _offset: int, _length: int) -> bytes:
+ """ Read blob content pointed by the given `fd` (file descriptor). Both
+ parameters `_offset` and `_length` are ignored. """
+ # TODO: use offset/length
+
+ try:
+ entry = self._fd2entry[fd]
+ except KeyError:
+ raise pyfuse3.FUSEError(errno.ENOENT)
+
+ if isinstance(entry, ArtifactEntry):
+ blob = self.get_blob(entry.swhid)
+ return blob.encode()
+ else:
+ # Meta JSON entries (under the root meta/ directory)
+ if entry.name.endswith(".json"):
+ swhid = parse_swhid(entry.name.replace(".json", ""))
+ metadata = self.get_metadata(swhid)
+ return str(metadata).encode()
+ else:
+ # TODO: error handling
+ raise pyfuse3.FUSEError(errno.ENOENT)
+
+ async def lookup(
+ self, parent_inode: int, name: str, _ctx: pyfuse3.RequestContext
+ ) -> pyfuse3.EntryAttributes:
+ """ Look up a directory entry by name and get its attributes """
+
+ name = os.fsdecode(name)
+ path = Path(self.inode2path(parent_inode), name)
+ parent_entry = self.inode2entry(parent_inode)
+
+ attr = None
+ if isinstance(parent_entry, ArtifactEntry):
+ metadata = self.get_metadata(parent_entry.swhid)
+ for entry in metadata:
+ if entry["name"] == name:
+ swhid = entry["target"]
+ attr = self.get_attrs(ArtifactEntry(name, swhid))
+ # TODO: this is fragile, maybe cache attrs?
+ else:
+ if parent_entry == ROOT_DIRENTRY:
+ if name == ARCHIVE_DIRENTRY.name:
+ attr = self.get_attrs(ARCHIVE_DIRENTRY)
+ elif name == META_DIRENTRY.name:
+ attr = self.get_attrs(META_DIRENTRY)
+ else:
+ swhid = parse_swhid(name)
+ attr = self.get_attrs(ArtifactEntry(name, swhid))
+
+ if attr:
+ self._inode2path[attr.st_ino] = path
+ return attr
+ else:
+ # TODO: error handling (name not found)
+ return pyfuse3.EntryAttributes()
+
+
+def main(swhids: List[SWHID], root_path: Path, conf: Dict[str, Any]) -> None:
+ """ swh-fuse CLI entry-point """
+
+ # Use pyfuse3 asyncio layer to match the rest of Software Heritage codebase
+ pyfuse3_asyncio.enable()
+
+ with FuseCache(conf["cache"]) as cache:
+ fs = Fuse(swhids, root_path, cache, conf)
+ fuse_options = set(pyfuse3.default_options)
+ fuse_options.add("fsname=swhfs")
+ fuse_options.add("debug")
+ pyfuse3.init(fs, root_path, fuse_options)
-def fuse():
- logging.error("not implemented: FUSE mounting")
- sys.exit(1)
+ loop = asyncio.get_event_loop()
+ try:
+ loop.run_until_complete(pyfuse3.main())
+ fs.shutdown()
+ finally:
+ pyfuse3.close(unmount=True)
+ loop.close()
diff --git a/swh/fuse/tests/api_data.py b/swh/fuse/tests/api_data.py
new file mode 100644
--- /dev/null
+++ b/swh/fuse/tests/api_data.py
@@ -0,0 +1,445 @@
+# GENERATED FILE, DO NOT EDIT.
+# Run './gen-api-data.py > api_data.py' instead.
+# fmt: off
+
+API_URL = 'https://invalid-test-only.archive.softwareheritage.org/api/1'
+ROOT_SWHID = 'swh:1:dir:9eb62ef7dd283f7385e7d31af6344d9feedd25de'
+ROOT_URL = 'directory/9eb62ef7dd283f7385e7d31af6344d9feedd25de/'
+README_URL = 'content/sha1_git:669ac7c32292798644b21dbb5a0dc657125f444d/'
+README_RAW_URL = 'content/sha1_git:669ac7c32292798644b21dbb5a0dc657125f444d/raw/'
+
+MOCK_ARCHIVE = {
+ 'directory/9eb62ef7dd283f7385e7d31af6344d9feedd25de/': # NoQA: E501
+ r"""[
+ {
+ "checksums": {
+ "sha1": "39a0a88cd8eae4504e1d33b0c0f88059044d761f",
+ "sha1_git": "a0a96088c74f49a961a80bc0851a84214b0a9f83",
+ "sha256": "7c0f4eaf45838f26ae951b490beb0d11034a30b21e5c39a54c4223f5c2018890"
+ },
+ "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
+ "length": 16166,
+ "name": ".clang-format",
+ "perms": 33188,
+ "status": "visible",
+ "target": "a0a96088c74f49a961a80bc0851a84214b0a9f83",
+ "target_url": "https://archive.softwareheritage.org/api/1/content/sha1_git:a0a96088c74f49a961a80bc0851a84214b0a9f83/",
+ "type": "file"
+ },
+ {
+ "checksums": {
+ "sha1": "0e31de4130c64f23e9ee5fc761fdbd807dc94360",
+ "sha1_git": "43967c6b20151ee126db08e24758e3c789bcb844",
+ "sha256": "dbd64d3f532b962d4681d79077cc186340f5f439de7f99c709b01892332af866"
+ },
+ "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
+ "length": 59,
+ "name": ".cocciconfig",
+ "perms": 33188,
+ "status": "visible",
+ "target": "43967c6b20151ee126db08e24758e3c789bcb844",
+ "target_url": "https://archive.softwareheritage.org/api/1/content/sha1_git:43967c6b20151ee126db08e24758e3c789bcb844/",
+ "type": "file"
+ },
+ {
+ "checksums": {
+ "sha1": "52b62d115dfae2ed19561db14e8d10ee22659e7f",
+ "sha1_git": "a64d219137455f407a7b1f2c6b156c5575852e9e",
+ "sha256": "4c9ba8e0ef521ce01474e98eddfc77afaec8a8e259939a139590c00505646527"
+ },
+ "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
+ "length": 71,
+ "name": ".get_maintainer.ignore",
+ "perms": 33188,
+ "status": "visible",
+ "target": "a64d219137455f407a7b1f2c6b156c5575852e9e",
+ "target_url": "https://archive.softwareheritage.org/api/1/content/sha1_git:a64d219137455f407a7b1f2c6b156c5575852e9e/",
+ "type": "file"
+ },
+ {
+ "checksums": {
+ "sha1": "6cc5a38e6f6ca93e21c78cb9c54794a42c3031c3",
+ "sha1_git": "4b32eaa9571e64e47b51c43537063f56b204d8b3",
+ "sha256": "dc52a4e1ee3615c87691aca7f667c7e49f6900f36b5c20339ac497366ba9406c"
+ },
+ "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
+ "length": 62,
+ "name": ".gitattributes",
+ "perms": 33188,
+ "status": "visible",
+ "target": "4b32eaa9571e64e47b51c43537063f56b204d8b3",
+ "target_url": "https://archive.softwareheritage.org/api/1/content/sha1_git:4b32eaa9571e64e47b51c43537063f56b204d8b3/",
+ "type": "file"
+ },
+ {
+ "checksums": {
+ "sha1": "1966a794db7d9518f321a8ecb0736c16de59bd91",
+ "sha1_git": "162bd2b67bdf6a28be7a361b8418e4e31d542854",
+ "sha256": "a9766c936a81df2ed3ea41f506ddaad88a78d9cf47e093df414b3b6f2e6d8e14"
+ },
+ "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
+ "length": 1852,
+ "name": ".gitignore",
+ "perms": 33188,
+ "status": "visible",
+ "target": "162bd2b67bdf6a28be7a361b8418e4e31d542854",
+ "target_url": "https://archive.softwareheritage.org/api/1/content/sha1_git:162bd2b67bdf6a28be7a361b8418e4e31d542854/",
+ "type": "file"
+ },
+ {
+ "checksums": {
+ "sha1": "2bb15bd51c981842b6f710d2e057972e5f22cfcc",
+ "sha1_git": "332c7833057f51da02805add9b60161ff31aee71",
+ "sha256": "d7b69571529964b3c8444a73ac720bbb883cc70fc4b78a36d1ac277f660c50bb"
+ },
+ "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
+ "length": 17283,
+ "name": ".mailmap",
+ "perms": 33188,
+ "status": "visible",
+ "target": "332c7833057f51da02805add9b60161ff31aee71",
+ "target_url": "https://archive.softwareheritage.org/api/1/content/sha1_git:332c7833057f51da02805add9b60161ff31aee71/",
+ "type": "file"
+ },
+ {
+ "checksums": {
+ "sha1": "0473e748fee37c7b68487fb102c0d563bbc641b3",
+ "sha1_git": "a635a38ef9405fdfcfe97f3a435393c1e9cae971",
+ "sha256": "fb5a425bd3b3cd6071a3a9aff9909a859e7c1158d54d32e07658398cd67eb6a0"
+ },
+ "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
+ "length": 496,
+ "name": "COPYING",
+ "perms": 33188,
+ "status": "visible",
+ "target": "a635a38ef9405fdfcfe97f3a435393c1e9cae971",
+ "target_url": "https://archive.softwareheritage.org/api/1/content/sha1_git:a635a38ef9405fdfcfe97f3a435393c1e9cae971/",
+ "type": "file"
+ },
+ {
+ "checksums": {
+ "sha1": "f27043aefa4b69b921df3728ee906c3f03087d29",
+ "sha1_git": "32ee70a7562eec7345e98841473abb438379a4fd",
+ "sha256": "23242c7183ee2815e27fea2346b0e5ad9131b8b611b200ae0418d4027cea2a3d"
+ },
+ "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
+ "length": 99788,
+ "name": "CREDITS",
+ "perms": 33188,
+ "status": "visible",
+ "target": "32ee70a7562eec7345e98841473abb438379a4fd",
+ "target_url": "https://archive.softwareheritage.org/api/1/content/sha1_git:32ee70a7562eec7345e98841473abb438379a4fd/",
+ "type": "file"
+ },
+ {
+ "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
+ "length": null,
+ "name": "Documentation",
+ "perms": 16384,
+ "target": "1ba46735273aa020a173c0ad0c813179530dd117",
+ "target_url": "https://archive.softwareheritage.org/api/1/directory/1ba46735273aa020a173c0ad0c813179530dd117/",
+ "type": "dir"
+ },
+ {
+ "checksums": {
+ "sha1": "2491dd3bed10f6918ed1657ab5a7a8efbddadf5d",
+ "sha1_git": "fa441b98c9f6eac1617acf1772ae8b371cfd42aa",
+ "sha256": "75df66064f75e91e6458862cd9413b19e65b77eefcc8a95dcbd6bf36fd2e4b59"
+ },
+ "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
+ "length": 1327,
+ "name": "Kbuild",
+ "perms": 33188,
+ "status": "visible",
+ "target": "fa441b98c9f6eac1617acf1772ae8b371cfd42aa",
+ "target_url": "https://archive.softwareheritage.org/api/1/content/sha1_git:fa441b98c9f6eac1617acf1772ae8b371cfd42aa/",
+ "type": "file"
+ },
+ {
+ "checksums": {
+ "sha1": "6b9b12a6bbff219dfbb45ec068af9aa7cf1b7288",
+ "sha1_git": "745bc773f567067a85ce6574fb41ce80833247d9",
+ "sha256": "a592dae7d067cd8e5dc43e3f9dc363eba9eb1f7cf80c6178b5cd291c0b76d3ec"
+ },
+ "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
+ "length": 555,
+ "name": "Kconfig",
+ "perms": 33188,
+ "status": "visible",
+ "target": "745bc773f567067a85ce6574fb41ce80833247d9",
+ "target_url": "https://archive.softwareheritage.org/api/1/content/sha1_git:745bc773f567067a85ce6574fb41ce80833247d9/",
+ "type": "file"
+ },
+ {
+ "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
+ "length": null,
+ "name": "LICENSES",
+ "perms": 16384,
+ "target": "a49a894ea3684b6c044448c37f812356550d14a2",
+ "target_url": "https://archive.softwareheritage.org/api/1/directory/a49a894ea3684b6c044448c37f812356550d14a2/",
+ "type": "dir"
+ },
+ {
+ "checksums": {
+ "sha1": "eb207b62f2fe0225dc55d9b87d82f6009e864117",
+ "sha1_git": "f0068bceeb6158a30c6eee430ca6d2a7e4c4013a",
+ "sha256": "3c81b34eaf99d943e4c2fac2548f3d9d740a9e9683ccedbaacfb82796c7965e1"
+ },
+ "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
+ "length": 569101,
+ "name": "MAINTAINERS",
+ "perms": 33188,
+ "status": "visible",
+ "target": "f0068bceeb6158a30c6eee430ca6d2a7e4c4013a",
+ "target_url": "https://archive.softwareheritage.org/api/1/content/sha1_git:f0068bceeb6158a30c6eee430ca6d2a7e4c4013a/",
+ "type": "file"
+ },
+ {
+ "checksums": {
+ "sha1": "9bfa205a4d23b9a60889bd9b59010574670b8b90",
+ "sha1_git": "f2116815416091dbfa7dcf58ae179ae3241ec1b1",
+ "sha256": "e87fb2b9482b9066b47c1656e55ebc4897bbb226daa122bcd4a2858fef19e597"
+ },
+ "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
+ "length": 63305,
+ "name": "Makefile",
+ "perms": 33188,
+ "status": "visible",
+ "target": "f2116815416091dbfa7dcf58ae179ae3241ec1b1",
+ "target_url": "https://archive.softwareheritage.org/api/1/content/sha1_git:f2116815416091dbfa7dcf58ae179ae3241ec1b1/",
+ "type": "file"
+ },
+ {
+ "checksums": {
+ "sha1": "ca1dc365022dcaa728dfb11bcde40ad3cce0574b",
+ "sha1_git": "669ac7c32292798644b21dbb5a0dc657125f444d",
+ "sha256": "bad58d396f62102befaf23a8a2ab6b1693fdc8f318de3059b489781f28865612"
+ },
+ "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
+ "length": 727,
+ "name": "README",
+ "perms": 33188,
+ "status": "visible",
+ "target": "669ac7c32292798644b21dbb5a0dc657125f444d",
+ "target_url": "https://archive.softwareheritage.org/api/1/content/sha1_git:669ac7c32292798644b21dbb5a0dc657125f444d/",
+ "type": "file"
+ },
+ {
+ "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
+ "length": null,
+ "name": "arch",
+ "perms": 16384,
+ "target": "cf12c1ce4de958ab4ddcb008fe89118b82a3c7b7",
+ "target_url": "https://archive.softwareheritage.org/api/1/directory/cf12c1ce4de958ab4ddcb008fe89118b82a3c7b7/",
+ "type": "dir"
+ },
+ {
+ "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
+ "length": null,
+ "name": "block",
+ "perms": 16384,
+ "target": "a77c89fa64b8ec37c9aa0fa98add54bfb6075257",
+ "target_url": "https://archive.softwareheritage.org/api/1/directory/a77c89fa64b8ec37c9aa0fa98add54bfb6075257/",
+ "type": "dir"
+ },
+ {
+ "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
+ "length": null,
+ "name": "certs",
+ "perms": 16384,
+ "target": "527d8f94235029c6f571414df5f8ed2951a0ca5b",
+ "target_url": "https://archive.softwareheritage.org/api/1/directory/527d8f94235029c6f571414df5f8ed2951a0ca5b/",
+ "type": "dir"
+ },
+ {
+ "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
+ "length": null,
+ "name": "crypto",
+ "perms": 16384,
+ "target": "1fb1357e2d22af4332091937ed960a47f78d0b5e",
+ "target_url": "https://archive.softwareheritage.org/api/1/directory/1fb1357e2d22af4332091937ed960a47f78d0b5e/",
+ "type": "dir"
+ },
+ {
+ "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
+ "length": null,
+ "name": "drivers",
+ "perms": 16384,
+ "target": "3b5be1ee0216ec59c70e132681be4a5d79e7da9b",
+ "target_url": "https://archive.softwareheritage.org/api/1/directory/3b5be1ee0216ec59c70e132681be4a5d79e7da9b/",
+ "type": "dir"
+ },
+ {
+ "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
+ "length": null,
+ "name": "fs",
+ "perms": 16384,
+ "target": "1dbf8d211613db72f5b83b0987023bd5acf866ee",
+ "target_url": "https://archive.softwareheritage.org/api/1/directory/1dbf8d211613db72f5b83b0987023bd5acf866ee/",
+ "type": "dir"
+ },
+ {
+ "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
+ "length": null,
+ "name": "include",
+ "perms": 16384,
+ "target": "74991fd1a983c6b3f72c8815f7de81a3abddb255",
+ "target_url": "https://archive.softwareheritage.org/api/1/directory/74991fd1a983c6b3f72c8815f7de81a3abddb255/",
+ "type": "dir"
+ },
+ {
+ "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
+ "length": null,
+ "name": "init",
+ "perms": 16384,
+ "target": "c944a589113271d878e27bbc31ae369edecaff90",
+ "target_url": "https://archive.softwareheritage.org/api/1/directory/c944a589113271d878e27bbc31ae369edecaff90/",
+ "type": "dir"
+ },
+ {
+ "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
+ "length": null,
+ "name": "ipc",
+ "perms": 16384,
+ "target": "ff553b9398fea6b2e290ea4a95f7a94f1cf3c22c",
+ "target_url": "https://archive.softwareheritage.org/api/1/directory/ff553b9398fea6b2e290ea4a95f7a94f1cf3c22c/",
+ "type": "dir"
+ },
+ {
+ "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
+ "length": null,
+ "name": "kernel",
+ "perms": 16384,
+ "target": "8c700fd3589e6d2befa4d9b2cc79471eac37da38",
+ "target_url": "https://archive.softwareheritage.org/api/1/directory/8c700fd3589e6d2befa4d9b2cc79471eac37da38/",
+ "type": "dir"
+ },
+ {
+ "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
+ "length": null,
+ "name": "lib",
+ "perms": 16384,
+ "target": "0f2936da43bebe4f26b3be83e8fa392c4f9e82cf",
+ "target_url": "https://archive.softwareheritage.org/api/1/directory/0f2936da43bebe4f26b3be83e8fa392c4f9e82cf/",
+ "type": "dir"
+ },
+ {
+ "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
+ "length": null,
+ "name": "mm",
+ "perms": 16384,
+ "target": "e15d954c1ed09e6fc29c184515834696d8e70e7c",
+ "target_url": "https://archive.softwareheritage.org/api/1/directory/e15d954c1ed09e6fc29c184515834696d8e70e7c/",
+ "type": "dir"
+ },
+ {
+ "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
+ "length": null,
+ "name": "net",
+ "perms": 16384,
+ "target": "41e1603b37542d265eade0555e0db66668135575",
+ "target_url": "https://archive.softwareheritage.org/api/1/directory/41e1603b37542d265eade0555e0db66668135575/",
+ "type": "dir"
+ },
+ {
+ "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
+ "length": null,
+ "name": "samples",
+ "perms": 16384,
+ "target": "9fa649fea3c8ab6b4926f0e7721a21a36b685153",
+ "target_url": "https://archive.softwareheritage.org/api/1/directory/9fa649fea3c8ab6b4926f0e7721a21a36b685153/",
+ "type": "dir"
+ },
+ {
+ "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
+ "length": null,
+ "name": "scripts",
+ "perms": 16384,
+ "target": "e4e5b45d7c44d0bd2c6feb1a257fff7303d2c67e",
+ "target_url": "https://archive.softwareheritage.org/api/1/directory/e4e5b45d7c44d0bd2c6feb1a257fff7303d2c67e/",
+ "type": "dir"
+ },
+ {
+ "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
+ "length": null,
+ "name": "security",
+ "perms": 16384,
+ "target": "a4a58d89fc506c3660610105a08de60614cdc980",
+ "target_url": "https://archive.softwareheritage.org/api/1/directory/a4a58d89fc506c3660610105a08de60614cdc980/",
+ "type": "dir"
+ },
+ {
+ "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
+ "length": null,
+ "name": "sound",
+ "perms": 16384,
+ "target": "bf9e1568b8ce61157a322fddbaab1a0c76be15ef",
+ "target_url": "https://archive.softwareheritage.org/api/1/directory/bf9e1568b8ce61157a322fddbaab1a0c76be15ef/",
+ "type": "dir"
+ },
+ {
+ "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
+ "length": null,
+ "name": "tools",
+ "perms": 16384,
+ "target": "83d6279411023bf7edf6bde6ce2e3748912f4936",
+ "target_url": "https://archive.softwareheritage.org/api/1/directory/83d6279411023bf7edf6bde6ce2e3748912f4936/",
+ "type": "dir"
+ },
+ {
+ "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
+ "length": null,
+ "name": "usr",
+ "perms": 16384,
+ "target": "aae2ca939e0f7ac6b5e489e4c7835e1a15588cff",
+ "target_url": "https://archive.softwareheritage.org/api/1/directory/aae2ca939e0f7ac6b5e489e4c7835e1a15588cff/",
+ "type": "dir"
+ },
+ {
+ "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
+ "length": null,
+ "name": "virt",
+ "perms": 16384,
+ "target": "d7f6f10a8509839e404d1cc5af51317ac8b26276",
+ "target_url": "https://archive.softwareheritage.org/api/1/directory/d7f6f10a8509839e404d1cc5af51317ac8b26276/",
+ "type": "dir"
+ }
+]
+""", # NoQA: E501
+ 'content/sha1_git:669ac7c32292798644b21dbb5a0dc657125f444d/': # NoQA: E501
+ r"""{
+ "checksums": {
+ "blake2s256": "746aaa0816ffc8cadf5e7f70b8bb93a47a76299ef263c743dbfef2644c6a0245",
+ "sha1": "ca1dc365022dcaa728dfb11bcde40ad3cce0574b",
+ "sha1_git": "669ac7c32292798644b21dbb5a0dc657125f444d",
+ "sha256": "bad58d396f62102befaf23a8a2ab6b1693fdc8f318de3059b489781f28865612"
+ },
+ "data_url": "https://archive.softwareheritage.org/api/1/content/sha1_git:669ac7c32292798644b21dbb5a0dc657125f444d/raw/",
+ "filetype_url": "https://archive.softwareheritage.org/api/1/content/sha1_git:669ac7c32292798644b21dbb5a0dc657125f444d/filetype/",
+ "language_url": "https://archive.softwareheritage.org/api/1/content/sha1_git:669ac7c32292798644b21dbb5a0dc657125f444d/language/",
+ "length": 727,
+ "license_url": "https://archive.softwareheritage.org/api/1/content/sha1_git:669ac7c32292798644b21dbb5a0dc657125f444d/license/",
+ "status": "visible"
+}
+""", # NoQA: E501
+ 'content/sha1_git:669ac7c32292798644b21dbb5a0dc657125f444d/raw/': # NoQA: E501
+ r"""Linux kernel
+============
+
+There are several guides for kernel developers and users. These guides can
+be rendered in a number of formats, like HTML and PDF. Please read
+Documentation/admin-guide/README.rst first.
+
+In order to build the documentation, use ``make htmldocs`` or
+``make pdfdocs``. The formatted documentation can also be read online at:
+
+ https://www.kernel.org/doc/html/latest/
+
+There are various text files in the Documentation/ subdirectory,
+several of them using the Restructured Text markup notation.
+
+Please read the Documentation/process/changes.rst file, as it contains the
+requirements for building and running the kernel, and information about
+the problems which may result by upgrading your kernel.
+""", # NoQA: E501
+}
diff --git a/swh/fuse/tests/conftest.py b/swh/fuse/tests/conftest.py
new file mode 100644
--- /dev/null
+++ b/swh/fuse/tests/conftest.py
@@ -0,0 +1,65 @@
+# 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 multiprocessing import Process
+from os import listdir
+from pathlib import Path
+import subprocess
+from tempfile import NamedTemporaryFile, TemporaryDirectory
+import time
+
+from click.testing import CliRunner
+import pytest
+import yaml
+
+from swh.fuse import cli
+
+from .api_data import API_URL, MOCK_ARCHIVE, ROOT_SWHID
+
+
+@pytest.fixture
+def web_api_mock(requests_mock):
+ for api_call, data in MOCK_ARCHIVE.items():
+ requests_mock.get(f"{API_URL}/{api_call}", text=data)
+ return requests_mock
+
+
+@pytest.fixture
+def fuse_mntdir(web_api_mock):
+ tmpdir = TemporaryDirectory(suffix=".swh-fuse-test")
+ tmpfile = NamedTemporaryFile(suffix=".swh-fuse-test.yml")
+
+ config = {
+ "cache": {"metadata": {"in-memory": True}, "blob": {"in-memory": True}},
+ "web-api": {"url": API_URL, "auth-token": None},
+ }
+
+ # Run FUSE in foreground mode but in a separate process, so it does not
+ # block execution and remains easy to kill during teardown
+ def fuse_process(tmpdir, tmpfile):
+ with tmpdir as mntdir, tmpfile as config_path:
+ config_path = Path(config_path.name)
+ config_path.write_text(yaml.dump(config))
+ CliRunner().invoke(
+ cli.mount,
+ args=[mntdir, ROOT_SWHID, "--foreground", "--config-file", config_path],
+ )
+
+ fuse = Process(target=fuse_process, args=[tmpdir, tmpfile])
+ fuse.start()
+ # Wait max 3 seconds for the FUSE to correctly mount
+ i = 0
+ while i < 30:
+ try:
+ root = listdir(tmpdir.name)
+ if root:
+ break
+ except FileNotFoundError:
+ i += 1
+ time.sleep(0.1)
+
+ yield tmpdir.name
+
+ subprocess.run(["fusermount", "-u", tmpdir.name], check=True)
diff --git a/swh/fuse/tests/gen-api-data.py b/swh/fuse/tests/gen-api-data.py
new file mode 100755
--- /dev/null
+++ b/swh/fuse/tests/gen-api-data.py
@@ -0,0 +1,51 @@
+#!/usr/bin/env python3
+
+# 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
+
+import json
+
+import requests
+
+API_URL_real = "https://archive.softwareheritage.org/api/1"
+API_URL_test = "https://invalid-test-only.archive.softwareheritage.org/api/1"
+
+# Use the Linux kernel as a testing repository
+ROOT_HASH = "9eb62ef7dd283f7385e7d31af6344d9feedd25de"
+README_HASH = "669ac7c32292798644b21dbb5a0dc657125f444d"
+
+ROOT_SWHID = f"swh:1:dir:{ROOT_HASH}"
+
+urls = {
+ "ROOT": f"directory/{ROOT_HASH}/",
+ "README": f"content/sha1_git:{README_HASH}/",
+ "README_RAW": f"content/sha1_git:{README_HASH}/raw/",
+}
+
+print("# GENERATED FILE, DO NOT EDIT.")
+print("# Run './gen-api-data.py > api_data.py' instead.")
+print("# fmt: off")
+
+print("")
+print(f"API_URL = '{API_URL_test}'")
+print(f"ROOT_SWHID = '{ROOT_SWHID}'")
+for name, url in urls.items():
+ print(f"{name}_URL = '{url}'")
+print("")
+
+print("MOCK_ARCHIVE = {")
+for url in urls.values():
+ print(f" '{url}': # NoQA: E501")
+ print(' r"""', end="")
+
+ data = requests.get(f"{API_URL_real}/{url}").text
+ if url.endswith("/raw/"):
+ print(data, end="")
+ else:
+ parsed = json.loads(data)
+ print(json.dumps(parsed, indent=2, sort_keys=True))
+
+ print('""", # NoQA: E501')
+print("}")
diff --git a/swh/fuse/tests/test_cli.py b/swh/fuse/tests/test_cli.py
--- a/swh/fuse/tests/test_cli.py
+++ b/swh/fuse/tests/test_cli.py
@@ -3,17 +3,15 @@
# License: GNU General Public License version 3, or any later version
# See top-level LICENSE file for more information
-import unittest
+from pathlib import Path
-from click.testing import CliRunner
+from .api_data import ROOT_SWHID
-from swh.fuse import cli
-
-class TestMount(unittest.TestCase):
- def setUp(self):
- self.runner = CliRunner()
-
- def test_no_args(self):
- result = self.runner.invoke(cli.mount)
- self.assertNotEqual(result.exit_code, 0)
+def test_mountpoint(fuse_mntdir):
+ archive_dir = Path(fuse_mntdir, "archive")
+ meta_dir = Path(fuse_mntdir, "meta")
+ swhid_dir = Path(fuse_mntdir, "archive", ROOT_SWHID)
+ assert archive_dir.is_dir()
+ assert meta_dir.is_dir()
+ assert swhid_dir.is_dir()
diff --git a/swh/fuse/tests/test_content.py b/swh/fuse/tests/test_content.py
new file mode 100644
--- /dev/null
+++ b/swh/fuse/tests/test_content.py
@@ -0,0 +1,16 @@
+from pathlib import Path
+
+from .api_data import MOCK_ARCHIVE, README_RAW_URL, ROOT_SWHID
+
+
+def test_file_exists(fuse_mntdir):
+ readme_path = Path(fuse_mntdir, "archive", ROOT_SWHID, "README")
+ assert readme_path.is_file()
+
+
+def test_cat_file(fuse_mntdir):
+ readme_path = Path(fuse_mntdir, "archive", ROOT_SWHID, "README")
+ expected = MOCK_ARCHIVE[README_RAW_URL]
+ with open(readme_path, "r") as f:
+ actual = f.read()
+ assert actual == expected
diff --git a/swh/fuse/tests/test_directory.py b/swh/fuse/tests/test_directory.py
new file mode 100644
--- /dev/null
+++ b/swh/fuse/tests/test_directory.py
@@ -0,0 +1,14 @@
+import json
+from os import listdir
+from pathlib import Path
+
+from .api_data import MOCK_ARCHIVE, ROOT_SWHID, ROOT_URL
+
+
+def test_ls_root_swhid(fuse_mntdir):
+ root_resp = json.loads(MOCK_ARCHIVE[ROOT_URL])
+ expected = [entry["name"] for entry in root_resp]
+
+ swhid_dir = Path(fuse_mntdir, "archive", ROOT_SWHID)
+ actual = listdir(swhid_dir)
+ assert actual == expected
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Dec 21 2024, 1:49 PM (11 w, 4 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3228254
Attached To
D4064: Early FUSE implementation, with support for blob and directory objects
Event Timeline
Log In to Comment