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
16pub 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#[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#[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
95pub 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
102pub 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}