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::{fs::File, os::unix::io::AsFd, path::PathBuf};
8
9const HEADING_COMPRESSION: &str = "Compression";
10const HEADING_RANGE: &str = "Range";
11
12/// Defragment files or directories on a btrfs filesystem
13#[derive(Parser, Debug)]
14pub struct FilesystemDefragCommand {
15    /// Be verbose, print file names as they are defragmented
16    #[clap(long, short)]
17    pub verbose: bool,
18
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 = std::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 {
97                if let Err(e) = self.defrag_one(path, &args) {
98                    eprintln!("error: {e:#}");
99                    errors += 1;
100                }
101            }
102        }
103
104        if errors > 0 {
105            anyhow::bail!("{errors} error(s) during defragmentation");
106        }
107
108        Ok(())
109    }
110}
111
112impl FilesystemDefragCommand {
113    /// Defragment a single file.
114    fn defrag_one(
115        &self,
116        path: &std::path::Path,
117        args: &DefragRangeArgs,
118    ) -> Result<()> {
119        if self.verbose {
120            println!("{}", path.display());
121        }
122        let file = File::open(path)
123            .with_context(|| format!("failed to open '{}'", path.display()))?;
124
125        if let Some(step) = self.step {
126            self.defrag_in_steps(&file, path, args, step)?;
127        } else {
128            defrag_range(file.as_fd(), args).with_context(|| {
129                format!("defrag failed on '{}'", path.display())
130            })?;
131        }
132        Ok(())
133    }
134
135    /// Walk a directory tree and defragment every regular file.
136    ///
137    /// Does not follow symlinks and does not cross filesystem boundaries,
138    /// matching the C reference's `nftw(path, cb, 10, FTW_MOUNT | FTW_PHYS)`.
139    fn defrag_recursive(
140        &self,
141        dir: &std::path::Path,
142        args: &DefragRangeArgs,
143    ) -> Result<u64> {
144        use std::os::unix::fs::MetadataExt;
145
146        let dir_dev = std::fs::metadata(dir)
147            .with_context(|| format!("cannot stat '{}'", dir.display()))?
148            .dev();
149
150        let mut errors = 0u64;
151        let mut stack = vec![dir.to_path_buf()];
152
153        while let Some(current) = stack.pop() {
154            let entries = match std::fs::read_dir(&current) {
155                Ok(e) => e,
156                Err(e) => {
157                    eprintln!(
158                        "error: cannot read '{}': {e}",
159                        current.display()
160                    );
161                    errors += 1;
162                    continue;
163                }
164            };
165
166            for entry in entries {
167                let entry = match entry {
168                    Ok(e) => e,
169                    Err(e) => {
170                        eprintln!("error: directory entry read failed: {e}");
171                        errors += 1;
172                        continue;
173                    }
174                };
175
176                let path = entry.path();
177
178                // Use symlink_metadata to avoid following symlinks (FTW_PHYS).
179                let meta = match std::fs::symlink_metadata(&path) {
180                    Ok(m) => m,
181                    Err(e) => {
182                        eprintln!(
183                            "error: cannot stat '{}': {e}",
184                            path.display()
185                        );
186                        errors += 1;
187                        continue;
188                    }
189                };
190
191                if meta.is_dir() {
192                    // Don't cross filesystem boundaries (FTW_MOUNT).
193                    if meta.dev() == dir_dev {
194                        stack.push(path);
195                    }
196                } else if meta.is_file() {
197                    if let Err(e) = self.defrag_one(&path, args) {
198                        eprintln!("error: {e:#}");
199                        errors += 1;
200                    }
201                }
202                // Skip symlinks, sockets, fifos, etc.
203            }
204        }
205
206        Ok(errors)
207    }
208
209    /// Process a file in fixed-size steps, flushing between each step.
210    ///
211    /// Matches `defrag_range_in_steps` from the C reference.
212    fn defrag_in_steps(
213        &self,
214        file: &File,
215        path: &std::path::Path,
216        args: &DefragRangeArgs,
217        step: u64,
218    ) -> Result<()> {
219        use std::os::unix::fs::MetadataExt;
220
221        let file_size = file.metadata()?.size();
222        let mut offset = args.start;
223        let end = if args.len == u64::MAX {
224            u64::MAX
225        } else {
226            args.start.saturating_add(args.len)
227        };
228
229        while offset < end {
230            // Re-check file size each iteration in case it changed.
231            let current_size = file.metadata()?.size();
232            if offset >= current_size {
233                break;
234            }
235
236            let remaining = end.saturating_sub(offset).min(step);
237            let mut step_args = args.clone();
238            step_args.start = offset;
239            step_args.len = remaining;
240            // Always flush between steps.
241            step_args.flush = true;
242
243            defrag_range(file.as_fd(), &step_args).with_context(|| {
244                format!(
245                    "defrag failed on '{}' at offset {offset}",
246                    path.display()
247                )
248            })?;
249
250            offset = match offset.checked_add(step) {
251                Some(next) => next,
252                None => break, // overflow means we've covered the whole file
253            };
254        }
255
256        // If the file grew since we started, the original file_size might be
257        // less than the current size, but we only defrag through `end`.
258        let _ = file_size;
259
260        Ok(())
261    }
262}