use std::path::PathBuf;
use anyhow::{bail, Context, Result};
use crate::net::Http;
use crate::platform::{self, Platform};
use crate::release::{self, Repo, Version, OLDEST_PUBLISHED};
use crate::ui::Ui;
use crate::{archive, checksums, install, verify};
pub struct Session {
pub dir: PathBuf,
pub ui: Ui,
pub repo: Repo,
pub http: Http,
}
impl Session {
pub fn new(dir: Option<PathBuf>, quiet: bool) -> Result<Self> {
let dir = match dir {
Some(dir) => dir,
None => platform::default_install_dir()?,
};
Ok(Self {
dir,
ui: Ui::new(quiet),
repo: Repo::from_env(),
http: Http::new(),
})
}
}
pub fn install(session: &Session, version: Option<&str>, force: bool, dry_run: bool) -> Result<()> {
let ui = &session.ui;
ui.say("Accent CMS Installer");
ui.say("===================");
ui.blank();
let platform = platform::detect()?;
ui.say(format!(
"Detected platform: {} {} ({})",
platform.os, platform.arch, platform.target
));
let target = resolve_version(session, version)?;
if !force && already_installed(session, &target)? {
return Ok(());
}
fetch_and_place(session, &platform, &target, dry_run)
}
pub fn update(session: &Session, force: bool) -> Result<()> {
let ui = &session.ui;
let platform = platform::detect()?;
let current = install::installed_version(&session.dir);
let latest = resolve_version(session, None)?;
match ¤t {
Some(current) if current == &latest && !force => {
ui.say(format!("Already up to date ({latest})."));
install::check_path(&session.dir, ui);
install::check_shadowing(&session.dir, ui);
return Ok(());
}
Some(current) => ui.say(format!("Updating {current} -> {latest}")),
None => ui.say(format!(
"Accent CMS is not installed in {} — installing {latest}",
session.dir.display()
)),
}
fetch_and_place(session, &platform, &latest, false)
}
pub fn uninstall(session: &Session, purge: bool) -> Result<()> {
let ui = &session.ui;
let binary = install::binary_in(&session.dir);
if install::remove(&session.dir)? {
ui.say(format!("Removed {}", binary.display()));
} else {
ui.say(format!("Nothing to remove at {}", binary.display()));
}
if purge {
install::purge_data(ui)?;
ui.say("Licence key and cached development certificate are gone.");
}
if let Some(other) = install::resolve_on_path(platform::BIN_NAME) {
ui.blank();
ui.warn(format!(
"Note: 'accent' still resolves to {} — this installer did not put it there.",
other.display()
));
}
Ok(())
}
pub fn which(session: &Session) -> Result<()> {
let binary = install::binary_in(&session.dir);
if !binary.exists() {
bail!(
"Accent CMS is not installed in {}\nRun `accentup install` to install it.",
session.dir.display()
);
}
match install::installed_version(&session.dir) {
Some(version) => println!("{} ({version})", binary.display()),
None => println!("{} (unknown version)", binary.display()),
}
install::check_shadowing(&session.dir, &session.ui);
Ok(())
}
fn resolve_version(session: &Session, requested: Option<&str>) -> Result<Version> {
let ui = &session.ui;
match requested {
Some(raw) => {
let version = Version::parse(raw)?;
ui.say(format!("Installing version: {version}"));
Ok(version)
}
None => {
ui.say("Fetching latest version...");
let version = release::resolve_latest(&session.http, &session.repo)?;
ui.say(format!("Latest version: {version}"));
Ok(version)
}
}
}
fn already_installed(session: &Session, target: &Version) -> Result<bool> {
let ui = &session.ui;
let binary = install::binary_in(&session.dir);
if !binary.exists() {
return Ok(false);
}
let current = install::installed_version(&session.dir);
match ¤t {
Some(current) => ui.say(format!(
"Accent CMS is already installed: accent {}",
current.number()
)),
None => ui.say(format!(
"Accent CMS is already installed at: {}",
binary.display()
)),
}
if current.as_ref() == Some(target) {
ui.say(format!("Already up to date ({target})."));
install::check_path(&session.dir, ui);
install::check_shadowing(&session.dir, ui);
return Ok(true);
}
bail!(
"{} already exists.\n\
To reinstall or update in place: accentup update (or accentup install --force)\n\
Or remove it first: rm {}",
binary.display(),
binary.display()
)
}
fn fetch_and_place(
session: &Session,
platform: &Platform,
version: &Version,
dry_run: bool,
) -> Result<()> {
let ui = &session.ui;
let asset = platform.asset_name(&version.tag());
let workdir = tempfile::Builder::new()
.prefix("accentup-")
.tempdir()
.context("could not create a temporary directory")?;
let archive_path = workdir.path().join(&asset);
ui.say(format!("Downloading {asset}..."));
let digest = session
.http
.download(
&session.repo.download_url(version, &asset),
&archive_path,
ui,
)
.with_context(|| {
format!(
"downloading {asset} ({version} may not be published — see {}, which carries \
{OLDEST_PUBLISHED} and later)",
session.repo.releases_page()
)
})?;
verify_download(session, version, &asset, &digest)?;
if dry_run {
ui.blank();
ui.say(format!(
"Dry run: accent {version} for {} verified, nothing written.",
platform.target
));
return Ok(());
}
ui.say("Extracting...");
let staged = archive::unpack(&archive_path, workdir.path(), platform.archive)?;
let report_url = format!("https://github.com/{}/discussions", session.repo.slug());
let installed = install::place(&staged, &session.dir, |pending| {
let version_line = install::runs(pending, &report_url)?;
ui.say(format!("Verified: {version_line}"));
Ok(())
})?;
ui.say(format!("Installed accent to {}", installed.display()));
install::check_path(&session.dir, ui);
install::check_shadowing(&session.dir, ui);
ui.blank();
ui.say("Installation complete! Run 'accent --version' to verify.");
Ok(())
}
fn verify_download(session: &Session, version: &Version, asset: &str, digest: &str) -> Result<()> {
let ui = &session.ui;
ui.say("Downloading checksums...");
let checksums = session
.http
.get_text(&session.repo.checksums_url(version))
.with_context(|| format!("downloading {}", release::checksums_name(version)))?;
let signature = session
.http
.get_text(&session.repo.signature_url(version))
.with_context(|| format!("downloading {}.asc", release::checksums_name(version)))?;
verify::signature(checksums.as_bytes(), &signature).with_context(|| {
format!(
"signature verification failed, nothing was installed — do not use this download; \
report it via https://github.com/{}/discussions",
session.repo.slug()
)
})?;
ui.say("Signature verified.");
let expected = checksums::lookup(&checksums, asset)?;
verify::checksum(expected, digest)
.context("the download does not match its published checksum — nothing was installed")?;
ui.say("Checksum verified.");
Ok(())
}