Files
tuliprox/src/utils/file/config_reader.rs
T

426 lines
16 KiB
Rust
Raw Normal View History

2025-05-10 18:11:30 +02:00
use crate::tuliprox_error::{create_tuliprox_error, create_tuliprox_error_result, handle_tuliprox_error_result, info_err, str_to_io_error, to_io_error, TuliProxError, TuliProxErrorKind};
2025-05-04 10:59:08 +02:00
use crate::model::ApiProxyConfig;
use crate::model::{Config, ConfigDto, ConfigInput, ConfigInputAlias, InputType};
use crate::model::Mappings;
use crate::utils::CONSTANTS;
use crate::utils::env_resolving_reader::EnvResolvingReader;
use crate::utils::file_utils::{file_reader, resolve_relative_path};
use crate::utils::{file_utils, multi_file_reader};
use crate::utils::request::{get_credentials_from_url, get_local_file_content};
2025-03-20 18:27:34 +01:00
use crate::utils::sys_utils::exit;
2023-10-26 19:59:55 +02:00
use chrono::Local;
use log::{debug, error, info, warn};
use serde::Serialize;
2025-03-20 18:27:34 +01:00
use std::env;
use std::fs::File;
2025-04-16 18:51:16 +02:00
use std::io::{self, BufRead, BufReader, Cursor, Error, Read};
2025-03-20 18:27:34 +01:00
use std::path::PathBuf;
use url::Url;
enum EitherReader<L, R> {
Left(L),
Right(R),
}
// `Read`-Trait für Either implementieren
impl<L: Read, R: Read> Read for EitherReader<L, R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self {
EitherReader::Left(reader) => reader.read(buf),
EitherReader::Right(reader) => reader.read(buf),
}
}
}
pub fn config_file_reader(file: File, resolve_env: bool) -> impl Read
{
if resolve_env {
EitherReader::Left(EnvResolvingReader::new(file_reader(file)))
} else {
EitherReader::Right(BufReader::new(file))
}
}
2025-05-10 18:11:30 +02:00
pub fn read_mappings(args_mapping: Option<String>, cfg: &mut Config, resolve_env: bool) -> Result<Option<String>, TuliProxError> {
2024-11-04 18:46:56 +01:00
let mappings_file: String = args_mapping.unwrap_or_else(|| file_utils::get_default_mappings_path(cfg.t_config_path.as_str()));
match read_mapping(mappings_file.as_str(), resolve_env) {
Ok(mappings) => {
2024-09-11 19:41:50 +02:00
match mappings {
2025-01-10 13:07:35 +01:00
None => {
debug!("no mapping loaded");
Ok(Some(mappings_file))
}
Some(mappings_cfg) => {
cfg.set_mappings(&mappings_cfg);
Ok(None)
}
2024-09-11 19:41:50 +02:00
}
}
Err(err) => Err(err),
}
}
2025-05-10 18:11:30 +02:00
pub async fn read_api_proxy_config(args_api_proxy_config: Option<String>, cfg: &mut Config) -> Result<Option<String>, TuliProxError> {
2024-11-04 18:46:56 +01:00
let api_proxy_config_file: String = args_api_proxy_config.unwrap_or_else(|| file_utils::get_default_api_proxy_config_path(cfg.t_config_path.as_str()));
2024-05-10 12:01:48 +02:00
api_proxy_config_file.clone_into(&mut cfg.t_api_proxy_file_path);
2025-02-25 19:07:26 +01:00
let api_proxy_config = read_api_proxy(cfg, api_proxy_config_file.as_str(), true);
match api_proxy_config {
None => {
warn!("cant read api_proxy_config file: {}", api_proxy_config_file.as_str());
Ok(None)
}
Some(config) => {
2025-03-11 14:56:10 +01:00
cfg.set_api_proxy(Some(config)).await?;
Ok(Some(api_proxy_config_file))
}
}
}
2025-05-10 18:11:30 +02:00
pub fn read_config(config_path: &str, config_file: &str, sources_file: &str, include_computed: bool) -> Result<Config, TuliProxError> {
let files = vec![std::path::PathBuf::from(config_file), std::path::PathBuf::from(sources_file)];
2024-03-26 12:34:57 +01:00
match multi_file_reader::MultiFileReader::new(&files) {
Ok(reader) => {
match serde_yaml::from_reader::<_, Config>(reader) {
2023-12-04 17:27:34 +01:00
Ok(mut result) => {
2024-05-10 12:01:48 +02:00
result.t_config_path = config_path.to_string();
result.t_config_file_path = config_file.to_string();
result.t_sources_file_path = sources_file.to_string();
2025-04-17 23:09:15 +02:00
match result.prepare(include_computed) {
2024-05-10 10:13:50 +02:00
Err(err) => Err(err),
_ => Ok(result),
2023-12-04 17:27:34 +01:00
}
}
Err(e) => {
2025-05-10 18:11:30 +02:00
create_tuliprox_error_result!(TuliProxErrorKind::Info, "cant read config file: {}", e)
}
}
}
2025-05-10 18:11:30 +02:00
Err(err) => create_tuliprox_error_result!(TuliProxErrorKind::Info, "{}", err)
}
}
2025-05-10 18:11:30 +02:00
pub fn read_mapping(mapping_file: &str, resolve_var: bool) -> Result<Option<Mappings>, TuliProxError> {
2023-10-20 17:54:25 +02:00
let mapping_file = std::path::PathBuf::from(mapping_file);
2024-05-10 10:13:50 +02:00
if let Ok(file) = file_utils::open_file(&mapping_file) {
let mapping: Result<Mappings, _> = serde_yaml::from_reader(config_file_reader(file, resolve_var));
2025-04-24 20:59:07 +02:00
return match mapping {
2024-09-18 19:50:38 +02:00
Ok(mut result) => {
2025-05-10 18:11:30 +02:00
handle_tuliprox_error_result!(TuliProxErrorKind::Info, result.prepare());
2025-04-24 20:59:07 +02:00
Ok(Some(result))
2025-03-20 18:27:34 +01:00
}
2024-09-18 19:50:38 +02:00
Err(err) => {
2025-04-24 20:59:07 +02:00
Err(info_err!(err.to_string()))
2024-09-18 19:50:38 +02:00
}
2025-04-24 20:59:07 +02:00
};
}
2024-05-10 10:13:50 +02:00
warn!("cant read mapping file: {}", mapping_file.to_str().unwrap_or("?"));
Ok(None)
}
pub fn read_api_proxy(config: &Config, api_proxy_file: &str, resolve_env: bool) -> Option<ApiProxyConfig> {
2024-11-04 18:46:56 +01:00
file_utils::open_file(&std::path::PathBuf::from(api_proxy_file)).map_or(None, |file| {
let mapping: Result<ApiProxyConfig, _> = serde_yaml::from_reader(config_file_reader(file, resolve_env));
2025-03-20 18:27:34 +01:00
match mapping {
Ok(mut result) => {
match result.prepare(config) {
2025-03-20 18:27:34 +01:00
Err(err) => {
2025-04-10 00:06:48 +02:00
exit!("cant read api-proxy-config file: {err}");
2025-03-20 18:27:34 +01:00
}
_ => {
Some(result)
}
}
}
2025-03-20 18:27:34 +01:00
Err(err) => {
2025-04-10 00:06:48 +02:00
error!("cant read api-proxy-config file: {err}");
2025-03-20 18:27:34 +01:00
None
}
}
})
}
2025-05-10 18:11:30 +02:00
fn write_config_file<T>(file_path: &str, backup_dir: &str, config: &T, default_name: &str) -> Result<(), TuliProxError>
2025-03-20 18:27:34 +01:00
where
T: ?Sized + Serialize,
{
let path = PathBuf::from(file_path);
let filename = path.file_name().map_or(default_name.to_string(), |f| f.to_string_lossy().to_string());
2025-02-26 13:46:23 +01:00
let backup_path = PathBuf::from(backup_dir).join(format!("{filename}_{}", Local::now().format("%Y%m%d_%H%M%S")));
2023-10-26 19:59:55 +02:00
2023-10-26 19:59:55 +02:00
match std::fs::copy(&path, &backup_path) {
Ok(_) => {}
2023-12-04 17:27:34 +01:00
Err(err) => { error!("Could not backup file {}:{}", &backup_path.to_str().unwrap_or("?"), err) }
2023-10-26 19:59:55 +02:00
}
info!("Saving file to {}", &path.to_str().unwrap_or("?"));
2025-01-17 18:32:17 +01:00
File::create(&path)
.and_then(|f| serde_yaml::to_writer(f, &config).map_err(to_io_error))
2025-05-10 18:11:30 +02:00
.map_err(|err| create_tuliprox_error!(TuliProxErrorKind::Info, "Could not write file {}: {}", &path.to_str().unwrap_or("?"), err))
}
2025-05-10 18:11:30 +02:00
pub fn save_api_proxy(file_path: &str, backup_dir: &str, config: &ApiProxyConfig) -> Result<(), TuliProxError> {
write_config_file(file_path, backup_dir, config, "api-proxy.yml")
}
2025-05-10 18:11:30 +02:00
pub fn save_main_config(file_path: &str, backup_dir: &str, config: &ConfigDto) -> Result<(), TuliProxError> {
write_config_file(file_path, backup_dir, config, "config.yml")
}
2024-11-04 18:46:56 +01:00
pub fn resolve_env_var(value: &str) -> String {
2025-03-14 13:15:26 +01:00
if value.is_empty() {
return String::new();
}
2025-04-17 15:11:28 +02:00
CONSTANTS.re_env_var.replace_all(value, |caps: &regex::Captures| {
2024-11-15 17:49:48 +01:00
let var_name = &caps["var"];
env::var(var_name).unwrap_or_else(|e| {
error!("Could not resolve env var '{var_name}': {e}");
format!("${{env:{var_name}}}")
})
2024-11-15 17:49:48 +01:00
}).to_string()
2024-12-31 14:40:31 +01:00
}
const CSV_SEPARATOR: char = ';';
2025-03-20 18:27:34 +01:00
const HEADER_PREFIX: char = '#';
const FIELD_MAX_CON: &str = "max_connections";
const FIELD_PRIO: &str = "priority";
const FIELD_URL: &str = "url";
const FIELD_NAME: &str = "name";
2025-03-21 18:48:13 +01:00
const FIELD_USERNAME: &str = "username";
const FIELD_PASSWORD: &str = "password";
const FIELD_UNKNOWN: &str = "?";
const DEFAULT_COLUMNS: &[&str] = &[FIELD_URL, FIELD_MAX_CON, FIELD_PRIO, FIELD_NAME, FIELD_USERNAME, FIELD_PASSWORD];
2025-04-10 00:06:48 +02:00
fn csv_assign_mandatory_fields(alias: &mut ConfigInputAlias, input_type: InputType) {
if !alias.url.is_empty() {
match Url::parse(alias.url.as_str()) {
Ok(url) => {
2025-03-21 18:48:13 +01:00
let (username, password) = get_credentials_from_url(&url);
if username.is_none() || password.is_none() {
// xtream url
2025-04-10 00:06:48 +02:00
if input_type == InputType::XtreamBatch {
alias.url = url.origin().ascii_serialization().to_string();
2025-04-10 00:06:48 +02:00
} else if input_type == InputType::M3uBatch && alias.username.is_some() && alias.password.is_some() {
alias.url = format!("{}/get_php?username={}&password={}&type=m3u_plus",
url.origin().ascii_serialization(),
alias.username.as_deref().unwrap_or(""),
alias.password.as_deref().unwrap_or("")
);
}
} else {
2025-04-10 00:06:48 +02:00
if input_type == InputType::XtreamBatch {
alias.url = url.origin().ascii_serialization().to_string();
}
2025-03-21 18:48:13 +01:00
// m3u url
alias.username = username;
alias.password = password;
}
if alias.name.is_empty() {
2025-04-10 00:06:48 +02:00
let username = alias.username.as_deref().unwrap_or_default();
2025-03-21 18:48:13 +01:00
let domain: Vec<&str> = url.domain().unwrap_or_default().split('.').collect();
if domain.len() > 1 {
alias.name = format!("{}_{username}", domain[domain.len() - 2]);
} else {
2025-04-10 00:06:48 +02:00
alias.name = username.to_string();
2025-03-21 18:48:13 +01:00
}
}
}
2025-03-21 17:15:28 +01:00
Err(_err) => {}
}
}
}
2025-03-20 18:27:34 +01:00
fn csv_assign_config_input_column(config_input: &mut ConfigInputAlias, header: &str, raw_value: &str) -> Result<(), io::Error> {
let value = raw_value.trim();
if !value.is_empty() {
2025-03-20 18:27:34 +01:00
match header {
FIELD_URL => {
let url = Url::parse(value.trim()).map_err(to_io_error)?;
config_input.url = url.to_string();
}
2025-03-20 18:27:34 +01:00
FIELD_MAX_CON => {
let max_connections = value.parse::<u16>().unwrap_or(1);
config_input.max_connections = max_connections;
}
2025-03-20 18:27:34 +01:00
FIELD_PRIO => {
let priority = value.parse::<i16>().unwrap_or(0);
config_input.priority = priority;
}
FIELD_NAME => {
config_input.name = value.to_string();
}
FIELD_USERNAME => {
config_input.username = Some(value.to_string());
}
FIELD_PASSWORD => {
config_input.password = Some(value.to_string());
}
2025-03-20 18:27:34 +01:00
_ => {}
}
}
2025-03-20 18:27:34 +01:00
Ok(())
}
pub fn csv_read_inputs_from_reader(batch_input_type: InputType, reader: impl BufRead) -> Result<Vec<ConfigInputAlias>, io::Error> {
let input_type = match batch_input_type {
InputType::M3uBatch | InputType::M3u => InputType::M3uBatch,
InputType::XtreamBatch | InputType::Xtream => InputType::XtreamBatch
};
let mut result = vec![];
let mut default_columns = vec![];
default_columns.extend_from_slice(DEFAULT_COLUMNS);
2025-04-10 18:08:11 +02:00
let mut header_defined = false;
2025-04-10 00:06:48 +02:00
for line in reader.lines() {
let line = line?;
if line.is_empty() {
continue;
}
if line.starts_with(HEADER_PREFIX) {
if !header_defined {
2025-04-10 18:08:11 +02:00
header_defined = true;
default_columns = line[1..].split(CSV_SEPARATOR).map(|s| {
match s {
FIELD_URL => FIELD_URL,
FIELD_MAX_CON => FIELD_MAX_CON,
FIELD_PRIO => FIELD_PRIO,
FIELD_NAME => FIELD_NAME,
FIELD_USERNAME => FIELD_USERNAME,
FIELD_PASSWORD => FIELD_PASSWORD,
_ => {
error!("Field {s} is unsupported for csv input");
FIELD_UNKNOWN
}
}
2025-04-10 18:08:11 +02:00
}).collect();
}
continue;
}
let mut config_input = ConfigInputAlias {
id: 0,
2025-04-10 00:06:48 +02:00
name: String::new(),
url: String::new(),
username: None,
password: None,
priority: 0,
max_connections: 1,
t_base_url: String::default(),
};
let columns: Vec<&str> = line.split(CSV_SEPARATOR).collect();
2025-04-10 00:06:48 +02:00
for (&header, &value) in default_columns.iter().zip(columns.iter()) {
if let Err(err) = csv_assign_config_input_column(&mut config_input, header, value) {
2025-04-10 00:06:48 +02:00
error!("Could not parse input line: {line} err: {err}");
2025-03-20 18:27:34 +01:00
}
}
2025-04-10 00:06:48 +02:00
csv_assign_mandatory_fields(&mut config_input, input_type);
result.push(config_input);
}
Ok(result)
}
pub fn csv_read_inputs(input: &ConfigInput) -> Result<Vec<ConfigInputAlias>, io::Error> {
let file_uri = input.url.to_string();
2025-04-16 20:28:02 +02:00
let file_path = get_csv_file_path(&file_uri)?;
2025-04-16 18:51:16 +02:00
match get_local_file_content(&file_path) {
Ok(content) => {
csv_read_inputs_from_reader(input.input_type, EnvResolvingReader::new(file_reader(Cursor::new(content))))
}
Err(err) => {
Err(err)
}
2025-03-20 18:27:34 +01:00
}
2025-04-16 18:51:16 +02:00
}
2025-04-16 18:51:16 +02:00
fn get_csv_file_path(file_uri: &String) -> Result<PathBuf, Error> {
if file_uri.contains("://") {
if let Ok(url) = file_uri.parse::<url::Url>() {
if url.scheme() == "file" {
2025-04-24 20:59:07 +02:00
return match url.to_file_path() {
Ok(path) => Ok(path),
Err(()) => Err(str_to_io_error(&format!("Could not open {file_uri}"))),
};
2025-04-16 18:51:16 +02:00
}
}
Err(str_to_io_error(&format!("Only file:// is supported {file_uri}")))
} else {
2025-04-16 20:28:02 +02:00
resolve_relative_path(file_uri)
2025-04-16 18:51:16 +02:00
}
2025-03-20 18:27:34 +01:00
}
2024-12-31 14:40:31 +01:00
#[cfg(test)]
mod tests {
2025-05-04 10:59:08 +02:00
use crate::model::InputType;
use crate::utils::config_reader::{csv_read_inputs_from_reader, resolve_env_var};
use std::io::{BufReader, Cursor};
const M3U_BATCH: &str = r#"
#url;name;max_connections;priority
http://hd.providerline.com:8080/get.php?username=user1&password=user1&type=m3u_plus;input_1
http://hd.providerline.com/get.php?username=user2&password=user2&type=m3u_plus;input_2;1;2
http://hd.providerline.com/get.php?username=user3&password=user3&type=m3u_plus;input_3;1;2
http://hd.providerline.com/get.php?username=user4&password=user4&type=m3u_plus;input_4
"#;
const XTREAM_BATCH: &str = r#"
#name;username;password;url;max_connections
input_1;desanocra;eyCG8SN523KQ;http://provider_1.tv:80;1
input_2;desanocra;eyCG8SN523KQ;http://provider_2.tv:8080;1
"#;
2025-03-20 18:27:34 +01:00
#[test]
fn test_read_inputs_xtream_as_m3u() {
let reader = BufReader::new(Cursor::new(XTREAM_BATCH));
let result = csv_read_inputs_from_reader(InputType::M3uBatch, reader);
2025-03-20 18:27:34 +01:00
assert_eq!(result.is_ok(), true);
let aliases = result.unwrap();
assert_eq!(aliases.is_empty(), false);
for config in aliases {
assert_eq!(config.url.contains("username"), true);
}
}
#[test]
fn test_read_inputs_m3u_as_m3u() {
let reader = BufReader::new(Cursor::new(M3U_BATCH));
let result = csv_read_inputs_from_reader(InputType::M3uBatch, reader);
assert_eq!(result.is_ok(), true);
let aliases = result.unwrap();
assert_eq!(aliases.is_empty(), false);
for config in aliases {
assert_eq!(config.url.contains("username"), true);
}
}
#[test]
fn test_read_inputs_xtream_as_xtream() {
let reader = BufReader::new(Cursor::new(XTREAM_BATCH));
let result = csv_read_inputs_from_reader(InputType::XtreamBatch, reader);
assert_eq!(result.is_ok(), true);
let aliases = result.unwrap();
assert_eq!(aliases.is_empty(), false);
for config in aliases {
assert_eq!(config.url.contains("username"), false);
}
}
#[test]
fn test_read_inputs_m3u_as_xtream() {
let reader = BufReader::new(Cursor::new(M3U_BATCH));
let result = csv_read_inputs_from_reader(InputType::XtreamBatch, reader);
assert_eq!(result.is_ok(), true);
let aliases = result.unwrap();
assert_eq!(aliases.is_empty(), false);
for config in aliases {
assert_eq!(config.url.contains("username"), false);
}
2025-03-20 18:27:34 +01:00
}
2024-12-31 14:40:31 +01:00
#[test]
fn test_resolve() {
2025-03-20 18:27:34 +01:00
let resolved = resolve_env_var("${env:HOME}");
2024-12-31 14:40:31 +01:00
assert_eq!(resolved, std::env::var("HOME").unwrap());
}
}