Static Files Guide
Serve static assets efficiently and securely with Squirrel
Basic Static File Serving
Serve static files from a directory:
mux := squirrel.NewSqurlMux()
// Serve files from ./static directory
mux.Handle("/static/", squirrel.StripPrefix("/static/", squirrel.FileServer(squirrel.Dir("./static"))))
// Alternative: serve a specific file
mux.HandleFunc("/favicon.ico", func(w *squirrel.Response, r *squirrel.Request) {
squirrel.ServeFile(w, r, "./static/favicon.ico")
})
// Serve from multiple directories
mux.Handle("/assets/", squirrel.StripPrefix("/assets/", squirrel.FileServer(squirrel.Dir("./assets"))))
mux.Handle("/uploads/", squirrel.StripPrefix("/uploads/", squirrel.FileServer(squirrel.Dir("./uploads"))))Custom File Server
Create a custom file server with additional features:
func CustomFileServer(root string) squirrel.HandlerFunc {
fs := squirrel.FileServer(squirrel.Dir(root))
return func(w *squirrel.Response, r *squirrel.Request) {
// Security: prevent directory traversal
if strings.Contains(r.URL.Path, "..") {
w.WriteStatus(400)
w.WriteString("Invalid path")
return
}
// Add security headers
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
// Set cache headers for static assets
if isStaticAsset(r.URL.Path) {
w.Header().Set("Cache-Control", "public, max-age=31536000") // 1 year
}
fs.ServeHTTP(w, r)
}
}
func isStaticAsset(path string) bool {
staticExts := []string{".css", ".js", ".png", ".jpg", ".jpeg", ".gif", ".ico", ".svg"}
ext := strings.ToLower(filepath.Ext(path))
for _, staticExt := range staticExts {
if ext == staticExt {
return true
}
}
return false
}
// Usage
mux.Handle("/static/", squirrel.StripPrefix("/static/", CustomFileServer("./static")))Content Type Detection
Automatically set correct content types:
func ServeFileWithContentType(w *squirrel.Response, r *squirrel.Request, filepath string) {
// Open the file
file, err := os.Open(filepath)
if err != nil {
w.WriteStatus(404)
w.WriteString("File not found")
return
}
defer file.Close()
// Get file info
fileInfo, err := file.Stat()
if err != nil {
w.WriteStatus(500)
w.WriteString("Error reading file")
return
}
// Detect content type
buffer := make([]byte, 512)
_, err = file.Read(buffer)
if err != nil {
w.WriteStatus(500)
w.WriteString("Error reading file")
return
}
contentType := http.DetectContentType(buffer)
// Override for specific extensions
ext := strings.ToLower(filepath.Ext(filepath))
switch ext {
case ".css":
contentType = "text/css"
case ".js":
contentType = "application/javascript"
case ".json":
contentType = "application/json"
case ".xml":
contentType = "application/xml"
}
// Set headers
w.Header().Set("Content-Type", contentType)
w.Header().Set("Content-Length", fmt.Sprintf("%d", fileInfo.Size()))
// Reset file pointer to beginning
file.Seek(0, 0)
// Copy file to response
io.Copy(w, file)
}
// Usage in handler
func serveAssetHandler(w *squirrel.Response, r *squirrel.Request) {
filename := r.PathValue("filename")
filepath := "./assets/" + filename
ServeFileWithContentType(w, r, filepath)
}File Upload Handling
Handle file uploads securely:
func uploadHandler(w *squirrel.Response, r *squirrel.Request) {
// Limit upload size (10MB)
r.Body = http.MaxBytesReader(w, r.Body, 10<<20)
// Parse multipart form
if err := r.ParseMultipartForm(10 << 20); err != nil {
w.WriteStatus(400)
w.JSON(map[string]string{"error": "File too large"})
return
}
// Get the file
file, header, err := r.FormFile("file")
if err != nil {
w.WriteStatus(400)
w.JSON(map[string]string{"error": "No file provided"})
return
}
defer file.Close()
// Validate file type
allowedTypes := map[string]bool{
"image/jpeg": true,
"image/png": true,
"image/gif": true,
"text/plain": true,
}
// Read first 512 bytes to detect content type
buffer := make([]byte, 512)
_, err = file.Read(buffer)
if err != nil {
w.WriteStatus(500)
w.JSON(map[string]string{"error": "Error reading file"})
return
}
contentType := http.DetectContentType(buffer)
if !allowedTypes[contentType] {
w.WriteStatus(400)
w.JSON(map[string]string{"error": "File type not allowed"})
return
}
// Reset file pointer
file.Seek(0, 0)
// Generate unique filename
ext := filepath.Ext(header.Filename)
filename := fmt.Sprintf("%d%s", time.Now().UnixNano(), ext)
filepath := "./uploads/" + filename
// Create the file
dst, err := os.Create(filepath)
if err != nil {
w.WriteStatus(500)
w.JSON(map[string]string{"error": "Error saving file"})
return
}
defer dst.Close()
// Copy file content
_, err = io.Copy(dst, file)
if err != nil {
w.WriteStatus(500)
w.JSON(map[string]string{"error": "Error saving file"})
return
}
w.JSON(map[string]string{
"filename": filename,
"size": fmt.Sprintf("%d", header.Size),
"type": contentType,
})
}Caching Strategies
Implement efficient caching for static assets:
func CachingFileServer(root string) squirrel.HandlerFunc {
fs := squirrel.FileServer(squirrel.Dir(root))
return func(w *squirrel.Response, r *squirrel.Request) {
// Get file info for ETag generation
filepath := path.Join(root, r.URL.Path)
fileInfo, err := os.Stat(filepath)
if err != nil {
w.WriteStatus(404)
return
}
// Generate ETag based on file modification time and size
etag := fmt.Sprintf(`"%x-%x"`, fileInfo.ModTime().Unix(), fileInfo.Size())
// Check If-None-Match header
if match := r.Header.Get("If-None-Match"); match != "" {
if match == etag {
w.WriteStatus(304) // Not Modified
return
}
}
// Set caching headers
w.Header().Set("ETag", etag)
w.Header().Set("Last-Modified", fileInfo.ModTime().UTC().Format(http.TimeFormat))
// Set cache duration based on file type
ext := strings.ToLower(filepath.Ext(r.URL.Path))
switch ext {
case ".css", ".js":
w.Header().Set("Cache-Control", "public, max-age=86400") // 1 day
case ".png", ".jpg", ".jpeg", ".gif", ".ico":
w.Header().Set("Cache-Control", "public, max-age=604800") // 1 week
case ".woff", ".woff2", ".ttf":
w.Header().Set("Cache-Control", "public, max-age=2592000") // 30 days
default:
w.Header().Set("Cache-Control", "public, max-age=3600") // 1 hour
}
fs.ServeHTTP(w, r)
}
}
// Usage
mux.Handle("/static/", squirrel.StripPrefix("/static/", CachingFileServer("./static")))Secure File Serving
Implement security measures for file serving:
func SecureFileServer(root string, allowedExts []string) squirrel.HandlerFunc {
return func(w *squirrel.Response, r *squirrel.Request) {
// Security checks
if strings.Contains(r.URL.Path, "..") {
w.WriteStatus(400)
w.WriteString("Invalid path")
return
}
// Check file extension
ext := strings.ToLower(filepath.Ext(r.URL.Path))
allowed := false
for _, allowedExt := range allowedExts {
if ext == allowedExt {
allowed = true
break
}
}
if !allowed {
w.WriteStatus(403)
w.WriteString("File type not allowed")
return
}
// Construct safe file path
cleanPath := filepath.Clean(r.URL.Path)
fullPath := filepath.Join(root, cleanPath)
// Ensure the path is within the root directory
if !strings.HasPrefix(fullPath, filepath.Clean(root)+string(os.PathSeparator)) {
w.WriteStatus(403)
w.WriteString("Access denied")
return
}
// Check if file exists and is not a directory
fileInfo, err := os.Stat(fullPath)
if err != nil {
w.WriteStatus(404)
w.WriteString("File not found")
return
}
if fileInfo.IsDir() {
w.WriteStatus(403)
w.WriteString("Directory listing not allowed")
return
}
// Add security headers
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("Content-Security-Policy", "default-src 'none'")
// Serve the file
squirrel.ServeFile(w, r, fullPath)
}
}
// Usage with allowed extensions
allowedExts := []string{".css", ".js", ".png", ".jpg", ".jpeg", ".gif", ".ico", ".svg"}
mux.Handle("/assets/", squirrel.StripPrefix("/assets/", SecureFileServer("./assets", allowedExts)))Compression
Enable gzip compression for static files:
func CompressedFileServer(root string) squirrel.HandlerFunc {
return func(w *squirrel.Response, r *squirrel.Request) {
// Check if client accepts gzip
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
// Serve uncompressed
squirrel.FileServer(squirrel.Dir(root)).ServeHTTP(w, r)
return
}
// Check if file should be compressed
ext := strings.ToLower(filepath.Ext(r.URL.Path))
compressible := map[string]bool{
".css": true,
".js": true,
".html": true,
".json": true,
".xml": true,
".svg": true,
}
if !compressible[ext] {
// Serve uncompressed for non-compressible files
squirrel.FileServer(squirrel.Dir(root)).ServeHTTP(w, r)
return
}
// Set compression headers
w.Header().Set("Content-Encoding", "gzip")
w.Header().Set("Vary", "Accept-Encoding")
// Create gzip writer
gz := gzip.NewWriter(w)
defer gz.Close()
// Create a custom ResponseWriter that writes to gzip
gzw := &gzipResponseWriter{
ResponseWriter: w,
Writer: gz,
}
squirrel.FileServer(squirrel.Dir(root)).ServeHTTP(gzw, r)
}
}
type gzipResponseWriter struct {
squirrel.ResponseWriter
io.Writer
}
func (w *gzipResponseWriter) Write(b []byte) (int, error) {
return w.Writer.Write(b)
}