はじめに (対象読者・この記事でわかること)

本記事は、Linux のシェル操作に慣れた開発者やインフラエンジニア、または日常的に大量ファイルを扱うシステム管理者を対象としています。
「再帰的にファイルだけを削除したい」ものの、誤ってディレクトリや重要ファイルまで削除してしまうリスクを避けたい、あるいは削除速度を制御したいというニーズに応える内容です。
この記事を読むことで、以下が実現できるようになります。

  • find コマンドと sleep を組み合わせた「ゆっくり削除」スクリプトの作成方法
  • ディレクトリ構造を保持しつつ、ファイルだけを安全に再帰的に削除するテクニック
  • 実運用での注意点や、誤削除防止のベストプラクティス

背景として、サーバー上で一括削除を行う際に「削除が速すぎて監視が追いつかない」ケースや、削除途中にプロセスが停止した際のリカバリが難しい点が挙げられます。これらを解決するために「ゆっくり削除」アプローチが有効です。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。
- 基本的な Linux シェル操作(bash 等)
- findxargswhile ループなどの標準コマンドの使い方
- ファイルシステムの権限や所有権に関する基礎知識

再帰的ゆっくり削除の概要と必要性

大量ファイルが格納された階層構造のディレクトリを一括で削除する場合、rm -rf は瞬時に全体を消去します。
しかし、以下のような問題が発生しがちです。

  1. 誤削除リスク
    - rm -rf はディレクトリも対象に含めるため、誤って上位ディレクトリを指定すると全体が消失する。
  2. I/O 負荷の集中
    - 短時間に大量の削除 I/O が走り、ディスクやディスクキャッシュが一気に圧迫され、他のプロセスのパフォーマンスが低下する。
  3. ロギング・監査の困難
    - 瞬間的に大量ログが出力され、削除対象や削除順序を後から追跡しにくい。

これらを緩和する手段として「再帰的かつゆっくり」削除があります。
具体的には、find で対象ファイルを列挙し、1 ファイルごとに一定時間待機(sleep)しながら rm を実行します。
この手法のメリットは次の通りです。

  • 安全性向上:削除対象がファイルだけになるため、ディレクトリは残る。
  • I/O スローダウン:一定間隔で削除を行うことで、ディスク負荷が分散される。
  • 監査容易化:削除ログが時間的に分散し、個別ファイルの削除履歴が取得しやすくなる。

具体的な手順と実装例

以下では、実際に「再帰的にゆっくりファイルだけを削除」するシェルスクリプトを作成し、動作確認までの流れを解説します。
スクリプトは 3 つの主要パートに分かれています。

  1. 対象ディレクトリと削除間隔の取得
  2. find でファイルを列挙し、while read で 1 行ずつ処理
  3. 削除実行とログ出力、エラーハンドリング

ステップ1:スクリプト雛形の作成

まずは基本的なスクリプトの雛形です。ファイル名は slow-rm.sh とします。

Bash
#!/usr/bin/env bash # ------------------------------------------------- # slow-rm.sh : 再帰的にファイルだけを遅延削除 # Usage : ./slow-rm.sh <対象ディレクトリ> <間隔(秒)> # ------------------------------------------------- set -euo pipefail TARGET_DIR="${1:-}" INTERVAL="${2:-1}" # デフォルトは 1 秒 if [[ -z "$TARGET_DIR" ]]; then echo "Error: 対象ディレクトリを指定してください。" >&2 exit 1 fi if [[ ! -d "$TARGET_DIR" ]]; then echo "Error: 指定されたパスはディレクトリではありません -> $TARGET_DIR" >&2 exit 1 fi LOG_FILE="slow-rm-$(date +%Y%m%d%H%M%S).log" echo "開始日時: $(date)" > "$LOG_FILE" echo "対象ディレクトリ: $TARGET_DIR" >> "$LOG_FILE" echo "削除間隔: $INTERVAL 秒" >> "$LOG_FILE"
  • set -euo pipefail により、エラー時に即座に終了し、未定義変数の使用を防止します。
  • 引数チェックでユーザーフレンドリーにエラーメッセージを出します。
  • ログファイルはタイムスタンプ付きで自動生成し、削除操作の追跡を可能にします。

ステップ2:再帰的にファイルを列挙し、1 ファイルずつ削除

次に findwhile read を組み合わせ、1 ファイルごとに rmsleep を実行します。

Bash
# ファイル列挙 → パイプで while ループへ find "$TARGET_DIR" -type f -print0 | while IFS= read -r -d '' file; do # 削除実行 if rm -f "$file"; then echo "$(date +%Y-%m-%dT%H:%M:%S) INFO 削除: $file" >> "$LOG_FILE" else echo "$(date +%Y-%m-%dT%H:%M:%S) ERROR 削除失敗: $file" >> "$LOG_FILE" fi # 指定秒数待機 sleep "$INTERVAL" done

ポイント解説:

  • -print0read -d '' を組み合わせることで、ファイル名にスペースや改行が含まれていても安全に処理できます。
  • rm -f で強制削除し、失敗した場合はエラーログを残すようにしています。
  • sleep "$INTERVAL" によって、削除間隔を柔軟に調整可能です。

ステップ3:実行後のクリーンアップとサマリーログ

全ファイル削除が完了したら、簡単なサマリーをログに追記します。

Bash
echo "終了日時: $(date)" >> "$LOG_FILE" TOTAL=$(find "$TARGET_DIR" -type f | wc -l) echo "残存ファイル数: $TOTAL" >> "$LOG_FILE" echo "削除完了。ログは $LOG_FILE に保存されています。"
  • TOTAL は削除後に残っているファイル数を取得し、何らかの残存がある場合は手動確認が必要であることを示唆します。
  • 終了時刻を記録し、実行時間の把握ができます。

ハマった点やエラー解決

発生した問題 原因 解決策
find がシンボリックリンク先のファイルも削除した -L オプションを付与していた -L を外し、リンクは対象外に。
大量ファイルで while read が途中で止まった パイプラインが rm の終了コードで中断された set +e で一時的にエラー無視、または || true を付与。
sleep が 0.5 秒未満の小数に対応しない sleep の実装が整数部のみ受け付けた環境 sleep 0.1 が使用可能な GNU coreutils に統一。
権限エラーで削除できないファイルが残った 実行ユーザーが対象ファイルの所有者でなかった sudo で実行、または chmod で権限を一時的に緩める。

ベストプラクティスまとめ

  • バックアップを必ず取得: 重要ディレクトリに対しては、削除前に tar 等でアーカイブを作成。
  • テスト環境で Dry-run: -exec echo rm {} \; で削除対象を先に確認。
  • ログは永続化: 監査目的で /var/log/slow-rm/ 配下に保存し、ローテーション設定を検討。
  • 間隔はディスク負荷に応じて調整: SSD では 0.1〜0.5 秒、HDD では 1〜2 秒が目安。

まとめ

本記事では、再帰的にファイルだけをゆっくり削除するシェルスクリプトの作成手順 を解説しました。

  • 安全性find -type f でディレクトリは残し、ファイルのみ対象にする。
  • スローダウンsleep による削除間隔で I/O 負荷を分散し、システムの安定性を確保。
  • 監査性:実行ログをタイムスタンプ付きで保存し、削除履歴を容易に追跡できる。

この手法を導入すれば、誤削除リスクを抑えつつ、ディスク負荷をコントロールしたバッチ削除が実現できます。今後は、削除対象の拡張(例:特定拡張子のみ)や、並列化による高速化 についても検討予定です。

参考資料