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::{
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#[derive(Parser, Debug)]
18pub struct FilesystemDefragCommand {
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 #[allow(clippy::cast_possible_truncation)]
77 {
79 args = args.extent_thresh(thresh as u32);
80 }
81 }
82 if self.flush {
83 args = args.flush();
84 }
85 if self.nocomp {
86 args = args.nocomp();
87 } else if let Some(spec) = compress {
88 args = args.compress(spec);
89 }
90
91 let mut errors = 0u64;
92
93 for path in &self.paths {
94 let meta = fs::symlink_metadata(path).with_context(|| {
95 format!("cannot access '{}'", path.display())
96 })?;
97
98 if self.recursive && meta.is_dir() {
99 errors += self.defrag_recursive(path, &args)?;
100 } else if let Err(e) = self.defrag_one(path, &args) {
101 eprintln!("error: {e:#}");
102 errors += 1;
103 }
104 }
105
106 if errors > 0 {
107 anyhow::bail!("{errors} error(s) during defragmentation");
108 }
109
110 Ok(())
111 }
112}
113
114impl FilesystemDefragCommand {
115 fn defrag_one(
117 &self,
118 path: &std::path::Path,
119 args: &DefragRangeArgs,
120 ) -> Result<()> {
121 log::info!("{}", path.display());
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 = 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 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 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 && let Err(e) = self.defrag_one(&path, args)
198 {
199 eprintln!("error: {e:#}");
200 errors += 1;
201 }
202 }
204 }
205
206 Ok(errors)
207 }
208
209 #[allow(clippy::unused_self)] fn defrag_in_steps(
214 &self,
215 file: &File,
216 path: &std::path::Path,
217 args: &DefragRangeArgs,
218 step: u64,
219 ) -> Result<()> {
220 use std::os::unix::fs::MetadataExt;
221
222 let file_size = file.metadata()?.size();
223 let mut offset = args.start;
224 let end = if args.len == u64::MAX {
225 u64::MAX
226 } else {
227 args.start.saturating_add(args.len)
228 };
229
230 while offset < end {
231 let current_size = file.metadata()?.size();
233 if offset >= current_size {
234 break;
235 }
236
237 let remaining = end.saturating_sub(offset).min(step);
238 let mut step_args = args.clone();
239 step_args.start = offset;
240 step_args.len = remaining;
241 step_args.flush = true;
243
244 defrag_range(file.as_fd(), &step_args).with_context(|| {
245 format!(
246 "defrag failed on '{}' at offset {offset}",
247 path.display()
248 )
249 })?;
250
251 offset = match offset.checked_add(step) {
252 Some(next) => next,
253 None => break, };
255 }
256
257 let _ = file_size;
260
261 Ok(())
262 }
263}