Conversation
cargo fmt --check was already failing on main for this file; fixing it separately so the importer change stays clean.
…ation Phase A of the ingestion design in #26; implements the importer from #25. New 'import <path.mbox>' subcommand: a streaming, header-only mbox parser (bounded memory regardless of file size, mboxrd >From escaping, CRLF, capped header lines, malformed-region recovery with skip counts). Messages dedupe into seen_mails under the 'mid:<Message-ID>' namespace so re-imports add zero counts and RFC ids can never collide with Gmail API ids. Sender extraction shares the scan's From/Return-Path priority and cleanup, so counts are consistent across sources. Ingestion coordination, shared by scan and import: - ingest_runs table (migration 1) records one row per run: source, state (running/done/failed/cancelled/abandoned), pid, timestamps with a ~2s heartbeat, messages_seen/new, bytes_done/bytes_total for imports (the byte offset doubles as the resume point), resume_token for scans, and error kind/message on failure. All writes ride the existing single db_writer channel. - Schema versioning via PRAGMA user_version with guarded migrations; the pre-existing tables are the version-0 baseline and old databases upgrade transparently on startup. Databases newer than the binary are refused. - Exclusive kernel flock on <db>.ingest.lock before touching the database; a second ingester exits promptly with a clear message, and the next ingester marks rows orphaned by a dead one as abandoned (liveness is judged by the lock, never by pids or heartbeats). flock comes from rustix (fs feature only): safe wrappers, no unsafe in this crate. - SIGTERM/SIGINT stop fetching/parsing, drain the writer, persist resume state, mark the row cancelled, and exit 0. 'import --resume <run_id>' continues from the recorded offset after validating a file fingerprint (size + mtime + hash of the first 64 KiB), falling back to a full re-parse (still correct via dedupe) if the file changed. - --db/--credentials/--tokens flags with GMAIL_STATS_* env fallbacks and the previous cwd-relative defaults; bare 'cargo run' remains the OAuth scan. Output is now quiet-ish by default (periodic progress lines); --verbose restores the per-message sender lines, which were also a privacy leak, and --quiet reduces output to errors plus the summary. The scan's credential/auth failures now land as failed run rows with an error kind instead of panics, and it records a best-effort total_estimate from users.getProfile for future progress display. Tests cover the parser fixtures (escaping, CRLF, folding, malformed regions, truncation, resume offsets, ~50MB on-disk and ~64MB generated streams), import idempotency, resume with matching and stale fingerprints, cancellation, the flock refusing a second ingester, and migration of a version-0 database. README documents both ingestion modes, the Takeout how-to including the ~2 day Advanced Protection delay, the new flags, and the interim double-counting caveat when mixing sources in one database.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #25
This is Phase A of the ingestion design in #26: the CLI-only importer plus the coordination primitives (run tracking, locking, schema versioning, clean shutdown, path flags). Deliberately no web/viewer changes and no new endpoints — but a terminal scan already writes live run rows, and Ctrl-C yields a clean
cancelledrow with resume state, ready for Phase B's read-only progress UI.What's here
gmail_stats import <path.mbox>— streaming, header-only mbox importer (src/mbox.rs): bounded memory regardless of file size (capped reusable line buffer over aBufReader; bodies skipped without interpretation), mboxrd>Fromescaping, CRLF, over-long lines truncated but fully consumed so framing survives, malformed regions skipped to the next validFromseparator and counted (as are messages missing aMessage-ID). Dedupe key ismid:<Message-ID>inseen_mails— the permanent namespace rule from Design proposal: drive scans and imports from the web UI #26 — so re-imports add zero counts and RFC ids can't collide with Gmail API ids. Sender extraction reuses the scan's From→Return-Path priority (sharedpick_sender) andcleanup_sender, so counts are consistent across sources.bytes_doneon the run row is the committed parse offset (only ever advanced behind the FIFO writer channel, so it never points past uncommitted work).import <path> --resume <run_id>validates a fingerprint (size + mtime + FNV-1a of the first 64 KiB) and seeks to the offset; on mismatch it falls back to a full re-parse, which dedupe keeps correct.ingest_runs(src/ingest.rs): one row per run — source, state (running/done/failed/cancelled/abandoned), pid, started/updated/finished timestamps with a ~2s heartbeat,messages_seen/messages_new,bytes_done/bytes_total, persisted scanresume_token,error_kind/error. Written by both modes through the existing single-writer channel (newWriteMsg::Progressvariant next toSeen);messages_newincrements inside the same transaction as the insert, so it's exact across writer restarts. The next ingester marks rows orphaned by a dead process asabandoned— safe because liveness is judged by the flock, never pids/heartbeats.PRAGMA user_version+ guarded migrations. Existing tables are the version-0 baseline (still applied as idempotent DDL);ingest_runsis migration 1. Old DBs upgrade transparently on startup; DBs newer than the binary are refused with a clear message.flockon<db>.ingest.lockbefore touching the DB; a second ingester exits promptly with "another ingester is already running…". Crate choice: rustix (fsfeature only) — safe wrappers so this crate stays free ofunsafe, much smaller thannix, and maintained; Unix-only per maintainer decision 6.cancelled, exits 0 (a second signal aborts immediately). The scan also stops advancing its resume token past a partially-fetched page.--db,--credentials,--tokenswithGMAIL_STATS_DB/_CREDENTIALS/_TOKENSenv fallbacks and the previous cwd-relative defaults; barecargo runis still the OAuth scan. Arg parsing is hand-rolled (~90 lines incl.--flag=value) — a clap dependency didn't seem justified for two subcommands and six flags.mainprinted asender: …line per message — noisy and it sprays inbox metadata into the terminal, so per the design note this is now off by default. Default output is a periodic progress line;--verboserestores per-message detail;--quietleaves only errors + the final summary.credentials.jsonand authenticator failures now produce afailedrun row witherror_kind(missing_credentials,auth_required,policy_enforceddetected by message) instead of a panic, plus a hint pointing APP users at Takeout; best-efforttotal_estimatefromusers.getProfile.The first commit is a whitespace-only
rustfmtpass onsrc/bin/web.rs(it was already failingcargo fmt --checkonmain); no functional web change, and easy to drop if unwanted.Testing
cargo build,cargo test(55 tests),cargo clippy --all-targets -- -D warnings, andcargo fmt --checkall pass.>Fromescaping (and the unescaped-separator case), missing Message-ID, malformed-region recovery with skip counts, folded headers, case-insensitivity/first-wins, 24 KiB lines, header-only messages, resume-from-offset; plus a ~50 MB on-disk mbox and a ~64 MB generated stream (aReadimpl that synthesizes messages on the fly, so the input provably never exists in memory).mid:namespacing verified; resume with matching fingerprint imports only the tail; stale fingerprint falls back to a full parse; resuming a scan run is rejected; pre-cancelled import yields acancelledrow; non-mbox files are refused by theFromsniff.user_versionbumped), newer-DB refusal; run-row lifecycle (running→abandoned/failed); writer dedupe + progress accounting.cancelledrow at 87,921 messages;--resumethen imported exactly the remaining 32,079 (500 senders, 120,000 total — no overlap, no gap); a scan with missing credentials recordedfailed/missing_credentialsand exited nonzero without touching OAuth.Not exercised: the real OAuth flow / live Gmail API (no credentials in this environment). The scan path was refactored mechanically (channel message type, run row, cancellation checks) and compiles/passes the existing transient-error tests, but a live end-to-end scan should get a once-over before/after merge.
total_estimateviaget_profileis best-effort and ignored on failure, so it can't break a scan.Deferred by design (per the #26 phase plan): consuming the persisted scan
resume_tokenon a later scan (decision 4 is still open — it's persisted, not yet read back),awaiting_auth/auth_urlstates and the auth timeout (Phase C), and cross-source dedupe (Phase D).