1
0
Fork 0
mirror of https://codeberg.org/icewind/haze.git synced 2026-06-03 09:04:12 +02:00
haze/src/sources.rs
2026-04-16 16:30:09 +02:00

175 lines
6 KiB
Rust

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<P: AsRef<Path>>(base_dir: P) -> Result<Self> {
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<Utf8PathBuf> {
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<U: IntoUrl, SizeFN: FnOnce(u64), ProgressFN: Fn(u64)>(
url: U,
size: SizeFN,
progress: ProgressFN,
) -> Result<Vec<u8>> {
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<U: IntoUrl>(url: U) -> Result<String> {
get_url(url)
.await?
.error_for_status()
.into_diagnostic()?
.text()
.await
.into_diagnostic()
}
async fn get_url<U: IntoUrl>(url: U) -> Result<Response> {
Client::builder()
.build()
.into_diagnostic()?
.get(url)
.header(
HeaderName::from_static("user-agent"),
format!("haze {}", env!("CARGO_PKG_VERSION")),
)
.send()
.await
.into_diagnostic()
}