aviary-cli/src/errors.rs

503 lines
20 KiB
Rust

use core::fmt;
use std::{path::{PathBuf, Path}, io};
#[derive(Debug)]
pub enum AviaryUploadError {
/// 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,
}
impl AviaryUploadError {
pub fn from_open_error(e: io::ErrorKind, location: &Path) -> AviaryUploadError {
match e {
io::ErrorKind::NotFound => AviaryUploadError::FileDNE(location.to_owned()),
io::ErrorKind::PermissionDenied => AviaryUploadError::ReadPermissionDenied(location.to_owned()),
_ => panic!(
"Received an error kind that should be impossible for {}: {:?}",
location.display(),
e
)
}
}
pub fn from_upload_error(err: ureq::Error) -> AviaryUploadError {
match err {
ureq::Error::Status(code, msg) => AviaryUploadError::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 =>
AviaryUploadError::BadServerParameter,
ureq::ErrorKind::Dns =>
AviaryUploadError::ConnectionError(
format!("DNS issue: {}", transport.message().unwrap_or(""))),
ureq::ErrorKind::Io =>
AviaryUploadError::ConnectionError(
format!("IO issue: {}", transport.message().unwrap_or(""))),
ureq::ErrorKind::ConnectionFailed =>
AviaryUploadError::ConnectionError(
format!("Connection issue: {}", transport.message().unwrap_or(""))),
ureq::ErrorKind::TooManyRedirects =>
AviaryUploadError::ServerError("Too many redirects".to_owned()),
ureq::ErrorKind::BadHeader =>
AviaryUploadError::ServerError("Invalid header from server".to_owned()),
ureq::ErrorKind::BadStatus =>
AviaryUploadError::ServerError("Invalid status from server".to_owned()),
unk => panic!("Unexpected transport error kind {unk}:\n{transport}")
},
}
}
}
impl fmt::Display for AviaryUploadError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "\x1b[31mError!\x1b[0m\n")?;
match self {
Self::FileDNE(path) =>
writeln!(f, "The file at \x1b[36m{}\x1b[0m does not exist", path.display()),
Self::ReadPermissionDenied(path) =>
writeln!(f, "Permission denied for the file \x1b[36m{}\x1b[0m", path.display()),
Self::StreamReadError(path, err) =>
writeln!(f,
concat!(
"Something bad happened while we were reading from ",
"\x1b[36m{}\x1b[0m. This could be caused by removing a ",
"drive/phone without unplugging it, or several other possible ",
"things. This error might give you a clue:\n\n{}"
),
path.display(), err
),
Self::ImageFormatError(path) =>
writeln!(f,
concat!(
"We had a little bit of trouble understanding the image at ",
"\x1b[36m{}\x1b[0m. Normally, this indicates that it might not be ",
"an image file (e.g. if it's a video instead). If you're sure that ",
"it's an image, this could indicate that it's corrupted or of an ",
"unsupported format.\n\n",
"\x1b[34mSupported formats:\x1b[0m\n",
" - gif\n",
" - jpg\n",
" - ico\n",
" - png\n",
" - tiff\n",
" - webp\n",
" - bmp\n",
" - hdr",
),
path.display(),
),
Self::ServerError(msg) =>
writeln!(f,
concat!(
"The server seems to be down or misbehaving at the minute. ",
"Normally, this issue should resolve by itself once the server ",
"maintainers identify the problem and fix it.\n\n",
"If the issue persists, please submit an issue on the bug ",
"tracker. This error message may help to identify the problem:",
"\n\n\x1b[31m{}\x1b[0m",
),
msg,
),
Self::ConnectionError(msg) =>
writeln!(f,
concat!(
"There was an issue connecting to the server! Check that your ",
"internet is online, and you can access websites normally. ",
"This information might help diagnose the issue:\n\n",
"\x1b[31m{}\x1b[0m",
),
msg,
),
Self::BadServerParameter =>
writeln!(f,
concat!(
"The server URL you provided was \x1b[31minvalid\x1b[0m! ",
"Please check to make sure that it is properly formatted, like ",
"\x1b[1m0x0.st\x1b[0m or \x1b[1menvs.sh\x1b[0m.",
),
),
}
}
}
#[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.
///
/// Note: If there is a key mismatch for an image in a gallery, rather than on the index, this
/// should be interpretted as an expiration instead, and should use the [`ExpiredImage`][]
/// variant.
///
/// Handling: 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}")
},
}
}}
}
impl fmt::Display for AviaryDownloadError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let AviaryDownloadError::Cancelled = self {
writeln!(f, "\x1b[31mCancelled\x1b[0m")?;
} else {
writeln!(f, "\x1b[31mError!\x1b[0m\n")?;
}
match self {
AviaryDownloadError::MalformedKey(invalid) =>
write!(f, concat!(
"It looks like that key {} Double check to make sure that you ",
"copied it correctly. If you did, ask the person who sent you the ",
"link to make sure that /they/ copied it correctly."
), if *invalid {
"isn't valid base64, and won't work here."
} else {
"is the wrong length!"
}),
AviaryDownloadError::ServerError(err_code, msg) =>
write!(f, concat!(
"The server storing this gallery seems to be behaving in a way we ",
"didn't expect. Double check to make sure that you've set the ",
"correct instance with \x1b[35m-s\x1b[0m/\x1b[35m--server\x1b[0m, ",
"and if the problem persists, reach out the server administrator to ",
"let them know that something went wrong.\n\n",
"This information might help to diagnose the issue:\n\x1b[31m{}",
), if let Some(err_code) = err_code {
format!("Error code {:03}:\n{}", err_code, &msg[..usize::min(50, msg.len())])
} else {
format!("Server misbehaviour: {}", msg)
}),
AviaryDownloadError::PermissionDenied(path) =>
write!(f, concat!(
"It looks like you don't have permission for the directory you're ",
"trying to download this album into. Specifically, permission was ",
"denied for \x1b[36m{}\x1b[0m\n\n",
"Try acquiring permission, or picking a different directory with ",
"\x1b[35m-ox1b[0m/x1b[35m--outputx1b[0m."
), path.display()),
AviaryDownloadError::FilesystemError(err, path) =>
write!(f, concat!(
"Some uncommon filesystem error happened while we were trying to ",
"write to/create \x1b[36m{}\x1b[0m. This shouldn't happen very ",
"often, but here's a few guesses as to what might have happened:\n",
"\x1b[37m- \x1b[0mA disk was ejected while we were saving a file\n",
"\x1b[37m- \x1b[0mYou're using a special filesystem that ",
"encountered an error\n",
"\x1b[37m- \x1b[0mThe disk you were writing to is read only\n\n",
"If you're getting this error and you don't know why, we'd really ",
"appreciate it if you could open an issue on our bug tracker. Even ",
"just the filesystem you were using and the error you got could be ",
"enough to help.\n\n",
"The full error that we got is:\n\x1b[31m{:?}",
), path.display(), err),
AviaryDownloadError::BadServerParameter =>
write!(f, concat!(
"Oops! The server you entered looks like it might not be correct. ",
"Double check that your \x1b[35m-s\x1b[0m/\x1b[35m--server\x1b[0m ",
"parameter is correct"
)),
AviaryDownloadError::ConnectionError(msg) =>
write!(f, concat!(
"There was an issue connecting to the server! Check that your ",
"internet is online, and you can access websites normally. ",
"This information might help diagnose the issue:\n\n",
"\x1b[31m{}\x1b[0m"
), msg),
AviaryDownloadError::MissingIndex =>
write!(f, concat!(
"No album found at this URL! Double check that the file ID you ",
"entered is correct and that you've specified the right instance ",
"using \x1b[35m-s\x1b[0m/\x1b[35m--server\x1b[0m. If you have, this ",
"album may have expired."
)),
AviaryDownloadError::ExpiredImage =>
write!(f, concat!(
"One or more images in this album have expired. Currently, the ",
"command line tool has no support for downloading partially expired ",
"albums, although this may change in the future. Please contact the ",
"original uploader to get a new album link."
)),
AviaryDownloadError::KeyMismatch =>
write!(f, concat!(
"Wrong key! Either the base64 key that you entered isn't right, or ",
"it doesn't go to this album. Double check that you've copied both ",
"the album ID and the key correctly. If you're not using the main ",
"instance, you should also double check that you've set the ",
"\x1b[35m-s\x1b[0m/\x1b[35m--server\x1b[0m. If all of this is ",
"correct, then the album may be expired."
)),
AviaryDownloadError::CorruptIndex(msg) =>
write!(f, concat!(
"Something very strange has occurred. \x1b[31;1mIf you are a ",
"non-technical user, feel free to stop here and open an issue, and ",
"we can help figure out why this happened and how we can stop it ",
"from happening in the future.\x1b[0m If you are a technical user, ",
"read on to learn what happened.\n\n",
"The index which stores a listing of the files in this album (along ",
"with metadata like the title and description) was corrupt. ",
"However, the decryption was succeeded, including the MAC ",
"checking. This could stem from a couple possibilities:\n",
"\x1b[37m- \x1b[0mThe key/file id pair provided was valid, but ",
"points to an image rather than a gallery\n",
"\x1b[37m- \x1b[0mA very poorly behaved client generated this ",
"album, and produced a corrupt index\n",
"\x1b[37m- \x1b[0mThis is a key/file pair which is meant to be ",
"used with a different project with a very similar encryption scheme\n\n",
"The specific error is as follows:\n\x1b[31m{}"
), msg),
AviaryDownloadError::NotOverwriting(path) =>
write!(f, concat!(
"The file \x1b[36m{}\x1b[0m already exists! To keep your data ",
"safe, we won't overwrite it unless you pass the ",
"\x1b[35m-f\x1b[0m/\x1b[35m--force\x1b[0m flag. Consider removing ",
"the file yourself or choosing a new directory with the ",
"\x1b[35m-o\x1b[0m/\x1b[35m--output\x1b[0m flag.",
), path.display()),
AviaryDownloadError::Cancelled => Ok(()),
}
}
}