Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,16 +165,16 @@ Here is the matrix of deployed services:
| | Bazarr | 6767 (Localhost) | `http://localhost:6767` | Subtitles |
| | Prowlarr | 9696 (Localhost) | `http://localhost:9696` | Torrent Indexers |
| | Jackett | 9117 (Localhost) | `http://localhost:9117` | Indexer Proxy |
| **Download** | QBittorrent | 8080 (Localhost) | `http://localhost:8080` | Torrent Client |
| **Download** | QBittorrent | 8080 (Localhost), 6881 | `http://localhost:8080` | Torrent Client |
| **Apps** | Nextcloud | 4443 (Localhost) | `https://localhost:4443` | Personal Cloud |
| | Vaultwarden | 8001 (Localhost) | `http://localhost:8001` | Password Manager |
| | Filebrowser | 8002 (Localhost) | `http://localhost:8002` | Web File Manager |
| | Yourls | 8003 (Localhost) | `http://localhost:8003` | URL Shortener |
| | GLPI | 8088 (Localhost) | `http://localhost:8088` | IT Asset Management |
| | Gitea | 3000 (Localhost) | `http://localhost:3000` | Self-hosted Git |
| | Gitea | 3000 (Localhost), 2222 | `http://localhost:3000` | Self-hosted Git |
| | Roundcube | 8090 (Localhost) | `http://localhost:8090` | Webmail |
| | Mailserver | 25, 143, 587, 993 | - | Full Mail Server |
| | Syncthing | 8384 (Localhost), 22000 | `http://localhost:8384` | File Synchronization |
| | Syncthing | 8384 (Localhost), 22000, 21027 | `http://localhost:8384` | File Synchronization |

---

Expand Down Expand Up @@ -319,16 +319,16 @@ Voici la matrice des services déployés :
| | Bazarr | 6767 (Localhost) | `http://localhost:6767` | Sous-titres |
| | Prowlarr | 9696 (Localhost) | `http://localhost:9696` | Indexeurs Torrent |
| | Jackett | 9117 (Localhost) | `http://localhost:9117` | Proxy Indexeurs |
| **Download** | QBittorrent | 8080 (Localhost) | `http://localhost:8080` | Client Torrent |
| **Download** | QBittorrent | 8080 (Localhost), 6881 | `http://localhost:8080` | Client Torrent |
| **Apps** | Nextcloud | 4443 (Localhost) | `https://localhost:4443` | Personal Cloud |
| | Vaultwarden | 8001 (Localhost) | `http://localhost:8001` | Password Manager |
| | Filebrowser | 8002 (Localhost) | `http://localhost:8002` | Web File Manager |
| | Yourls | 8003 (Localhost) | `http://localhost:8003` | URL Shortener |
| | GLPI | 8088 (Localhost) | `http://localhost:8088` | IT Asset Management |
| | Gitea | 3000 (Localhost) | `http://localhost:3000` | Self-hosted Git |
| | Gitea | 3000 (Localhost), 2222 | `http://localhost:3000` | Self-hosted Git |
| | Roundcube | 8090 (Localhost) | `http://localhost:8090` | Webmail |
| | Mailserver | 25, 143, 587, 993 | - | Full Mail Server |
| | Syncthing | 8384 (Localhost), 22000 | `http://localhost:8384` | Synchronisation de Fichiers |
| | Syncthing | 8384 (Localhost), 22000, 21027 | `http://localhost:8384` | Synchronisation de Fichiers |

---

Expand Down
27 changes: 17 additions & 10 deletions server_manager/src/core/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use tokio::sync::RwLock;
struct CachedConfig {
config: Config,
last_mtime: Option<SystemTime>,
last_check: SystemTime,
}

static CONFIG_CACHE: OnceLock<RwLock<CachedConfig>> = OnceLock::new();
Expand Down Expand Up @@ -42,28 +43,34 @@ impl Config {
RwLock::new(CachedConfig {
config: Config::default(),
last_mtime: None,
last_check: std::time::UNIX_EPOCH,
})
});

// Fast path: Optimistic read
// Fast path: Optimistic read with throttle check
{
let guard = cache.read().await;
if let Some(cached_mtime) = guard.last_mtime {
// Check if file still matches
if let Ok(metadata) = tokio::fs::metadata("config.yaml").await {
if let Ok(modified) = metadata.modified() {
if modified == cached_mtime {
return Ok(guard.config.clone());
}
}
if let Ok(elapsed) = SystemTime::now().duration_since(guard.last_check) {
if elapsed.as_millis() < 500 {
return Ok(guard.config.clone());
}
}
}

// Slow path: Update cache
let mut guard = cache.write().await;

// Check metadata again (double-checked locking pattern)
// Double check time under write lock
if let Ok(elapsed) = SystemTime::now().duration_since(guard.last_check) {
if elapsed.as_millis() < 500 {
return Ok(guard.config.clone());
}
}

// Update check time
guard.last_check = SystemTime::now();

// Check metadata
let metadata_res = tokio::fs::metadata("config.yaml").await;

match metadata_res {
Expand Down
92 changes: 91 additions & 1 deletion server_manager/src/core/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use std::sync::OnceLock;
use std::time::SystemTime;
use tokio::sync::RwLock;

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub enum Role {
Expand All @@ -23,14 +26,94 @@ pub struct User {
pub quota_gb: Option<u64>,
}

#[derive(Debug, Clone)]
struct CachedUsers {
manager: UserManager,
last_mtime: Option<SystemTime>,
last_check: SystemTime,
}

static USERS_CACHE: OnceLock<RwLock<CachedUsers>> = OnceLock::new();

#[derive(Debug, Serialize, Deserialize, Default, Clone)]
pub struct UserManager {
users: HashMap<String, User>,
}

impl UserManager {
pub async fn load_async() -> Result<Self> {
tokio::task::spawn_blocking(Self::load).await?
let cache = USERS_CACHE.get_or_init(|| {
RwLock::new(CachedUsers {
manager: UserManager::default(),
last_mtime: None,
last_check: std::time::UNIX_EPOCH,
})
});

// Fast path
{
let guard = cache.read().await;
if let Ok(elapsed) = SystemTime::now().duration_since(guard.last_check) {
if elapsed.as_millis() < 500 {
return Ok(guard.manager.clone());
}
}
}

// Slow path
let mut guard = cache.write().await;

if let Ok(elapsed) = SystemTime::now().duration_since(guard.last_check) {
if elapsed.as_millis() < 500 {
return Ok(guard.manager.clone());
}
}

guard.last_check = SystemTime::now();

// Path logic (duplicated from load to check metadata)
let path = Path::new("users.yaml");
let fallback_path = Path::new("/opt/server_manager/users.yaml");
let check_path = if fallback_path.exists() {
Some(fallback_path)
} else if path.exists() {
Some(path)
} else {
None
};

if let Some(p) = check_path {
// Check metadata
let metadata = tokio::fs::metadata(p).await
.context("Failed to get users.yaml metadata")?;
let modified = metadata.modified().unwrap_or(SystemTime::now());

if let Some(cached_mtime) = guard.last_mtime {
if modified == cached_mtime {
// No change, return cached
return Ok(guard.manager.clone());
}
}

// Changed or first load -> Reload
let manager = tokio::task::spawn_blocking(Self::load).await??;
guard.manager = manager.clone();
guard.last_mtime = Some(modified);
Ok(manager)
} else {
// No file -> Load default (will create file)
let manager = tokio::task::spawn_blocking(Self::load).await??;
guard.manager = manager.clone();

// Try to get mtime of newly created file
if let Ok(meta) = tokio::fs::metadata(path).await {
guard.last_mtime = meta.modified().ok();
} else if let Ok(meta) = tokio::fs::metadata(fallback_path).await {
guard.last_mtime = meta.modified().ok();
}

Ok(manager)
}
}

pub fn load() -> Result<Self> {
Expand Down Expand Up @@ -212,6 +295,13 @@ impl UserManager {
pub fn list_users(&self) -> Vec<&User> {
self.users.values().collect()
}

pub async fn invalidate_cache_async() {
if let Some(cache) = USERS_CACHE.get() {
let mut guard = cache.write().await;
guard.last_check = std::time::UNIX_EPOCH;
}
}
}

#[cfg(test)]
Expand Down
11 changes: 10 additions & 1 deletion server_manager/src/interface/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,10 +275,19 @@ fn print_deployment_summary(secrets: &secrets::Secrets) {
append_row("Roundcube", "http://<IP>:8090", "-", "Login with Mail creds");
append_row("MailServer", "PORTS: 25, 143...", "CLI", "docker exec -ti mailserver setup ...");
append_row("Plex", "http://<IP>:32400/web", "-", "Follow Web Setup");
append_row("Jellyfin", "http://<IP>:8096", "-", "Follow Web Setup");
append_row("ArrStack", "http://<IP>:8989 (Sonarr)", "-", "No auth by default");
append_row("QBittorrent", "http://<IP>:8080", "admin", "adminadmin");
append_row("Filebrowser", "http://<IP>:8002", "admin", "admin");
append_row("Syncthing", "http://<IP>:8384", "-", "No auth by default");
append_row("Uptime Kuma", "http://<IP>:3001", "-", "Create Admin Account");
append_row("Netdata", "http://<IP>:19999", "-", "No auth by default");
append_row("Tautulli", "http://<IP>:8181", "-", "Follow Web Setup");
append_row("Overseerr", "http://<IP>:5055", "-", "Follow Web Setup");

summary.push_str("=================================================================================\n\n");
summary.push_str("NOTE: Replace <IP> with your server's IP address.");
summary.push_str("NOTE: Replace <IP> with your server's IP address. Some services bound to localhost\n");
summary.push_str(" must be accessed via SSH Tunnel or Nginx Proxy Manager.");

println!("{}", summary);

Expand Down
Loading