package main import ( "archive/zip" "bufio" "fmt" "io" "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() 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 for _, pat := range includes { p := filepath.ToSlash(filepath.Clean(pat)) // 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 } // ---- Zip the repo ---- 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() 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) // only include files matching your patterns if !shouldInclude(relPath, includes) { return nil } file, err := os.Open(path) if err != nil { server.BroadcastToRoom("/", "build", "buildlogs", fmt.Sprintf("❌ Could not open: %s", relPath)) return err } defer file.Close() info, _ := file.Stat() header, _ := zip.FileInfoHeader(info) header.Name = relPath 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())) } } } }