Based on https://github.com/nnevatie/capnwebcpp
1package gocapnweb
2
3import (
4 "fmt"
5 "io"
6 "log"
7 "mime"
8 "net/http"
9 "os"
10 "path"
11 "path/filepath"
12 "strings"
13
14 "github.com/labstack/echo/v4"
15)
16
17// SetupFileEndpoint sets up a static file server endpoint using Echo.
18func SetupFileEndpoint(e *echo.Echo, urlPath string, fsRoot string) {
19 // Clean the URL path to ensure it ends with a slash for proper matching
20 if !strings.HasSuffix(urlPath, "/") {
21 urlPath += "/"
22 }
23
24 // Create handler function
25 fileHandler := func(c echo.Context) error {
26 // Extract the file path from the URL
27 requestPath := c.Request().URL.Path
28 filePath := requestPath
29
30 // Remove the base path prefix
31 basePath := strings.TrimSuffix(urlPath, "/")
32 if strings.HasPrefix(filePath, basePath) {
33 filePath = filePath[len(basePath):]
34 }
35
36 // Remove leading slash from file path
37 filePath = strings.TrimPrefix(filePath, "/")
38
39 // Default to index.html for directory requests
40 if filePath == "" || strings.HasSuffix(filePath, "/") {
41 filePath = path.Join(filePath, "index.html")
42 }
43
44 // Construct full filesystem path
45 fullPath := filepath.Join(fsRoot, filePath)
46
47 // Security check: ensure the path is within fsRoot
48 absRoot, err := filepath.Abs(fsRoot)
49 if err != nil {
50 log.Printf("Error getting absolute path for root: %v", err)
51 return echo.NewHTTPError(http.StatusInternalServerError, "Internal server error")
52 }
53
54 absPath, err := filepath.Abs(fullPath)
55 if err != nil {
56 log.Printf("Error getting absolute path for file: %v", err)
57 return echo.NewHTTPError(http.StatusInternalServerError, "Internal server error")
58 }
59
60 // Ensure the resolved path is within the root directory
61 if !strings.HasPrefix(absPath, absRoot) {
62 log.Printf("Access denied for path outside root: %s", absPath)
63 return echo.NewHTTPError(http.StatusForbidden, "Access denied")
64 }
65
66 // Check if file exists and is a regular file
67 fileInfo, err := os.Stat(absPath)
68 if err != nil {
69 if os.IsNotExist(err) {
70 return echo.NewHTTPError(http.StatusNotFound, "File not found")
71 } else {
72 log.Printf("Error accessing file: %v", err)
73 return echo.NewHTTPError(http.StatusInternalServerError, "Internal server error")
74 }
75 }
76
77 if !fileInfo.Mode().IsRegular() {
78 return echo.NewHTTPError(http.StatusNotFound, "Not a file")
79 }
80
81 // Open and read the file
82 file, err := os.Open(absPath)
83 if err != nil {
84 log.Printf("Error opening file: %v", err)
85 return echo.NewHTTPError(http.StatusInternalServerError, "Failed to read file")
86 }
87 defer file.Close()
88
89 // Determine content type based on file extension
90 contentType := getContentType(filepath.Ext(absPath))
91 c.Response().Header().Set("Content-Type", contentType)
92
93 // Set content length
94 c.Response().Header().Set("Content-Length", fmt.Sprintf("%d", fileInfo.Size()))
95
96 // Copy file contents to response
97 if _, err := io.Copy(c.Response(), file); err != nil {
98 log.Printf("Error writing file to response: %v", err)
99 return err
100 }
101
102 return nil
103 }
104
105 // Register the handler for the path pattern
106 pathPattern := urlPath + "*"
107 e.GET(pathPattern, fileHandler)
108}
109
110// getContentType returns the MIME type for a given file extension.
111func getContentType(ext string) string {
112 // First try the standard mime package
113 if mimeType := mime.TypeByExtension(ext); mimeType != "" {
114 return mimeType
115 }
116
117 // Fallback to common types
118 switch strings.ToLower(ext) {
119 case ".html", ".htm":
120 return "text/html; charset=utf-8"
121 case ".css":
122 return "text/css; charset=utf-8"
123 case ".js":
124 return "text/javascript; charset=utf-8"
125 case ".mjs":
126 return "text/javascript; charset=utf-8"
127 case ".json":
128 return "application/json; charset=utf-8"
129 case ".png":
130 return "image/png"
131 case ".jpg", ".jpeg":
132 return "image/jpeg"
133 case ".gif":
134 return "image/gif"
135 case ".svg":
136 return "image/svg+xml"
137 case ".txt":
138 return "text/plain; charset=utf-8"
139 case ".ico":
140 return "image/x-icon"
141 case ".woff":
142 return "font/woff"
143 case ".woff2":
144 return "font/woff2"
145 case ".ttf":
146 return "font/ttf"
147 case ".eot":
148 return "application/vnd.ms-fontobject"
149 default:
150 return "application/octet-stream"
151 }
152}