Linuxでディレクトリを効率的にコピーする複数の方法

tarストリームでディレクトリをコピーする場合でも、既存targetへ直接展開しません。set -o pipefailだけに依存せず、run-owned archiveを一度ファイルとして作り、list・compare・extract・inventory/hashを段階ごとに検証してから新規targetへatomicに採用します。

目次

target不存在とpipefailを固定する

sourceは静止したregular directory、targetは不存在を必須にします。既存sentinelがあれば変更前に停止します。4ブロックは同じBashセッションで順に実行します。

set -Eeuo pipefail
set -o pipefail
source=/srv/lab/source
target=/srv/lab/target-tar
parent=$(dirname -- "$target")
test -d "$source" && test ! -L "$source"
test -d "$parent" && test ! -L "$parent"
test ! -e "$target" || { printf 'STOP: target-tar exists\n' >&2; exit 20; }
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"
}

検証済みarchiveを介して展開する

targetの親にmktempで作ったwork内へarchiveと空stageを作ります。tar create、member path検査、sourceとのtar compare、extract、NUL-safe inventory/hash比較を別々の終了コードで検査するため、source側失敗をpipelineが隠しません。

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 "$parent" ".ittrip-127.$run_id.XXXXXXXX")
printf '%s\n' "$run_id" > "$work/.owner"
archive="$work/source.tar"
stage="$work/stage"
mkdir -- "$stage"
tar --create --file="$archive" --directory="$source" -- .
tar --list --file="$archive" > "$work/members.txt"
awk '/^\// || /(^|\/)\.\.(\/|$)/ { bad=1 } END { exit bad }' "$work/members.txt"
tar --compare --file="$archive" --directory="$source" -- .
tar --extract --file="$archive" --directory="$stage"
tree_manifest "$source" "$work/source.manifest" ''
tree_manifest "$stage" "$work/stage.manifest" ''
cmp --silent "$work/source.manifest" "$work/stage.manifest"

空stageだけをatomic adoptする

stageがsourceと一致した後、RENAME_NOREPLACEでtargetへ採用します。承認後にtargetが出現した場合も上書きせず、成功時のmanifest hashとrun_idをreceiptに保存します。

atomic_noreplace "$stage" "$target"
test ! -e "$stage" && test -d "$target"
tree_manifest "$target" "$work/target.manifest" ''
cmp --silent "$work/source.manifest" "$work/target.manifest"
manifest_sha=$(sha256sum -- "$work/target.manifest"); manifest_sha=${manifest_sha%% *}; manifest_sha=${manifest_sha#\\}
receipt="$parent/.ittrip-127-$run_id.receipt"
( umask 077; set -o noclobber; printf 'target=%s\nmanifest_sha=%s\nrun_id=%s\n' "$target" "$manifest_sha" "$run_id" > "$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 verified-archive copy target=%s manifest=%s\n' "$target" "$manifest_sha"

run-owned targetだけをquarantineする

復旧時はreceiptと現在manifestを再照合します。後続変更や第三の状態では停止し、完全一致する今回のtargetだけをno-clobber quarantineへ移して開始前の不存在へ戻します。

test -f "$receipt" && test ! -L "$receipt"
grep -Fx "target=$target" "$receipt" >/dev/null
grep -Fx "run_id=$run_id" "$receipt" >/dev/null
rollback_manifest=$(mktemp -p "$parent" ".ittrip-127-rollback.$run_id.XXXXXXXX")
tree_manifest "$target" "$rollback_manifest" ''
rollback_sha=$(sha256sum -- "$rollback_manifest"); rollback_sha=${rollback_sha%% *}; rollback_sha=${rollback_sha#\\}
rm -- "$rollback_manifest"
grep -Fx "manifest_sha=$rollback_sha" "$receipt" >/dev/null
quarantine="$target.failed-$run_id"
test ! -e "$quarantine"
atomic_noreplace "$target" "$quarantine"
test ! -e "$target" && test -d "$quarantine"
rm -- "$receipt"

受入条件

既存target sentinelでsource tar作成を開始せずsentinel hashが不変であること、壊したsource fixtureではtargetが作られないこと、clean runではsource・stage・targetのinventory/hashが一致することを確認します。

方式選択

同一hostの単純複製ならcp –archive、搬送可能な成果物が必要なら検証済みtar、継続差分ならrsync等を検討します。ACL・xattr・hard linkの保持はtar実装とoptionを明示し、暗黙に完全保持とみなしません。

公式情報・参考資料

ディレクトリコピー手段の選択の構文と制約は、本文末の一次資料と対象環境のlocal helpで照合します。ディレクトリコピー手段の選択の記事確認日は2026年7月17日で、版が異なる場合はoption、default、終了statusの差を先に確認してください。

この記事を書いた人

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

コメント

コメントする

目次