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" version = "0.1.0"
dependencies = [ dependencies = [
"actix-rt", "actix-rt",
"lazy_static",
"rocket", "rocket",
"serde", "serde",
"xml-rs", "xml-rs",

View file

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

View file

@ -1,10 +1,9 @@
//! Contains the actual Torznab API //! Contains the actual Torznab API
use crate::data::*; use crate::data::*;
use lazy_static::lazy_static;
use rocket::http::Status; use rocket::http::Status;
use rocket::response::status; use rocket::response::status;
use rocket::FromForm;
use rocket::{get, response::content::RawXml}; use rocket::{get, response::content::RawXml};
use rocket::{FromForm, State};
use std::str; use std::str;
use xml::writer::{EmitterConfig, XmlEvent}; 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`) /// 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. /// Note that an apikey is *not* required for this function, regardless of whether it's required for the rest.
#[get("/api?t=caps")] #[get("/api?t=caps", rank = 1)]
pub(crate) async fn caps() -> status::Custom<RawXml<String>> { pub(crate) async fn caps(conf: &State<Config>) -> 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();
}
}
}
let buffer = Vec::new(); let buffer = Vec::new();
let mut writer = EmitterConfig::new().create_writer(buffer); 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 // Add the search types
writer.write(XmlEvent::start_element("searching")).unwrap(); writer.write(XmlEvent::start_element("searching")).unwrap();
for item in conf.caps.searching { for item in &conf.caps.searching {
let mut available = "yes"; let mut available = "yes";
if !item.available { if !item.available {
available = "no"; 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::end_element()).unwrap(); // close `searching`
writer.write(XmlEvent::start_element("categories")).unwrap(); writer.write(XmlEvent::start_element("categories")).unwrap();
for i in conf.caps.categories { for i in &conf.caps.categories {
writer writer
.write( .write(
XmlEvent::start_element("category") XmlEvent::start_element("category")
@ -173,7 +142,7 @@ pub(crate) async fn caps() -> status::Custom<RawXml<String>> {
.attr("name", i.name.as_str()), .attr("name", i.name.as_str()),
) )
.unwrap(); .unwrap();
for j in i.subcategories { for j in &i.subcategories {
writer writer
.write( .write(
XmlEvent::start_element("subcat") 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` writer.write(XmlEvent::end_element()).unwrap(); // close `categories`
match conf.caps.genres { match &conf.caps.genres {
Some(genres) => { Some(genres) => {
writer.write(XmlEvent::start_element("genres")).unwrap(); writer.write(XmlEvent::start_element("genres")).unwrap();
@ -207,7 +176,7 @@ pub(crate) async fn caps() -> status::Custom<RawXml<String>> {
None => {} None => {}
} }
match conf.caps.tags { match &conf.caps.tags {
Some(tags) => { Some(tags) => {
writer.write(XmlEvent::start_element("tags")).unwrap(); 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)); return status::Custom(Status::Ok, RawXml(result));
} }
#[get("/api?t=search&<form..>")] #[get("/api?t=search&<form..>", rank = 2)]
/// The general search function /// The general search function
pub(crate) async fn search(form: SearchForm) -> status::Custom<RawXml<String>> { pub(crate) async fn search(
// The compiler won't let you get a field from a struct in the Option here, since the default is None conf: &State<Config>,
// So this is needed form: SearchForm,
let conf; ) -> status::Custom<RawXml<String>> {
unsafe { // oh god this is horrible but it works
if CONFIG.is_none() { let parameters = form.to_parameters((**conf).clone());
return (*STATUS_CONFIG_NOT_SPECIFIED).clone();
} else {
conf = CONFIG.clone().ok_or("").unwrap();
}
}
let parameters = form.to_parameters(conf.clone());
let mut unauthorized = false; let mut unauthorized = false;
match conf.auth { 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; return search_handler(conf, search_parameters).await;
} }
#[get("/api?t=tvsearch&<form..>")] #[get("/api?t=tvsearch&<form..>", rank = 3)]
/// The TV search function /// The TV search function
pub(crate) async fn tv_search(form: SearchForm) -> status::Custom<RawXml<String>> { pub(crate) async fn tv_search(
// The compiler won't let you get a field from a struct in the Option here, since the default is None conf: &State<Config>,
// So this is needed form: SearchForm,
let conf; ) -> status::Custom<RawXml<String>> {
unsafe { // oh god this is horrible but it works
if CONFIG.is_none() { let parameters = form.to_parameters((**conf).clone());
return (*STATUS_CONFIG_NOT_SPECIFIED).clone();
} else {
conf = CONFIG.clone().ok_or("").unwrap();
}
}
let parameters = form.to_parameters(conf.clone());
let mut unauthorized = false; let mut unauthorized = false;
match conf.auth { 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; return search_handler(conf, search_parameters).await;
} }
#[get("/api?t=movie&<form..>")] #[get("/api?t=movie&<form..>", rank = 4)]
/// The movie search function /// The movie search function
pub(crate) async fn movie_search(form: SearchForm) -> status::Custom<RawXml<String>> { pub(crate) async fn movie_search(
// The compiler won't let you get a field from a struct in the Option here, since the default is None conf: &State<Config>,
// So this is needed form: SearchForm,
let conf; ) -> status::Custom<RawXml<String>> {
unsafe { // oh god this is horrible but it works
if CONFIG.is_none() { let parameters = form.to_parameters((**conf).clone());
return (*STATUS_CONFIG_NOT_SPECIFIED).clone();
} else {
conf = CONFIG.clone().ok_or("").unwrap();
}
}
let parameters = form.to_parameters(conf.clone());
let mut unauthorized = false; let mut unauthorized = false;
match conf.auth { 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; return search_handler(conf, search_parameters).await;
} }
#[get("/api?t=music&<form..>")] #[get("/api?t=music&<form..>", rank = 5)]
/// The music search function /// The music search function
pub(crate) async fn music_search(form: SearchForm) -> status::Custom<RawXml<String>> { pub(crate) async fn music_search(
// The compiler won't let you get a field from a struct in the Option here, since the default is None conf: &State<Config>,
// So this is needed form: SearchForm,
let conf; ) -> status::Custom<RawXml<String>> {
unsafe { // oh god this is horrible but it works
if CONFIG.is_none() { let parameters = form.to_parameters((**conf).clone());
return (*STATUS_CONFIG_NOT_SPECIFIED).clone();
} else {
conf = CONFIG.clone().ok_or("").unwrap();
}
}
let parameters = form.to_parameters(conf.clone());
let mut unauthorized = false; let mut unauthorized = false;
match conf.auth { 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; return search_handler(conf, search_parameters).await;
} }
#[get("/api?t=book&<form..>")] #[get("/api?t=book&<form..>", rank = 6)]
/// The music search function /// The music search function
pub(crate) async fn book_search(form: SearchForm) -> status::Custom<RawXml<String>> { pub(crate) async fn book_search(
// The compiler won't let you get a field from a struct in the Option here, since the default is None conf: &State<Config>,
// So this is needed form: SearchForm,
let conf; ) -> status::Custom<RawXml<String>> {
unsafe { // oh god this is horrible but it works
if CONFIG.is_none() { let parameters = form.to_parameters((**conf).clone());
return (*STATUS_CONFIG_NOT_SPECIFIED).clone();
} else {
conf = CONFIG.clone().ok_or("").unwrap();
}
}
let parameters = form.to_parameters(conf.clone());
let mut unauthorized = false; let mut unauthorized = false;
match conf.auth { 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; 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 buffer = Vec::new();
let mut writer = EmitterConfig::new().create_writer(buffer); let mut writer = EmitterConfig::new().create_writer(buffer);
writer writer
@ -499,7 +436,7 @@ async fn search_handler(conf: Config, parameters: SearchParameters) -> status::C
// add `title` // add `title`
writer.write(XmlEvent::start_element("title")).unwrap(); writer.write(XmlEvent::start_element("title")).unwrap();
let mut title_provided = false; let mut title_provided = false;
match conf.caps.server_info { match &conf.caps.server_info {
Some(server_info) => { Some(server_info) => {
if server_info.contains_key("title") { if server_info.contains_key("title") {
match server_info.get("title") { match server_info.get("title") {

View file

@ -91,23 +91,6 @@ pub(crate) fn create_empty_config() -> Config {
mod tests { mod tests {
use crate::{api, dummy::create_empty_config, run}; 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] #[actix_rt::test]
async fn api_with_empty_config() { async fn api_with_empty_config() {
run(create_empty_config()).await.unwrap(); run(create_empty_config()).await.unwrap();

View file

@ -8,11 +8,19 @@ use rocket::{self};
/// Runs the server /// Runs the server
pub async fn run(conf: data::Config) -> Result<bool, rocket::Error> { pub async fn run(conf: data::Config) -> Result<bool, rocket::Error> {
unsafe {
api::CONFIG = Some(conf);
}
match rocket::build() 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() .launch()
.await .await
{ {

View file

@ -5,7 +5,7 @@
//! - Please implement the `season`, `ep`, and `id` attributes for torrents when possible //! - 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. //! - 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 //! - 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 better docs for using the library
// TODO: Add parsing for `season` and `ep` // TODO: Add parsing for `season` and `ep`