//! filebrowser config bootstrap helper. //! //! Mirrors the legacy first-boot behavior that writes //! `/var/lib/archipelago/filebrowser-data/.filebrowser.json` before //! starting the container with `--config /data/.filebrowser.json`. use anyhow::{Context, Result}; use std::path::PathBuf; use tokio::fs; pub const DEFAULT_SRV_ROOT: &str = "/var/lib/archipelago/filebrowser"; pub const DEFAULT_DATA_DIR: &str = "/var/lib/archipelago/filebrowser-data"; pub const DEFAULT_CONFIG_PATH: &str = "/var/lib/archipelago/filebrowser-data/.filebrowser.json"; const DEFAULT_CONFIG_JSON: &str = "{\"port\":80,\"baseURL\":\"\",\"address\":\"0.0.0.0\",\"database\":\"/data/filebrowser.db\",\"root\":\"/srv\",\"log\":\"stdout\"}\n"; #[derive(Debug, Clone)] pub struct EnsurePaths { pub srv_root: PathBuf, pub data_dir: PathBuf, pub config_path: PathBuf, } impl Default for EnsurePaths { fn default() -> Self { Self { srv_root: PathBuf::from(DEFAULT_SRV_ROOT), data_dir: PathBuf::from(DEFAULT_DATA_DIR), config_path: PathBuf::from(DEFAULT_CONFIG_PATH), } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum EnsureOutcome { Written, Unchanged, } pub async fn ensure_config(paths: &EnsurePaths) -> Result { fs::create_dir_all(&paths.srv_root) .await .with_context(|| format!("creating {}", paths.srv_root.display()))?; fs::create_dir_all(&paths.data_dir) .await .with_context(|| format!("creating {}", paths.data_dir.display()))?; for d in ["Documents", "Photos", "Music", "Downloads", "Builds"] { fs::create_dir_all(paths.srv_root.join(d)) .await .with_context(|| format!("creating {}/{}", paths.srv_root.display(), d))?; } if paths.config_path.exists() { return Ok(EnsureOutcome::Unchanged); } let parent = paths .config_path .parent() .ok_or_else(|| anyhow::anyhow!("config_path has no parent directory"))?; fs::create_dir_all(parent) .await .with_context(|| format!("creating {}", parent.display()))?; let tmp = paths.config_path.with_extension("tmp"); fs::write(&tmp, DEFAULT_CONFIG_JSON) .await .with_context(|| format!("writing tmp {}", tmp.display()))?; fs::rename(&tmp, &paths.config_path) .await .with_context(|| { format!( "renaming {} -> {}", tmp.display(), paths.config_path.display() ) })?; Ok(EnsureOutcome::Written) } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn ensure_config_creates_dirs_and_file() { let tmp = tempfile::TempDir::new().unwrap(); let paths = EnsurePaths { srv_root: tmp.path().join("filebrowser"), data_dir: tmp.path().join("filebrowser-data"), config_path: tmp.path().join("filebrowser-data/.filebrowser.json"), }; let out = ensure_config(&paths).await.unwrap(); assert_eq!(out, EnsureOutcome::Written); assert!(paths.config_path.exists()); assert!(paths.srv_root.join("Documents").exists()); assert!(paths.srv_root.join("Photos").exists()); } #[tokio::test] async fn ensure_config_is_idempotent() { let tmp = tempfile::TempDir::new().unwrap(); let paths = EnsurePaths { srv_root: tmp.path().join("filebrowser"), data_dir: tmp.path().join("filebrowser-data"), config_path: tmp.path().join("filebrowser-data/.filebrowser.json"), }; let first = ensure_config(&paths).await.unwrap(); assert_eq!(first, EnsureOutcome::Written); let second = ensure_config(&paths).await.unwrap(); assert_eq!(second, EnsureOutcome::Unchanged); } }