use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::process::Command;
struct RawFile {
owner: &'static str,
repo: &'static str,
commit: &'static str,
path: &'static str,
sha256: &'static str,
bytes: u64,
}
impl RawFile {
fn url(&self) -> String {
format!(
"https://raw.githubusercontent.com/{}/{}/{}/{}",
self.owner, self.repo, self.commit, self.path
)
}
}
struct ReleaseAsset {
owner: &'static str,
repo: &'static str,
tag: &'static str,
name: &'static str,
sha256: &'static str,
bytes: u64,
}
impl ReleaseAsset {
fn url(&self) -> String {
format!(
"https://github.com/{}/{}/releases/download/{}/{}",
self.owner, self.repo, self.tag, self.name
)
}
}
const IS_FAA: RawFile = RawFile {
owner: "thanhleviet", repo: "ISfinder-sequences",
commit: "bedbeb5c16618eca3e16ef74adf865d6b88cafb5", path: "IS.faa",
sha256: "7672fecd114c962c2d8640b70324e10b9a2f111c9ad6d40112800fdb65213b12", bytes: 3_324_545,
};
const IS_CSV: RawFile = RawFile {
owner: "thanhleviet", repo: "ISfinder-sequences",
commit: "bedbeb5c16618eca3e16ef74adf865d6b88cafb5", path: "IS.csv",
sha256: "1d7d4fa2d33488f2a21a6ca1ad908d4960267582b56addcfa5e098a5414cd5e2", bytes: 893_572,
};
const IS_FNA: RawFile = RawFile {
owner: "thanhleviet", repo: "ISfinder-sequences",
commit: "bedbeb5c16618eca3e16ef74adf865d6b88cafb5", path: "IS.fna",
sha256: "47607f9e398d200f1954b18b9d91d6f5a612331ee137cb10d63897688f9d5507", bytes: 8_697_930,
};
const ISOSDB_ZIP: RawFile = RawFile {
owner: "joshuakirsch", repo: "pseudoR",
commit: "9e44803ddca7f2af678c72c9f02284e94a204164", path: "ISOSDB.V3.fna.zip",
sha256: "f24b89f3fee3d7e929a745d7a3587fdef2e3ca5da7792451fa510b5b4134e60a", bytes: 9_910_945,
};
const IS_FAM_ANNOT: RawFile = RawFile {
owner: "joshuakirsch", repo: "pseudoR",
commit: "9e44803ddca7f2af678c72c9f02284e94a204164", path: "IS_fam_annot.txt",
sha256: "41d5120ea638456829029a02c91abc24943f3bdb71fd663a314356d2ba3a34d0", bytes: 367_225,
};
const ISOSDB_ZIP_MEMBER: &str = "ISOSDB.V3.fna";
const ISOSDB_FNA_SHA256: &str = "31541c4348da01e19db8be71c8f44b0e21c3462d36bfcd217aee3315474e7748";
const HOST_NEG: ReleaseAsset = ReleaseAsset {
owner: "necoli1822", repo: "rust-ise", tag: "v0.2.3", name: "negharvest_reps.fna",
sha256: "35dbe24307005b9da14d881698bd0195072ed56e7a29ca39fa377fa3973fdfbd", bytes: 35_668_576,
};
pub struct Sources {
pub isosdb_fna: PathBuf,
pub fam_annot: PathBuf,
pub isfinder_faa: PathBuf,
pub isfinder_csv: PathBuf,
pub isfinder_fna: PathBuf,
pub host: Option<PathBuf>,
}
pub fn default_cache() -> PathBuf {
if let Some(x) = std::env::var_os("XDG_CACHE_HOME") {
return PathBuf::from(x).join("rust-ise").join("sources");
}
if let Some(h) = std::env::var_os("HOME") {
return PathBuf::from(h).join(".cache").join("rust-ise").join("sources");
}
std::env::temp_dir().join("rust-ise-sources")
}
fn sha256_hex(path: &Path) -> Result<String, String> {
use sha2::{Digest, Sha256};
let mut f = File::open(path).map_err(|e| format!("open {}: {e}", path.display()))?;
let mut hasher = Sha256::new();
let mut buf = [0u8; 1 << 16];
loop {
let n = f.read(&mut buf).map_err(|e| format!("read {}: {e}", path.display()))?;
if n == 0 {
break;
}
hasher.update(&buf[..n]);
}
let digest = hasher.finalize();
let mut s = String::with_capacity(64);
for b in digest {
s.push_str(&format!("{b:02x}"));
}
Ok(s)
}
fn matches(path: &Path, expect: &str) -> bool {
path.exists() && sha256_hex(path).map(|h| h == expect).unwrap_or(false)
}
fn curl(url: &str, dest: &Path) -> Result<(), String> {
let dest_s = dest.to_str().ok_or("non-utf8 dest path")?;
let status = Command::new("curl")
.args([
"-sSL", "--fail", "--retry", "3", "--retry-delay", "2", "--max-time", "600",
"-o", dest_s, url,
])
.status()
.map_err(|e| format!("failed to launch curl (is it on PATH?): {e}"))?;
if !status.success() {
return Err(format!("curl failed ({status}) for {url}"));
}
Ok(())
}
fn verify(path: &Path, bytes: u64, sha256: &str) -> Result<(), String> {
let got = std::fs::metadata(path).map_err(|e| format!("stat {}: {e}", path.display()))?.len();
if got != bytes {
let _ = std::fs::remove_file(path);
return Err(format!("{}: size {got} != expected {bytes}", path.display()));
}
let h = sha256_hex(path)?;
if h != sha256 {
let _ = std::fs::remove_file(path);
return Err(format!("{}: sha256 {h} != expected {sha256}", path.display()));
}
Ok(())
}
fn fetch_to(url: &str, basename: &str, bytes: u64, sha256: &str, cache: &Path) -> Result<PathBuf, String> {
let dest = cache.join(basename);
if matches(&dest, sha256) {
eprintln!("[build-db] cached {basename} (sha256 ok)");
return Ok(dest);
}
eprintln!("[build-db] downloading {basename} …");
let part = cache.join(format!("{basename}.part"));
curl(url, &part)?;
verify(&part, bytes, sha256)?;
std::fs::rename(&part, &dest).map_err(|e| format!("rename {}: {e}", dest.display()))?;
Ok(dest)
}
fn fetch_raw(spec: &RawFile, cache: &Path) -> Result<PathBuf, String> {
let base = Path::new(spec.path)
.file_name()
.and_then(|s| s.to_str())
.ok_or("bad source path")?;
fetch_to(&spec.url(), base, spec.bytes, spec.sha256, cache)
}
fn fetch_release(a: &ReleaseAsset, cache: &Path) -> Result<PathBuf, String> {
fetch_to(&a.url(), a.name, a.bytes, a.sha256, cache)
}
fn fetch_isosdb_fna(cache: &Path) -> Result<PathBuf, String> {
let dest = cache.join(ISOSDB_ZIP_MEMBER);
if matches(&dest, ISOSDB_FNA_SHA256) {
eprintln!("[build-db] cached {ISOSDB_ZIP_MEMBER} (sha256 ok)");
return Ok(dest);
}
let zip_path = fetch_raw(&ISOSDB_ZIP, cache)?;
eprintln!("[build-db] extracting {ISOSDB_ZIP_MEMBER} from ISOSDB.V3.fna.zip …");
let f = File::open(&zip_path).map_err(|e| format!("open {}: {e}", zip_path.display()))?;
let mut archive = zip::ZipArchive::new(f).map_err(|e| format!("open zip: {e}"))?;
let mut entry = archive
.by_name(ISOSDB_ZIP_MEMBER)
.map_err(|e| format!("zip member {ISOSDB_ZIP_MEMBER}: {e}"))?;
let part = cache.join(format!("{ISOSDB_ZIP_MEMBER}.part"));
{
let mut out = File::create(&part).map_err(|e| format!("create {}: {e}", part.display()))?;
std::io::copy(&mut entry, &mut out).map_err(|e| format!("extract: {e}"))?;
}
let h = sha256_hex(&part)?;
if h != ISOSDB_FNA_SHA256 {
let _ = std::fs::remove_file(&part);
return Err(format!("{ISOSDB_ZIP_MEMBER}: sha256 {h} != expected {ISOSDB_FNA_SHA256}"));
}
std::fs::rename(&part, &dest).map_err(|e| format!("rename {}: {e}", dest.display()))?;
Ok(dest)
}
pub fn fetch_sources(cache: &Path, want_host: bool) -> Result<Sources, String> {
std::fs::create_dir_all(cache).map_err(|e| format!("mkdir {}: {e}", cache.display()))?;
eprintln!("[build-db] source cache: {}", cache.display());
let isfinder_faa = fetch_raw(&IS_FAA, cache)?;
let isfinder_csv = fetch_raw(&IS_CSV, cache)?;
let isfinder_fna = fetch_raw(&IS_FNA, cache)?;
let fam_annot = fetch_raw(&IS_FAM_ANNOT, cache)?;
let isosdb_fna = fetch_isosdb_fna(cache)?;
let host = if want_host {
Some(fetch_release(&HOST_NEG, cache)?)
} else {
None
};
Ok(Sources { isosdb_fna, fam_annot, isfinder_faa, isfinder_csv, isfinder_fna, host })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn urls_are_commit_pinned() {
assert_eq!(
IS_FAA.url(),
"https://raw.githubusercontent.com/thanhleviet/ISfinder-sequences/\
bedbeb5c16618eca3e16ef74adf865d6b88cafb5/IS.faa"
);
assert!(ISOSDB_ZIP.url().contains("9e44803ddca7f2af678c72c9f02284e94a204164"));
assert_eq!(
HOST_NEG.url(),
"https://github.com/necoli1822/rust-ise/releases/download/v0.2.3/negharvest_reps.fna"
);
for s in [
IS_FAA.sha256, IS_CSV.sha256, IS_FNA.sha256, ISOSDB_ZIP.sha256,
IS_FAM_ANNOT.sha256, ISOSDB_FNA_SHA256, HOST_NEG.sha256,
] {
assert_eq!(s.len(), 64, "sha256 must be 64 hex chars: {s}");
assert!(s.bytes().all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase()));
}
}
#[test]
fn sha256_of_known_file() {
let p = std::env::temp_dir().join(format!("rust_ise_sha_{}.bin", std::process::id()));
std::fs::write(&p, b"abc").unwrap();
let h = sha256_hex(&p).unwrap();
let _ = std::fs::remove_file(&p);
assert_eq!(h, "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
}
}