circuitpython_deploy/
file_ops.rs1use crate::error::{CpdError, Result};
2use indicatif::{ProgressBar, ProgressStyle};
3use std::fs;
4use std::path::{Path, PathBuf};
5use walkdir::WalkDir;
6
7pub struct FileOperations {
8 verbose: bool,
9}
10
11impl FileOperations {
12 pub fn new(verbose: bool) -> Self {
13 Self { verbose }
14 }
15
16 pub fn copy_file(&self, from: &Path, to: &Path) -> Result<()> {
18 if let Some(parent) = to.parent() {
19 fs::create_dir_all(parent).map_err(|e| {
20 if self.verbose {
21 eprintln!("Failed to create directory {}: {}", parent.display(), e);
22 }
23 CpdError::Io(e)
24 })?;
25 }
26
27 fs::copy(from, to).map_err(|e| {
28 if self.verbose {
29 eprintln!("Failed to copy {} to {}: {}", from.display(), to.display(), e);
30 }
31 CpdError::FileCopyFailed {
32 from: from.display().to_string(),
33 to: to.display().to_string(),
34 }
35 })?;
36
37 if let Ok(metadata) = fs::metadata(from) {
39 if let Ok(modified) = metadata.modified() {
40 let _ = filetime::set_file_mtime(to, filetime::FileTime::from_system_time(modified));
41 }
42 }
43
44 if self.verbose {
45 println!("Copied: {} -> {}", from.display(), to.display());
46 }
47
48 Ok(())
49 }
50
51 pub fn copy_directory_contents(
53 &self,
54 from_dir: &Path,
55 to_dir: &Path,
56 filter: &dyn Fn(&Path) -> bool,
57 dry_run: bool,
58 ) -> Result<CopyResult> {
59 let mut files_to_copy = Vec::new();
60 let mut _total_size = 0u64;
61
62 for entry in WalkDir::new(from_dir)
64 .into_iter()
65 .filter_map(|e| e.ok())
66 .filter(|e| e.file_type().is_file())
67 {
68 let path = entry.path();
69 if filter(path) {
70 if let Ok(metadata) = entry.metadata() {
71 _total_size += metadata.len();
72 }
73 files_to_copy.push(path.to_path_buf());
74 }
75 }
76
77 let progress = if !dry_run && !files_to_copy.is_empty() {
78 let pb = ProgressBar::new(files_to_copy.len() as u64);
79 pb.set_style(
80 ProgressStyle::default_bar()
81 .template("[{elapsed_precise}] {bar:40.cyan/blue} {pos:>7}/{len:7} {msg}")
82 .unwrap()
83 .progress_chars("##-"),
84 );
85 Some(pb)
86 } else {
87 None
88 };
89
90 let mut result = CopyResult {
91 files_copied: 0,
92 files_failed: 0,
93 bytes_copied: 0,
94 failed_files: Vec::new(),
95 };
96
97 for file_path in &files_to_copy {
99 let relative_path = file_path.strip_prefix(from_dir).unwrap();
100 let dest_path = to_dir.join(relative_path);
101
102 if let Some(pb) = &progress {
103 pb.set_message(format!("Copying {}", relative_path.display()));
104 }
105
106 if dry_run {
107 println!("Would copy: {} -> {}", file_path.display(), dest_path.display());
108 result.files_copied += 1;
109 } else {
110 match self.copy_file(file_path, &dest_path) {
111 Ok(_) => {
112 result.files_copied += 1;
113 if let Ok(metadata) = fs::metadata(file_path) {
114 result.bytes_copied += metadata.len();
115 }
116 }
117 Err(e) => {
118 result.files_failed += 1;
119 result.failed_files.push((file_path.clone(), e.to_string()));
120
121 if !e.is_recoverable() {
123 if let Some(pb) = &progress {
124 pb.finish_with_message("Deployment failed");
125 }
126 return Err(e);
127 }
128 }
129 }
130 }
131
132 if let Some(pb) = &progress {
133 pb.inc(1);
134 }
135 }
136
137 if let Some(pb) = &progress {
138 pb.finish_with_message("Deployment completed");
139 }
140
141 Ok(result)
142 }
143
144 pub fn create_backup(&self, source_dir: &Path, backup_dir: &Path) -> Result<()> {
146 if !source_dir.exists() {
147 return Ok(()); }
149
150 fs::create_dir_all(backup_dir).map_err(|_| CpdError::BackupDirectoryCreationFailed {
151 path: backup_dir.display().to_string(),
152 })?;
153
154 let result = self.copy_directory_contents(
155 source_dir,
156 backup_dir,
157 &|_| true, false,
159 )?;
160
161 if self.verbose {
162 println!(
163 "Backup completed: {} files, {} bytes",
164 result.files_copied,
165 format_bytes(result.bytes_copied)
166 );
167 }
168
169 Ok(())
170 }
171
172 #[allow(dead_code)]
174 pub fn clean_destination(&self, source_dir: &Path, dest_dir: &Path, filter: &dyn Fn(&Path) -> bool) -> Result<()> {
175 if !dest_dir.exists() {
176 return Ok(());
177 }
178
179 let mut files_to_remove = Vec::new();
180
181 for entry in WalkDir::new(dest_dir)
182 .into_iter()
183 .filter_map(|e| e.ok())
184 .filter(|e| e.file_type().is_file())
185 {
186 let dest_path = entry.path();
187 let relative_path = dest_path.strip_prefix(dest_dir).unwrap();
188 let source_path = source_dir.join(relative_path);
189
190 if !source_path.exists() || !filter(&source_path) {
192 files_to_remove.push(dest_path.to_path_buf());
193 }
194 }
195
196 for file_path in files_to_remove {
197 if let Err(e) = fs::remove_file(&file_path) {
198 if self.verbose {
199 eprintln!("Failed to remove {}: {}", file_path.display(), e);
200 }
201 } else if self.verbose {
202 println!("Removed: {}", file_path.display());
203 }
204 }
205
206 Ok(())
207 }
208}
209
210#[derive(Debug)]
211pub struct CopyResult {
212 pub files_copied: usize,
213 pub files_failed: usize,
214 pub bytes_copied: u64,
215 pub failed_files: Vec<(PathBuf, String)>,
216}
217
218impl CopyResult {
219 #[allow(dead_code)]
220 pub fn is_success(&self) -> bool {
221 self.files_failed == 0
222 }
223
224 pub fn summary(&self) -> String {
225 if self.files_failed == 0 {
226 format!(
227 "Successfully copied {} files ({})",
228 self.files_copied,
229 format_bytes(self.bytes_copied)
230 )
231 } else {
232 format!(
233 "Copied {} files, {} failed ({})",
234 self.files_copied,
235 self.files_failed,
236 format_bytes(self.bytes_copied)
237 )
238 }
239 }
240}
241
242fn format_bytes(bytes: u64) -> String {
243 const UNITS: &[&str] = &["B", "KB", "MB", "GB"];
244 let mut size = bytes as f64;
245 let mut unit_index = 0;
246
247 while size >= 1024.0 && unit_index < UNITS.len() - 1 {
248 size /= 1024.0;
249 unit_index += 1;
250 }
251
252 if unit_index == 0 {
253 format!("{} {}", bytes, UNITS[unit_index])
254 } else {
255 format!("{:.1} {}", size, UNITS[unit_index])
256 }
257}
258
259extern crate filetime;
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265
266 #[test]
267 fn test_format_bytes() {
268 assert_eq!(format_bytes(0), "0 B");
269 assert_eq!(format_bytes(512), "512 B");
270 assert_eq!(format_bytes(1024), "1.0 KB");
271 assert_eq!(format_bytes(1536), "1.5 KB");
272 assert_eq!(format_bytes(1048576), "1.0 MB");
273 assert_eq!(format_bytes(1073741824), "1.0 GB");
274 }
275}