]>
Commit | Line | Data |
---|---|---|
923072b8 FG |
1 | // Take a look at the license at the top of the repository in the LICENSE file. |
2 | ||
487cf647 FG |
3 | use crate::sys::utils::{get_all_data, to_cpath}; |
4 | use crate::{DiskExt, DiskType}; | |
923072b8 FG |
5 | |
6 | use libc::statvfs; | |
7 | use std::ffi::{OsStr, OsString}; | |
8 | use std::fs; | |
9 | use std::mem; | |
10 | use std::os::unix::ffi::OsStrExt; | |
11 | use std::path::{Path, PathBuf}; | |
12 | ||
13 | macro_rules! cast { | |
14 | ($x:expr) => { | |
15 | u64::from($x) | |
16 | }; | |
17 | } | |
18 | ||
19 | #[doc = include_str!("../../md_doc/disk.md")] | |
20 | #[derive(PartialEq, Eq)] | |
21 | pub struct Disk { | |
22 | type_: DiskType, | |
23 | device_name: OsString, | |
24 | file_system: Vec<u8>, | |
25 | mount_point: PathBuf, | |
26 | total_space: u64, | |
27 | available_space: u64, | |
28 | is_removable: bool, | |
29 | } | |
30 | ||
31 | impl DiskExt for Disk { | |
32 | fn type_(&self) -> DiskType { | |
33 | self.type_ | |
34 | } | |
35 | ||
36 | fn name(&self) -> &OsStr { | |
37 | &self.device_name | |
38 | } | |
39 | ||
40 | fn file_system(&self) -> &[u8] { | |
41 | &self.file_system | |
42 | } | |
43 | ||
44 | fn mount_point(&self) -> &Path { | |
45 | &self.mount_point | |
46 | } | |
47 | ||
48 | fn total_space(&self) -> u64 { | |
49 | self.total_space | |
50 | } | |
51 | ||
52 | fn available_space(&self) -> u64 { | |
53 | self.available_space | |
54 | } | |
55 | ||
56 | fn is_removable(&self) -> bool { | |
57 | self.is_removable | |
58 | } | |
59 | ||
60 | fn refresh(&mut self) -> bool { | |
61 | unsafe { | |
62 | let mut stat: statvfs = mem::zeroed(); | |
487cf647 | 63 | let mount_point_cpath = to_cpath(&self.mount_point); |
923072b8 FG |
64 | if statvfs(mount_point_cpath.as_ptr() as *const _, &mut stat) == 0 { |
65 | let tmp = cast!(stat.f_bsize).saturating_mul(cast!(stat.f_bavail)); | |
66 | self.available_space = cast!(tmp); | |
67 | true | |
68 | } else { | |
69 | false | |
70 | } | |
71 | } | |
72 | } | |
73 | } | |
74 | ||
75 | fn new_disk( | |
76 | device_name: &OsStr, | |
77 | mount_point: &Path, | |
78 | file_system: &[u8], | |
79 | removable_entries: &[PathBuf], | |
80 | ) -> Option<Disk> { | |
487cf647 | 81 | let mount_point_cpath = to_cpath(mount_point); |
923072b8 FG |
82 | let type_ = find_type_for_device_name(device_name); |
83 | let mut total = 0; | |
84 | let mut available = 0; | |
85 | unsafe { | |
86 | let mut stat: statvfs = mem::zeroed(); | |
87 | if statvfs(mount_point_cpath.as_ptr() as *const _, &mut stat) == 0 { | |
88 | let bsize = cast!(stat.f_bsize); | |
89 | let blocks = cast!(stat.f_blocks); | |
90 | let bavail = cast!(stat.f_bavail); | |
91 | total = bsize.saturating_mul(blocks); | |
92 | available = bsize.saturating_mul(bavail); | |
93 | } | |
94 | if total == 0 { | |
95 | return None; | |
96 | } | |
97 | let mount_point = mount_point.to_owned(); | |
98 | let is_removable = removable_entries | |
99 | .iter() | |
100 | .any(|e| e.as_os_str() == device_name); | |
101 | Some(Disk { | |
102 | type_, | |
103 | device_name: device_name.to_owned(), | |
104 | file_system: file_system.to_owned(), | |
105 | mount_point, | |
106 | total_space: cast!(total), | |
107 | available_space: cast!(available), | |
108 | is_removable, | |
109 | }) | |
110 | } | |
111 | } | |
112 | ||
113 | #[allow(clippy::manual_range_contains)] | |
114 | fn find_type_for_device_name(device_name: &OsStr) -> DiskType { | |
115 | // The format of devices are as follows: | |
116 | // - device_name is symbolic link in the case of /dev/mapper/ | |
117 | // and /dev/root, and the target is corresponding device under | |
118 | // /sys/block/ | |
119 | // - In the case of /dev/sd, the format is /dev/sd[a-z][1-9], | |
120 | // corresponding to /sys/block/sd[a-z] | |
121 | // - In the case of /dev/nvme, the format is /dev/nvme[0-9]n[0-9]p[0-9], | |
122 | // corresponding to /sys/block/nvme[0-9]n[0-9] | |
123 | // - In the case of /dev/mmcblk, the format is /dev/mmcblk[0-9]p[0-9], | |
124 | // corresponding to /sys/block/mmcblk[0-9] | |
125 | let device_name_path = device_name.to_str().unwrap_or_default(); | |
126 | let real_path = fs::canonicalize(device_name).unwrap_or_else(|_| PathBuf::from(device_name)); | |
127 | let mut real_path = real_path.to_str().unwrap_or_default(); | |
128 | if device_name_path.starts_with("/dev/mapper/") { | |
129 | // Recursively solve, for example /dev/dm-0 | |
130 | if real_path != device_name_path { | |
131 | return find_type_for_device_name(OsStr::new(&real_path)); | |
132 | } | |
133 | } else if device_name_path.starts_with("/dev/sd") || device_name_path.starts_with("/dev/vd") { | |
134 | // Turn "sda1" into "sda" or "vda1" into "vda" | |
135 | real_path = real_path.trim_start_matches("/dev/"); | |
136 | real_path = real_path.trim_end_matches(|c| c >= '0' && c <= '9'); | |
137 | } else if device_name_path.starts_with("/dev/nvme") { | |
138 | // Turn "nvme0n1p1" into "nvme0n1" | |
487cf647 FG |
139 | real_path = match real_path.find('p') { |
140 | Some(idx) => &real_path["/dev/".len()..idx], | |
141 | None => &real_path["/dev/".len()..], | |
142 | }; | |
923072b8 FG |
143 | } else if device_name_path.starts_with("/dev/root") { |
144 | // Recursively solve, for example /dev/mmcblk0p1 | |
145 | if real_path != device_name_path { | |
146 | return find_type_for_device_name(OsStr::new(&real_path)); | |
147 | } | |
148 | } else if device_name_path.starts_with("/dev/mmcblk") { | |
149 | // Turn "mmcblk0p1" into "mmcblk0" | |
487cf647 FG |
150 | real_path = match real_path.find('p') { |
151 | Some(idx) => &real_path["/dev/".len()..idx], | |
152 | None => &real_path["/dev/".len()..], | |
153 | }; | |
923072b8 FG |
154 | } else { |
155 | // Default case: remove /dev/ and expects the name presents under /sys/block/ | |
156 | // For example, /dev/dm-0 to dm-0 | |
157 | real_path = real_path.trim_start_matches("/dev/"); | |
158 | } | |
159 | ||
160 | let trimmed: &OsStr = OsStrExt::from_bytes(real_path.as_bytes()); | |
161 | ||
162 | let path = Path::new("/sys/block/") | |
163 | .to_owned() | |
164 | .join(trimmed) | |
165 | .join("queue/rotational"); | |
166 | // Normally, this file only contains '0' or '1' but just in case, we get 8 bytes... | |
167 | match get_all_data(path, 8) | |
168 | .unwrap_or_default() | |
169 | .trim() | |
170 | .parse() | |
171 | .ok() | |
172 | { | |
173 | // The disk is marked as rotational so it's a HDD. | |
174 | Some(1) => DiskType::HDD, | |
175 | // The disk is marked as non-rotational so it's very likely a SSD. | |
176 | Some(0) => DiskType::SSD, | |
177 | // Normally it shouldn't happen but welcome to the wonderful world of IT! :D | |
178 | Some(x) => DiskType::Unknown(x), | |
179 | // The information isn't available... | |
180 | None => DiskType::Unknown(-1), | |
181 | } | |
182 | } | |
183 | ||
184 | fn get_all_disks_inner(content: &str) -> Vec<Disk> { | |
185 | // The goal of this array is to list all removable devices (the ones whose name starts with | |
186 | // "usb-"). Then we check if | |
187 | let removable_entries = match fs::read_dir("/dev/disk/by-id/") { | |
188 | Ok(r) => r | |
189 | .filter_map(|res| Some(res.ok()?.path())) | |
190 | .filter_map(|e| { | |
191 | if e.file_name() | |
192 | .and_then(|x| Some(x.to_str()?.starts_with("usb-"))) | |
193 | .unwrap_or_default() | |
194 | { | |
195 | e.canonicalize().ok() | |
196 | } else { | |
197 | None | |
198 | } | |
199 | }) | |
200 | .collect::<Vec<PathBuf>>(), | |
201 | _ => Vec::new(), | |
202 | }; | |
203 | ||
204 | content | |
205 | .lines() | |
206 | .map(|line| { | |
207 | let line = line.trim_start(); | |
208 | // mounts format | |
209 | // http://man7.org/linux/man-pages/man5/fstab.5.html | |
210 | // fs_spec<tab>fs_file<tab>fs_vfstype<tab>other fields | |
211 | let mut fields = line.split_whitespace(); | |
212 | let fs_spec = fields.next().unwrap_or(""); | |
213 | let fs_file = fields | |
214 | .next() | |
215 | .unwrap_or("") | |
216 | .replace("\\134", "\\") | |
217 | .replace("\\040", " ") | |
218 | .replace("\\011", "\t") | |
219 | .replace("\\012", "\n"); | |
220 | let fs_vfstype = fields.next().unwrap_or(""); | |
221 | (fs_spec, fs_file, fs_vfstype) | |
222 | }) | |
223 | .filter(|(fs_spec, fs_file, fs_vfstype)| { | |
224 | // Check if fs_vfstype is one of our 'ignored' file systems. | |
225 | let filtered = matches!( | |
226 | *fs_vfstype, | |
227 | "rootfs" | // https://www.kernel.org/doc/Documentation/filesystems/ramfs-rootfs-initramfs.txt | |
228 | "sysfs" | // pseudo file system for kernel objects | |
229 | "proc" | // another pseudo file system | |
230 | "tmpfs" | | |
231 | "devtmpfs" | | |
232 | "cgroup" | | |
233 | "cgroup2" | | |
234 | "pstore" | // https://www.kernel.org/doc/Documentation/ABI/testing/pstore | |
235 | "squashfs" | // squashfs is a compressed read-only file system (for snaps) | |
236 | "rpc_pipefs" | // The pipefs pseudo file system service | |
237 | "iso9660" // optical media | |
238 | ); | |
239 | ||
240 | !(filtered || | |
241 | fs_file.starts_with("/sys") || // check if fs_file is an 'ignored' mount point | |
242 | fs_file.starts_with("/proc") || | |
243 | (fs_file.starts_with("/run") && !fs_file.starts_with("/run/media")) || | |
244 | fs_spec.starts_with("sunrpc")) | |
245 | }) | |
246 | .filter_map(|(fs_spec, fs_file, fs_vfstype)| { | |
247 | new_disk( | |
248 | fs_spec.as_ref(), | |
249 | Path::new(&fs_file), | |
250 | fs_vfstype.as_bytes(), | |
251 | &removable_entries, | |
252 | ) | |
253 | }) | |
254 | .collect() | |
255 | } | |
256 | ||
257 | pub(crate) fn get_all_disks() -> Vec<Disk> { | |
258 | get_all_disks_inner(&get_all_data("/proc/mounts", 16_385).unwrap_or_default()) | |
259 | } | |
260 | ||
261 | // #[test] | |
262 | // fn check_all_disks() { | |
263 | // let disks = get_all_disks_inner( | |
264 | // r#"tmpfs /proc tmpfs rw,seclabel,relatime 0 0 | |
265 | // proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0 | |
266 | // systemd-1 /proc/sys/fs/binfmt_misc autofs rw,relatime,fd=29,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=17771 0 0 | |
267 | // tmpfs /sys tmpfs rw,seclabel,relatime 0 0 | |
268 | // sysfs /sys sysfs rw,seclabel,nosuid,nodev,noexec,relatime 0 0 | |
269 | // securityfs /sys/kernel/security securityfs rw,nosuid,nodev,noexec,relatime 0 0 | |
270 | // cgroup2 /sys/fs/cgroup cgroup2 rw,seclabel,nosuid,nodev,noexec,relatime,nsdelegate 0 0 | |
271 | // pstore /sys/fs/pstore pstore rw,seclabel,nosuid,nodev,noexec,relatime 0 0 | |
272 | // none /sys/fs/bpf bpf rw,nosuid,nodev,noexec,relatime,mode=700 0 0 | |
273 | // configfs /sys/kernel/config configfs rw,nosuid,nodev,noexec,relatime 0 0 | |
274 | // selinuxfs /sys/fs/selinux selinuxfs rw,relatime 0 0 | |
275 | // debugfs /sys/kernel/debug debugfs rw,seclabel,nosuid,nodev,noexec,relatime 0 0 | |
276 | // tmpfs /dev/shm tmpfs rw,seclabel,relatime 0 0 | |
277 | // devpts /dev/pts devpts rw,seclabel,relatime,gid=5,mode=620,ptmxmode=666 0 0 | |
278 | // tmpfs /sys/fs/selinux tmpfs rw,seclabel,relatime 0 0 | |
279 | // /dev/vda2 /proc/filesystems xfs rw,seclabel,relatime,attr2,inode64,logbufs=8,logbsize=32k,noquota 0 0 | |
280 | // "#, | |
281 | // ); | |
282 | // assert_eq!(disks.len(), 1); | |
283 | // assert_eq!( | |
284 | // disks[0], | |
285 | // Disk { | |
286 | // type_: DiskType::Unknown(-1), | |
287 | // name: OsString::from("devpts"), | |
288 | // file_system: vec![100, 101, 118, 112, 116, 115], | |
289 | // mount_point: PathBuf::from("/dev/pts"), | |
290 | // total_space: 0, | |
291 | // available_space: 0, | |
292 | // } | |
293 | // ); | |
294 | // } |