handler.go

 1package routing
 2
 3import (
 4	"fmt"
 5	"net/http"
 6	"os"
 7	"path/filepath"
 8	"strings"
 9)
10
11// RouteFileHandler serves route files from the data directory.
12// URL pattern: /route/{hash}.{polyline6,geojson}
13// Maps to: ./data/route/{hash[0:2]}/{hash[2:]}.{ext}
14func RouteFileHandler(baseDir string) http.HandlerFunc {
15	return func(w http.ResponseWriter, r *http.Request) {
16		// Extract hash and extension from URL path
17		// Path is expected to be /route/{hash}.{ext}
18		path := strings.TrimPrefix(r.URL.Path, "/route/")
19		if path == "" {
20			http.Error(w, "Not found", http.StatusNotFound)
21			return
22		}
23
24		// Split into hash and extension
25		parts := strings.SplitN(path, ".", 2)
26		if len(parts) != 2 {
27			http.Error(w, "Invalid filename format", http.StatusBadRequest)
28			return
29		}
30
31		hash := parts[0]
32		ext := parts[1]
33
34		// Validate hash length (SHA256 hex = 64 chars)
35		if len(hash) != 64 {
36			http.Error(w, "Invalid hash", http.StatusBadRequest)
37			return
38		}
39
40		// Validate extension
41		var contentType string
42		switch ext {
43		case "polyline6":
44			contentType = "text/plain"
45		case "geojson":
46			contentType = "application/geo+json"
47		default:
48			http.Error(w, "Unsupported file type", http.StatusBadRequest)
49			return
50		}
51
52		// Construct file path: {baseDir}/{hash[0:2]}/{hash[2:]}.{ext}
53		filePath := filepath.Join(baseDir, hash[0:2], hash[2:]+"."+ext)
54
55		// Open the file
56		file, err := os.Open(filePath)
57		if err != nil {
58			if os.IsNotExist(err) {
59				http.Error(w, "Not found", http.StatusNotFound)
60				return
61			}
62			http.Error(w, fmt.Sprintf("Error opening file: %v", err), http.StatusInternalServerError)
63			return
64		}
65		defer file.Close()
66
67		// Get file info for Content-Length
68		stat, err := file.Stat()
69		if err != nil {
70			http.Error(w, fmt.Sprintf("Error getting file info: %v", err), http.StatusInternalServerError)
71			return
72		}
73
74		// Set headers
75		w.Header().Set("Content-Type", contentType)
76		w.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size()))
77		w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
78
79		// Stream the file
80		w.WriteHeader(http.StatusOK)
81		http.ServeContent(w, r, filePath, stat.ModTime(), file)
82	}
83}