pub(crate) mod check;
pub(crate) mod install;
pub(crate) mod release;
pub(crate) mod verify;
use std::path::{Path, PathBuf};
use std::time::Duration;
use anyhow::{Context, bail};
use decdn_common::cli::SelfUpdateArgs;
use minisign_verify::PublicKey;
use semver::Version;
use tempfile::NamedTempFile;
pub use check::{NudgePolicy, Pending, start as start_nudge};
use install::Ownership;
use release::{Clients, Target, Upgrade};
const DOWNLOAD_TIMEOUT: Duration = Duration::from_secs(600);
const MAX_MANIFEST_BYTES: usize = 64 * 1024;
const MAX_SIGNATURE_BYTES: usize = 4 * 1024;
const MAX_ARCHIVE_BYTES: usize = 256 * 1024 * 1024;
pub async fn self_update(args: &SelfUpdateArgs) -> anyhow::Result<()> {
let target = Target::current().context(
"no release archive is built for this platform; build from source or run \
`cargo install --locked decdn-cli`",
)?;
let data_dir =
decdn_common::cli::default_data_dir().context("cannot determine the home directory")?;
let home = data_dir.parent().unwrap_or(&data_dir).to_path_buf();
let exe = install::running_exe()?;
let cargo_home = std::env::var_os("CARGO_HOME").map(PathBuf::from);
let install_root = std::env::var_os("CARGO_INSTALL_ROOT").map(PathBuf::from);
let cargo_bins = install::cargo_bin_dirs(&home, cargo_home.as_deref(), install_root.as_deref());
match install::classify(&exe, &cargo_bins, install::probe_writable) {
Ownership::Replaceable => {}
Ownership::CargoBin => bail!(
"{} was installed by cargo; run `cargo install --locked decdn-cli` to update it",
exe.display()
),
Ownership::NotWritable(reason) => bail!(
"cannot replace {}: {reason}. Update it the way it was installed (`docker pull`, \
the package manager, or the fleet tooling that placed it)",
exe.display()
),
}
let running = release::running_version()?;
let request = Request {
keys: &verify::trusted_keys()?,
target,
running: &running,
exe: &exe,
clients: &Clients::build(DOWNLOAD_TIMEOUT)?,
base: &release::releases_base(),
check: args.check,
};
match apply(&request, |staged| install::swap(staged, &exe)).await? {
Outcome::UpToDate => println!("decdn {running} is up to date"),
Outcome::Downgrade { latest } => println!(
"the latest release ({latest}) is older than this build ({running}); nothing to do"
),
Outcome::PreRelease { latest } => {
println!("the latest release ({latest}) is a pre-release; nothing to do");
}
Outcome::Available { latest } => println!(
"decdn {latest} is available (signed manifest ok); run `decdn self-update` to \
install it over {}",
exe.display()
),
Outcome::Updated { latest } => {
println!("updated {}: {running} -> {latest}", exe.display());
}
}
Ok(())
}
pub(crate) struct Request<'a> {
pub(crate) keys: &'a [PublicKey],
pub(crate) target: Target,
pub(crate) running: &'a Version,
pub(crate) exe: &'a Path,
pub(crate) clients: &'a Clients,
pub(crate) base: &'a str,
pub(crate) check: bool,
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum Outcome {
UpToDate,
Downgrade {
latest: Version,
},
PreRelease {
latest: Version,
},
Available {
latest: Version,
},
Updated {
latest: Version,
},
}
pub(crate) async fn apply(
request: &Request<'_>,
swap: impl FnOnce(NamedTempFile) -> anyhow::Result<()>,
) -> anyhow::Result<Outcome> {
let Request {
keys,
target,
running,
exe,
clients,
base,
check,
} = *request;
let tag = release::resolve_latest(clients, base).await?;
let latest = release::version_from_tag(&tag)?;
match release::decide(running, &latest) {
Upgrade::Newer => {}
Upgrade::UpToDate => return Ok(Outcome::UpToDate),
Upgrade::Downgrade => return Ok(Outcome::Downgrade { latest }),
Upgrade::PreRelease => return Ok(Outcome::PreRelease { latest }),
}
let sums = release::fetch_asset(clients, base, &tag, "SHA256SUMS", MAX_MANIFEST_BYTES).await?;
let minisig = release::fetch_asset(
clients,
base,
&tag,
"SHA256SUMS.minisig",
MAX_SIGNATURE_BYTES,
)
.await?;
let minisig = std::str::from_utf8(&minisig).context("SHA256SUMS.minisig is not UTF-8")?;
let manifest = verify::verify_manifest_with(keys, &sums, minisig)?;
let name = target.archive_name(&latest);
if manifest.digest_for(&name).is_none() {
bail!("release {tag} ships no {name}");
}
if check {
return Ok(Outcome::Available { latest });
}
eprintln!("Downloading {name}...");
let archive = release::fetch_asset(clients, base, &tag, &name, MAX_ARCHIVE_BYTES).await?;
verify::verify_archive(&manifest, &name, &archive)?;
let bytes = install::extract_binary(&archive, target.kind(), MAX_ARCHIVE_BYTES as u64)?;
let dir = exe
.parent()
.context("the running binary has no parent directory")?;
let staged = install::stage(dir, &bytes)?;
swap(staged)?;
Ok(Outcome::Updated { latest })
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] mod tests;