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