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