use crate::Package;
use anyhow::{anyhow, bail, Context, Result};
use bytes::Buf;
use flate2::{read::GzDecoder, CrcReader};
use reqwest::blocking::get;
use reqwest::{StatusCode, Url};
use std::os::unix::fs::{symlink, PermissionsExt};
use std::path::Path;
use std::process::Command;
use std::{
fs::{self, Permissions},
io::Seek,
};
use tar::Archive;
pub fn install_package<P: AsRef<Path>>(
pkg_name: &str, server_url: impl reqwest::IntoUrl, package_dir: P, bin_dir: P, registry_file: P, ) -> Result<()> {
let server_url = server_url
.into_url()
.context("Could not parse server URL")?;
let package_dir = package_dir.as_ref();
let bin_dir = bin_dir.as_ref();
let pkg =
get_pkg_data(pkg_name, &server_url).context("Could not get package data from server")?;
fs::create_dir_all(package_dir).context("Could not create install directory for package")?;
let install_dir = package_dir.join(pkg_name);
download_install_file(pkg_name, pkg.crc, &server_url, &install_dir)
.context("Could not install file")?;
if pkg.has_installer {
run_install_script(&install_dir).context("Could not run install script for file")?;
}
if pkg.add_to_path {
let relative_exe_path: &Path = pkg.executable_path.as_ref().context("Package is configured to add executable to path, but is not configured with an executable path")?.as_ref();
let bin_exe_path = bin_dir.join(
relative_exe_path
.file_name()
.context("Could not get file name from executable path")?,
);
let package_exe_path = install_dir.join(relative_exe_path);
fs::create_dir_all(bin_dir).context("Could not create bin directory")?;
let link: &Path = &bin_exe_path;
let source: &Path = &package_exe_path;
log::info!("Creating symlink to {package_exe_path:?} at {bin_exe_path:?}");
symlink(source, link).context("Could not create symbolic link to package executable")?;
}
add_to_registry(registry_file.as_ref(), pkg).context("Could not add package to registry")?;
Ok(())
}
fn get_pkg_data(pkg_name: &str, server_url: &Url) -> Result<Package> {
let url = server_url
.join(format!("{}/{}", crate::DATA_ENDPOINT, pkg_name).as_ref())
.context("Could not parse URL")?;
log::info!("Downloading data for package {pkg_name} from {url}...");
let response = get(url.as_ref()).context("Request failed")?;
log::info!("Got reponse from {url}");
match response.status() {
StatusCode::OK => (),
StatusCode::NOT_FOUND => bail!("Package does not exist on server (404)"),
r => bail!("Response from server was not okay (code {})", r.as_u16()),
}
let package: Package = response.json().context("Could not parse JSON response")?;
log::debug!("Package data: {package:?}");
Ok(package)
}
fn download_install_file(
pkg_name: &str,
checksum: u32,
server_url: &Url,
install_dir: &Path,
) -> Result<()> {
let url = server_url
.join(format!("{}/{}.dcspkg", crate::FILE_ENDPOINT, pkg_name).as_ref())
.context("Could not parse URL")?;
log::info!("Downloading compressed package {pkg_name} from {url}...");
let response = get(url.as_ref()).context("Request failed")?;
log::info!("Got reponse from {url}");
if response.status() != StatusCode::OK {
bail!(
"Response was not okay (got code {} when requesting {})",
response.status().as_u16(),
url
)
}
let compressed = response
.bytes()
.context("Could not get content of response")?;
log::info!("Decompressing and unpacking package...");
let reader = GzDecoder::new(CrcReader::new(compressed.reader()));
let mut archive = Archive::new(reader);
archive
.unpack(install_dir)
.context("Could not unpack archive")?;
let downloaded_checksum = archive.into_inner().into_inner().crc().sum();
log::info!("Checksum of downloaded package is {downloaded_checksum} (expected {checksum})");
log::info!("Unpacked archive");
log::debug!("Unpacked into {:?}", install_dir);
Ok(())
}
fn run_install_script(path: &Path) -> Result<()> {
let script = path.join("install.sh");
if !script.exists() {
return Err(anyhow!(
"We were lied to by the server, install.sh does not exist at {script:?}"
));
}
log::info!("Got install script at {script:?}");
fs::set_permissions(&script, Permissions::from_mode(0o764))
.context("Failed to set file permissions for install script")?;
log::info!("Executing install script...");
let output = Command::new("sh")
.arg(&script)
.output()
.context("Could not execute install.sh")?;
if !output.stdout.is_empty() {
log::info!(
"Install script stdout: {}",
String::from_utf8_lossy(&output.stdout)
)
}
if !output.stderr.is_empty() {
log::info!(
"Install script stderr: {}",
String::from_utf8_lossy(&output.stderr)
)
}
if !output.status.success() {
bail!(
"Install script exited with code {}",
output.status.to_string()
)
}
log::info!("Install script finished, cleaning up...");
fs::remove_file(&script).context("Could not remove script")?;
Ok(())
}
fn add_to_registry(registry_file: &Path, package: Package) -> Result<()> {
if !registry_file.exists() {
fs::write(registry_file, "[]").context("Could not create package registry")?;
}
let mut file = fs::OpenOptions::new()
.read(true)
.write(true)
.open(registry_file)
.context("Could not open registry file")?;
let mut loaded: Vec<Package> =
serde_json::from_reader(&file).context("Could not deserialize registry file context")?;
log::debug!("Deserialised contents of registry file");
loaded.push(package);
file.rewind()
.and_then(|_| serde_json::to_writer(&file, &loaded).map_err(Into::into))
.context("Could not write registry back to file")?;
log::info!("Added package to local registry");
Ok(())
}