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",
"glob",
"indicatif",
"thiserror",
"walkdir",
]
@ -318,26 +317,6 @@ dependencies = [
"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]]
name = "unicode-ident"
version = "1.0.18"

View file

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

0
readme.md Normal file
View file

View file

@ -1,10 +1,11 @@
use std::fs;
use std::io::{Read, Write};
use std::io::{Read, Write, stdout, Stdout};
use std::path::{Path, PathBuf};
use std::collections::VecDeque;
use std::time::{Instant, Duration};
use colored::Colorize;
use indicatif::{ProgressBar, ProgressStyle};
use anyhow::Result;
use anyhow::{Result, Context};
use walkdir::WalkDir;
use glob::glob;
use clap::Parser;
@ -34,56 +35,183 @@ struct Args {
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 {
ProgressBar::new(0).with_style(
ProgressStyle::with_template("{msg}\n[{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})")
let pb = ProgressBar::new(0);
pb.set_style(
ProgressStyle::with_template("{msg}\n[{bar:40.cyan/blue}] {percent}% ({bytes}/{total_bytes} @ {binary_bytes_per_sec} - ETA: {eta})")
.unwrap()
.progress_chars("##-"),
)
.progress_chars("█░-"),
);
pb
}
fn create_summary_progress_bar() -> ProgressBar {
ProgressBar::new(0).with_style(
ProgressStyle::with_template("\n[{bar:40.green/yellow}] {pos}/{len} files")
let pb = ProgressBar::new(0);
pb.set_style(
ProgressStyle::with_template("[{bar:40.green/yellow}] {pos}/{len} files")
.unwrap()
.progress_chars("##-"),
)
.progress_chars("█░-"),
);
pb
}
fn collect_files(sources: &[String], recursive: bool) -> Result<Vec<PathBuf>> {
let mut all_files = Vec::new();
for src_pattern in sources {
if src_pattern.contains('*') {
for entry in glob(src_pattern)? {
let path = entry?;
if path.is_dir() && !recursive {
eprintln!("{}: Skipping directory (use -r to copy)", path.display().to_string().yellow());
continue;
if path.is_dir() {
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);
}
} else {
let path = PathBuf::from(src_pattern);
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());
continue;
if path.is_dir() {
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 {
let mut dir_files = Vec::new();
for path in &all_files {
if path.is_dir() {
for entry in WalkDir::new(path) {
for entry in WalkDir::new(path).min_depth(1) {
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),
_ => continue,
}
}
}
@ -98,94 +226,170 @@ fn collect_files(sources: &[String], recursive: bool) -> Result<Vec<PathBuf>> {
Ok(all_files)
}
fn copy_file_with_progress(src: &Path, dst: &Path, pb: &ProgressBar, preserve: bool) -> Result<()> {
let mut src_file = fs::File::open(src)?;
let file_size = src_file.metadata()?.len();
pb.set_length(file_size);
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;
fn ensure_directory_exists(path: &Path) -> Result<()> {
if let Some(parent) = path.parent() {
if !parent.exists() {
fs::create_dir_all(parent)
.with_context(|| format!("Failed to create directory: {}", parent.display()))?;
}
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;
pb.set_position(total_copied);
}
if preserve {
let metadata = src_file.metadata()?;
filetime::set_file_times(
dst,
FileTime::from_last_access_time(&metadata),
FileTime::from_last_modification_time(&metadata),
)?;
if let Ok(metadata) = fs::metadata(src) {
let _ = filetime::set_file_times(
dst,
FileTime::from_last_access_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(())
}
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<()> {
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 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();
summary_pb.set_length(total_files as u64);
let mut history = VecDeque::with_capacity(args.max_history);
let mut success_count = 0;
for src in &all_files {
let dst = if PathBuf::from(&args.dest).is_dir() {
PathBuf::from(&args.dest).join(
src.file_name()
.unwrap_or_else(|| src.as_os_str())
)
let dest_path = PathBuf::from(&args.dest);
// If we're copying multiple files, destination must be a directory
if all_files.len() > 1 && !dest_path.is_dir() && !all_paths.iter().all(|p| p.is_dir()) {
if !dest_path.exists() {
// 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 {
PathBuf::from(&args.dest)
};
return Err(anyhow::anyhow!("Destination must be a directory when copying multiple files: {}", dest_path.display()));
}
}
file_pb.reset();
file_pb.set_message(format!("{}: {}", "Copying".bold().yellow(), src.display()));
match copy_file_with_progress(src, &dst, &file_pb, args.preserve) {
Ok(_) => {
success_count += 1;
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);
// Process directories first to ensure they exist
for src in all_paths.iter().filter(|p| p.is_dir()) {
let dst = compute_dest_path(src, None, &dest_path);
if !dst.exists() {
if let Err(e) = fs::create_dir_all(&dst) {
display.add_error(&src.display().to_string(), &format!("Failed to create directory: {}", 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);
}
file_pb.finish_and_clear();
summary_pb.finish_with_message(
format!("{} ({} successful, {} errors)",
format!("{} ({} successful, {} failed)",
"Copy complete".bold().green(),
success_count.to_string().green(),
(total_files - success_count).to_string().red()
)
);
// Clean up terminal state
display.cleanup();
if success_count != total_files {
std::process::exit(1);
}