aviary-cli/src/main.rs

204 lines
7.0 KiB
Rust
Raw Normal View History

mod parse;
mod crypto;
mod upload;
mod thumbnailing;
2022-08-12 01:41:18 +00:00
mod protobuf;
mod errors;
2022-08-14 01:55:53 +00:00
use std::{borrow::Cow, io::stdout};
2022-08-14 01:56:38 +00:00
use std::io::{Read, Write};
2022-08-14 15:54:20 +00:00
use std::fs::{File, self, OpenOptions};
2022-08-12 01:41:18 +00:00
use std::path::Path;
2022-08-12 01:41:18 +00:00
use errors::AviaryError;
use itertools::{Itertools, Either};
2022-08-12 20:14:30 +00:00
use parse::{CreateArgs, Command, DownloadArgs};
2022-08-12 01:41:18 +00:00
use ::protobuf::Message;
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();
2022-08-14 01:55:53 +00:00
let server_no_trailing_slash = args.server.trim_end_matches('/');
let full_server = if server_no_trailing_slash.starts_with("http") {
Cow::Borrowed(server_no_trailing_slash)
} else {
Cow::Owned(format!("https://{}", server_no_trailing_slash))
};
2022-08-12 20:14:30 +00:00
match args.command {
2022-08-14 01:55:53 +00:00
Command::Create(create_args) => create(&*full_server, create_args),
Command::Download(download_args) => download(&*full_server, download_args),
2022-08-12 20:14:30 +00:00
}
}
fn create(server: &str, args: CreateArgs) {
print!("Checking files...");
let (files, errors): (Vec<_>, Vec<_>) = args.images.iter()
.map(Path::new)
.map(|path| File::open(path)
.map(|file| (path, file))
2022-08-12 01:41:18 +00:00
.map_err(|e| AviaryError::from_open_error(e.kind(), &path))
)
.partition_result();
if !errors.is_empty() {
println!(" \x1b[31mError!\x1b[0m");
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!("\x1b[37m- \x1b[31m{}", path.display()));
}
if !noread.is_empty() {
println!("\x1b[0m\nWe found these files, but didn't have permission to open them:");
noread.iter().for_each(|path| println!("\x1b[37m- \x1b[31m{}", path.display()));
}
} else {
println!(" \x1b[32mDone!\n");
let agent = upload::get_agent();
2022-08-12 01:41:18 +00:00
let index_url = files.into_iter()
.inspect(|(path, _)| print!("\x1b[36m{}\x1b[0m\n\x1b[37m├─\x1b[0m Reading... ", path.display()))
2022-08-14 01:41:22 +00:00
.inspect(|_| drop(stdout().flush()))
.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))
)
.inspect(|r| if r.is_ok() { print!("\x1b[32mDone!\n\x1b[37m├─\x1b[0m Thumbnailing... ") })
2022-08-14 01:41:22 +00:00
.inspect(|_| drop(stdout().flush()))
.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))
}))
.inspect(|r| if r.is_ok() { print!("\x1b[32mDone!\n\x1b[37m├─\x1b[0m Encrypting... ")})
2022-08-14 01:41:22 +00:00
.inspect(|_| drop(stdout().flush()))
.map_ok(|(raw_dat, thumbnail, blurhash)| {
let key = crypto::make_key();
(
key,
crypto::encrypt(&key, &raw_dat),
crypto::encrypt(&key, &thumbnail),
blurhash
)
})
.inspect(|r| if r.is_ok() { print!("\x1b[32mDone!\n\x1b[37m└─\x1b[0m Uploading... ")})
2022-08-14 01:41:22 +00:00
.inspect(|_| drop(stdout().flush()))
.map(|r| r.and_then(|(key, full_img, thumb, blurhash)|
2022-08-14 01:55:53 +00:00
upload::put_data(&agent, server, &thumb)
.and_then(|thumb_url|
2022-08-14 01:55:53 +00:00
upload::put_data(&agent, server, &full_img)
.map(|full_url| (key, full_url, thumb_url, blurhash)))
2022-08-12 01:41:18 +00:00
.map_err(AviaryError::from_upload_error)
))
.map(|r| r.and_then(|(key, full_url, thumb_url, blurhash)| {
2022-08-14 01:55:53 +00:00
let full_trimmed = trim_url(server, &full_url);
let thmb_trimmed = trim_url(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)))
}
}))
.inspect(|r| if r.is_ok() { println!("\x1b[32mDone!\n")})
2022-08-12 01:41:18 +00:00
.map_ok(|(key, full_url, thumb_url, blurhash)| protobuf::image::Image {
key: key.into(),
full_url, thumb_url, blurhash,
special_fields: Default::default()
})
.collect::<Result<Vec<_>, _>>()
2022-08-12 01:41:18 +00:00
.and_then(|image_info|{
let index = protobuf::index::Index {
images: image_info,
title: args.title,
desc: None,
special_fields: Default::default()
};
let index_key = crypto::make_key();
let encrypted_index = crypto::encrypt(
&index_key,
&index.write_to_bytes()
.expect("Error serializing protocol buffers")
);
let encoded_key = base64::encode(index_key);
print!("\x1b[0mUploading index... ");
2022-08-14 01:55:53 +00:00
upload::put_data(&agent, server, &encrypted_index)
2022-08-12 01:41:18 +00:00
.map_err(|e| AviaryError::from_upload_error(e))
.map(|url| format!("{}#{}", url.trim().trim_end_matches(".bin"), &encoded_key))
});
match index_url {
Ok(url) =>
println!("\x1b[32mDone!\n\n\x1b[34mYour gallery is: \x1b[1;0m{}", url),
Err(e) =>
print!("{}", e),
}
}
}
2022-08-12 20:14:30 +00:00
fn download(server: &str, args: DownloadArgs) {
2022-08-14 01:56:38 +00:00
let mut download_buffer = Vec::with_capacity(5_000_000);
let mut decrypt_buffer = Vec::with_capacity(5_000_000);
let index_url = format!("{}/{}.bin", server, args.id);
let key: [u8; 32] = base64::decode(args.key)
.expect("key is malformed b64")
.try_into()
.expect("key is the wrong size");
let download_agent = upload::get_agent();
let encrypted_index = upload::get_data(&download_agent, &index_url, 500_000, &mut download_buffer)
.expect("GET failed");
let serialized_index = crypto::decrypt(&key, &encrypted_index, &mut decrypt_buffer)
.expect("wrong key or corrupt index");
let index = protobuf::index::Index::parse_from_bytes(&serialized_index)
.expect("malformed index");
2022-08-14 15:54:20 +00:00
let dest_dir: Cow<Path> = args.output.map(Cow::Owned)
.unwrap_or_else(|| Cow::Borrowed(
index.title.as_ref()
.map(String::as_str)
.unwrap_or("Unnamed-Album").as_ref()));
fs::create_dir_all(&dest_dir).expect("Failed to create destination directory");
for (indx, image) in index.images.into_iter().enumerate() {
let path = dest_dir.join(format!("{indx:03}.webp"));
let mut dest_file = OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(path)
.expect("Failed to open");
let key = image.key.try_into().expect("Invalid key size");
let encrypted_thumbnail = upload::get_data(
&download_agent,
&format!("{}/{}.bin", server, image.thumb_url),
7680_000,
&mut download_buffer
).expect("Failed to retrieve image data");
let decrypted_thumb = crypto::decrypt(
&key,
encrypted_thumbnail,
&mut decrypt_buffer
).expect("Invalid image data referenced by index or bad key in index");
dest_file.write_all(&decrypted_thumb).expect("Failed to write to disk");
}
2022-08-12 20:14:30 +00:00
}