use std::collections::BTreeSet;
use crate::compose::types::{ComposeFile, VolumeMount};
use crate::error::Result;
use crate::libpod::types::volume::SystemDf;
use crate::libpod::API_PREFIX;
use crate::units::{format_bytes, SizeFormat};
use super::super::Engine;
const SIZE_FORMAT: SizeFormat = SizeFormat::decimal().with_significant(3);
#[derive(Default, Clone, Copy, Debug)]
#[non_exhaustive]
pub struct VolumesDisplayOptions {
pub size: bool,
}
impl VolumesDisplayOptions {
#[must_use]
pub fn with_size(mut self, size: bool) -> Self {
self.size = size;
self
}
}
#[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<()> {
self.list_volumes_with_display(file, services, opts, VolumesDisplayOptions::default())
.await
}
pub async fn list_volumes_with_display(
&self,
file: &ComposeFile,
services: &[String],
opts: VolumesOptions,
display: VolumesDisplayOptions,
) -> Result<()> {
for s in services {
if !file.services.contains_key(s) {
return Err(crate::error::ComposeError::ServiceNotFound(s.clone()));
}
}
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(());
}
let usage = if display.size {
self.volume_disk_usage().await?
} else {
std::collections::HashMap::new()
};
if opts.json {
let arr: Vec<_> = rows
.iter()
.map(|(_, name, driver, external)| {
let size = display.size.then(|| {
let u = usage.get(name.as_str());
serde_json::json!({
"Size": u.map(|u| u.size).unwrap_or(0),
"ReclaimableSize": u.map(|u| u.reclaimable).unwrap_or(0),
"Links": u.map(|u| u.links).unwrap_or(0),
})
});
serde_json::json!({
"Name": name,
"Driver": driver,
"External": external,
"Usage": size,
})
})
.collect();
println!("{}", serde_json::to_string_pretty(&arr).unwrap_or_default());
return Ok(());
}
let mut headers: Vec<&str> = vec!["NAME", "DRIVER", "EXTERNAL"];
if display.size {
headers.push("SIZE");
headers.push("RECLAIMABLE");
}
let mut table = crate::ui::Table::new(&headers)
.cap(0, 48)
.identity_col(0)
.caution_col(2);
for (_, name, driver, external) in &rows {
let mut row = vec![
name.clone(),
driver.clone(),
if *external { "yes" } else { "no" }.to_string(),
];
if display.size {
let (size, reclaimable) = size_cells(usage.get(name.as_str()));
row.push(size);
row.push(reclaimable);
}
table.push(row);
}
table.print();
Ok(())
}
async fn volume_disk_usage(
&self,
) -> Result<std::collections::HashMap<String, crate::libpod::types::volume::VolumeDiskUsage>> {
let df: SystemDf = self
.client
.get_json(&format!("{API_PREFIX}/system/df"))
.await
.map_err(crate::error::ComposeError::Podman)?;
Ok(df
.volumes
.into_iter()
.map(|v| (v.name.clone(), v))
.collect())
}
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);
}
}
fn size_cells(usage: Option<&crate::libpod::types::volume::VolumeDiskUsage>) -> (String, String) {
match usage {
Some(u) => (
format_bytes(u.size, &SIZE_FORMAT),
format_bytes(u.reclaimable, &SIZE_FORMAT),
),
None => (String::new(), String::new()),
}
}
#[cfg(test)]
#[path = "list_tests.rs"]
mod size_tests;