Skip to main content

btrfs_cli/filesystem/
defrag.rs

1use crate::{Format, Runnable};
2use anyhow::{Context, Result};
3use btrfs_uapi::defrag::{
4    CompressSpec, CompressType, DefragRangeArgs, defrag_range,
5};
6use clap::Parser;
7use std::{
8    fs::{self, File},
9    os::unix::io::AsFd,
10    path::PathBuf,
11};
12
13const HEADING_COMPRESSION: &str = "Compression";
14const HEADING_RANGE: &str = "Range";
15
16/// Defragment files or directories on a btrfs filesystem
17#[derive(Parser, Debug)]
18pub struct FilesystemDefragCommand {
19    /// Defragment files in subdirectories recursively
20    #[clap(long, short)]
21    pub recursive: bool,
22
23    /// Flush data to disk immediately after defragmentation
24    #[clap(long, short)]
25    pub flush: bool,
26
27    /// Compress the file while defragmenting (optionally specify type: zlib, lzo, zstd)
28    #[clap(long, short, conflicts_with = "nocomp", help_heading = HEADING_COMPRESSION)]
29    pub compress: Option<Option<CompressType>>,
30
31    /// Compression level (used together with --compress)
32    #[clap(long = "level", short = 'L', requires = "compress", help_heading = HEADING_COMPRESSION)]
33    pub compress_level: Option<i8>,
34
35    /// Disable compression during defragmentation
36    #[clap(long, conflicts_with = "compress", help_heading = HEADING_COMPRESSION)]
37    pub nocomp: bool,
38
39    /// Defragment only bytes starting at this offset
40    #[clap(long, short, help_heading = HEADING_RANGE)]
41    pub start: Option<u64>,
42
43    /// Defragment only this many bytes
44    #[clap(long, help_heading = HEADING_RANGE)]
45    pub len: Option<u64>,
46
47    /// Target extent size threshold in bytes; extents larger than this are
48    /// considered already defragmented
49    #[clap(long, short, help_heading = HEADING_RANGE)]
50    pub target: Option<u64>,
51
52    /// Process the file in steps of this size rather than all at once
53    #[clap(long, help_heading = HEADING_RANGE)]
54    pub step: Option<u64>,
55
56    /// One or more files or directories to defragment
57    #[clap(required = true)]
58    pub paths: Vec<PathBuf>,
59}
60
61impl Runnable for FilesystemDefragCommand {
62    fn run(&self, _format: Format, _dry_run: bool) -> Result<()> {
63        let compress = self.compress.as_ref().map(|ct| CompressSpec {
64            compress_type: ct.unwrap_or(CompressType::Zlib),
65            level: self.compress_level,
66        });
67
68        let mut args = DefragRangeArgs::new();
69        if let Some(start) = self.start {
70            args = args.start(start);
71        }
72        if let Some(len) = self.len {
73            args = args.len(len);
74        }
75        if let Some(thresh) = self.target {
76            args = args.extent_thresh(thresh as u32);
77        }
78        if self.flush {
79            args = args.flush();
80        }
81        if self.nocomp {
82            args = args.nocomp();
83        } else if let Some(spec) = compress {
84            args = args.compress(spec);
85        }
86
87        let mut errors = 0u64;
88
89        for path in &self.paths {
90            let meta = fs::symlink_metadata(path).with_context(|| {
91                format!("cannot access '{}'", path.display())
92            })?;
93
94            if self.recursive && meta.is_dir() {
95                errors += self.defrag_recursive(path, &args)?;
96            } else if let Err(e) = self.defrag_one(path, &args) {
97                eprintln!("error: {e:#}");
98                errors += 1;
99            }
100        }
101
102        if errors > 0 {
103            anyhow::bail!("{errors} error(s) during defragmentation");
104        }
105
106        Ok(())
107    }
108}
109
110impl FilesystemDefragCommand {
111    /// Defragment a single file.
112    fn defrag_one(
113        &self,
114        path: &std::path::Path,
115        args: &DefragRangeArgs,
116    ) -> Result<()> {
117        log::info!("{}", path.display());
118        let file = File::open(path)
119            .with_context(|| format!("failed to open '{}'", path.display()))?;
120
121        if let Some(step) = self.step {
122            self.defrag_in_steps(&file, path, args, step)?;
123        } else {
124            defrag_range(file.as_fd(), args).with_context(|| {
125                format!("defrag failed on '{}'", path.display())
126            })?;
127        }
128        Ok(())
129    }
130
131    /// Walk a directory tree and defragment every regular file.
132    ///
133    /// Does not follow symlinks and does not cross filesystem boundaries,
134    /// matching the C reference's `nftw(path, cb, 10, FTW_MOUNT | FTW_PHYS)`.
135    fn defrag_recursive(
136        &self,
137        dir: &std::path::Path,
138        args: &DefragRangeArgs,
139    ) -> Result<u64> {
140        use std::os::unix::fs::MetadataExt;
141
142        let dir_dev = fs::metadata(dir)
143            .with_context(|| format!("cannot stat '{}'", dir.display()))?
144            .dev();
145
146        let mut errors = 0u64;
147        let mut stack = vec![dir.to_path_buf()];
148
149        while let Some(current) = stack.pop() {
150            let entries = match fs::read_dir(&current) {
151                Ok(e) => e,
152                Err(e) => {
153                    eprintln!(
154                        "error: cannot read '{}': {e}",
155                        current.display()
156                    );
157                    errors += 1;
158                    continue;
159                }
160            };
161
162            for entry in entries {
163                let entry = match entry {
164                    Ok(e) => e,
165                    Err(e) => {
166                        eprintln!("error: directory entry read failed: {e}");
167                        errors += 1;
168                        continue;
169                    }
170                };
171
172                let path = entry.path();
173
174                // Use symlink_metadata to avoid following symlinks (FTW_PHYS).
175                let meta = match fs::symlink_metadata(&path) {
176                    Ok(m) => m,
177                    Err(e) => {
178                        eprintln!(
179                            "error: cannot stat '{}': {e}",
180                            path.display()
181                        );
182                        errors += 1;
183                        continue;
184                    }
185                };
186
187                if meta.is_dir() {
188                    // Don't cross filesystem boundaries (FTW_MOUNT).
189                    if meta.dev() == dir_dev {
190                        stack.push(path);
191                    }
192                } else if meta.is_file()
193                    && let Err(e) = self.defrag_one(&path, args)
194                {
195                    eprintln!("error: {e:#}");
196                    errors += 1;
197                }
198                // Skip symlinks, sockets, fifos, etc.
199            }
200        }
201
202        Ok(errors)
203    }
204
205    /// Process a file in fixed-size steps, flushing between each step.
206    ///
207    /// Matches `defrag_range_in_steps` from the C reference.
208    fn defrag_in_steps(
209        &self,
210        file: &File,
211        path: &std::path::Path,
212        args: &DefragRangeArgs,
213        step: u64,
214    ) -> Result<()> {
215        use std::os::unix::fs::MetadataExt;
216
217        let file_size = file.metadata()?.size();
218        let mut offset = args.start;
219        let end = if args.len == u64::MAX {
220            u64::MAX
221        } else {
222            args.start.saturating_add(args.len)
223        };
224
225        while offset < end {
226            // Re-check file size each iteration in case it changed.
227            let current_size = file.metadata()?.size();
228            if offset >= current_size {
229                break;
230            }
231
232            let remaining = end.saturating_sub(offset).min(step);
233            let mut step_args = args.clone();
234            step_args.start = offset;
235            step_args.len = remaining;
236            // Always flush between steps.
237            step_args.flush = true;
238
239            defrag_range(file.as_fd(), &step_args).with_context(|| {
240                format!(
241                    "defrag failed on '{}' at offset {offset}",
242                    path.display()
243                )
244            })?;
245
246            offset = match offset.checked_add(step) {
247                Some(next) => next,
248                None => break, // overflow means we've covered the whole file
249            };
250        }
251
252        // If the file grew since we started, the original file_size might be
253        // less than the current size, but we only defrag through `end`.
254        let _ = file_size;
255
256        Ok(())
257    }
258}