From 71dcbf814ba1acbfaca4efb4696fb0f5b39a8cb9 Mon Sep 17 00:00:00 2001 From: Blake Matthes Date: Sat, 6 Sep 2025 17:01:49 -0500 Subject: [PATCH] feat(controller): added copy by server only currently --- controller/copy_build.go | 125 +++++++++++++++++++++++ controller/go.mod | 11 +- controller/go.sum | 23 +++++ controller/main.go | 180 +++++++++++++++++++-------------- controller/stream_output.go | 21 ++++ controller/update_channel.go | 35 +++++++ controller/zip_app.go | 191 +++++++++++++++++++++++------------ 7 files changed, 445 insertions(+), 141 deletions(-) create mode 100644 controller/copy_build.go create mode 100644 controller/stream_output.go create mode 100644 controller/update_channel.go diff --git a/controller/copy_build.go b/controller/copy_build.go new file mode 100644 index 0000000..399dd68 --- /dev/null +++ b/controller/copy_build.go @@ -0,0 +1,125 @@ +package main + +import ( + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "regexp" + "sort" + "strconv" + + socketio "github.com/googollee/go-socket.io" +) + +func copyBuild(server *socketio.Server, plant string) { + // Load from environment in real-life code! + user := os.Getenv("ADM_USER") + pass := os.Getenv("ADM_PASS") + + // latest build + latestbuild := lastestBuild() + src := latestbuild + plantServer := fmt.Sprintf("\\\\%v\\e$\\lst", plant) + + // Build PowerShell + psScript := fmt.Sprintf(` +$secPass = ConvertTo-SecureString '%s' -AsPlainText -Force +$cred = New-Object System.Management.Automation.PSCredential ('%s', $secPass) + +# Clean up any previous mapped drive +Get-PSDrive -Name "z" -ErrorAction SilentlyContinue | Remove-PSDrive -Force + +try { + # Map a temporary drive "z:" to the destination UNC with creds + New-PSDrive -Name "z" -PSProvider FileSystem -Root '%s' -Credential $cred | Out-Null + + # Ensure target dir exists on server + if (-not (Test-Path -Path '%s')) { + New-Item -ItemType Directory -Path '%s' -Force | Out-Null + } + + Write-Host "Starting to copy files to the server" + Copy-Item -Path '%s' -Destination "z:\" -Force + + #Write-Host "Copy successful completed" +} +catch { + Write-Host "Error: $_" +} +finally { + # Always remove the drive + if (Get-PSDrive -Name "z" -ErrorAction SilentlyContinue) { + Remove-PSDrive -Name "z" + } +} +`, pass, user, plantServer, plantServer, plantServer, src) + + msg := fmt.Sprintf("Getting ready to copy %s to %s", src, plantServer) + fmt.Printf("Getting ready to copy %s to %s", src, plant) + + server.BroadcastToRoom("/", "update", "updateLogs", msg) + + cmd := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", psScript) + + stdout, _ := cmd.StdoutPipe() // writes to the system what the powershell script is doing + stderr, _ := cmd.StderrPipe() + + if err := cmd.Start(); err != nil { + server.BroadcastToRoom("/", "update", "updateLogs", fmt.Sprintf("❌ Failed to start copy: %v", err)) + fmt.Printf("❌ Failed to start copy: %v", err) + return + } + + // Forward stdout + stderr live + go streamOutput(stdout, server, "update") + go streamOutput(stderr, server, "update") + + if err := cmd.Wait(); err != nil { + server.BroadcastToRoom("/", "update", "updateLogs", fmt.Sprintf("❌ Copy process failed: %v", err)) + return + } + + server.BroadcastToRoom("/", "update", "updateLogs", fmt.Sprintf("✅ Copy to %s successful", plant)) +} + +func lastestBuild() string { + // Path to your build folder + buildDir := "../builds" + + entries, err := os.ReadDir(buildDir) + if err != nil { + log.Fatal(err) + } + + // Regex to match release-1234.zip + re := regexp.MustCompile(`^release-(\d+)\.zip$`) + + var builds []int + files := make(map[int]string) + + for _, entry := range entries { + if entry.IsDir() { + continue + } + name := entry.Name() + match := re.FindStringSubmatch(name) + if match != nil { + num, _ := strconv.Atoi(match[1]) + builds = append(builds, num) + files[num] = filepath.Join(buildDir, name) + } + } + + if len(builds) == 0 { + log.Fatal("No release zip files found") + } + + // Sort build numbers ascending + sort.Ints(builds) + latest := builds[len(builds)-1] + latestFile := files[latest] + + return latestFile +} diff --git a/controller/go.mod b/controller/go.mod index 14f64f4..b1a4046 100644 --- a/controller/go.mod +++ b/controller/go.mod @@ -2,7 +2,11 @@ module lst.net go 1.24.3 -require github.com/gin-gonic/gin v1.10.1 +require ( + github.com/gin-gonic/gin v1.10.1 + github.com/googollee/go-socket.io v1.7.0 + github.com/joho/godotenv v1.5.1 +) require ( github.com/bytedance/sonic v1.11.6 // indirect @@ -10,11 +14,16 @@ require ( github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/geoffgarside/ber v1.1.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.20.0 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/gofrs/uuid v4.0.0+incompatible // indirect + github.com/gomodule/redigo v1.8.4 // indirect + github.com/gorilla/websocket v1.4.2 // indirect + github.com/hirochachacha/go-smb2 v1.1.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/leodido/go-urn v1.4.0 // indirect diff --git a/controller/go.sum b/controller/go.sum index c1db8e3..dbfe423 100644 --- a/controller/go.sum +++ b/controller/go.sum @@ -10,6 +10,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/geoffgarside/ber v1.1.0 h1:qTmFG4jJbwiSzSXoNJeHcOprVzZ8Ulde2Rrrifu5U9w= +github.com/geoffgarside/ber v1.1.0/go.mod h1:jVPKeCbj6MvQZhwLYsGwaGI52oUorHoHKNecGT85ZCc= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= @@ -22,7 +24,19 @@ github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBEx github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gomodule/redigo v1.8.4 h1:Z5JUg94HMTR1XpwBaSH4vq3+PNSIykBLxMdglbw10gg= +github.com/gomodule/redigo v1.8.4/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/googollee/go-socket.io v1.7.0 h1:ODcQSAvVIPvKozXtUGuJDV3pLwdpBLDs1Uoq/QHIlY8= +github.com/googollee/go-socket.io v1.7.0/go.mod h1:0vGP8/dXR9SZUMMD4+xxaGo/lohOw3YWMh2WRiWeKxg= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hirochachacha/go-smb2 v1.1.0 h1:b6hs9qKIql9eVXAiN0M2wSFY5xnhbHAQoCwRKbaRTZI= +github.com/hirochachacha/go-smb2 v1.1.0/go.mod h1:8F1A4d5EZzrGu5R7PU163UcMRDJQl4FtcxjBfsY8TZE= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= @@ -46,6 +60,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -59,19 +74,27 @@ github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/controller/main.go b/controller/main.go index 937d1d5..06f83bd 100644 --- a/controller/main.go +++ b/controller/main.go @@ -3,89 +3,117 @@ package main import ( "fmt" "log" - "os" - "path/filepath" - "strings" + "net/http" + "time" - "github.com/gin-gonic/gin" + socketio "github.com/googollee/go-socket.io" + "github.com/joho/godotenv" ) func main() { - r := gin.Default() + err := godotenv.Load("../.env") + if err != nil { + //log := logger.New() + //log.Info("Warning: .env file not found (ok in Docker/production)", "system", map[string]interface{}{}) + fmt.Println("Warning: .env file not found") + } + server := socketio.NewServer(nil) - // POST /build -> run npm build + increment .build - r.POST("/build", func(c *gin.Context) { - host, err := os.Hostname() - if err != nil { - c.JSON(500, gin.H{"error": "Could not retrieve hostname"}) - return - } - - log.Println(host) - if strings.Contains(host, "VMS") || strings.Contains(host, "vms") { - c.JSON(500, gin.H{"error": "You are not allowed to run the build on a production server"}) - return - } - // run the old builder first this will be removed once we switch fully over here and shut down the old version - if err := runNpmV2Build(); err != nil { - c.JSON(500, gin.H{"error": "npm build failed on lstV2", "details": err.Error()}) - return - } - - // the new builder - if err := runNpmBuild(); err != nil { - c.JSON(500, gin.H{"error": "npm build failed", "details": err.Error()}) - return - } - - buildNum, err := bumpBuild() - if err != nil { - c.JSON(500, gin.H{"error": "failed updating build counter", "details": err.Error()}) - return - } - - // run the zip - includes, _ := loadIncludePatterns("../.include") - - // Name the archive after build number if available - data, _ := os.ReadFile("../.build") - buildNum1 := strings.TrimSpace(string(data)) - if buildNum1 == "" { - buildNum1 = "0" - } - - buildDir, err := getBuildDir() - if err != nil { - log.Fatal(err) - } - //buildDir, err := ensureBuildDir("../builds") - if err != nil { - c.JSON(500, gin.H{"error": err.Error()}) - return - } - - zipPath := filepath.Join(buildDir, fmt.Sprintf("release-%d.zip", buildNum)) - - if err := zipProject("..", zipPath, includes); err != nil { - c.JSON(500, gin.H{"error": err.Error()}) - return - } - - c.JSON(200, gin.H{ - "message": "build successful", - "build": buildNum, - }) + server.OnConnect("/", func(s socketio.Conn) error { + fmt.Println("✅ Client connected:", s.ID()) + return nil }) - // GET /version -> read current build version - r.GET("/version", func(c *gin.Context) { - data, err := os.ReadFile("../.build") - if err != nil { - c.JSON(404, gin.H{"error": "no build info"}) - return - } - c.JSON(200, gin.H{"build": strings.TrimSpace(string(data))}) + // ROOM SUBSCRIBE + server.OnEvent("/", "subscribe:logs", func(s socketio.Conn) { + s.Join("logs") + fmt.Println("📺", s.ID(), "joined logs") + s.Emit("info", "Subscribed to logs") }) - r.Run(":8080") // serve API + server.OnEvent("/", "unsubscribe:logs", func(s socketio.Conn) { + s.Leave("logs") + fmt.Println("👋", s.ID(), "left logs") + s.Emit("info", "Unsubscribed from logs") + }) + + server.OnEvent("/", "subscribe:errors", func(s socketio.Conn) { + s.Join("errors") + fmt.Println("📺", s.ID(), "joined errors") + s.Emit("info", "Subscribed to errors") + }) + + server.OnEvent("/", "unsubscribe:errors", func(s socketio.Conn) { + s.Leave("errors") + fmt.Println("👋", s.ID(), "left errors") + s.Emit("info", "Unsubscribed from errors") + }) + + server.OnDisconnect("/", func(s socketio.Conn, reason string) { + fmt.Println("❌ Client disconnected:", s.ID(), reason) + }) + + // build stuff + // Subscribe to build room + server.OnEvent("/", "subscribe:build", func(s socketio.Conn) { + s.Join("build") + fmt.Println("📺", s.ID(), "joined build room") + s.Emit("info", "Subscribed to build log room") + }) + + server.OnEvent("/", "unsubscribe:build", func(s socketio.Conn) { + s.Leave("build") + fmt.Println("👋", s.ID(), "left build room") + s.Emit("info", "Unsubscribed from build log room") + }) + + registerBuildChannel(server) + registerUpdateChannel(server) + + // Broadcast logs to room + go func() { + for i := 0; ; i++ { + time.Sleep(2 * time.Second) + msg := fmt.Sprintf("Log line %d @ %s", i, time.Now().Format(time.RFC3339)) + server.BroadcastToRoom("/", "logs", "logs", msg) + } + }() + + // Broadcast errors to room + go func() { + for i := 0; ; i++ { + time.Sleep(5 * time.Second) + msg := fmt.Sprintf("Error #%d @ %s", i, time.Now().Format(time.RFC3339)) + server.BroadcastToRoom("/", "errors", "errors", msg) + } + }() + + go server.Serve() + defer server.Close() + + // Enable CORS wrapper for Socket.IO route + http.Handle("/socket.io/", withCORS(server)) + http.Handle("/", http.FileServer(http.Dir("./static"))) + + fmt.Println("🚀 Socket.IO server running on :8000") + log.Fatal(http.ListenAndServe(":8000", nil)) +} + +// Reuse your proper CORS handler +func withCORS(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + origin := r.Header.Get("Origin") + if origin != "" { + w.Header().Set("Access-Control-Allow-Origin", origin) + w.Header().Set("Vary", "Origin") + } + w.Header().Set("Access-Control-Allow-Methods", "GET,POST,OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + w.Header().Set("Access-Control-Allow-Credentials", "true") + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusNoContent) + return + } + h.ServeHTTP(w, r) + }) } diff --git a/controller/stream_output.go b/controller/stream_output.go new file mode 100644 index 0000000..6fcca0b --- /dev/null +++ b/controller/stream_output.go @@ -0,0 +1,21 @@ +package main + +import ( + "bufio" + "fmt" + "io" + + socketio "github.com/googollee/go-socket.io" +) + +// streamOutput reads io.Reader line by line and broadcasts +func streamOutput(r io.Reader, server *socketio.Server, room string) { + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := scanner.Text() + server.BroadcastToRoom("/", room, "buildlogs", line) + } + if err := scanner.Err(); err != nil { + server.BroadcastToRoom("/", room, "buildlogs", fmt.Sprintf("❌ Log stream error: %v", err)) + } +} diff --git a/controller/update_channel.go b/controller/update_channel.go new file mode 100644 index 0000000..14455a5 --- /dev/null +++ b/controller/update_channel.go @@ -0,0 +1,35 @@ +package main + +import ( + "fmt" + + socketio "github.com/googollee/go-socket.io" +) + +func registerUpdateChannel(server *socketio.Server) { + + server.OnEvent("/", "subscribe:update", func(s socketio.Conn) { + s.Join("update") + s.Emit("updateLogs", "👋 Subscribed to update channel") + fmt.Println("📺", s.ID(), "joined update room") + }) + + // copy files based on the target passed over + server.OnEvent("/", "update", func(s socketio.Conn, target string) { + server.BroadcastToRoom("/", "update", "updateLogs", + fmt.Sprintf("🚀 Copying latest build to %v", target)) + copyBuild(server, target) + }) + + // update the server + // server.OnEvent("/", "update", func(s socketio.Conn, target string) { + // msg := fmt.Sprintf("🔧 Running updateServer on: %s", target) + // server.BroadcastToRoom("/update", "update", "updateLogs", msg) + + // }) + + server.OnEvent("/", "unsubscribe:update", func(s socketio.Conn) { + s.Leave("update") + s.Emit("updateLogs", "👋 Unsubscribed from update logs") + }) +} diff --git a/controller/zip_app.go b/controller/zip_app.go index a4bba9c..3ffcf09 100644 --- a/controller/zip_app.go +++ b/controller/zip_app.go @@ -8,92 +8,155 @@ import ( "io/fs" "os" "path/filepath" + "sort" + "strconv" "strings" + + socketio "github.com/googollee/go-socket.io" ) // ---- Load ignore patterns ---- func loadIncludePatterns(file string) ([]string, error) { - f, err := os.Open(file) - if err != nil { - return nil, err - } - defer f.Close() + f, err := os.Open(file) + if err != nil { + return nil, err + } + defer f.Close() - var patterns []string - scanner := bufio.NewScanner(f) - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if line == "" || strings.HasPrefix(line, "#") { - continue - } - patterns = append(patterns, filepath.ToSlash(filepath.Clean(line))) - } - return patterns, scanner.Err() + var patterns []string + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + patterns = append(patterns, filepath.ToSlash(filepath.Clean(line))) + } + return patterns, scanner.Err() } // ---- Simple matcher ---- // (Later swap for github.com/sabhiram/go-gitignore lib for proper rules) func shouldInclude(path string, includes []string) bool { - cleanPath := filepath.ToSlash(filepath.Clean(path)) // normalize to forward slashes + cleanPath := filepath.ToSlash(filepath.Clean(path)) // normalize to forward slashes - for _, pat := range includes { - p := filepath.ToSlash(filepath.Clean(pat)) + for _, pat := range includes { + p := filepath.ToSlash(filepath.Clean(pat)) - // exact match (file or folder) - if cleanPath == p { - return true - } + // exact match (file or folder) + if cleanPath == p { + return true + } - // if p is a folder, include all paths under it - if strings.HasPrefix(cleanPath, p+"/") { - return true - } - } - return false + // if p is a folder, include all paths under it + if strings.HasPrefix(cleanPath, p+"/") { + return true + } + } + return false } // ---- Zip the repo ---- -func zipProject(srcDir, zipFile string, includes []string) error { - outFile, err := os.Create(zipFile) - if err != nil { - return err - } - defer outFile.Close() +func zipProject(server *socketio.Server, srcDir, zipFile string, includes []string) error { + outFile, err := os.Create(zipFile) + if err != nil { + return err + } + defer outFile.Close() - archive := zip.NewWriter(outFile) - defer archive.Close() + archive := zip.NewWriter(outFile) + defer archive.Close() - err = filepath.WalkDir(srcDir, func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - if d.IsDir() { - return nil - } + err = filepath.WalkDir(srcDir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + server.BroadcastToRoom("/", "build", "buildlogs", fmt.Sprintf("❌ Error walking %s: %v", path, err)) + return err + } + if d.IsDir() { + return nil + } - relPath, _ := filepath.Rel(srcDir, path) - if !shouldInclude(relPath, includes) { - return nil // skip anything not explicitly included - } + relPath, _ := filepath.Rel(srcDir, path) - file, err := os.Open(path) - if err != nil { - return err - } - defer file.Close() + // only include files matching your patterns + if !shouldInclude(relPath, includes) { + return nil + } - info, _ := file.Stat() - header, _ := zip.FileInfoHeader(info) - header.Name = relPath + file, err := os.Open(path) + if err != nil { + server.BroadcastToRoom("/", "build", "buildlogs", fmt.Sprintf("❌ Could not open: %s", relPath)) + return err + } + defer file.Close() - writer, err := archive.CreateHeader(header) - if err != nil { - return err - } - _, err = io.Copy(writer, file) - fmt.Println("Added:", relPath) - return err - }) + info, _ := file.Stat() + header, _ := zip.FileInfoHeader(info) + header.Name = relPath - return err + writer, err := archive.CreateHeader(header) + if err != nil { + server.BroadcastToRoom("/", "build", "buildlogs", fmt.Sprintf("❌ Could not add header for: %s", relPath)) + return err + } + + if _, err := io.Copy(writer, file); err != nil { + server.BroadcastToRoom("/", "build", "buildlogs", fmt.Sprintf("❌ Error zipping: %s", relPath)) + return err + } + + // ✅ Send progress back to clients + server.BroadcastToRoom("/", "build", "buildlogs", fmt.Sprintf("📦 Added: %s", relPath)) + return nil + }) + + if err == nil { + server.BroadcastToRoom("/", "build", "buildlogs", fmt.Sprintf("✅ Archive created: %s", zipFile)) + doCleanup(server, filepath.Dir(zipFile)) + } + + return err +} + +func doCleanup(server *socketio.Server, dir string) { + // read max builds from env + maxStr := os.Getenv("MAX_BUILDS") + if maxStr == "" { + return + } + maxBuilds, err := strconv.Atoi(maxStr) + if err != nil || maxBuilds <= 0 { + return + } + + entries, err := os.ReadDir(dir) + if err != nil { + return + } + + var files []os.FileInfo + for _, e := range entries { + if !e.IsDir() && strings.HasSuffix(e.Name(), ".zip") { + info, err := e.Info() + if err == nil { + files = append(files, info) + } + } + } + + // Sort by modification time newest → oldest + sort.Slice(files, func(i, j int) bool { + return files[i].ModTime().After(files[j].ModTime()) + }) + + // Delete extras + if len(files) > maxBuilds { + for _, f := range files[maxBuilds:] { + toDelete := filepath.Join(dir, f.Name()) + if err := os.Remove(toDelete); err == nil { + server.BroadcastToRoom("/", "build", "buildlogs", + fmt.Sprintf("🧹 Removed old build: %s", f.Name())) + } + } + } }