ディレクトリ圧縮では、固定archiveを直接truncateせず、復元先へ既存内容を混在させません。archiveとrestore destinationの双方を不存在gateにし、run-owned stagingでgzip test・member list・隔離extract・inventory/hashを完了してからRENAME_NOREPLACEで確定します。
archiveとrestore先の不存在を確認する
どちらかが存在すればsentinelを保護して停止します。sourceは静止したdirectoryとし、更新中ならsnapshotを先に作ります。4ブロックは同じBashセッションで実行します。
set -Eeuo pipefail
source=/srv/app/config
archive=/srv/backup/config-20260717.tar.gz
restore=/srv/restore/config-check
archive_parent=$(dirname -- "$archive")
restore_parent=$(dirname -- "$restore")
test -d "$source" && test ! -L "$source"
test -d "$archive_parent" && test -d "$restore_parent"
test ! -e "$archive" || { printf 'STOP: archive exists\n' >&2; exit 20; }
test ! -e "$restore" || { printf 'STOP: restore destination exists\n' >&2; exit 21; }
command -v python3 >/dev/null
tree_manifest() {
local tree=$1 output=$2 marker_name=${3-}
(
cd -- "$tree"
find . -xdev -mindepth 1 -type d ! -name "$marker_name" -printf 'd\t%m\t%U\t%G\t%P\0' | sort -z
find . -xdev -mindepth 1 -type l ! -name "$marker_name" -printf 'l\t%m\t%U\t%G\t%P\t%l\0' | sort -z
find . -xdev -mindepth 1 -type f ! -name "$marker_name" -print0 | sort -z | xargs -0 -r sha256sum --zero --
) > "$output"
}
run-owned archiveと隔離extractを検証する
最終archiveと同じfilesystemのmktemp内で作成し、gzip test、絶対path・.. member拒否、隔離extract、sourceとのNUL-safe inventory/hash比較を行います。検証前に本番名やrestore先を作りません。
atomic_noreplace() {
python3 - "$1" "$2" <<'PY'
import ctypes, os, sys
src, dst = map(os.fsencode, sys.argv[1:])
libc = ctypes.CDLL(None, use_errno=True)
try:
renameat2 = libc.renameat2
except AttributeError:
raise SystemExit("renameat2 is unavailable; no non-atomic fallback is allowed")
renameat2.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_uint]
if renameat2(-100, src, -100, dst, 1) != 0: # RENAME_NOREPLACE
number = ctypes.get_errno()
raise OSError(number, os.strerror(number), os.fsdecode(dst))
PY
}
run_id=$(date -u +%Y%m%dT%H%M%SZ)-$$-$RANDOM
work=$(mktemp -d -p "$archive_parent" ".ittrip-129.$run_id.XXXXXXXX")
printf '%s\n' "$run_id" > "$work/.owner"
restore_work=$(mktemp -d -p "$restore_parent" ".ittrip-129-restore.$run_id.XXXXXXXX")
printf '%s\n' "$run_id" > "$restore_work/.owner"
tmp_archive="$work/$(basename -- "$archive")"
extract_stage="$restore_work/extract"
source_parent=$(dirname -- "$source")
source_name=$(basename -- "$source")
mkdir -- "$extract_stage"
tar --create --gzip --file="$tmp_archive" --directory="$source_parent" -- "$source_name"
gzip --test "$tmp_archive"
tar --list --file="$tmp_archive" > "$work/members.txt"
awk '/^\// || /(^|\/)\.\.(\/|$)/ { bad=1 } END { exit bad }' "$work/members.txt"
tar --extract --file="$tmp_archive" --directory="$extract_stage"
tree_manifest "$source" "$work/source.manifest" ''
tree_manifest "$extract_stage/$source_name" "$work/extract.manifest" ''
cmp --silent "$work/source.manifest" "$work/extract.manifest"
archive_sha=$(sha256sum -- "$tmp_archive"); archive_sha=${archive_sha%% *}; archive_sha=${archive_sha#\\}
2つの新規成果物をno-clobberで確定する
archiveを採用後にrestore側で競合した場合は、このrunのarchive hashを確認してquarantineへ戻します。両方が確定して初めてreceiptを作り、archive SHA-256とrestore manifest SHA-256を結びます。
atomic_noreplace "$tmp_archive" "$archive"
if ! atomic_noreplace "$extract_stage" "$restore"; then
archive_quarantine="$archive.failed-$run_id"
test ! -e "$archive_quarantine"
test "$(sha256sum -- "$archive" | sed 's/^\\//;s/ .*//')" = "$archive_sha"
atomic_noreplace "$archive" "$archive_quarantine"
printf 'restore adopt failed; run-owned archive quarantined\n' >&2
exit 31
fi
test "$(sha256sum -- "$archive" | sed 's/^\\//;s/ .*//')" = "$archive_sha"
tree_manifest "$restore/$source_name" "$work/final.manifest" ''
cmp --silent "$work/source.manifest" "$work/final.manifest"
restore_sha=$(sha256sum -- "$work/final.manifest"); restore_sha=${restore_sha%% *}; restore_sha=${restore_sha#\\}
receipt="$archive_parent/.ittrip-129-$run_id.receipt"
( umask 077; set -o noclobber; printf 'archive=%s\narchive_sha=%s\nrestore=%s\nrestore_sha=%s\nrun_id=%s\n' "$archive" "$archive_sha" "$restore" "$restore_sha" "$run_id" > "$receipt" )
test "$(cat -- "$work/.owner")" = "$run_id"
retained_archive_work="$work.completed-$run_id"
test ! -e "$retained_archive_work"
atomic_noreplace "$work" "$retained_archive_work"
test "$(cat -- "$restore_work/.owner")" = "$run_id"
retained_restore_work="$restore_work.completed-$run_id"
test ! -e "$retained_restore_work"
atomic_noreplace "$restore_work" "$retained_restore_work"
printf 'run-owned evidence retained archive_work=%s restore_work=%s; disposal requires a separate approved retention procedure\n' "$retained_archive_work" "$retained_restore_work"
exact stateだけを復旧する
復旧時は両hashとpathを再bindします。archiveを先にquarantineし、restore移動に失敗した場合はarchiveをatomicに元へ戻します。既存の第三状態を削除するfallbackはありません。
# 両方がこのrunのexact stateである場合だけquarantineし、片側失敗ならarchiveを戻す。
grep -Fx "archive=$archive" "$receipt" >/dev/null
grep -Fx "archive_sha=$archive_sha" "$receipt" >/dev/null
grep -Fx "restore=$restore" "$receipt" >/dev/null
grep -Fx "restore_sha=$restore_sha" "$receipt" >/dev/null
test "$(sha256sum -- "$archive" | sed 's/^\\//;s/ .*//')" = "$archive_sha"
rollback_manifest=$(mktemp -p "$archive_parent" ".ittrip-129-rollback.$run_id.XXXXXXXX")
tree_manifest "$restore/$source_name" "$rollback_manifest" ''
test "$(sha256sum -- "$rollback_manifest" | sed 's/^\\//;s/ .*//')" = "$restore_sha"
rm -- "$rollback_manifest"
archive_quarantine="$archive.review-$run_id"
restore_quarantine="$restore.review-$run_id"
test ! -e "$archive_quarantine" && test ! -e "$restore_quarantine"
atomic_noreplace "$archive" "$archive_quarantine"
if ! atomic_noreplace "$restore" "$restore_quarantine"; then
atomic_noreplace "$archive_quarantine" "$archive"
exit 41
fi
test ! -e "$archive" && test ! -e "$restore"
printf 'pre-state restored; run-owned outputs quarantined\n'
受入条件
既存archive fixtureと既存destination fixtureの各テストでbytes/hashが不変です。clean runではgzip、list、extractが0終了し、sourceとrestore/configのinventory/hashが一致します。archiveまたはrestoreの途中失敗時に既存物へ上書き・mixしないことも確認します。
圧縮とバックアップを区別する
圧縮率が高くても復元できなければbackupではありません。別媒体保管、権限、暗号化、保持期間、定期復元試験を別途設計し、本番rootへの直接extractは行わないでください。
公式情報・参考資料
ディレクトリ圧縮の構文と制約は、本文末の一次資料と対象環境のlocal helpで照合します。ディレクトリ圧縮の記事確認日は2026年7月17日で、版が異なる場合はoption、default、終了statusの差を先に確認してください。

コメント