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" } }