wip
This commit is contained in:
parent
133aac1aac
commit
7c011a30d0
5 changed files with 290 additions and 106 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1 +1,3 @@
|
|||
target
|
||||
/target
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
|
|
21
Cargo.lock
generated
21
Cargo.lock
generated
|
@ -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"
|
||||
|
|
15
Cargo.toml
15
Cargo.toml
|
@ -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
0
readme.md
Normal file
356
src/main.rs
356
src/main.rs
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue