algae_cli/
files.rs

1use std::{
2	fmt::Debug,
3	io::{stderr, IsTerminal as _},
4	path::{Path, PathBuf},
5};
6
7use age::{Identity, Recipient};
8use indicatif::{ProgressBar, ProgressBarIter, ProgressStyle};
9use miette::{Context as _, IntoDiagnostic as _, Result};
10use tokio::{fs::File, io::AsyncRead};
11use tokio_util::compat::{TokioAsyncReadCompatExt as _, TokioAsyncWriteCompatExt as _};
12use tracing::instrument;
13
14use crate::streams::{decrypt_stream, encrypt_stream};
15
16/// Wraps a [`tokio::io::AsyncRead`] with an [`indicatif::ProgressBar`].
17///
18/// The progress bar outputs to stderr iff that's terminal, and nothing is displayed otherwise.
19pub fn with_progress_bar<R: AsyncRead + Unpin>(
20	expected_length: u64,
21	reader: R,
22) -> ProgressBarIter<R> {
23	if stderr().is_terminal() {
24		let style = ProgressStyle::default_bar()
25			.template("[{bar:.green/blue}] {wide_msg} {binary_bytes}/{binary_total_bytes} ({eta})")
26			.expect("BUG: progress bar template invalid");
27		ProgressBar::new(expected_length).with_style(style)
28	} else {
29		ProgressBar::hidden()
30	}
31	.wrap_async_read(reader)
32}
33
34/// Encrypt a path to another given a [`Recipient`].
35///
36/// If stderr is a terminal, this will show a progress bar.
37#[instrument(level = "debug", skip(key))]
38pub async fn encrypt_file(
39	input_path: impl AsRef<Path> + Debug,
40	output_path: impl AsRef<Path> + Debug,
41	key: Box<dyn Recipient + Send>,
42) -> Result<u64> {
43	let input = File::open(input_path)
44		.await
45		.into_diagnostic()
46		.wrap_err("opening the plainetxt")?;
47	let input_length = input
48		.metadata()
49		.await
50		.into_diagnostic()
51		.wrap_err("reading input file length")?
52		.len();
53
54	let output = File::create_new(output_path)
55		.await
56		.into_diagnostic()
57		.wrap_err("opening the encrypted output")?;
58
59	encrypt_stream(
60		with_progress_bar(input_length, input),
61		output.compat_write(),
62		key,
63	)
64	.await
65}
66
67/// Decrypt a path to another given an [`Identity`].
68///
69/// If stderr is a terminal, this will show a progress bar.
70#[instrument(level = "debug", skip(key))]
71pub async fn decrypt_file(
72	input_path: impl AsRef<Path> + Debug,
73	output_path: impl AsRef<Path> + Debug,
74	key: Box<dyn Identity>,
75) -> Result<u64> {
76	let input = File::open(input_path)
77		.await
78		.into_diagnostic()
79		.wrap_err("opening the input file")?;
80	let input_length = input
81		.metadata()
82		.await
83		.into_diagnostic()
84		.wrap_err("reading input file length")?
85		.len();
86
87	let output = File::create_new(output_path)
88		.await
89		.into_diagnostic()
90		.wrap_err("opening the output file")?;
91
92	decrypt_stream(with_progress_bar(input_length, input).compat(), output, key).await
93}
94
95/// Append `.age` to a file path.
96pub fn append_age_ext(path: impl AsRef<Path>) -> PathBuf {
97	let mut path = path.as_ref().as_os_str().to_owned();
98	path.push(".age");
99	path.into()
100}
101
102/// Remove the `.age` suffix from a file path, if present.
103///
104/// Returns `Err(original path)` if the suffix isn't present.
105pub fn remove_age_ext<T: AsRef<Path>>(path: T) -> std::result::Result<PathBuf, T> {
106	if !path.as_ref().extension().is_some_and(|ext| ext == "age") {
107		Err(path)
108	} else {
109		Ok(path.as_ref().with_extension(""))
110	}
111}