aviary-cli/src/errors.rs

379 lines
15 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.
///
/// 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}")
},
}
}}
}