mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-13 20:14:16 +00:00
feat: docsgpt backup and docsgpt restore
`docsgpt backup` writes one archive holding a pg_dump of the database, a tar of each data volume and a manifest of what it came from; `docsgpt restore` puts it back over an install. The settings file is left out unless --with-settings asks for it, since it holds the install's secrets, and a backup taken with a newer DocsGPT is refused without --force. The volume tars go through the image the install already runs, so a backup pulls nothing extra, and compose calls can now redirect stdout and stdin so the dump never passes through this process.
This commit is contained in:
1 parent
4abd2c0c9f
commit
fe68fec69e
9 files changed
+489
-7
No files matched your search
@@ -22,10 +22,16 @@ class DeployError(Exception):
|
||||
"""A problem the user can act on; the command prints it without a traceback."""
|
||||
|
||||
|
||||
def run(args: Sequence[str], *, cwd: Optional[Path] = None, capture: bool = False, check: bool = True):
|
||||
"""Run a command, streaming its output unless ``capture``; with ``check`` a failure raises DeployError."""
|
||||
def run(args: Sequence[str], *, cwd: Optional[Path] = None, capture: bool = False, check: bool = True,
|
||||
stdout=None, stdin=None):
|
||||
"""Run a command, streaming its output unless ``capture`` or a file is given for ``stdout``.
|
||||
|
||||
``stdout`` and ``stdin`` take open files, so a dump goes straight to disk and
|
||||
back in again without passing through this process.
|
||||
"""
|
||||
streams = {"capture_output": True} if capture else {"stdout": stdout, "stdin": stdin}
|
||||
try:
|
||||
result = subprocess.run(list(args), cwd=cwd, text=True, capture_output=capture, check=False)
|
||||
result = subprocess.run(list(args), cwd=cwd, text=True, check=False, **streams)
|
||||
except FileNotFoundError as exc:
|
||||
raise DeployError(f"{args[0]} is not installed or not on PATH") from exc
|
||||
if check and result.returncode != 0:
|
||||
@@ -94,9 +100,29 @@ class Docker:
|
||||
raise DeployError("Docker is not running. Start it with `sudo systemctl start docker` and run this again.")
|
||||
raise DeployError("Docker is not running. Start Docker Desktop and run this again.")
|
||||
|
||||
def compose(self, directory: Path, *args: str, capture: bool = False, check: bool = True):
|
||||
def compose(self, directory: Path, *args: str, capture: bool = False, check: bool = True,
|
||||
stdout=None, stdin=None):
|
||||
"""``docker compose <args>`` in ``directory``, which holds the Compose file and its ``.env``."""
|
||||
return self._run(["docker", "compose", *args], cwd=directory, capture=capture, check=check)
|
||||
return self._run(["docker", "compose", *args], cwd=directory, capture=capture, check=check,
|
||||
stdout=stdout, stdin=stdin)
|
||||
|
||||
def export_volume(self, volume: str, dest: Path, image: str) -> None:
|
||||
"""Write ``volume`` to ``dest`` as a tar, through an image the install already has."""
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
with dest.open("wb") as handle:
|
||||
self._run(
|
||||
["docker", "run", "--rm", "-v", f"{volume}:/data:ro", image, "tar", "cf", "-", "-C", "/data", "."],
|
||||
stdout=handle,
|
||||
)
|
||||
|
||||
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."""
|
||||
with source.open("rb") as handle:
|
||||
self._run(
|
||||
["docker", "run", "--rm", "-i", "-v", f"{volume}:/data", image,
|
||||
"sh", "-c", "find /data -mindepth 1 -delete && tar xf - -C /data"],
|
||||
stdin=handle,
|
||||
)
|
||||
|
||||
def volume_exists(self, name: str) -> bool:
|
||||
return self._run(["docker", "volume", "inspect", name], capture=True, check=False).returncode == 0
|
||||
|
||||
Reference in new issue
Block a user