package main import ( "archive/zip" "bufio" "fmt" "io" "log" "os" "os/exec" "path/filepath" "regexp" "strconv" "strings" "time" socketio "github.com/googollee/go-socket.io" ) func UpdateApp(server *socketio.Server) <-chan string { updates := make(chan string) exePath, _ := os.Executable() exeDir := filepath.Dir(exePath) rootDir := filepath.Join(exeDir, "..") entries, err := os.ReadDir(rootDir) if err != nil { msg := fmt.Sprintf("failed to read root dir %s: %v", rootDir, err) updates <- msg server.BroadcastToRoom("/", "update", "updateLogs", msg) return updates } var zips []string for _, e := range entries { if !e.IsDir() && filepath.Ext(e.Name()) == ".zip" { zips = append(zips, filepath.Join(rootDir, e.Name())) } } go func() { defer close(updates) switch len(zips) { case 0: msg := "No zip files found in root directory" updates <- msg server.BroadcastToRoom("/", "update", "updateLogs", msg) case 1: version, err := extractVersion(filepath.Base(zips[0])) if err != nil { msg := fmt.Sprintf("could not parse version: %v", err) updates <- msg server.BroadcastToRoom("/", "update", "updateLogs", msg) } msg := "Stopping the services" updates <- msg server.BroadcastToRoom("/", "update", "updateLogs", msg) // 1. Stop services + pool stopService("LSTV2") time.Sleep(1 * time.Second) stopService("lst_app") //stopAppPool("LogisticsSupportTool") time.Sleep(1 * time.Second) msg = "Checking cleanup of files" updates <- msg server.BroadcastToRoom("/", "update", "updateLogs", msg) if version%10 == 0 { msg = fmt.Sprintf("Release %d is a cleanup milestone — cleaning root", version) updates <- msg server.BroadcastToRoom("/", "update", "updateLogs", msg) if err := cleanupRoot(rootDir); err != nil { msg = fmt.Sprintf("cleanup failed: %v", err) updates <- msg server.BroadcastToRoom("/", "update", "updateLogs", msg) } } msg = "Unzipping New Build" updates <- msg server.BroadcastToRoom("/", "update", "updateLogs", msg) if err := Unzip(zips[0], rootDir); err != nil { msg = fmt.Sprintf("unzip failed: %v", err) updates <- msg server.BroadcastToRoom("/", "update", "updateLogs", msg) } msg = "Running App Update" updates <- msg server.BroadcastToRoom("/", "update", "updateLogs", msg) if err := runNPMInstall(rootDir); err != nil { server.BroadcastToRoom("/", "update", "updateLogs", fmt.Sprintf("npm install failed: %v", err)) } msg = "Running DB Migrations" updates <- msg server.BroadcastToRoom("/", "update", "updateLogs", msg) if err := runNPMMigrate(rootDir); err != nil { msg = fmt.Sprintf("npm migrate failed: %v", err) updates <- msg server.BroadcastToRoom("/", "update", "updateLogs", msg) } msg = "Starting Services back up" updates <- msg server.BroadcastToRoom("/", "update", "updateLogs", msg) startService("lst_app") time.Sleep(2 * time.Second) startService("LSTV2") time.Sleep(2 * time.Second) //startAppPool("LogisticsSupportTool") if err := os.Remove(zips[0]); err != nil { msg = fmt.Sprintf("warning: failed to delete zip %s: %v", zips[0], err) updates <- msg server.BroadcastToRoom("/", "update", "updateLogs", msg) } else { msg = fmt.Sprintf("Deleted zip %s", zips[0]) updates <- msg server.BroadcastToRoom("/", "update", "updateLogs", msg) } msg = "Deployment finished successfully" updates <- msg server.BroadcastToRoom("/", "update", "updateLogs", msg) default: msg := fmt.Sprintf("Error: too many zip files in root: %v\n", zips) updates <- msg server.BroadcastToRoom("/", "update", "updateLogs", msg) } updates <- "done" }() return updates } func Unzip(srcZip, destDir string) error { r, err := zip.OpenReader(srcZip) if err != nil { return fmt.Errorf("opening zip: %w", err) } defer r.Close() for _, f := range r.File { // Build the destination path for each file fpath := filepath.Join(destDir, f.Name) // Check for ZipSlip (directory traversal) if !strings.HasPrefix(fpath, filepath.Clean(destDir)+string(os.PathSeparator)) { return fmt.Errorf("illegal file path: %s", fpath) } if f.FileInfo().IsDir() { // Create folder if err := os.MkdirAll(fpath, os.ModePerm); err != nil { return err } continue } // Make sure target folder exists if err := os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil { return err } // Open file in archive rc, err := f.Open() if err != nil { return err } // Create destination file outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) if err != nil { rc.Close() return err } // Copy file contents _, err = io.Copy(outFile, rc) // Close files outFile.Close() rc.Close() if err != nil { return err } } return nil } // stopService runs "sc stop " func stopService(name string) error { cmd := exec.Command("sc", "stop", name) output, err := cmd.CombinedOutput() if err != nil { return err } log.Printf("Stopped service %s: %s", name, output) return nil } // stopAppPool runs appcmd to stop an IIS app pool func stopAppPool(name string) error { cmd := exec.Command(`C:\Windows\System32\inetsrv\appcmd.exe`, "stop", "apppool", "/apppool.name:"+name) output, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("failed to stop app pool %s: %v, output: %s", name, err, string(output)) } log.Printf("Stopped app pool %s: %s", name, string(output)) return nil } func cleanupRoot(rootDir string) error { // Default cleanup targets targets := []string{ "dist", filepath.Join("frontend", "dist"), filepath.Join("lstDocs", "build"), } // Check if .includeCleanup file exists cleanupFile := filepath.Join(rootDir, ".includeCleanup") if _, err := os.Stat(cleanupFile); err == nil { f, err := os.Open(cleanupFile) if err != nil { return err } defer f.Close() sc := bufio.NewScanner(f) for sc.Scan() { line := strings.TrimSpace(sc.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } targets = append(targets, line) } if sc.Err() != nil { return sc.Err() } } // Delete them (remove dirs/files under rootDir) for _, t := range targets { p := filepath.Join(rootDir, t) if _, err := os.Stat(p); err == nil { log.Printf("Removing %s", p) if err := os.RemoveAll(p); err != nil { return err } } } return nil } func extractVersion(filename string) (int, error) { re := regexp.MustCompile(`release-(\d+)\.zip`) m := re.FindStringSubmatch(filename) if m == nil { return 0, fmt.Errorf("filename does not match release pattern: %s", filename) } return strconv.Atoi(m[1]) } func runNPMInstall(rootDir string) error { frontendDir := filepath.Join(rootDir, "frontend") // adapt if needed cmd := exec.Command("npm", "install") cmd.Dir = frontendDir cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr log.Println("Running npm install in", frontendDir) return cmd.Run() } func runNPMMigrate(rootDir string) error { frontendDir := filepath.Join(rootDir, "frontend") // same dir cmd := exec.Command("npm", "run", "db:migrate") cmd.Dir = frontendDir cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr log.Println("Running npm run db:migrate in", frontendDir) return cmd.Run() } func startService(name string) error { cmd := exec.Command("sc", "start", name) output, err := cmd.CombinedOutput() if err != nil { return err } log.Printf("Started service %s: %s", name, output) return nil } func startAppPool(name string) error { cmd := exec.Command(`C:\Windows\System32\inetsrv\appcmd.exe`, "start", "apppool", "/apppool.name:"+name) output, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("failed to stop app pool %s: %v, output: %s", name, err, string(output)) } log.Printf("Stopped app pool %s: %s", name, string(output)) return nil }