1use anyhow::{Result, anyhow};
30use libc::statvfs;
31use serde::{Deserialize, Serialize};
32use std::ffi::{CString, c_char};
33use std::fs::{read_dir, read_to_string};
34use std::path::{Path, PathBuf};
35
36use crate::traits::ToJson;
37use crate::utils::Size;
38
39#[derive(Debug, Deserialize, Serialize, Clone)]
45pub struct Partitions {
46 pub parts: Vec<Partition>,
47}
48
49impl Partitions {
50 pub fn new() -> Result<Self> {
51 let contents = read_to_string("/proc/partitions")?;
52 Self::from_str(&contents)
53 }
54
55 fn from_str(s: &str) -> Result<Self> {
58 let lines = s.lines().skip(1).filter(|s| {
59 !s.is_empty() && !s.starts_with('m') && !s.contains("loop") && !s.contains("ram")
60 });
61
62 let mut parts = Vec::new();
63 for line in lines {
64 match Partition::try_from(line) {
65 Ok(part) => parts.push(part),
66 Err(why) => return Err(anyhow!("{why}")),
67 }
68 }
69
70 Ok(Self { parts })
71 }
72}
73
74impl ToJson for Partitions {}
75
76#[derive(Debug, Deserialize, Serialize, Clone)]
78pub struct Partition {
79 pub major: usize,
81
82 pub minor: usize,
84
85 pub blocks: u64,
87
88 pub name: String,
90
91 pub dev_info: DeviceInfo,
93
94 pub statvfs: Option<FileSystemStats>,
96}
97
98impl Partition {
99 pub fn get_logical_size(&self) -> Option<Size> {
104 let lbsize = self.dev_info.logical_block_size;
105 match lbsize {
106 Some(lbsize) => {
107 let blocks = self.blocks;
108 Some(Size::B(blocks * lbsize))
109 }
110 None => None,
111 }
112 }
113}
114
115impl TryFrom<&str> for Partition {
116 type Error = String;
117 fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
118 let mut chs = value.split_whitespace();
119
120 match (chs.next(), chs.next(), chs.next(), chs.next()) {
121 (Some(major), Some(minor), Some(blocks), Some(name)) => {
122 let major = major.parse::<usize>().map_err(|err| format!("{err}"))?;
123 let minor = minor.parse::<usize>().map_err(|err| format!("{err}"))?;
124 let blocks = blocks.parse::<u64>().map_err(|err| format!("{err}"))?;
125
126 Ok(Self {
127 major,
128 minor,
129 blocks,
130 name: name.to_string(),
131 dev_info: DeviceInfo::get(name),
132 statvfs: FileSystemStats::from_path(Path::new("/dev/").join(name)).ok(), })
134 }
135 _ => Err(format!("String '{value}' parsing error")),
136 }
137 }
138}
139
140#[derive(Debug, Deserialize, Serialize, Clone)]
145pub struct DeviceInfo {
146 pub model: Option<String>,
148
149 pub vendor: Option<String>,
151
152 pub serial: Option<String>,
154
155 pub logical_block_size: Option<u64>,
157}
158
159impl DeviceInfo {
160 pub fn get(devname: &str) -> Self {
162 let path = Path::new("/sys/block/").join(devname);
163 let device = path.join("device");
164 let queue = path.join("queue");
165
166 let model = device.join("model");
167 let vendor = device.join("vendor");
168 let serial = device.join("serial");
169
170 let logical_block_size = queue.join("logical_block_size");
171 let logical_block_size = match read_to_string(logical_block_size) {
172 Ok(lbs) => lbs.trim().parse::<u64>().ok(),
173 Err(_) => None,
174 };
175
176 Self {
177 model: read_to_string(model)
178 .ok()
179 .and_then(|m| Some(m.trim().to_string())),
180 vendor: read_to_string(vendor)
181 .ok()
182 .and_then(|v| Some(v.trim().to_string())),
183 serial: read_to_string(serial)
184 .ok()
185 .and_then(|s| Some(s.trim().to_string())),
186 logical_block_size,
187 }
188 }
189
190 pub fn is_none(&self) -> bool {
192 self.model.is_none()
193 && self.vendor.is_none()
194 && self.serial.is_none()
195 && self.logical_block_size.is_none()
196 }
197}
198
199#[derive(Debug, Deserialize, Serialize, Clone)]
201pub struct Storages {
202 pub storages: Vec<Storage>,
203}
204
205impl Storages {
206 pub fn new() -> Result<Self> {
211 let dir_contents = read_dir("/sys/block")?.filter(|entry| {
212 if entry.is_err() {
213 false
214 } else {
215 let entry = entry.as_ref().unwrap();
216 let s = entry.path().to_string_lossy().to_string();
217 !(s.contains("loop") || s.contains("zram"))
218 }
219 });
220
221 let mut storages = vec![];
222 for dir in dir_contents {
223 let dir = dir?.path();
224 storages.push(Storage::from_pathbuf(&dir)?);
225 }
226 Ok(Self { storages })
227 }
228}
229
230#[derive(Debug, Deserialize, Serialize, Clone)]
232pub struct Storage {
233 pub devname: String,
237
238 pub removable: bool,
240
241 pub ro: bool,
243
244 pub size: Size,
246
247 pub hidden: bool,
249
250 pub uuid: Option<String>,
252
253 pub model: Option<String>,
255
256 pub vendor: Option<String>,
258
259 pub serial: Option<String>,
261
262 pub revision: Option<String>,
264
265 pub wwid_eui: Option<String>,
268
269 pub transport: Option<String>,
271}
272
273impl Storage {
274 pub fn from_pathbuf(path: &PathBuf) -> Result<Self> {
276 let read = |file: &str| read_to_string(path.join(file));
277
278 let devname = path
279 .strip_prefix("/sys/block/")?
280 .to_string_lossy()
281 .to_string();
282 let removable = {
283 let data = read("removable")?;
284 if data.trim() == "0" { false } else { true }
285 };
286 let ro = {
287 let data = read("ro")?;
288 if data.trim() == "0" { false } else { true }
289 };
290 let hidden = {
291 let data = read("hidden")?;
292 if data.trim() == "0" { false } else { true }
293 };
294 let size = {
295 let data = read("size")?;
296 Size::B(data.trim().parse()?)
297 };
298 let uuid = read("uuid").and_then(|a| Ok(a.trim().to_string())).ok();
299 let model = read("device/model")
300 .and_then(|a| Ok(a.trim().to_string()))
301 .ok();
302 let vendor = read("device/vendor")
303 .and_then(|a| Ok(a.trim().to_string()))
304 .ok();
305 let serial = read("device/serial")
306 .and_then(|a| Ok(a.trim().to_string()))
307 .ok();
308 let revision = read("device/firmware_rev")
309 .and_then(|a| Ok(a.trim().to_string()))
310 .ok();
311 let transport = read("device/transport")
312 .and_then(|a| Ok(a.trim().to_string()))
313 .ok();
314 let wwid_eui = read("wwid").and_then(|a| Ok(a.replace("eui.", ""))).ok();
315
316 Ok(Self {
317 devname,
318 removable,
319 ro,
320 size,
321 hidden,
322 uuid,
323 model,
324 vendor,
325 serial,
326 transport,
327 wwid_eui,
328 revision,
329 })
330 }
331}
332
333#[derive(Debug, Deserialize, Serialize, Clone)]
335pub struct Mounts {
336 pub mounts: Vec<MountEntry>,
337}
338
339#[derive(Debug, Deserialize, Serialize, Clone)]
341pub struct MountEntry {
342 pub device: String,
344
345 pub mount_point: String,
347
348 pub filesystem: String,
350
351 pub options: String,
353
354 pub dump: u8,
356
357 pub pass: u8,
359
360 pub fstats: Option<FileSystemStats>,
362}
363
364impl TryFrom<&str> for MountEntry {
365 type Error = anyhow::Error;
366
367 fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
368 let values = value.split_whitespace().collect::<Vec<_>>();
369 if values.len() != 6 {
370 return Err(anyhow!(
371 "Format of mount string is incorrect\n(string: \"{value}\")",
372 ));
373 }
374
375 Ok(Self {
376 device: values[0].to_string(),
377 mount_point: values[1].to_string(),
378 filesystem: values[2].to_string(),
379 options: values[3].to_string(),
380 dump: values[4].parse()?,
381 pass: values[5].parse()?,
382 fstats: FileSystemStats::from_path(values[1]).ok(),
383 })
384 }
385}
386
387impl Mounts {
388 pub fn new() -> Result<Self> {
389 let contents = read_to_string("/proc/mounts")?;
390 let lines = contents.lines();
391 let mut mounts = vec![];
392
393 for line in lines {
394 if line.starts_with("/")
395 || line.starts_with("udev")
396 || line.starts_with("sysfs")
397 || line.starts_with("tmpfs")
398 {
399 mounts.push(MountEntry::try_from(line)?);
400 }
401 }
402 Ok(Self { mounts })
403 }
404}
405
406#[derive(Debug, Deserialize, Serialize, Clone, Copy)]
408pub struct FileSystemStats {
409 pub block_size: u64,
411
412 pub fragment_size: u64,
414
415 pub total_blocks: u64,
417
418 pub free_blocks: u64,
420
421 pub available_blocks: u64,
424
425 pub total_inodes: u64,
427
428 pub free_inodes: u64,
430}
431
432impl FileSystemStats {
433 pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
435 let path_str = path
436 .as_ref()
437 .to_str()
438 .ok_or_else(|| anyhow!("Invalid characters in path ()"))?;
439 let c_path = CString::new(path_str)
440 .map_err(|err| anyhow!("Failed to convert Rust string into C string: {err}"))?;
441
442 unsafe { Self::statvfs(c_path.as_ptr()) }
445 }
446
447 unsafe fn statvfs(path: *const c_char) -> Result<Self> {
449 let mut stats: libc::statvfs = unsafe { std::mem::zeroed() };
450 let result = unsafe { statvfs(path, &mut stats) };
451
452 if result == 0 {
453 Ok(Self {
454 block_size: stats.f_bsize as u64,
455 fragment_size: stats.f_frsize as u64,
456 total_blocks: stats.f_blocks as u64,
457 free_blocks: stats.f_bfree as u64,
458 available_blocks: stats.f_bavail as u64,
459 total_inodes: stats.f_files as u64,
460 free_inodes: stats.f_ffree as u64,
461 })
462 } else {
463 Err(anyhow!(
464 "statvfs() failed: errno {}",
465 std::io::Error::last_os_error()
466 ))
467 }
468 }
469
470 pub fn total_bytes(&self) -> u64 {
472 self.total_blocks * self.fragment_size
473 }
474
475 pub fn total_size(&self) -> Size {
477 Size::B(self.total_bytes())
478 }
479
480 pub fn free_bytes(&self) -> u64 {
482 self.free_blocks * self.fragment_size
483 }
484
485 pub fn free_size(&self) -> Size {
487 Size::B(self.free_bytes())
488 }
489
490 pub fn avail_bytes(&self) -> u64 {
492 self.available_blocks * self.fragment_size
493 }
494
495 pub fn avail_size(&self) -> Size {
496 Size::B(self.avail_bytes())
497 }
498
499 pub fn used_bytes(&self) -> u64 {
501 if self.total_bytes() == 0 {
502 return 0;
503 }
504 self.total_bytes() - self.free_bytes()
505 }
506
507 pub fn used_size(&self) -> Size {
508 Size::B(self.used_bytes())
509 }
510
511 pub fn usage_percent(&self) -> f64 {
513 if self.total_bytes() == 0 {
514 return 0.;
515 }
516 let used = self.used_bytes() as f64;
517 let total = self.total_bytes() as f64;
518 (used / total) * 100.
519 }
520}
521
522#[cfg(test)]
523mod tests {
524 use super::*;
525
526 const PARTITIONS: &str = "major minor #blocks name
527
528 259 0 250059096 nvme0n1
529 259 1 102400 nvme0n1p1
530 259 2 16384 nvme0n1p2
531 259 3 249068548 nvme0n1p3
532 259 4 866304 nvme0n1p4
533 8 0 468851544 sda
534 8 1 614400 sda1
535 8 2 73138176 sda2
536 8 3 337163264 sda3
537 8 4 57933824 sda4
538 253 0 3976960 zram0";
539
540 #[test]
541 fn partitions_from_str_test() {
542 let parts = Partitions::from_str(PARTITIONS).unwrap();
543 dbg!(&parts);
544 assert_eq!(parts.parts.len(), 10);
545 assert_eq!(&parts.parts[0].name, "nvme0n1");
546 assert_eq!(parts.parts[0].major, 259);
547 assert_eq!(parts.parts[0].minor, 0);
548 assert_eq!(parts.parts[0].blocks, 250059096);
549 let _ = std::fs::write("./test-filesystems.json", parts.to_json_pretty().unwrap());
550 }
551
552 #[test]
553 fn partition_invalid_str_test() {
554 let s = "256 0 nvme";
555 let part = Partition::try_from(s);
556 assert!(part.is_err());
557 }
558
559 #[test]
560 fn partition_valid_str_test() {
561 let s = "255 4 666 sda";
562 let part = Partition::try_from(s);
563 assert!(part.is_ok());
564 }
565}