Skip to main content

btrfs_uapi/
defrag.rs

1//! # File defragmentation: rewriting fragmented extents into contiguous runs
2//!
3//! Defragmenting a file rewrites its extents contiguously on disk, which can
4//! improve sequential read performance.  Optionally applies or removes
5//! transparent compression at the same time.
6
7use crate::raw::{
8    BTRFS_DEFRAG_RANGE_COMPRESS, BTRFS_DEFRAG_RANGE_COMPRESS_LEVEL,
9    BTRFS_DEFRAG_RANGE_NOCOMPRESS, BTRFS_DEFRAG_RANGE_START_IO,
10    btrfs_ioc_defrag_range, btrfs_ioctl_defrag_range_args,
11};
12use std::{
13    mem,
14    os::{fd::AsRawFd, unix::io::BorrowedFd},
15};
16
17/// Compression algorithm to use when defragmenting.
18///
19/// Corresponds to the `BTRFS_COMPRESS_*` values from `compression.h`.
20/// The numeric values are part of the on-disk/ioctl ABI.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum CompressType {
23    Zlib = 1,
24    Lzo = 2,
25    Zstd = 3,
26}
27
28impl std::fmt::Display for CompressType {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        match self {
31            Self::Zlib => f.write_str("zlib"),
32            Self::Lzo => f.write_str("lzo"),
33            Self::Zstd => f.write_str("zstd"),
34        }
35    }
36}
37
38impl std::str::FromStr for CompressType {
39    type Err = String;
40
41    fn from_str(s: &str) -> Result<Self, Self::Err> {
42        match s.to_ascii_lowercase().as_str() {
43            "zlib" => Ok(Self::Zlib),
44            "lzo" => Ok(Self::Lzo),
45            "zstd" => Ok(Self::Zstd),
46            _ => Err(format!(
47                "unknown compress type '{s}'; expected zlib, lzo, or zstd"
48            )),
49        }
50    }
51}
52
53/// Arguments for a defragmentation operation.
54///
55/// Construct with [`DefragRangeArgs::new`] and use the builder methods to set
56/// options. All options are optional; the defaults match the kernel's defaults.
57#[derive(Debug, Clone)]
58pub struct DefragRangeArgs {
59    /// Start offset in bytes. Defaults to `0`.
60    pub start: u64,
61    /// Number of bytes to defragment. Defaults to `u64::MAX` (the entire file).
62    pub len: u64,
63    /// Flush dirty pages to disk immediately after defragmenting.
64    pub flush: bool,
65    /// Extents larger than this threshold are considered already defragmented
66    /// and will not be rewritten. `0` uses the kernel default (32 MiB as of
67    /// recent kernels). `1` forces every extent to be rewritten.
68    pub extent_thresh: u32,
69    /// Compress the file while defragmenting. `None` leaves the file's
70    /// existing compression attribute unchanged.
71    pub compress: Option<CompressSpec>,
72    /// Explicitly disable compression during defragmentation (uncompress if
73    /// necessary). Mutually exclusive with `compress`.
74    pub nocomp: bool,
75}
76
77/// Compression specification for [`DefragRangeArgs`].
78#[derive(Debug, Clone, Copy)]
79pub struct CompressSpec {
80    /// Compression algorithm to use.
81    pub compress_type: CompressType,
82    /// Optional compression level. When `None`, the kernel default for the
83    /// chosen algorithm is used. When `Some`, the
84    /// `BTRFS_DEFRAG_RANGE_COMPRESS_LEVEL` flag is set and the level is
85    /// passed via the `compress.level` union member.
86    pub level: Option<i8>,
87}
88
89impl DefragRangeArgs {
90    /// Create a new `DefragRangeArgs` with all defaults: defragment the
91    /// entire file, no compression change, no flush.
92    #[must_use]
93    pub fn new() -> Self {
94        Self {
95            start: 0,
96            len: u64::MAX,
97            flush: false,
98            extent_thresh: 0,
99            compress: None,
100            nocomp: false,
101        }
102    }
103
104    /// Set the start offset in bytes.
105    #[must_use]
106    pub fn start(mut self, start: u64) -> Self {
107        self.start = start;
108        self
109    }
110
111    /// Set the number of bytes to defragment.
112    #[must_use]
113    pub fn len(mut self, len: u64) -> Self {
114        self.len = len;
115        self
116    }
117
118    /// Flush dirty data to disk after defragmenting.
119    #[must_use]
120    pub fn flush(mut self) -> Self {
121        self.flush = true;
122        self
123    }
124
125    /// Set the extent size threshold. Extents larger than this will not be
126    /// rewritten.
127    #[must_use]
128    pub fn extent_thresh(mut self, thresh: u32) -> Self {
129        self.extent_thresh = thresh;
130        self
131    }
132
133    /// Compress the file using the given algorithm while defragmenting.
134    #[must_use]
135    pub fn compress(mut self, spec: CompressSpec) -> Self {
136        self.compress = Some(spec);
137        self.nocomp = false;
138        self
139    }
140
141    /// Disable compression while defragmenting (decompresses existing data).
142    #[must_use]
143    pub fn nocomp(mut self) -> Self {
144        self.nocomp = true;
145        self.compress = None;
146        self
147    }
148}
149
150impl Default for DefragRangeArgs {
151    fn default() -> Self {
152        Self::new()
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    // --- CompressType Display ---
161
162    #[test]
163    fn compress_type_display() {
164        assert_eq!(format!("{}", CompressType::Zlib), "zlib");
165        assert_eq!(format!("{}", CompressType::Lzo), "lzo");
166        assert_eq!(format!("{}", CompressType::Zstd), "zstd");
167    }
168
169    // --- CompressType FromStr ---
170
171    #[test]
172    fn compress_type_from_str() {
173        assert_eq!("zlib".parse::<CompressType>().unwrap(), CompressType::Zlib);
174        assert_eq!("lzo".parse::<CompressType>().unwrap(), CompressType::Lzo);
175        assert_eq!("zstd".parse::<CompressType>().unwrap(), CompressType::Zstd);
176    }
177
178    #[test]
179    fn compress_type_from_str_case_insensitive() {
180        assert_eq!("ZLIB".parse::<CompressType>().unwrap(), CompressType::Zlib);
181        assert_eq!("Zstd".parse::<CompressType>().unwrap(), CompressType::Zstd);
182    }
183
184    #[test]
185    fn compress_type_from_str_invalid() {
186        assert!("lz4".parse::<CompressType>().is_err());
187        assert!("".parse::<CompressType>().is_err());
188    }
189
190    // --- DefragRangeArgs builder ---
191
192    #[test]
193    fn defrag_args_defaults() {
194        let args = DefragRangeArgs::new();
195        assert_eq!(args.start, 0);
196        assert_eq!(args.len, u64::MAX);
197        assert!(!args.flush);
198        assert_eq!(args.extent_thresh, 0);
199        assert!(args.compress.is_none());
200        assert!(!args.nocomp);
201    }
202
203    #[test]
204    fn defrag_args_builder_chain() {
205        let args = DefragRangeArgs::new()
206            .start(4096)
207            .len(1024 * 1024)
208            .flush()
209            .extent_thresh(256 * 1024);
210        assert_eq!(args.start, 4096);
211        assert_eq!(args.len, 1024 * 1024);
212        assert!(args.flush);
213        assert_eq!(args.extent_thresh, 256 * 1024);
214    }
215
216    #[test]
217    fn defrag_args_compress_clears_nocomp() {
218        let args = DefragRangeArgs::new().nocomp().compress(CompressSpec {
219            compress_type: CompressType::Zstd,
220            level: None,
221        });
222        assert!(args.compress.is_some());
223        assert!(!args.nocomp);
224    }
225
226    #[test]
227    fn defrag_args_nocomp_clears_compress() {
228        let args = DefragRangeArgs::new()
229            .compress(CompressSpec {
230                compress_type: CompressType::Zlib,
231                level: Some(3),
232            })
233            .nocomp();
234        assert!(args.compress.is_none());
235        assert!(args.nocomp);
236    }
237
238    #[test]
239    fn defrag_args_default_trait() {
240        let a = DefragRangeArgs::default();
241        let b = DefragRangeArgs::new();
242        assert_eq!(a.start, b.start);
243        assert_eq!(a.len, b.len);
244    }
245}
246
247/// Defragment a byte range of the file referred to by `fd`.
248///
249/// `fd` must be an open file descriptor to a regular file on a btrfs
250/// filesystem. Pass `&DefragRangeArgs::new()` to defragment the entire file
251/// with default settings.
252pub fn defrag_range(fd: BorrowedFd, args: &DefragRangeArgs) -> nix::Result<()> {
253    let mut raw: btrfs_ioctl_defrag_range_args = unsafe { mem::zeroed() };
254
255    raw.start = args.start;
256    raw.len = args.len;
257    raw.extent_thresh = args.extent_thresh;
258
259    if args.flush {
260        raw.flags |= u64::from(BTRFS_DEFRAG_RANGE_START_IO);
261    }
262
263    if args.nocomp {
264        raw.flags |= u64::from(BTRFS_DEFRAG_RANGE_NOCOMPRESS);
265    } else if let Some(spec) = args.compress {
266        raw.flags |= u64::from(BTRFS_DEFRAG_RANGE_COMPRESS);
267        match spec.level {
268            None => {
269                raw.__bindgen_anon_1.compress_type = spec.compress_type as u32;
270            }
271            Some(level) => {
272                raw.flags |= u64::from(BTRFS_DEFRAG_RANGE_COMPRESS_LEVEL);
273                raw.__bindgen_anon_1.compress.type_ = spec.compress_type as u8;
274                raw.__bindgen_anon_1.compress.level = level;
275            }
276        }
277    }
278
279    unsafe { btrfs_ioc_defrag_range(fd.as_raw_fd(), &raw const raw) }?;
280    Ok(())
281}