btrfs_cli/filesystem/
defrag.rs1use 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#[derive(Parser, Debug)]
14pub struct FilesystemDefragCommand {
15 #[clap(long, short)]
17 pub verbose: bool,
18
19 #[clap(long, short)]
21 pub recursive: bool,
22
23 #[clap(long, short)]
25 pub flush: bool,
26
27 #[clap(long, short, conflicts_with = "nocomp", help_heading = HEADING_COMPRESSION)]
29 pub compress: Option<Option<CompressType>>,
30
31 #[clap(long = "level", short = 'L', requires = "compress", help_heading = HEADING_COMPRESSION)]
33 pub compress_level: Option<i8>,
34
35 #[clap(long, conflicts_with = "compress", help_heading = HEADING_COMPRESSION)]
37 pub nocomp: bool,
38
39 #[clap(long, short, help_heading = HEADING_RANGE)]
41 pub start: Option<u64>,
42
43 #[clap(long, help_heading = HEADING_RANGE)]
45 pub len: Option<u64>,
46
47 #[clap(long, short, help_heading = HEADING_RANGE)]
50 pub target: Option<u64>,
51
52 #[clap(long, help_heading = HEADING_RANGE)]
54 pub step: Option<u64>,
55
56 #[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 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 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(¤t) {
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 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 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 }
204 }
205
206 Ok(errors)
207 }
208
209 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 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 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, };
254 }
255
256 let _ = file_size;
259
260 Ok(())
261 }
262}