Files
lst/controller/update.go

306 lines
7.6 KiB
Go

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)
rootDir := filepath.Join("..")
entries, err := os.ReadDir(rootDir)
if err != nil {
//log.Fatal("failed to read root dir: %v", err)
server.BroadcastToRoom("/", "update", "updateLogs", fmt.Sprintf("failed to read root dir: %v", err))
}
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")
stopService("lst_app")
stopAppPool("LogisticsSupportTool")
time.Sleep(2 * 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")
startService("LSTV2")
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)
}
}()
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 <service>"
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
}