503 lines
20 KiB
Rust
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(()),
|
|
}
|
|
}
|
|
}
|