bread-utils extracts genuinely duplicated logic found across breadbox, breadclip, breadmon, breadcrumbs, bos-settings, and breadhelp: - hypr: Hyprland socket1 request/response client (breadbox's get_active_workspace + breadclip's position.rs hyprctl_json were near-identical), socket2 path resolution (breadmon), and a version-tolerant `fullscreen` field parser (Hyprland has shipped both bool and int representations across versions). - singleton: correct flock-based single-instance toggle, replacing the TOCTOU-prone read-pid/check-proc/kill/write-pid pattern duplicated verbatim between breadbox and breadclip (breadclip's own comment says "matches breadbox pattern"). - proc: breadcrumbs' timeout-guarded subprocess runner, promoted verbatim as the one implementation in the ecosystem that already got this right. - atomic + xdg: atomic (temp-then-rename) file writes with an optional .bak-before-overwrite variant, and XDG path helpers that never fall back to a literal "~/..." string (the exact breadclip-core and breadpad-shared bug: PathBuf never expands `~`). - tomlcfg (feature "toml"): the load_doc/save_doc TOML-editing discipline bos-settings and breadhelp both implemented byte-for-byte identically in the same fix pass that introduced it. - gtk_popup (feature "gtk"): layer-shell overlay window setup, visible-row navigation, and click-outside-close, deduplicated from breadbox and breadclip (~150 duplicated lines, per both apps' own "same as breadbox" comments). bread-onnx extracts the embedding pipeline (tokenize -> tensor build -> mean-pool -> L2-normalize) duplicated near-verbatim between breadarr and breadsearch, a shared execution-provider session builder with loud EP- registration logging, and a model download+integrity helper. Defaults AMD iGPU acceleration to ort::ep::MIGraphX (not ROCm) per this machine's own breadsearch-gpu-backends lesson: ROCMExecutionProvider silently no-ops to CPU on distro ROCm onnxruntime builds compiled with --use_migraphx. Both crates build and pass their own test suites standalone. Consumer migrations follow in subsequent commits.
112 lines
4.3 KiB
Rust
112 lines
4.3 KiB
Rust
//! Model download + integrity checking.
|
|
//!
|
|
//! `breadarrd/src/matcher/mod.rs::download` (async, `reqwest`) and
|
|
//! `breadmill/src/main.rs::download_if_missing` (sync, `ureq`) independently
|
|
//! implement "download to a temp file, then rename over the destination"
|
|
//! for fetching an ONNX model/tokenizer if it isn't already present —
|
|
//! genuinely duplicated intent, different HTTP clients. Neither verifies
|
|
//! the download's integrity beyond "the response wasn't empty". This module
|
|
//! is a fresh, shared implementation (sync, `ureq` — matching this
|
|
//! workspace's existing `bakery` convention for downloads) that adds an
|
|
//! optional SHA-256 check, built on [`bread_utils::atomic::write_atomic_bytes`]
|
|
//! for the same crash-safety property both originals already had.
|
|
//!
|
|
//! `breadarrd`'s async caller should wrap a call to [`ensure_file`] in
|
|
//! `tokio::task::spawn_blocking` rather than block its async runtime
|
|
//! directly — see that crate's migration for the concrete pattern.
|
|
|
|
use std::io::Read;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use sha2::{Digest, Sha256};
|
|
|
|
/// Download `url` to `dest` if `dest` doesn't already exist. If
|
|
/// `expected_sha256` is given, verifies the downloaded bytes against it
|
|
/// (case-insensitive hex) before the atomic rename and returns an error on
|
|
/// mismatch — the temp file is discarded, `dest` is left untouched. An
|
|
/// already-present `dest` is trusted as-is and not re-verified (matches
|
|
/// both original implementations' "if it exists, skip" behavior; re-hashing
|
|
/// a ~90MB+ model file on every startup would be wasted work for the common
|
|
/// case of a stable, previously-verified file).
|
|
pub fn ensure_file(url: &str, dest: &Path, expected_sha256: Option<&str>) -> anyhow::Result<PathBuf> {
|
|
if dest.exists() {
|
|
return Ok(dest.to_path_buf());
|
|
}
|
|
|
|
tracing::info!("bread-onnx: downloading {url} -> {}", dest.display());
|
|
let agent = ureq::AgentBuilder::new()
|
|
.timeout(std::time::Duration::from_secs(300))
|
|
.build();
|
|
let response = agent
|
|
.get(url)
|
|
.call()
|
|
.map_err(|e| anyhow::anyhow!("failed to download {url}: {e}"))?;
|
|
|
|
let mut bytes = Vec::new();
|
|
response
|
|
.into_reader()
|
|
.read_to_end(&mut bytes)
|
|
.map_err(|e| anyhow::anyhow!("failed to read response body from {url}: {e}"))?;
|
|
|
|
if bytes.is_empty() {
|
|
anyhow::bail!("empty download from {url}");
|
|
}
|
|
|
|
if let Some(expected) = expected_sha256 {
|
|
let actual = sha256_hex(&bytes);
|
|
if !actual.eq_ignore_ascii_case(expected) {
|
|
anyhow::bail!(
|
|
"checksum mismatch for {url}: expected {expected}, got {actual} — refusing to install"
|
|
);
|
|
}
|
|
tracing::info!("bread-onnx: verified sha256 for {}", dest.display());
|
|
}
|
|
|
|
if let Some(parent) = dest.parent() {
|
|
std::fs::create_dir_all(parent)?;
|
|
}
|
|
bread_utils::atomic::write_atomic_bytes(dest, &bytes, None)
|
|
.map_err(|e| anyhow::anyhow!("failed to write {}: {e}", dest.display()))?;
|
|
|
|
tracing::info!(
|
|
"bread-onnx: saved {} ({:.1} MB)",
|
|
dest.display(),
|
|
bytes.len() as f64 / 1_048_576.0
|
|
);
|
|
Ok(dest.to_path_buf())
|
|
}
|
|
|
|
pub fn sha256_hex(data: &[u8]) -> String {
|
|
let mut hasher = Sha256::new();
|
|
hasher.update(data);
|
|
hex::encode(hasher.finalize())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn sha256_hex_matches_known_vector() {
|
|
// sha256("") — well-known empty-input digest.
|
|
assert_eq!(
|
|
sha256_hex(b""),
|
|
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn ensure_file_skips_download_when_already_present() {
|
|
let dir = std::env::temp_dir().join(format!("bread-onnx-download-test-{}", std::process::id()));
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
let dest = dir.join("model.onnx");
|
|
std::fs::write(&dest, b"already here").unwrap();
|
|
|
|
// A bogus URL would fail if actually requested — success here proves
|
|
// the existing-file short-circuit fired instead of dialing out.
|
|
let result = ensure_file("http://127.0.0.1:1/unreachable", &dest, None);
|
|
assert!(result.is_ok());
|
|
assert_eq!(std::fs::read(&dest).unwrap(), b"already here");
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
}
|
|
}
|