How It Works¶
The migration pipeline processes Google Takeout archives through five stages:
graph TD
A[Google Takeout<br/>.tgz archives] --> B[1. Streaming Reader]
B --> C[2. EXIF Processor]
C --> D[3. Upload Engine]
D --> E[4. Album Creator]
D --> F[5. State Tracker]
B -->|album manifest| E
B -->|album membership| F
F -.->|resume state| D
F -.->|accumulated albums| E
1. Streaming Reader¶
Two input modes are supported, both implementing the same streaming interface:
| Mode | Flag | Behaviour |
|---|---|---|
| Archive (recommended) | --takeout-archive <file.tgz> |
Reads a .tgz/.tar.gz directly, entry-by-entry — no extraction, no extra disk space |
| Directory | --takeout-dir <dir> |
Walks an already-extracted Takeout/ directory |
Instead of extracting gigabytes to disk, the archive reader decompresses the gzip stream and iterates tar entries in memory:
graph LR
A[.tgz file] --> B[gzip decompress]
B --> C[tar entry iterator]
C --> D{Entry type?}
D -->|photo/video| E[Stream to EXIF]
D -->|.json sidecar| F[Parse metadata]
D -->|metadata.json| G[Extract album map]
F --> H[MediaMeta: timestamp,<br/>GPS, camera]
G --> I[AlbumManifest: album<br/>→ file membership]
The reader also extracts album manifests from Google's metadata.json and the
per-photo sidecars, mapping which photos belong to which albums. When you
process multiple archives in separate sync runs, this membership is recorded
to the SQLite state database after each run, so albums that span multiple
archives are accumulated and finally recreated by gphoto2proton
albums-finalize.
2. EXIF Processor¶
Google Takeout stores metadata in .json sidecar files alongside each photo,
but the actual image files often lack proper EXIF data. This stage writes the
metadata back into the image.
For each media file, the processor:
- Reads the JSON sidecar for
photoTakenTime,geoData, and camera info - Constructs an
exiftoolcommand with the correct arguments - Pipes the raw image through
exiftool— output is a new image with EXIF tags embedded
Tags restored:
| EXIF Tag | Source |
|---|---|
DateTimeOriginal |
JSON photoTakenTime.timestamp |
GPSLatitude / GPSLongitude |
JSON geoData.latitude / .longitude |
GPSAltitude |
JSON geoData.altitude |
Make / Model |
JSON camera metadata |
ImageDescription |
JSON description |
FileModifyDate |
Capture timestamp |
If exiftool is not installed, the stage is skipped with a warning.
3. Upload Engine¶
Uploads photos to Proton Drive using the Proton-API-Bridge SDK.
graph TD
A[Photo stream] --> B[Proton-API-Bridge<br/>SDK]
B --> C{Session saved?}
C -->|No| D[Login with<br/>--username / --password]
D --> E[Session stored<br/>locally]
E --> B
C -->|Yes| F[Upload to Proton Drive]
F --> G[Return Proton File ID]
The uploader:
- Authenticates directly with the Proton API (SRP login) — no OAuth2, no browser, fully headless
- Alternatively reuses an existing session imported from the proton-drive CLI
via
gphoto2proton import-session— no password or CAPTCHA needed - Detects MIME type from file extension
- Streams the photo data directly — no temp file on disk
- Saves the authenticated session for reuse on later runs
- Maps local filenames to Proton file IDs for album creation
See Authentication for the full login story.
Supported formats:
| Extension | MIME Type |
|---|---|
.jpg / .jpeg |
image/jpeg |
.png |
image/png |
.heic |
image/heic |
.mov |
video/quicktime |
.mp4 |
video/mp4 |
.cr2 |
image/x-canon-cr2 |
.nef |
image/x-nikon-nef |
.arw |
image/x-sony-arw |
4. Album Creator¶
Albums are created from the album manifest of the current input. When you
process several archives across multiple sync runs, album membership is also
persisted to the state database after every run; the gphoto2proton
albums-finalize command then recreates the accumulated, cross-archive albums in
one pass.
For each album:
- Looks up the Proton file ID for each photo in the album
- Creates the album via the Proton Photos HTTP API
- Adds the photos to the album
- Records the album state in the SQLite tracker
5. State Tracker¶
A SQLite database (pure Go, no CGo) tracks every file's migration state:
- Pending — Not yet processed
- Processing — Currently uploading
- Uploaded — Successfully uploaded
- Failed — Upload error occurred
- Skipped — Skipped (resume mode, already uploaded)
It also keeps an album_members table that accumulates album membership across
archive runs. On resume, the tracker identifies completed files (skipped) and
failed files (retried), ensuring the migration picks up exactly where it left
off without re-uploading anything.
Error Handling¶
The pipeline is designed for resilience:
- Per-file errors — A failed upload does not stop the entire migration
- Album creation errors — Failed albums are logged and skipped, migration continues
- State persistence — Every success is recorded immediately
- Resume — Interrupted runs restart from the last checkpoint