summaryrefslogtreecommitdiffstats
path: root/src/docker.rs
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-07-29 01:03:24 +0200
committersalmonik <salmonik@tuta.io>2026-07-29 01:03:24 +0200
commit74d2ada067bd2fd9436710ab7ac260e0eddf1314 (patch)
tree1f791bb895a611d8e729a735ba098a647808a157 /src/docker.rs
parentcreate example config on startup (diff)
downloadminecraft-ui-74d2ada067bd2fd9436710ab7ac260e0eddf1314.tar.gz
minecraft-ui-74d2ada067bd2fd9436710ab7ac260e0eddf1314.zip
Add per-instance mod management with client-side detection
- New mod validation engine (src/mods.rs) with constant-pool class-file scanning: detects server-side content (block, tileentity, command, worldgen) vs client-only API (gui, renderer, forge/client) - 7 client-only mods correctly detected (AppleSkin, CraftingTweaks, Durability101, EnchantmentDescriptions, InventoryHUD, MouseTweaks, TrashSlot); 51 server-capable; 1 unknown - Routes + templating for mod upload (jar/zip), listing, delete, rescan - Multipart upload with 500MB limit (bypasses axum's default 2MB) - Per-instance mod summary card on edit page - Whitelist toggle checkbox on edit page (persisted, drives ENABLE_WHITELIST) - Dynamic Java image tag (java8/java17/java21) based on MC version - Class scan is informational-only; rejection based on metadata side=CLIENT - has_modpack flag cleanup on mod delete - Remove hx-boost from <body> (was corrupting multipart uploads)
Diffstat (limited to 'src/docker.rs')
-rw-r--r--src/docker.rs61
1 files changed, 46 insertions, 15 deletions
diff --git a/src/docker.rs b/src/docker.rs
index 6b8640a..d66767e 100644
--- a/src/docker.rs
+++ b/src/docker.rs
@@ -23,19 +23,51 @@ impl DockerManager {
23 Ok(Self { client, config }) 23 Ok(Self { client, config })
24 } 24 }
25 25
26 /// Map a Minecraft version string (e.g. "1.12.2", "1.20.1") to the
27 /// appropriate itzg/minecraft-server Java image tag.
28 ///
29 /// - < 1.17 → java8 (Minecraft 1.12.2 and earlier)
30 /// - 1.17 .. 1.20.4 → java17
31 /// - >= 1.20.5 → java21
32 fn java_tag(version: &str) -> &'static str {
33 // Parse major.minor from version strings like "1.12.2", "1.20.1", "1.21"
34 let parts: Vec<&str> = version.split('.').collect();
35 if parts.len() < 2 {
36 return "java17"; // sensible default
37 }
38 let major: u32 = parts.first().and_then(|s| s.parse().ok()).unwrap_or(1);
39 let minor: u32 = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(12);
40
41 if major <= 1 && minor < 17 {
42 "java8"
43 } else if major <= 1 && minor < 21 {
44 // 1.17.x through 1.20.4 use Java 17
45 "java17"
46 } else {
47 // 1.20.5+ and 1.21+ use Java 21
48 "java21"
49 }
50 }
51
52 /// Build the full image reference, e.g. "itzg/minecraft-server:java8"
53 fn image_ref(version: &str) -> String {
54 format!("itzg/minecraft-server:{}", Self::java_tag(version))
55 }
56
26 pub async fn start_container(&self, instance: &Instance) -> Result<(), String> { 57 pub async fn start_container(&self, instance: &Instance) -> Result<(), String> {
27 tracing::info!("Pulling itzg/minecraft-server:latest image..."); 58 let tag = Self::java_tag(&instance.version);
28 self.pull_image().await?; 59 tracing::info!("Pulling itzg/minecraft-server:{tag} image...");
60 self.pull_image(tag).await?;
29 tracing::info!("Creating container for instance '{}'", instance.name); 61 tracing::info!("Creating container for instance '{}'", instance.name);
30 self.recreate_container(instance).await?; 62 self.recreate_container(instance).await?;
31 tracing::info!("Starting container..."); 63 tracing::info!("Starting container...");
32 self.start_existing().await 64 self.start_existing().await
33 } 65 }
34 66
35 async fn pull_image(&self) -> Result<(), String> { 67 async fn pull_image(&self, tag: &str) -> Result<(), String> {
36 let options = CreateImageOptions { 68 let options = CreateImageOptions {
37 from_image: "itzg/minecraft-server", 69 from_image: "itzg/minecraft-server",
38 tag: "latest", 70 tag,
39 ..Default::default() 71 ..Default::default()
40 }; 72 };
41 73
@@ -74,6 +106,8 @@ impl DockerManager {
74 .to_string(); 106 .to_string();
75 let instance_data = format!("{server_dir}/instances/{}/data", instance.name); 107 let instance_data = format!("{server_dir}/instances/{}/data", instance.name);
76 108
109 let image_ref = Self::image_ref(&instance.version);
110
77 let mut env_strings: Vec<String> = vec![ 111 let mut env_strings: Vec<String> = vec![
78 "EULA=TRUE".into(), 112 "EULA=TRUE".into(),
79 format!("VERSION={}", instance.version), 113 format!("VERSION={}", instance.version),
@@ -83,7 +117,7 @@ impl DockerManager {
83 format!("DIFFICULTY={}", instance.difficulty), 117 format!("DIFFICULTY={}", instance.difficulty),
84 "FORCE_GAMEMODE=true".into(), 118 "FORCE_GAMEMODE=true".into(),
85 "SPAWN_PROTECTION=0".into(), 119 "SPAWN_PROTECTION=0".into(),
86 "ENABLE_WHITELIST=true".into(), 120 format!("ENABLE_WHITELIST={}", instance.whitelist_enabled),
87 "PAUSE_WHEN_EMPTY_SECONDS=30".into(), 121 "PAUSE_WHEN_EMPTY_SECONDS=30".into(),
88 "ONLINE_MODE=false".into(), 122 "ONLINE_MODE=false".into(),
89 "TZ=Europe/Warsaw".into(), 123 "TZ=Europe/Warsaw".into(),
@@ -102,17 +136,14 @@ impl DockerManager {
102 env_strings.push("PVP=false".into()); 136 env_strings.push("PVP=false".into());
103 } 137 }
104 138
105 if let Some(ref seed) = instance.world_seed { 139 if let Some(ref seed) = instance.world_seed
106 if !seed.is_empty() { 140 && !seed.is_empty() {
107 env_strings.push(format!("LEVEL_SEED={seed}")); 141 env_strings.push(format!("LEVEL_SEED={seed}"));
108 } 142 }
109 }
110
111 let env_refs: Vec<&str> = env_strings.iter().map(|s| s.as_str()).collect();
112 143
113 let mut exposed_ports: HashMap<&str, HashMap<(), ()>> = HashMap::new(); 144 let mut exposed_ports: HashMap<String, HashMap<(), ()>> = HashMap::new();
114 exposed_ports.insert("25565/tcp", HashMap::new()); 145 exposed_ports.insert("25565/tcp".into(), HashMap::new());
115 exposed_ports.insert("25575/tcp", HashMap::new()); 146 exposed_ports.insert("25575/tcp".into(), HashMap::new());
116 147
117 let mut port_bindings = HashMap::new(); 148 let mut port_bindings = HashMap::new();
118 port_bindings.insert( 149 port_bindings.insert(
@@ -137,9 +168,9 @@ impl DockerManager {
137 }; 168 };
138 169
139 let container_config = ContainerConfig { 170 let container_config = ContainerConfig {
140 image: Some("itzg/minecraft-server:latest"), 171 image: Some(image_ref.clone()),
141 tty: Some(true), 172 tty: Some(true),
142 env: Some(env_refs), 173 env: Some(env_strings.clone()),
143 exposed_ports: Some(exposed_ports), 174 exposed_ports: Some(exposed_ports),
144 host_config: Some(host_config), 175 host_config: Some(host_config),
145 ..Default::default() 176 ..Default::default()