Implement proper error logging and handling (closes: #6)

* Use the `thiserror` crate to make our own error type
* Implement Rocket's `Responder` type for the error type
* Adjust all methods to use the error type
* Small documentation tweaks
This commit is contained in:
Paul van Tilburg 2022-05-26 19:57:16 +02:00
parent 9d1bfd8351
commit b53365a293
Signed by: paul
GPG Key ID: C6DE073EDA9EEC4D
4 changed files with 67 additions and 29 deletions

1
Cargo.lock generated
View File

@ -1702,6 +1702,7 @@ dependencies = [
"rocket_dyn_templates",
"rss",
"tempfile",
"thiserror",
"tokio",
]

View File

@ -16,6 +16,7 @@ rocket = { version = "0.5.0-rc.2", features = ["json"] }
rocket_dyn_templates = { version = "0.1.0-rc.2", features = ["tera"] }
rss = "2.0.1"
tempfile = "3"
thiserror = "1.0.31"
tokio = { version = "1.6.1", features = ["process"] }
[package.metadata.deb]

View File

@ -8,13 +8,15 @@
#![deny(missing_docs)]
use std::path::PathBuf;
use std::process::ExitStatus;
use chrono::{DateTime, NaiveDateTime, Utc};
use rocket::fairing::AdHoc;
use rocket::http::uri::Absolute;
use rocket::http::Status;
use rocket::response::Redirect;
use rocket::serde::{Deserialize, Serialize};
use rocket::{get, routes, uri, Build, Responder, Rocket, State};
use rocket::{get, routes, uri, Build, Request, Responder, Rocket, State};
use rocket_dyn_templates::{context, Template};
use rss::extension::itunes::{
ITunesCategoryBuilder, ITunesChannelExtensionBuilder, ITunesItemExtensionBuilder,
@ -22,9 +24,43 @@ use rss::extension::itunes::{
use rss::{
CategoryBuilder, ChannelBuilder, EnclosureBuilder, GuidBuilder, ImageBuilder, ItemBuilder,
};
use tokio::process::Command;
pub(crate) mod mixcloud;
/// The possible errors that can occur.
#[derive(Debug, thiserror::Error)]
pub(crate) enum Error {
#[error("Command failed: {0:?} exited with {1}")]
CommandFailed(Command, ExitStatus),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("No redirect URL found")]
NoRedirectUrlFound,
#[error("HTTP error: {0}")]
Request(#[from] reqwest::Error),
#[error("Unknown supported back-end: {0}")]
UnsupportedBackend(String),
}
impl<'r, 'o: 'r> rocket::response::Responder<'r, 'o> for Error {
fn respond_to(self, _request: &'r Request<'_>) -> rocket::response::Result<'o> {
eprintln!("💥 Encountered error: {}", self);
match self {
Error::NoRedirectUrlFound => Err(Status::NotFound),
_ => Err(Status::InternalServerError),
}
}
}
/// Result type that defaults to [`Error`] as the default error type.
pub(crate) type Result<T, E = Error> = std::result::Result<T, E>;
/// The extra application specific configuration.
#[derive(Debug, Deserialize, Serialize)]
#[serde(crate = "rocket::serde")]
@ -39,22 +75,22 @@ pub(crate) struct Config {
#[response(content_type = "application/xml")]
struct RssFeed(String);
/// Retrieves a download using youtube-dl and redirection.
/// Retrieves a download by redirecting to the URL resolved by the selected back-end.
#[get("/download/<backend>/<file..>")]
pub(crate) async fn download(file: PathBuf, backend: &str) -> Option<Redirect> {
pub(crate) async fn download(file: PathBuf, backend: &str) -> Result<Redirect> {
match backend {
"mixcloud" => {
let key = format!("/{}/", file.with_extension("").to_string_lossy());
mixcloud::redirect_url(&key).await.map(Redirect::to)
}
_ => None,
_ => Err(Error::UnsupportedBackend(backend.to_string())),
}
}
/// Handler for retrieving the RSS feed of an Mixcloud user.
/// Handler for retrieving the RSS feed of user on a certain back-end.
#[get("/feed/<backend>/<username>")]
async fn feed(backend: &str, username: &str, config: &State<Config>) -> Option<RssFeed> {
async fn feed(backend: &str, username: &str, config: &State<Config>) -> Result<RssFeed> {
let user = mixcloud::get_user(username).await?;
let cloudcasts = mixcloud::get_cloudcasts(username).await?;
let mut last_build = DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(0, 0), Utc);
@ -156,7 +192,7 @@ async fn feed(backend: &str, username: &str, config: &State<Config>) -> Option<R
.build();
let feed = RssFeed(channel.to_string());
Some(feed)
Ok(feed)
}
/// Returns a simple index page that explains the usage.

View File

@ -10,6 +10,8 @@ use reqwest::Url;
use rocket::serde::Deserialize;
use tokio::process::Command;
use super::{Error, Result};
/// A Mixcloud user.
#[derive(Debug, Deserialize)]
#[serde(crate = "rocket::serde")]
@ -108,52 +110,50 @@ pub(crate) fn estimated_file_size(duration: u32) -> u32 {
}
/// Retrieves the user data using the Mixcloud API.
pub(crate) async fn get_user(username: &str) -> Option<User> {
let mut url = Url::parse(API_BASE_URL).unwrap();
pub(crate) async fn get_user(username: &str) -> Result<User> {
let mut url = Url::parse(API_BASE_URL).expect("URL can always be parsed");
url.set_path(username);
println!("⏬ Retrieving user {username} from {url}...");
let response = reqwest::get(url).await.ok()?;
let user = match response.error_for_status() {
Ok(res) => res.json().await.ok()?,
Err(_err) => return None,
};
let response = reqwest::get(url).await?.error_for_status()?;
let user = response.json().await?;
Some(user)
Ok(user)
}
/// Retrieves the cloudcasts of the user using the Mixcloud API.
pub(crate) async fn get_cloudcasts(username: &str) -> Option<Vec<Cloudcast>> {
let mut url = Url::parse(API_BASE_URL).unwrap();
pub(crate) async fn get_cloudcasts(username: &str) -> Result<Vec<Cloudcast>> {
let mut url = Url::parse(API_BASE_URL).expect("URL can always be parsed");
url.set_path(&format!("{username}/cloudcasts/"));
println!("⏬ Retrieving cloudcasts of user {username} from {url}...");
let response = reqwest::get(url).await.ok()?;
let cloudcasts: CloudcastData = match response.error_for_status() {
Ok(res) => res.json().await.ok()?,
Err(_err) => return None,
};
let response = reqwest::get(url).await?.error_for_status()?;
let cloudcasts: CloudcastData = response.json().await?;
Some(cloudcasts.data)
Ok(cloudcasts.data)
}
/// Retrieves the redirect URL for the provided Mixcloud cloudcast key.
pub(crate) async fn redirect_url(key: &str) -> Option<String> {
pub(crate) async fn redirect_url(key: &str) -> Result<String> {
let mut cmd = Command::new("youtube-dl");
cmd.args(&["--format", "http"])
.arg("--get-url")
.arg(&format!("{FILES_BASE_URL}{key}"))
.stdout(Stdio::piped());
let output = cmd.output().await.ok()?;
println!("🌍 Determining direct URL for {key}...");
let output = cmd.output().await?;
if output.status.success() {
let direct_url = String::from_utf8_lossy(&output.stdout)
.trim_end()
.to_owned();
println!("🌍 Determined direct URL for {key}: {direct_url}...");
Some(direct_url)
if direct_url.is_empty() {
return Err(Error::NoRedirectUrlFound);
} else {
println!(" Found direct URL: {direct_url}");
Ok(direct_url)
}
} else {
None
Err(Error::CommandFailed(cmd, output.status))
}
}