feat(backup): auto-discover container-side capture uid:gid (drop the literal)
The hardcoded uid:gid in libreportal.backup.files labels was brittle: matched the default PUID in the compose, but a PUID change (or new image version) would drift silently and the next restore would chown to a stale owner. Make it impossible to drift by letting the engine learn the uid at capture time. backup_files.sh: - After a successful tar capture, run `stat -c '%u:%g'` inside the container and write the result to a <host_subdir>.lp-owner sidecar in staging. The sidecar rides in the snapshot alongside the captured tree. - Restore reads it back when the descriptor doesn't pin uid:gid; falls back to 0:0 with a clear notice if missing. - The 5-field form (with explicit uid:gid) is still supported as an override; it wins and skips the sidecar write entirely. Update all 4 current labels to the new 3-field form "<container>:<container_path>:<host_subdir>" (nextcloud, bookstack, gitea, owncloud). Engine handles both formats during the transition. Verified with stubs: 3-field capture writes the sidecar with the discovered 33:33; restore reads it back; 5-field override correctly skips the sidecar write. backup_files.sh parses. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
parent
0f6d39475e
commit
d424473b2e
@ -33,7 +33,7 @@ services:
|
|||||||
libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA
|
libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA
|
||||||
libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA
|
libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA
|
||||||
libreportal.backup.db: "mariadb:bookstack_db:db:"
|
libreportal.backup.db: "mariadb:bookstack_db:db:"
|
||||||
libreportal.backup.files: "bookstack:/config:data:1000:1000"
|
libreportal.backup.files: "bookstack:/config:data"
|
||||||
traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA
|
traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA
|
||||||
# TRAEFIK_PORT_1_BEGIN
|
# TRAEFIK_PORT_1_BEGIN
|
||||||
traefik.http.routers.bookstack-service.entrypoints: web,websecure
|
traefik.http.routers.bookstack-service.entrypoints: web,websecure
|
||||||
|
|||||||
@ -65,7 +65,7 @@ services:
|
|||||||
libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA
|
libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA
|
||||||
libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA
|
libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA
|
||||||
libreportal.backup.db: "sqlite:::data/gitea/gitea/gitea.db"
|
libreportal.backup.db: "sqlite:::data/gitea/gitea/gitea.db"
|
||||||
libreportal.backup.files: "gitea-service:/data:data/gitea:1000:1000"
|
libreportal.backup.files: "gitea-service:/data:data/gitea"
|
||||||
traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA
|
traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA
|
||||||
# TRAEFIK_PORT_1_BEGIN
|
# TRAEFIK_PORT_1_BEGIN
|
||||||
traefik.http.routers.gitea-service.entrypoints: web,websecure
|
traefik.http.routers.gitea-service.entrypoints: web,websecure
|
||||||
|
|||||||
@ -33,7 +33,7 @@ services:
|
|||||||
libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA
|
libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA
|
||||||
libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA
|
libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA
|
||||||
libreportal.backup.db: "mariadb:nextcloud-db:db_data:"
|
libreportal.backup.db: "mariadb:nextcloud-db:db_data:"
|
||||||
libreportal.backup.files: "nextcloud-service:/var/www/html:html:33:33"
|
libreportal.backup.files: "nextcloud-service:/var/www/html:html"
|
||||||
# GLUETUN_OFF_BEGIN
|
# GLUETUN_OFF_BEGIN
|
||||||
networks:
|
networks:
|
||||||
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
|
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
|
||||||
|
|||||||
@ -33,7 +33,7 @@ services:
|
|||||||
libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA
|
libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA
|
||||||
libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA
|
libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA
|
||||||
libreportal.backup.db: "mariadb:owncloud-mariadb:mysql:"
|
libreportal.backup.db: "mariadb:owncloud-mariadb:mysql:"
|
||||||
libreportal.backup.files: "owncloud-service:/mnt/data:files:33:33"
|
libreportal.backup.files: "owncloud-service:/mnt/data:files"
|
||||||
traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA
|
traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA
|
||||||
# TRAEFIK_PORT_1_BEGIN
|
# TRAEFIK_PORT_1_BEGIN
|
||||||
traefik.http.routers.owncloud-service.entrypoints: web,websecure
|
traefik.http.routers.owncloud-service.entrypoints: web,websecure
|
||||||
|
|||||||
@ -20,15 +20,24 @@
|
|||||||
# Declared per app as a compose label (multiple allowed):
|
# Declared per app as a compose label (multiple allowed):
|
||||||
#
|
#
|
||||||
# labels:
|
# labels:
|
||||||
# libreportal.backup.files: "<container>:<container_path>:<host_subdir>:<uid>:<gid>"
|
# libreportal.backup.files: "<container>:<container_path>:<host_subdir>"
|
||||||
#
|
#
|
||||||
# container service to exec/read through
|
# container service to exec/read through
|
||||||
# container_path path inside the container to capture (a bind-mount target)
|
# container_path path inside the container to capture (a bind-mount target)
|
||||||
# host_subdir app-dir-relative dir it maps to (excluded from the snapshot)
|
# host_subdir app-dir-relative dir it maps to (excluded from the snapshot)
|
||||||
# uid:gid ownership to restore the files as (the app's runtime user)
|
#
|
||||||
|
# Ownership for restore is **auto-discovered** at capture time: the engine runs
|
||||||
|
# `stat -c '%u:%g'` inside the container and writes the result to a sidecar
|
||||||
|
# `<host_subdir>.lp-owner` in staging. Restore reads it back — so a PUID/UID
|
||||||
|
# change on the running container is reflected on the next backup with no label
|
||||||
|
# edit needed.
|
||||||
|
#
|
||||||
|
# Legacy 5-field form is still supported as an explicit override:
|
||||||
|
# "<container>:<container_path>:<host_subdir>:<uid>:<gid>"
|
||||||
|
# If uid:gid are present, they override the discovered value.
|
||||||
#
|
#
|
||||||
# Example (Nextcloud):
|
# Example (Nextcloud):
|
||||||
# "nextcloud-service:/var/www/html:html:33:33"
|
# "nextcloud-service:/var/www/html:html"
|
||||||
|
|
||||||
# Staging lives at the app root (never inside an excluded path) so it rides in
|
# Staging lives at the app root (never inside an excluded path) so it rides in
|
||||||
# the snapshot alongside the DB dumps under .lp-backup/.
|
# the snapshot alongside the DB dumps under .lp-backup/.
|
||||||
@ -82,8 +91,24 @@ backupFilesCapture()
|
|||||||
# which the backup user still couldn't read. Hand the staging tree to
|
# which the backup user still couldn't read. Hand the staging tree to
|
||||||
# the backup user so restic can read it; modes are unchanged, so the
|
# the backup user so restic can read it; modes are unchanged, so the
|
||||||
# owner can now read everything. Real ownership is reapplied from the
|
# owner can now read everything. Real ownership is reapplied from the
|
||||||
# descriptor on restore.
|
# discovered-or-overridden uid:gid on restore.
|
||||||
runFileOp chown -R "$docker_install_user":"$docker_install_user" "$stage" 2>/dev/null
|
runFileOp chown -R "$docker_install_user":"$docker_install_user" "$stage" 2>/dev/null
|
||||||
|
|
||||||
|
# Auto-discover the in-container uid:gid and write a sidecar that
|
||||||
|
# rides in the snapshot beside the staging dir. Restore reads this
|
||||||
|
# back, so a PUID/UID change is picked up on the next backup without
|
||||||
|
# any label edit. An explicit uid:gid in the descriptor wins; this
|
||||||
|
# only writes when the descriptor didn't pin them.
|
||||||
|
if [[ -z "$uid" || -z "$gid" ]]; then
|
||||||
|
local meta_file="$app_dir/$backup_files_stage_subdir/$subdir.lp-owner"
|
||||||
|
local discovered
|
||||||
|
discovered=$(docker exec "$container" stat -c '%u:%g' "$cpath" 2>/dev/null)
|
||||||
|
if [[ -n "$discovered" ]]; then
|
||||||
|
echo "$discovered" | runFileWrite "$meta_file" 2>/dev/null
|
||||||
|
runFileOp chown "$docker_install_user":"$docker_install_user" "$meta_file" 2>/dev/null
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
isSuccessful "captured $subdir ($(du -sh "$stage" 2>/dev/null | cut -f1))"
|
isSuccessful "captured $subdir ($(du -sh "$stage" 2>/dev/null | cut -f1))"
|
||||||
else
|
else
|
||||||
isError "capture of $subdir from $container failed"
|
isError "capture of $subdir from $container failed"
|
||||||
@ -125,6 +150,19 @@ restoreFilesRehydratePreStart()
|
|||||||
IFS=':' read -r container cpath subdir uid gid <<< "$desc"
|
IFS=':' read -r container cpath subdir uid gid <<< "$desc"
|
||||||
stage="$app_dir/$backup_files_stage_subdir/$subdir"
|
stage="$app_dir/$backup_files_stage_subdir/$subdir"
|
||||||
[[ -d "$stage" ]] || { isNotice "No captured files for $subdir — skipping"; continue; }
|
[[ -d "$stage" ]] || { isNotice "No captured files for $subdir — skipping"; continue; }
|
||||||
|
|
||||||
|
# If the descriptor didn't pin uid:gid, read the auto-discovered sidecar
|
||||||
|
# written at capture time. Falls back to 0:0 (container root) if the
|
||||||
|
# sidecar is missing — safe but worth a notice so misconfig is visible.
|
||||||
|
if [[ -z "$uid" || -z "$gid" ]]; then
|
||||||
|
local meta_file="$app_dir/$backup_files_stage_subdir/$subdir.lp-owner"
|
||||||
|
if [[ -f "$meta_file" ]]; then
|
||||||
|
local meta; meta=$(cat "$meta_file" 2>/dev/null)
|
||||||
|
IFS=':' read -r uid gid <<< "$meta"
|
||||||
|
else
|
||||||
|
isNotice "No owner sidecar for $subdir — restoring as 0:0 (set uid:gid explicitly or re-capture to fix)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
uid="${uid:-0}"; gid="${gid:-0}"
|
uid="${uid:-0}"; gid="${gid:-0}"
|
||||||
|
|
||||||
isNotice "Restoring $subdir as $uid:$gid — via container"
|
isNotice "Restoring $subdir as $uid:$gid — via container"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user