Service agnostic DDNS

This commit is contained in:
Antoine Gersant 2024-10-08 21:59:40 -07:00
parent deeb3e8a05
commit 316f5c0219
8 changed files with 46 additions and 92 deletions

1
Cargo.lock generated
View file

@ -1390,7 +1390,6 @@ dependencies = [
"axum-extra",
"axum-range",
"axum-test",
"base64 0.22.1",
"bitcode",
"branca",
"bytes",

View file

@ -12,7 +12,6 @@ ui = ["native-windows-gui", "native-windows-derive"]
ape = "0.5"
axum-extra = { version = "0.9.3", features = ["typed-header"] }
axum-range = "0.4.0"
base64 = "0.22.1"
bitcode = { version = "0.6.3", features = ["serde"] }
branca = "0.10.1"
chumsky = "0.9.3"

View file

@ -82,6 +82,8 @@ pub enum Error {
MiscSettingsNotFound,
#[error("Index album art pattern is not a valid regex")]
IndexAlbumArtPatternInvalid,
#[error("DDNS update URL is invalid")]
DDNSUpdateURLInvalid,
#[error(transparent)]
Toml(#[from] toml::de::Error),
@ -138,6 +140,7 @@ pub struct App {
pub port: u16,
pub web_dir_path: PathBuf,
pub swagger_dir_path: PathBuf,
pub ddns_manager: ddns::Manager,
pub scanner: scanner::Scanner,
pub index_manager: index::Manager,
pub config_manager: config::Manager,
@ -168,6 +171,7 @@ impl App {
let auth_secret = Self::get_or_create_auth_secret(&auth_secret_file_path).await?;
let config_manager = config::Manager::new(&paths.config_file_path, auth_secret).await?;
let ddns_manager = ddns::Manager::new(config_manager.clone());
let ndb_manager = ndb::Manager::new(&paths.data_dir_path)?;
let index_manager = index::Manager::new(&paths.data_dir_path).await?;
let scanner = scanner::Scanner::new(index_manager.clone(), config_manager.clone()).await?;
@ -179,6 +183,7 @@ impl App {
port,
web_dir_path: paths.web_dir_path,
swagger_dir_path: paths.swagger_dir_path,
ddns_manager,
scanner,
index_manager,
config_manager,

View file

@ -22,8 +22,8 @@ use super::auth;
#[derive(Default)]
pub struct Config {
pub reindex_every_n_seconds: Option<u64>,
pub album_art_pattern: Option<String>,
pub ddns_url: Option<String>,
pub album_art_pattern: Option<Regex>,
pub ddns_url: Option<http::Uri>,
pub mount_dirs: Vec<MountDir>,
pub users: HashMap<String, User>,
}
@ -45,10 +45,22 @@ impl TryFrom<storage::Config> for Config {
.filter_map(|m| m.try_into().ok())
.collect();
let ddns_url = match c.ddns_url.map(http::Uri::try_from) {
Some(Ok(u)) => Some(u),
Some(Err(_)) => return Err(Error::DDNSUpdateURLInvalid),
None => None,
};
let album_art_pattern = match c.album_art_pattern.as_deref().map(Regex::new) {
Some(Ok(u)) => Some(u),
Some(Err(_)) => return Err(Error::IndexAlbumArtPatternInvalid),
None => None,
};
Ok(Config {
reindex_every_n_seconds: c.reindex_every_n_seconds, // TODO validate and warn
album_art_pattern: c.album_art_pattern, // TODO validate and warn
ddns_url: c.ddns_url, // TODO validate and warn
album_art_pattern,
ddns_url,
mount_dirs,
users,
})
@ -92,25 +104,25 @@ impl Manager {
// TODO persistence
}
pub async fn get_index_album_art_pattern(&self) -> String {
pub async fn get_index_album_art_pattern(&self) -> Regex {
let config = self.config.read().await;
let pattern = config.album_art_pattern.clone();
pattern.unwrap_or("Folder.(jpeg|jpg|png)".to_owned())
pattern.unwrap_or_else(|| Regex::new("Folder.(jpeg|jpg|png)").unwrap())
}
pub async fn set_index_album_art_pattern(&self, regex: Regex) {
let mut config = self.config.write().await;
config.album_art_pattern = Some(regex.as_str().to_owned());
config.album_art_pattern = Some(regex);
// TODO persistence
}
pub async fn get_ddns_update_url(&self) -> Option<String> {
pub async fn get_ddns_update_url(&self) -> Option<http::Uri> {
self.config.read().await.ddns_url.clone()
}
pub async fn set_ddns_update_url(&self, url: http::Uri) {
let mut config = self.config.write().await;
config.ddns_url = Some(url.to_string());
config.ddns_url = Some(url);
// TODO persistence
}

View file

@ -1,45 +1,26 @@
use base64::prelude::*;
use log::{debug, error};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use crate::app::Error;
use crate::db::DB;
const DDNS_UPDATE_URL: &str = "https://ydns.io/api/v1/update/";
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub struct Config {
pub ddns_host: String,
pub ddns_username: String,
pub ddns_password: String,
}
use crate::app::{config, Error};
#[derive(Clone)]
pub struct Manager {
db: DB,
config_manager: config::Manager,
}
impl Manager {
pub fn new(db: DB) -> Self {
Self { db }
pub fn new(config_manager: config::Manager) -> Self {
Self { config_manager }
}
async fn update_my_ip(&self) -> Result<(), Error> {
let config = self.config().await?;
if config.ddns_host.is_empty() || config.ddns_username.is_empty() {
async fn update_ddns(&self) -> Result<(), Error> {
let url = self.config_manager.get_ddns_update_url().await;
let Some(url) = url else {
debug!("Skipping DDNS update because credentials are missing");
return Ok(());
}
};
let full_url = format!("{}?host={}", DDNS_UPDATE_URL, &config.ddns_host);
let credentials = format!("{}:{}", &config.ddns_username, &config.ddns_password);
let response = ureq::get(full_url.as_str())
.set(
"Authorization",
&format!("Basic {}", BASE64_STANDARD_NO_PAD.encode(credentials)),
)
.call();
let response = ureq::get(&url.to_string()).call();
match response {
Ok(_) => Ok(()),
@ -48,33 +29,12 @@ impl Manager {
}
}
pub async fn config(&self) -> Result<Config, Error> {
Ok(sqlx::query_as!(
Config,
"SELECT ddns_host, ddns_username, ddns_password FROM config"
)
.fetch_one(self.db.connect().await?.as_mut())
.await?)
}
pub async fn set_config(&self, new_config: &Config) -> Result<(), Error> {
sqlx::query!(
"UPDATE config SET ddns_host = $1, ddns_username = $2, ddns_password = $3",
new_config.ddns_host,
new_config.ddns_username,
new_config.ddns_password
)
.execute(self.db.connect().await?.as_mut())
.await?;
Ok(())
}
pub fn begin_periodic_updates(&self) {
tokio::spawn({
let ddns = self.clone();
async move {
loop {
if let Err(e) = ddns.update_my_ip().await {
if let Err(e) = ddns.update_ddns().await {
error!("Dynamic DNS update error: {:?}", e);
}
tokio::time::sleep(Duration::from_secs(60 * 30)).await;

View file

@ -102,12 +102,17 @@ async fn get_settings(
State(config_manager): State<config::Manager>,
) -> Result<Json<dto::Settings>, APIError> {
let settings = dto::Settings {
album_art_pattern: config_manager.get_index_album_art_pattern().await,
album_art_pattern: config_manager
.get_index_album_art_pattern()
.await
.as_str()
.to_owned(),
reindex_every_n_seconds: config_manager.get_index_sleep_duration().await.as_secs(),
ddns_update_url: config_manager
.get_ddns_update_url()
.await
.unwrap_or_default(),
.unwrap_or_default()
.to_string(),
};
Ok(Json(settings))
}

View file

@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
use crate::app::{config, ddns, index, thumbnail};
use crate::app::{config, index, thumbnail};
use std::{convert::From, path::PathBuf};
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)]
@ -95,33 +95,6 @@ pub struct UserUpdate {
pub new_is_admin: Option<bool>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub struct DDNSConfig {
pub host: String,
pub username: String,
pub password: String,
}
impl From<DDNSConfig> for ddns::Config {
fn from(c: DDNSConfig) -> Self {
Self {
ddns_host: c.host,
ddns_username: c.username,
ddns_password: c.password,
}
}
}
impl From<ddns::Config> for DDNSConfig {
fn from(c: ddns::Config) -> Self {
Self {
host: c.ddns_host,
username: c.ddns_username,
password: c.ddns_password,
}
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub struct MountDir {
pub source: PathBuf,

View file

@ -120,7 +120,8 @@ impl From<app::Error> for APIError {
app::Error::AuthenticationSecretNotFound => APIError::Internal,
app::Error::AuthenticationSecretInvalid => APIError::Internal,
app::Error::MiscSettingsNotFound => APIError::Internal,
app::Error::IndexAlbumArtPatternInvalid => APIError::Internal,
app::Error::DDNSUpdateURLInvalid => APIError::InvalidDDNSURL,
app::Error::IndexAlbumArtPatternInvalid => APIError::InvalidAlbumArtPattern,
app::Error::Toml(_) => APIError::Internal,
app::Error::IndexDeserializationError => APIError::Internal,