TJ Zhang

Restoring 214,000 iCloud Files the Web UI Couldn't Handle

I accidentally deleted 214,000 files from iCloud Drive. Apple's web UI for restoring them doesn't work at scale.

How I Got Here

I was reorganizing my Documents folder with shell commands—bulk mv operations, nothing exotic. At some point, iCloud's sync engine couldn't keep up. It got confused, and my entire Documents folder got marked as deleted across devices.

This is the fundamental issue with consumer cloud drives: they're designed for human-paced, manual changes. Click "Save" in an app, one file at a time, seconds between operations. They're not designed for hundreds of file operations per second. The sync systems race, conflict resolution fails silently, and deletions propagate before you notice.

But that's a longer conversation about why I no longer trust iCloud with anything I care about. First, I needed to get my files back.

The Problem

iCloud.com has a "Data Recovery" page at icloud.com/recovery where you can restore deleted files. The UI shows a list of files with a "Restore All" button. Simple enough.

Except the button stays greyed out until all files finish loading. With 200k+ files, the browser runs out of memory and crashes before that happens. I tried Safari, Chrome, different machines—same result every time.

The UI tries to render everything before letting you do anything. Classic frontend anti-pattern at scale.

Finding the API

The web UI clearly talks to some backend. I opened Chrome DevTools, clicked around on a smaller account, and watched the network tab.

Two endpoints do all the work:

Fetching deleted files:

GET /ws/_all_/list/enumerate/tombstones?clientId=...&dsid=...&limit=50

Returns paginated JSON with file IDs and a continuation marker. The web UI fetches all pages before rendering—but nothing stops you from processing them incrementally.

Restoring files:

PUT /v1/items?clientId=...&dsid=...
Body: { "drive_item_update_request": { "is_recover": "true" }, "item_ids": [...] }

Accepts batches of file IDs. The web UI sends one massive request; I send batches of 100.

The Authentication Problem

The API needs clientId, dsid, and session cookies. These come from Apple's auth flow, which involves 2FA and device trust. Not something you can easily script.

My solution: let the browser handle auth, then steal the credentials.

The tool launches Chrome, opens icloud.com/recovery, and waits for you to log in normally. Once authenticated, it intercepts API requests to grab clientId and dsid, then extracts cookies from the browser context.

# Watch for authenticated API requests
if "icloud.com" in url and "clientId=" in url:
    params = parse_qs(urlparse(url).query)
    client_id = params.get("clientId", [None])[0]
    dsid = params.get("dsid", [None])[0]
    # Got what we need

Playwright's CDP connection makes this straightforward—connect to Chrome's debug port, set up request interception, wait for the right request to fly by.

Handling Session Expiry

iCloud sessions expire after ~30 minutes. With 200k files at 500/minute, that's guaranteed to happen mid-restore.

The API returns HTTP 421 (Apple's non-standard way of saying "session expired") when your session expires. When that happens, the tool reloads the page (you're still logged in, just need fresh tokens), waits for a new API request, extracts the new credentials, and continues.

except httpx.HTTPStatusError as e:
    if e.response.status_code in (401, 403, 421):
        # Refresh credentials via browser reload
        creds = await browser.refresh_credentials()
        continue

No manual intervention needed. The browser stays open in the background, maintaining your session.

Results

214,146 files restored in about 3-4 hours, with automatic credential refresh every ~30 minutes. The web UI couldn't load the first page; this tool finished the job while I slept.

The code is at github.com/odysseus0/icloud-restore. It's a simple Python CLI—uvx icloud-restore if you have uv installed.


The fix Apple should ship: paginated rendering with a "Restore All" button that doesn't wait for everything to load. Until then, there's this.