Open Source · MIT License

Your iCloud Drive.
On Your Terms.

A powerful terminal utility to download, sync, and back up your iCloud Drive files — with resume support, differential updates, and parallel transfers.

zsh — iFetch

Rasil's Field note

Building iFetch: Making iCloud Transfers Recoverable

A lockdown project about getting my files out of iCloud without starting over every time something failed.

2020 was strange. The world outside had gone quiet, everyone was locked inside, and my laptop had become the place where almost everything happened.

I also had more time than usual to get annoyed by small problems.

One of them was iCloud Drive. It worked well when I used it the way Apple expected: save a file on one device, open it on another, and let sync happen somewhere in the background. Then I tried to get a large collection of files out of it.

That was much less pleasant.

A long download could stall. Starting again could mean repeating work. Even when it looked finished, I had no good answer to a basic question: did I get everything?

So, while stuck at home, I started building iFetch. It was a Python command-line tool that gave me more control over downloading an iCloud Drive folder. I could see what it was doing, stop it, and run it again without throwing away all the work it had already done.

Downloading was the easy part

My first plan was short:

  • log in to iCloud, including two-factor authentication;
  • find a folder;
  • walk through everything inside it;
  • recreate the same folder structure locally.

That got files onto my computer. It also fell apart as soon as anything went wrong.

Large downloads involve a lot of things I do not control. The connection can drop. A signed URL can expire. A remote file reference can go stale. The process can crash. Retrying the whole folder from the beginning works, technically, but it is a terrible way to spend an afternoon.

I stopped thinking of the job as one big download. iFetch would handle each file separately, and each file could be downloaded in byte ranges. If one part failed, I wanted to redo that part, not everything before it.

Building around failure

The first version looked like this:

remote file → bytes → local file

The version I trusted looked more like this:

resolve → enumerate → open → range → temp file → checkpoint
        → retry or refresh → checksum → archive → replace → report

It is longer because the short version kept finding new ways to break.

Keeping the half-finished file

iFetch writes incoming bytes to a .temp file. Next to it, a small .download file records the last completed position.

At first, those files looked like cleanup work. They turned out to be the thing that made resume possible. If the connection dropped or I stopped the process, the next run could pick up from the saved position instead of going back to zero.

The final destination stays untouched during the download. Once the temporary file is complete, iFetch calculates a checksum, archives the previous local copy under .versions/, and replaces it. A broken transfer should not ruin the good copy I already have.

Retrying stale information is pointless

Some failures needed more than another attempt. iCloud download URLs can expire, and the file object returned by the library can become stale too.

When a request returns 404 or 410, iFetch can ask for a new signed URL. If the file object itself no longer works, it resolves the file again from its iCloud path. Other temporary failures wait and retry, with longer pauses after each attempt. If the server sends a Retry-After value, iFetch listens to it.

This was one of the more useful lessons from the project. A retry only helps if the input is still valid.

Threads made hidden problems visible

Downloading several files at once made iFetch faster. It also exposed a bug in the iCloud library that I would not have seen in a simple loop.

The library loads directory children lazily. While one thread was reading the directory, another could change its internal dictionary. The result was the wonderfully literal error: dictionary changed size during iteration.

The fix was to resolve the children first, freeze that list, and only then hand the work to the thread pool. Adding threads was easy. Understanding what those threads touched took longer.

What iFetch can do

The command is simple:

python ifetch/cli.py Documents/Photos ~/Backups/Photos

iFetch can walk through owned and shared iCloud Drive folders, download several files at once, resume partial files, refresh expired cloud references, keep previous local versions, and write a JSON report when it finishes.

It also supports include and exclude filters. Directories always remain traversable, even when their names do not match a filter. Otherwise, a filter such as *.pdf could reject the Documents folder before iFetch ever finds Documents/report.pdf.

The report became more important than I expected. A progress bar feels reassuring, but it does not tell me much after the terminal closes. The report tells me which files succeeded, which failed, how many bytes moved, and what checksums were produced.

I eventually added a Google Drive exporter too. That part is less central to iFetch, but it came from the same itch. I wanted moving my own files between services to feel boring and repeatable.

Where it falls short

The biggest limitation is the feature I called "differential updates." That name promises more than the current code delivers.

iFetch uses file size to decide what to do. If there is no local file, it downloads the whole remote file in ranges. If the local file is smaller, it resumes from the local size. If both files have the same size, it assumes nothing changed.

That last case is the problem. Two files can have the same size and different contents. iFetch will miss that change because it does not compare remote and local chunk hashes.

There are other unfinished parts. Version archives exist, but there is no friendly restore command. The report does not include every skipped file. Profiles only parse JSON even though the documentation mentions YAML. Plugin failures stay out of the main transfer, which is good for the download and bad for debugging the plugin.

The largest risk is outside my code: iFetch depends on pyicloud, which talks to Apple's private interfaces. Apple can change those interfaces without warning. This is useful software built on ground I do not own.

If I return to the project, I would start with real content comparison, a restore command, better reports, and proper packaging. I would also add continuous integration so the private API failures show up sooner.

What stuck with me

Before iFetch, I thought reliability mostly meant retries. Now I think retries are the least interesting part.

The parts I trust are the boring ones: the temporary file that survives a crash, the checkpoint that saves completed work, the old copy moved out of harm's way, and the report waiting at the end. None of them is impressive alone. Together, they make failure less dramatic.

I also stopped seeing temporary files as trash. Sometimes a half-finished file is hours of saved work. Deleting it because the process failed would be the tidy thing to do and the wrong thing to do.

I do not want to overstate what a file downloader meant during the first month of a pandemic. Still, very little felt under control then. Taking one frustrating job and making it understandable was satisfying.

2026 update: what Apple changed

I came back to this project six years later to see whether Apple had closed the gap.

Apple has made parts of the experience better. macOS Sequoia added Keep Downloaded, which lets users pin iCloud Drive files and folders locally. iCloud.com can download several selected files. Apple's Data & Privacy tools can also prepare an account-level copy that includes iCloud documents.

In December 2022, Apple announced Advanced Data Protection for iCloud. It adds optional end-to-end encryption for iCloud Drive and other categories. That is good for security, but it changes the export story. When Advanced Data Protection is enabled, Apple cannot include the protected iCloud Drive data in a Data & Privacy export. A copy made from a trusted device matters more in that setup.

Finder's Keep Downloaded option solves part of the problem I had in 2020. It can keep a folder available on a Mac, which is a big improvement. The files still live inside the synchronized iCloud Drive, though. Keeping them downloaded is not the same as making an independent archive.

Apple's current guidance still points users toward Finder, Files, or iCloud.com. There is still no official general-purpose iCloud Drive CLI, and CloudKit only gives developers access to their own app containers. It does not let a tool walk through an arbitrary user's whole iCloud Drive.

So the answer in 2026 is mixed. Apple has made local access easier and iCloud data more secure. For a repeatable migration or backup with checkpoints, explicit retries, version archives, and a machine-readable report, I would still want a tool like iFetch.

That surprised me a little. Six years is a long time in software, but getting all of your files out of a cloud service is still a very different job from keeping them synced inside it.

iCloud Wasn't Built for Bulk Downloads

Apple's iCloud Drive is seamless on Apple devices — until you need to download thousands of files, migrate between accounts, or create local backups. Native tools choke on large directories, stall mid-transfer, and offer no resume capability. iFetch fixes that.

🔒

Data Recovery

Disabled iCloud Drive? Your files are trapped in the cloud with no easy way to pull them down in bulk.

🔀

Account Migration

Switching Apple IDs means losing your Drive structure. No built-in tool to export everything at once.

Unreliable Sync

Large transfers time out, connections drop, and you're left wondering which files actually made it.

Engineered for Reliability

Every feature is designed around one goal: get your data from iCloud to your machine, completely and efficiently.

Parallel Downloads

Configurable worker threads (default 4) for blazing fast concurrent transfers. Saturate your bandwidth.

--max-workers=8
🔄

Differential Updates

Only changed chunks are fetched. File-level delta sync saves bandwidth and time on re-runs.

--chunk-size=2097152
⏸️

Resume Downloads

Checkpointed progress tracking. Connection dropped? Pick up exactly where you left off.

.temp + tracker files
🔐

Secure 2FA Auth

Full support for two-factor and two-step authentication. Credentials stored in your system keyring.

icloud --username=you@me.com
🗂

Profile Filters

Include/exclude glob patterns. Sync only PDFs, skip archives — personalised sync sets per profile.

--profile pdf_backup
🗄

Version History

Automatic on-disk archiving of previous file versions. Rollback any file to a prior state.

.versions/ directory
🧩

Plugin System

Drop a Python file, subclass BasePlugin, hook into auth, progress, and completion events.

plugins/ auto-discover
🤝

Shared Folders

Access and download items shared with your account. Browse shared roots with a single flag.

--list-shared
📊

Download Reports

Structured JSON summary of every session — successes, failures, bytes transferred, and changed chunks.

download_report.json

Up and Running in 3 Minutes

iFetch runs on Python 3.9+ and works on macOS, Linux, and WSL. Follow these steps to get started.

01

Create a Virtual Environment

Isolate iFetch's dependencies in a clean virtual environment.

$ python3 -m venv ivenv
$ source ivenv/bin/activate
02

Install Dependencies

Pull in the Python packages iFetch relies on.

$ pip install pyicloud tqdm requests keyring
03

Authenticate with iCloud

Store your Apple ID credentials securely in your system keyring. You'll complete 2FA once.

$ icloud --username=you@icloud.com
Enter iCloud password: ••••••••
Two-factor authentication required.
Enter verification code: 123456
✓ Authentication successful
04

Start Fetching

Point iFetch at an iCloud Drive path and a local destination. That's it.

$ python ifetch/cli.py Documents/Photos ~/Backups/Photos
======================================================================
iCloud Drive Downloader
Remote Path: Documents/Photos
Local Path: /Users/you/Backups/Photos
Parallel Workers: 4
======================================================================
Authenticating with iCloud... ✓
Downloading from 'Documents/Photos' to '~/Backups/Photos'

Command Reference

Everything is driven from the terminal. Here's the full reference.

Download Files & Folders

Recursively download any iCloud Drive path to a local directory with parallel workers, retry logic, and differential updates.

# Basic download
$ python ifetch/cli.py Documents/Photos ~/Downloads/icloud-photos

# Advanced: 8 workers, 5 retries, 2MB chunks, JSON log
$ python ifetch/cli.py Documents/Code ~/Work/Code \
    --email=you@apple.com \
    --max-workers=8 \
    --max-retries=5 \
    --chunk-size=2097152 \
    --log-file=download.log

List Directory Contents

Preview what's in an iCloud Drive directory without downloading anything.

$ python ifetch/cli.py Documents --list

Listing contents of 'Documents':
──────────────────────────────────────
📁 Photos
📁 Programming
📄 resume.pdf
📄 budget.xlsx

Shared Items

Browse and download files and folders that others have shared with your iCloud account.

$ python ifetch/cli.py --list-shared --email you@apple.com

Listing top-level shared items:
──────────────────────────────────────
📁 Team Assets
📁 Vacation Photos
📄 contract_final.pdf

Profile-Based Filtering

Define include/exclude glob patterns in a JSON profile to sync only what you need.

# ~/.ifetch_profiles.json
{
  "pdf_backup": {
    "include": ["Documents/**/*.pdf"],
    "exclude": ["Documents/Private/*"]
  }
}

$ python ifetch/cli.py Documents ~/PDFs \
    --profile pdf_backup \
    --email you@apple.com

Extend with Plugins

Drop a Python file in plugins/, subclass BasePlugin, and hook into authentication, download progress, and completion events.

# plugins/notify.py
from ifetch.plugin import BasePlugin

class Notify(BasePlugin):
    def after_download(self, remote_item, local_path, success, **kw):
        if success:
            print(f"✓ {remote_item.name} → {local_path}")

iFetch auto-discovers all plugins on startup. No configuration needed.

All CLI Flags

Flag Description Default
--emailiCloud account email (or ICLOUD_EMAIL env var)env / prompt
--max-workers NNumber of concurrent download threads4
--max-retries NRetry attempts per failed chunk (exponential backoff)3
--chunk-size BYTESByte size for each differential-download chunk1 MB
--log-file PATHPath to save structured JSON logsconsole only
--listList directory contents only (no downloads)off
--list-sharedList top-level items shared with youoff
--profile NAMEApply include/exclude patterns from profile fileno filter
--profile-file PATHCustom path to profile JSON~/.ifetch_profiles.json