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