From 434763649629eae7ae0de14a7350bebfa36875a7 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 16 Sep 2026 22:01:39 +0100 Subject: [PATCH] fix: stage a restored volume under /tmp, where the image can write The image does not run as root, so the staging directory could not be created at the container root: `mkdir /stage` failed with permission denied and every restore would have failed. It goes under /tmp now, and the command is built as one string instead of concatenated pieces inside the argument list. Checked against a real volume and the published image: a truncated tar fails and leaves the volume exactly as it was, and a whole one restores it. --- docsgpt/deploy/docker.py | 14 +++++++++----- tests/deploy/test_docker.py | 3 ++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/docsgpt/deploy/docker.py b/docsgpt/deploy/docker.py index c44eb241..2b3dcdfa 100644 --- a/docsgpt/deploy/docker.py +++ b/docsgpt/deploy/docker.py @@ -117,13 +117,17 @@ class Docker: def import_volume(self, volume: str, source: Path, image: str) -> None: """Replace ``volume``'s contents with the tar at ``source``; the volume is created when missing.""" + # Unpack into the container's own filesystem first, so a truncated or corrupt tar fails + # before the live volume is touched rather than halfway through emptying it. It goes under + # /tmp because the image does not run as root and cannot write to /. + staging = "/tmp/docsgpt-restore" + script = ( + f"set -e; rm -rf {staging}; mkdir -p {staging}; tar xf - -C {staging}; " + f"find /data -mindepth 1 -delete; tar cf - -C {staging} . | tar xf - -C /data" + ) with source.open("rb") as handle: self._run( - ["docker", "run", "--rm", "-i", "-v", f"{volume}:/data", image, "sh", "-c", - # Unpack into the container's own filesystem first: a truncated or corrupt tar must - # fail before the live volume is touched, not halfway through emptying it. - "set -e; rm -rf /stage; mkdir /stage; tar xf - -C /stage; " - "find /data -mindepth 1 -delete; tar cf - -C /stage . | tar xf - -C /data"], + ["docker", "run", "--rm", "-i", "-v", f"{volume}:/data", image, "sh", "-c", script], stdin=handle, ) diff --git a/tests/deploy/test_docker.py b/tests/deploy/test_docker.py index 61c15dd8..0f1a3e8b 100644 --- a/tests/deploy/test_docker.py +++ b/tests/deploy/test_docker.py @@ -130,7 +130,8 @@ class TestVolumes: assert args[-2] == "-c" assert "tar xf - -C /data" in args[-1] command = args[-1] - assert command.index("tar xf - -C /stage") < command.index("find /data -mindepth 1 -delete"), ( + assert "mkdir -p /tmp/" in command, "the image does not run as root, so staging goes under /tmp" + assert command.index("tar xf - -C /tmp/") < command.index("find /data -mindepth 1 -delete"), ( "the incoming tar is unpacked outside the volume first, so a corrupt one leaves it alone" ) assert runner.streams[0][1] is not None, "the tar is fed in on stdin"