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 }