switch from static mut config to using Rocket's State

This commit is contained in:
askiiart 2024-12-01 22:00:19 -06:00
parent f2b559ab5f
commit 01c9984d99
Signed by untrusted user who does not match committer: askiiart
GPG key ID: EA85979611654C30
6 changed files with 61 additions and 135 deletions

1
Cargo.lock generated
View file

@ -1277,7 +1277,6 @@ name = "torznab-toolkit"
version = "0.1.0"
dependencies = [
"actix-rt",
"lazy_static",
"rocket",
"serde",
"xml-rs",

View file

@ -5,7 +5,6 @@ edition = "2021"
[dependencies]
actix-rt = "2.10.0"
lazy_static = "1.5.0"
rocket = "0.5.1"
serde = { version = "1.0.215", features = ["derive"] }
xml-rs = "0.8.23"

View file

@ -1,10 +1,9 @@
//! Contains the actual Torznab API
use crate::data::*;
use lazy_static::lazy_static;
use rocket::http::Status;
use rocket::response::status;
use rocket::FromForm;
use rocket::{get, response::content::RawXml};
use rocket::{FromForm, State};
use std::str;
use xml::writer::{EmitterConfig, XmlEvent};
@ -81,41 +80,11 @@ impl SearchForm {
}
}
// Holds the config for torznab-toolkit.
//
// A search function (`/api?t=search`) and capabilities (`/api?t=caps` - `Caps`) are required, everything else is optional.
//
// <div class="warning">It's required to be set to <i>something</i>, which is why it's an Option set to None.
//
// However, this is NOT optional, and attempting to do anything with CONFIG not set will return an `Err`.</div>
pub(crate) static mut CONFIG: Option<Config> = None;
lazy_static! {
static ref STATUS_CONFIG_NOT_SPECIFIED: status::Custom<RawXml<String>> = status::Custom(
Status::InternalServerError,
RawXml("500 Internal server error: Config not specified".to_string()),
);
}
/// Capabilities API endpoint (`/api?t=caps`)
///
/// Note that an apikey is *not* required for this function, regardless of whether it's required for the rest.
#[get("/api?t=caps")]
pub(crate) async fn caps() -> status::Custom<RawXml<String>> {
// The compiler won't let you get a field from a struct in the Option here, since the default is None
// So this is needed
let conf;
unsafe {
match CONFIG {
Some(ref config) => {
conf = config.clone();
}
None => {
return (*STATUS_CONFIG_NOT_SPECIFIED).clone();
}
}
}
#[get("/api?t=caps", rank = 1)]
pub(crate) async fn caps(conf: &State<Config>) -> status::Custom<RawXml<String>> {
let buffer = Vec::new();
let mut writer = EmitterConfig::new().create_writer(buffer);
@ -148,7 +117,7 @@ pub(crate) async fn caps() -> status::Custom<RawXml<String>> {
// Add the search types
writer.write(XmlEvent::start_element("searching")).unwrap();
for item in conf.caps.searching {
for item in &conf.caps.searching {
let mut available = "yes";
if !item.available {
available = "no";
@ -165,7 +134,7 @@ pub(crate) async fn caps() -> status::Custom<RawXml<String>> {
writer.write(XmlEvent::end_element()).unwrap(); // close `searching`
writer.write(XmlEvent::start_element("categories")).unwrap();
for i in conf.caps.categories {
for i in &conf.caps.categories {
writer
.write(
XmlEvent::start_element("category")
@ -173,7 +142,7 @@ pub(crate) async fn caps() -> status::Custom<RawXml<String>> {
.attr("name", i.name.as_str()),
)
.unwrap();
for j in i.subcategories {
for j in &i.subcategories {
writer
.write(
XmlEvent::start_element("subcat")
@ -187,7 +156,7 @@ pub(crate) async fn caps() -> status::Custom<RawXml<String>> {
}
writer.write(XmlEvent::end_element()).unwrap(); // close `categories`
match conf.caps.genres {
match &conf.caps.genres {
Some(genres) => {
writer.write(XmlEvent::start_element("genres")).unwrap();
@ -207,7 +176,7 @@ pub(crate) async fn caps() -> status::Custom<RawXml<String>> {
None => {}
}
match conf.caps.tags {
match &conf.caps.tags {
Some(tags) => {
writer.write(XmlEvent::start_element("tags")).unwrap();
@ -233,21 +202,14 @@ pub(crate) async fn caps() -> status::Custom<RawXml<String>> {
return status::Custom(Status::Ok, RawXml(result));
}
#[get("/api?t=search&<form..>")]
#[get("/api?t=search&<form..>", rank = 2)]
/// The general search function
pub(crate) async fn search(form: SearchForm) -> status::Custom<RawXml<String>> {
// The compiler won't let you get a field from a struct in the Option here, since the default is None
// So this is needed
let conf;
unsafe {
if CONFIG.is_none() {
return (*STATUS_CONFIG_NOT_SPECIFIED).clone();
} else {
conf = CONFIG.clone().ok_or("").unwrap();
}
}
let parameters = form.to_parameters(conf.clone());
pub(crate) async fn search(
conf: &State<Config>,
form: SearchForm,
) -> status::Custom<RawXml<String>> {
// oh god this is horrible but it works
let parameters = form.to_parameters((**conf).clone());
let mut unauthorized = false;
match conf.auth {
@ -276,21 +238,14 @@ pub(crate) async fn search(form: SearchForm) -> status::Custom<RawXml<String>> {
return search_handler(conf, search_parameters).await;
}
#[get("/api?t=tvsearch&<form..>")]
#[get("/api?t=tvsearch&<form..>", rank = 3)]
/// The TV search function
pub(crate) async fn tv_search(form: SearchForm) -> status::Custom<RawXml<String>> {
// The compiler won't let you get a field from a struct in the Option here, since the default is None
// So this is needed
let conf;
unsafe {
if CONFIG.is_none() {
return (*STATUS_CONFIG_NOT_SPECIFIED).clone();
} else {
conf = CONFIG.clone().ok_or("").unwrap();
}
}
let parameters = form.to_parameters(conf.clone());
pub(crate) async fn tv_search(
conf: &State<Config>,
form: SearchForm,
) -> status::Custom<RawXml<String>> {
// oh god this is horrible but it works
let parameters = form.to_parameters((**conf).clone());
let mut unauthorized = false;
match conf.auth {
@ -326,21 +281,14 @@ pub(crate) async fn tv_search(form: SearchForm) -> status::Custom<RawXml<String>
return search_handler(conf, search_parameters).await;
}
#[get("/api?t=movie&<form..>")]
#[get("/api?t=movie&<form..>", rank = 4)]
/// The movie search function
pub(crate) async fn movie_search(form: SearchForm) -> status::Custom<RawXml<String>> {
// The compiler won't let you get a field from a struct in the Option here, since the default is None
// So this is needed
let conf;
unsafe {
if CONFIG.is_none() {
return (*STATUS_CONFIG_NOT_SPECIFIED).clone();
} else {
conf = CONFIG.clone().ok_or("").unwrap();
}
}
let parameters = form.to_parameters(conf.clone());
pub(crate) async fn movie_search(
conf: &State<Config>,
form: SearchForm,
) -> status::Custom<RawXml<String>> {
// oh god this is horrible but it works
let parameters = form.to_parameters((**conf).clone());
let mut unauthorized = false;
match conf.auth {
@ -376,21 +324,14 @@ pub(crate) async fn movie_search(form: SearchForm) -> status::Custom<RawXml<Stri
return search_handler(conf, search_parameters).await;
}
#[get("/api?t=music&<form..>")]
#[get("/api?t=music&<form..>", rank = 5)]
/// The music search function
pub(crate) async fn music_search(form: SearchForm) -> status::Custom<RawXml<String>> {
// The compiler won't let you get a field from a struct in the Option here, since the default is None
// So this is needed
let conf;
unsafe {
if CONFIG.is_none() {
return (*STATUS_CONFIG_NOT_SPECIFIED).clone();
} else {
conf = CONFIG.clone().ok_or("").unwrap();
}
}
let parameters = form.to_parameters(conf.clone());
pub(crate) async fn music_search(
conf: &State<Config>,
form: SearchForm,
) -> status::Custom<RawXml<String>> {
// oh god this is horrible but it works
let parameters = form.to_parameters((**conf).clone());
let mut unauthorized = false;
match conf.auth {
@ -426,21 +367,14 @@ pub(crate) async fn music_search(form: SearchForm) -> status::Custom<RawXml<Stri
return search_handler(conf, search_parameters).await;
}
#[get("/api?t=book&<form..>")]
#[get("/api?t=book&<form..>", rank = 6)]
/// The music search function
pub(crate) async fn book_search(form: SearchForm) -> status::Custom<RawXml<String>> {
// The compiler won't let you get a field from a struct in the Option here, since the default is None
// So this is needed
let conf;
unsafe {
if CONFIG.is_none() {
return (*STATUS_CONFIG_NOT_SPECIFIED).clone();
} else {
conf = CONFIG.clone().ok_or("").unwrap();
}
}
let parameters = form.to_parameters(conf.clone());
pub(crate) async fn book_search(
conf: &State<Config>,
form: SearchForm,
) -> status::Custom<RawXml<String>> {
// oh god this is horrible but it works
let parameters = form.to_parameters((**conf).clone());
let mut unauthorized = false;
match conf.auth {
@ -476,7 +410,10 @@ pub(crate) async fn book_search(form: SearchForm) -> status::Custom<RawXml<Strin
return search_handler(conf, search_parameters).await;
}
async fn search_handler(conf: Config, parameters: SearchParameters) -> status::Custom<RawXml<String>> {
async fn search_handler(
conf: &State<Config>,
parameters: SearchParameters,
) -> status::Custom<RawXml<String>> {
let buffer = Vec::new();
let mut writer = EmitterConfig::new().create_writer(buffer);
writer
@ -499,7 +436,7 @@ async fn search_handler(conf: Config, parameters: SearchParameters) -> status::C
// add `title`
writer.write(XmlEvent::start_element("title")).unwrap();
let mut title_provided = false;
match conf.caps.server_info {
match &conf.caps.server_info {
Some(server_info) => {
if server_info.contains_key("title") {
match server_info.get("title") {

View file

@ -91,23 +91,6 @@ pub(crate) fn create_empty_config() -> Config {
mod tests {
use crate::{api, dummy::create_empty_config, run};
#[actix_rt::test]
async fn caps_test_with_empty_config() {
unsafe {
crate::api::CONFIG = Some(create_empty_config());
println!("{:?}", crate::api::CONFIG);
}
println!("{:?}", crate::api::caps().await);
}
#[actix_rt::test]
async fn caps_test_no_config() {
unsafe {
println!("{:?}", crate::api::CONFIG);
}
println!("{:?}", crate::api::caps().await);
}
#[actix_rt::test]
async fn api_with_empty_config() {
run(create_empty_config()).await.unwrap();

View file

@ -8,11 +8,19 @@ use rocket::{self};
/// Runs the server
pub async fn run(conf: data::Config) -> Result<bool, rocket::Error> {
unsafe {
api::CONFIG = Some(conf);
}
match rocket::build()
.mount("/", rocket::routes![api::caps, api::search])
.mount(
"/",
rocket::routes![
api::caps,
api::search,
api::tv_search,
api::movie_search,
api::music_search,
api::book_search
],
)
.manage(conf)
.launch()
.await
{

View file

@ -5,7 +5,7 @@
//! - Please implement the `season`, `ep`, and `id` attributes for torrents when possible
//! - Implementing `id`, at least, is far out of scope of this library, and providing `season` and `ep` more effective than this library parsing for them. However, parsing for those as an optional fallback may be added later.
//! - See [here](https://torznab.github.io/spec-1.3-draft/revisions/1.0-Torznab-Torrent-Support.html) for details
//!
//!
//! TODO: Add better docs for using the library
// TODO: Add parsing for `season` and `ep`