upstream_rs/services/packaging/
disk_impact.rs1use 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}