package main import ( "archive/zip" "bytes" "compress/gzip" "crypto/sha256" _ "embed" "flag" "fmt" "io" "log" "mime" "net" "net/http" "os/exec" "path" "runtime" "strings" "time" ) //go:embed app.zip var bundle []byte func handler() http.Handler { archive, err := zip.NewReader(bytes.NewReader(bundle), int64(len(bundle))) if err != nil { panic(err) } files := make(map[string]*zip.File) for _, file := range archive.File { files[strings.TrimSuffix(file.Name, ".gz")] = file } mux := http.NewServeMux() mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") io.WriteString(w, `{"status":"ok","service":"fdm-studio"}`) }) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet && r.Method != http.MethodHead { w.WriteHeader(http.StatusMethodNotAllowed) return } name := strings.TrimPrefix(path.Clean(r.URL.Path), "/") if name == "" { name = "index.html" } file, ok := files[name] if !ok { http.NotFound(w, r) return } reader, err := file.Open() if err != nil { http.Error(w, "资源读取失败", 500) return } defer reader.Close() data, err := io.ReadAll(reader) if err != nil { http.Error(w, "资源读取失败", 500) return } encoded := false for _, item := range strings.Split(r.Header.Get("Accept-Encoding"), ",") { if strings.TrimSpace(item) == "gzip" { encoded = true } } w.Header().Set("Vary", "Accept-Encoding") if encoded { w.Header().Set("Content-Encoding", "gzip") } else { decoded, err := gzip.NewReader(bytes.NewReader(data)) if err != nil { http.Error(w, "资源解码失败", 500) return } defer decoded.Close() data, err = io.ReadAll(decoded) if err != nil { http.Error(w, "资源解码失败", 500) return } } contentType := mime.TypeByExtension(path.Ext(name)) if contentType == "" { contentType = "application/octet-stream" } w.Header().Set("Content-Type", contentType) w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("Cache-Control", "no-cache") if strings.HasPrefix(name, "assets/") || strings.HasPrefix(name, "data/stream/") || strings.HasPrefix(name, "data/full/") { w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") } w.Header().Set("ETag", fmt.Sprintf(`"%x"`, sha256.Sum256(data))) http.ServeContent(w, r, name, time.Time{}, bytes.NewReader(data)) }) return mux } func main() { defaultListen := "127.0.0.1:9999" if runtime.GOOS == "windows" { defaultListen = "0.0.0.0:9999" } listen := flag.String("listen", defaultListen, "服务监听地址") open := flag.Bool("open", runtime.GOOS == "windows", "启动时打开浏览器") flag.Parse() server := &http.Server{Addr: *listen, Handler: handler(), ReadHeaderTimeout: 10 * time.Second, IdleTimeout: 60 * time.Second} listener, err := net.Listen("tcp", *listen) if err != nil { log.Fatal(err) } _, port, _ := net.SplitHostPort(*listen) log.Printf("FDM 演讲服务已启动:http://127.0.0.1:%s", port) if strings.HasPrefix(*listen, "0.0.0.0:") { addresses, _ := net.InterfaceAddrs() for _, address := range addresses { if ip, ok := address.(*net.IPNet); ok && !ip.IP.IsLoopback() && ip.IP.To4() != nil { log.Printf("同一局域网设备可访问:http://%s:%s", ip.IP, port) } } } if *open && runtime.GOOS == "windows" { go func() { time.Sleep(500 * time.Millisecond) if err := exec.Command("rundll32", "url.dll,FileProtocolHandler", "http://127.0.0.1:"+port).Run(); err != nil { log.Print("请手动打开上方网址") } }() } log.Fatal(server.Serve(listener)) }