feat: alive UI wave, galaxy presence, spread and fleet enhancements
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Dashboard ambient layer, comrade presence, Mission Deck and War Room, Emberwake supply chain, spread/docs publishing, fleet policy and modules API, CI docker mining, and refreshed USB pack.
This commit is contained in:
106
server/internal/api/spread_export.go
Normal file
106
server/internal/api/spread_export.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var slugSanitize = regexp.MustCompile(`[^a-zA-Z0-9._-]+`)
|
||||
|
||||
// sanitizeExportSlug lowercases and strips unsafe characters for filenames and campaign segments.
|
||||
func sanitizeExportSlug(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
s = strings.ToLower(s)
|
||||
s = slugSanitize.ReplaceAllString(s, "-")
|
||||
s = strings.Trim(s, "-.")
|
||||
if s == "" {
|
||||
return "site"
|
||||
}
|
||||
if len(s) > 48 {
|
||||
s = s[:48]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// zipTemplateReplacements walks templateDir, applies repl to file contents, and writes a ZIP archive.
|
||||
// remap rewrites archive entry paths (e.g. plugin-template → my-site).
|
||||
func zipTemplateReplacements(templateDir string, repl map[string]string, remap func(rel string) string) ([]byte, error) {
|
||||
templateDir, err := filepath.Abs(templateDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
zw := zip.NewWriter(&buf)
|
||||
err = filepath.Walk(templateDir, func(path string, info os.FileInfo, walkErr error) error {
|
||||
if walkErr != nil || info.IsDir() {
|
||||
return walkErr
|
||||
}
|
||||
rel, err := filepath.Rel(templateDir, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rel = filepath.ToSlash(rel)
|
||||
if remap != nil {
|
||||
rel = remap(rel)
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content := string(data)
|
||||
for k, v := range repl {
|
||||
content = strings.ReplaceAll(content, k, v)
|
||||
}
|
||||
w, err := zw.Create(rel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.WriteString(w, content)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := zw.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func writeZipAttachment(w http.ResponseWriter, filename string, data []byte) {
|
||||
w.Header().Set("Content-Type", "application/zip")
|
||||
w.Header().Set("Content-Disposition", `attachment; filename="`+filename+`"`)
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
func slugDisplayName(slug string) string {
|
||||
parts := strings.Split(slug, "-")
|
||||
for i, p := range parts {
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
parts[i] = strings.ToUpper(p[:1]) + p[1:]
|
||||
}
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
func buildQuerySuffix(buildID, campaign string) (querySuffix, getQuerySuffix string) {
|
||||
var qparts []string
|
||||
if buildID != "" {
|
||||
qparts = append(qparts, "pin="+buildID)
|
||||
}
|
||||
if campaign != "" {
|
||||
qparts = append(qparts, "c="+campaign)
|
||||
}
|
||||
if len(qparts) == 0 {
|
||||
return "", ""
|
||||
}
|
||||
joined := strings.Join(qparts, "&")
|
||||
return "?" + joined, "&" + joined
|
||||
}
|
||||
Reference in New Issue
Block a user