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 if let Err(e) = self.defrag_one(path, &args) {
97 eprintln!("error: {e:#}");
98 errors += 1;
99 }
100 }
101
102 if errors > 0 {
103 anyhow::bail!("{errors} error(s) during defragmentation");
104 }
105
106 Ok(())
107 }
108}
109
110impl FilesystemDefragCommand {
111 fn defrag_one(
113 &self,
114 path: &std::path::Path,
115 args: &DefragRangeArgs,
116 ) -> Result<()> {
117 log::info!("{}", path.display());
118 let file = File::open(path)
119 .with_context(|| format!("failed to open '{}'", path.display()))?;
120
121 if let Some(step) = self.step {
122 self.defrag_in_steps(&file, path, args, step)?;
123 } else {
124 defrag_range(file.as_fd(), args).with_context(|| {
125 format!("defrag failed on '{}'", path.display())
126 })?;
127 }
128 Ok(())
129 }
130
131 fn defrag_recursive(
136 &self,
137 dir: &std::path::Path,
138 args: &DefragRangeArgs,
139 ) -> Result<u64> {
140 use std::os::unix::fs::MetadataExt;
141
142 let dir_dev = fs::metadata(dir)
143 .with_context(|| format!("cannot stat '{}'", dir.display()))?
144 .dev();
145
146 let mut errors = 0u64;
147 let mut stack = vec![dir.to_path_buf()];
148
149 while let Some(current) = stack.pop() {
150 let entries = match fs::read_dir(¤t) {
151 Ok(e) => e,
152 Err(e) => {
153 eprintln!(
154 "error: cannot read '{}': {e}",
155 current.display()
156 );
157 errors += 1;
158 continue;
159 }
160 };
161
162 for entry in entries {
163 let entry = match entry {
164 Ok(e) => e,
165 Err(e) => {
166 eprintln!("error: directory entry read failed: {e}");
167 errors += 1;
168 continue;
169 }
170 };
171
172 let path = entry.path();
173
174 let meta = match fs::symlink_metadata(&path) {
176 Ok(m) => m,
177 Err(e) => {
178 eprintln!(
179 "error: cannot stat '{}': {e}",
180 path.display()
181 );
182 errors += 1;
183 continue;
184 }
185 };
186
187 if meta.is_dir() {
188 if meta.dev() == dir_dev {
190 stack.push(path);
191 }
192 } else if meta.is_file()
193 && let Err(e) = self.defrag_one(&path, args)
194 {
195 eprintln!("error: {e:#}");
196 errors += 1;
197 }
198 }
200 }
201
202 Ok(errors)
203 }
204
205 fn defrag_in_steps(
209 &self,
210 file: &File,
211 path: &std::path::Path,
212 args: &DefragRangeArgs,
213 step: u64,
214 ) -> Result<()> {
215 use std::os::unix::fs::MetadataExt;
216
217 let file_size = file.metadata()?.size();
218 let mut offset = args.start;
219 let end = if args.len == u64::MAX {
220 u64::MAX
221 } else {
222 args.start.saturating_add(args.len)
223 };
224
225 while offset < end {
226 let current_size = file.metadata()?.size();
228 if offset >= current_size {
229 break;
230 }
231
232 let remaining = end.saturating_sub(offset).min(step);
233 let mut step_args = args.clone();
234 step_args.start = offset;
235 step_args.len = remaining;
236 step_args.flush = true;
238
239 defrag_range(file.as_fd(), &step_args).with_context(|| {
240 format!(
241 "defrag failed on '{}' at offset {offset}",
242 path.display()
243 )
244 })?;
245
246 offset = match offset.checked_add(step) {
247 Some(next) => next,
248 None => break, };
250 }
251
252 let _ = file_size;
255
256 Ok(())
257 }
258}