Python講座 / ファイル一括コピーを事前確認と照合付きで行う

ファイルの一括処理では、変更予定を確認する段階と実行する段階を分けます。この実習はテキストファイルを新しい場所へコピーし、同名の出力が既にある場合は上書きせず停止します。

対象範囲と保存方針を決める

新しい練習フォルダーを使います。対象はcopy-demo/inからcopy-demo/outへのコピーで、サブフォルダーを再帰検索せず、シンボリックリンクの項目も除きます。元のファイルは移動・削除しません。

移動は入力側も変更します。コピー、内容照合、後の片付けを分けると、失敗したときの状態を判断しやすくなります。同名の出力に対して無断で別名を作るのではなく、この例は競合として止めます。

サンプルの準備とコピーを分けます。空の練習フォルダーでsetup_copy.pyを一度実行し、copy-demo/in/note.txtを作ります。この準備は書き込みますが、コピーの既定のdry-runはパスを表示するだけで出力フォルダーもファイルも作りません。--applyで初めてコピーし、既存のコピー先があれば停止します。元ファイルの移動・削除は行いません。

全体を setup_copy.py に保存し、同じ練習フォルダーで python setup_copy.py を実行します。

from pathlib import Path
source = Path("copy-demo/in")
source.mkdir(parents=True, exist_ok=True)
with (source / "note.txt").open("x", encoding="utf-8") as f:
    f.write("demo\n")
print("sample ready")

sample readyを確認してからsafe_copy.pyを実行します。

コピー先と失敗処理は、try・except・else・finally・raiseで例外を扱うで扱う規則とつながります。

既定の事前確認を実行する

safe_copy.pyをUTF-8で保存し、先に別の準備段階を実行します。

import argparse
import hashlib
import shutil
from pathlib import Path
parser = argparse.ArgumentParser()
parser.add_argument("--apply", action="store_true")
args = parser.parse_args()
source, target = Path("copy-demo/in"), Path("copy-demo/out")
if not source.is_dir():
    parser.error("prepare copy-demo/in first")
files = sorted(p for p in source.glob("*.txt") if p.is_file() and not p.is_symlink())
for src in files:
    dst = target / src.name
    if dst.exists() or dst.is_symlink():
        raise FileExistsError(dst)
    print("copy", src.name)
    if args.apply:
        target.mkdir(parents=True, exist_ok=True)
        with src.open("rb") as incoming, dst.open("xb") as outgoing:
            shutil.copyfileobj(incoming, outgoing)
        if hashlib.sha256(src.read_bytes()).digest() != hashlib.sha256(dst.read_bytes()).digest():
            raise RuntimeError("copy verification failed")
print("applied" if args.apply else "dry-run")

最初の予想結果です。

copy note.txt
dry-run

確認したコピーを適用して内容を照合する

対象一覧を確認した後に実行します。

python safe_copy.py --apply

最初はcopy note.txtとappliedが表示され、copy-demo/out/note.txtができます。xbで開くため、既存の出力は拒否します。小さな例ではSHA-256で元と先のバイト列の一致も確認します。

再び実行するとFileExistsErrorになるはずです。コピー失敗では不完全な出力が残る場合があります。削除して再試行する前に、解決済みの絶対パスと内容から、そのファイルが今回の練習対象か確認してください。

拡張する前に限界を理解する

このハッシュ計算は小さなファイル全体をメモリーへ読みます。大きなファイルには分割読み取りが必要です。バイトの一致だけで権限やすべてのメタデータを検証できず、信頼できない利用者が親ディレクトリーを同時に差し替える状況も防いでいません。

改名や削除を追加するなら、対象一覧、衝突検出、バックアップと復旧を設計します。練習結果の片付けでも、絶対パスが使い捨てのcopy-demo内に収まることを確認し、意図したものだけを扱います。相対パスの範囲はpathlibで確認できます。

関連する公式リファレンス