use crate::config::HazeConfig; use camino::Utf8PathBuf; use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use miette::{Context, IntoDiagnostic, Report, Result}; use reqwest::header::HeaderName; use reqwest::{Client, IntoUrl, Response}; use sha2::{Digest, Sha512}; use std::fs::read_to_string; use std::io::Cursor; use std::path::{Path, PathBuf}; use std::time::Duration; use tokio::fs::create_dir_all; use zip::read::root_dir_common_filter; use zip::ZipArchive; pub struct Sources { #[allow(dead_code)] base_dir: PathBuf, server_version: u8, server_branched_off: bool, } impl Sources { pub fn new>(base_dir: P) -> Result { let base_dir = base_dir.as_ref(); let versions_source = read_to_string(base_dir.join("version.php")) .into_diagnostic() .wrap_err("failed to read version.php")?; let version_line = versions_source .lines() .find(|line| line.starts_with("$OC_Version")) .ok_or_else(|| Report::msg("failed to find line containing $OC_Version"))?; let version_str_line = versions_source .lines() .find(|line| line.starts_with("$OC_VersionString")) .ok_or_else(|| Report::msg("failed to find line containing $OC_VersionString"))?; let (major, _) = version_line .split_once('[') .and_then(|(_, line)| line.split_once(',')) .ok_or_else(|| Report::msg("failed to find version number in line"))?; let server_version = major .trim() .parse() .into_diagnostic() .wrap_err("failed to parse version number")?; let server_branched_off = !version_str_line.contains("dev"); Ok(Sources { base_dir: base_dir.into(), server_version, server_branched_off, }) } pub fn get_server_version_branch(&self) -> String { if self.server_branched_off { format!("stable{}", self.server_version) } else { "master".into() } } } pub async fn download_nc(config: &HazeConfig, version: &str) -> Result { if !version.chars().all(|c| c.is_ascii_digit() || c == '.') { return Err(Report::msg(format!("Invalid version: {version}"))); } let root = config.work_dir.join("sources"); create_dir_all(&root) .await .into_diagnostic() .wrap_err("failed to create parent directory for sources")?; let dest = root.join(version); if !dest.exists() { let progress = MultiProgress::new(); let download_style = ProgressStyle::with_template("{spinner:.green} {msg} [{elapsed_precise}] [{bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})") .unwrap(); let download_bar = ProgressBar::new(0) .with_message("Downloading") .with_style(download_style.clone()); let download_bar = progress.add(download_bar); let archive = download_url( format!("https://download.nextcloud.com/server/releases/nextcloud-{version}.zip"), |size| { download_bar.set_length(size); }, |count| { download_bar.inc(count); }, ) .await .wrap_err_with(|| format!("Failed to download archive for {}", version))?; download_bar.finish(); let expected_hash = download_text(format!( "https://download.nextcloud.com/server/releases/nextcloud-{version}.zip.sha512" )) .await .wrap_err_with(|| format!("Failed to download hash for {}", version))?; let expected_hash = expected_hash .split_once(' ') .map(|(hash, _)| hash) .unwrap_or(expected_hash.trim()); let hash_bar = ProgressBar::new(download_bar.length().unwrap_or_default()) .with_message("Validating") .with_style(download_style.clone()); let hash_bar = progress.add(hash_bar); let mut hasher = Sha512::new(); for chunk in archive.chunks(1014 * 1024) { hash_bar.inc(chunk.len() as u64); hasher.update(chunk); } let hash = hasher.finalize(); let hash = base16ct::lower::encode_string(&hash); if expected_hash != hash { return Err(Report::msg(format!( "Invalid hash for downloaded: {version}, expected {expected_hash} but got {hash}" ))); } hash_bar.finish(); let extract_bar = ProgressBar::new_spinner().with_message("Extracting"); extract_bar.enable_steady_tick(Duration::from_millis(100)); let extract_bar = progress.add(extract_bar); let mut archive = ZipArchive::new(Cursor::new(archive)).into_diagnostic()?; archive .extract_unwrapped_root_dir(&dest, root_dir_common_filter) .into_diagnostic() .wrap_err("Failed to extract archive")?; extract_bar.finish(); } Ok(dest) } async fn download_url( url: U, size: SizeFN, progress: ProgressFN, ) -> Result> { let mut res = get_url(url).await?.error_for_status().into_diagnostic()?; let mut buff = Vec::new(); size(res.content_length().unwrap_or_default()); while let Some(chunk) = res.chunk().await.into_diagnostic()? { progress(chunk.len() as u64); buff.extend(chunk); } Ok(buff) } async fn download_text(url: U) -> Result { get_url(url) .await? .error_for_status() .into_diagnostic()? .text() .await .into_diagnostic() } async fn get_url(url: U) -> Result { Client::builder() .build() .into_diagnostic()? .get(url) .header( HeaderName::from_static("user-agent"), format!("haze {}", env!("CARGO_PKG_VERSION")), ) .send() .await .into_diagnostic() }