module Bootstrap open System open System.IO open System.Runtime.InteropServices open System.Security.Cryptography open System.Text let digest path = use stream = File.OpenRead path SHA256.HashData stream |> Convert.ToHexString |> _.ToLowerInvariant() let write (path: string) (bytes: byte[]) = Directory.CreateDirectory(Path.GetDirectoryName path) |> ignore File.WriteAllBytes(path, bytes) let text path value = write path (Encoding.UTF8.GetBytes(value: string)) let trust source runtime = write (Path.Combine(runtime, "cacerts")) (File.ReadAllBytes source) type Paths = { Data: string State: string Backups: string Files: string } type Config = { Paths: Paths PasswordPath: string Eula: string Args: string Saltatory: string } let prefixes = [ "mods", "mods" "config", "config" "defaultconfigs", "defaultconfigs" "kubejs", "kubejs" "libraries", "libraries" "libraries-integratedscripting", "libraries-integratedscripting" "settings", "settings" "tools", "tools" ] let parse (value: string) = value.Split('\n', StringSplitOptions.RemoveEmptyEntries) |> Array.map (fun line -> let line = line.TrimEnd '\r' let hash, path = line.Substring(0, 64), line.Substring(66) hash, path) let sync cache prefix inventory includeFile fetchManifest fetch = let root = Path.Combine(cache, prefix) Directory.CreateDirectory root |> ignore let files = fetchManifest inventory |> parse |> Array.filter (snd >> includeFile) for hash, name in files do let path = Path.Combine(root, name) Directory.CreateDirectory(Path.GetDirectoryName path) |> ignore fetch (prefix + "/" + name) hash path let names = files |> Array.map (snd >> fun name -> Path.GetFullPath(Path.Combine(root, name))) |> Set.ofArray for path in Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories) do if not (names.Contains(Path.GetFullPath path)) then File.Delete path files let copyFiles source target files = for _, name in files do let path = Path.Combine(target, name) Directory.CreateDirectory(Path.GetDirectoryName path) |> ignore File.Copy(Path.Combine(source, name), path, true) let run config arch fetchManifest fetch fetchSaltatory = let p = config.Paths let cache = Path.Combine(p.State, "cache") Directory.CreateDirectory p.Data |> ignore Directory.CreateDirectory p.Backups |> ignore Directory.CreateDirectory(Path.Combine(p.State, "world")) |> ignore Directory.CreateDirectory(Path.Combine(p.State, "world/data/chunky")) |> ignore for prefix, target in prefixes do let includeFile (name: string) = (prefix <> "mods" || not (name.StartsWith("saltatory-", StringComparison.OrdinalIgnoreCase))) && (prefix <> "config" || not (name.StartsWith("chunky/tasks/", StringComparison.Ordinal))) let files = sync cache prefix (prefix + ".sha256") includeFile fetchManifest fetch let destination = Path.Combine(p.Data, target) if Directory.Exists destination then Directory.Delete(destination, true) copyFiles (Path.Combine(cache, prefix)) destination files let saltatory = Path.Combine(cache, "saltatory.jar") fetchSaltatory config.Saltatory saltatory File.Copy(saltatory, Path.Combine(p.Data, "mods", "saltatory.jar"), true) for name in [ "ops.json"; "whitelist.json"; "banned-ips.json"; "banned-players.json"; "usercache.json" ] do let path = Path.Combine(p.State, name) if not (File.Exists path) then text path "[]" let names = Path.Combine(p.State, "usernamecache.json") if not (File.Exists names) then text names "{}" for name in [ "local"; "journeymap" ] do Directory.CreateDirectory(Path.Combine(p.State, name)) |> ignore let world = Path.Combine(p.State, "world") for prefix in [ "datapacks"; "serverconfig" ] do let name = "world/" + prefix let files = sync cache name ("world-" + prefix + ".sha256") (fun _ -> true) fetchManifest fetch copyFiles (Path.Combine(cache, name)) (Path.Combine(world, prefix)) files let tools = match arch with | Architecture.X64 -> "amd64" | Architecture.Arm64 -> "arm64" | _ -> invalidOp "unsupported architecture" for name in [ "mc-server-runner"; "mc-monitor" ] do let source = Path.Combine(p.Data, "tools", tools, name) let path = Path.Combine(p.Data, name) File.Copy(source, path, true) File.SetUnixFileMode(path, enum 0o755) let properties = File.ReadAllText(Path.Combine(p.Files, "server.properties")).TrimEnd() let password = File.ReadAllText(config.PasswordPath).TrimEnd() text (Path.Combine(p.Data, "server.properties")) (properties + "\nrcon.password=" + password + "\n") File.Copy(Path.Combine(p.Data, config.Args), Path.Combine(p.Data, "server_args.txt"), true) let args = File.ReadAllText(Path.Combine(p.Data, "settings", "user_jvm_args.txt")).TrimEnd() text (Path.Combine(p.Data, "user_jvm_args.txt")) (args + "\n-Djavax.net.ssl.trustStore=/data/cacerts\n") text (Path.Combine(p.Data, "eula.txt")) ("eula=" + config.Eula.ToLowerInvariant() + "\n") printfn "Minecraft bootstrap ready"