Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
477 changes: 417 additions & 60 deletions Cargo.lock

Large diffs are not rendered by default.

12 changes: 8 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,16 @@ edition = "2024"

[dependencies]
base64 = "0.22.1"
clap = { version = "4.5.60", features = ["cargo", "derive"] }
clap = { version = "4.6", features = ["cargo", "derive"] }
cliclack = "0.4.1"
chrono = "0.4.44"
derive_more = { version = "2.1.1", features = ["constructor"] }
dialoguer = { version = "0.12.0" }
directories = "6.0.0"
dlmgr = "0.3.1"
duct = { version = "1.1.1" }
futures-util = { version = "0.3" }
hex-literal = "1.1.0"
home = { version = "0.5.12" }
humansize = "2"
qemu_img_cmd_types = { path = "libs/qemu_img_cmd_types" }
Expand All @@ -28,15 +33,14 @@ rustls = "0.23.37"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = { version = "1.0.149" }
snafu = { version = "0.9.0", features = ["rust_1_81"] }
tempfile = "3.26.0"
tempfile = "3.27.0"
time = "0.3.47"
tokio = { version = "1.50.0", features = ["rt", "macros", "io-std", "fs"] }
tokio-tungstenite = { version = "0.28.0", features = ["rustls-tls-webpki-roots", "stream", "connect"], default-features = false }
url = { version = "2.5.0" }
x509-parser = "0.18.1"
cliclack = "0.3.9"
unicode-segmentation = "1.12.0"
chrono = "0.4.44"
which = "8.0.2"


[build-dependencies]
Expand Down
2 changes: 1 addition & 1 deletion libs/qemu_img_cmd_types/src/info/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ pub struct QemuInfo {
#[serde(rename = "actual-size")]
pub actual_size: u64,
#[serde(rename = "dirty-flag")]
pub dirty_flag: bool,
pub dirty_flag: Option<bool>,
pub children: Vec<Box<QemuInfoChild>>,
#[serde(rename = "format-specific", skip_serializing_if = "Option::is_none")]
pub format_specific: Option<QemuInfoFormatSpecific>,
Expand Down
2 changes: 2 additions & 0 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ pub struct GlobalArguments {

#[derive(clap::Subcommand)]
pub enum Action {
#[clap(hide = true)]
DlQemuImg,
#[clap(hide = true)]
Proxy(crate::tasks_internal::proxy::ProxyArguments),

Expand Down
14 changes: 11 additions & 3 deletions src/helpers/helper_cmd_error.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
use snafu::prelude::*;

#[derive(Debug, Snafu)]
#[snafu(visibility(pub))]
pub enum HelperCommandError {
#[snafu(transparent)]
#[snafu(display("JSON Serialization/Deserialization error"), context(false))]
JsonError { source: serde_json::Error },
#[snafu(transparent)]
#[snafu(display("IO Error"), context(false))]
IoError { source: std::io::Error },
#[snafu(transparent)]
#[snafu(display("IO Error {action}"))]
IoErrorPerformingAction {
source: std::io::Error,
action: &'static str,
},
#[snafu(display("Task Panicked"), context(false))]
TaskPanicked { source: tokio::task::JoinError },
#[snafu(display("Helper command returned invalid response: {reason}"))]
InvalidResponse { reason: &'static str },
#[snafu(display("qemu-img not found"))]
QemuImgNotFound,
#[snafu(whatever, display("{message}"))]
UnhandledError {
message: String,
Expand Down
5 changes: 4 additions & 1 deletion src/helpers/mtls/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::api::storage_api::entities::CmdSubmitResponse;
use crate::helpers::cmd::cmd_response::poll_for_cmd_response_type;
use crate::helpers::helper_cmd_error::HelperCommandError;
use crate::helpers::mtls::init_mtls_cmd::InitMtlsCredentialsCmdResponse;
use crate::task_common::error::HelperCommandSnafu;
use crate::task_common::error::TaskError;
use base64::Engine;
use base64::engine::GeneralPurpose;
Expand Down Expand Up @@ -57,7 +58,9 @@ impl MtlsCredentialHelper {
let init_response: InitMtlsCredentialsCmdResponse =
poll_for_cmd_response_type(cmd_api, submit_resp, "INIT_MTLS_CREDENTIALS").await?;

Ok(MtlsIssuedCredentialHelper::build(self.keypair, init_response).await?)
MtlsIssuedCredentialHelper::build(self.keypair, init_response)
.await
.context(HelperCommandSnafu)
}
}

Expand Down
28 changes: 20 additions & 8 deletions src/helpers/qemu/mod.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
mod convert_progress;
pub mod qemu_img_cmd_provider;

use crate::helpers::helper_cmd_error::HelperCommandError;
use crate::helpers::qemu::convert_progress::{QemuConvertProgressProvider, report_progress};
use crate::helpers::qemu::qemu_img_cmd_provider::QemuImgCmdProvider;
use duct::Expression;
use qemu_img_cmd_types::info::QemuInfo;
use std::path::{Path, PathBuf};
use std::process::Output;
use std::sync::Arc;
use tokio::task::{JoinError, JoinHandle};

pub async fn qemu_img_info(path: &Path) -> Result<QemuInfo, HelperCommandError> {
pub async fn qemu_img_info(
qemu_img: QemuImgCmdProvider,
path: &Path,
) -> Result<QemuInfo, HelperCommandError> {
let path_as_os_str = path.as_os_str().to_os_string();
let qemu_info_json = tokio::task::spawn_blocking(move || {
duct::cmd!("qemu-img", "info", "--output", "json", &path_as_os_str)
.stdout_capture()
.read()
duct::cmd!(
qemu_img.bin_path,
"info",
"--output",
"json",
&path_as_os_str
)
.stdout_capture()
.read()
})
.await??;

Expand Down Expand Up @@ -70,14 +81,14 @@ impl QemuImgConvert {
)
}

fn build_expression(&self) -> Expression {
fn build_expression(&self, qemu_img: QemuImgCmdProvider) -> Expression {
match self.op {
ConvertOperation::Import {
ref source_file,
ref source_format,
} => {
duct::cmd!(
"qemu-img",
&qemu_img.bin_path,
"convert",
"-p", //Display progress bar
"-n", //Skip the creation of the target volume
Expand All @@ -95,7 +106,7 @@ impl QemuImgConvert {
ref target_format,
} => {
duct::cmd!(
"qemu-img",
&qemu_img.bin_path,
"convert",
"-p", //Display progress bar
"--object",
Expand All @@ -112,6 +123,7 @@ impl QemuImgConvert {
}

pub async fn qemu_img_convert(
qemu_img: QemuImgCmdProvider,
args: QemuImgConvert,
) -> (
Arc<QemuConvertProgressProvider>,
Expand All @@ -120,7 +132,7 @@ pub async fn qemu_img_convert(
let convert_progress_provider = Arc::new(QemuConvertProgressProvider::default());
let convert_progress_provider2 = convert_progress_provider.clone();
let task_handle = tokio::task::spawn_blocking(move || {
let reader = args.build_expression().reader()?;
let reader = args.build_expression(qemu_img).reader()?;
report_progress(convert_progress_provider2, reader)
});

Expand Down
93 changes: 93 additions & 0 deletions src/helpers/qemu/qemu_img_cmd_provider/dl_win.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
use crate::helpers::helper_cmd_error::{HelperCommandError, IoErrorPerformingActionSnafu};
use cliclack::progress_bar;
use directories::BaseDirs;
use dlmgr::consumers::atomic_file_consumer_sha256::AtomicFileConsumerSha256;
use dlmgr::{DownloadTask, DownloadTaskBuilder};
use hex_literal::hex;
use snafu::ResultExt;
use std::path::Path;
use std::path::PathBuf;
use tokio::fs;
use url::Url;

const QEMU_IMG_WIN_URL: &str =
"https://cl1.gallium-cdn.com/utils/qemu/win64/v10.2.1-20260305/qemu-img.exe";
const QEMU_IMG_WIN_SHA256: [u8; 32] =
hex!("cfae8f5bbced4bc8ea2dc1ca9581349a23c30fc55221b5d8465b982b590e6330");

pub fn cache_dir_qemu_img_exe_path() -> Result<PathBuf, HelperCommandError> {
let base_dirs = BaseDirs::new().ok_or_else(|| HelperCommandError::InvalidResponse {
reason: "base directories not available",
})?;
let cache_dir = base_dirs.cache_dir();

let cli_cache_dir = cache_dir.join("Gallium-CLI");

let bin_dir = cli_cache_dir.join("bin");

Ok(bin_dir.join("qemu-img.exe"))
}

async fn get_and_create_parent_dir(path: &Path) -> Result<(), HelperCommandError> {
let parent_dir = path
.parent()
.ok_or_else(|| HelperCommandError::InvalidResponse {
reason: "dir parent not available",
})?;

fs::create_dir_all(parent_dir)
.await
.context(IoErrorPerformingActionSnafu {
action: "dir parent create",
})?;

Ok(())
}

pub async fn download_qemu_img() -> Result<(), HelperCommandError> {
let task_builder = DownloadTaskBuilder::new();
let qemu_img_exe_path = cache_dir_qemu_img_exe_path()?;

get_and_create_parent_dir(&qemu_img_exe_path).await?;

let (consumer, mut complete_notify) =
AtomicFileConsumerSha256::new(qemu_img_exe_path, QEMU_IMG_WIN_SHA256)
.await
.whatever_context::<_, HelperCommandError>("setup download")?;

let task: DownloadTask = task_builder
.begin_download(
Url::parse(QEMU_IMG_WIN_URL)
.whatever_context::<_, HelperCommandError>("parse download url")?
.into(),
consumer,
)
.await
.whatever_context::<_, HelperCommandError>("begin download")?;

let p = task.progress_provider();

let mut ui_tick = tokio::time::interval(tokio::time::Duration::from_millis(100));

let progress = progress_bar(100);

progress.start("Downloading qemu-img");

loop {
tokio::select! {
_ = ui_tick.tick() => {
progress.set_position(p.progress_percent() as u64);
}
r = &mut complete_notify => {
progress.set_position(100);
r.whatever_context::<_, HelperCommandError>("await completion message")?
.whatever_context::<_, HelperCommandError>("validate qemu-img")?;
break;
}
}
}

progress.stop("Download complete");

Ok(())
}
50 changes: 50 additions & 0 deletions src/helpers/qemu/qemu_img_cmd_provider/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
pub mod dl_win;

use crate::helpers::helper_cmd_error::HelperCommandError;
use std::path::PathBuf;

#[derive(Clone)]
pub struct QemuImgCmdProvider {
pub bin_path: PathBuf,
}

impl QemuImgCmdProvider {
pub async fn find_bin() -> Result<QemuImgCmdProvider, HelperCommandError> {
if let Ok(bin_path) = std::env::var("QEMU_IMG_BIN").map(PathBuf::from) {
return if tokio::fs::try_exists(&bin_path).await.unwrap_or(false) {
Ok(QemuImgCmdProvider { bin_path })
} else {
Err(HelperCommandError::InvalidResponse {
reason: "QEMU_IMG_BIN env var is set but does not point to a file",
})
};
}

if cfg!(target_os = "windows") {
find_in_cache().await
} else {
find_in_path().await
}
}
}

async fn find_in_cache() -> Result<QemuImgCmdProvider, HelperCommandError> {
let bin_path = dl_win::cache_dir_qemu_img_exe_path()?;
if tokio::fs::try_exists(&bin_path).await.unwrap_or(false) {
Ok(QemuImgCmdProvider { bin_path })
} else {
Err(HelperCommandError::QemuImgNotFound)
}
}

async fn find_in_path() -> Result<QemuImgCmdProvider, HelperCommandError> {
use which::which;
match tokio::task::spawn_blocking(|| which("qemu-img")).await? {
Ok(bin_path) => Ok(QemuImgCmdProvider { bin_path }),
Err(which::Error::CannotFindBinaryPath) => Err(HelperCommandError::QemuImgNotFound),
Err(e) => Err(HelperCommandError::UnhandledError {
message: format!("{e}"),
source: Some(Box::new(e)),
}),
}
}
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ async fn main() -> Result<(), TaskError> {
let invocation = Invocation::parse();

match invocation.action {
Some(Action::DlQemuImg) => crate::tasks_internal::qemu_img::dl_qemu_img().await,
Some(Action::Proxy(args)) => crate::tasks_internal::proxy::proxy(&args).await,
Some(Action::Export(args)) => {
crate::tasks::export::export_main(&invocation.gargs, args).await
Expand Down
5 changes: 3 additions & 2 deletions src/task_common/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::helpers::helper_cmd_error::HelperCommandError;
use snafu::prelude::*;

#[derive(Debug, Snafu)]
#[snafu(visibility(pub))]
pub enum TaskError {
#[snafu(display("Missing or invalid input for {field}"))]
UserInputInvalid { field: &'static str },
Expand All @@ -21,7 +22,7 @@ pub enum TaskError {
},
#[snafu(display("Requested operation not supported ({op}): {reason}"))]
RequestedOperationNotSupported { op: &'static str, reason: String },
#[snafu(transparent)]
#[snafu(display("API Client Error"), context(false))]
ApiClientError { source: ApiClientError },
#[snafu(display("API Response missing expected field: {field}"))]
ApiResponseMissingField { field: &'static str },
Expand All @@ -32,7 +33,7 @@ pub enum TaskError {
cmd_type: String,
serde_err: Option<serde_json::Error>,
},
#[snafu(transparent)]
#[snafu(display("Helper command error"))]
HelperCommand { source: HelperCommandError },
#[snafu(display("Failed to initialize {name}"))]
Initialize {
Expand Down
Loading