1use anyhow::{Result, anyhow};
24use libc::statvfs;
25use serde::{Deserialize, Serialize};
26use std::ffi::{CString, c_char};
27use std::fs::{read_dir, read_to_string};
28use std::path::{Path, PathBuf};
29
30use crate::traits::ToJson;
31use crate::utils::Size;
32
33#[derive(Debug, Deserialize, Serialize, Clone)]
35pub struct Partitions {
36 pub parts: Vec<Partition>,
37}
38
39impl Partitions {
40 pub fn new() -> Result<Self> {
41 let contents = read_to_string("/proc/partitions")?;
42 Self::from_str(&contents)
43 }
44
45 fn from_str(s: &str) -> Result<Self> {
46 let lines = s.lines().skip(1).filter(|s| {
47 !s.is_empty() && !s.starts_with('m') && !s.contains("loop") && !s.contains("ram")
48 });
49
50 let mut parts = Vec::new();
51 for line in lines {
52 match Partition::try_from(line) {
53 Ok(part) => parts.push(part),
54 Err(why) => return Err(anyhow!("{why}")),
55 }
56 }
57
58 Ok(Self { parts })
59 }
60}
61
62impl ToJson for Partitions {}
63
64#[derive(Debug, Deserialize, Serialize, Clone)]
65pub struct Partition {
66 pub major: usize,
67 pub minor: usize,
68 pub blocks: u64,
69 pub name: String,
70 pub dev_info: DeviceInfo,
71 pub statvfs: Option<FileSystemStats>,
72}
73
74impl Partition {
75 pub fn get_logical_size(&self) -> Option<Size> {
76 let lbsize = self.dev_info.logical_block_size;
77 match lbsize {
78 Some(lbsize) => {
79 let blocks = self.blocks;
80 Some(Size::B(blocks * lbsize))
81 }
82 None => None,
83 }
84 }
85}
86
87impl TryFrom<&str> for Partition {
88 type Error = String;
89 fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
90 let mut chs = value.split_whitespace();
91
92 match (chs.next(), chs.next(), chs.next(), chs.next()) {
93 (Some(major), Some(minor), Some(blocks), Some(name)) => {
94 let major = major.parse::<usize>().map_err(|err| format!("{err}"))?;
95 let minor = minor.parse::<usize>().map_err(|err| format!("{err}"))?;
96 let blocks = blocks.parse::<u64>().map_err(|err| format!("{err}"))?;
97
98 Ok(Self {
99 major,
100 minor,
101 blocks,
102 name: name.to_string(),
103 dev_info: DeviceInfo::get(name),
104 statvfs: FileSystemStats::from_path(Path::new("/dev/").join(name)).ok(), })
106 }
107 _ => Err(format!("String '{value}' parsing error")),
108 }
109 }
110}
111
112#[derive(Debug, Deserialize, Serialize, Clone)]
113pub struct DeviceInfo {
114 pub model: Option<String>,
115 pub vendor: Option<String>,
116 pub serial: Option<String>,
117 pub logical_block_size: Option<u64>,
118}
119
120impl DeviceInfo {
121 pub fn get(devname: &str) -> Self {
122 let path = Path::new("/sys/block/").join(devname);
123 let device = path.join("device");
124 let queue = path.join("queue");
125
126 let model = device.join("model");
127 let vendor = device.join("vendor");
128 let serial = device.join("serial");
129
130 let logical_block_size = queue.join("logical_block_size");
131 let logical_block_size = match read_to_string(logical_block_size) {
132 Ok(lbs) => lbs.trim().parse::<u64>().ok(),
133 Err(_) => None,
134 };
135
136 Self {
137 model: read_to_string(model)
138 .ok()
139 .and_then(|m| Some(m.trim().to_string())),
140 vendor: read_to_string(vendor)
141 .ok()
142 .and_then(|v| Some(v.trim().to_string())),
143 serial: read_to_string(serial)
144 .ok()
145 .and_then(|s| Some(s.trim().to_string())),
146 logical_block_size,
147 }
148 }
149
150 pub fn is_none(&self) -> bool {
151 self.model.is_none()
152 && self.vendor.is_none()
153 && self.serial.is_none()
154 && self.logical_block_size.is_none()
155 }
156}
157
158#[derive(Debug, Deserialize, Serialize, Clone)]
160pub struct Storages {
161 pub storages: Vec<Storage>,
162}
163
164impl Storages {
165 pub fn new() -> Result<Self> {
166 let dir_contents = read_dir("/sys/block")?.filter(|entry| {
167 if entry.is_err() {
168 false
169 } else {
170 let entry = entry.as_ref().unwrap();
171 let s = entry.path().to_string_lossy().to_string();
172 !(s.contains("loop") || s.contains("zram"))
173 }
174 });
175
176 let mut storages = vec![];
177 for dir in dir_contents {
178 let dir = dir?.path();
179 storages.push(Storage::from_pathbuf(&dir)?);
180 }
181 Ok(Self { storages })
182 }
183}
184
185#[derive(Debug, Deserialize, Serialize, Clone)]
186pub struct Storage {
187 pub devname: String,
189
190 pub removable: bool,
191
192 pub ro: bool,
194
195 pub size: Size,
197
198 pub hidden: bool,
199
200 pub uuid: Option<String>,
201
202 pub model: Option<String>,
204
205 pub vendor: Option<String>,
207
208 pub serial: Option<String>,
210
211 pub revision: Option<String>,
213
214 pub wwid_eui: Option<String>,
215
216 pub transport: Option<String>,
217}
218
219impl Storage {
220 pub fn from_pathbuf(path: &PathBuf) -> Result<Self> {
221 let read = |file: &str| read_to_string(path.join(file));
222
223 let devname = path
224 .strip_prefix("/sys/block/")?
225 .to_string_lossy()
226 .to_string();
227 let removable = {
228 let data = read("removable")?;
229 if data.trim() == "0" { false } else { true }
230 };
231 let ro = {
232 let data = read("ro")?;
233 if data.trim() == "0" { false } else { true }
234 };
235 let hidden = {
236 let data = read("hidden")?;
237 if data.trim() == "0" { false } else { true }
238 };
239 let size = {
240 let data = read("size")?;
241 Size::B(data.trim().parse()?)
242 };
243 let uuid = read("uuid").and_then(|a| Ok(a.trim().to_string())).ok();
244 let model = read("device/model")
245 .and_then(|a| Ok(a.trim().to_string()))
246 .ok();
247 let vendor = read("device/vendor")
248 .and_then(|a| Ok(a.trim().to_string()))
249 .ok();
250 let serial = read("device/serial")
251 .and_then(|a| Ok(a.trim().to_string()))
252 .ok();
253 let revision = read("device/firmware_rev")
254 .and_then(|a| Ok(a.trim().to_string()))
255 .ok();
256 let transport = read("device/transport")
257 .and_then(|a| Ok(a.trim().to_string()))
258 .ok();
259 let wwid_eui = read("wwid").and_then(|a| Ok(a.replace("eui.", ""))).ok();
260
261 Ok(Self {
262 devname,
263 removable,
264 ro,
265 size,
266 hidden,
267 uuid,
268 model,
269 vendor,
270 serial,
271 transport,
272 wwid_eui,
273 revision,
274 })
275 }
276}
277
278#[derive(Debug, Deserialize, Serialize, Clone)]
280pub struct Mounts {
281 pub mounts: Vec<MountEntry>,
282}
283
284#[derive(Debug, Deserialize, Serialize, Clone)]
285pub struct MountEntry {
286 pub device: String,
287 pub mount_point: String,
288 pub filesystem: String,
289 pub options: String,
290 pub dump: u8,
291 pub pass: u8,
292 pub fstats: Option<FileSystemStats>,
293}
294
295impl TryFrom<&str> for MountEntry {
296 type Error = anyhow::Error;
297
298 fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
299 let values = value.split_whitespace().collect::<Vec<_>>();
300 if values.len() != 6 {
301 return Err(anyhow!(
302 "Format of mount string is incorrect\n(string: \"{value}\")",
303 ));
304 }
305
306 Ok(Self {
307 device: values[0].to_string(),
308 mount_point: values[1].to_string(),
309 filesystem: values[2].to_string(),
310 options: values[3].to_string(),
311 dump: values[4].parse()?,
312 pass: values[5].parse()?,
313 fstats: FileSystemStats::from_path(values[1]).ok(),
314 })
315 }
316}
317
318impl Mounts {
319 pub fn new() -> Result<Self> {
320 let contents = read_to_string("/proc/mounts")?;
321 let lines = contents.lines();
322 let mut mounts = vec![];
323
324 for line in lines {
325 if line.starts_with("/")
326 || line.starts_with("udev")
327 || line.starts_with("sysfs")
328 || line.starts_with("tmpfs")
329 {
330 mounts.push(MountEntry::try_from(line)?);
331 }
332 }
333 Ok(Self { mounts })
334 }
335}
336
337#[derive(Debug, Deserialize, Serialize, Clone, Copy)]
338pub struct FileSystemStats {
339 pub block_size: u64,
340 pub fragment_size: u64,
341 pub total_blocks: u64,
342 pub free_blocks: u64,
343 pub available_blocks: u64,
344 pub total_inodes: u64,
345 pub free_inodes: u64,
346}
347
348impl FileSystemStats {
349 pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
350 let path_str = path
351 .as_ref()
352 .to_str()
353 .ok_or_else(|| anyhow!("Invalid characters in path ()"))?;
354 let c_path = CString::new(path_str)
355 .map_err(|err| anyhow!("Failed to convert Rust string into C string: {err}"))?;
356
357 unsafe { Self::statvfs(c_path.as_ptr()) }
358 }
359
360 unsafe fn statvfs(path: *const c_char) -> Result<Self> {
361 let mut stats: libc::statvfs = unsafe { std::mem::zeroed() };
362 let result = unsafe { statvfs(path, &mut stats) };
363
364 if result == 0 {
365 Ok(Self {
366 block_size: stats.f_bsize as u64,
367 fragment_size: stats.f_frsize as u64,
368 total_blocks: stats.f_blocks as u64,
369 free_blocks: stats.f_bfree as u64,
370 available_blocks: stats.f_bavail as u64,
371 total_inodes: stats.f_files as u64,
372 free_inodes: stats.f_ffree as u64,
373 })
374 } else {
375 Err(anyhow!(
376 "statvfs() failed: errno {}",
377 std::io::Error::last_os_error()
378 ))
379 }
380 }
381
382 pub fn total_bytes(&self) -> u64 {
383 self.total_blocks * self.fragment_size
384 }
385
386 pub fn total_size(&self) -> Size {
387 Size::B(self.total_bytes())
388 }
389
390 pub fn free_bytes(&self) -> u64 {
391 self.free_blocks * self.fragment_size
392 }
393
394 pub fn free_size(&self) -> Size {
395 Size::B(self.free_bytes())
396 }
397
398 pub fn avail_bytes(&self) -> u64 {
399 self.available_blocks * self.fragment_size
400 }
401
402 pub fn avail_size(&self) -> Size {
403 Size::B(self.avail_bytes())
404 }
405
406 pub fn used_bytes(&self) -> u64 {
407 if self.total_bytes() == 0 {
408 return 0;
409 }
410 self.total_bytes() - self.free_bytes()
411 }
412
413 pub fn used_size(&self) -> Size {
414 Size::B(self.used_bytes())
415 }
416
417 pub fn usage_percent(&self) -> f64 {
418 if self.total_bytes() == 0 {
419 return 0.;
420 }
421 let used = self.used_bytes() as f64;
422 let total = self.total_bytes() as f64;
423 (used / total) * 100.
424 }
425}
426
427#[cfg(test)]
428mod tests {
429 use super::*;
430
431 const PARTITIONS: &str = "major minor #blocks name
432
433 259 0 250059096 nvme0n1
434 259 1 102400 nvme0n1p1
435 259 2 16384 nvme0n1p2
436 259 3 249068548 nvme0n1p3
437 259 4 866304 nvme0n1p4
438 8 0 468851544 sda
439 8 1 614400 sda1
440 8 2 73138176 sda2
441 8 3 337163264 sda3
442 8 4 57933824 sda4
443 253 0 3976960 zram0";
444
445 #[test]
446 fn partitions_from_str_test() {
447 let parts = Partitions::from_str(PARTITIONS).unwrap();
448 dbg!(&parts);
449 assert_eq!(parts.parts.len(), 10);
450 assert_eq!(&parts.parts[0].name, "nvme0n1");
451 assert_eq!(parts.parts[0].major, 259);
452 assert_eq!(parts.parts[0].minor, 0);
453 assert_eq!(parts.parts[0].blocks, 250059096);
454 let _ = std::fs::write("./test-filesystems.json", parts.to_json_pretty().unwrap());
455 }
456
457 #[test]
458 fn partition_invalid_str_test() {
459 let s = "256 0 nvme";
460 let part = Partition::try_from(s);
461 assert!(part.is_err());
462 }
463
464 #[test]
465 fn partition_valid_str_test() {
466 let s = "255 4 666 sda";
467 let part = Partition::try_from(s);
468 assert!(part.is_ok());
469 }
470}