119 lines
3.8 KiB
Rust

//! 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<EnsureOutcome> {
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);
}
}