Skip to main content

upstream_rs/services/packaging/
disk_impact.rs

1use std::fs;
2use std::path::Path;
3
4use anyhow::{Context, Result};
5use walkdir::WalkDir;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum SizeConfidence {
9    Exact,
10    Estimated,
11    Unknown,
12}
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct ByteEstimate {
16    pub bytes: Option<u64>,
17    pub confidence: SizeConfidence,
18}
19
20impl ByteEstimate {
21    pub fn exact(bytes: u64) -> Self {
22        Self {
23            bytes: Some(bytes),
24            confidence: SizeConfidence::Exact,
25        }
26    }
27
28    pub fn estimated(bytes: u64) -> Self {
29        Self {
30            bytes: Some(bytes),
31            confidence: SizeConfidence::Estimated,
32        }
33    }
34
35    pub fn unknown() -> Self {
36        Self {
37            bytes: None,
38            confidence: SizeConfidence::Unknown,
39        }
40    }
41
42    pub fn is_unknown(self) -> bool {
43        self.bytes.is_none()
44    }
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub struct SignedByteEstimate {
49    pub bytes: Option<i128>,
50    pub confidence: SizeConfidence,
51}
52
53impl SignedByteEstimate {
54    pub fn exact(bytes: i128) -> Self {
55        Self {
56            bytes: Some(bytes),
57            confidence: SizeConfidence::Exact,
58        }
59    }
60
61    pub fn estimated(bytes: i128) -> Self {
62        Self {
63            bytes: Some(bytes),
64            confidence: SizeConfidence::Estimated,
65        }
66    }
67
68    pub fn unknown() -> Self {
69        Self {
70            bytes: None,
71            confidence: SizeConfidence::Unknown,
72        }
73    }
74
75    pub fn is_unknown(self) -> bool {
76        self.bytes.is_none()
77    }
78}
79
80impl std::ops::Add for SignedByteEstimate {
81    type Output = Self;
82
83    fn add(self, other: Self) -> Self {
84        match (self.bytes, other.bytes) {
85            (Some(left), Some(right)) => {
86                let confidence = combine_confidence(self.confidence, other.confidence);
87                Self {
88                    bytes: Some(left.saturating_add(right)),
89                    confidence,
90                }
91            }
92            _ => Self::unknown(),
93        }
94    }
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct DiskImpact {
99    pub download: ByteEstimate,
100    pub net: SignedByteEstimate,
101}
102
103impl DiskImpact {
104    pub fn empty() -> Self {
105        Self {
106            download: ByteEstimate::exact(0),
107            net: SignedByteEstimate::exact(0),
108        }
109    }
110
111    pub fn unknown() -> Self {
112        Self {
113            download: ByteEstimate::unknown(),
114            net: SignedByteEstimate::unknown(),
115        }
116    }
117}
118
119impl std::ops::Add for DiskImpact {
120    type Output = Self;
121
122    fn add(mut self, other: Self) -> Self {
123        self.download = add_unsigned(self.download, other.download);
124        self.net = self.net + other.net;
125        self
126    }
127}
128
129pub fn estimate_path_size(path: &Path) -> Result<u64> {
130    if !path.exists() {
131        return Ok(0);
132    }
133
134    if path.is_file() || path.is_symlink() {
135        return fs::symlink_metadata(path)
136            .map(|metadata| metadata.len())
137            .with_context(|| format!("Failed to read metadata for '{}'", path.display()));
138    }
139
140    let mut total = 0_u64;
141    for entry in WalkDir::new(path).follow_links(false) {
142        let entry = entry.with_context(|| format!("Failed to scan '{}'", path.display()))?;
143        if entry.file_type().is_file() || entry.file_type().is_symlink() {
144            let metadata = entry.metadata().with_context(|| {
145                format!("Failed to read metadata for '{}'", entry.path().display())
146            })?;
147            total = total.saturating_add(metadata.len());
148        }
149    }
150    Ok(total)
151}
152
153pub fn estimate_existing_paths(paths: impl IntoIterator<Item = impl AsRef<Path>>) -> Result<u64> {
154    let mut total = 0_u64;
155    for path in paths {
156        total = total.saturating_add(estimate_path_size(path.as_ref())?);
157    }
158    Ok(total)
159}
160
161pub fn asset_size_estimate(bytes: u64) -> ByteEstimate {
162    if bytes == 0 {
163        ByteEstimate::unknown()
164    } else {
165        ByteEstimate::estimated(bytes)
166    }
167}
168
169pub fn install_impact_from_download(download: ByteEstimate) -> DiskImpact {
170    let net = match download.bytes {
171        Some(bytes) => SignedByteEstimate {
172            bytes: Some(i128::from(bytes)),
173            confidence: download.confidence,
174        },
175        None => SignedByteEstimate::unknown(),
176    };
177    DiskImpact { download, net }
178}
179
180fn add_unsigned(left: ByteEstimate, right: ByteEstimate) -> ByteEstimate {
181    match (left.bytes, right.bytes) {
182        (Some(a), Some(b)) => ByteEstimate {
183            bytes: Some(a.saturating_add(b)),
184            confidence: combine_confidence(left.confidence, right.confidence),
185        },
186        _ => ByteEstimate::unknown(),
187    }
188}
189
190fn combine_confidence(left: SizeConfidence, right: SizeConfidence) -> SizeConfidence {
191    match (left, right) {
192        (SizeConfidence::Unknown, _) | (_, SizeConfidence::Unknown) => SizeConfidence::Unknown,
193        (SizeConfidence::Estimated, _) | (_, SizeConfidence::Estimated) => {
194            SizeConfidence::Estimated
195        }
196        (SizeConfidence::Exact, SizeConfidence::Exact) => SizeConfidence::Exact,
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::{SignedByteEstimate, estimate_path_size};
203    use std::fs;
204
205    #[test]
206    fn signed_estimates_add_and_preserve_estimated_confidence() {
207        let total = SignedByteEstimate::exact(10) + SignedByteEstimate::estimated(-3);
208        assert_eq!(total.bytes, Some(7));
209        assert_eq!(format!("{:?}", total.confidence), "Estimated");
210    }
211
212    #[test]
213    fn path_size_counts_nested_files() {
214        let root =
215            std::env::temp_dir().join(format!("upstream-disk-impact-test-{}", std::process::id()));
216        let _ = fs::remove_dir_all(&root);
217        fs::create_dir_all(root.join("nested")).expect("create dir");
218        fs::write(root.join("a"), b"abc").expect("write a");
219        fs::write(root.join("nested").join("b"), b"defg").expect("write b");
220
221        assert_eq!(estimate_path_size(&root).expect("size"), 7);
222        fs::remove_dir_all(root).expect("cleanup");
223    }
224}