use std::path::Path;
use aws_lc_rs::digest::{SHA256, digest};
const RELEASES_URL: &str = concat!(env!("CARGO_PKG_REPOSITORY"), "/releases");
const ARCHIVE: &str = "yapi-docs.tar.gz";
const VERSION_FILE: &str = ".version";
const BOOK_URL: &str = "https://nikolish.in/yapi/";
const PI_URL: &str = "https://github.com/earendil-works/pi/blob/v1.0.0/packages/coding-agent";
pub fn releases_url() -> String {
std::env::var("YAPI_RELEASES_URL")
.ok()
.map(|url| url.trim_end_matches('/').to_owned())
.filter(|url| !url.is_empty())
.unwrap_or_else(|| RELEASES_URL.to_owned())
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Locations {
pub main: String,
pub pi_docs: String,
pub pi_examples: String,
}
impl Locations {
pub fn find(agent_dir: &Path) -> Locations {
let dir = crate::config::docs_dir(agent_dir);
if current(&dir) {
Locations {
main: dir.join("index.md").display().to_string(),
pi_docs: dir.join("pi").join("docs").display().to_string(),
pi_examples: dir.join("pi").join("examples").display().to_string(),
}
} else {
Locations {
main: BOOK_URL.to_owned(),
pi_docs: format!("{PI_URL}/docs"),
pi_examples: format!("{PI_URL}/examples"),
}
}
}
}
pub async fn download(releases: &str, agent_dir: &Path) -> Result<(), String> {
let version = env!("CARGO_PKG_VERSION");
let dir = crate::config::docs_dir(agent_dir);
if current(&dir) {
return Ok(());
}
let url = format!("{releases}/download/v{version}/{ARCHIVE}");
let archive = crate::tools::external::fetch(&url).await?;
let sums = crate::tools::external::fetch(&format!("{url}.sha256")).await?;
let expected = String::from_utf8_lossy(&sums)
.split_whitespace()
.next()
.unwrap_or_default()
.to_lowercase();
if crate::time::hex(digest(&SHA256, &archive).as_ref()) != expected {
return Err(format!("{url} does not match its SHA-256 checksum"));
}
tokio::task::spawn_blocking(move || replace(&archive, &dir))
.await
.map_err(|err| err.to_string())?
}
fn current(dir: &Path) -> bool {
std::fs::read_to_string(dir.join(VERSION_FILE))
.is_ok_and(|text| text.trim() == env!("CARGO_PKG_VERSION"))
}
fn replace(archive: &[u8], dir: &Path) -> Result<(), String> {
let fresh = dir.with_file_name(format!(".docs.{}", std::process::id()));
let result = tar::Archive::new(flate2::read::GzDecoder::new(archive))
.unpack(&fresh)
.map_err(|err| format!("could not unpack {ARCHIVE}: {err}"))
.and_then(|()| {
let _ = std::fs::remove_dir_all(dir);
std::fs::rename(&fresh, dir).map_err(|err| err.to_string())
});
let _ = std::fs::remove_dir_all(&fresh);
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn local_else_published_docs() {
let agent = std::env::temp_dir().join(format!("yapi-docs-find-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&agent);
let published = Locations::find(&agent);
assert_eq!(published.main, "https://nikolish.in/yapi/");
assert_eq!(
published.pi_docs,
"https://github.com/earendil-works/pi/blob/v1.0.0/packages/coding-agent/docs"
);
assert_eq!(
published.pi_examples,
"https://github.com/earendil-works/pi/blob/v1.0.0/packages/coding-agent/examples"
);
std::fs::create_dir_all(agent.join("docs")).unwrap();
std::fs::write(agent.join("docs").join(VERSION_FILE), "0.0.1\n").unwrap();
assert_eq!(
Locations::find(&agent),
published,
"an older version's docs"
);
let version = format!("{}\n", env!("CARGO_PKG_VERSION"));
std::fs::write(agent.join("docs").join(VERSION_FILE), version).unwrap();
let local = Locations::find(&agent);
let docs = agent.join("docs");
assert_eq!(local.main, docs.join("index.md").display().to_string());
assert_eq!(
local.pi_docs,
docs.join("pi").join("docs").display().to_string()
);
assert_eq!(
local.pi_examples,
docs.join("pi").join("examples").display().to_string()
);
std::fs::remove_dir_all(&agent).unwrap();
}
}