A small utility for migrating photos from flickr to grain

Files
README.md

flickr2grain #

Local utility for migrating Flickr albums to grain.social as atproto records. Runs on your machine — no cloud component, no telemetry.

Quick start #

  1. npm install
  2. Register a Flickr app (see docs/flickr-setup.md) and put the credentials in .env.local.
  3. npm run dev
  4. Visit http://127.0.0.1:5173, click Connect Flickr, complete the OAuth flow.
  5. Click Connect atproto, enter your handle, complete the OAuth flow at your PDS.
  6. Pick an album. Click Migrate. Progress streams live.

Development #

  • npm run typecheck — strict TypeScript
  • npm run test — full Vitest suite (unit + integration)
  • npm run build — production build to dist/

Platform paths #

flickr2grain stores credentials + its state database at platform-appropriate locations:

OS Path
macOS ~/Library/Application Support/flickr2grain/
Linux ~/.local/share/flickr2grain/
Windows %LOCALAPPDATA%\flickr2grain\

The directory contains:

  • db.sqlite — main state (mode 0600).
  • logs/migration-<id>.ndjson — one file per migration, greppable state-transition log.

Resetting #

Two ways:

  1. In-app reset button (recommended): wipes credentials but keeps migration history. All auth must be redone before the next migration. If you reconnect under a different atproto handle after reset, prior migrations will be preserved as history but cannot be resumed against the new DID — start a fresh migration instead.
  2. Full wipe: rm -rf "~/Library/Application Support/flickr2grain" (or the platform equivalent). This deletes everything — credentials, migration history, logs.

Security model — v1 (local-only) #

flickr2grain stores plaintext OAuth tokens on disk with file mode 0600 (user-only readable). This is appropriate for the v1 threat model:

  • Single-user trusted machine: the user is expected to have full-disk encryption enabled and to be the only person with access to the machine.
  • No network-exposed interface: the server binds to 127.0.0.1 only.
  • No remote state: all credentials live on this one machine.

Explicit non-goal: hosted deployment. If this utility is ever repackaged as a hosted service for multiple users, the token storage layer MUST be replaced with a KMS-backed encrypted store (e.g. AWS KMS, HashiCorp Vault, GCP KMS) with per-user data-encryption keys, rotation, and audit logging. The current flickr_tokens / atproto_sessions repository interfaces are narrow enough that such a migration is localised to src/storage/repos/.

Tokens are wiped by:

  • The in-app Reset button.
  • POST /api/reset.
  • Deleting the platform data directory (full wipe).

Data flow — what lands where #

  • Flickr: only read operations — album list, photo metadata, EXIF, original-file download.
  • atproto: only write operations — uploadBlob, createRecord for social.grain.photo, social.grain.photo.exif, social.grain.gallery, social.grain.gallery.item.
  • Nothing is sent anywhere else.

Troubleshooting #

  • "Reset the Flickr/atproto connection": Click the in-app reset, then re-authenticate. If that doesn't work, do a full wipe.
  • "Blob upload rejected": The uploaded blob exceeded the PDS's lexicon-declared limit. If this happens consistently, pass { maxBytes: 1_000_000 } when starting a migration (the utility defaults to 2 MB per user-confirmed lexicon update; some PDSes may still enforce 1 MB literally).
  • "Migration halted with pds_rejected": The PDS refused a record's shape. Likely a lexicon change. Check logs/migration-<id>.ndjson for the exact error; the fix is typically a record-builder update.
  • "EXIF fields look wrong": Run the verification procedure in docs/exif-verification.md.

End-to-End Smoke Test (Operational Checklist) #

With real Flickr credentials in .env.local, verify: