use crate::features::maintenance::MaintenanceContext;
use crate::store::{Db, daemon_settings};
use bollard::{
container::PruneContainersOptions, image::PruneImagesOptions, network::PruneNetworksOptions,
volume::PruneVolumesOptions,
};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::time::Duration;
const BUILD_CACHE_PRUNE_TIMEOUT: Duration = Duration::from_secs(600);
pub struct PruneStat {
pub count: u32,
pub reclaimed_bytes: u64,
}
pub async fn prune_containers_core(state: &MaintenanceContext) -> Result<PruneStat, String> {
let result = state
.docker
.inner
.prune_containers(None::<PruneContainersOptions<String>>)
.await;
state.docker_cache.df.invalidate().await;
state.docker_cache.networks.invalidate().await;
match result {
Ok(r) => Ok(PruneStat {
count: r.containers_deleted.map(|v| v.len() as u32).unwrap_or(0),
reclaimed_bytes: r.space_reclaimed.unwrap_or(0) as u64,
}),
Err(e) => Err(e.to_string()),
}
}
pub async fn prune_volumes_core(
state: &MaintenanceContext,
all: bool,
) -> Result<PruneStat, String> {
let mut filters = std::collections::HashMap::new();
if all {
filters.insert("all", vec!["true"]);
}
let result = state
.docker
.inner
.prune_volumes(Some(PruneVolumesOptions { filters }))
.await;
state.docker_cache.df.invalidate().await;
match result {
Ok(r) => Ok(PruneStat {
count: r.volumes_deleted.map(|v| v.len() as u32).unwrap_or(0),
reclaimed_bytes: r.space_reclaimed.unwrap_or(0) as u64,
}),
Err(e) => Err(e.to_string()),
}
}
pub async fn prune_images_core(
state: &MaintenanceContext,
all: bool,
older_than_hours: u32,
) -> Result<PruneStat, String> {
let result = state
.docker
.inner
.prune_images(Some(PruneImagesOptions {
filters: image_prune_filters(all, older_than_hours),
}))
.await;
state.docker_cache.df.invalidate().await;
match result {
Ok(r) => Ok(PruneStat {
count: r.images_deleted.map(|v| v.len() as u32).unwrap_or(0),
reclaimed_bytes: r.space_reclaimed.unwrap_or(0) as u64,
}),
Err(e) => Err(e.to_string()),
}
}
fn image_prune_filters(
all: bool,
older_than_hours: u32,
) -> std::collections::HashMap<String, Vec<String>> {
let mut filters = std::collections::HashMap::new();
if all {
filters.insert("dangling".to_string(), vec!["false".to_string()]);
}
if older_than_hours > 0 {
filters.insert("until".to_string(), vec![format!("{older_than_hours}h")]);
}
filters
}
fn build_cache_prune_args(older_than_hours: u32) -> Vec<String> {
build_cache_prune_args_for(crate::engine::engine(), older_than_hours)
}
fn build_cache_prune_args_for(engine: crate::engine::Engine, older_than_hours: u32) -> Vec<String> {
let mut args = match engine {
crate::engine::Engine::Docker => {
vec!["builder".to_string(), "prune".to_string(), "-f".to_string()]
}
crate::engine::Engine::Podman => vec![
"system".to_string(),
"prune".to_string(),
"-f".to_string(),
"--build".to_string(),
],
};
if older_than_hours > 0 {
args.push("--filter".to_string());
args.push(format!("until={older_than_hours}h"));
}
args
}
pub async fn prune_build_cache_core(
state: &MaintenanceContext,
older_than_hours: u32,
) -> Result<PruneStat, String> {
let output = tokio::time::timeout(
BUILD_CACHE_PRUNE_TIMEOUT,
crate::engine::cli()
.args(build_cache_prune_args(older_than_hours))
.kill_on_drop(true)
.output(),
)
.await;
match output {
Ok(Ok(out)) if out.status.success() => {
record_build_cache_prune(&state.db, older_than_hours).await;
let text = String::from_utf8_lossy(&out.stdout);
let reclaimed_bytes = parse_reclaimed_space(&text);
Ok(PruneStat {
count: 0,
reclaimed_bytes,
})
}
Ok(Ok(out)) => Err(String::from_utf8_lossy(&out.stderr).trim().to_string()),
Ok(Err(e)) => Err(e.to_string()),
Err(_) => Err(format!(
"docker builder prune passou de {}s e foi interrompido",
BUILD_CACHE_PRUNE_TIMEOUT.as_secs()
)),
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct BuildCachePrune {
pub at: DateTime<Utc>,
pub older_than_hours: u32,
}
async fn record_build_cache_prune(db: &Db, older_than_hours: u32) {
let marker = BuildCachePrune {
at: Utc::now(),
older_than_hours,
};
let Ok(json) = serde_json::to_string(&marker) else {
return;
};
if let Err(e) =
daemon_settings::set(db, daemon_settings::KEY_BUILD_CACHE_LAST_PRUNE, &json).await
{
tracing::warn!(error = %e, "docker_prune: falha ao registrar a limpeza do cache de build");
}
}
pub async fn last_build_cache_prune(db: &Db) -> Option<BuildCachePrune> {
let raw = daemon_settings::get(db, daemon_settings::KEY_BUILD_CACHE_LAST_PRUNE)
.await
.ok()
.flatten()?;
serde_json::from_str(&raw).ok()
}
fn parse_reclaimed_space(output: &str) -> u64 {
for line in output.lines() {
let line = line.trim();
if let Some(rest) = line.strip_prefix("Total reclaimed space:") {
let rest = rest.trim();
if let Some((num, unit)) = rest.split_once(char::is_whitespace) {
let n: f64 = num.parse().unwrap_or(0.0);
return match unit.to_uppercase().as_str() {
"GB" | "GIB" => (n * 1_000_000_000.0) as u64,
"MB" | "MIB" => (n * 1_000_000.0) as u64,
"KB" | "KIB" => (n * 1_000.0) as u64,
_ => n as u64,
};
}
}
}
0
}
pub async fn prune_networks_core(state: &MaintenanceContext) -> Result<PruneStat, String> {
let result = state
.docker
.inner
.prune_networks(None::<PruneNetworksOptions<String>>)
.await;
state.docker_cache.networks.invalidate().await;
match result {
Ok(r) => Ok(PruneStat {
count: r.networks_deleted.map(|v| v.len() as u32).unwrap_or(0),
reclaimed_bytes: 0,
}),
Err(e) => Err(e.to_string()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_cache_prune_podman_usa_system_prune() {
use crate::engine::Engine;
assert_eq!(
build_cache_prune_args_for(Engine::Podman, 24),
["system", "prune", "-f", "--build", "--filter", "until=24h"]
);
}
#[test]
fn zero_hours_prunes_all_build_cache() {
assert_eq!(build_cache_prune_args(0), ["builder", "prune", "-f"]);
}
#[test]
fn retention_hours_become_an_until_filter() {
assert_eq!(
build_cache_prune_args(168),
["builder", "prune", "-f", "--filter", "until=168h"]
);
}
#[test]
fn image_filters_combine_all_and_age() {
assert!(image_prune_filters(false, 0).is_empty());
let filters = image_prune_filters(true, 24);
assert_eq!(filters["dangling"], ["false"]);
assert_eq!(filters["until"], ["24h"]);
}
}