1package main23import (4 "encoding/json"5 "io"6 "io/fs"7 "net/http"8 "strconv"9 "strings"1011 "github.com/FelineStateMachine/atlas/internal/bundle"12)1314// dataTypes names the content types the bundle layout can hold. The framework15// sends nosniff, so every response says what it is or the browser drops it.16var dataTypes = map[string]string{17 ".json": "application/json",18 ".text": "application/json",19 ".bin": "application/octet-stream",20 ".jpg": "image/jpeg",21 ".png": "image/png",22 ".webp": "image/webp",23 ".svg": "image/svg+xml",24}2526func routes(files fs.FS, registry *bundle.Registry) http.Handler {27 mux := http.NewServeMux()28 registerParityRoutes(mux)2930 // The catalog is composed from whatever bundles are installed right now,31 // so it is the one response that must never be cached.32 mux.HandleFunc("GET /data/catalog.json", func(w http.ResponseWriter, r *http.Request) {33 w.Header().Set("Content-Type", "application/json")34 w.Header().Set("Cache-Control", "no-store")35 _, _ = w.Write(registry.Snapshot().Catalog)36 })3738 // Volume content lives under a stamp taken from the bundle's own version,39 // so every URL names exactly one build of one volume: cacheable forever,40 // and gone -- 404, and the frontend refetches the catalog -- the moment41 // a newer bundle takes the slug over.42 mux.HandleFunc("GET /data/v/{slug}/{stamp}/{rest...}", func(w http.ResponseWriter, r *http.Request) {43 held, ok := registry.Snapshot().Volumes[r.PathValue("slug")]44 if !ok || r.PathValue("stamp") != bundle.ShortStamp(held.Manifest.Version.Stamp) {45 http.NotFound(w, r)46 return47 }48 rest := r.PathValue("rest")49 if !strings.HasPrefix(rest, "worlds/") && !strings.HasPrefix(rest, "tiles/") &&50 !strings.HasPrefix(rest, "icons/") {51 http.NotFound(w, r)52 return53 }54 dot := strings.LastIndexByte(rest, '.')55 if dot < 0 {56 http.NotFound(w, r)57 return58 }59 kind, ok := dataTypes[rest[dot:]]60 if !ok {61 http.NotFound(w, r)62 return63 }64 entry, size, err := held.OpenEntry(rest)65 if err != nil {66 http.NotFound(w, r)67 return68 }69 defer entry.Close()70 w.Header().Set("Content-Type", kind)71 w.Header().Set("Content-Length", strconv.FormatInt(size, 10))72 w.Header().Set("Cache-Control", "private, max-age=31536000, immutable")73 _, _ = io.Copy(w, entry)74 })7576 // Adding a volume from inside the application: a native picker, then the77 // chosen files are validated and copied into the bundles directory, which78 // is the same road a hand-dropped file takes.79 mux.HandleFunc("POST /data/bundles/import", func(w http.ResponseWriter, r *http.Request) {80 paths, ready := pickBundleFiles()81 w.Header().Set("Content-Type", "application/json")82 if !ready {83 w.WriteHeader(http.StatusServiceUnavailable)84 _ = json.NewEncoder(w).Encode(map[string]any{85 "problems": []string{"the application window is not ready"},86 })87 return88 }89 installed, refused := registry.Install(paths)90 _ = json.NewEncoder(w).Encode(map[string]any{91 "imported": installed,92 "problems": refused,93 })94 })9596 mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {97 if r.URL.Path != "/" {98 http.NotFound(w, r)99 return100 }101 data, err := fs.ReadFile(files, "assets/index.html")102 if err != nil {103 http.Error(w, "embedded application shell is unavailable", http.StatusInternalServerError)104 return105 }106 w.Header().Set("Content-Type", "text/html; charset=utf-8")107 _, _ = w.Write(data)108 })109 return mux110}