Runxi Yu’s Personal Forge
Warning: Due to various recent migrations, viewing non-HEAD refs may be broken.
/main.go (raw)
package main
import (
"bytes"
"embed"
"encoding/csv"
"encoding/json"
"fmt"
"io/fs"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"time"
)
//go:embed static/*
var embeddedStatic embed.FS
func main() {
fs, err := fs.Sub(embeddedStatic, "static")
if err != nil {
panic(err)
}
http.Handle("/", http.FileServer(http.FS(fs)))
http.HandleFunc("/submit", handleForm)
http.HandleFunc("/fwdaiusyflaidsunfuoiawenufwylnfkalhjdslkjfhjlwadk.csv", handleCSV)
if err := os.MkdirAll("responses", 0755); err != nil {
log.Fatalf("unable to create responses folder: %v", err)
}
log.Println("listening 127.0.0.1:9074")
log.Fatal(http.ListenAndServe("127.0.0.1:9074", nil))
}
func handleForm(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, "Unable to parse form", http.StatusBadRequest)
return
}
data := make(map[string]string)
for key, values := range r.PostForm {
if len(values) > 0 {
data[key] = values[0]
} else {
data[key] = ""
}
}
ip := r.Header.Get("X-Forwarded-For")
if ip != "" {
ip = strings.Split(ip, ",")[0]
ip = strings.TrimSpace(ip)
} else {
ip, _, _ = net.SplitHostPort(r.RemoteAddr)
}
data["ip_address"] = ip
filename := time.Now().Format("20060102_150405.000") + ".json"
filePath := filepath.Join("responses", filename)
file, err := os.Create(filePath)
if err != nil {
http.Error(w, "无法打开保存文件:"+err.Error(), http.StatusInternalServerError)
return
}
defer file.Close()
encoder := json.NewEncoder(file)
encoder.SetIndent("", "\t")
if err := encoder.Encode(data); err != nil {
http.Error(w, "无法写入保存文件:"+err.Error(), http.StatusInternalServerError)
return
}
go func(data map[string]string) {
cmd := "/sbin/sendmail"
args := []string{"-t", "-i"}
bodyBuilder := &strings.Builder{}
fmt.Fprintf(bodyBuilder, "From: tiffany@runxiyu.org\n")
fmt.Fprintf(bodyBuilder, "To: tiffany@runxiyu.org\n")
fmt.Fprintf(bodyBuilder, "Subject: Survey response from %s\n", data["ip_address"])
fmt.Fprintf(bodyBuilder, "MIME-Version: 1.0\n")
fmt.Fprintf(bodyBuilder, "Content-Type: text/plain; charset=UTF-8\n")
fmt.Fprintf(bodyBuilder, "Content-Transfer-Encoding: 8bit\n\n")
jsonBytes, err := json.MarshalIndent(data, "", "\t")
if err == nil {
bodyBuilder.Write(jsonBytes)
sendmail := exec.Command(cmd, args...)
stdin, err := sendmail.StdinPipe()
if err == nil {
if err := sendmail.Start(); err == nil {
stdin.Write([]byte(bodyBuilder.String()))
stdin.Close()
sendmail.Wait()
}
}
}
}(data)
fmt.Fprintf(w, `恭喜您完成所有测试并衷心感谢您的参与!
如有兴趣了解实验数据分析结果,请关注微信公众号 @WIT studio。`)
}
func handleCSV(w http.ResponseWriter, r *http.Request) {
files, err := ioutil.ReadDir("responses")
if err != nil {
http.Error(w, "无法读取 responses 目录:"+err.Error(), http.StatusInternalServerError)
return
}
records := []map[string]string{}
fieldSet := make(map[string]struct{})
// First pass: collect all unique field names
for _, file := range files {
if !file.IsDir() && strings.HasSuffix(file.Name(), ".json") {
content, err := ioutil.ReadFile(filepath.Join("responses", file.Name()))
if err != nil {
continue
}
var data map[string]string
if err := json.Unmarshal(content, &data); err != nil {
continue
}
for k := range data {
fieldSet[k] = struct{}{}
}
records = append(records, data)
}
}
// Define the desired column order
preferredOrder := []string{
"ip_address", "gender", "age", "dialect", "guanhua_details", "wuyu_details",
"other_details", "usage_frequency", "fluency", "foreign_language",
"music_training", "music_freq", "absolute_pitch",
}
// Add q1-q20
for i := 1; i <= 20; i++ {
preferredOrder = append(preferredOrder, fmt.Sprintf("q%d", i))
}
// Add cadence1-4
for i := 1; i <= 4; i++ {
preferredOrder = append(preferredOrder, fmt.Sprintf("cadence%d", i))
}
// Add cadence_sample_1-4
for i := 1; i <= 4; i++ {
preferredOrder = append(preferredOrder, fmt.Sprintf("cadence_sample_%d", i))
}
// Add style columns
preferredOrder = append(preferredOrder,
"style1trap", "style2drill", "style3drumbass", "style4reggaetton", "style5rb",
)
// Add style_sample_1-5
for i := 1; i <= 5; i++ {
preferredOrder = append(preferredOrder, fmt.Sprintf("style_sample_%d", i))
}
// Build final field list with preferred order first, then remaining fields
fields := make([]string, 0, len(fieldSet))
remainingFields := make([]string, 0)
// First add fields that are in preferred order and exist in the data
for _, field := range preferredOrder {
if _, exists := fieldSet[field]; exists {
fields = append(fields, field)
delete(fieldSet, field) // Remove from set to track remaining fields
}
}
// Then add any remaining fields that weren't in preferred order
for field := range fieldSet {
remainingFields = append(remainingFields, field)
}
// Sort remaining fields alphabetically for consistency
sort.Strings(remainingFields)
fields = append(fields, remainingFields...)
// Prepare CSV buffer
var buf bytes.Buffer
buf.WriteString("\uFEFF") // UTF-8 BOM
writer := csv.NewWriter(&buf)
writer.Write(fields)
for _, record := range records {
row := make([]string, len(fields))
for i, field := range fields {
row[i] = record[field] // Will be empty string if field doesn't exist
}
writer.Write(row)
}
writer.Flush()
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
w.Header().Set("Content-Disposition", "attachment; filename=\"responses.csv\"")
w.Write(buf.Bytes())
}