🎉

【Box】間違って削除したファイルを秒速で「一括復元」する技術|名前重複も自動回避するPython救出ボット(解決編)

に公開
2026/02/02

前回、APIを使って削除ログを調査した結果、犯人は私(kmg)であることが判明しました。
↓前回の記事のリンク

https://zenn.dev/kmg/articles/fb857e15874d17

さて、犯人が分かれば次は**「救出(Restore)」です。
今回は、特定した削除ログを元に、
「ゴミ箱にあるファイルを一括で、しかも名前重複を自動回避しながら元の場所に戻す」**という、実務特化のPythonスクリプトを公開します。

1. なぜ「標準機能」ではなく、あえて「API」なのか?

「ゴミ箱から戻すだけなら、ブラウザ画面でやればいいのでは?」
数個のファイルならその通りです。しかし、実務の現場では**「手動では詰むパターン」**が存在します。

① 量の壁(1,000件クリックできますか?)

誤操作でフォルダごと消した場合など、対象が数百〜数千件に及ぶことがあります。BoxのWeb画面はページ送りが発生するため、これらを手動で一括選択して戻すのは苦行以外の何物でもありません。
大事な親フォルダ―ならあり得る話でそうなってまってからでは遅いため事前対策が必要です。

② 「名前の重複」という二次災害

これがAPIを使う最大の理由です。
「あ!消しちゃった!」と焦って、元の場所に同じ名前のファイルを新しく作り直してしまう……現場ではよくあることです。

この状態で標準機能の「復元」ボタンを押すと、「同名のファイルが存在します」とエラーが出て止まります。 手動の場合、一度新しいファイルをどこかへ動かすか、名前を変えてから復元する必要がありますが、APIなら**「被っていたら自動で名前を変えて復元する」**というロジックを組めます。

2. 実装のポイント:APIの「裏仕様」を突く

今回は確実性を重視して、SDK(ライブラリ)の便利機能ではなく**「Box APIの生の仕様」**を直接利用します。

Boxにおいて「ゴミ箱からの復元」とは、実質的に**「ファイルの『削除日時(trashed_at)』というパラメータを『空(None)』に更新する」**という操作を指します。

この仕組みを直接叩くことで、ライブラリのバージョンに左右されず、どんな環境でも確実にファイルを蘇生させることができます。

3. 【完成コード】自動復旧&リネームボット

以下のコードは、過去24時間以内に「指定したユーザー」が削除したファイルを自動で探し出し、復元するスクリプトです。

https://zenn.dev/kmg/articles/378337c1811608

▼ 配布用コードの特徴

  • 確実な復元: APIエンドポイントを直接叩くため、SDKのバージョンに依存せず動作。
  • 重複回避: 元の場所に同じ名前のファイルがある場合、自動で 復元_時刻_ファイル名 にリネームして救出。
import os
import json
from datetime import datetime, timedelta, timezone
from boxsdk import JWTAuth, Client
from boxsdk.exception import BoxAPIException

# ==========================================
# 設定エリア
# ==========================================
# 取得した config.json のファイル名
CONFIG_FILE_NAME = 'config.json' 

# 救出したい「実行者」の名前(Box上の表示名を正確に)
MY_NAME = 'あなたの名前' 
# ==========================================

def get_val(obj, key, default=None):
    if isinstance(obj, dict): return obj.get(key, default)
    return getattr(obj, key, default)

def force_restore(client, file_id, new_name=None):
    """SDKのメソッドを使わず、APIを直接叩いて復元するプロの技"""
    url = f"{client._session._api_config.BASE_API_URL}/files/{file_id}"
    data = {"trashed_at": None} # 削除日時をNoneに書き換える=復元
    if new_name:
        data["name"] = new_name
    
    return client.make_request(
        'POST',
        url,
        data=json.dumps(data),
        headers={'Content-Type': 'application/json'}
    )

def main():
    base_dir = os.path.dirname(os.path.abspath(__file__))
    auth = JWTAuth.from_settings_file(os.path.join(base_dir, CONFIG_FILE_NAME))
    client = Client(auth)
    
    # 過去24時間のログを対象
    target_time = (datetime.now(timezone.utc) - timedelta(days=1)).isoformat()
    
    print(f"--- 復旧ミッション開始(ターゲット: {MY_NAME}) ---")
    
    events = client.events().get_admin_events(
        created_after=target_time,
        event_types=['DELETE'],
        limit=100
    )

    if isinstance(events, dict) and 'entries' in events:
        events = events['entries']

    count = 0
    for event in events:
        if isinstance(event, str): continue
        source = get_val(event, 'source')
        created_by = get_val(event, 'created_by')
        
        user_name = get_val(created_by, 'name', 'Unknown')
        item_name = get_val(source, 'item_name', 'Unknown')
        item_id = get_val(source, 'item_id', None)
        item_type = get_val(source, 'item_type', None)

        if item_type == 'file' and user_name == MY_NAME:
            count += 1
            print(f"[{count}] 発見: {item_name} (ID: {item_id})")
            try:
                force_restore(client, item_id)
                print(f"   -> ✅ 成功!元の場所に戻りました。")
            except BoxAPIException as e:
                # 名前重複エラー (409) のハンドル
                if e.status == 409 and (e.code == 'item_name_in_use' or e.code == 'item_name_already_used'):
                    try:
                        new_name = f"復元_{datetime.now().strftime('%H%M%S')}_{item_name}"
                        force_restore(client, item_id, new_name=new_name)
                        print(f"   -> 🔄 名前重複のため '{new_name}' として救出しました。")
                    except Exception as e2:
                        print(f"   -> ❌ リネーム救出失敗: {e2}")
                elif e.status == 403:
                    # 重複ログ等ですでに復元済みの場合はスキップ
                    print(f"   -> ℹ️ すでに復元済みか、操作不要です。")
                else:
                    print(f"   -> ❌ エラー: {e}")

    if count == 0:
        print("\n※対象ファイルが見つかりませんでした。")

if __name__ == "__main__":
    main()

4. まとめ:トラブルを「資産」に変える

このスクリプトを使えば、数千件のファイル消失も、コーヒーを飲んでいる間に復旧可能です。
手作業のミス(二次災害)に怯える必要もありません。

技術力とは、単にコードが書けることではなく、**「現場のパニックを冷静に沈める準備ができていること」**だと私は考えます。

たとえトラブルの原因が自分自身であったとしても、それを一瞬で、かつスマートに解決できる仕組みを持っていれば、それはもはや「ミス」ではなく「システムの堅牢性を証明する機会」にすらなり得ます。


私が公開している本「Box Link Patrol Pro」では、今回の復元技術をさらに進化させ、24時間365日 Boxを監視し続ける「完全自動パトロールシステム」の構築ガイドと全コードを公開しています。

https://zenn.dev/kmg/books/9eebd2060b6b81

「毎回スクリプトを叩くのすら面倒だ」「自分が見ていない間もシステムに守ってほしい」という方は、ぜひこちらの詳細をご覧ください。私の "自戒" と "現場の執念" から生まれた、実務特化のツールです。

Discussion