use std::{
fs,
io::{Read, Write},
path::{Path, PathBuf},
sync::mpsc,
};
use anyhow::{Context, Result};
use assert2::assert;
use colored::Colorize;
use ignore::{WalkBuilder, WalkState};
use indicatif::{ProgressBar, ProgressStyle};
use tempfile::NamedTempFile;
use crate::crypt::{HEADER_LEN, MAGIC, is_encrypted_version};
#[allow(dead_code)]
#[cfg(any(test, debug_assertions))]
pub fn format_hex(value: &[u8]) -> String {
use std::fmt::Write;
value.iter().fold(String::new(), |mut output, b| {
let _ = write!(output, "{b:02x}");
output
})
}
pub fn atomic_write(path: &Path, data: &[u8]) -> Result<()> {
let parent = path.parent().unwrap_or_else(|| Path::new("."));
let mut temp_file = NamedTempFile::new_in(parent)
.with_context(|| "Failed to create temp file for atomic write")?;
temp_file.write_all(data)?;
temp_file
.persist(path)
.with_context(|| format!("Failed to persist atomic write to {}", path.display()))?;
Ok(())
}
pub fn prompt_password(prompt: &str) -> Result<String> {
print!("{prompt}");
std::io::stdout().flush()?;
let mut password = String::new();
std::io::stdin().read_line(&mut password)?;
assert!(!password.is_empty(), "Password must not be empty");
Ok(password.trim().to_string())
}
pub fn list_files(
paths: impl IntoIterator<Item = impl AsRef<Path>>,
cwd: impl AsRef<Path>,
) -> Vec<PathBuf> {
let mut paths_iter = paths.into_iter();
let cwd = cwd.as_ref();
let mut builder = if let Some(first_path) = paths_iter.next() {
debug_assert!(first_path.as_ref().is_relative());
WalkBuilder::new(cwd.join(first_path))
} else {
return Vec::new();
};
for p in paths_iter {
debug_assert!(p.as_ref().is_relative());
builder.add(cwd.join(p));
}
builder
.current_dir(cwd)
.hidden(false)
.git_ignore(true)
.ignore(true)
.git_global(true)
.git_exclude(true)
.follow_links(false)
.threads(0);
let parallel_walker: ignore::WalkParallel = builder.build_parallel();
let (tx, rx) = mpsc::channel();
parallel_walker.run(|| {
let tx = tx.clone();
Box::new(move |result| {
if let Ok(entry) = result
&& let Some(file_type) = entry.file_type()
&& file_type.is_file()
{
let _ = tx.send(entry.into_path());
}
WalkState::Continue
})
});
drop(tx);
rx.into_iter().collect()
}
const REPORT_LIST_LIMIT: usize = 10;
pub fn print_pre_report(action: &str, files: &[PathBuf], repo_path: &Path) {
let count = files.len();
println!(
"\n{} {} {}",
action.bold(),
format!("({count} files)").cyan(),
":".dimmed()
);
for f in &files[..count.min(REPORT_LIST_LIMIT)] {
let relative = pathdiff::diff_paths(f, repo_path).unwrap_or_else(|| f.clone());
println!(" {}", relative.display());
}
if count > REPORT_LIST_LIMIT {
let remaining = count - REPORT_LIST_LIMIT;
println!(" {}", format!("... and {remaining} more files").dimmed());
}
println!();
}
pub fn print_post_report(action: &str, total: usize, skipped: usize, failed: usize) {
let succeeded = total - skipped - failed;
let label = format!("{action} complete").bold();
if failed > 0 {
println!(
"\n{}: {} succeeded, {} skipped, {} {}",
label,
succeeded.to_string().green(),
skipped.to_string().yellow(),
failed.to_string().red(),
"failed".red(),
);
} else {
println!(
"\n{}: {} succeeded, {} skipped",
label,
succeeded.to_string().green(),
skipped.to_string().yellow(),
);
}
}
pub fn create_progress_bar(len: usize, prefix: &'static str) -> ProgressBar {
let pb = ProgressBar::new(len as u64);
pb.set_style(
ProgressStyle::with_template(
"{prefix:.bold} {bar:40.cyan/blue} {pos}/{len} {spinner} [{elapsed_precise}]",
)
.unwrap()
.progress_chars("#>-"),
);
pb.set_prefix(prefix);
pb.enable_steady_tick(std::time::Duration::from_millis(200));
pb
}
pub fn is_file_encrypted(path: &Path) -> Result<bool> {
let mut file = fs::File::open(path).with_context(|| {
format!(
"Failed to open file for encryption check: {}",
path.display()
)
})?;
let mut header_bytes = [0u8; HEADER_LEN];
let bytes_read = file.read(&mut header_bytes)?;
if bytes_read < HEADER_LEN {
return Ok(false);
}
Ok(&header_bytes[0..5] == MAGIC && is_encrypted_version(header_bytes[5]))
}
pub fn resolve_target_files(
paths: &[PathBuf],
crypt_list: &[String],
repo_path: &Path,
) -> Vec<PathBuf> {
if paths.is_empty() {
list_files(crypt_list.iter(), repo_path)
} else {
list_files(paths, repo_path)
}
}
#[cfg(test)]
mod tests {
use assert2::assert;
use path_absolutize::Absolutize as _;
use super::*;
#[test]
fn test_list_files() {
let paths = vec!["docs", ".gitignore", "src", "some_thing_not_exist"]
.into_iter()
.map(PathBuf::from);
let res = list_files(paths, ".")
.into_iter()
.map(|x| x.absolutize().unwrap().to_path_buf())
.collect::<Vec<_>>();
dbg!(&res);
assert!(
res.contains(
&Path::new("docs/README_zh-CN.md")
.absolutize()
.unwrap()
.to_path_buf()
)
);
assert!(res.contains(&Path::new(".gitignore").absolutize().unwrap().to_path_buf()));
assert!(
res.contains(
&Path::new("src/utils/mod.rs")
.absolutize()
.unwrap()
.to_path_buf()
)
);
assert!(!res.contains(&Path::new("docs/").absolutize().unwrap().to_path_buf()));
}
#[test]
fn test_cwd() {
assert_eq!(
list_files([".gitignore"], Path::new(".").absolutize().unwrap()),
vec![Path::new(".gitignore").absolutize().unwrap()]
);
assert_eq!(
list_files(["lib.rs"], Path::new("src").absolutize().unwrap()),
vec![Path::new("src/lib.rs").absolutize().unwrap()]
);
}
}