Specified API errors

This commit is contained in:
Antoine Gersant 2022-11-21 21:37:47 -08:00
parent 1484ecabe9
commit eaec68dff0
5 changed files with 140 additions and 79 deletions

View file

@ -15,6 +15,8 @@
- Additional metadata fields are now indexed: lyricist, composer, genre and label (thanks @pmphfm)
- Endpoints returning thumbnail images or audio files no longer use HTTP `content-encoding`
- The `/thumbnail` endpoint now supports an optional parameter for small/large/native image sizing. (thanks @Saecki)
- Log file now contain more details about the cause of failed HTTP requests (3xx, 4xx, 5xx)
- Startup errors emit clearer messages
### Web client

View file

@ -8,12 +8,12 @@ use crate::db::{self, misc_settings, DB};
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Auth secret does not have the expected format")]
AuthenticationSecretInvalid,
#[error("Missing auth secret")]
AuthSecretNotFound,
AuthenticationSecretNotFound,
#[error(transparent)]
DatabaseConnection(#[from] db::Error),
#[error("Auth secret does not have the expected format")]
InvalidAuthSecret,
#[error("Missing settings")]
MiscSettingsNotFound,
#[error("Index album art pattern is not a valid regex")]
@ -56,12 +56,12 @@ impl Manager {
.select(auth_secret)
.get_result(&mut connection)
.map_err(|e| match e {
diesel::result::Error::NotFound => Error::AuthSecretNotFound,
diesel::result::Error::NotFound => Error::AuthenticationSecretNotFound,
e => e.into(),
})?;
secret
.try_into()
.map_err(|_| Error::InvalidAuthSecret)
.map_err(|_| Error::AuthenticationSecretInvalid)
.map(|key| AuthSecret { key })
}

View file

@ -11,16 +11,16 @@ use crate::utils::{get_audio_format, AudioFormat};
pub enum Error {
#[error("No embedded artwork was found in `{0}`")]
EmbeddedArtworkNotFound(PathBuf),
#[error(transparent)]
Id3(#[from] id3::Error),
#[error(transparent)]
Image(#[from] image::error::ImageError),
#[error("Could not read thumbnail from ID3 tag in `{0}`:\n\n{1}")]
Id3(PathBuf, id3::Error),
#[error("Could not read thumbnail image in `{0}`:\n\n{1}")]
Image(PathBuf, image::error::ImageError),
#[error("Filesystem error for `{0}`: `{1}`")]
Io(PathBuf, std::io::Error),
#[error(transparent)]
Metaflac(#[from] metaflac::Error),
#[error(transparent)]
Mp4aMeta(#[from] mp4ameta::Error),
#[error("Could not read thumbnail from flac file in `{0}`:\n\n{1}")]
Metaflac(PathBuf, metaflac::Error),
#[error("Could not read thumbnail from mp4 file in `{0}`:\n\n{1}")]
Mp4aMeta(PathBuf, mp4ameta::Error),
#[error("This file format is not supported: {0}")]
UnsupportedFormat(&'static str),
}
@ -94,7 +94,9 @@ impl Manager {
let path = self.get_thumbnail_path(image_path, thumbnailoptions);
let mut out_file =
File::create(&path).map_err(|e| Error::Io(self.thumbnails_dir_path.clone(), e))?;
thumbnail.write_to(&mut out_file, ImageOutputFormat::Jpeg(quality))?;
thumbnail
.write_to(&mut out_file, ImageOutputFormat::Jpeg(quality))
.map_err(|e| Error::Image(image_path.to_owned(), e))?;
Ok(path)
}
@ -130,11 +132,13 @@ fn generate_thumbnail(image_path: &Path, options: &Options) -> Result<DynamicIma
out_dimension,
background,
));
final_image.copy_from(
&scaled_image,
(out_dimension - scaled_width) / 2,
(out_dimension - scaled_height) / 2,
)?;
final_image
.copy_from(
&scaled_image,
(out_dimension - scaled_width) / 2,
(out_dimension - scaled_height) / 2,
)
.map_err(|e| Error::Image(image_path.to_owned(), e))?;
} else {
final_image = source_image.thumbnail(out_dimension, out_dimension);
}
@ -153,7 +157,7 @@ fn read(image_path: &Path) -> Result<DynamicImage, Error> {
Some(AudioFormat::OGG) => read_vorbis(image_path),
Some(AudioFormat::OPUS) => read_opus(image_path),
Some(AudioFormat::WAVE) => read_wave(image_path),
None => Ok(image::open(image_path)?),
None => image::open(image_path).map_err(|e| Error::Image(image_path.to_owned(), e)),
}
}
@ -162,40 +166,44 @@ fn read_ape(_: &Path) -> Result<DynamicImage, Error> {
}
fn read_flac(path: &Path) -> Result<DynamicImage, Error> {
let tag = metaflac::Tag::read_from_path(path)?;
let tag =
metaflac::Tag::read_from_path(path).map_err(|e| Error::Metaflac(path.to_owned(), e))?;
if let Some(p) = tag.pictures().next() {
return Ok(image::load_from_memory(&p.data)?);
return image::load_from_memory(&p.data).map_err(|e| Error::Image(path.to_owned(), e));
}
Err(Error::EmbeddedArtworkNotFound(path.to_owned()))
}
fn read_mp3(path: &Path) -> Result<DynamicImage, Error> {
let tag = id3::Tag::read_from_path(path)?;
let tag = id3::Tag::read_from_path(path).map_err(|e| Error::Id3(path.to_owned(), e))?;
read_id3(path, &tag)
}
fn read_aiff(path: &Path) -> Result<DynamicImage, Error> {
let tag = id3::Tag::read_from_aiff_path(path)?;
let tag = id3::Tag::read_from_aiff_path(path).map_err(|e| Error::Id3(path.to_owned(), e))?;
read_id3(path, &tag)
}
fn read_wave(path: &Path) -> Result<DynamicImage, Error> {
let tag = id3::Tag::read_from_wav_path(path)?;
let tag = id3::Tag::read_from_wav_path(path).map_err(|e| Error::Id3(path.to_owned(), e))?;
read_id3(path, &tag)
}
fn read_id3(path: &Path, tag: &id3::Tag) -> Result<DynamicImage, Error> {
if let Some(p) = tag.pictures().next() {
return Ok(image::load_from_memory(&p.data)?);
}
Err(Error::EmbeddedArtworkNotFound(path.to_owned()))
tag.pictures()
.next()
.ok_or_else(|| Error::EmbeddedArtworkNotFound(path.to_owned()))
.and_then(|d| {
image::load_from_memory(&d.data).map_err(|e| Error::Image(path.to_owned(), e))
})
}
fn read_mp4(path: &Path) -> Result<DynamicImage, Error> {
let tag = mp4ameta::Tag::read_from_path(path)?;
let tag =
mp4ameta::Tag::read_from_path(path).map_err(|e| Error::Mp4aMeta(path.to_owned(), e))?;
tag.artwork()
.and_then(|d| image::load_from_memory(d.data).ok())
.ok_or_else(|| Error::EmbeddedArtworkNotFound(path.to_owned()))
.and_then(|d| image::load_from_memory(d.data).map_err(|e| Error::Image(path.to_owned(), e)))
}
fn read_vorbis(_: &Path) -> Result<DynamicImage, Error> {

View file

@ -73,22 +73,40 @@ pub fn make_config() -> impl FnOnce(&mut ServiceConfig) + Clone {
impl ResponseError for APIError {
fn status_code(&self) -> StatusCode {
match self {
APIError::AuthorizationTokenEncoding => StatusCode::INTERNAL_SERVER_ERROR,
APIError::AdminPermissionRequired => StatusCode::UNAUTHORIZED,
APIError::AudioFileIOError => StatusCode::NOT_FOUND,
APIError::AuthenticationRequired => StatusCode::UNAUTHORIZED,
APIError::BrancaTokenEncoding => StatusCode::INTERNAL_SERVER_ERROR,
APIError::DdnsUpdateQueryFailed(s) => {
StatusCode::from_u16(*s).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
}
APIError::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
APIError::DeletingOwnAccount => StatusCode::CONFLICT,
APIError::EmbeddedArtworkNotFound => StatusCode::NOT_FOUND,
APIError::EmptyPassword => StatusCode::BAD_REQUEST,
APIError::EmptyUsername => StatusCode::BAD_REQUEST,
APIError::IncorrectCredentials => StatusCode::UNAUTHORIZED,
APIError::Internal => StatusCode::INTERNAL_SERVER_ERROR,
APIError::Io(_, _) => StatusCode::INTERNAL_SERVER_ERROR,
APIError::LastFMAccountNotLinked => StatusCode::NO_CONTENT,
APIError::LastFMLinkContentBase64DecodeError => StatusCode::BAD_REQUEST,
APIError::LastFMLinkContentEncodingError => StatusCode::BAD_REQUEST,
APIError::LastFMNowPlaying(_) => StatusCode::FAILED_DEPENDENCY,
APIError::LastFMScrobble(_) => StatusCode::FAILED_DEPENDENCY,
APIError::LastFMScrobblerAuthentication(_) => StatusCode::FAILED_DEPENDENCY,
APIError::OwnAdminPrivilegeRemoval => StatusCode::CONFLICT,
APIError::PasswordHashing => StatusCode::INTERNAL_SERVER_ERROR,
APIError::PlaylistNotFound => StatusCode::NOT_FOUND,
APIError::Settings(_) => StatusCode::INTERNAL_SERVER_ERROR,
APIError::SongMetadataNotFound => StatusCode::NOT_FOUND,
APIError::ThumbnailFlacDecoding(_, _) => StatusCode::INTERNAL_SERVER_ERROR,
APIError::ThumbnailFileIOError => StatusCode::NOT_FOUND,
APIError::ThumbnailId3Decoding(_, _) => StatusCode::INTERNAL_SERVER_ERROR,
APIError::ThumbnailImageDecoding(_, _) => StatusCode::INTERNAL_SERVER_ERROR,
APIError::ThumbnailMp4Decoding(_, _) => StatusCode::INTERNAL_SERVER_ERROR,
APIError::TomlDeserialization(_) => StatusCode::INTERNAL_SERVER_ERROR,
APIError::UnsupportedThumbnailFormat(_) => StatusCode::INTERNAL_SERVER_ERROR,
APIError::UserNotFound => StatusCode::NOT_FOUND,
APIError::VFSPathNotFound => StatusCode::NOT_FOUND,
}

View file

@ -1,3 +1,4 @@
use std::path::PathBuf;
use thiserror::Error;
use crate::app::index::QueryError;
@ -6,51 +7,83 @@ use crate::db;
#[derive(Error, Debug)]
pub enum APIError {
#[error("Could not encode authorization token")]
AuthorizationTokenEncoding,
#[error("Administrator permission is required")]
AdminPermissionRequired,
#[error("Audio file could not be opened")]
AudioFileIOError,
#[error("Authentication is required")]
AuthenticationRequired,
#[error("Incorrect Credentials")]
IncorrectCredentials,
#[error("Could not encode Branca token")]
BrancaTokenEncoding,
#[error("Database error:\n\n{0}")]
Database(diesel::result::Error),
#[error("DDNS update query failed with HTTP status {0}")]
DdnsUpdateQueryFailed(u16),
#[error("Cannot delete your own account")]
DeletingOwnAccount,
#[error("EmbeddedArtworkNotFound")]
EmbeddedArtworkNotFound,
#[error("EmptyUsername")]
EmptyUsername,
#[error("EmptyPassword")]
EmptyPassword,
#[error("Cannot delete your own account")]
DeletingOwnAccount,
#[error("Cannot remove your own admin privilege")]
OwnAdminPrivilegeRemoval,
#[error("Audio file could not be opened")]
AudioFileIOError,
#[error("Thumbnail file could not be opened")]
ThumbnailFileIOError,
#[error("Incorrect Credentials")]
IncorrectCredentials,
#[error("No last.fm account has been linked")]
LastFMAccountNotLinked,
#[error("Could not decode content as base64 after linking last.fm account")]
LastFMLinkContentBase64DecodeError,
#[error("Could not decode content as UTF-8 after linking last.fm account")]
LastFMLinkContentEncodingError,
#[error("Path not found in virtual filesystem")]
VFSPathNotFound,
#[error("User not found")]
UserNotFound,
#[error("Playlist not found")]
PlaylistNotFound,
#[error("Song not found")]
SongMetadataNotFound,
#[error("Could send Now Playing update to last.fm:\n\n{0}")]
LastFMNowPlaying(rustfm_scrobble::ScrobblerError),
#[error("Could emit scrobble with last.fm:\n\n{0}")]
LastFMScrobble(rustfm_scrobble::ScrobblerError),
#[error("Could authenticate with last.fm:\n\n{0}")]
LastFMScrobblerAuthentication(rustfm_scrobble::ScrobblerError),
#[error("Internal server error")]
Internal,
#[error("File I/O error for `{0}`:\n\n{1}")]
Io(PathBuf, std::io::Error),
#[error("Cannot remove your own admin privilege")]
OwnAdminPrivilegeRemoval,
#[error("Could not hash password")]
PasswordHashing,
#[error("Playlist not found")]
PlaylistNotFound,
#[error("Settings error:\n\n{0}")]
Settings(settings::Error),
#[error("Song not found")]
SongMetadataNotFound,
#[error("Could not decode thumbnail from flac file `{0}`:\n\n{1}")]
ThumbnailFlacDecoding(PathBuf, metaflac::Error),
#[error("Thumbnail file could not be opened")]
ThumbnailFileIOError,
#[error("Could not decode thumbnail from ID3 tag in `{0}`:\n\n{1}")]
ThumbnailId3Decoding(PathBuf, id3::Error),
#[error("Could not decode image thumbnail in `{0}`:\n\n{1}")]
ThumbnailImageDecoding(PathBuf, image::error::ImageError),
#[error("Could not decode thumbnail from mp4 file `{0}`:\n\n{1}")]
ThumbnailMp4Decoding(PathBuf, mp4ameta::Error),
#[error("Toml deserialization error:\n\n{0}")]
TomlDeserialization(toml::de::Error),
#[error("Unsupported thumbnail format: `{0}`")]
UnsupportedThumbnailFormat(&'static str),
#[error("User not found")]
UserNotFound,
#[error("Path not found in virtual filesystem")]
VFSPathNotFound,
}
impl From<config::Error> for APIError {
fn from(error: config::Error) -> APIError {
match error {
config::Error::Ddns(e) => e.into(),
config::Error::Io(_, _) => APIError::Internal,
config::Error::Io(p, e) => APIError::Io(p, e),
config::Error::Settings(e) => e.into(),
config::Error::Toml(_) => APIError::Internal,
config::Error::Toml(e) => APIError::TomlDeserialization(e),
config::Error::User(e) => e.into(),
config::Error::Vfs(e) => e.into(),
}
@ -60,7 +93,7 @@ impl From<config::Error> for APIError {
impl From<playlist::Error> for APIError {
fn from(error: playlist::Error) -> APIError {
match error {
playlist::Error::Database(_) => APIError::Internal,
playlist::Error::Database(e) => APIError::Database(e),
playlist::Error::DatabaseConnection(e) => e.into(),
playlist::Error::PlaylistNotFound => APIError::PlaylistNotFound,
playlist::Error::UserNotFound => APIError::UserNotFound,
@ -72,7 +105,7 @@ impl From<playlist::Error> for APIError {
impl From<QueryError> for APIError {
fn from(error: QueryError) -> APIError {
match error {
QueryError::Database(_) => APIError::Internal,
QueryError::Database(e) => APIError::Database(e),
QueryError::DatabaseConnection(e) => e.into(),
QueryError::SongNotFound(_) => APIError::SongMetadataNotFound,
QueryError::Vfs(e) => e.into(),
@ -83,12 +116,12 @@ impl From<QueryError> for APIError {
impl From<settings::Error> for APIError {
fn from(error: settings::Error) -> APIError {
match error {
settings::Error::AuthSecretNotFound => APIError::Internal,
settings::Error::AuthenticationSecretNotFound => APIError::Settings(error),
settings::Error::DatabaseConnection(e) => e.into(),
settings::Error::InvalidAuthSecret => APIError::Internal,
settings::Error::MiscSettingsNotFound => APIError::Internal,
settings::Error::IndexAlbumArtPatternInvalid => APIError::Internal,
settings::Error::Database(_) => APIError::Internal,
settings::Error::AuthenticationSecretInvalid => APIError::Settings(error),
settings::Error::MiscSettingsNotFound => APIError::Settings(error),
settings::Error::IndexAlbumArtPatternInvalid => APIError::Settings(error),
settings::Error::Database(e) => APIError::Database(e),
}
}
}
@ -96,18 +129,18 @@ impl From<settings::Error> for APIError {
impl From<user::Error> for APIError {
fn from(error: user::Error) -> APIError {
match error {
user::Error::AuthorizationTokenEncoding => APIError::Internal,
user::Error::BrancaTokenEncoding => APIError::Internal,
user::Error::Database(_) => APIError::Internal,
user::Error::AuthorizationTokenEncoding => APIError::AuthorizationTokenEncoding,
user::Error::BrancaTokenEncoding => APIError::BrancaTokenEncoding,
user::Error::Database(e) => APIError::Database(e),
user::Error::DatabaseConnection(e) => e.into(),
user::Error::EmptyUsername => APIError::EmptyUsername,
user::Error::EmptyPassword => APIError::EmptyPassword,
user::Error::IncorrectUsername => APIError::IncorrectCredentials,
user::Error::IncorrectPassword => APIError::IncorrectCredentials,
user::Error::InvalidAuthToken => APIError::IncorrectCredentials,
user::Error::EmptyUsername => APIError::EmptyUsername,
user::Error::IncorrectAuthorizationScope => APIError::IncorrectCredentials,
user::Error::PasswordHashing => APIError::Internal,
user::Error::IncorrectPassword => APIError::IncorrectCredentials,
user::Error::IncorrectUsername => APIError::IncorrectCredentials,
user::Error::InvalidAuthToken => APIError::IncorrectCredentials,
user::Error::MissingLastFMSessionKey => APIError::IncorrectCredentials,
user::Error::PasswordHashing => APIError::PasswordHashing,
}
}
}
@ -117,7 +150,7 @@ impl From<vfs::Error> for APIError {
match error {
vfs::Error::CouldNotMapToVirtualPath(_) => APIError::VFSPathNotFound,
vfs::Error::CouldNotMapToRealPath(_) => APIError::VFSPathNotFound,
vfs::Error::Database(_) => APIError::Internal,
vfs::Error::Database(e) => APIError::Database(e),
vfs::Error::DatabaseConnection(e) => e.into(),
}
}
@ -126,9 +159,9 @@ impl From<vfs::Error> for APIError {
impl From<ddns::Error> for APIError {
fn from(error: ddns::Error) -> APIError {
match error {
ddns::Error::Database(_) => APIError::Internal,
ddns::Error::Database(e) => APIError::Database(e),
ddns::Error::DatabaseConnection(e) => e.into(),
ddns::Error::UpdateQueryFailed(_) => APIError::Internal,
ddns::Error::UpdateQueryFailed(s) => APIError::DdnsUpdateQueryFailed(s),
}
}
}
@ -138,7 +171,7 @@ impl From<db::Error> for APIError {
match error {
db::Error::ConnectionPoolBuild => APIError::Internal,
db::Error::ConnectionPool => APIError::Internal,
db::Error::Io(_, _) => APIError::Internal,
db::Error::Io(p, e) => APIError::Io(p, e),
db::Error::Migration => APIError::Internal,
}
}
@ -147,9 +180,9 @@ impl From<db::Error> for APIError {
impl From<lastfm::Error> for APIError {
fn from(error: lastfm::Error) -> APIError {
match error {
lastfm::Error::ScrobblerAuthentication(_) => APIError::Internal,
lastfm::Error::Scrobble(_) => APIError::Internal,
lastfm::Error::NowPlaying(_) => APIError::Internal,
lastfm::Error::ScrobblerAuthentication(e) => APIError::LastFMScrobblerAuthentication(e),
lastfm::Error::Scrobble(e) => APIError::LastFMScrobble(e),
lastfm::Error::NowPlaying(e) => APIError::LastFMNowPlaying(e),
lastfm::Error::Query(e) => e.into(),
lastfm::Error::User(e) => e.into(),
}
@ -160,12 +193,12 @@ impl From<thumbnail::Error> for APIError {
fn from(error: thumbnail::Error) -> APIError {
match error {
thumbnail::Error::EmbeddedArtworkNotFound(_) => APIError::EmbeddedArtworkNotFound,
thumbnail::Error::Id3(_) => APIError::Internal,
thumbnail::Error::Image(_) => APIError::Internal,
thumbnail::Error::Io(_, _) => APIError::Internal,
thumbnail::Error::Metaflac(_) => APIError::Internal,
thumbnail::Error::Mp4aMeta(_) => APIError::Internal,
thumbnail::Error::UnsupportedFormat(_) => APIError::Internal,
thumbnail::Error::Id3(p, e) => APIError::ThumbnailId3Decoding(p, e),
thumbnail::Error::Image(p, e) => APIError::ThumbnailImageDecoding(p, e),
thumbnail::Error::Io(p, e) => APIError::Io(p, e),
thumbnail::Error::Metaflac(p, e) => APIError::ThumbnailFlacDecoding(p, e),
thumbnail::Error::Mp4aMeta(p, e) => APIError::ThumbnailMp4Decoding(p, e),
thumbnail::Error::UnsupportedFormat(f) => APIError::UnsupportedThumbnailFormat(f),
}
}
}