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