package main import ( "fmt" "os" "os/exec" "path/filepath" "regexp" "strings" ) func instanceDir(dataDir, instanceID string) string { return filepath.Join(dataDir, "instances", instanceID) } func getContainerEngine() string { if _, err := exec.LookPath("podman"); err == nil { return "podman" } return "docker" } func writeCompose(dataDir, instanceID, compose string, port int) error { dir := instanceDir(dataDir, instanceID) if err := os.MkdirAll(dir, 0755); err != nil { return err } // Ensure the studioE5 mu-plugin is available and substitute its path muDir, err := writeMUPlugin(dataDir) if err != nil { return err } compose = strings.ReplaceAll(compose, "{MU_PLUGINS_DIR}", filepath.Dir(muDir)) compose = strings.ReplaceAll(compose, "{INSTANCE_ID}", instanceID) compose = strings.ReplaceAll(compose, "{PORT}", fmt.Sprintf("%d", port)) f := filepath.Join(dir, "docker-compose.yml") return os.WriteFile(f, []byte(compose), 0644) } func writeInitScript(dataDir, instanceID, script string) error { dir := instanceDir(dataDir, instanceID) if err := os.MkdirAll(dir, 0755); err != nil { return err } f := filepath.Join(dir, "wp-init.sh") return os.WriteFile(f, []byte(script), 0755) } func configureEngineCmd(cmd *exec.Cmd, dir string) { hideWindow(cmd) logPath := filepath.Join(dir, "compose.log") if f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644); err == nil { cmd.Stdout = f cmd.Stderr = f } } func dockerComposeUp(dataDir, instanceID string) error { dir := instanceDir(dataDir, instanceID) cmd := exec.Command(getContainerEngine(), "compose", "-f", filepath.Join(dir, "docker-compose.yml"), "up", "-d") configureEngineCmd(cmd, dir) return cmd.Run() } func dockerComposeDown(dataDir, instanceID string) error { dir := instanceDir(dataDir, instanceID) cmd := exec.Command(getContainerEngine(), "compose", "-f", filepath.Join(dir, "docker-compose.yml"), "down") configureEngineCmd(cmd, dir) return cmd.Run() } func dockerComposeStop(dataDir, instanceID string) error { dir := instanceDir(dataDir, instanceID) cmd := exec.Command(getContainerEngine(), "compose", "-f", filepath.Join(dir, "docker-compose.yml"), "stop") configureEngineCmd(cmd, dir) return cmd.Run() } func dockerComposeStart(dataDir, instanceID string) error { dir := instanceDir(dataDir, instanceID) cmd := exec.Command(getContainerEngine(), "compose", "-f", filepath.Join(dir, "docker-compose.yml"), "start") configureEngineCmd(cmd, dir) return cmd.Run() } func dockerComposeRm(dataDir, instanceID string) error { dir := instanceDir(dataDir, instanceID) cmd := exec.Command(getContainerEngine(), "compose", "-f", filepath.Join(dir, "docker-compose.yml"), "down", "-v") configureEngineCmd(cmd, dir) if err := cmd.Run(); err != nil { return err } return os.RemoveAll(dir) } // extractPublicURL tries to find the public URL from a WordPress compose config. func extractPublicURL(composeConfig string) string { re := regexp.MustCompile(`define\('WP_HOME',\s*'([^']+)'\);`) m := re.FindStringSubmatch(composeConfig) if len(m) > 1 { return m[1] } return "" } // updateWordPressURLs patches wp-config.php inside the WordPress container // so that WP_HOME and WP_SITEURL point to the public URL. func updateWordPressURLs(dataDir, instanceID, publicURL string) error { if publicURL == "" { return nil } dir := instanceDir(dataDir, instanceID) composeFile := filepath.Join(dir, "docker-compose.yml") engine := getContainerEngine() script := fmt.Sprintf(`#!/bin/sh CONFIG=/var/www/html/wp-config.php if [ -f "$CONFIG" ]; then sed -i "s|define('WP_HOME',[^;]*);|define('WP_HOME', '%s');|" "$CONFIG" sed -i "s|define('WP_SITEURL',[^;]*);|define('WP_SITEURL', '%s');|" "$CONFIG" if ! grep -q "define('WP_HOME'" "$CONFIG"; then sed -i "/That's all, stop editing/i define('WP_HOME', '%s');\ndefine('WP_SITEURL', '%s');" "$CONFIG" fi fi `, publicURL, publicURL, publicURL, publicURL) scriptPath := filepath.Join(dir, "update-wp-urls.sh") if err := os.WriteFile(scriptPath, []byte(script), 0755); err != nil { return err } defer os.Remove(scriptPath) cpCmd := exec.Command(engine, "compose", "-f", composeFile, "cp", scriptPath, "app:/tmp/update-wp-urls.sh") configureEngineCmd(cpCmd, dir) if err := cpCmd.Run(); err != nil { return err } execCmd := exec.Command(engine, "compose", "-f", composeFile, "exec", "-T", "app", "sh", "/tmp/update-wp-urls.sh") configureEngineCmd(execCmd, dir) return execCmd.Run() } // stripWordPressHardcodedURLs removes hardcoded WP_HOME/WP_SITEURL defines // from wp-config.php so the studioE5 mu-plugin can compute them from the Host // header. This is useful when repairing older instances created before the // mu-plugin existed. func stripWordPressHardcodedURLs(dataDir, instanceID string) error { dir := instanceDir(dataDir, instanceID) composeFile := filepath.Join(dir, "docker-compose.yml") engine := getContainerEngine() script := `#!/bin/sh CONFIG=/var/www/html/wp-config.php if [ -f "$CONFIG" ]; then # Remove hardcoded WP_HOME / WP_SITEURL defines so the mu-plugin controls them sed -i "/define('WP_HOME',/d" "$CONFIG" sed -i "/define('WP_SITEURL',/d" "$CONFIG" fi ` scriptPath := filepath.Join(dir, "strip-wp-urls.sh") if err := os.WriteFile(scriptPath, []byte(script), 0755); err != nil { return err } defer os.Remove(scriptPath) cpCmd := exec.Command(engine, "compose", "-f", composeFile, "cp", scriptPath, "app:/tmp/strip-wp-urls.sh") configureEngineCmd(cpCmd, dir) if err := cpCmd.Run(); err != nil { return err } execCmd := exec.Command(engine, "compose", "-f", composeFile, "exec", "-T", "app", "sh", "/tmp/strip-wp-urls.sh") configureEngineCmd(execCmd, dir) return execCmd.Run() }