Skip to main content

hibp_sync_client/
lib.rs

1//! Syncs a local HIBP sha1t48 dataset from an
2//! [hibp-bin-fetch](https://crates.io/crates/hibp-bin-fetch) serve instance, with
3//! support for full syncs, delta syncs, and crash-safe resumption.
4//!
5//! This is the client counterpart to `hibp-bin-fetch serve`. It runs as a persistent
6//! daemon, waking once per night at a configurable UTC time to pull whichever prefix files
7//! have changed since the last sync, or all of them on first run. The resulting dataset is
8//! compatible with [hibp-verifier](https://crates.io/crates/hibp-verifier) for
9//! sub-microsecond password breach checking.
10//!
11//! # Nightly Schedule
12//!
13//! The daemon sleeps until the configured `--sync-at` time (UTC, default `04:00`), runs a
14//! sync cycle, then sleeps until the same time the following day. Set `--sync-at` to a time
15//! comfortably after the server's `--download-at` time so the server has finished its own
16//! nightly download before the client requests changes. A sync failure is logged as an error
17//! but does not exit the process - the client waits for the next scheduled cycle.
18//!
19//! Pass `--sync-on-start` to run an immediate sync cycle before entering the nightly
20//! schedule, which is useful on first deployment or after an extended outage.
21//!
22//! # Sync Modes
23//!
24//! ## Full Sync
25//!
26//! On first run (no local dataset yet), every prefix file is fetched from the server.
27//! The server divides the full dataset into `--segments` chunks and the client fetches
28//! them sequentially, decompressing each segment and writing the prefix files to a staging
29//! directory before atomically moving them to the data directory.
30//!
31//! ## Delta Sync
32//!
33//! After each successful sync the client writes the server's `last_updated` timestamp
34//! to a local state file. This timestamp acts as a version identifier. On the next run,
35//! the client asks the server for its `prev_last_updated` - the timestamp of the cycle
36//! immediately before the current one. If that matches the locally stored timestamp, the
37//! client is exactly one cycle behind and only the changed prefixes need to be fetched.
38//!
39//! If the timestamps do not match (e.g. the client has been offline for multiple nightly
40//! cycles and the server has since moved on), delta sync is not possible and the client
41//! falls back to a full sync automatically.
42//!
43//! # Crash-Safe Design
44//!
45//! All downloads land in a `.staging/` subdirectory of the data directory before being
46//! committed. If the process is interrupted mid-download, the next run resumes from where
47//! it left off - completed segments are not re-fetched. If the server's data has changed
48//! between attempts, the stale staging directory is discarded and a fresh sync starts.
49//!
50//! # Usage
51//!
52//! First, ensure an `hibp-bin-fetch serve` instance is running and accessible.
53//!
54//! ## CLI
55//!
56//! Run as a persistent daemon (syncs nightly at 04:00 UTC by default):
57//!
58//! ```sh
59//! hibp-sync-client --server-url http://192.168.1.10:8765 --data-dir ./hibp-data
60//! ```
61//!
62//! Sync immediately on startup, then continue on the nightly schedule:
63//!
64//! ```sh
65//! hibp-sync-client --server-url http://192.168.1.10:8765 --data-dir ./hibp-data --sync-on-start
66//! ```
67//!
68//! Sync at a custom time and with finer resume granularity:
69//!
70//! ```sh
71//! hibp-sync-client --server-url http://192.168.1.10:8765 --data-dir ./hibp-data \
72//!     --sync-at 05:30 --segments 32
73//! ```
74//!
75//! ## Library
76//!
77//! ```rust,ignore
78//! use hibp_sync_client::sync::{Config, Outcome, sync};
79//! use std::path::PathBuf;
80//!
81//! let config = Config {
82//!     server_url: "http://192.168.1.10:8765".parse().unwrap(),
83//!     data_dir: PathBuf::from("./hibp-data"),
84//!     segments: 16,
85//! };
86//!
87//! match sync(&config).await? {
88//!     Outcome::UpToDate => println!("already up to date"),
89//!     Outcome::DeltaSync { changed_count } => println!("{changed_count} files updated"),
90//!     Outcome::FullSync { file_count } => println!("{file_count} files written"),
91//! }
92//! ```
93
94pub mod client;
95pub mod error;
96pub mod sync;
97pub mod wire;