Skip to main content

linuxutils_system/
fstrim.rs

1use 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    /// Trim all mounted filesystems that support discard
29    #[arg(short = 'a', long)]
30    all: bool,
31
32    /// Trim all mounted filesystems mentioned in /etc/fstab
33    #[arg(short = 'A', long)]
34    fstab: bool,
35
36    /// Byte offset to start trimming from
37    #[arg(short = 'o', long, default_value = "0")]
38    offset: u64,
39
40    /// Number of bytes to trim
41    #[arg(short = 'l', long)]
42    length: Option<u64>,
43
44    /// Minimum contiguous free range to discard
45    #[arg(short = 'm', long, default_value = "0")]
46    minimum: u64,
47
48    /// Comma-separated filesystem type filter
49    #[arg(short = 't', long)]
50    types: Option<String>,
51
52    /// Print number of discarded bytes
53    #[arg(short = 'v', long)]
54    verbose: bool,
55
56    /// Do everything except the actual FITRIM ioctl
57    #[arg(short = 'n', long)]
58    dry_run: bool,
59
60    /// Mountpoint to trim (not used with --all or --fstab)
61    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        // Format: id parent major:minor root mountpoint options ... - fstype source super_options
122        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                    // Silently skip unsupported/read-only (EOPNOTSUPP, EROFS, EACCES)
230                    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}