linuxutils_system/
fstrim.rs1use linuxutils_common::man::ManContent;
2
3pub const MAN: ManContent = ManContent::empty();
4
5use clap::Parser;
6use std::{
7 fs::File,
8 io::{self, BufRead},
9 os::unix::io::AsRawFd,
10 process::ExitCode,
11};
12
13const FITRIM: libc::c_ulong = 0xc0185879;
14
15#[repr(C)]
16struct FstrimRange {
17 start: u64,
18 len: u64,
19 minlen: u64,
20}
21
22#[derive(Parser)]
23#[command(
24 name = "fstrim",
25 about = "Discard unused blocks on a mounted filesystem"
26)]
27pub struct Args {
28 #[arg(short = 'a', long)]
30 all: bool,
31
32 #[arg(short = 'A', long)]
34 fstab: bool,
35
36 #[arg(short = 'o', long, default_value = "0")]
38 offset: u64,
39
40 #[arg(short = 'l', long)]
42 length: Option<u64>,
43
44 #[arg(short = 'm', long, default_value = "0")]
46 minimum: u64,
47
48 #[arg(short = 't', long)]
50 types: Option<String>,
51
52 #[arg(short = 'v', long)]
54 verbose: bool,
55
56 #[arg(short = 'n', long)]
58 dry_run: bool,
59
60 mountpoint: Option<String>,
62}
63
64fn do_fstrim(
65 path: &str,
66 offset: u64,
67 length: u64,
68 minlen: u64,
69 dry_run: bool,
70) -> io::Result<u64> {
71 let f = File::open(path)?;
72 let mut range = FstrimRange {
73 start: offset,
74 len: length,
75 minlen,
76 };
77
78 if dry_run {
79 return Ok(0);
80 }
81
82 let ret = unsafe { libc::ioctl(f.as_raw_fd(), FITRIM, &mut range) };
83 if ret < 0 {
84 Err(io::Error::last_os_error())
85 } else {
86 Ok(range.len)
87 }
88}
89
90fn format_bytes(bytes: u64) -> String {
91 const KIB: u64 = 1024;
92 const MIB: u64 = 1024 * KIB;
93 const GIB: u64 = 1024 * MIB;
94 const TIB: u64 = 1024 * GIB;
95
96 if bytes >= TIB {
97 format!("{:.1} TiB", bytes as f64 / TIB as f64)
98 } else if bytes >= GIB {
99 format!("{:.1} GiB", bytes as f64 / GIB as f64)
100 } else if bytes >= MIB {
101 format!("{:.1} MiB", bytes as f64 / MIB as f64)
102 } else if bytes >= KIB {
103 format!("{:.1} KiB", bytes as f64 / KIB as f64)
104 } else {
105 format!("{bytes} B")
106 }
107}
108
109struct MountEntry {
110 mountpoint: String,
111 fstype: String,
112}
113
114fn read_mountinfo() -> io::Result<Vec<MountEntry>> {
115 let file = File::open("/proc/self/mountinfo")?;
116 let mut entries = Vec::new();
117
118 for line in io::BufReader::new(file).lines() {
119 let line = line?;
120 let fields: Vec<&str> = line.split_whitespace().collect();
121 if let Some(sep_idx) = fields.iter().position(|&f| f == "-")
123 && sep_idx + 1 < fields.len()
124 && fields.len() > 4
125 {
126 let mountpoint = fields[4].to_string();
127 let fstype = fields[sep_idx + 1].to_string();
128 entries.push(MountEntry { mountpoint, fstype });
129 }
130 }
131
132 Ok(entries)
133}
134
135fn read_fstab_mountpoints() -> io::Result<Vec<String>> {
136 let file = File::open("/etc/fstab")?;
137 let mut mountpoints = Vec::new();
138
139 for line in io::BufReader::new(file).lines() {
140 let line = line?;
141 let line = line.trim();
142 if line.is_empty() || line.starts_with('#') {
143 continue;
144 }
145 let fields: Vec<&str> = line.split_whitespace().collect();
146 if fields.len() >= 4 {
147 let mountpoint = fields[1];
148 let options = fields[3];
149 if options.contains("X-fstrim.notrim") {
150 continue;
151 }
152 if mountpoint != "none" && mountpoint != "swap" {
153 mountpoints.push(mountpoint.to_string());
154 }
155 }
156 }
157
158 Ok(mountpoints)
159}
160
161fn type_matches(fstype: &str, filter: &Option<String>) -> bool {
162 let Some(filter) = filter else {
163 return fstype != "autofs";
164 };
165
166 for entry in filter.split(',') {
167 if let Some(excluded) = entry.strip_prefix("no") {
168 if fstype == excluded {
169 return false;
170 }
171 } else if fstype != entry {
172 return false;
173 }
174 }
175 true
176}
177
178pub fn run(args: Args) -> ExitCode {
179 let length = args.length.unwrap_or(u64::MAX);
180
181 if args.all || args.fstab {
182 let mounts = match read_mountinfo() {
183 Ok(m) => m,
184 Err(e) => {
185 eprintln!("fstrim: failed to read mountinfo: {e}");
186 return ExitCode::FAILURE;
187 }
188 };
189
190 let targets: Vec<&MountEntry> = if args.fstab {
191 let fstab_mounts = read_fstab_mountpoints().unwrap_or_default();
192 mounts
193 .iter()
194 .filter(|m| {
195 fstab_mounts.contains(&m.mountpoint)
196 && type_matches(&m.fstype, &args.types)
197 })
198 .collect()
199 } else {
200 mounts
201 .iter()
202 .filter(|m| type_matches(&m.fstype, &args.types))
203 .collect()
204 };
205
206 let mut successes = 0;
207 let mut failures = 0;
208
209 for mount in &targets {
210 match do_fstrim(
211 &mount.mountpoint,
212 args.offset,
213 length,
214 args.minimum,
215 args.dry_run,
216 ) {
217 Ok(trimmed) => {
218 successes += 1;
219 if args.verbose {
220 println!(
221 "{}: {} ({trimmed} bytes) trimmed",
222 mount.mountpoint,
223 format_bytes(trimmed)
224 );
225 }
226 }
227 Err(e) => {
228 let errno = e.raw_os_error().unwrap_or(0);
229 if errno == libc::EOPNOTSUPP
231 || errno == libc::EROFS
232 || errno == libc::EACCES
233 {
234 continue;
235 }
236 eprintln!("fstrim: {}: {e}", mount.mountpoint);
237 failures += 1;
238 }
239 }
240 }
241
242 if failures > 0 && successes == 0 {
243 ExitCode::from(32)
244 } else if failures > 0 {
245 ExitCode::from(64)
246 } else {
247 ExitCode::SUCCESS
248 }
249 } else {
250 let mountpoint = match &args.mountpoint {
251 Some(m) => m.as_str(),
252 None => {
253 eprintln!("fstrim: no mountpoint specified");
254 return ExitCode::FAILURE;
255 }
256 };
257
258 match do_fstrim(
259 mountpoint,
260 args.offset,
261 length,
262 args.minimum,
263 args.dry_run,
264 ) {
265 Ok(trimmed) => {
266 if args.verbose {
267 println!(
268 "{mountpoint}: {} ({trimmed} bytes) trimmed",
269 format_bytes(trimmed)
270 );
271 }
272 ExitCode::SUCCESS
273 }
274 Err(e) => {
275 eprintln!("fstrim: {mountpoint}: {e}");
276 ExitCode::FAILURE
277 }
278 }
279 }
280}