Produce actual errors when downloading instead of panicing
This commit is contained in:
parent
590dd48d54
commit
b76c812186
235
src/errors.rs
235
src/errors.rs
|
@ -2,7 +2,7 @@ use core::fmt;
|
||||||
use std::{path::{PathBuf, Path}, io};
|
use std::{path::{PathBuf, Path}, io};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum AviaryError {
|
pub enum AviaryUploadError {
|
||||||
/// One of the files passed by the user simply did not exist
|
/// One of the files passed by the user simply did not exist
|
||||||
///
|
///
|
||||||
/// The attached path is the file which didn't exist
|
/// The attached path is the file which didn't exist
|
||||||
|
@ -52,11 +52,11 @@ pub enum AviaryError {
|
||||||
BadServerParameter,
|
BadServerParameter,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AviaryError {
|
impl AviaryUploadError {
|
||||||
pub fn from_open_error(e: io::ErrorKind, location: &Path) -> AviaryError {
|
pub fn from_open_error(e: io::ErrorKind, location: &Path) -> AviaryUploadError {
|
||||||
match e {
|
match e {
|
||||||
io::ErrorKind::NotFound => AviaryError::FileDNE(location.to_owned()),
|
io::ErrorKind::NotFound => AviaryUploadError::FileDNE(location.to_owned()),
|
||||||
io::ErrorKind::PermissionDenied => AviaryError::ReadPermissionDenied(location.to_owned()),
|
io::ErrorKind::PermissionDenied => AviaryUploadError::ReadPermissionDenied(location.to_owned()),
|
||||||
_ => panic!(
|
_ => panic!(
|
||||||
"Received an error kind that should be impossible for {}: {:?}",
|
"Received an error kind that should be impossible for {}: {:?}",
|
||||||
location.display(),
|
location.display(),
|
||||||
|
@ -65,36 +65,36 @@ impl AviaryError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_upload_error(err: ureq::Error) -> AviaryError {
|
pub fn from_upload_error(err: ureq::Error) -> AviaryUploadError {
|
||||||
match err {
|
match err {
|
||||||
ureq::Error::Status(code, msg) => AviaryError::ServerError(
|
ureq::Error::Status(code, msg) => AviaryUploadError::ServerError(
|
||||||
format!("Error code {} received from server: {}", code,
|
format!("Error code {} received from server: {}", code,
|
||||||
msg.into_string().unwrap_or(String::new()))),
|
msg.into_string().unwrap_or(String::new()))),
|
||||||
ureq::Error::Transport(transport) => match transport.kind() {
|
ureq::Error::Transport(transport) => match transport.kind() {
|
||||||
ureq::ErrorKind::InvalidUrl =>
|
ureq::ErrorKind::InvalidUrl =>
|
||||||
AviaryError::BadServerParameter,
|
AviaryUploadError::BadServerParameter,
|
||||||
ureq::ErrorKind::Dns =>
|
ureq::ErrorKind::Dns =>
|
||||||
AviaryError::ConnectionError(
|
AviaryUploadError::ConnectionError(
|
||||||
format!("DNS issue: {}", transport.message().unwrap_or(""))),
|
format!("DNS issue: {}", transport.message().unwrap_or(""))),
|
||||||
ureq::ErrorKind::Io =>
|
ureq::ErrorKind::Io =>
|
||||||
AviaryError::ConnectionError(
|
AviaryUploadError::ConnectionError(
|
||||||
format!("IO issue: {}", transport.message().unwrap_or(""))),
|
format!("IO issue: {}", transport.message().unwrap_or(""))),
|
||||||
ureq::ErrorKind::ConnectionFailed =>
|
ureq::ErrorKind::ConnectionFailed =>
|
||||||
AviaryError::ConnectionError(
|
AviaryUploadError::ConnectionError(
|
||||||
format!("Connection issue: {}", transport.message().unwrap_or(""))),
|
format!("Connection issue: {}", transport.message().unwrap_or(""))),
|
||||||
ureq::ErrorKind::TooManyRedirects =>
|
ureq::ErrorKind::TooManyRedirects =>
|
||||||
AviaryError::ServerError("Too many redirects".to_owned()),
|
AviaryUploadError::ServerError("Too many redirects".to_owned()),
|
||||||
ureq::ErrorKind::BadHeader =>
|
ureq::ErrorKind::BadHeader =>
|
||||||
AviaryError::ServerError("Invalid header from server".to_owned()),
|
AviaryUploadError::ServerError("Invalid header from server".to_owned()),
|
||||||
ureq::ErrorKind::BadStatus =>
|
ureq::ErrorKind::BadStatus =>
|
||||||
AviaryError::ServerError("Invalid status from server".to_owned()),
|
AviaryUploadError::ServerError("Invalid status from server".to_owned()),
|
||||||
unk => panic!("Unexpected transport error kind {unk}:\n{transport}")
|
unk => panic!("Unexpected transport error kind {unk}:\n{transport}")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for AviaryError {
|
impl fmt::Display for AviaryUploadError {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
writeln!(f, "\x1b[31mError!\x1b[0m\n")?;
|
writeln!(f, "\x1b[31mError!\x1b[0m\n")?;
|
||||||
match self {
|
match self {
|
||||||
|
@ -171,3 +171,208 @@ impl fmt::Display for AviaryError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum AviaryDownloadError {
|
||||||
|
/// Indicates that the key passed to the program was malformed
|
||||||
|
///
|
||||||
|
/// There are two possibities here:
|
||||||
|
/// - The key was not valid base 64
|
||||||
|
/// - The key was valid, but was not 32 bytes
|
||||||
|
///
|
||||||
|
/// Which of these two was the case is designated by the associated boolean. If
|
||||||
|
/// `true`, the key was invalid base64. If `false`, it was the wrong length.
|
||||||
|
///
|
||||||
|
/// Note that this error should only be used for keys passed to the program by the user. If a
|
||||||
|
/// key of the wrong length is listed in the index file, this should be considered a bad
|
||||||
|
///
|
||||||
|
/// Handling: Sugguest that the user check to make sure that they copied the base64
|
||||||
|
/// key correctly, and that it is encoded in base64.
|
||||||
|
MalformedKey(bool),
|
||||||
|
|
||||||
|
/// The server encountered an error or misbehaved
|
||||||
|
///
|
||||||
|
/// If this was due to a bad code, the associated data will be the error code, along
|
||||||
|
/// with the response from the server, as a String.
|
||||||
|
///
|
||||||
|
/// If this was due to misbehavior, there will be no code, and the String will be a
|
||||||
|
/// short explaination of what went wrong. Note that misbehavior includes any *unexpected*
|
||||||
|
/// behavior, not necessarily indicating that the server is broken.
|
||||||
|
///
|
||||||
|
/// Handling: Inform the user that something went wrong with the server, and that
|
||||||
|
/// they should try another instance and/or contact the instance admin.
|
||||||
|
ServerError(Option<u16>, String),
|
||||||
|
|
||||||
|
/// The filesystem denied access to a path with a permission denied error
|
||||||
|
///
|
||||||
|
/// This will either be permission denied to create a directory or permission denied to
|
||||||
|
/// create/write to a file
|
||||||
|
///
|
||||||
|
/// The associated path is the path for which permission was denied
|
||||||
|
///
|
||||||
|
/// Handling: Ask the user to check that they own the output directory and have
|
||||||
|
/// permission for it. Sugguest trying a different directory.
|
||||||
|
PermissionDenied(PathBuf),
|
||||||
|
|
||||||
|
/// Some uncommon filesystem error occured
|
||||||
|
///
|
||||||
|
/// This is a sort of wildcard error for filesystem errors which aren't likely to happen under
|
||||||
|
/// normal circumstances. Examples include:
|
||||||
|
/// - The disk is ejected while a write operation is ongoing
|
||||||
|
/// - The filesystem does not support creating directories
|
||||||
|
/// - The filesystem operation timed out
|
||||||
|
///
|
||||||
|
/// The associated error and path are the error that triggered this and the path the operation
|
||||||
|
/// occured for.
|
||||||
|
///
|
||||||
|
/// Handling: Assume the user is a more technical user and state as much. Give the error
|
||||||
|
/// details as well as the path. In case the user is not a technical user, provide them with a
|
||||||
|
/// link to get help / report a bug.
|
||||||
|
FilesystemError(io::Error, PathBuf),
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
|
||||||
|
/// Some network error prevented us from connecting to the server
|
||||||
|
///
|
||||||
|
/// No evidence exists to indicate that this was the fault of the server rather than
|
||||||
|
/// us.
|
||||||
|
///
|
||||||
|
/// Handling: Ask the user to validate that they can connect to the internet, and
|
||||||
|
/// sugguest that they try manually entering the URL into the browser to see if it
|
||||||
|
/// works.
|
||||||
|
ConnectionError(String),
|
||||||
|
|
||||||
|
/// Missing or expired index
|
||||||
|
///
|
||||||
|
/// Indicates that the server reported that the index file did not exist (404). This could be
|
||||||
|
/// due to using the wrong server URL or miscopying the file/index ID. A similar error can
|
||||||
|
/// also exist when the server has expired the index file and it has not yet been overwritten
|
||||||
|
/// by a new file.
|
||||||
|
///
|
||||||
|
/// Handling: Ask that the user check to make sure they copied the URL correctly and that
|
||||||
|
/// they've specified the right backend server URL. If they have, alert them that it's likely
|
||||||
|
/// that the gallery has expired.
|
||||||
|
MissingIndex,
|
||||||
|
|
||||||
|
/// One (or more) of the images in the gallery has expired
|
||||||
|
///
|
||||||
|
/// Occurs when the index points to a URL which the server claims does not exist. This almost
|
||||||
|
/// always indicates that the image has expired. Other possibilities include:
|
||||||
|
/// - The index was incorrect even *before* it was encrypted and uploaded
|
||||||
|
/// - The key was compramised, and the server took advantage of this to substitute its own
|
||||||
|
/// index file, but then gave faulty urls?? don't know why they'd do that
|
||||||
|
/// - ???
|
||||||
|
///
|
||||||
|
/// Handling: Ask that the user check to make sure they copied the URL correctly and that
|
||||||
|
/// they've specified the right backend server URL. If they have, alert them that it's likely
|
||||||
|
/// that the gallery has expired.
|
||||||
|
ExpiredImage,
|
||||||
|
|
||||||
|
/// Indicates that there was a mismatch between the key and the cyphertext
|
||||||
|
///
|
||||||
|
/// This could mean that the key was correct, but the cyphertext was invalid (like if it wasn't
|
||||||
|
/// encrypted data at all), or it could mean that the key (although still 32 bytes of base64)
|
||||||
|
/// didn't match up with the cyphertext.
|
||||||
|
///
|
||||||
|
/// An important reason this might occur is if the gallery expired and the server re-assigned
|
||||||
|
/// the ID to another file, and both used the .bin extension.
|
||||||
|
///
|
||||||
|
/// Handling: If this occurs for an image where the URL and key are both provided by the
|
||||||
|
/// index, it should always be interpreted as an expiration. If it occurs for an index, ask
|
||||||
|
/// the user to verify that they key and file ID were copied correctly, and if they are
|
||||||
|
/// correct, then the gallery expired.
|
||||||
|
KeyMismatch,
|
||||||
|
|
||||||
|
/// Indicates that the index did not conform to the protobuf spec
|
||||||
|
///
|
||||||
|
/// This is perhaps one of the least likely errors to occur in typical usage. This indicates
|
||||||
|
/// that the index violated some basic guarantee in some way. This typically corresponds to a
|
||||||
|
/// parse error. However, this isn't simple corruption, as the index was
|
||||||
|
/// still correctly encrypted and the encryption tag matched up.
|
||||||
|
///
|
||||||
|
/// Some examples for events which could trigger this error:
|
||||||
|
/// - The index is not valid protobuf
|
||||||
|
/// - The index specifies a key which is not 32 bytes in length
|
||||||
|
/// - The index specified a file ID which contains illegal characters
|
||||||
|
///
|
||||||
|
/// The most likely explanations for this are as follows:
|
||||||
|
/// - The key/file id pair provided to program are valid, but point to an image rather than a
|
||||||
|
/// gallery. This is unlikely because this information is never exposed to the user.
|
||||||
|
/// - These are valid key/file id pairs, but are intended for a different project which uses
|
||||||
|
/// the same encryption scheme, stores different data / doesn't use a protobuf index.
|
||||||
|
/// - A very poorly behaved client generated this index
|
||||||
|
///
|
||||||
|
/// The associated str will provide a more specific explanation for exactly what went wrong.
|
||||||
|
///
|
||||||
|
/// Handling: This is incredibly unlikely to happen to an unsuspecting user. Provide the user
|
||||||
|
/// with a detailed technical description of what happened, as well as a link to report a bug,
|
||||||
|
/// in the event that they are a non-technical user who, against all odds, encountered this
|
||||||
|
/// problem.
|
||||||
|
CorruptIndex(&'static str),
|
||||||
|
|
||||||
|
/// A file already exists in the destination folder and we don't want to overwrite it
|
||||||
|
///
|
||||||
|
/// Handling: Alert the user that the file exists and that we won't overwrite it. Their
|
||||||
|
/// options are:
|
||||||
|
/// - Choose a new destination directory
|
||||||
|
/// - Remove the existing files
|
||||||
|
/// - Add the `--force/-f` option to force the program to overwrite the files
|
||||||
|
NotOverwriting(PathBuf),
|
||||||
|
|
||||||
|
/// Cancelled
|
||||||
|
///
|
||||||
|
/// The user cancelled the operation, e.g. C^c
|
||||||
|
///
|
||||||
|
/// Handling: Exit politely and state the reason
|
||||||
|
Cancelled,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AviaryDownloadError {
|
||||||
|
pub fn from_download_error(
|
||||||
|
is_index: bool
|
||||||
|
) -> impl Fn(ureq::Error) -> AviaryDownloadError {
|
||||||
|
move|err: ureq::Error| {
|
||||||
|
match err {
|
||||||
|
ureq::Error::Status(code, msg) => match (code/100, code%100) {
|
||||||
|
(4, 04) =>
|
||||||
|
if is_index {
|
||||||
|
AviaryDownloadError::MissingIndex
|
||||||
|
} else {
|
||||||
|
AviaryDownloadError::ExpiredImage
|
||||||
|
}
|
||||||
|
_ => AviaryDownloadError::ServerError(
|
||||||
|
Some(code),
|
||||||
|
msg.into_string().unwrap_or(String::new())
|
||||||
|
),
|
||||||
|
},
|
||||||
|
ureq::Error::Transport(transport) => match transport.kind() {
|
||||||
|
ureq::ErrorKind::InvalidUrl =>
|
||||||
|
if is_index {
|
||||||
|
AviaryDownloadError::BadServerParameter
|
||||||
|
} else {
|
||||||
|
AviaryDownloadError::CorruptIndex("Index lists bad characters in file ID")
|
||||||
|
},
|
||||||
|
ureq::ErrorKind::Dns =>
|
||||||
|
AviaryDownloadError::ConnectionError(
|
||||||
|
format!("DNS issue: {}", transport.message().unwrap_or(""))),
|
||||||
|
ureq::ErrorKind::Io =>
|
||||||
|
AviaryDownloadError::ConnectionError(
|
||||||
|
format!("IO issue: {}", transport.message().unwrap_or(""))),
|
||||||
|
ureq::ErrorKind::ConnectionFailed =>
|
||||||
|
AviaryDownloadError::ConnectionError(
|
||||||
|
format!("Connection issue: {}", transport.message().unwrap_or(""))),
|
||||||
|
ureq::ErrorKind::TooManyRedirects =>
|
||||||
|
AviaryDownloadError::ServerError(Some(302), "Too many redirects".to_owned()),
|
||||||
|
ureq::ErrorKind::BadHeader =>
|
||||||
|
AviaryDownloadError::ServerError(None, "Invalid header from server".to_owned()),
|
||||||
|
ureq::ErrorKind::BadStatus =>
|
||||||
|
AviaryDownloadError::ServerError(None, "Invalid status from server".to_owned()),
|
||||||
|
unk => panic!("Unexpected transport error kind {unk}:\n{transport}")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
78
src/main.rs
78
src/main.rs
|
@ -6,16 +6,17 @@ mod protobuf;
|
||||||
mod errors;
|
mod errors;
|
||||||
|
|
||||||
use std::{borrow::Cow, io::stdout};
|
use std::{borrow::Cow, io::stdout};
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write, self};
|
||||||
use std::fs::{File, self, OpenOptions};
|
use std::fs::{File, self, OpenOptions};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use errors::AviaryError;
|
use errors::AviaryUploadError;
|
||||||
use itertools::{Itertools, Either};
|
use itertools::{Itertools, Either};
|
||||||
use parse::{CreateArgs, Command, DownloadArgs};
|
use parse::{CreateArgs, Command, DownloadArgs};
|
||||||
use ::protobuf::Message;
|
use ::protobuf::Message;
|
||||||
use sanitise_file_name::sanitise_with_options;
|
use sanitise_file_name::sanitise_with_options;
|
||||||
|
|
||||||
|
use crate::errors::AviaryDownloadError;
|
||||||
use crate::protobuf::image::Format;
|
use crate::protobuf::image::Format;
|
||||||
|
|
||||||
fn trim_url<'a>(base_url: &str, url: &'a str) -> Option<&'a str> {
|
fn trim_url<'a>(base_url: &str, url: &'a str) -> Option<&'a str> {
|
||||||
|
@ -45,7 +46,7 @@ fn main() {
|
||||||
};
|
};
|
||||||
match args.command {
|
match args.command {
|
||||||
Command::Create(create_args) => create(&*full_server, create_args),
|
Command::Create(create_args) => create(&*full_server, create_args),
|
||||||
Command::Download(download_args) => download(&*full_server, download_args),
|
Command::Download(download_args) => download(&*full_server, download_args).unwrap(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,7 +56,7 @@ fn create(server: &str, args: CreateArgs) {
|
||||||
.map(Path::new)
|
.map(Path::new)
|
||||||
.map(|path| File::open(path)
|
.map(|path| File::open(path)
|
||||||
.map(|file| (path, file))
|
.map(|file| (path, file))
|
||||||
.map_err(|e| AviaryError::from_open_error(e.kind(), &path))
|
.map_err(|e| AviaryUploadError::from_open_error(e.kind(), &path))
|
||||||
)
|
)
|
||||||
.partition_result();
|
.partition_result();
|
||||||
|
|
||||||
|
@ -63,8 +64,8 @@ fn create(server: &str, args: CreateArgs) {
|
||||||
println!(" \x1b[31mError!\x1b[0m");
|
println!(" \x1b[31mError!\x1b[0m");
|
||||||
let (nonexistant, noread): (Vec<_>, Vec<_>) = errors.iter()
|
let (nonexistant, noread): (Vec<_>, Vec<_>) = errors.iter()
|
||||||
.partition_map(|e| match e {
|
.partition_map(|e| match e {
|
||||||
AviaryError::FileDNE(path) => Either::Left(path),
|
AviaryUploadError::FileDNE(path) => Either::Left(path),
|
||||||
AviaryError::ReadPermissionDenied(path) => Either::Right(path),
|
AviaryUploadError::ReadPermissionDenied(path) => Either::Right(path),
|
||||||
other => panic!("This error should not be possible! {other:?}")
|
other => panic!("This error should not be possible! {other:?}")
|
||||||
});
|
});
|
||||||
if !nonexistant.is_empty() {
|
if !nonexistant.is_empty() {
|
||||||
|
@ -86,13 +87,13 @@ fn create(server: &str, args: CreateArgs) {
|
||||||
let mut buff = Vec::with_capacity(file.metadata()?.len() as usize);
|
let mut buff = Vec::with_capacity(file.metadata()?.len() as usize);
|
||||||
file.read_to_end(&mut buff)?;
|
file.read_to_end(&mut buff)?;
|
||||||
Ok((path, buff))
|
Ok((path, buff))
|
||||||
})().map_err(|e| AviaryError::StreamReadError(path.to_owned(), e))
|
})().map_err(|e| AviaryUploadError::StreamReadError(path.to_owned(), e))
|
||||||
)
|
)
|
||||||
.inspect(|r| if r.is_ok() { print!("\x1b[32mDone!\n\x1b[37m├─\x1b[0m Thumbnailing... ") })
|
.inspect(|r| if r.is_ok() { print!("\x1b[32mDone!\n\x1b[37m├─\x1b[0m Thumbnailing... ") })
|
||||||
.inspect(|_| drop(stdout().flush()))
|
.inspect(|_| drop(stdout().flush()))
|
||||||
.map(|r| r.and_then(|(path, raw_dat)| {
|
.map(|r| r.and_then(|(path, raw_dat)| {
|
||||||
let (full, thumbnail, blurhash, format) = thumbnailing::thumbnail(&raw_dat)
|
let (full, thumbnail, blurhash, format) = thumbnailing::thumbnail(&raw_dat)
|
||||||
.map_err(|_| AviaryError::ImageFormatError(path.to_owned()))?;
|
.map_err(|_| AviaryUploadError::ImageFormatError(path.to_owned()))?;
|
||||||
Ok((full.map(Either::Right).unwrap_or(Either::Left(raw_dat)), thumbnail, blurhash, format))
|
Ok((full.map(Either::Right).unwrap_or(Either::Left(raw_dat)), thumbnail, blurhash, format))
|
||||||
}))
|
}))
|
||||||
.inspect(|r| if r.is_ok() { print!("\x1b[32mDone!\n\x1b[37m├─\x1b[0m Encrypting... ")})
|
.inspect(|r| if r.is_ok() { print!("\x1b[32mDone!\n\x1b[37m├─\x1b[0m Encrypting... ")})
|
||||||
|
@ -114,7 +115,7 @@ fn create(server: &str, args: CreateArgs) {
|
||||||
.and_then(|thumb_url|
|
.and_then(|thumb_url|
|
||||||
upload::put_data(&agent, server, &full_img)
|
upload::put_data(&agent, server, &full_img)
|
||||||
.map(|full_url| (key, full_url, thumb_url, blurhash, format)))
|
.map(|full_url| (key, full_url, thumb_url, blurhash, format)))
|
||||||
.map_err(AviaryError::from_upload_error)
|
.map_err(AviaryUploadError::from_upload_error)
|
||||||
))
|
))
|
||||||
.map(|r| r.and_then(|(key, full_url, thumb_url, blurhash, format)| {
|
.map(|r| r.and_then(|(key, full_url, thumb_url, blurhash, format)| {
|
||||||
let full_trimmed = trim_url(server, &full_url);
|
let full_trimmed = trim_url(server, &full_url);
|
||||||
|
@ -122,7 +123,7 @@ fn create(server: &str, args: CreateArgs) {
|
||||||
if let (Some(full_url), Some(thmb_url)) = (full_trimmed, thmb_trimmed) {
|
if let (Some(full_url), Some(thmb_url)) = (full_trimmed, thmb_trimmed) {
|
||||||
Ok((key, full_url.to_owned(), thmb_url.to_owned(), blurhash, format))
|
Ok((key, full_url.to_owned(), thmb_url.to_owned(), blurhash, format))
|
||||||
} else {
|
} else {
|
||||||
Err(AviaryError::ServerError(format!("Received bad response from server: {}", full_url)))
|
Err(AviaryUploadError::ServerError(format!("Received bad response from server: {}", full_url)))
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.inspect(|r| if r.is_ok() { println!("\x1b[32mDone!\n")})
|
.inspect(|r| if r.is_ok() { println!("\x1b[32mDone!\n")})
|
||||||
|
@ -138,7 +139,7 @@ fn create(server: &str, args: CreateArgs) {
|
||||||
images: image_info,
|
images: image_info,
|
||||||
title: args.title,
|
title: args.title,
|
||||||
desc: None,
|
desc: None,
|
||||||
special_fields: Default::default()
|
special_fields: Default::default(),
|
||||||
};
|
};
|
||||||
let index_key = crypto::make_key();
|
let index_key = crypto::make_key();
|
||||||
let encrypted_index = crypto::encrypt(
|
let encrypted_index = crypto::encrypt(
|
||||||
|
@ -149,7 +150,7 @@ fn create(server: &str, args: CreateArgs) {
|
||||||
let encoded_key = base64::encode(index_key);
|
let encoded_key = base64::encode(index_key);
|
||||||
print!("\x1b[0mUploading index... ");
|
print!("\x1b[0mUploading index... ");
|
||||||
upload::put_data(&agent, server, &encrypted_index)
|
upload::put_data(&agent, server, &encrypted_index)
|
||||||
.map_err(|e| AviaryError::from_upload_error(e))
|
.map_err(|e| AviaryUploadError::from_upload_error(e))
|
||||||
.map(|url| format!("{}#{}", url.trim().trim_end_matches(".bin"), &encoded_key))
|
.map(|url| format!("{}#{}", url.trim().trim_end_matches(".bin"), &encoded_key))
|
||||||
});
|
});
|
||||||
match index_url {
|
match index_url {
|
||||||
|
@ -178,28 +179,33 @@ const SANITIZATION_OPTIONS: sanitise_file_name::Options<fn(char) -> Option<char>
|
||||||
six_measures_of_barley: "Unnamed-Album"
|
six_measures_of_barley: "Unnamed-Album"
|
||||||
};
|
};
|
||||||
|
|
||||||
fn download(server: &str, args: DownloadArgs) {
|
fn download(server: &str, args: DownloadArgs) -> Result<(), AviaryDownloadError> {
|
||||||
let mut download_buffer = Vec::with_capacity(5_000_000);
|
let mut download_buffer = Vec::with_capacity(5_000_000);
|
||||||
let mut decrypt_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 index_url = format!("{}/{}.bin", server, args.id);
|
||||||
let key: [u8; 32] = base64::decode(args.key)
|
let key: [u8; 32] = base64::decode(args.key)
|
||||||
.expect("key is malformed b64")
|
.map_err(|_| AviaryDownloadError::MalformedKey(true))?
|
||||||
.try_into()
|
.try_into()
|
||||||
.expect("key is the wrong size");
|
.map_err(|_| AviaryDownloadError::MalformedKey(false))?;
|
||||||
let download_agent = upload::get_agent();
|
let download_agent = upload::get_agent();
|
||||||
let encrypted_index = upload::get_data(&download_agent, &index_url, 500_000, &mut download_buffer)
|
let encrypted_index = upload::get_data(&download_agent, &index_url, 500_000, &mut download_buffer)
|
||||||
.expect("GET failed");
|
.map_err(AviaryDownloadError::from_download_error(true))?;
|
||||||
let serialized_index = crypto::decrypt(&key, &encrypted_index, &mut decrypt_buffer)
|
let serialized_index = crypto::decrypt(&key, &encrypted_index, &mut decrypt_buffer)
|
||||||
.expect("wrong key or corrupt index");
|
.ok_or(AviaryDownloadError::KeyMismatch)?;
|
||||||
let index = protobuf::index::Index::parse_from_bytes(&serialized_index)
|
let index = protobuf::index::Index::parse_from_bytes(&serialized_index)
|
||||||
.expect("malformed index");
|
.map_err(|_| AviaryDownloadError::CorruptIndex("Index was not a valid protobuf structure"))?;
|
||||||
|
println!("{index:?}");
|
||||||
|
|
||||||
let dest_dir: Cow<Path> = args.output.map(Cow::Owned)
|
let dest_dir: Cow<Path> = args.output.map(Cow::Owned)
|
||||||
.unwrap_or_else(||
|
.unwrap_or_else(||
|
||||||
index.title.map(|title|
|
index.title.map(|title|
|
||||||
Cow::Owned(sanitise_with_options(&title, &SANITIZATION_OPTIONS).into()))
|
Cow::Owned(sanitise_with_options(&title, &SANITIZATION_OPTIONS).into()))
|
||||||
.unwrap_or(Cow::Borrowed("Unnamed-Album".as_ref())));
|
.unwrap_or(Cow::Borrowed("Unnamed-Album".as_ref())));
|
||||||
fs::create_dir_all(&dest_dir).expect("Failed to create destination directory");
|
fs::create_dir_all(&dest_dir).map_err(|e| match e.kind() {
|
||||||
|
io::ErrorKind::PermissionDenied =>
|
||||||
|
AviaryDownloadError::PermissionDenied(dest_dir.as_ref().to_owned()),
|
||||||
|
_ => AviaryDownloadError::FilesystemError(e, dest_dir.as_ref().to_owned()),
|
||||||
|
})?;
|
||||||
for (indx, image) in index.images.into_iter().enumerate() {
|
for (indx, image) in index.images.into_iter().enumerate() {
|
||||||
let extension = image.format.enum_value().map(|f| match f {
|
let extension = image.format.enum_value().map(|f| match f {
|
||||||
Format::PNG => "png",
|
Format::PNG => "png",
|
||||||
|
@ -207,26 +213,46 @@ fn download(server: &str, args: DownloadArgs) {
|
||||||
Format::AVIF => "avif",
|
Format::AVIF => "avif",
|
||||||
Format::JPG => "jpg",
|
Format::JPG => "jpg",
|
||||||
Format::GIF => "gif",
|
Format::GIF => "gif",
|
||||||
}).unwrap_or(".unk");
|
}).unwrap_or("unk");
|
||||||
let path = dest_dir.join(format!("{indx:03}.{extension}"));
|
let path = dest_dir.join(format!("{indx:03}.{extension}"));
|
||||||
let mut dest_file = OpenOptions::new()
|
let mut dest_file = OpenOptions::new()
|
||||||
.write(true)
|
.write(true)
|
||||||
.truncate(true)
|
.truncate(true)
|
||||||
.create(true)
|
.create(true)
|
||||||
.open(path)
|
.open(&path)
|
||||||
.expect("Failed to open");
|
.map_err(|e| match e.kind() {
|
||||||
let key = image.key.try_into().expect("Invalid key size");
|
io::ErrorKind::PermissionDenied =>
|
||||||
|
AviaryDownloadError::PermissionDenied(path.clone()),
|
||||||
|
io::ErrorKind::AlreadyExists =>
|
||||||
|
AviaryDownloadError::NotOverwriting(path.clone()),
|
||||||
|
io::ErrorKind::Interrupted =>
|
||||||
|
AviaryDownloadError::Cancelled,
|
||||||
|
_ => AviaryDownloadError::FilesystemError(e, path.clone())
|
||||||
|
})?;
|
||||||
|
let key = image.key.try_into()
|
||||||
|
.map_err(|_|
|
||||||
|
AviaryDownloadError::CorruptIndex("Key of invalid length specified in index")
|
||||||
|
)?;
|
||||||
let encrypted_thumbnail = upload::get_data(
|
let encrypted_thumbnail = upload::get_data(
|
||||||
&download_agent,
|
&download_agent,
|
||||||
&format!("{}/{}.bin", server, image.full_url),
|
&format!("{}/{}.bin", server, image.full_url),
|
||||||
7680_000,
|
7680_000,
|
||||||
&mut download_buffer
|
&mut download_buffer
|
||||||
).expect("Failed to retrieve image data");
|
).map_err(AviaryDownloadError::from_download_error(false))?;
|
||||||
let decrypted_thumb = crypto::decrypt(
|
let decrypted_thumb = crypto::decrypt(
|
||||||
&key,
|
&key,
|
||||||
encrypted_thumbnail,
|
encrypted_thumbnail,
|
||||||
&mut decrypt_buffer
|
&mut decrypt_buffer
|
||||||
).expect("Invalid image data referenced by index or bad key in index");
|
).ok_or(AviaryDownloadError::ExpiredImage)?;
|
||||||
dest_file.write_all(&decrypted_thumb).expect("Failed to write to disk");
|
dest_file.write_all(&decrypted_thumb)
|
||||||
|
.map_err(|e| match e.kind() {
|
||||||
|
io::ErrorKind::PermissionDenied =>
|
||||||
|
AviaryDownloadError::PermissionDenied(path.clone()),
|
||||||
|
io::ErrorKind::Interrupted =>
|
||||||
|
AviaryDownloadError::Cancelled,
|
||||||
|
_ => AviaryDownloadError::FilesystemError(e, path.clone())
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue