Based on https://github.com/nnevatie/capnwebcpp
at main 4.1 kB view raw
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}