Error cleanup

This commit is contained in:
Antoine Gersant 2022-11-21 16:31:49 -08:00
parent 1e9d307a05
commit 4c5a6bc2d6
5 changed files with 89 additions and 81 deletions

View file

@ -16,14 +16,6 @@ pub enum QueryError {
SongNotFound(PathBuf), SongNotFound(PathBuf),
#[error(transparent)] #[error(transparent)]
Vfs(#[from] vfs::Error), Vfs(#[from] vfs::Error),
#[error("Unspecified")]
Unspecified,
}
impl From<anyhow::Error> for QueryError {
fn from(_: anyhow::Error) -> Self {
QueryError::Unspecified
}
} }
sql_function!( sql_function!(
@ -44,8 +36,7 @@ impl Index {
// Browse top-level // Browse top-level
let real_directories: Vec<Directory> = directories::table let real_directories: Vec<Directory> = directories::table
.filter(directories::parent.is_null()) .filter(directories::parent.is_null())
.load(&mut connection) .load(&mut connection)?;
.map_err(anyhow::Error::new)?;
let virtual_directories = real_directories let virtual_directories = real_directories
.into_iter() .into_iter()
.filter_map(|d| d.virtualize(&vfs)); .filter_map(|d| d.virtualize(&vfs));
@ -58,8 +49,7 @@ impl Index {
let real_directories: Vec<Directory> = directories::table let real_directories: Vec<Directory> = directories::table
.filter(directories::parent.eq(&real_path_string)) .filter(directories::parent.eq(&real_path_string))
.order(sql::<sql_types::Bool>("path COLLATE NOCASE ASC")) .order(sql::<sql_types::Bool>("path COLLATE NOCASE ASC"))
.load(&mut connection) .load(&mut connection)?;
.map_err(anyhow::Error::new)?;
let virtual_directories = real_directories let virtual_directories = real_directories
.into_iter() .into_iter()
.filter_map(|d| d.virtualize(&vfs)); .filter_map(|d| d.virtualize(&vfs));
@ -68,8 +58,7 @@ impl Index {
let real_songs: Vec<Song> = songs::table let real_songs: Vec<Song> = songs::table
.filter(songs::parent.eq(&real_path_string)) .filter(songs::parent.eq(&real_path_string))
.order(sql::<sql_types::Bool>("path COLLATE NOCASE ASC")) .order(sql::<sql_types::Bool>("path COLLATE NOCASE ASC"))
.load(&mut connection) .load(&mut connection)?;
.map_err(anyhow::Error::new)?;
let virtual_songs = real_songs.into_iter().filter_map(|s| s.virtualize(&vfs)); let virtual_songs = real_songs.into_iter().filter_map(|s| s.virtualize(&vfs));
output.extend(virtual_songs.map(CollectionFile::Song)); output.extend(virtual_songs.map(CollectionFile::Song));
} }
@ -95,20 +84,16 @@ impl Index {
songs songs
.filter(path.like(&song_path_filter)) .filter(path.like(&song_path_filter))
.order(path) .order(path)
.load(&mut connection) .load(&mut connection)?
.map_err(anyhow::Error::new)?
} else { } else {
songs songs.order(path).load(&mut connection)?
.order(path)
.load(&mut connection)
.map_err(anyhow::Error::new)?
}; };
let virtual_songs = real_songs.into_iter().filter_map(|s| s.virtualize(&vfs)); let virtual_songs = real_songs.into_iter().filter_map(|s| s.virtualize(&vfs));
Ok(virtual_songs.collect::<Vec<_>>()) Ok(virtual_songs.collect::<Vec<_>>())
} }
pub fn get_random_albums(&self, count: i64) -> anyhow::Result<Vec<Directory>> { pub fn get_random_albums(&self, count: i64) -> Result<Vec<Directory>, QueryError> {
use self::directories::dsl::*; use self::directories::dsl::*;
let vfs = self.vfs_manager.get_vfs()?; let vfs = self.vfs_manager.get_vfs()?;
let mut connection = self.db.connect()?; let mut connection = self.db.connect()?;
@ -123,7 +108,7 @@ impl Index {
Ok(virtual_directories.collect::<Vec<_>>()) Ok(virtual_directories.collect::<Vec<_>>())
} }
pub fn get_recent_albums(&self, count: i64) -> anyhow::Result<Vec<Directory>> { pub fn get_recent_albums(&self, count: i64) -> Result<Vec<Directory>, QueryError> {
use self::directories::dsl::*; use self::directories::dsl::*;
let vfs = self.vfs_manager.get_vfs()?; let vfs = self.vfs_manager.get_vfs()?;
let mut connection = self.db.connect()?; let mut connection = self.db.connect()?;
@ -138,7 +123,7 @@ impl Index {
Ok(virtual_directories.collect::<Vec<_>>()) Ok(virtual_directories.collect::<Vec<_>>())
} }
pub fn search(&self, query: &str) -> anyhow::Result<Vec<CollectionFile>> { pub fn search(&self, query: &str) -> Result<Vec<CollectionFile>, QueryError> {
let vfs = self.vfs_manager.get_vfs()?; let vfs = self.vfs_manager.get_vfs()?;
let mut connection = self.db.connect()?; let mut connection = self.db.connect()?;
let like_test = format!("%{}%", query); let like_test = format!("%{}%", query);

View file

@ -1,4 +1,3 @@
use anyhow::*;
use image::{DynamicImage, GenericImage, GenericImageView, ImageBuffer, ImageOutputFormat}; use image::{DynamicImage, GenericImage, GenericImageView, ImageBuffer, ImageOutputFormat};
use std::cmp; use std::cmp;
use std::collections::hash_map::DefaultHasher; use std::collections::hash_map::DefaultHasher;
@ -8,6 +7,24 @@ use std::path::{Path, PathBuf};
use crate::utils::{get_audio_format, AudioFormat}; use crate::utils::{get_audio_format, AudioFormat};
#[derive(thiserror::Error, Debug)]
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("Filesystem error for `{0}`: `{1}`")]
Io(PathBuf, std::io::Error),
#[error(transparent)]
Metaflac(#[from] metaflac::Error),
#[error(transparent)]
Mp4aMeta(#[from] mp4ameta::Error),
#[error("This file format is not supported: {0}")]
UnsupportedFormat(&'static str),
}
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct Options { pub struct Options {
pub max_dimension: Option<u32>, pub max_dimension: Option<u32>,
@ -37,7 +54,11 @@ impl Manager {
} }
} }
pub fn get_thumbnail(&self, image_path: &Path, thumbnailoptions: &Options) -> Result<PathBuf> { pub fn get_thumbnail(
&self,
image_path: &Path,
thumbnailoptions: &Options,
) -> Result<PathBuf, Error> {
match self.retrieve_thumbnail(image_path, thumbnailoptions) { match self.retrieve_thumbnail(image_path, thumbnailoptions) {
Some(path) => Ok(path), Some(path) => Ok(path),
None => self.create_thumbnail(image_path, thumbnailoptions), None => self.create_thumbnail(image_path, thumbnailoptions),
@ -60,13 +81,19 @@ impl Manager {
} }
} }
fn create_thumbnail(&self, image_path: &Path, thumbnailoptions: &Options) -> Result<PathBuf> { fn create_thumbnail(
&self,
image_path: &Path,
thumbnailoptions: &Options,
) -> Result<PathBuf, Error> {
let thumbnail = generate_thumbnail(image_path, thumbnailoptions)?; let thumbnail = generate_thumbnail(image_path, thumbnailoptions)?;
let quality = 80; let quality = 80;
fs::create_dir_all(&self.thumbnails_dir_path)?; fs::create_dir_all(&self.thumbnails_dir_path)
.map_err(|e| Error::Io(self.thumbnails_dir_path.clone(), e))?;
let path = self.get_thumbnail_path(image_path, thumbnailoptions); let path = self.get_thumbnail_path(image_path, thumbnailoptions);
let mut out_file = File::create(&path)?; 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))?;
Ok(path) Ok(path)
} }
@ -79,7 +106,7 @@ impl Manager {
} }
} }
fn generate_thumbnail(image_path: &Path, options: &Options) -> Result<DynamicImage> { fn generate_thumbnail(image_path: &Path, options: &Options) -> Result<DynamicImage, Error> {
let source_image = DynamicImage::ImageRgb8(read(image_path)?.into_rgb8()); let source_image = DynamicImage::ImageRgb8(read(image_path)?.into_rgb8());
let (source_width, source_height) = source_image.dimensions(); let (source_width, source_height) = source_image.dimensions();
let largest_dimension = cmp::max(source_width, source_height); let largest_dimension = cmp::max(source_width, source_height);
@ -115,7 +142,7 @@ fn generate_thumbnail(image_path: &Path, options: &Options) -> Result<DynamicIma
Ok(final_image) Ok(final_image)
} }
fn read(image_path: &Path) -> Result<DynamicImage> { fn read(image_path: &Path) -> Result<DynamicImage, Error> {
match get_audio_format(image_path) { match get_audio_format(image_path) {
Some(AudioFormat::AIFF) => read_aiff(image_path), Some(AudioFormat::AIFF) => read_aiff(image_path),
Some(AudioFormat::APE) => read_ape(image_path), Some(AudioFormat::APE) => read_ape(image_path),
@ -130,67 +157,53 @@ fn read(image_path: &Path) -> Result<DynamicImage> {
} }
} }
fn read_ape(_: &Path) -> Result<DynamicImage> { fn read_ape(_: &Path) -> Result<DynamicImage, Error> {
bail!("Embedded images are not supported in APE files"); Err(Error::UnsupportedFormat("ape"))
} }
fn read_flac(path: &Path) -> Result<DynamicImage> { fn read_flac(path: &Path) -> Result<DynamicImage, Error> {
let tag = metaflac::Tag::read_from_path(path)?; let tag = metaflac::Tag::read_from_path(path)?;
if let Some(p) = tag.pictures().next() { if let Some(p) = tag.pictures().next() {
return Ok(image::load_from_memory(&p.data)?); return Ok(image::load_from_memory(&p.data)?);
} }
Err(Error::EmbeddedArtworkNotFound(path.to_owned()))
bail!(
"Embedded flac artwork not found for file: {}",
path.display()
);
} }
fn read_mp3(path: &Path) -> Result<DynamicImage> { fn read_mp3(path: &Path) -> Result<DynamicImage, Error> {
let tag = id3::Tag::read_from_path(path)?; let tag = id3::Tag::read_from_path(path)?;
read_id3(path, &tag) read_id3(path, &tag)
} }
fn read_aiff(path: &Path) -> Result<DynamicImage> { 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)?;
read_id3(path, &tag) read_id3(path, &tag)
} }
fn read_wave(path: &Path) -> Result<DynamicImage> { 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)?;
read_id3(path, &tag) read_id3(path, &tag)
} }
fn read_id3(path: &Path, tag: &id3::Tag) -> Result<DynamicImage> { fn read_id3(path: &Path, tag: &id3::Tag) -> Result<DynamicImage, Error> {
if let Some(p) = tag.pictures().next() { if let Some(p) = tag.pictures().next() {
return Ok(image::load_from_memory(&p.data)?); return Ok(image::load_from_memory(&p.data)?);
} }
Err(Error::EmbeddedArtworkNotFound(path.to_owned()))
bail!(
"Embedded id3 artwork not found for file: {}",
path.display()
);
} }
fn read_mp4(path: &Path) -> Result<DynamicImage> { fn read_mp4(path: &Path) -> Result<DynamicImage, Error> {
let tag = mp4ameta::Tag::read_from_path(path)?; let tag = mp4ameta::Tag::read_from_path(path)?;
tag.artwork()
match tag.artwork().map(|d| d.data) { .and_then(|d| image::load_from_memory(d.data).ok())
Some(v) => Ok(image::load_from_memory(v)?), .ok_or_else(|| Error::EmbeddedArtworkNotFound(path.to_owned()))
_ => bail!(
"Embedded mp4 artwork not found for file: {}",
path.display()
),
}
} }
fn read_vorbis(_: &Path) -> Result<DynamicImage> { fn read_vorbis(_: &Path) -> Result<DynamicImage, Error> {
bail!("Embedded images are not supported in Vorbis files"); Err(Error::UnsupportedFormat("vorbis"))
} }
fn read_opus(_: &Path) -> Result<DynamicImage> { fn read_opus(_: &Path) -> Result<DynamicImage, Error> {
bail!("Embedded images are not supported in Opus files"); Err(Error::UnsupportedFormat("opus"))
} }
#[cfg(test)] #[cfg(test)]

View file

@ -208,7 +208,7 @@ impl Manager {
.map(AuthToken) .map(AuthToken)
} }
pub fn count(&self) -> anyhow::Result<i64> { pub fn count(&self) -> Result<i64, Error> {
use crate::db::users::dsl::*; use crate::db::users::dsl::*;
let mut connection = self.db.connect()?; let mut connection = self.db.connect()?;
let count = users.count().get_result(&mut connection)?; let count = users.count().get_result(&mut connection)?;

View file

@ -15,7 +15,7 @@ use actix_web_httpauth::extractors::bearer::BearerAuth;
use futures_util::future::err; use futures_util::future::err;
use percent_encoding::percent_decode_str; use percent_encoding::percent_decode_str;
use std::future::Future; use std::future::Future;
use std::path::Path; use std::path::{Path, PathBuf};
use std::pin::Pin; use std::pin::Pin;
use std::str; use std::str;
@ -73,9 +73,11 @@ pub fn make_config() -> impl FnOnce(&mut ServiceConfig) + Clone {
impl ResponseError for APIError { impl ResponseError for APIError {
fn status_code(&self) -> StatusCode { fn status_code(&self) -> StatusCode {
match self { match self {
APIError::AdminPermissionRequired => StatusCode::UNAUTHORIZED,
APIError::AudioFileIOError => StatusCode::NOT_FOUND, APIError::AudioFileIOError => StatusCode::NOT_FOUND,
APIError::AuthenticationRequired => StatusCode::UNAUTHORIZED, APIError::AuthenticationRequired => StatusCode::UNAUTHORIZED,
APIError::DeletingOwnAccount => StatusCode::CONFLICT, APIError::DeletingOwnAccount => StatusCode::CONFLICT,
APIError::EmbeddedArtworkNotFound => StatusCode::NOT_FOUND,
APIError::EmptyPassword => StatusCode::BAD_REQUEST, APIError::EmptyPassword => StatusCode::BAD_REQUEST,
APIError::EmptyUsername => StatusCode::BAD_REQUEST, APIError::EmptyUsername => StatusCode::BAD_REQUEST,
APIError::IncorrectCredentials => StatusCode::UNAUTHORIZED, APIError::IncorrectCredentials => StatusCode::UNAUTHORIZED,
@ -87,7 +89,6 @@ impl ResponseError for APIError {
APIError::PlaylistNotFound => StatusCode::NOT_FOUND, APIError::PlaylistNotFound => StatusCode::NOT_FOUND,
APIError::SongMetadataNotFound => StatusCode::NOT_FOUND, APIError::SongMetadataNotFound => StatusCode::NOT_FOUND,
APIError::ThumbnailFileIOError => StatusCode::NOT_FOUND, APIError::ThumbnailFileIOError => StatusCode::NOT_FOUND,
APIError::Unspecified => StatusCode::INTERNAL_SERVER_ERROR,
APIError::UserNotFound => StatusCode::NOT_FOUND, APIError::UserNotFound => StatusCode::NOT_FOUND,
APIError::VFSPathNotFound => StatusCode::NOT_FOUND, APIError::VFSPathNotFound => StatusCode::NOT_FOUND,
} }
@ -106,7 +107,7 @@ impl FromRequest for Auth {
fn from_request(request: &HttpRequest, payload: &mut Payload) -> Self::Future { fn from_request(request: &HttpRequest, payload: &mut Payload) -> Self::Future {
let user_manager = match request.app_data::<Data<user::Manager>>() { let user_manager = match request.app_data::<Data<user::Manager>>() {
Some(m) => m.clone(), Some(m) => m.clone(),
None => return Box::pin(err(ErrorInternalServerError(APIError::Unspecified))), None => return Box::pin(err(ErrorInternalServerError(APIError::Internal))),
}; };
let bearer_auth_future = BearerAuth::from_request(request, payload); let bearer_auth_future = BearerAuth::from_request(request, payload);
@ -155,7 +156,7 @@ impl FromRequest for AdminRights {
fn from_request(request: &HttpRequest, payload: &mut Payload) -> Self::Future { fn from_request(request: &HttpRequest, payload: &mut Payload) -> Self::Future {
let user_manager = match request.app_data::<Data<user::Manager>>() { let user_manager = match request.app_data::<Data<user::Manager>>() {
Some(m) => m.clone(), Some(m) => m.clone(),
None => return Box::pin(err(ErrorInternalServerError(APIError::Unspecified))), None => return Box::pin(err(ErrorInternalServerError(APIError::Internal))),
}; };
let auth_future = Auth::from_request(request, payload); let auth_future = Auth::from_request(request, payload);
@ -164,7 +165,7 @@ impl FromRequest for AdminRights {
let user_manager_count = user_manager.clone(); let user_manager_count = user_manager.clone();
let user_count = block(move || user_manager_count.count()).await; let user_count = block(move || user_manager_count.count()).await;
match user_count { match user_count {
Err(_) => return Err(ErrorInternalServerError(APIError::Unspecified)), Err(e) => return Err(e.into()),
Ok(0) => return Ok(AdminRights { auth: None }), Ok(0) => return Ok(AdminRights { auth: None }),
_ => (), _ => (),
}; };
@ -175,7 +176,7 @@ impl FromRequest for AdminRights {
if is_admin { if is_admin {
Ok(AdminRights { auth: Some(auth) }) Ok(AdminRights { auth: Some(auth) })
} else { } else {
Err(ErrorForbidden(APIError::Unspecified)) Err(ErrorForbidden(APIError::AdminPermissionRequired))
} }
}) })
} }
@ -212,7 +213,7 @@ where
{ {
actix_web::web::block(f) actix_web::web::block(f)
.await .await
.map_err(|_| APIError::Unspecified) .map_err(|_| APIError::Internal)
.and_then(|r| r.map_err(|e| e.into())) .and_then(|r| r.map_err(|e| e.into()))
} }
@ -522,13 +523,13 @@ async fn get_thumbnail(
) -> Result<MediaFile, APIError> { ) -> Result<MediaFile, APIError> {
let options = thumbnail::Options::from(options_input.0); let options = thumbnail::Options::from(options_input.0);
let thumbnail_path = block(move || { let thumbnail_path = block(move || -> Result<PathBuf, APIError> {
let vfs = vfs_manager.get_vfs()?; let vfs = vfs_manager.get_vfs()?;
let path = percent_decode_str(&path).decode_utf8_lossy(); let path = percent_decode_str(&path).decode_utf8_lossy();
let image_path = vfs.virtual_to_real(Path::new(path.as_ref()))?; let image_path = vfs.virtual_to_real(Path::new(path.as_ref()))?;
thumbnails_manager thumbnails_manager
.get_thumbnail(&image_path, &options) .get_thumbnail(&image_path, &options)
.map_err(|_| APIError::Unspecified) .map_err(|e| e.into())
}) })
.await?; .await?;

View file

@ -1,15 +1,19 @@
use thiserror::Error; use thiserror::Error;
use crate::app::index::QueryError; use crate::app::index::QueryError;
use crate::app::{config, ddns, lastfm, playlist, settings, user, vfs}; use crate::app::{config, ddns, lastfm, playlist, settings, thumbnail, user, vfs};
use crate::db; use crate::db;
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum APIError { pub enum APIError {
#[error("Administrator permission is required")]
AdminPermissionRequired,
#[error("Authentication is required")] #[error("Authentication is required")]
AuthenticationRequired, AuthenticationRequired,
#[error("Incorrect Credentials")] #[error("Incorrect Credentials")]
IncorrectCredentials, IncorrectCredentials,
#[error("EmbeddedArtworkNotFound")]
EmbeddedArtworkNotFound,
#[error("EmptyUsername")] #[error("EmptyUsername")]
EmptyUsername, EmptyUsername,
#[error("EmptyPassword")] #[error("EmptyPassword")]
@ -38,14 +42,6 @@ pub enum APIError {
SongMetadataNotFound, SongMetadataNotFound,
#[error("Internal server error")] #[error("Internal server error")]
Internal, Internal,
#[error("Unspecified")]
Unspecified,
}
impl From<anyhow::Error> for APIError {
fn from(_: anyhow::Error) -> Self {
APIError::Unspecified
}
} }
impl From<config::Error> for APIError { impl From<config::Error> for APIError {
@ -78,7 +74,6 @@ impl From<QueryError> for APIError {
QueryError::DatabaseConnection(e) => e.into(), QueryError::DatabaseConnection(e) => e.into(),
QueryError::SongNotFound(_) => APIError::SongMetadataNotFound, QueryError::SongNotFound(_) => APIError::SongMetadataNotFound,
QueryError::Vfs(e) => e.into(), QueryError::Vfs(e) => e.into(),
QueryError::Unspecified => APIError::Unspecified,
} }
} }
} }
@ -158,3 +153,17 @@ impl From<lastfm::Error> for APIError {
} }
} }
} }
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,
}
}
}