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 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 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 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(¤t) {
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 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 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 }
202 }
203
204 Ok(errors)
205 }
206
207 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 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 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, };
252 }
253
254 let _ = file_size;
257
258 Ok(())
259 }
260}