Google Drive APIでファイルアップロード時に「storage quota exceeded」エラーが出た話と対処方法

先日、Python スクリプトから Google Drive API を利用してファイルをアップロードしようとした際に、以下のように `"The user's Drive storage quota has been exceeded."` というエラーが発生しました。

```plaintext
googleapiclient.errors.HttpError: 
<HttpError 403 when requesting 
https://www.googleapis.com/upload/drive/v3/files?fields=id&alt=json&uploadType=multipart 
returned "The user's Drive storage quota has been exceeded.". 
Details: [
  {
    'message': "The user's Drive storage quota has been exceeded.",
    'domain': 'usageLimits',
    'reason': 'storageQuotaExceeded'
  }
]>
```

エラーメッセージに記載されている通り、Google Drive のストレージの割り当て上限に達してしまったことが原因でした。

普段は Google Drive の使用量をあまり意識していませんでしたが、API経由でファイルをアップロードする際に上限に達した場合の処理を備忘録として記事化しておきます。

## 現在のストレージ使用量を確認

まず現状を把握するため、Google Drive API を使って現在のストレージ使用量を確認しました。
about().get() メソッドを利用すると、ストレージに関する情報を取得できます。

以下が実行した Python コードです。

```python
import sys
import os
from google.oauth2.service_account import Credentials
from googleapiclient.discovery import build

# サービスアカウントキーのファイルパス ※実際のパスに置き換えてください
SERVICE_ACCOUNT_FILE = 'path/to/your/service_account.json'
SCOPES = ['https://www.googleapis.com/auth/drive']

# 認証情報を作成
creds = Credentials.from_service_account_file(SERVICE_ACCOUNT_FILE, scopes=SCOPES)
drive_service = build('drive', 'v3', credentials=creds)

# aboutリソースから storageQuota フィールドを取得
try:
    about = drive_service.about().get(fields="storageQuota").execute()
    print(about)
except Exception as e:
    print(f"ストレージ情報の取得中にエラーが発生しました: {e}")
```

実行例:

```
{
  "storageQuota": {
    "limit": "16106127360",
    "usage": "16084258429",
    "usageInDrive": "16084258429",
    "usageInDriveTrash": "0"
  }
}
```

- limit: ストレージ容量の上限（バイト単位）
- usage: 現在の使用量（バイト単位）

無料のGoogleアカウントに紐づくDriveでは通常15GiBのストレージ制限があり、サービスアカウント経由でもこの制限が適用されるようです。今回の結果からも、使用量がほぼ上限に達していることが確認できました。


## 自分が所有するファイル一覧をリストアップ

ストレージ状況を把握するために、「自分がオーナーのファイル一覧」をリストアップするスクリプトも作成しました。  
不要ファイルの整理や、容量を圧迫しているファイルの特定に役立ちます。

以下のPythonコードで、自分が所有しているすべてのファイルを取得して表示しました。

```python
from google.oauth2.service_account import Credentials
from googleapiclient.discovery import build

# サービスアカウントキーのファイルパス
SERVICE_ACCOUNT_FILE = 'path/to/your/service_account.json'
SCOPES = ['https://www.googleapis.com/auth/drive']

# 認証情報を作成
creds = Credentials.from_service_account_file(SERVICE_ACCOUNT_FILE, scopes=SCOPES)
drive_service = build('drive', 'v3', credentials=creds)

# 自分が所有しているすべてのファイルを取得
query = "'me' in owners and trashed = false"
page_token = None

print("自分が所有するファイル一覧：")

while True:
    response = drive_service.files().list(
        q=query,
        fields="nextPageToken, files(id, name, mimeType, size, createdTime)",
        pageSize=100,
        pageToken=page_token
    ).execute()

    for file in response.get('files', []):
        print(f"- {file['name']} (ID: {file['id']}, MIME: {file['mimeType']}, Size: {file.get('size', 'N/A')} bytes)")

    page_token = response.get('nextPageToken', None)
    if page_token is None:
        break
```
実行例:

```
自分が所有するファイル一覧：
- sample.txt (ID: xxxxxxxxxx, MIME: text/plain, Size: 68 bytes)
- hoge.zip (ID: yyyyyyyyyy, MIME: application/x-zip-compressed, Size: 55884639 bytes)
- fuge.zip (ID: zzzzzzzzzz, MIME: application/x-zip-compressed, Size: 830795032 bytes)

...
```

## idを指定してファイルを削除

先ほど表示したファイル一覧から、必要なくなったファイルのidを指定して削除するスクリプトを作成しました。

```python:delete_file.py
import sys
from google.oauth2.service_account import Credentials
from googleapiclient.discovery import build

SERVICE_ACCOUNT_FILE = 'path/to/your/service_account.json'
SCOPES = ['https://www.googleapis.com/auth/drive']

def delete_file(file_id):
    creds = Credentials.from_service_account_file(SERVICE_ACCOUNT_FILE, scopes=SCOPES)
    drive_service = build('drive', 'v3', credentials=creds)

    try:
        drive_service.files().delete(fileId=file_id).execute()
        print(f"ファイル（ID: {file_id}）を削除しました。")
    except Exception as e:
        print(f"ファイル削除中にエラーが発生しました: {e}")

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("使い方: python delete_file.py <file_id>")
        sys.exit(1)

    file_id = sys.argv[1]
    delete_file(file_id)
```

このスクリプトを実行したときは以下のようになります。

```bash
python delete_file.py "xxxxxxxxxx"
ファイル（ID: xxxxxxxxxx）を削除しました。
```

## 古い ZIPファイルを削除して空き容量を確保

ストレージ容量を圧迫している原因を探ったところ、過去にアップロードしたZIPファイルが大量に溜まっていることが原因でした。
１ファイルずつ削除することもできましたが、対象件数が100件以上あったため、手作業では現実的ではありませんでした。
そこで、Google Drive API を利用して、60日よりも前にアップロードしたZIPファイルを検索し削除する Python スクリプトを作成しました。

```python
from google.oauth2.service_account import Credentials
from googleapiclient.discovery import build
from datetime import datetime, timedelta, timezone
import time

SERVICE_ACCOUNT_FILE = 'path/to/your/service_account.json'
SCOPES = ['https://www.googleapis.com/auth/drive']

creds = Credentials.from_service_account_file(SERVICE_ACCOUNT_FILE, scopes=SCOPES)
drive_service = build('drive', 'v3', credentials=creds)

# サービスアカウントのメールアドレス（オーナー識別に使用）
owner_email = creds.service_account_email

# --- 削除条件設定 ---
DAYS_THRESHOLD = 60  # 60日より古いファイルを削除
TARGET_EXTENSIONS = ('.zip', '.tar.gz')  # 対象拡張子（小文字）
# --------------------

threshold_date = datetime.now(timezone.utc) - timedelta(days=DAYS_THRESHOLD)
query = f"'{owner_email}' in owners and trashed = false"

page_token = None
deleted_count = 0
processed_count = 0

print(f"削除処理を開始します。作成日時が {threshold_date.strftime('%Y-%m-%d %H:%M:%S %Z')} より前の {', '.join(TARGET_EXTENSIONS)} ファイルを削除します。")

while True:
    try:
        response = drive_service.files().list(
            q=query,
            fields="nextPageToken, files(id, name, createdTime, size)",
            pageSize=100,
            pageToken=page_token
        ).execute()
    except Exception as e:
        print(f"\nファイルリストの取得中にエラーが発生しました: {e}")
        print("60秒待機して再試行します...")
        time.sleep(60)
        continue

    files = response.get('files', [])
    if not files:
        print("\n対象ファイルが見つかりませんでした。")
        break

    print(f"\n{len(files)} 件のファイルを取得しました。削除対象か確認します...")

    for file in files:
        processed_count += 1
        name = file.get('name', 'N/A')
        file_id = file.get('id')
        created_time_str = file.get('createdTime')

        if not name or not file_id or not created_time_str:
            print(f" - [スキップ] ファイル情報が不完全です: ID={file_id}, Name={name}")
            continue

        if not name.lower().endswith(TARGET_EXTENSIONS):
            continue

        try:
            created_time = datetime.fromisoformat(created_time_str.replace('Z', '+00:00'))
        except ValueError:
            print(f" - [スキップ] 作成日時の形式が無効です: {name} ({created_time_str})")
            continue

        if created_time < threshold_date:
            file_size_mb = int(file.get('size', 0)) / (1024 * 1024)
            print(f" - [削除対象] {name} (作成日: {created_time.strftime('%Y-%m-%d')}, サイズ: {file_size_mb:.2f} MB, ID: {file_id})")
            try:
                drive_service.files().delete(fileId=file_id).execute()
                deleted_count += 1
                print(f"   -> 削除成功")
                time.sleep(0.5)
            except Exception as e:
                print(f"   -> !!! 削除失敗: {name} - {e}")
                # 必要に応じてリトライなどのエラーハンドリングを追加してください

    page_token = response.get('nextPageToken')
    if page_token is None:
        print("\n全てのファイルの確認が完了しました。")
        break

print(f"\n--- 処理結果 ---")
print(f"確認したファイル数: {processed_count}")
print(f"削除したファイル数: {deleted_count} 件")
print(f"-----------------")
```

同じようなエラーに遭遇した方の参考になれば幸いです。