fix an issue with link-dest pointing towards the wrong directory
This commit is contained in:
@@ -3,7 +3,6 @@ from __future__ import annotations
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from ..commands.retention_apply import run_retention_apply
|
|
||||||
from ..config.load import load_global_config, load_host_config
|
from ..config.load import load_global_config, load_host_config
|
||||||
from ..config.merge import build_effective_config
|
from ..config.merge import build_effective_config
|
||||||
from ..errors import ConfigError
|
from ..errors import ConfigError
|
||||||
@@ -19,6 +18,7 @@ from ..snapshot import (
|
|||||||
snapshot_dir_name,
|
snapshot_dir_name,
|
||||||
utc_now,
|
utc_now,
|
||||||
)
|
)
|
||||||
|
from ..snapshot_meta import read_snapshot_meta
|
||||||
from ..util import ensure_dir, realpath_startswith, sanitize_host, write_yaml_atomic
|
from ..util import ensure_dir, realpath_startswith, sanitize_host, write_yaml_atomic
|
||||||
|
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ def _find_latest_snapshot(parent: Path) -> Path | None:
|
|||||||
def select_scheduled_base(dirs: HostBackupDirs) -> Path | None:
|
def select_scheduled_base(dirs: HostBackupDirs) -> Path | None:
|
||||||
"""
|
"""
|
||||||
Base selection rule:
|
Base selection rule:
|
||||||
scheduled -> manual -> none
|
scheduled -> manual -> none
|
||||||
"""
|
"""
|
||||||
base = _find_latest_snapshot(dirs.scheduled)
|
base = _find_latest_snapshot(dirs.scheduled)
|
||||||
if base is not None:
|
if base is not None:
|
||||||
@@ -55,36 +55,37 @@ def select_scheduled_base(dirs: HostBackupDirs) -> Path | None:
|
|||||||
return _find_latest_snapshot(dirs.manual)
|
return _find_latest_snapshot(dirs.manual)
|
||||||
|
|
||||||
|
|
||||||
def _base_meta_from_path(base_dir: Path | None) -> dict[str, Any] | None:
|
def _base_meta_from_path(base_dir: Path | None, link_dest: str | None) -> dict[str, Any] | None:
|
||||||
|
"""
|
||||||
|
Build base metadata for meta.yaml.
|
||||||
|
|
||||||
|
Important: link_dest is the actual rsync --link-dest directory.
|
||||||
|
For our snapshot layout, that must be "<snapshot_dir>/data".
|
||||||
|
"""
|
||||||
if base_dir is None:
|
if base_dir is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
kind = base_dir.parent.name
|
kind = base_dir.parent.name
|
||||||
if kind not in ("scheduled", "manual"):
|
if kind not in ("scheduled", "manual"):
|
||||||
|
# Should not happen with current selection logic, but keep meta robust.
|
||||||
kind = "unknown"
|
kind = "unknown"
|
||||||
|
|
||||||
|
base_meta = read_snapshot_meta(base_dir)
|
||||||
|
base_id = base_meta.get("id") if isinstance(base_meta.get("id"), str) else None
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"kind": kind,
|
"kind": kind,
|
||||||
"dirname": base_dir.name,
|
"dirname": base_dir.name,
|
||||||
"id": None,
|
"id": base_id,
|
||||||
"path": None,
|
"path": link_dest,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def run_scheduled(
|
def run_scheduled(prefix: Path, host: str, dry_run: bool, prune: bool = False, prune_max_delete: int | None = None, prune_protect_bases: bool = False, ) -> dict[str, Any]:
|
||||||
prefix: Path,
|
|
||||||
host: str,
|
|
||||||
dry_run: bool,
|
|
||||||
prune: bool = False,
|
|
||||||
prune_max_delete: int = 10,
|
|
||||||
prune_protect_bases: bool = False,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
host = sanitize_host(host)
|
host = sanitize_host(host)
|
||||||
paths = PobsyncPaths(home=prefix)
|
paths = PobsyncPaths(home=prefix)
|
||||||
|
|
||||||
if prune_max_delete < 0:
|
|
||||||
raise ConfigError("--prune-max-delete must be >= 0")
|
|
||||||
|
|
||||||
# Load and merge config
|
# Load and merge config
|
||||||
global_cfg = load_global_config(paths.global_config_path)
|
global_cfg = load_global_config(paths.global_config_path)
|
||||||
host_cfg = load_host_config(paths.hosts_dir / f"{host}.yaml")
|
host_cfg = load_host_config(paths.hosts_dir / f"{host}.yaml")
|
||||||
@@ -102,7 +103,10 @@ def run_scheduled(
|
|||||||
|
|
||||||
# Base snapshot (absolute path)
|
# Base snapshot (absolute path)
|
||||||
base_dir = select_scheduled_base(dirs)
|
base_dir = select_scheduled_base(dirs)
|
||||||
link_dest = str(base_dir) if base_dir else None
|
|
||||||
|
# BUGFIX: rsync --link-dest must point at the snapshot "data" root, not the snapshot dir itself.
|
||||||
|
# Our destination root is "<incomplete>/data/", so the base root must be "<base>/data/".
|
||||||
|
link_dest = str(base_dir / "data") if base_dir else None
|
||||||
|
|
||||||
ssh_cfg = cfg.get("ssh", {}) or {}
|
ssh_cfg = cfg.get("ssh", {}) or {}
|
||||||
rsync_cfg = cfg.get("rsync", {}) or {}
|
rsync_cfg = cfg.get("rsync", {}) or {}
|
||||||
@@ -186,6 +190,7 @@ def run_scheduled(
|
|||||||
meta_path = meta_dir / "meta.yaml"
|
meta_path = meta_dir / "meta.yaml"
|
||||||
log_path = meta_dir / "rsync.log"
|
log_path = meta_dir / "rsync.log"
|
||||||
|
|
||||||
|
# Pre-build command so we can record it in metadata.
|
||||||
dest = str(data_dir) + "/"
|
dest = str(data_dir) + "/"
|
||||||
cmd = build_rsync_command(
|
cmd = build_rsync_command(
|
||||||
rsync_binary=str(rsync_binary),
|
rsync_binary=str(rsync_binary),
|
||||||
@@ -211,8 +216,9 @@ def run_scheduled(
|
|||||||
"started_at": format_iso_z(ts),
|
"started_at": format_iso_z(ts),
|
||||||
"ended_at": None,
|
"ended_at": None,
|
||||||
"duration_seconds": None,
|
"duration_seconds": None,
|
||||||
"base": _base_meta_from_path(base_dir),
|
"base": _base_meta_from_path(base_dir, link_dest),
|
||||||
"rsync": {"exit_code": None, "command": cmd, "stats": {}},
|
"rsync": {"exit_code": None, "command": cmd, "stats": {}},
|
||||||
|
# Keep existing fields for future expansion / compatibility with current structure.
|
||||||
"overrides": {"includes": [], "excludes": [], "base": None},
|
"overrides": {"includes": [], "excludes": [], "base": None},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,7 +259,7 @@ def run_scheduled(
|
|||||||
final_dir = dirs.scheduled / snap_name
|
final_dir = dirs.scheduled / snap_name
|
||||||
incomplete_dir.rename(final_dir)
|
incomplete_dir.rename(final_dir)
|
||||||
|
|
||||||
out: dict[str, Any] = {
|
return {
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"dry_run": False,
|
"dry_run": False,
|
||||||
"host": host,
|
"host": host,
|
||||||
@@ -262,28 +268,3 @@ def run_scheduled(
|
|||||||
"rsync": {"exit_code": result.exit_code},
|
"rsync": {"exit_code": result.exit_code},
|
||||||
}
|
}
|
||||||
|
|
||||||
if prune:
|
|
||||||
prune_result = run_retention_apply(
|
|
||||||
prefix=prefix,
|
|
||||||
host=host,
|
|
||||||
kind="scheduled",
|
|
||||||
protect_bases=bool(prune_protect_bases),
|
|
||||||
yes=True,
|
|
||||||
max_delete=int(prune_max_delete),
|
|
||||||
acquire_lock=False, # already under host lock
|
|
||||||
)
|
|
||||||
# Merge actions for human output
|
|
||||||
actions = []
|
|
||||||
if isinstance(prune_result.get("actions"), list):
|
|
||||||
actions.extend([f"prune: {a}" for a in prune_result["actions"]])
|
|
||||||
if actions:
|
|
||||||
out["actions"] = out.get("actions", []) + actions
|
|
||||||
out["prune"] = {
|
|
||||||
"ok": bool(prune_result.get("ok")),
|
|
||||||
"deleted": prune_result.get("deleted", []),
|
|
||||||
"max_delete": prune_result.get("max_delete"),
|
|
||||||
"protect_bases": prune_result.get("protect_bases"),
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user