use std::collections::BTreeSet;
use crate::compose::types::{ComposeFile, VolumeMount};
use crate::error::Result;
use super::Engine;
#[derive(Default)]
pub struct VolumesOptions {
pub quiet: bool,
pub json: bool,
}
impl Engine {
pub async fn list_volumes(
&self,
file: &ComposeFile,
services: &[String],
opts: VolumesOptions,
) -> Result<()> {
let keys = self.selected_volume_keys(file, services);
let rows: Vec<(String, String, String, bool)> = keys
.iter()
.map(|key| {
let cfg = file.volumes.get(key.as_str()).and_then(|c| c.as_ref());
let external = cfg.and_then(|c| c.external).unwrap_or(false);
let name = match cfg.and_then(|c| c.name.as_deref()) {
Some(n) => n.to_string(),
None if external => key.to_string(),
None => format!("{}_{}", self.project, key),
};
let driver = cfg
.and_then(|c| c.driver.clone())
.unwrap_or_else(|| "local".into());
(key.to_string(), name, driver, external)
})
.collect();
if opts.quiet {
for (_, name, _, _) in &rows {
println!("{name}");
}
return Ok(());
}
if opts.json {
let arr: Vec<_> = rows
.iter()
.map(|(_, name, driver, external)| {
serde_json::json!({ "Name": name, "Driver": driver, "External": external })
})
.collect();
println!("{}", serde_json::to_string_pretty(&arr).unwrap_or_default());
return Ok(());
}
println!("{:<40} {:<12}", "NAME", "DRIVER");
for (_, name, driver, _) in &rows {
println!("{name:<40} {driver:<12}");
}
Ok(())
}
fn selected_volume_keys(&self, file: &ComposeFile, services: &[String]) -> Vec<String> {
if services.is_empty() {
return file.volumes.keys().cloned().collect();
}
let used: BTreeSet<String> = services
.iter()
.filter_map(|s| file.services.get(s))
.flat_map(|svc| svc.volumes.iter().filter_map(mount_source_name))
.filter(|src| file.volumes.contains_key(src))
.collect();
file.volumes
.keys()
.filter(|k| used.contains(k.as_str()))
.cloned()
.collect()
}
}
fn mount_source_name(m: &VolumeMount) -> Option<String> {
match m {
VolumeMount::Short(s) => {
let parts: Vec<&str> = s.splitn(3, ':').collect();
if parts.len() >= 2 && !parts[0].starts_with(['.', '/', '~']) {
Some(parts[0].to_string())
} else {
None
}
}
VolumeMount::Long { source, .. } => source.clone(),
}
}
#[cfg(test)]
mod tests {
use super::mount_source_name;
use crate::compose::types::VolumeMount;
#[test]
fn named_volume_short_form_has_source() {
assert_eq!(
mount_source_name(&VolumeMount::Short("data:/var/lib".into())),
Some("data".to_string())
);
}
#[test]
fn bind_and_anonymous_have_no_source() {
assert_eq!(
mount_source_name(&VolumeMount::Short("./host:/c".into())),
None
);
assert_eq!(
mount_source_name(&VolumeMount::Short("/abs:/c".into())),
None
);
assert_eq!(mount_source_name(&VolumeMount::Short("/data".into())), None);
}
}