136 lines
3.6 KiB
Go
136 lines
3.6 KiB
Go
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))
|
||
}
|