Linuxでのファイルバックアップの基本と応用

ファイルバックアップは、固定名のarchiveとchecksumを直接上書きせず、同じrunの一時領域で作成・検査してから採用します。既存のarchive、checksum、復元先のどれか一つでも存在すれば、sentinelをbyte単位で保護するため変更前に停止します。

目次

3つの不存在を開始条件にする

GNU tar・gzip・sha256sum・Python 3とrenameat2(RENAME_NOREPLACE)を前提にします。sourceは通常の静止した設定ディレクトリとし、稼働中更新がある場合はsnapshotを先に取得します。4ブロックは同じBashセッションで順に実行します。

set -Eeuo pipefail
source=/srv/app/config
archive=/srv/backup/config-20260717.tar.gz
checksum="$archive.sha256"
restore=/srv/restore-check
backup_parent=$(dirname -- "$archive")
restore_parent=$(dirname -- "$restore")
test -d "$source" && test ! -L "$source"
test -d "$backup_parent" && test -d "$restore_parent"
test ! -e "$archive" || { printf 'STOP: archive exists\n' >&2; exit 20; }
test ! -e "$checksum" || { printf 'STOP: checksum exists\n' >&2; exit 21; }
test ! -e "$restore" || { printf 'STOP: restore destination exists\n' >&2; exit 22; }
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固定の一時archiveを検査する

archiveとchecksumは最終名の親にあるmktemp領域へ作ります。gzip test、member一覧の絶対パス・親遡及拒否、checksum、隔離展開、sourceとのNUL-safe inventory/hash一致が全部通るまで最終名へ移しません。

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 "$backup_parent" ".ittrip-109.$run_id.XXXXXXXX")
printf '%s\n' "$run_id" > "$work/.owner"
tmp_archive="$work/$(basename -- "$archive")"
tmp_checksum="$work/$(basename -- "$checksum")"
source_parent=$(dirname -- "$source")
source_name=$(basename -- "$source")
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"
archive_sha=$(sha256sum -- "$tmp_archive"); archive_sha=${archive_sha%% *}; archive_sha=${archive_sha#\\}
printf '%s  %s\n' "$archive_sha" "$(basename -- "$archive")" > "$tmp_checksum"
( cd -- "$work" && sha256sum --check "$(basename -- "$tmp_checksum")" )
mkdir -- "$work/extract"
tar --extract --file="$tmp_archive" --directory="$work/extract"
tree_manifest "$source" "$work/source.manifest" ''
tree_manifest "$work/extract/$source_name" "$work/extract.manifest" ''
cmp --silent "$work/source.manifest" "$work/extract.manifest"

archiveとchecksumをno-clobberで確定する

2ファイルを同時renameするAPIはないため、両方にRENAME_NOREPLACEを使い、checksum側の競合時はこのrunのarchive hashを再確認してquarantineへ戻します。既存ファイルを削除するfallbackはありません。

archive_committed=0
atomic_noreplace "$tmp_archive" "$archive"
archive_committed=1
if ! atomic_noreplace "$tmp_checksum" "$checksum"; then
  quarantine="$archive.failed-$run_id"
  test "$archive_committed" -eq 1 && test ! -e "$quarantine"
  current=$(sha256sum -- "$archive"); current=${current%% *}; current=${current#\\}
  test "$current" = "$archive_sha"
  atomic_noreplace "$archive" "$quarantine"
  printf 'checksum commit failed; run-owned archive quarantined at %s\n' "$quarantine" >&2
  exit 31
fi
( cd -- "$backup_parent" && sha256sum --check "$(basename -- "$checksum")" )
test "$(sha256sum -- "$archive" | sed 's/^\\//;s/ .*//')" = "$archive_sha"
printf 'archive_pair_committed run_id=%s sha256=%s\n' "$run_id" "$archive_sha"

隔離復元で内容を照合する

復元先も不存在を必須にし、検査済みextract treeをatomicに採用します。sourceとrestoreのdirectory・symlink・regular-file SHA-256が一致した後だけacceptとし、run-owned workはowner token確認後に同一filesystemのretention pathへno-clobberで移し、監査後の破棄は管理者承認を伴う別手順へ分離します。

test ! -e "$restore"
atomic_noreplace "$work/extract" "$restore"
test ! -e "$work/extract" && test -d "$restore/$source_name"
tree_manifest "$restore/$source_name" "$work/restored.manifest" ''
cmp --silent "$work/source.manifest" "$work/restored.manifest"
receipt="$backup_parent/.ittrip-109-$run_id.receipt"
( umask 077; set -o noclobber; printf 'archive=%s\nchecksum=%s\nrestore=%s\nsha256=%s\n' "$archive" "$checksum" "$restore" "$archive_sha" > "$receipt" )
test "$(cat -- "$work/.owner")" = "$run_id"
retained_work="$work.completed-$run_id"
test ! -e "$retained_work"
atomic_noreplace "$work" "$retained_work"
printf 'run-owned evidence retained=%s; disposal requires a separate approved retention procedure\n' "$retained_work"
printf 'accepted archive=%s checksum=%s isolated_restore=%s receipt=%s\n' "$archive" "$checksum" "$restore" "$receipt"

受入テスト

既存archive、既存checksum、既存restoreの各sentinel fixtureで終了し、各bytes/hashが不変であることを確認します。clean runではtar list、gzip test、sha256sum –check、隔離展開、inventory/hash比較が全て0で、receiptの4パスとarchive SHA-256が実体と一致することを確認します。

運用上の注意

同一ディスクの一世代だけでは媒体故障や暗号化被害に備えられません。検証済みpairを別媒体へ複製し、保持期間と復元演習を別途設計してください。未知のarchiveを本番rootへ直接展開してはいけません。

公式情報・参考資料

ファイルバックアップの構文と制約は、本文末の一次資料と対象環境のlocal helpで照合します。ファイルバックアップの記事確認日は2026年7月17日で、版が異なる場合はoption、default、終了statusの差を先に確認してください。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次