2026-08-17 05:25:54 +02:00
|
|
|
"""Restore a volume's file tree by writing into its mountpoint.
|
|
|
|
|
|
|
|
|
|
That shortcut only holds for a plain local volume, where the mountpoint *is*
|
|
|
|
|
the storage. A volume with driver options - NFS, a bind device, tmpfs - keeps
|
|
|
|
|
the same ``/var/lib/docker/volumes/<name>/_data`` path, but docker mounts the
|
|
|
|
|
real backing store over it on demand and unmounts it again when the last
|
|
|
|
|
consumer stops. Writing there while nothing has it mounted lands in the empty
|
|
|
|
|
directory underneath, is hidden by the next mount, and rsync reports success.
|
|
|
|
|
"""
|
|
|
|
|
|
2025-12-26 18:13:26 +01:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
import sys
|
2026-08-18 01:51:18 +02:00
|
|
|
from pathlib import Path
|
2025-12-26 18:13:26 +01:00
|
|
|
|
2026-08-17 05:25:54 +02:00
|
|
|
from .run import docker_volume_exists, run, stdout_of
|
|
|
|
|
|
|
|
|
|
INSPECT_FORMAT = (
|
|
|
|
|
"{{ .Mountpoint }}|{{ .Driver }}|{{ if .Options }}opts{{ else }}plain{{ end }}"
|
|
|
|
|
)
|
2025-12-26 18:13:26 +01:00
|
|
|
|
|
|
|
|
|
2026-07-12 14:53:55 +02:00
|
|
|
def restore_volume_files(volume_name: str, backup_files_dir: str) -> int:
|
2026-08-18 01:51:18 +02:00
|
|
|
if not Path(backup_files_dir).is_dir():
|
2025-12-26 18:13:26 +01:00
|
|
|
print(f"ERROR: backup files dir not found: {backup_files_dir}", file=sys.stderr)
|
|
|
|
|
return 2
|
|
|
|
|
|
|
|
|
|
if not docker_volume_exists(volume_name):
|
|
|
|
|
print(f"Volume {volume_name} does not exist. Creating...")
|
|
|
|
|
run(["docker", "volume", "create", volume_name])
|
|
|
|
|
else:
|
|
|
|
|
print(f"Volume {volume_name} already exists.")
|
|
|
|
|
|
2026-07-12 14:53:55 +02:00
|
|
|
cp = run(
|
2026-08-17 05:25:54 +02:00
|
|
|
["docker", "volume", "inspect", "--format", INSPECT_FORMAT, volume_name],
|
2026-07-12 14:53:55 +02:00
|
|
|
capture=True,
|
2025-12-26 18:13:26 +01:00
|
|
|
)
|
2026-08-17 05:25:54 +02:00
|
|
|
fields = stdout_of(cp).split("|")
|
|
|
|
|
mountpoint = fields[0] if fields else ""
|
2026-07-12 14:53:55 +02:00
|
|
|
if not mountpoint:
|
|
|
|
|
print(
|
|
|
|
|
f"ERROR: could not resolve mountpoint for volume {volume_name}",
|
|
|
|
|
file=sys.stderr,
|
|
|
|
|
)
|
|
|
|
|
return 2
|
|
|
|
|
|
2026-08-18 01:51:18 +02:00
|
|
|
driver, options = ([*fields, "local", "plain"])[1:3]
|
2026-08-17 05:25:54 +02:00
|
|
|
if (driver != "local" or options == "opts") and not os.path.ismount(mountpoint):
|
|
|
|
|
print(
|
|
|
|
|
f"ERROR: volume {volume_name} has a backing store of its own "
|
|
|
|
|
f"(driver {driver}) but nothing has it mounted; writing to "
|
|
|
|
|
f"{mountpoint} now would land under the mount and be lost. "
|
|
|
|
|
"Start a container that mounts the volume, then restore again.",
|
|
|
|
|
file=sys.stderr,
|
|
|
|
|
)
|
|
|
|
|
return 2
|
|
|
|
|
|
2026-08-18 01:51:18 +02:00
|
|
|
# rsync reads "dir/" as its contents and "dir" as the directory itself.
|
|
|
|
|
src = f"{Path(backup_files_dir)}{os.sep}"
|
|
|
|
|
dest = f"{Path(mountpoint)}{os.sep}"
|
2026-07-12 14:53:55 +02:00
|
|
|
run(["rsync", "-avv", "--delete", src, dest])
|
2025-12-26 18:13:26 +01:00
|
|
|
print("File restore complete.")
|
|
|
|
|
return 0
|