Start working on reading config from env, cli, and toml

This commit is contained in:
SeanOMik 2022-06-13 22:44:23 -04:00
parent 0de10cbffe
commit 5426c6186e
7 changed files with 2281 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
/target
.vscode
# Debug related directories that we don't want included
/torrents
/output
config.toml

1996
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

17
Cargo.toml Normal file
View File

@ -0,0 +1,17 @@
[package]
name = "cross-seed"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
tokio = { version = "1.19.2", features = ["full"] }
toml = "0.5.9"
lava_torrent = "0.7.0" # https://docs.rs/lava_torrent/0.7.0/lava_torrent/
torznab = "0.7.2" # https://docs.rs/torznab/0.7.2/torznab/
magnet-url = "2.0.0"
serde = { version = "1.0", features = ["derive"] }
figment = { version = "0.10", features = ["toml", "env"] }
wild = "2.0.4"
argmap = "1.1.2"

View File

@ -0,0 +1,94 @@
use std::sync::Arc;
use figment::{Provider, Metadata, Profile, Error};
use figment::value::{Map, Dict, Value, Tag};
use serde::Deserialize;
/// A provider that fetches its data from a given URL.
pub struct CliProvider {
/// The profile to emit data to if nesting is disabled.
profile: Option<Profile>,
args: Vec<std::string::String>,
}
impl CliProvider {
pub fn new() -> CliProvider {
CliProvider {
profile: None,
args: wild::args().collect(),
}
}
}
impl Provider for CliProvider {
/// Returns metadata with kind `Network`, custom source `self.url`,
/// and interpolator that returns a URL of `url/a/b/c` for key `a.b.c`.
fn metadata(&self) -> Metadata {
let args = &self.args;
Metadata::named("CLI Flags")
.source(args.join(" "))
//.source(args.map(|args| args.collect::<Vec<_>>().join(" ")).unwrap_or(String::default()))
/* .interpolater(move |profile, keys| match profile.is_custom() {
true => format!("{}/{}/{}", url, profile, keys.join("/")),
false => format!("{}/{}", url, keys.join("/")),
}) */
}
/// Fetches the data from `self.url`. Note that `Dict`, `Map`, and
/// `Profile` are `Deserialize`, so we can deserialized to them.
fn data(&self) -> Result<Map<Profile, Dict>, Error> {
// Parse a `Value` from a `String`
fn parse_from_string(string: &String) -> Value {
// TODO: Other integer types
match string.parse::<i32>() {
Ok(i) => Value::Num(Tag::Default, figment::value::Num::I32(i)),
Err(_) => match string.parse::<bool>() {
Ok(b) => Value::Bool(Tag::Default, b),
Err(_) => Value::from(string.to_owned()),
},
}
}
fn fetch<'a, T: Deserialize<'a>>(args: &Vec<std::string::String>) -> Result<T, Error> {
let (args, argv) = argmap::parse(args.iter());
let mut dict = Dict::new();
for (key, vals) in argv {
let len = vals.len();
if len == 0 {
continue;
}
let key_vec: Vec<&str> = key.split(".").collect();
for key in key_vec.iter() {
dict.insert(key.to_owned(), Value::from(key.to_owned()));
}
if len == 1 {
dict.insert(key, parse_from_string(&vals[0]));
} else {
let mut values = Vec::new();
for val in &vals {
values.push(parse_from_string(val));
}
dict.insert(key, Value::Array(Tag::Default, values));
}
}
Ok(T::deserialize(dict).unwrap())
//Ok(T::deserialize(args.unwrap_or(&std::env::args()))?)
//Profile::default()
}
match &self.profile {
// Don't nest: `fetch` into a `Dict`.
Some(profile) => Ok(profile.collect(fetch(&self.args)?)),
// Nest: `fetch` into a `Map<Profile, Dict>`.
None => fetch(&self.args),
}
}
}

87
src/config/config.rs Normal file
View File

@ -0,0 +1,87 @@
use serde::{Deserialize,Serialize};
use std::path::Path;
use std::env;
use std::collections::HashMap;
use figment::{Figment, providers::{Format, Toml, Env}};
use figment::value::Value as FigmentValue;
use super::CliProvider;
#[derive(Deserialize, Serialize)]
pub struct Config {
/// The path of the torrents to search.
torrents_path: String,
/// The output path of the torrents.
output_path: Option<String>,
//pub indexers: HashMap<String, Indexer>,
/// Used for deserializing the indexers into a Vec<Indexer>.
#[serde(rename = "indexers")]
indexers_map: HashMap<String, FigmentValue>,
/// The indexers to search.
#[serde(skip)]
pub indexers: Vec<Indexer>,
}
#[derive(Deserialize, Serialize)]
pub struct Indexer {
#[serde(skip_deserializing)]
pub name: String,
pub enabled: Option<bool>,
pub url: String,
pub api_key: String,
}
// Allow dead code for functions. We should probably remove this later on.
#[allow(dead_code)]
impl Config {
pub fn new() -> Config {
// The path of the config file without the file extension
let path = match env::var("CROSS_SEED_CONFIG") {
Ok(path) => path,
Err(_) => "config".to_string(),
};
// TODO: Create a command line argument `Provider` (https://docs.rs/figment/0.10.6/figment/trait.Provider.html)
// TODO: Figure out priority
// Merge the config files
let figment = Figment::new()
.join(Toml::file(format!("{}.toml", path)))
.join(Env::prefixed("CROSS_SEED_"))
.join(CliProvider::new());
let mut config: Config = figment.extract().unwrap();
// Parse the indexers map into a vector.
for (name, value) in &mut config.indexers_map {
let mut indexer: Indexer = value.deserialize().unwrap();
indexer.name = name.to_owned();
config.indexers.push(indexer);
}
config
}
pub fn torrents_path(&self) -> &Path {
Path::new(&self.torrents_path)
}
pub fn torrents_path_str(&self) -> &String {
&self.torrents_path
}
pub fn output_path(&self) -> Option<&Path> {
match self.output_path {
Some(ref path) => Some(Path::new(path)),
None => None,
}
}
pub fn output_path_str(&self) -> Option<&String> {
self.output_path.as_ref()
}
}

5
src/config/mod.rs Normal file
View File

@ -0,0 +1,5 @@
pub mod config;
pub use config::Config;
pub mod cli_provider;
pub use cli_provider::CliProvider;

75
src/main.rs Normal file
View File

@ -0,0 +1,75 @@
mod config;
use config::Config;
use std::path::{Path, PathBuf};
use std::error::Error;
use lava_torrent::torrent::v1::Torrent;
use torznab::Client as TorznabClient;
fn read_torrents(path: &Path) -> Result<Vec<PathBuf>, Box<dyn Error>> {
let mut torrents = Vec::new();
for entry in path.read_dir()? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
let filename = path.file_name().unwrap().to_str().unwrap();
if filename.ends_with(".torrent") {
torrents.push(path);
}
} else {
let mut inner = read_torrents(&path)?;
torrents.append(&mut inner);
}
}
return Ok(torrents);
}
fn main() {
// Get config and debug the torrents
let config = Config::new();//.expect("Failed to get config");
println!("Searching torrents in: {}", config.torrents_path_str());
println!("Searching {} trackers: ", config.indexers.len());
for indexer in config.indexers.iter() {
println!(" {}: {}", indexer.name, indexer.url);
}
let torrents = read_torrents(config.torrents_path()).unwrap();
for torrent_path in torrents.iter() {
let torrent = Torrent::read_from_file(torrent_path).unwrap();
println!("{}:", torrent.name);
/* for indexer in config.indexers.iter() {
if indexer.enabled {
let client = TorznabClient::new(indexer.url.clone());
let results = client.search(&torrent).unwrap();
println!("{}", results);
}
} */
//TorznabClient
/*if let Some(announce) = torrent.announce {
println!(" Announce: {}", announce);
}
if let Some(announce_list) = torrent.announce_list {
println!(" Announce list:");
for announce in announce_list {
for ann in announce {
println!(" {}", ann);
}
}
}
println!(" Files:");
if let Some(files) = torrent.files {
for file in files.iter() {
println!(" {}", file.path.to_str().unwrap());
}
} */
}
}