mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-10 18:02:17 +02:00
496 lines
19 KiB
Rust
496 lines
19 KiB
Rust
use crate::model::Config;
|
|
use crate::model::{ApiProxyConfig, AppConfig, SourcesConfig};
|
|
use crate::{utils};
|
|
use crate::utils::file_reader;
|
|
use crate::utils::sys_utils::exit;
|
|
use crate::utils::{open_file, read_mappings_file, EnvResolvingReader, FileLockManager};
|
|
use arc_swap::{ArcSwap, ArcSwapAny};
|
|
use chrono::Local;
|
|
use log::{error, info, warn};
|
|
use serde::Serialize;
|
|
use shared::error::{create_tuliprox_error, info_err, TuliproxError, TuliproxErrorKind};
|
|
use shared::model::{ApiProxyConfigDto, AppConfigDto, ConfigDto, ConfigInputAliasDto, ConfigPaths, HdHomeRunDeviceOverview, InputType, SourcesConfigDto, TargetUserDto};
|
|
use shared::utils::{CONSTANTS};
|
|
use std::env;
|
|
use std::fs::File;
|
|
use std::io::{self, Read};
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
use arc_swap::access::{Access};
|
|
use crate::repository::user_repository::{get_api_user_db_path, load_api_user};
|
|
use tokio::fs;
|
|
|
|
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(file_reader(file))
|
|
}
|
|
}
|
|
|
|
pub async fn read_api_proxy_config(config: &AppConfig, resolve_env: bool) -> Result<Option<ApiProxyConfig>, TuliproxError> {
|
|
let paths = <Arc<ArcSwap<ConfigPaths>> as Access<ConfigPaths>>::load(&config.paths);
|
|
let api_proxy_file_path = paths.api_proxy_file_path.as_str();
|
|
if let Some(api_proxy_dto) = read_api_proxy_file(api_proxy_file_path, resolve_env)? {
|
|
let mut errors = vec![];
|
|
let mut api_proxy: ApiProxyConfig = ApiProxyConfig::from(&api_proxy_dto);
|
|
api_proxy.migrate_api_user(config, &mut errors).await;
|
|
if !errors.is_empty() {
|
|
for error in errors {
|
|
error!("{error}");
|
|
}
|
|
}
|
|
Ok(Some(api_proxy))
|
|
} else {
|
|
warn!("cant read api_proxy_config file: {api_proxy_file_path}");
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
pub fn read_sources_file(sources_file: &str, resolve_env: bool, include_computed: bool, hdhr_config: Option<&HdHomeRunDeviceOverview>) -> Result<SourcesConfigDto, TuliproxError> {
|
|
match open_file(&std::path::PathBuf::from(sources_file)) {
|
|
Ok(file) => {
|
|
let maybe_sources: Result<SourcesConfigDto, _> = serde_yaml::from_reader(config_file_reader(file, resolve_env));
|
|
match maybe_sources {
|
|
Ok(mut sources) => {
|
|
if resolve_env {
|
|
if let Err(err) = sources.prepare(include_computed, hdhr_config) {
|
|
return Err(info_err!(format!("Can't read the sources-config file: {sources_file}: {err}")));
|
|
}
|
|
}
|
|
Ok(sources)
|
|
}
|
|
Err(err) => Err(info_err!(format!("Can't read the sources-config file: {sources_file}: {err}")))
|
|
}
|
|
}
|
|
Err(err) => Err(info_err!(format!("Can't read the sources-config file: {sources_file}: {err}")))
|
|
}
|
|
}
|
|
|
|
pub fn read_config_file(config_file: &str, resolve_env: bool, include_computed: bool) -> Result<ConfigDto, TuliproxError> {
|
|
match open_file(&std::path::PathBuf::from(config_file)) {
|
|
Ok(file) => {
|
|
let maybe_config: Result<ConfigDto, _> = serde_yaml::from_reader(config_file_reader(file, resolve_env));
|
|
match maybe_config {
|
|
Ok(mut config) => {
|
|
if resolve_env {
|
|
config.prepare(include_computed)?;
|
|
}
|
|
Ok(config)
|
|
}
|
|
Err(err) => Err(info_err!(format!("Can't read the config file: {config_file}: {err}")))
|
|
}
|
|
}
|
|
Err(err) => Err(info_err!(format!("Can't read the config file: {config_file}: {err}")))
|
|
}
|
|
}
|
|
|
|
pub fn read_app_config_dto(paths: &ConfigPaths,
|
|
resolve_env: bool,
|
|
include_computed: bool) -> Result<AppConfigDto, TuliproxError> {
|
|
let config_file = paths.config_file_path.as_str();
|
|
let sources_file = paths.sources_file_path.as_str();
|
|
let api_proxy_file = paths.api_proxy_file_path.as_str();
|
|
|
|
let config = read_config_file(config_file, resolve_env, include_computed)?;
|
|
let sources = read_sources_file(sources_file, resolve_env, include_computed, config.get_hdhr_device_overview().as_ref())?;
|
|
let mappings = if let Some(mappings_file) = paths.mapping_file_path.as_ref() {
|
|
read_mappings_file(mappings_file, resolve_env).unwrap_or(None)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let api_proxy = read_api_proxy_file(api_proxy_file, resolve_env).unwrap_or(None);
|
|
|
|
Ok(AppConfigDto {
|
|
config,
|
|
sources,
|
|
mappings,
|
|
api_proxy,
|
|
})
|
|
}
|
|
|
|
pub async fn prepare_sources_batch(sources: &mut SourcesConfigDto, include_computed: bool) -> Result<(), TuliproxError> {
|
|
|
|
let mut current_index = 0;
|
|
|
|
for source in &mut sources.sources {
|
|
let max_id_in_source = source.inputs.iter()
|
|
.flat_map(|item| {
|
|
std::iter::once(item.id).chain(
|
|
item.aliases
|
|
.as_ref()
|
|
.into_iter()
|
|
.flatten()
|
|
.map(|alias| alias.id),
|
|
)
|
|
})
|
|
.max()
|
|
.unwrap_or(0);
|
|
|
|
current_index = std::cmp::max(current_index, max_id_in_source);
|
|
}
|
|
|
|
for source in &mut sources.sources {
|
|
for input in &mut source.inputs {
|
|
match get_batch_aliases(input.input_type, input.url.as_str()).await {
|
|
Ok(Some((_, aliases))) => {
|
|
if let Some(idx) = input.prepare_batch(aliases, current_index)? {
|
|
current_index = idx;
|
|
}
|
|
}
|
|
Ok(None) => {}
|
|
Err(err) => {
|
|
error!("Failed to read config files aliases: {err}");
|
|
return Err(err);
|
|
}
|
|
}
|
|
// we need to prepare epg after alias, because epg `auto` depends on the first input url.
|
|
input.prepare_epg(include_computed)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn get_batch_aliases(input_type: InputType, url: &str) -> Result<Option<(PathBuf, Vec<ConfigInputAliasDto>)>, TuliproxError> {
|
|
if input_type == InputType::M3uBatch || input_type == InputType::XtreamBatch {
|
|
return match utils::csv_read_inputs(input_type, url).await {
|
|
Ok((file_path, batch_aliases)) => {
|
|
Ok(Some((file_path, batch_aliases)))
|
|
}
|
|
Err(err) => {
|
|
Err(TuliproxError::new(TuliproxErrorKind::Info, err.to_string()))
|
|
}
|
|
};
|
|
}
|
|
Ok(None)
|
|
}
|
|
|
|
pub async fn prepare_users(app_config_dto: &mut AppConfigDto, app_config: &AppConfig) -> Result<(), TuliproxError> {
|
|
let use_user_db = app_config_dto
|
|
.api_proxy
|
|
.as_ref()
|
|
.is_some_and(|p| p.use_user_db);
|
|
|
|
if use_user_db {
|
|
let user_db_path = get_api_user_db_path(app_config);
|
|
if user_db_path.exists() {
|
|
match load_api_user(app_config).await {
|
|
Ok(stored_users) => if let Some(api_proxy) = app_config_dto.api_proxy.as_mut() {
|
|
api_proxy.user.extend(stored_users.iter().map(TargetUserDto::from));
|
|
},
|
|
Err(err) => {
|
|
warn!("Failed to load users from DB at {}: {err}", user_db_path.display());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn read_initial_app_config(paths: &mut ConfigPaths,
|
|
resolve_env: bool,
|
|
include_computed: bool,
|
|
server_mode: bool) -> Result<AppConfig, TuliproxError> {
|
|
let config_path = paths.config_path.as_str();
|
|
let config_file = paths.config_file_path.as_str();
|
|
let sources_file = paths.sources_file_path.as_str();
|
|
|
|
let config_dto = read_config_file(config_file, resolve_env, include_computed)?;
|
|
let mut sources_dto = read_sources_file(sources_file, resolve_env, include_computed, config_dto.get_hdhr_device_overview().as_ref())?;
|
|
prepare_sources_batch(&mut sources_dto, include_computed).await?;
|
|
let sources: SourcesConfig = SourcesConfig::try_from(sources_dto)?;
|
|
let mut config: Config = Config::from(config_dto);
|
|
config.prepare(config_path)?;
|
|
config.update_runtime();
|
|
|
|
if paths.mapping_file_path.is_none() {
|
|
let mut path = config.mapping_path.as_ref().map_or_else(|| utils::get_default_mappings_path(config_path), ToString::to_string);
|
|
if resolve_env {
|
|
path = resolve_env_var(&path);
|
|
}
|
|
paths.mapping_file_path.replace(path);
|
|
}
|
|
|
|
let mut app_config = AppConfig {
|
|
config: Arc::new(ArcSwap::from_pointee(config)),
|
|
sources: Arc::new(ArcSwap::from_pointee(sources)),
|
|
hdhomerun: Arc::new(ArcSwapAny::default()),
|
|
api_proxy: Arc::new(ArcSwapAny::default()),
|
|
paths: Arc::new(ArcSwap::from_pointee(paths.clone())),
|
|
file_locks: Arc::new(FileLockManager::default()),
|
|
custom_stream_response: Arc::new(ArcSwapAny::default()),
|
|
access_token_secret: Default::default(),
|
|
encrypt_secret: Default::default(),
|
|
};
|
|
app_config.prepare(include_computed)?;
|
|
//print_info(&app_config);
|
|
|
|
if let Some(mappings_file) = &paths.mapping_file_path {
|
|
match utils::read_mappings(mappings_file.as_str(), resolve_env) {
|
|
Ok(Some(mappings)) => app_config.set_mappings(mappings_file, &mappings),
|
|
Ok(None) => info!("Mapping file: not used"),
|
|
Err(err) => exit!("{err}"),
|
|
}
|
|
}
|
|
|
|
if server_mode {
|
|
match read_api_proxy_config(&app_config, resolve_env).await {
|
|
Ok(Some(api_proxy)) => app_config.set_api_proxy(api_proxy)?,
|
|
Ok(None) => info!("Api-Proxy file: not used"),
|
|
Err(err) => exit!("{err}"),
|
|
}
|
|
}
|
|
|
|
Ok(app_config)
|
|
}
|
|
|
|
pub fn read_api_proxy_file(api_proxy_file: &str, resolve_env: bool) -> Result<Option<ApiProxyConfigDto>, TuliproxError> {
|
|
open_file(&std::path::PathBuf::from(api_proxy_file)).map_or(Ok(None), |file| {
|
|
let maybe_api_proxy: Result<ApiProxyConfigDto, _> = serde_yaml::from_reader(config_file_reader(file, resolve_env));
|
|
match maybe_api_proxy {
|
|
Ok(mut api_proxy_dto) => {
|
|
if resolve_env {
|
|
if let Err(err) = api_proxy_dto.prepare() {
|
|
exit!("cant read api-proxy-config file: {err}");
|
|
}
|
|
}
|
|
Ok(Some(api_proxy_dto))
|
|
}
|
|
Err(err) => {
|
|
Err(info_err!(format!("cant read api-proxy-config file: {err}")))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
pub async fn read_api_proxy(config: &AppConfig, resolve_env: bool) -> Option<ApiProxyConfig> {
|
|
let paths = <Arc<ArcSwap<ConfigPaths>> as Access<ConfigPaths>>::load(&config.paths);
|
|
match read_api_proxy_file(paths.api_proxy_file_path.as_str(), resolve_env) {
|
|
Ok(Some(api_proxy_dto)) => {
|
|
let mut errors = vec![];
|
|
let mut api_proxy: ApiProxyConfig = api_proxy_dto.into();
|
|
api_proxy.migrate_api_user(config, &mut errors).await;
|
|
if !errors.is_empty() {
|
|
for error in errors {
|
|
error!("{error}");
|
|
}
|
|
}
|
|
Some(api_proxy)
|
|
}
|
|
Ok(None) => None,
|
|
Err(err) => {
|
|
error!("Failed to read Api-Proxy file {err}");
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn write_config_file<T>(file_path: &str, backup_dir: &str, config: &T, default_name: &str) -> Result<(), TuliproxError>
|
|
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());
|
|
let backup_path = PathBuf::from(backup_dir).join(format!("{filename}_{}", Local::now().format("%Y%m%d_%H%M%S")));
|
|
|
|
|
|
match fs::copy(&path, &backup_path).await {
|
|
Ok(_) => {}
|
|
Err(err) => { error!("Could not backup file {}:{}", &backup_path.to_str().unwrap_or("?"), err) }
|
|
}
|
|
info!("Saving file to {}", &path.to_str().unwrap_or("?"));
|
|
|
|
let serialized = serde_yaml::to_string(config)
|
|
.map_err(|err| create_tuliprox_error!(TuliproxErrorKind::Info, "Could not serialize file {}: {}", &path.to_str().unwrap_or("?"), err))?;
|
|
|
|
fs::write(&path, serialized)
|
|
.await
|
|
.map_err(|err| create_tuliprox_error!(TuliproxErrorKind::Info, "Could not write file {}: {}", &path.to_str().unwrap_or("?"), err))
|
|
}
|
|
|
|
pub fn format_sources_yaml_panel_api_query_params_flow_style(yaml: &str) -> String {
|
|
let has_trailing_newline = yaml.ends_with('\n');
|
|
let lines: Vec<&str> = yaml.split_terminator('\n').collect();
|
|
let mut out: Vec<String> = Vec::with_capacity(lines.len());
|
|
|
|
let mut panel_api_indent: Option<usize> = None;
|
|
let mut query_indent: Option<usize> = None;
|
|
let mut section_indent: Option<usize> = None;
|
|
|
|
let mut i = 0usize;
|
|
while i < lines.len() {
|
|
let line = lines[i];
|
|
let trimmed = line.trim_start();
|
|
let indent = line.len().saturating_sub(trimmed.len());
|
|
|
|
if !trimmed.is_empty() && !trimmed.starts_with('#') {
|
|
if let Some(pi) = panel_api_indent {
|
|
if indent <= pi && trimmed != "panel_api:" {
|
|
panel_api_indent = None;
|
|
query_indent = None;
|
|
section_indent = None;
|
|
}
|
|
}
|
|
if let Some(qi) = query_indent {
|
|
if indent <= qi && trimmed != "query_parameter:" {
|
|
query_indent = None;
|
|
section_indent = None;
|
|
}
|
|
}
|
|
if let Some(si) = section_indent {
|
|
if indent < si {
|
|
section_indent = None;
|
|
}
|
|
}
|
|
|
|
if panel_api_indent.is_none() && trimmed == "panel_api:" {
|
|
panel_api_indent = Some(indent);
|
|
} else if panel_api_indent.is_some() && query_indent.is_none() && trimmed == "query_parameter:" {
|
|
query_indent = Some(indent);
|
|
} else if query_indent.is_some()
|
|
&& matches!(trimmed, "client_info:" | "client_new:" | "client_renew:")
|
|
{
|
|
section_indent = Some(indent);
|
|
}
|
|
}
|
|
|
|
let in_target_section = panel_api_indent.is_some() && query_indent.is_some() && section_indent.is_some();
|
|
if in_target_section
|
|
&& indent == section_indent.unwrap_or(usize::MAX)
|
|
&& trimmed.starts_with("- key:")
|
|
&& !trimmed.starts_with("- {")
|
|
&& i + 1 < lines.len()
|
|
{
|
|
let next = lines[i + 1];
|
|
let next_trimmed = next.trim_start();
|
|
let next_indent = next.len().saturating_sub(next_trimmed.len());
|
|
|
|
if next_trimmed.starts_with("value:")
|
|
&& next_indent == indent.saturating_add(2)
|
|
{
|
|
let key_val = trimmed
|
|
.strip_prefix("- key:")
|
|
.unwrap_or_default()
|
|
.trim();
|
|
let value_val = next_trimmed
|
|
.strip_prefix("value:")
|
|
.unwrap_or_default()
|
|
.trim();
|
|
|
|
let new_line = format!(
|
|
"{}- {{ key: {}, value: {} }}",
|
|
" ".repeat(indent),
|
|
key_val,
|
|
value_val
|
|
);
|
|
out.push(new_line);
|
|
i += 2;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
out.push(line.to_string());
|
|
i += 1;
|
|
}
|
|
|
|
if has_trailing_newline {
|
|
out.join("\n") + "\n"
|
|
} else {
|
|
out.join("\n")
|
|
}
|
|
}
|
|
|
|
pub async fn save_api_proxy(file_path: &str, backup_dir: &str, config: &ApiProxyConfigDto) -> Result<(), TuliproxError> {
|
|
write_config_file(file_path, backup_dir, config, "api-proxy.yml").await
|
|
}
|
|
|
|
pub async fn save_main_config(file_path: &str, backup_dir: &str, config: &ConfigDto) -> Result<(), TuliproxError> {
|
|
write_config_file(file_path, backup_dir, config, "config.yml").await
|
|
}
|
|
|
|
pub async fn save_sources_config(file_path: &str, backup_dir: &str, config: &SourcesConfigDto) -> Result<(), TuliproxError> {
|
|
let path = PathBuf::from(file_path);
|
|
let filename = path.file_name().map_or("source.yml".to_string(), |f| f.to_string_lossy().to_string());
|
|
let backup_path = PathBuf::from(backup_dir).join(format!("{filename}_{}", Local::now().format("%Y%m%d_%H%M%S")));
|
|
|
|
match fs::copy(&path, &backup_path).await {
|
|
Ok(_) => {}
|
|
Err(err) => { error!("Could not backup file {}:{}", &backup_path.to_str().unwrap_or("?"), err) }
|
|
}
|
|
info!("Saving file to {}", &path.to_str().unwrap_or("?"));
|
|
|
|
let serialized = serde_yaml::to_string(config)
|
|
.map_err(|err| create_tuliprox_error!(TuliproxErrorKind::Info, "Could not serialize file {}: {}", &path.to_str().unwrap_or("?"), err))?;
|
|
let serialized = format_sources_yaml_panel_api_query_params_flow_style(&serialized);
|
|
|
|
fs::write(&path, serialized)
|
|
.await
|
|
.map_err(|err| create_tuliprox_error!(TuliproxErrorKind::Info, "Could not write file {}: {}", &path.to_str().unwrap_or("?"), err))
|
|
}
|
|
|
|
pub fn resolve_env_var(value: &str) -> String {
|
|
if value.is_empty() {
|
|
return String::new();
|
|
}
|
|
CONSTANTS.re_env_var.replace_all(value, |caps: ®ex::Captures| {
|
|
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}}}")
|
|
})
|
|
}).to_string()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::utils::resolve_env_var;
|
|
use crate::utils::format_sources_yaml_panel_api_query_params_flow_style;
|
|
|
|
#[test]
|
|
fn test_resolve() {
|
|
let resolved = resolve_env_var("${env:HOME}");
|
|
assert_eq!(resolved, std::env::var("HOME").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn test_sources_yaml_panel_api_query_params_flow_style() {
|
|
let input = r"sources:
|
|
- inputs:
|
|
- name: demo
|
|
panel_api:
|
|
url: https://example.invalid/api.php
|
|
query_parameter:
|
|
client_info:
|
|
- key: api_key
|
|
value: auto
|
|
- key: sub
|
|
value: '1'
|
|
client_new:
|
|
- key: type
|
|
value: m3u
|
|
";
|
|
let out = format_sources_yaml_panel_api_query_params_flow_style(input);
|
|
assert!(out.contains("- { key: api_key, value: auto }"));
|
|
assert!(out.contains("- { key: sub, value: '1' }"));
|
|
assert!(out.contains("- { key: type, value: m3u }"));
|
|
assert!(!out.contains("- key: api_key\n"));
|
|
}
|
|
}
|