Linuxでディレクトリの階層を指定してコピーする方法

ディレクトリ階層だけをコピーするときは、既存rootへmkdir -pを繰り返してはいけません。新規stageへsourceの相対directory名だけをNUL-safeに作成し、regular fileが0件であることを検証してから新規targetへatomicに採用します。

目次

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

sourceとtargetは別rootに固定し、targetが空に見えても存在すれば停止します。directory名は改行を含められるため、一覧はNUL区切りで比較します。4ブロックは同じBashセッションで実行します。

set -Eeuo pipefail
source=/srv/lab/source
target=/srv/lab/structure-copy
parent=$(dirname -- "$target")
test -d "$source" && test ! -L "$source"
test -d "$parent" && test ! -L "$parent"
test ! -e "$target" || { printf 'STOP: structure-copy root exists\n' >&2; exit 20; }
command -v python3 >/dev/null
directory_manifest() {
  ( cd -- "$1"; find . -xdev -type d -printf '%P\0' | sort -z ) > "$2"
}

run-owned stageへ一度だけ階層を作る

targetの親にmktempで作った空stageだけへmkdirします。mkdir -pはstage内部で親を補う目的に限定され、既存destinationへのsilent mergeには使いません。

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-117.$run_id.XXXXXXXX")
printf '%s\n' "$run_id" > "$work/.owner"
stage="$work/stage"
mkdir -- "$stage"
directory_manifest "$source" "$work/source.directories"
while IFS= read -r -d '' relative; do
  test -z "$relative" && continue
  mkdir -p -- "$stage/$relative"
done < "$work/source.directories"
directory_manifest "$stage" "$work/stage.directories"
cmp --silent "$work/source.directories" "$work/stage.directories"
test "$(find "$stage" -xdev -type f -printf x | wc -c)" -eq 0
test "$(find "$stage" -xdev ! -type d -printf x | wc -c)" -eq 0

directory一覧とfile 0件を検証して採用する

sourceとstageのdirectory listがbyte一致し、stageに非directoryが0件であることを確認します。採用はRENAME_NOREPLACEで行い、同名targetが途中で出現しても上書きしません。成功後はreceiptを残し、owner tokenが一致するworkだけを同一filesystemのretention pathへno-clobberで移します。破棄は保存期間を確認する別手順です。

atomic_noreplace "$stage" "$target"
test ! -e "$stage" && test -d "$target"
directory_manifest "$target" "$work/target.directories"
cmp --silent "$work/source.directories" "$work/target.directories"
test "$(find "$target" -xdev -type f -printf x | wc -c)" -eq 0
test "$(find "$target" -xdev ! -type d -printf x | wc -c)" -eq 0
directory_sha=$(sha256sum -- "$work/target.directories"); directory_sha=${directory_sha%% *}; directory_sha=${directory_sha#\\}
receipt="$parent/.ittrip-117-$run_id.receipt"
( umask 077; set -o noclobber; printf 'target=%s\ndirectory_sha=%s\nrun_id=%s\n' "$target" "$directory_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 directories_sha=%s regular_files=0 non_directories=0\n' "$directory_sha"

run-owned rootだけを復旧する

復旧時はreceiptのtarget・run_id・directory hashと、現在も非directory 0件であることを再確認します。第三の状態なら停止し、完全一致する今回のrootだけをquarantineへ移します。

# このrunが作った空階層だけをquarantineし、開始前のtarget不存在へ戻す。
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-117-rollback.$run_id.XXXXXXXX")
directory_manifest "$target" "$rollback_manifest"
rollback_sha=$(sha256sum -- "$rollback_manifest"); rollback_sha=${rollback_sha%% *}; rollback_sha=${rollback_sha#\\}
rm -- "$rollback_manifest"
grep -Fx "directory_sha=$rollback_sha" "$receipt" >/dev/null
test "$(find "$target" -xdev ! -type d -printf x | wc -c)" -eq 0
quarantine="$target.review-$run_id"
test ! -e "$quarantine"
atomic_noreplace "$target" "$quarantine"
test ! -e "$target" && test -d "$quarantine"
rm -- "$receipt"

受入条件

既存target sentinelでは変更前に終了し、sentinel hashが不変であることを確認します。clean runではsourceとtargetのNUL-safe directory listが一致し、regular file 0、symlink 0、その他の非directory 0です。復旧後はtarget不存在、quarantine側の同じdirectory hashを確認します。

metadataを別要件にする

この例は階層名だけを複製します。mode、owner、ACL、xattrも必要なら別の承認fieldとしてmanifestへ追加してください。targetをsource配下へ置くと探索中に自己増殖するため、必ず別rootを使います。

公式情報・参考資料

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

この記事を書いた人

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

コメント

コメントする

目次