diff --git a/src/pygobbler/__init__.py b/src/pygobbler/__init__.py index 86d6a23..836bb56 100644 --- a/src/pygobbler/__init__.py +++ b/src/pygobbler/__init__.py @@ -43,3 +43,4 @@ from .reject_probation import reject_probation from .set_permissions import set_permissions from .unpack_path import unpack_path +from .reroute_links import reroute_links diff --git a/src/pygobbler/remove_asset.py b/src/pygobbler/remove_asset.py index 4930585..117f8e8 100644 --- a/src/pygobbler/remove_asset.py +++ b/src/pygobbler/remove_asset.py @@ -4,6 +4,8 @@ def remove_asset(project: str, asset: str, staging: str, url: str, force: bool = False): """ Remove an asset of a project from the registry. + This should only be performed by Gobbler instance administrators. + Consider running :py:func:`~pygobbler.reroute_links.reroute_links` beforehand to avoid dangling references to files in this asset. Args: project: diff --git a/src/pygobbler/remove_project.py b/src/pygobbler/remove_project.py index ab6d484..9383ccb 100644 --- a/src/pygobbler/remove_project.py +++ b/src/pygobbler/remove_project.py @@ -4,6 +4,8 @@ def remove_project(project: str, staging: str, url: str): """ Remove a project from the registry. + This should only be performed by Gobbler instance administrators. + Consider running :py:func:`~pygobbler.reroute_links.reroute_links` beforehand to avoid dangling references to files in this project. Args: project: diff --git a/src/pygobbler/remove_version.py b/src/pygobbler/remove_version.py index 4735999..58b1dff 100644 --- a/src/pygobbler/remove_version.py +++ b/src/pygobbler/remove_version.py @@ -4,6 +4,8 @@ def remove_version(project: str, asset: str, version: str, staging: str, url: str, force: bool = False): """ Remove a version of a project asset from the registry. + This should only be performed by Gobbler instance administrators. + Consider running :py:func:`~pygobbler.reroute_links.reroute_links` beforehand to avoid dangling references to files in this version. Args: project: diff --git a/src/pygobbler/reroute_links.py b/src/pygobbler/reroute_links.py new file mode 100644 index 0000000..2403233 --- /dev/null +++ b/src/pygobbler/reroute_links.py @@ -0,0 +1,47 @@ +from typing import List +from ._utils import dump_request + + +def reroute_links(to_delete: List, staging: str, url: str, dry_run: bool = False) -> List: + """Reroute symbolic links to files in directories that are to be deleted, e.g., by :py:func:`~pygobbler.remove_project.remove_project`. + This preserves the validity of links within the Gobbler registry. + + Note that rerouting does not actually delete the directories specified in ``to_delete``. + Deletion requires separate invocations of :py:func:`~pygobbler.remove_project.remove_project` and friends - preferably after the user has verified that rerouting was successful! + + Rerouting is not necessary if ``to_delete`` consists only of probational versions, or projects/assets containing only probational versions. + The Gobbler should never create links to files in probational version directories. + + Args: + to_delete: + List of projects, assets or versions to be deleted. + Each entry should be a dicionary containing at least the ``project`` name. + When deleting an asset, the inner list should contain an additional ``asset`` name. + When deleting a version, the inner list should contain additional ``asset`` and ``version`` names. + Different inner lists may specify different projects, assets or versions. + + staging: + Path to the staging directory. + + url: + URL for the Gobbler REST API. + + dry_run: + Whether to perform a dry run of the rerouting. + + Returns: + List of dictionaries. + Each dictionary represents a rerouting action and contains the following fields. + + - ``path``, string containing the path to a symbolic link in the registry that was changed by rerouting. + - ``copy``, boolean indicating whether the link at ``path`` was replaced by a copy of its target file. + If ``False``, the link was merely updated to refer to a new target file. + - ``source``, the path to the target file that caused rerouting of ``path``. + Specifically, this is a file in one of the to-be-deleted directories specified in ``to_delete``. + If ``copy = TRUE``, this is the original linked-to file that was copied to ``path``. + + If ``dry_run = False``, the registry is modified as described by the rerouting actions. + Otherwise, no modifications are performed to the registry. + """ + out = dump_request(staging, url, "reroute_links", { "to_delete": to_delete, "dry_run": dry_run }) + return out["changes"] diff --git a/src/pygobbler/start_gobbler.py b/src/pygobbler/start_gobbler.py index efaefc1..7330238 100644 --- a/src/pygobbler/start_gobbler.py +++ b/src/pygobbler/start_gobbler.py @@ -14,7 +14,7 @@ def start_gobbler( registry: Optional[str] = None, port: Optional[int] = None, wait: float = 1, - version: str = "0.3.9", + version: str = "0.3.10", overwrite: bool = False) -> Tuple[bool, str, str, str]: """ Start a test Gobbler service. diff --git a/tests/test_reroute_links.py b/tests/test_reroute_links.py new file mode 100644 index 0000000..c385868 --- /dev/null +++ b/tests/test_reroute_links.py @@ -0,0 +1,32 @@ +import pygobbler as pyg +import os + + +def test_reroute_links(): + _, staging, registry, url = pyg.start_gobbler() + + pyg.remove_project("test-reroute", staging=staging, url=url) + pyg.create_project("test-reroute", staging=staging, url=url) + + src = pyg.allocate_upload_directory(staging) + with open(os.path.join(src, "foo"), "w") as handle: + handle.write("BAR") + + pyg.upload_directory("test-reroute", "simple", "v1", src, staging=staging, url=url) + pyg.upload_directory("test-reroute", "simple", "v2", src, staging=staging, url=url) + pyg.upload_directory("test-reroute", "simple", "v3", src, staging=staging, url=url) + + actions = pyg.reroute_links([{"project":"test-reroute", "asset":"simple", "version":"v1"}], staging=staging, url=url, dry_run=True) + print(actions) + assert all([x["source"] == "test-reroute/simple/v1/foo" for x in actions]) + all_paths = [x["path"] for x in actions] + assert "test-reroute/simple/v2/foo" in all_paths + assert "test-reroute/simple/v3/foo" in all_paths + all_copy = [x["copy"] for x in actions] + assert all_copy[all_paths.index("test-reroute/simple/v2/foo")] + assert not all_copy[all_paths.index("test-reroute/simple/v3/foo")] + assert os.path.islink(os.path.join(registry, "test-reroute/simple/v2/foo")) + + actions2 = pyg.reroute_links([{"project":"test-reroute", "asset":"simple", "version":"v1"}], staging=staging, url=url) + assert actions == actions2 + assert not os.path.islink(os.path.join(registry, "test-reroute/simple/v2/foo"))