mirror of
https://codeberg.org/icewind/haze.git
synced 2026-06-03 09:04:12 +02:00
175 lines
6 KiB
Rust
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()
|
|
}
|