Files
coa-server-manager/src-tauri/src/docker_install.rs
T
Fabien F. 45a699d2ca feat: install a new Docker server from the install screen (Linux)
The install commands ask coa_core::platform::flavor() once and hand a Docker
install to src-tauri/src/docker_install.rs; the repack arm of each command is
untouched (an early return and an extra optional parameter are all that changed
in lib.rs). New command install_environment tells the screen the kind of server,
the suggested folder (~/CoaServer) and why Docker cannot be used, if it cannot.

On Linux the install screen shows the game data folder (used in place, read-only)
and the server package (a folder or an address; there is no default address until
a signed Linux package is published), and disables Install until both are given
and Docker works. It shows Docker's own message with a "Check again" button when
Docker is missing, stopped or not allowed for the user. The Windows screen is
unchanged (checked side by side in the browser mock).

A debug build (tauri dev) accepts another package key named in
COA_DEV_TRUSTED_KEY, to try the installer on a package signed with a throwaway
key; a release build always uses the embedded key.

New text is English only for now (other languages fall back to it).
2026-10-08 17:28:52 +02:00

126 lines
5.5 KiB
Rust

//! The Linux (Docker) side of the install commands. `lib.rs` asks `coa_core::platform::flavor()` once per command and
//! calls this module for a Docker install; the code for a repack install stays where it was and is not changed by what
//! happens here.
use std::path::{Path, PathBuf};
use coa_core::docker::{self, install as core_install, SystemDocker};
use coa_core::download::Cancel;
use coa_core::install::{Preflight, Source};
use coa_core::registry::Registry;
use coa_core::{Error, Result};
use serde::Serialize;
use tauri::{AppHandle, Emitter, State};
use super::{blocking, data_dir, summary, AppState, InstallRequirements, ServerSummary, UiError};
/// What the install screen needs to know about this computer before it asks anything.
#[derive(Serialize)]
pub struct Environment {
pub flavor: coa_core::platform::Flavor,
pub default_dir: String,
/// Why Docker cannot be used (not installed, not running, no permission), in Docker's own words; none when it works.
pub docker_problem: Option<String>,
}
pub fn default_dir() -> String {
default_dir_in(std::env::var("HOME").ok().as_deref())
}
fn default_dir_in(home: Option<&str>) -> String {
match home.map(|h| h.trim_end_matches('/')).filter(|h| !h.is_empty()) {
Some(h) => format!("{h}/CoaServer"),
None => "/srv/coa".into(),
}
}
pub fn docker_problem() -> Option<String> {
docker::check_docker(&SystemDocker).err().map(|e| e.to_string())
}
pub fn environment() -> Environment {
Environment { flavor: coa_core::platform::flavor(), default_dir: default_dir(), docker_problem: docker_problem() }
}
pub fn preflight(dest: &str, data: Option<String>, needed: Option<u64>, registry: &Registry) -> Preflight {
// The size comes from the signed package list when the screen already has it; it is checked again at install time.
core_install::preflight(Path::new(dest), Path::new(data.as_deref().unwrap_or("")), needed.unwrap_or(1024 * 1024 * 1024), registry)
}
/// The Linux package has no default address yet: it is published only after the maintainer has signed it.
pub fn package_source(custom: Option<String>) -> Result<Source> {
let pick = custom.filter(|s| !s.trim().is_empty()).or_else(|| std::env::var("COA_PACKAGE_SOURCE").ok().filter(|s| !s.trim().is_empty()));
match pick {
Some(p) if p.to_ascii_lowercase().starts_with("http") => Ok(Source::Url(p)),
Some(p) => Ok(Source::Dir(PathBuf::from(p))),
None => Err(Error::Invalid("Choose the Linux server package (a folder or an address): none is published yet.".into())),
}
}
/// The key a Linux package must be signed with. A debug build (`tauri dev`) may name another one in `COA_DEV_TRUSTED_KEY`,
/// to try the installer on a package signed with a throwaway key; a release build always uses the embedded key.
pub fn trusted_key() -> String {
#[cfg(debug_assertions)]
if let Ok(k) = std::env::var("COA_DEV_TRUSTED_KEY") {
if !k.trim().is_empty() {
return k.trim().to_string();
}
}
coa_core::signing::EMBEDDED_PUBLIC_KEY.to_string()
}
pub async fn requirements(package: Option<String>) -> std::result::Result<InstallRequirements, UiError> {
blocking(move || {
let (m, _) = coa_core::pkgsource::fetch_manifest(&package_source(package)?, &trusted_key())?;
let archive = m.archive.ok_or_else(|| Error::InvalidManifest("no archive".into()))?;
Ok(InstallRequirements { download_bytes: archive.parts.iter().map(|p| p.size).sum(), unpacked_bytes: archive.unpacked_size, version: m.version })
})
.await
}
pub async fn install(app: AppHandle, state: State<'_, AppState>, dest: String, package: Option<String>, data: Option<String>) -> std::result::Result<ServerSummary, UiError> {
let source = package_source(package)?;
let data_dir_path = PathBuf::from(data.filter(|d| !d.trim().is_empty()).ok_or_else(|| Error::Invalid("Choose the folder that holds the game data.".into()))?);
let cancel = Cancel::default();
*state.install_cancel.lock().map_err(|_| Error::Invalid("state poisoned".into()))? = Some(cancel.clone());
let registry = Registry::at(data_dir().join("installs.json"));
let dest_path = PathBuf::from(dest);
let key = trusted_key();
let result = tauri::async_runtime::spawn_blocking(move || {
core_install::install(
&core_install::Params { source, dest: dest_path, data_dir: data_dir_path, trusted_key: &key, registry: &registry, cancel },
&|step| {
let _ = app.emit("install-progress", step);
},
)
})
.await;
if let Ok(mut c) = state.install_cancel.lock() {
*c = None;
}
let done = result.map_err(|e| Error::Invalid(e.to_string()))??;
Ok(summary(done.id, PathBuf::from(done.path)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_default_folder_is_in_the_home_folder() {
assert_eq!(default_dir_in(Some("/home/ana")), "/home/ana/CoaServer");
assert_eq!(default_dir_in(Some("/home/ana/")), "/home/ana/CoaServer");
assert_eq!(default_dir_in(None), "/srv/coa");
assert_eq!(default_dir_in(Some("")), "/srv/coa");
}
#[test]
fn a_linux_package_must_be_named_there_is_no_default() {
std::env::remove_var("COA_PACKAGE_SOURCE");
assert!(package_source(None).is_err());
assert!(package_source(Some(" ".into())).is_err());
assert!(matches!(package_source(Some("/tmp/pkg".into())).unwrap(), Source::Dir(_)));
assert!(matches!(package_source(Some("https://example.org/pkg".into())).unwrap(), Source::Url(_)));
}
}