#![allow(missing_docs)]
#[allow(unused_imports)]
use firkin_artifacts::SnapshotArtifactManifestError;
#[allow(unused_imports)]
use firkin_artifacts::{SnapshotArtifactManifest, is_snapshot_manifest_sidecar};
#[allow(unused_imports)]
use std::collections::BTreeSet;
#[allow(unused_imports)]
use std::fs;
#[allow(unused_imports)]
use std::io;
#[allow(unused_imports)]
use std::path::{Path, PathBuf};
#[allow(unused_imports)]
use std::time::Duration;
#[allow(unused_imports)]
use std::time::SystemTime;
#[allow(unused_imports)]
use thiserror::Error as ThisError;
#[derive(Debug, ThisError)]
pub enum ArtifactGcError {
#[error("artifact GC filesystem operation failed while {operation}: {source}")]
Io {
operation: &'static str,
#[source]
source: io::Error,
},
#[error("artifact GC manifest sidecar discovery failed: {0}")]
Manifest(#[from] SnapshotArtifactManifestError),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ArtifactGcPlan {
pub(crate) root: PathBuf,
keep: BTreeSet<PathBuf>,
delete: Vec<PathBuf>,
}
impl ArtifactGcPlan {
pub fn for_snapshot_dir(
root: impl AsRef<Path>,
manifests: impl IntoIterator<Item = SnapshotArtifactManifest>,
) -> Result<Self, ArtifactGcError> {
Self::for_snapshot_dir_with_retention(root, manifests, Duration::ZERO, SystemTime::now())
}
pub fn for_snapshot_dir_with_retention(
root: impl AsRef<Path>,
manifests: impl IntoIterator<Item = SnapshotArtifactManifest>,
min_unreferenced_age: Duration,
now: SystemTime,
) -> Result<Self, ArtifactGcError> {
let root = root.as_ref().to_path_buf();
let keep = manifests
.into_iter()
.map(|manifest| manifest.path().to_path_buf())
.collect::<BTreeSet<_>>();
let mut delete = Vec::new();
for entry in fs::read_dir(&root).map_err(|source| ArtifactGcError::Io {
operation: "read snapshot artifact directory",
source,
})? {
let entry = entry.map_err(|source| ArtifactGcError::Io {
operation: "read snapshot artifact entry",
source,
})?;
let path = entry.path();
let metadata = entry.metadata().map_err(|source| ArtifactGcError::Io {
operation: "stat snapshot artifact",
source,
})?;
if !keep.contains(&path)
&& !is_snapshot_manifest_sidecar(&path)
&& (metadata.is_file() || metadata.is_dir())
{
let modified = metadata.modified().map_err(|source| ArtifactGcError::Io {
operation: "stat snapshot artifact",
source,
})?;
if now.duration_since(modified).unwrap_or(Duration::ZERO) >= min_unreferenced_age {
delete.push(path);
}
}
}
delete.sort();
Ok(Self { root, keep, delete })
}
pub fn for_snapshot_dir_with_manifest_sidecars(
root: impl AsRef<Path>,
manifest_root: impl AsRef<Path>,
min_unreferenced_age: Duration,
now: SystemTime,
) -> Result<Self, ArtifactGcError> {
let manifests = SnapshotArtifactManifest::read_json_dir(manifest_root)?;
Self::for_snapshot_dir_with_retention(root, manifests, min_unreferenced_age, now)
}
#[must_use]
pub fn root(&self) -> &Path {
&self.root
}
#[must_use]
pub fn keep(&self) -> &BTreeSet<PathBuf> {
&self.keep
}
#[must_use]
pub fn delete(&self) -> &[PathBuf] {
&self.delete
}
#[must_use]
pub fn keeps_path(&self, path: impl AsRef<Path>) -> bool {
self.keep.contains(path.as_ref())
}
#[must_use]
pub fn deletes_path(&self, path: impl AsRef<Path>) -> bool {
self.delete
.iter()
.any(|candidate| candidate == path.as_ref())
}
pub fn execute(&self) -> Result<ArtifactGcReport, ArtifactGcError> {
let mut deleted = Vec::new();
for path in &self.delete {
if path.is_dir() {
fs::remove_dir_all(path).map_err(|source| ArtifactGcError::Io {
operation: "delete snapshot artifact directory",
source,
})?;
} else {
fs::remove_file(path).map_err(|source| ArtifactGcError::Io {
operation: "delete snapshot artifact file",
source,
})?;
}
deleted.push(path.clone());
}
Ok(ArtifactGcReport { deleted })
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ArtifactGcReport {
deleted: Vec<PathBuf>,
}
impl ArtifactGcReport {
#[must_use]
pub fn deleted(&self) -> &[PathBuf] {
&self.deleted
}
#[must_use]
pub const fn deleted_count(&self) -> usize {
self.deleted.len()
}
}