fix: container install flow, filebrowser auth, AppCard enrichment

- Fix .198-style fresh installs: systemd service ExecStartPre creates
  /run/user/1000, enable podman.socket, chmod 644 /etc/hosts
- Filebrowser: add /data volume for database (fixes read-only crash),
  secure auth with random password via backend RPC (no more admin/admin)
- AppCard: enrich installing state with marketplace metadata (icon,
  title, description, tier badge, author, version)
- Registry: btcpayserver 1.13.5 → 1.13.7, images mirrored
- ReadWritePaths: add home container paths for rootless podman

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-27 13:32:54 +00:00
parent ef2922d909
commit 121f17e44e
14 changed files with 215 additions and 54 deletions

View File

@@ -38,6 +38,7 @@ impl RpcHandler {
"package.stop" => self.handle_package_stop(params).await,
"package.restart" => self.handle_package_restart(params).await,
"package.uninstall" => self.handle_package_uninstall(params).await,
"app.filebrowser-token" => self.handle_filebrowser_token().await,
// Bundled app management (for pre-loaded container images)
"bundled-app-start" => self.handle_bundled_app_start(params).await,

View File

@@ -562,10 +562,18 @@ pub(super) async fn get_app_config(
.unwrap_or(8083);
(
vec![format!("{}:80", host_port)],
vec!["/var/lib/archipelago/filebrowser:/srv".to_string()],
vec![
"/var/lib/archipelago/filebrowser:/srv".to_string(),
"/var/lib/archipelago/filebrowser-data:/data".to_string(),
],
vec![],
None,
None,
Some(vec![
"--database=/data/database.db".to_string(),
"--root=/srv".to_string(),
"--address=0.0.0.0".to_string(),
"--port=80".to_string(),
]),
)
}
"nginx-proxy-manager" => (

View File

@@ -404,6 +404,67 @@ printtoconsole=1\n",
/// Run post-install hooks (Nextcloud trusted domains, Bitcoin UI container).
async fn run_post_install_hooks(&self, package_id: &str) {
if package_id == "filebrowser" {
tokio::spawn(async move {
// Wait for filebrowser to start and initialize its database
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
// Generate a random password (32 bytes, hex-encoded)
let mut buf = [0u8; 32];
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut buf);
let password = hex::encode(buf);
// Get a JWT token with default credentials
let login_res = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()
.unwrap_or_default()
.post("http://127.0.0.1:8083/api/login")
.json(&serde_json::json!({"username": "admin", "password": "admin"}))
.send()
.await;
let token = match login_res {
Ok(resp) if resp.status().is_success() => {
resp.text().await.unwrap_or_default().trim_matches('"').to_string()
}
_ => {
tracing::warn!("FileBrowser not ready for password change — keeping default");
return;
}
};
// Change admin password via filebrowser API
let change_res = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()
.unwrap_or_default()
.put("http://127.0.0.1:8083/api/users/1")
.header("X-Auth", &token)
.json(&serde_json::json!({"password": password}))
.send()
.await;
match change_res {
Ok(resp) if resp.status().is_success() => {
let secret_dir = "/var/lib/archipelago/secrets/filebrowser";
let _ = tokio::fs::create_dir_all(secret_dir).await;
let _ = tokio::fs::write(
format!("{}/password", secret_dir),
&password,
).await;
info!("FileBrowser admin password secured (default credentials replaced)");
}
Ok(resp) => {
tracing::warn!("FileBrowser password change failed: {}", resp.status());
}
Err(e) => {
tracing::warn!("FileBrowser password change error: {}", e);
}
}
});
}
if package_id == "nextcloud" {
let host_ip = self.config.host_ip.clone();
tokio::spawn(async move {
@@ -464,4 +525,36 @@ printtoconsole=1\n",
});
}
}
/// Get a fresh FileBrowser JWT token for the frontend.
/// Reads the stored random password and authenticates to filebrowser's API.
pub(in crate::api::rpc) async fn handle_filebrowser_token(
&self,
) -> Result<serde_json::Value> {
let secret_path = "/var/lib/archipelago/secrets/filebrowser/password";
let password = tokio::fs::read_to_string(secret_path)
.await
.unwrap_or_else(|_| "admin".to_string());
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()
.unwrap_or_default();
let resp = client
.post("http://127.0.0.1:8083/api/login")
.json(&serde_json::json!({"username": "admin", "password": password}))
.send()
.await
.context("Failed to connect to FileBrowser")?;
if !resp.status().is_success() {
return Err(anyhow::anyhow!("FileBrowser login failed ({})", resp.status()));
}
let token = resp.text().await.unwrap_or_default();
let token = token.trim_matches('"');
Ok(serde_json::json!({ "token": token }))
}
}