191 lines
6.5 KiB
Rust
191 lines
6.5 KiB
Rust
|
mod parse;
|
||
|
mod crypto;
|
||
|
mod upload;
|
||
|
mod thumbnailing;
|
||
|
|
||
|
use std::borrow::Cow;
|
||
|
use std::io::{self, ErrorKind, Read};
|
||
|
use std::fs::File;
|
||
|
use std::path::{PathBuf, Path};
|
||
|
|
||
|
use itertools::{Itertools, Either};
|
||
|
|
||
|
#[derive(Debug)]
|
||
|
enum AviaryError {
|
||
|
/// One of the files passed by the user simply did not exist
|
||
|
///
|
||
|
/// The attached path is the file which didn't exist
|
||
|
///
|
||
|
/// Handling: Halt execution before any upload begins and alert the user of any
|
||
|
/// missing files.
|
||
|
FileDNE(PathBuf),
|
||
|
|
||
|
/// The program lacks permission to read one of the images provided by the user
|
||
|
///
|
||
|
/// Handling: Halt execution before any upload begins and alert the user of the issue
|
||
|
ReadPermissionDenied(PathBuf),
|
||
|
|
||
|
/// There was an issue reading data from the disk
|
||
|
///
|
||
|
/// Handling: Halt execution immediately and alert the user
|
||
|
StreamReadError(PathBuf, io::Error),
|
||
|
|
||
|
/// One or more of the images wasn't correctly encoded
|
||
|
///
|
||
|
/// Handling: Halt execution immediately and ask the user if they're sure the
|
||
|
/// provided file is an image.
|
||
|
ImageFormatError(PathBuf),
|
||
|
|
||
|
/// The server is currently having some difficulties
|
||
|
///
|
||
|
/// Represents a 5XX or 4XX error, as well as any transport error that doesn't seem
|
||
|
/// like it could stem from a network issue.
|
||
|
///
|
||
|
/// Handling: Halt execution and alert the user that the server isn't working at the
|
||
|
/// minute, and ask them to try a different server or try again.
|
||
|
ServerError(String),
|
||
|
|
||
|
/// Couldn't connect to the server
|
||
|
///
|
||
|
/// Indicates that there was some network error which prevented the client from
|
||
|
/// reaching the server.
|
||
|
///
|
||
|
/// Handling: Halt execution and ask the user to check that their computer is
|
||
|
/// connected to the internet, and is not having any DNS issues.
|
||
|
ConnectionError(String),
|
||
|
|
||
|
/// The server URL provided by the user was invalid
|
||
|
///
|
||
|
/// Handling: Halt execution and alert the user that the server url they provided was
|
||
|
/// invalid, including a couple examples
|
||
|
BadServerParameter,
|
||
|
}
|
||
|
|
||
|
fn trim_url<'a>(base_url: &str, url: &'a str) -> Option<&'a str> {
|
||
|
if url.starts_with(base_url) {
|
||
|
let shortened = url
|
||
|
.trim()
|
||
|
.trim_start_matches(base_url)
|
||
|
.trim_matches('/')
|
||
|
.trim_end_matches(".bin");
|
||
|
if shortened.len() > 50 {
|
||
|
None
|
||
|
} else {
|
||
|
Some(shortened)
|
||
|
}
|
||
|
} else {
|
||
|
None
|
||
|
}
|
||
|
}
|
||
|
|
||
|
fn main() {
|
||
|
let args: parse::Args = argh::from_env();
|
||
|
let (files, errors): (Vec<_>, Vec<_>) = args.images.iter()
|
||
|
.map(Path::new)
|
||
|
.map(|path| File::open(path)
|
||
|
.map(|file| (path, file))
|
||
|
.map_err(|e| match e.kind() {
|
||
|
ErrorKind::NotFound => AviaryError::FileDNE(path.to_owned()),
|
||
|
ErrorKind::PermissionDenied => AviaryError::ReadPermissionDenied(path.to_owned()),
|
||
|
_ => panic!(
|
||
|
"Received an error kind that should be impossible for {}: {:?}",
|
||
|
path.display(),
|
||
|
e
|
||
|
)
|
||
|
}
|
||
|
))
|
||
|
.partition_result();
|
||
|
|
||
|
if !errors.is_empty() {
|
||
|
println!("[FATAL] The was a problem accessing some of the files you provided!");
|
||
|
let (nonexistant, noread): (Vec<_>, Vec<_>) = errors.iter()
|
||
|
.partition_map(|e| match e {
|
||
|
AviaryError::FileDNE(path) => Either::Left(path),
|
||
|
AviaryError::ReadPermissionDenied(path) => Either::Right(path),
|
||
|
other => panic!("This error should not be possible! {other:?}")
|
||
|
});
|
||
|
if !nonexistant.is_empty() {
|
||
|
println!("\nWe didn't see any files at the following locations:");
|
||
|
nonexistant.iter().for_each(|path| println!("{}", path.display()));
|
||
|
}
|
||
|
if !noread.is_empty() {
|
||
|
println!("\nWe found these files, but didn't have permission to open them:");
|
||
|
noread.iter().for_each(|path| println!("{}", path.display()));
|
||
|
}
|
||
|
} else {
|
||
|
let agent = upload::get_agent();
|
||
|
let full_server = if args.server.starts_with("http") {
|
||
|
Cow::Borrowed(&args.server)
|
||
|
} else {
|
||
|
Cow::Owned(format!("https://{}", args.server))
|
||
|
};
|
||
|
let upload_records = files.into_iter()
|
||
|
.map(|(path, mut file)|
|
||
|
(|| {
|
||
|
let mut buff = Vec::with_capacity(file.metadata()?.len() as usize);
|
||
|
file.read_to_end(&mut buff)?;
|
||
|
Ok((path, buff))
|
||
|
})().map_err(|e| AviaryError::StreamReadError(path.to_owned(), e))
|
||
|
)
|
||
|
.map(|r| r.and_then(|(path, raw_dat)| {
|
||
|
let (thumbnail, blurhash) = thumbnailing::thumbnail(&raw_dat)
|
||
|
.map_err(|_| AviaryError::ImageFormatError(path.to_owned()))?;
|
||
|
Ok((raw_dat, thumbnail, blurhash))
|
||
|
}))
|
||
|
.map_ok(|(raw_dat, thumbnail, blurhash)| {
|
||
|
let key = crypto::make_key();
|
||
|
(
|
||
|
key,
|
||
|
crypto::encrypt(&key, &raw_dat),
|
||
|
crypto::encrypt(&key, &thumbnail),
|
||
|
blurhash
|
||
|
)
|
||
|
})
|
||
|
.map(|r| r.and_then(|(key, full_img, thumb, blurhash)|
|
||
|
upload::put_data(&agent, &*full_server, &thumb)
|
||
|
.and_then(|thumb_url|
|
||
|
upload::put_data(&agent, &*full_server, &full_img)
|
||
|
.map(|full_url| (key, full_url, thumb_url, blurhash)))
|
||
|
.map_err(|err| match err {
|
||
|
ureq::Error::Status(code, msg) => AviaryError::ServerError(
|
||
|
format!("Error code {} received from server: {}", code,
|
||
|
msg.into_string().unwrap_or(String::new()))),
|
||
|
ureq::Error::Transport(transport) => match transport.kind() {
|
||
|
ureq::ErrorKind::InvalidUrl =>
|
||
|
AviaryError::BadServerParameter,
|
||
|
ureq::ErrorKind::Dns =>
|
||
|
AviaryError::ConnectionError(
|
||
|
format!("DNS issue: {}", transport.message().unwrap_or(""))),
|
||
|
ureq::ErrorKind::Io =>
|
||
|
AviaryError::ConnectionError(
|
||
|
format!("IO issue: {}", transport.message().unwrap_or(""))),
|
||
|
ureq::ErrorKind::ConnectionFailed =>
|
||
|
AviaryError::ConnectionError(
|
||
|
format!("Connection issue: {}", transport.message().unwrap_or(""))),
|
||
|
ureq::ErrorKind::TooManyRedirects =>
|
||
|
AviaryError::ServerError("Too many redirects".to_owned()),
|
||
|
ureq::ErrorKind::BadHeader =>
|
||
|
AviaryError::ServerError("Invalid header from server".to_owned()),
|
||
|
ureq::ErrorKind::BadStatus =>
|
||
|
AviaryError::ServerError("Invalid status from server".to_owned()),
|
||
|
unk => panic!("Unexpected transport error kind {unk}:\n{transport}")
|
||
|
},
|
||
|
})
|
||
|
))
|
||
|
.map(|r| r.and_then(|(key, full_url, thumb_url, blurhash)| {
|
||
|
let full_trimmed = trim_url(&*full_server, &full_url);
|
||
|
let thmb_trimmed = trim_url(&*full_server, &thumb_url);
|
||
|
if let (Some(full_url), Some(thmb_url)) = (full_trimmed, thmb_trimmed) {
|
||
|
Ok((key, full_url.to_owned(), thmb_url.to_owned(), blurhash))
|
||
|
} else {
|
||
|
Err(AviaryError::ServerError(format!("Received bad response from server: {}", full_url)))
|
||
|
}
|
||
|
}))
|
||
|
.collect::<Result<Vec<_>, _>>()
|
||
|
.unwrap();
|
||
|
upload_records.into_iter()
|
||
|
.for_each(|(key, full_url, thumb_url, blurhash)|
|
||
|
println!("{}, {}, {}, {}", base64::encode(key), full_url, thumb_url, blurhash))
|
||
|
}
|
||
|
}
|