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::, _>>() .unwrap(); upload_records.into_iter() .for_each(|(key, full_url, thumb_url, blurhash)| println!("{}, {}, {}, {}", base64::encode(key), full_url, thumb_url, blurhash)) } }