Some checks failed
CI / build (push) Failing after 36s
WASM reactive UI framework for Go: - reactive/ - Signal[T], Effect, Runtime - ui/ - Button, Text, Input, View, Canvas, SVG components - navigation/ - Router, guards, history management - auth/ - OIDC client for WASM applications - host/ - Static file server Extracted from arcadia as open-source component. Co-Authored-By: Claude <noreply@anthropic.com>
150 lines
3.6 KiB
Go
150 lines
3.6 KiB
Go
package host
|
|
|
|
import (
|
|
"compress/gzip"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// Server provides a high-performance static file server with gzip compression
|
|
// optimized for serving WASM applications built with the Iris framework.
|
|
type Server struct {
|
|
publicDir string
|
|
indexFile string
|
|
}
|
|
|
|
// New creates a new Server instance
|
|
func New(publicDir, indexFile string) *Server {
|
|
return &Server{
|
|
publicDir: publicDir,
|
|
indexFile: indexFile,
|
|
}
|
|
}
|
|
|
|
// ServeHTTP implements http.Handler interface
|
|
// Falls back to index file when:
|
|
// (1) Request path is not found
|
|
// (2) Request path is a directory
|
|
// Otherwise serves the requested file with gzip compression if supported.
|
|
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
p := filepath.Join(s.publicDir, filepath.Clean(r.URL.Path))
|
|
|
|
if info, err := os.Stat(p); err != nil {
|
|
s.serveFileWithCompression(w, r, filepath.Join(s.publicDir, s.indexFile))
|
|
return
|
|
} else if info.IsDir() {
|
|
s.serveFileWithCompression(w, r, filepath.Join(s.publicDir, s.indexFile))
|
|
return
|
|
}
|
|
|
|
s.serveFileWithCompression(w, r, p)
|
|
}
|
|
|
|
// serveFileWithCompression serves a file with gzip compression if supported by client
|
|
func (s *Server) serveFileWithCompression(w http.ResponseWriter, r *http.Request, filePath string) {
|
|
// Check if client accepts gzip
|
|
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
|
http.ServeFile(w, r, filePath)
|
|
return
|
|
}
|
|
|
|
// Open and read file
|
|
file, err := os.Open(filePath)
|
|
if err != nil {
|
|
http.Error(w, "File not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
// Get file info for headers
|
|
fileInfo, err := file.Stat()
|
|
if err != nil {
|
|
http.Error(w, "Error reading file", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Set content type based on file extension
|
|
contentType := getContentType(filepath.Ext(filePath))
|
|
w.Header().Set("Content-Type", contentType)
|
|
|
|
// Check if we should compress this file type
|
|
if shouldCompress(contentType) {
|
|
// Set gzip headers
|
|
w.Header().Set("Content-Encoding", "gzip")
|
|
w.Header().Set("Vary", "Accept-Encoding")
|
|
|
|
// Create gzip writer
|
|
gz := gzip.NewWriter(w)
|
|
defer gz.Close()
|
|
|
|
// Copy file content through gzip
|
|
_, err = io.Copy(gz, file)
|
|
if err != nil {
|
|
http.Error(w, "Error compressing file", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
} else {
|
|
// Serve without compression
|
|
http.ServeContent(w, r, fileInfo.Name(), fileInfo.ModTime(), file)
|
|
}
|
|
}
|
|
|
|
// shouldCompress determines if the content should be compressed based on Content-Type
|
|
func shouldCompress(contentType string) bool {
|
|
compressibleTypes := []string{
|
|
"text/html",
|
|
"text/css",
|
|
"text/javascript",
|
|
"application/javascript",
|
|
"application/json",
|
|
"application/wasm",
|
|
"text/plain",
|
|
"image/svg+xml",
|
|
}
|
|
|
|
for _, t := range compressibleTypes {
|
|
if strings.Contains(contentType, t) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// getContentType returns the appropriate content type for a file extension
|
|
func getContentType(ext string) string {
|
|
switch ext {
|
|
case ".html":
|
|
return "text/html; charset=utf-8"
|
|
case ".css":
|
|
return "text/css"
|
|
case ".js":
|
|
return "application/javascript"
|
|
case ".wasm":
|
|
return "application/wasm"
|
|
case ".json":
|
|
return "application/json"
|
|
case ".ico":
|
|
return "image/x-icon"
|
|
case ".png":
|
|
return "image/png"
|
|
case ".jpg", ".jpeg":
|
|
return "image/jpeg"
|
|
case ".gif":
|
|
return "image/gif"
|
|
case ".svg":
|
|
return "image/svg+xml"
|
|
case ".woff":
|
|
return "font/woff"
|
|
case ".woff2":
|
|
return "font/woff2"
|
|
case ".ttf":
|
|
return "font/ttf"
|
|
case ".eot":
|
|
return "application/vnd.ms-fontobject"
|
|
default:
|
|
return "application/octet-stream"
|
|
}
|
|
} |