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

allow using release sources

This commit is contained in:
Robin Appelman 2026-02-27 22:01:44 +01:00
commit f569ca17e2
9 changed files with 1226 additions and 113 deletions

View file

@ -5,6 +5,7 @@ use crate::mapping::{default_mappings, Mapping};
use crate::php::{PhpVersion, PHP_MEMORY_LIMIT};
use crate::service::Service;
use crate::service::ServiceTrait;
use crate::sources::download_nc;
use bollard::container::{ListContainersOptions, RemoveContainerOptions, UpdateContainerOptions};
use bollard::models::ContainerState;
use bollard::network::CreateNetworkOptions;
@ -57,6 +58,7 @@ pub struct CloudOptions {
pub php: PhpVersion,
pub services: Vec<Service>,
pub app_packages: Vec<Utf8PathBuf>,
pub version: Option<String>,
}
impl CloudOptions {
@ -69,6 +71,7 @@ impl CloudOptions {
db: Database::default(),
services: vec![],
app_packages: vec![],
version: None,
}
}
@ -82,6 +85,7 @@ impl CloudOptions {
let mut name = None;
let mut services = Vec::new();
let mut app_package = Vec::new();
let mut version = None;
while let Some(option) = args.peek() {
if let Ok(db_option) = Database::from_str(option.as_ref()) {
@ -96,6 +100,9 @@ impl CloudOptions {
} else if option.as_ref().ends_with(".tar.gz") {
app_package.push(option.to_string().into());
let _ = args.next();
} else if let Some(v) = option.as_ref().strip_prefix("v") {
version = Some(v.into());
let _ = args.next();
} else if option.as_ref() == "--name" {
let _ = args.next();
name = args.next().map(|s| s.into());
@ -112,6 +119,7 @@ impl CloudOptions {
.unwrap_or_default(),
services,
app_packages: app_package,
version,
})
}
}
@ -245,6 +253,12 @@ impl Cloud {
.wrap_err("Failed to create directory for app packages")?;
}
let source_root = if let Some(version) = options.version.as_deref() {
download_nc(config, version).await?
} else {
config.sources_root.clone()
};
let app_volumes = options
.app_packages
.iter()
@ -327,7 +341,7 @@ impl Cloud {
];
let volumes: Vec<String> = mappings
.into_iter()
.filter_map(|mapping| mapping.get_volume_arg(&id, config))
.filter_map(|mapping| mapping.get_volume_arg(&id, config, &source_root))
.collect();
if let Some(db_name) = options
@ -382,6 +396,7 @@ impl Cloud {
gateway,
&options.services,
&config.proxy,
options.version.as_deref(),
)
.await
.wrap_err("Failed to start php container")
@ -620,6 +635,7 @@ impl Cloud {
let labels = cloud.labels?;
let db = labels.get("haze-db")?.parse().ok()?;
let php = labels.get("haze-php")?.parse().ok()?;
let version = labels.get("haze-version").cloned();
let found_services = labels
.get("haze-services")?
@ -665,6 +681,7 @@ impl Cloud {
db,
services: found_services,
app_packages: vec![],
version,
},
pinned,
address,
@ -788,7 +805,11 @@ impl Cloud {
for mapping in mappings {
if let Some(rel_path) = path.strip_prefix(mapping.target.as_str()) {
let rel_path = rel_path.trim_matches('/');
return Some(mapping.source(&self.id, config).join(rel_path));
return Some(
mapping
.source(&self.id, config, &config.sources_root)
.join(rel_path),
);
}
}
None

View file

@ -88,6 +88,7 @@ fn subcommand_help(command: &dyn SubCommand) {
print!(" {}", "[php version]".green());
print!(" {}", "[database type]".green());
print!(" {}", "[services]".green());
print!(" {}", "[vX.Y.Z]".green());
}
let args = if let Some(args) = command.get_str("Args") {

View file

@ -103,13 +103,18 @@ async fn main() -> Result<ExitCode> {
services.push(cloud.db().name());
let services = services.join(", ");
let pin = if cloud.pinned { "*" } else { "" };
let version = match cloud.options.version.as_ref() {
Some(version) => format!(", v{version}"),
None => String::new(),
};
println!(
"Cloud {}{}, {}, {}, running on {}",
"Cloud {}{}, {}, {}{}, running on {}",
cloud.id,
pin,
cloud.php().name(),
services,
cloud.address
version,
cloud.address,
);
}
}

View file

@ -78,20 +78,25 @@ impl<'a> Mapping<'a> {
Ok(())
}
pub fn source(&self, id: &str, config: &HazeConfig) -> Utf8PathBuf {
pub fn source(&self, id: &str, config: &HazeConfig, source_root: &Utf8Path) -> Utf8PathBuf {
match self.source_type {
MappingSourceType::WorkDir => config.work_dir.join(id).join(self.source),
MappingSourceType::GlobalWorkDir => config.work_dir.join(self.source),
MappingSourceType::Sources => config.sources_root.join(self.source),
MappingSourceType::Sources => source_root.join(self.source),
MappingSourceType::Absolute => self.source.into(),
}
}
pub fn get_volume_arg(&self, id: &str, config: &HazeConfig) -> Option<String> {
pub fn get_volume_arg(
&self,
id: &str,
config: &HazeConfig,
source_root: &Utf8Path,
) -> Option<String> {
if !self.map {
return None;
}
let source = self.source(id, config);
let source = self.source(id, config, source_root);
Some(if self.read_only {
format!("{}:{}:ro", source, self.target)
} else {

View file

@ -164,6 +164,7 @@ impl PhpVersion {
host: &str,
services: &[Service],
proxy_config: &ProxyConfig,
version: Option<&str>,
) -> Result<String> {
ensure_network_exists(docker, "haze").await?;
pull_image(docker, self.image()).await?;
@ -192,6 +193,17 @@ impl PhpVersion {
proxy_config.addr(id, IpAddr::V4(Ipv4Addr::LOCALHOST))
));
let mut labels = hashmap! {
"haze-type".to_string() => "cloud".to_string(),
"haze-db".to_string() => db.name().to_string(),
"haze-php".to_string() => self.name().to_string(),
"haze-cloud-id".to_string() => id.to_string(),
"haze-services".to_string() => services.iter().map(|s| s.name()).join(","),
};
if let Some(version) = version {
labels.insert("haze-version".to_string(), version.to_string());
}
let config = Config {
image: Some(self.image().to_string()),
env: Some(env),
@ -211,13 +223,7 @@ impl PhpVersion {
}
},
}),
labels: Some(hashmap! {
"haze-type".to_string() => "cloud".to_string(),
"haze-db".to_string() => db.name().to_string(),
"haze-php".to_string() => self.name().to_string(),
"haze-cloud-id".to_string() => id.to_string(),
"haze-services".to_string() => services.iter().map(|s| s.name()).join(","),
}),
labels: Some(labels),
..Default::default()
};

View file

@ -1,6 +1,17 @@
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)]
@ -49,3 +60,116 @@ impl Sources {
}
}
}
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("Extracing");
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()
}