diff --git a/swh/graphql/backends/archive.py b/swh/graphql/backends/archive.py --- a/swh/graphql/backends/archive.py +++ b/swh/graphql/backends/archive.py @@ -3,7 +3,11 @@ # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information +import os +from typing import Any, Dict, Optional + from swh.graphql import server +from swh.model.model import Sha1Git from swh.model.swhids import ObjectType @@ -57,6 +61,14 @@ def get_releases(self, release_ids): return self.storage.release_get(releases=release_ids) + def get_directory_entry_by_path( + self, directory_id: Sha1Git, path: str + ) -> Optional[Dict[str, Any]]: + paths = [x.encode() for x in path.strip(os.path.sep).split(os.path.sep)] + return self.storage.directory_entry_get_by_path( + directory=directory_id, paths=paths + ) + def get_directory_entries(self, directory_id, after=None, first=50): return self.storage.directory_get_entries( directory_id, limit=first, page_token=after diff --git a/swh/graphql/resolvers/content.py b/swh/graphql/resolvers/content.py --- a/swh/graphql/resolvers/content.py +++ b/swh/graphql/resolvers/content.py @@ -6,7 +6,7 @@ from typing import Union from .base_node import BaseSWHNode -from .directory_entry import DirectoryEntryNode +from .directory_entry import BaseDirectoryEntryNode from .release import BaseReleaseNode from .snapshot_branch import BaseSnapshotBranchNode @@ -87,7 +87,7 @@ This request could be from directory entry, release or a branch """ - obj: Union[DirectoryEntryNode, BaseReleaseNode, BaseSnapshotBranchNode] + obj: Union[BaseDirectoryEntryNode, BaseReleaseNode, BaseSnapshotBranchNode] def _get_node_data(self): return self._get_content_by_hash(checksums={"sha1_git": self.obj.target_hash}) diff --git a/swh/graphql/resolvers/directory.py b/swh/graphql/resolvers/directory.py --- a/swh/graphql/resolvers/directory.py +++ b/swh/graphql/resolvers/directory.py @@ -36,7 +36,6 @@ def _get_node_data(self): swhid = self.kwargs.get("swhid") - # path = "" if ( swhid.object_type == ObjectType.DIRECTORY and self.archive.is_object_available(swhid.object_id, swhid.object_type) @@ -64,9 +63,9 @@ Node resolver for a directory requested as a target """ - from .directory_entry import DirectoryEntryNode + from .directory_entry import BaseDirectoryEntryNode - obj: Union[BaseSnapshotBranchNode, BaseReleaseNode, DirectoryEntryNode] + obj: Union[BaseSnapshotBranchNode, BaseReleaseNode, BaseDirectoryEntryNode] def _get_node_data(self): return self._get_directory_by_id(self.obj.target_hash) diff --git a/swh/graphql/resolvers/directory_entry.py b/swh/graphql/resolvers/directory_entry.py --- a/swh/graphql/resolvers/directory_entry.py +++ b/swh/graphql/resolvers/directory_entry.py @@ -10,11 +10,7 @@ from .base_node import BaseNode -class DirectoryEntryNode(BaseNode): - """ - Node resolver for a directory entry - """ - +class BaseDirectoryEntryNode(BaseNode): @property def target_hash(self): # for DirectoryNode return self._node.target @@ -24,6 +20,21 @@ return self._node.type +class DirectoryEntryNode(BaseDirectoryEntryNode): + """ + Node resolver for a directory entry requested with a + directory SWHID and a relative path + """ + + def _get_node_data(self): + # STORAGE-TODO, archive is returning a dict + # return DirectoryEntry object instead + return self.archive.get_directory_entry_by_path( + directory_id=self.kwargs.get("swhid").object_id, + path=self.kwargs.get("path"), + ) + + class DirectoryEntryConnection(BaseConnection): """ Connection resolver for entries in a directory @@ -33,7 +44,7 @@ obj: BaseDirectoryNode - _node_class = DirectoryEntryNode + _node_class = BaseDirectoryEntryNode def _get_paged_result(self) -> PagedResult: # FIXME, using dummy(local) pagination, move pagination to backend diff --git a/swh/graphql/resolvers/resolver_factory.py b/swh/graphql/resolvers/resolver_factory.py --- a/swh/graphql/resolvers/resolver_factory.py +++ b/swh/graphql/resolvers/resolver_factory.py @@ -5,7 +5,7 @@ from .content import ContentNode, HashContentNode, TargetContentNode from .directory import DirectoryNode, RevisionDirectoryNode, TargetDirectoryNode -from .directory_entry import DirectoryEntryConnection +from .directory_entry import DirectoryEntryConnection, DirectoryEntryNode from .origin import OriginConnection, OriginNode, TargetOriginNode from .release import ReleaseNode, TargetReleaseNode from .revision import ( @@ -49,6 +49,7 @@ "release-directory": TargetDirectoryNode, "release-content": TargetContentNode, "directory": DirectoryNode, + "directory-entry": DirectoryEntryNode, "content": ContentNode, "content-by-hash": HashContentNode, "dir-entry-dir": TargetDirectoryNode, diff --git a/swh/graphql/resolvers/resolvers.py b/swh/graphql/resolvers/resolvers.py --- a/swh/graphql/resolvers/resolvers.py +++ b/swh/graphql/resolvers/resolvers.py @@ -155,9 +155,17 @@ return resolver(obj, info, **kw) +@query.field("directoryEntry") +def directory_entry_resolver( + obj: None, info: GraphQLResolveInfo, **kw +) -> rs.directory.DirectoryNode: + resolver = get_node_resolver("directory-entry") + return resolver(obj, info, **kw) + + @directory_entry.field("target") def directory_entry_target_resolver( - obj: rs.directory_entry.DirectoryEntryNode, info: GraphQLResolveInfo, **kw + obj: rs.directory_entry.BaseDirectoryEntryNode, info: GraphQLResolveInfo, **kw ): """ directory entry target can be a directory or a content @@ -252,7 +260,7 @@ @directory.field("entries") -def directory_entry_resolver( +def directory_entries_resolver( obj, info: GraphQLResolveInfo, **kw ) -> rs.directory_entry.DirectoryEntryConnection: resolver = get_connection_resolver("directory-entries") diff --git a/swh/graphql/schema/schema.graphql b/swh/graphql/schema/schema.graphql --- a/swh/graphql/schema/schema.graphql +++ b/swh/graphql/schema/schema.graphql @@ -1010,6 +1010,21 @@ swhid: SWHID! ): Directory + """ + Get a directory entry with directory SWHID and a path + """ + directoryEntry( + """ + SWHID of the directory object + """ + swhid: SWHID! + + """ + Relative path to the requested object + """ + path: String! + ): DirectoryEntry + """ Get the content with a SWHID """ diff --git a/swh/graphql/tests/data.py b/swh/graphql/tests/data.py --- a/swh/graphql/tests/data.py +++ b/swh/graphql/tests/data.py @@ -3,7 +3,14 @@ # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information -from swh.model.model import ObjectType, Release, Revision, RevisionType +from swh.model.model import ( + Directory, + DirectoryEntry, + ObjectType, + Release, + Revision, + RevisionType, +) from swh.model.tests import swh_model_data @@ -92,9 +99,25 @@ ] +def get_directories_with_nested_path(): + return [ + Directory( + entries=( + DirectoryEntry( + name=b"sub-dir", + perms=0o644, + type="dir", + target=get_directories()[1].id, + ), + ) + ) + ] + + GRAPHQL_EXTRA_TEST_OBJECTS = { "release": get_releases_with_target(), "revision": get_revisions_with_parents(), + "directory": get_directories_with_nested_path(), } diff --git a/swh/graphql/tests/functional/test_directory_entry.py b/swh/graphql/tests/functional/test_directory_entry.py --- a/swh/graphql/tests/functional/test_directory_entry.py +++ b/swh/graphql/tests/functional/test_directory_entry.py @@ -5,8 +5,113 @@ import pytest +from swh.graphql import server +from swh.model.swhids import CoreSWHID, ObjectType + from . import utils -from ..data import get_directories +from ..data import get_contents, get_directories, get_directories_with_nested_path + + +def test_get_directory_entry_missing_path(client): + directory = get_directories()[0] + path = "missing" + query_str = """ + { + directoryEntry(swhid: "%s", path: "%s") { + name { + text + } + targetType + target { + ...on Content { + swhid + } + } + } + } + """ % ( + directory.swhid(), + path, + ) + utils.assert_missing_object(client, query_str, "directoryEntry") + + +def test_get_directory_entry(client): + directory = get_directories()[1] + storage = server.get_storage() + query_str = """ + { + directoryEntry(swhid: "%s", path: "%s") { + name { + text + } + targetType + target { + ...on Content { + swhid + } + ...on Directory { + swhid + } + } + } + } + """ + for entry in storage.directory_ls(directory.id, recursive=True): + if entry["type"] == "rev": + # FIXME, Revision is not supported as a directory entry target yet + continue + query = query_str % ( + directory.swhid(), + entry["name"].decode(), + ) + data, _ = utils.get_query_response( + client, + query, + ) + if entry["type"] == "file": + swhid = CoreSWHID( + object_type=ObjectType.CONTENT, object_id=entry["sha1_git"] + ) + elif entry["type"] == "dir": + swhid = CoreSWHID( + object_type=ObjectType.DIRECTORY, object_id=entry["target"] + ) + assert data["directoryEntry"] == { + "name": {"text": entry["name"].decode()}, + "target": {"swhid": str(swhid)}, + "targetType": entry["type"], + } + + +def test_get_directory_entry_with_nested_path(client): + directory = get_directories_with_nested_path()[0] + # the first entry to this directory is another directory with a file content + path = f"{directory.entries[0].name.decode()}/{get_directories()[1].entries[0].name.decode()}" # noqa: B950 + query_str = """ + { + directoryEntry(swhid: "%s", path: "%s") { + name { + text + } + targetType + target { + ...on Content { + swhid + } + } + } + } + """ % ( + directory.swhid(), + path, + ) + data, _ = utils.get_query_response(client, query_str) + assert data["directoryEntry"] == { + "name": {"text": path}, + "target": {"swhid": str(get_contents()[0].swhid())}, + "targetType": "file", + } @pytest.mark.parametrize("directory", get_directories()) diff --git a/swh/graphql/tests/unit/resolvers/test_resolvers.py b/swh/graphql/tests/unit/resolvers/test_resolvers.py --- a/swh/graphql/tests/unit/resolvers/test_resolvers.py +++ b/swh/graphql/tests/unit/resolvers/test_resolvers.py @@ -56,7 +56,7 @@ (rs.revision_parents_resolver, resolvers.revision.ParentRevisionConnection), # (rs.revision_log_resolver, resolvers.revision.LogRevisionConnection), ( - rs.directory_entry_resolver, + rs.directory_entries_resolver, resolvers.directory_entry.DirectoryEntryConnection, ), ],