omegaupload/cli/src/main.rs

269 lines
7.7 KiB
Rust
Raw Normal View History

2021-10-19 09:18:33 +00:00
#![warn(clippy::nursery, clippy::pedantic)]
#![deny(unsafe_code)]
2021-10-31 21:01:27 +00:00
// OmegaUpload CLI Client
// Copyright (C) 2021 Edward Shen
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
2022-08-24 07:58:20 +00:00
use std::io::{Cursor, Read, Write};
2021-10-31 04:00:09 +00:00
use std::path::PathBuf;
2021-10-16 16:50:11 +00:00
use anyhow::{anyhow, bail, Context, Result};
use atty::Stream;
2022-08-24 07:58:20 +00:00
use bytes::Bytes;
2021-10-27 05:30:37 +00:00
use clap::Parser;
2022-08-24 07:58:20 +00:00
use indicatif::{ProgressBar, ProgressStyle};
2021-10-31 01:38:55 +00:00
use omegaupload_common::crypto::{open_in_place, seal_in_place};
use omegaupload_common::fragment::Builder;
2022-07-27 02:38:45 +00:00
use omegaupload_common::secrecy::{ExposeSecret, SecretString, SecretVec};
2021-10-28 02:16:43 +00:00
use omegaupload_common::{
2021-10-31 01:38:55 +00:00
base64, Expiration, ParsedUrl, Url, API_ENDPOINT, EXPIRATION_HEADER_NAME,
2021-10-28 02:16:43 +00:00
};
2022-08-24 07:58:20 +00:00
use reqwest::blocking::{Body, Client};
2021-10-22 01:35:54 +00:00
use reqwest::header::EXPIRES;
2021-10-16 16:50:11 +00:00
use reqwest::StatusCode;
2022-07-11 03:27:51 +00:00
use rpassword::prompt_password;
2021-10-16 16:50:11 +00:00
2021-10-27 05:30:37 +00:00
#[derive(Parser)]
2021-10-16 16:50:11 +00:00
struct Opts {
#[clap(subcommand)]
action: Action,
}
2021-10-27 05:30:37 +00:00
#[derive(Parser)]
2021-10-16 16:50:11 +00:00
enum Action {
2022-01-18 05:29:39 +00:00
/// Upload a paste to an omegaupload server.
2021-10-16 16:50:11 +00:00
Upload {
2021-10-27 09:23:37 +00:00
/// The OmegaUpload instance to upload data to.
2021-10-16 16:50:11 +00:00
url: Url,
2021-10-27 09:23:37 +00:00
/// Encrypt the uploaded paste with the provided password, preventing
/// public access.
2021-10-16 16:50:11 +00:00
#[clap(short, long)]
2021-10-31 04:00:09 +00:00
password: bool,
2022-01-18 05:29:39 +00:00
/// How long for the paste to last, or until someone has read it.
#[clap(short, long, possible_values = Expiration::variants())]
2021-10-28 02:16:43 +00:00
duration: Option<Expiration>,
2022-01-12 04:42:40 +00:00
/// The path to the file to upload. If none is provided, then reads
/// stdin instead.
path: Option<PathBuf>,
/// Hint that the uploaded file should be syntax highlighted with a
/// specific language.
#[clap(short, long)]
language: Option<String>,
/// Don't provide a file name hint.
#[clap(short = 'F', long)]
no_file_name_hint: bool,
2021-10-16 16:50:11 +00:00
},
2022-01-18 05:29:39 +00:00
/// Download a paste from an omegaupload server.
2021-10-16 16:50:11 +00:00
Download {
2021-10-27 09:23:37 +00:00
/// The paste to download.
2021-10-16 16:50:11 +00:00
url: ParsedUrl,
},
}
fn main() -> Result<()> {
let opts = Opts::parse();
match opts.action {
2021-10-28 02:16:43 +00:00
Action::Upload {
url,
password,
duration,
2021-10-31 04:00:09 +00:00
path,
language,
no_file_name_hint,
} => handle_upload(url, password, duration, path, language, no_file_name_hint),
2021-10-16 16:50:11 +00:00
Action::Download { url } => handle_download(url),
}?;
Ok(())
}
2021-10-28 02:16:43 +00:00
fn handle_upload(
mut url: Url,
2021-10-31 04:00:09 +00:00
password: bool,
2021-10-28 02:16:43 +00:00
duration: Option<Expiration>,
2022-01-12 04:42:40 +00:00
path: Option<PathBuf>,
language: Option<String>,
no_file_name_hint: bool,
2021-10-28 02:16:43 +00:00
) -> Result<()> {
2021-10-16 16:50:11 +00:00
url.set_fragment(None);
2022-01-12 04:42:40 +00:00
if password && path.is_none() {
bail!("Reading data from stdin is incompatible with a password. Provide a path to a file to upload.");
}
2021-10-31 01:38:55 +00:00
let (data, key) = {
let mut container = if let Some(ref path) = path {
2022-01-12 04:42:40 +00:00
std::fs::read(path)?
} else {
let mut container = vec![];
std::io::stdin().lock().read_to_end(&mut container)?;
container
};
if container.is_empty() {
bail!("Nothing to upload.");
}
2021-10-31 04:00:09 +00:00
let password = if password {
2022-07-11 03:27:51 +00:00
let maybe_password = prompt_password("Please set the password for this paste: ")?;
2021-10-31 06:15:41 +00:00
Some(SecretVec::new(maybe_password.into_bytes()))
2021-10-31 04:00:09 +00:00
} else {
None
};
2021-10-31 01:38:55 +00:00
let enc_key = seal_in_place(&mut container, password)?;
let key = SecretString::new(base64::encode(&enc_key.expose_secret().as_ref()));
2021-10-31 01:38:55 +00:00
(container, key)
2021-10-16 16:50:11 +00:00
};
2022-08-24 07:58:20 +00:00
let mut req = Client::new().post(url.as_ref());
2021-10-28 02:16:43 +00:00
if let Some(duration) = duration {
2022-08-24 07:58:20 +00:00
req = req.header(&*EXPIRATION_HEADER_NAME, duration);
2021-10-28 02:16:43 +00:00
}
2022-08-24 07:58:20 +00:00
let data_size = data.len() as u64;
let progress_style = ProgressStyle::with_template(
"[{elapsed_precise}] {bar:40} {bytes}/{total_bytes} {eta_precise}",
)
.unwrap();
let progress_bar = ProgressBar::new(data_size).with_style(progress_style);
let res = req
.body(Body::sized(
WrappedBody::new(
move |amt| {
progress_bar.inc(amt as u64);
},
data,
),
data_size,
))
.build()
.expect("Failed to build body");
let res = reqwest::blocking::ClientBuilder::new()
.timeout(None)
.build()?
.execute(res)
.context("Request to server failed")?;
2021-10-16 16:50:11 +00:00
if res.status() != StatusCode::OK {
bail!("Upload failed. Got HTTP error {}", res.status());
}
url.path_segments_mut()
.map_err(|_| anyhow!("Failed to get base URL"))?
.extend(std::iter::once(res.text()?));
let mut fragment = Builder::new(key);
if password {
fragment = fragment.needs_password();
}
if !no_file_name_hint {
let file_name = path.and_then(|path| {
path.file_name()
.map(|str| str.to_string_lossy().to_string())
});
if let Some(file_name) = file_name {
fragment = fragment.file_name(file_name);
}
}
if let Some(language) = language {
fragment = fragment.language(language);
}
2021-10-16 16:50:11 +00:00
2022-01-17 03:40:14 +00:00
url.set_fragment(Some(fragment.build().expose_secret()));
2021-10-16 16:50:11 +00:00
2022-01-16 08:49:42 +00:00
println!("{url}");
2021-10-16 16:50:11 +00:00
Ok(())
}
2022-08-24 07:58:20 +00:00
struct WrappedBody<Callback> {
callback: Callback,
inner: Cursor<Bytes>,
}
impl<Callback> WrappedBody<Callback> {
fn new(callback: Callback, data: Vec<u8>) -> Self {
Self {
callback,
inner: Cursor::new(Bytes::from(data)),
}
}
}
impl<Callback: FnMut(usize)> Read for WrappedBody<Callback> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let res = self.inner.read(buf);
if let Ok(size) = res {
(self.callback)(size);
}
res
}
}
2021-10-27 08:49:06 +00:00
fn handle_download(mut url: ParsedUrl) -> Result<()> {
2021-10-28 02:16:43 +00:00
url.sanitized_url
2022-01-16 08:49:42 +00:00
.set_path(&format!("{API_ENDPOINT}{}", url.sanitized_url.path()));
2021-10-16 16:50:11 +00:00
let res = Client::new()
.get(url.sanitized_url)
.send()
.context("Failed to get data")?;
if res.status() != StatusCode::OK {
bail!("Got bad response from server: {}", res.status());
}
2021-10-27 08:49:06 +00:00
let expiration_text = res
.headers()
2021-10-22 01:35:54 +00:00
.get(EXPIRES)
.and_then(|v| Expiration::try_from(v).ok())
.as_ref()
.map_or_else(
|| "This paste will not expire.".to_string(),
ToString::to_string,
);
2021-10-22 01:35:54 +00:00
2021-10-16 16:50:11 +00:00
let mut data = res.bytes()?.as_ref().to_vec();
2021-10-31 07:57:52 +00:00
let password = if url.needs_password {
2021-10-16 16:50:11 +00:00
// Only print prompt on interactive, else it messes with output
2022-07-11 03:27:51 +00:00
let maybe_password = prompt_password("Please enter the password to access this paste: ")?;
2021-10-31 07:57:52 +00:00
Some(SecretVec::new(maybe_password.into_bytes()))
} else {
None
};
2021-10-16 16:50:11 +00:00
2021-10-31 06:15:41 +00:00
open_in_place(&mut data, &url.decryption_key, password)?;
2021-10-16 16:50:11 +00:00
if atty::is(Stream::Stdout) {
if let Ok(data) = String::from_utf8(data) {
std::io::stdout().write_all(data.as_bytes())?;
} else {
bail!("Binary output detected. Please pipe to a file.");
}
} else {
std::io::stdout().write_all(&data)?;
}
2022-01-16 08:49:42 +00:00
eprintln!("{expiration_text}");
2021-10-22 01:35:54 +00:00
2021-10-16 16:50:11 +00:00
Ok(())
}