upstream-rs 2.2.0

Fetch package updates directly from the source.
Documentation
use std::fs;
use std::path::Path;

use anyhow::{Context, Result};
use walkdir::WalkDir;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SizeConfidence {
    Exact,
    Estimated,
    Unknown,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ByteEstimate {
    pub bytes: Option<u64>,
    pub confidence: SizeConfidence,
}

impl ByteEstimate {
    pub fn exact(bytes: u64) -> Self {
        Self {
            bytes: Some(bytes),
            confidence: SizeConfidence::Exact,
        }
    }

    pub fn estimated(bytes: u64) -> Self {
        Self {
            bytes: Some(bytes),
            confidence: SizeConfidence::Estimated,
        }
    }

    pub fn unknown() -> Self {
        Self {
            bytes: None,
            confidence: SizeConfidence::Unknown,
        }
    }

    pub fn is_unknown(self) -> bool {
        self.bytes.is_none()
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SignedByteEstimate {
    pub bytes: Option<i128>,
    pub confidence: SizeConfidence,
}

impl SignedByteEstimate {
    pub fn exact(bytes: i128) -> Self {
        Self {
            bytes: Some(bytes),
            confidence: SizeConfidence::Exact,
        }
    }

    pub fn estimated(bytes: i128) -> Self {
        Self {
            bytes: Some(bytes),
            confidence: SizeConfidence::Estimated,
        }
    }

    pub fn unknown() -> Self {
        Self {
            bytes: None,
            confidence: SizeConfidence::Unknown,
        }
    }

    pub fn is_unknown(self) -> bool {
        self.bytes.is_none()
    }
}

impl std::ops::Add for SignedByteEstimate {
    type Output = Self;

    fn add(self, other: Self) -> Self {
        match (self.bytes, other.bytes) {
            (Some(left), Some(right)) => {
                let confidence = combine_confidence(self.confidence, other.confidence);
                Self {
                    bytes: Some(left.saturating_add(right)),
                    confidence,
                }
            }
            _ => Self::unknown(),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiskImpact {
    pub download: ByteEstimate,
    pub net: SignedByteEstimate,
}

impl DiskImpact {
    pub fn empty() -> Self {
        Self {
            download: ByteEstimate::exact(0),
            net: SignedByteEstimate::exact(0),
        }
    }

    pub fn unknown() -> Self {
        Self {
            download: ByteEstimate::unknown(),
            net: SignedByteEstimate::unknown(),
        }
    }
}

impl std::ops::Add for DiskImpact {
    type Output = Self;

    fn add(mut self, other: Self) -> Self {
        self.download = add_unsigned(self.download, other.download);
        self.net = self.net + other.net;
        self
    }
}

pub fn estimate_path_size(path: &Path) -> Result<u64> {
    if !path.exists() {
        return Ok(0);
    }

    if path.is_file() || path.is_symlink() {
        return fs::symlink_metadata(path)
            .map(|metadata| metadata.len())
            .with_context(|| format!("Failed to read metadata for '{}'", path.display()));
    }

    let mut total = 0_u64;
    for entry in WalkDir::new(path).follow_links(false) {
        let entry = entry.with_context(|| format!("Failed to scan '{}'", path.display()))?;
        if entry.file_type().is_file() || entry.file_type().is_symlink() {
            let metadata = entry.metadata().with_context(|| {
                format!("Failed to read metadata for '{}'", entry.path().display())
            })?;
            total = total.saturating_add(metadata.len());
        }
    }
    Ok(total)
}

pub fn estimate_existing_paths(paths: impl IntoIterator<Item = impl AsRef<Path>>) -> Result<u64> {
    let mut total = 0_u64;
    for path in paths {
        total = total.saturating_add(estimate_path_size(path.as_ref())?);
    }
    Ok(total)
}

pub fn asset_size_estimate(bytes: u64) -> ByteEstimate {
    if bytes == 0 {
        ByteEstimate::unknown()
    } else {
        ByteEstimate::estimated(bytes)
    }
}

pub fn install_impact_from_download(download: ByteEstimate) -> DiskImpact {
    let net = match download.bytes {
        Some(bytes) => SignedByteEstimate {
            bytes: Some(i128::from(bytes)),
            confidence: download.confidence,
        },
        None => SignedByteEstimate::unknown(),
    };
    DiskImpact { download, net }
}

fn add_unsigned(left: ByteEstimate, right: ByteEstimate) -> ByteEstimate {
    match (left.bytes, right.bytes) {
        (Some(a), Some(b)) => ByteEstimate {
            bytes: Some(a.saturating_add(b)),
            confidence: combine_confidence(left.confidence, right.confidence),
        },
        _ => ByteEstimate::unknown(),
    }
}

fn combine_confidence(left: SizeConfidence, right: SizeConfidence) -> SizeConfidence {
    match (left, right) {
        (SizeConfidence::Unknown, _) | (_, SizeConfidence::Unknown) => SizeConfidence::Unknown,
        (SizeConfidence::Estimated, _) | (_, SizeConfidence::Estimated) => {
            SizeConfidence::Estimated
        }
        (SizeConfidence::Exact, SizeConfidence::Exact) => SizeConfidence::Exact,
    }
}

#[cfg(test)]
mod tests {
    use super::{SignedByteEstimate, estimate_path_size};
    use std::fs;

    #[test]
    fn signed_estimates_add_and_preserve_estimated_confidence() {
        let total = SignedByteEstimate::exact(10) + SignedByteEstimate::estimated(-3);
        assert_eq!(total.bytes, Some(7));
        assert_eq!(format!("{:?}", total.confidence), "Estimated");
    }

    #[test]
    fn path_size_counts_nested_files() {
        let root =
            std::env::temp_dir().join(format!("upstream-disk-impact-test-{}", std::process::id()));
        let _ = fs::remove_dir_all(&root);
        fs::create_dir_all(root.join("nested")).expect("create dir");
        fs::write(root.join("a"), b"abc").expect("write a");
        fs::write(root.join("nested").join("b"), b"defg").expect("write b");

        assert_eq!(estimate_path_size(&root).expect("size"), 7);
        fs::remove_dir_all(root).expect("cleanup");
    }
}