This commit is contained in:
pika 2025-03-26 17:58:43 +01:00
parent 133aac1aac
commit 7c011a30d0
5 changed files with 290 additions and 106 deletions

4
.gitignore vendored
View file

@ -1 +1,3 @@
target /target
**/*.rs.bk
Cargo.lock

21
Cargo.lock generated
View file

@ -279,7 +279,6 @@ dependencies = [
"filetime", "filetime",
"glob", "glob",
"indicatif", "indicatif",
"thiserror",
"walkdir", "walkdir",
] ]
@ -318,26 +317,6 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.18" version = "1.0.18"

View file

@ -7,11 +7,10 @@ license = "MIT"
authors = ["Alexander Pieck <admin@team-pieck.de>"] authors = ["Alexander Pieck <admin@team-pieck.de>"]
[dependencies] [dependencies]
indicatif = "0.17.11" # Removed non-existent feature anyhow = "1.0.75"
anyhow = "1.0" clap = { version = "4.4.6", features = ["derive"] }
walkdir = "2.4" colored = "2.0.4"
glob = "0.3" filetime = "0.2.22"
clap = { version = "4.4", features = ["derive"] } glob = "0.3.1"
colored = "2.1" indicatif = "0.17.7"
filetime = "0.2" # For preserving timestamps walkdir = "2.4.0"
thiserror = "1.0" # For better error handling

0
readme.md Normal file
View file

View file

@ -1,10 +1,11 @@
use std::fs; use std::fs;
use std::io::{Read, Write}; use std::io::{Read, Write, stdout, Stdout};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::collections::VecDeque; use std::collections::VecDeque;
use std::time::{Instant, Duration};
use colored::Colorize; use colored::Colorize;
use indicatif::{ProgressBar, ProgressStyle}; use indicatif::{ProgressBar, ProgressStyle};
use anyhow::Result; use anyhow::{Result, Context};
use walkdir::WalkDir; use walkdir::WalkDir;
use glob::glob; use glob::glob;
use clap::Parser; use clap::Parser;
@ -34,56 +35,183 @@ struct Args {
preserve: bool, preserve: bool,
} }
struct DisplayManager {
history: VecDeque<String>,
max_history: usize,
current_file: String,
stdout: Stdout,
}
impl DisplayManager {
fn new(max_history: usize) -> Self {
// Initialize terminal
print!("\x1B[?25l"); // Hide cursor
println!(); // Initial newline for the display area
Self {
history: VecDeque::with_capacity(max_history),
max_history,
current_file: String::new(),
stdout: stdout(),
}
}
fn set_current_file(&mut self, file: &str) {
self.current_file = format!("{}: {}", "Copying".bold().yellow(), file);
self.update_display();
}
fn add_completed_file(&mut self, file: &str) {
if self.history.len() >= self.max_history {
self.history.pop_front();
}
self.history.push_back(format!("{}: {}", "Copied".green(), file));
self.update_display();
}
fn add_error(&mut self, file: &str, error: &str) {
if self.history.len() >= self.max_history {
self.history.pop_front();
}
self.history.push_back(format!("{}: {} - {}", "Error".red(), file, error));
self.update_display();
}
fn update_display(&mut self) {
// Move cursor to beginning of display area
print!("\x1B[7A\x1B[0J"); // Move up 7 lines and clear to end of screen
// Print current file line (progress bar will be rendered separately)
println!("{}", self.current_file);
// Print history lines
for _ in 0..self.max_history {
println!();
}
// Print summary line (progress bar will be rendered separately)
println!();
// Move cursor back to start of display area
print!("\x1B[7A");
// Move past the current file line to where history should begin
print!("\x1B[1B");
// Update history items
for (i, item) in self.history.iter().enumerate() {
print!("\x1B[{};0H{}\x1B[K", i + 2, item);
}
// Fill any remaining empty history lines
for i in self.history.len()..self.max_history {
print!("\x1B[{};0H\x1B[K", i + 2);
}
// Ensure stdout is flushed
let _ = self.stdout.flush();
}
fn cleanup(&mut self) {
print!("\x1B[?25h"); // Show cursor
let _ = self.stdout.flush();
}
}
fn format_speed(bytes_per_sec: f64) -> String {
if bytes_per_sec < 1024.0 {
format!("{:.1}B/s", bytes_per_sec)
} else if bytes_per_sec < 1024.0 * 1024.0 {
format!("{:.1}KB/s", bytes_per_sec / 1024.0)
} else if bytes_per_sec < 1024.0 * 1024.0 * 1024.0 {
format!("{:.1}MB/s", bytes_per_sec / (1024.0 * 1024.0))
} else {
format!("{:.1}GB/s", bytes_per_sec / (1024.0 * 1024.0 * 1024.0))
}
}
fn format_size(bytes: u64) -> String {
if bytes < 1024 {
format!("{}B", bytes)
} else if bytes < 1024 * 1024 {
format!("{:.1}KB", bytes as f64 / 1024.0)
} else if bytes < 1024 * 1024 * 1024 {
format!("{:.1}MB", bytes as f64 / (1024.0 * 1024.0))
} else {
format!("{:.1}GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
}
}
fn create_file_progress_bar() -> ProgressBar { fn create_file_progress_bar() -> ProgressBar {
ProgressBar::new(0).with_style( let pb = ProgressBar::new(0);
ProgressStyle::with_template("{msg}\n[{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})") pb.set_style(
ProgressStyle::with_template("{msg}\n[{bar:40.cyan/blue}] {percent}% ({bytes}/{total_bytes} @ {binary_bytes_per_sec} - ETA: {eta})")
.unwrap() .unwrap()
.progress_chars("##-"), .progress_chars("█░-"),
) );
pb
} }
fn create_summary_progress_bar() -> ProgressBar { fn create_summary_progress_bar() -> ProgressBar {
ProgressBar::new(0).with_style( let pb = ProgressBar::new(0);
ProgressStyle::with_template("\n[{bar:40.green/yellow}] {pos}/{len} files") pb.set_style(
ProgressStyle::with_template("[{bar:40.green/yellow}] {pos}/{len} files")
.unwrap() .unwrap()
.progress_chars("##-"), .progress_chars("█░-"),
) );
pb
} }
fn collect_files(sources: &[String], recursive: bool) -> Result<Vec<PathBuf>> { fn collect_files(sources: &[String], recursive: bool) -> Result<Vec<PathBuf>> {
let mut all_files = Vec::new(); let mut all_files = Vec::new();
for src_pattern in sources { for src_pattern in sources {
if src_pattern.contains('*') { if src_pattern.contains('*') {
for entry in glob(src_pattern)? { for entry in glob(src_pattern)? {
let path = entry?; let path = entry?;
if path.is_dir() && !recursive { if path.is_dir() {
eprintln!("{}: Skipping directory (use -r to copy)", path.display().to_string().yellow()); if recursive {
continue; // Add directory itself for creation
all_files.push(path.clone());
} else {
eprintln!("{}: Skipping directory (use -r to copy recursively)",
path.display().to_string().yellow());
continue;
}
} else {
all_files.push(path);
} }
all_files.push(path);
} }
} else { } else {
let path = PathBuf::from(src_pattern); let path = PathBuf::from(src_pattern);
if !path.exists() { if !path.exists() {
return Err(anyhow::anyhow!("Source file not found: {}", path.display())); return Err(anyhow::anyhow!("Source path not found: {}", path.display()));
} }
if path.is_dir() && !recursive {
eprintln!("{}: Skipping directory (use -r to copy)", path.display().to_string().yellow()); if path.is_dir() {
continue; if recursive {
// Add directory itself for creation
all_files.push(path.clone());
} else {
eprintln!("{}: Skipping directory (use -r to copy recursively)",
path.display().to_string().yellow());
continue;
}
} else {
all_files.push(path);
} }
all_files.push(path);
} }
} }
// If using recursive flag, collect all files under directories
if recursive { if recursive {
let mut dir_files = Vec::new(); let mut dir_files = Vec::new();
for path in &all_files { for path in &all_files {
if path.is_dir() { if path.is_dir() {
for entry in WalkDir::new(path) { for entry in WalkDir::new(path).min_depth(1) {
match entry { match entry {
Ok(e) if e.file_type().is_file() => dir_files.push(e.into_path()), Ok(e) => dir_files.push(e.into_path()),
Err(e) => eprintln!("{}: {}", "Warning".yellow(), e), Err(e) => eprintln!("{}: {}", "Warning".yellow(), e),
_ => continue,
} }
} }
} }
@ -98,94 +226,170 @@ fn collect_files(sources: &[String], recursive: bool) -> Result<Vec<PathBuf>> {
Ok(all_files) Ok(all_files)
} }
fn copy_file_with_progress(src: &Path, dst: &Path, pb: &ProgressBar, preserve: bool) -> Result<()> { fn ensure_directory_exists(path: &Path) -> Result<()> {
let mut src_file = fs::File::open(src)?; if let Some(parent) = path.parent() {
let file_size = src_file.metadata()?.len(); if !parent.exists() {
pb.set_length(file_size); fs::create_dir_all(parent)
.with_context(|| format!("Failed to create directory: {}", parent.display()))?;
let mut dst_file = fs::File::create(dst)?;
let mut buffer = vec![0u8; 64 * 1024];
let mut total_copied = 0;
loop {
let bytes_read = src_file.read(&mut buffer)?;
if bytes_read == 0 {
break;
} }
dst_file.write_all(&buffer[..bytes_read])?; }
Ok(())
}
fn copy_file_with_progress(
src: &Path,
dst: &Path,
pb: &ProgressBar,
display: &mut DisplayManager,
preserve: bool,
) -> Result<()> {
ensure_directory_exists(dst)?;
let mut src_file = fs::File::open(src)
.with_context(|| format!("Failed to open source file: {}", src.display()))?;
let file_size = src_file.metadata()
.with_context(|| format!("Failed to read metadata for: {}", src.display()))?.len();
pb.set_length(file_size);
display.set_current_file(&src.display().to_string());
let dst_file = fs::File::create(dst)
.with_context(|| format!("Failed to create destination file: {}", dst.display()))?;
let mut dst_file = dst_file;
let mut buffer = vec![0u8; 64 * 1024]; // 64KB buffer
let mut total_copied = 0;
let start_time = Instant::now();
loop {
let bytes_read = match src_file.read(&mut buffer) {
Ok(0) => break, // End of file
Ok(n) => n,
Err(e) => return Err(anyhow::anyhow!("Failed to read from {}: {}", src.display(), e)),
};
match dst_file.write_all(&buffer[..bytes_read]) {
Ok(_) => {},
Err(e) => return Err(anyhow::anyhow!("Failed to write to {}: {}", dst.display(), e)),
}
total_copied += bytes_read as u64; total_copied += bytes_read as u64;
pb.set_position(total_copied); pb.set_position(total_copied);
} }
if preserve { if preserve {
let metadata = src_file.metadata()?; if let Ok(metadata) = fs::metadata(src) {
filetime::set_file_times( let _ = filetime::set_file_times(
dst, dst,
FileTime::from_last_access_time(&metadata), FileTime::from_last_access_time(&metadata),
FileTime::from_last_modification_time(&metadata), FileTime::from_last_modification_time(&metadata),
)?; );
// Try to preserve permissions on Unix
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let permissions = metadata.permissions();
let _ = fs::set_permissions(dst, permissions);
}
}
} }
display.add_completed_file(&src.display().to_string());
Ok(()) Ok(())
} }
fn compute_dest_path(src: &Path, src_base: Option<&Path>, dest: &Path) -> PathBuf {
if let Some(base) = src_base {
if let Ok(relative) = src.strip_prefix(base) {
return dest.join(relative);
}
}
if dest.is_dir() {
dest.join(src.file_name().unwrap_or_else(|| src.as_os_str()))
} else {
dest.to_path_buf()
}
}
fn main() -> Result<()> { fn main() -> Result<()> {
let args = Args::parse(); let args = Args::parse();
// Reserve space for the display (7 lines)
for _ in 0..7 {
println!();
}
let mut display = DisplayManager::new(args.max_history);
let file_pb = create_file_progress_bar(); let file_pb = create_file_progress_bar();
let summary_pb = create_summary_progress_bar(); let summary_pb = create_summary_progress_bar();
let all_files = collect_files(&args.sources, args.recursive)?; let all_paths = collect_files(&args.sources, args.recursive)?;
// Filter to get just files for progress tracking
let all_files: Vec<_> = all_paths.iter()
.filter(|p| p.is_file())
.collect();
let total_files = all_files.len(); let total_files = all_files.len();
summary_pb.set_length(total_files as u64); summary_pb.set_length(total_files as u64);
let mut history = VecDeque::with_capacity(args.max_history);
let mut success_count = 0; let mut success_count = 0;
let dest_path = PathBuf::from(&args.dest);
for src in &all_files {
let dst = if PathBuf::from(&args.dest).is_dir() { // If we're copying multiple files, destination must be a directory
PathBuf::from(&args.dest).join( if all_files.len() > 1 && !dest_path.is_dir() && !all_paths.iter().all(|p| p.is_dir()) {
src.file_name() if !dest_path.exists() {
.unwrap_or_else(|| src.as_os_str()) // Create directory if it doesn't exist
) fs::create_dir_all(&dest_path)
.with_context(|| format!("Destination must be a directory when copying multiple files: {}", dest_path.display()))?;
} else { } else {
PathBuf::from(&args.dest) return Err(anyhow::anyhow!("Destination must be a directory when copying multiple files: {}", dest_path.display()));
}; }
}
file_pb.reset(); // Process directories first to ensure they exist
file_pb.set_message(format!("{}: {}", "Copying".bold().yellow(), src.display())); for src in all_paths.iter().filter(|p| p.is_dir()) {
let dst = compute_dest_path(src, None, &dest_path);
match copy_file_with_progress(src, &dst, &file_pb, args.preserve) { if !dst.exists() {
Ok(_) => { if let Err(e) = fs::create_dir_all(&dst) {
success_count += 1; display.add_error(&src.display().to_string(), &format!("Failed to create directory: {}", e));
if history.len() >= args.max_history {
history.pop_front();
}
history.push_back(format!("{}: {}", "Copied".green(), src.display()));
println!("\x1B[2J\x1B[H"); // Clear screen
println!("{}", file_pb.message());
for item in &history {
println!("{}", item);
}
println!("{}", summary_pb.message());
}
Err(e) => {
eprintln!("{}: {}", "Error".red(), e);
} }
} }
}
// Now copy all files
for src in &all_files {
let dst = compute_dest_path(src, None, &dest_path);
file_pb.reset();
match copy_file_with_progress(src, &dst, &file_pb, &mut display, args.preserve) {
Ok(_) => {
success_count += 1;
}
Err(e) => {
display.add_error(&src.display().to_string(), &e.to_string());
}
}
summary_pb.inc(1); summary_pb.inc(1);
} }
file_pb.finish_and_clear(); file_pb.finish_and_clear();
summary_pb.finish_with_message( summary_pb.finish_with_message(
format!("{} ({} successful, {} errors)", format!("{} ({} successful, {} failed)",
"Copy complete".bold().green(), "Copy complete".bold().green(),
success_count.to_string().green(), success_count.to_string().green(),
(total_files - success_count).to_string().red() (total_files - success_count).to_string().red()
) )
); );
// Clean up terminal state
display.cleanup();
if success_count != total_files { if success_count != total_files {
std::process::exit(1); std::process::exit(1);
} }