-
Notifications
You must be signed in to change notification settings - Fork 0
Work in Progress: Implement Go backend features #29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
b1702c1
760ea83
4ae4d54
a5102d9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,9 @@ | ||
| PORT=8080 | ||
| METRA_API_KEY= | ||
|
|
||
| # Tile upload (DigitalOcean Spaces) | ||
| TILES_S3_ENDPOINT=https://<region>.digitaloceanspaces.com | ||
| TILES_S3_BUCKET= | ||
| TILES_S3_ACCESS_KEY_ID= | ||
| TILES_S3_SECRET_ACCESS_KEY= | ||
| TILES_S3_REGION=us-east-1 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| # ── Build tippecanoe from source ───────────────────────────────────────────── | ||
| FROM debian:bookworm-slim AS tippecanoe-build | ||
| RUN apt-get update && apt-get install -y --no-install-recommends \ | ||
| git build-essential libsqlite3-dev zlib1g-dev ca-certificates \ | ||
| && rm -rf /var/lib/apt/lists/* | ||
| RUN git clone --depth 1 https://github.com/felt/tippecanoe.git /tippecanoe \ | ||
| && cd /tippecanoe && make -j$(nproc) | ||
|
|
||
| # ── Build Go binary ───────────────────────────────────────────────────────── | ||
| FROM golang:1.26-bookworm AS go-build | ||
| WORKDIR /src | ||
| COPY go.mod go.sum ./ | ||
| RUN go mod download | ||
| COPY . . | ||
| RUN CGO_ENABLED=1 go build -o /sync-static ./cmd/sync-static | ||
|
|
||
| # ── Runtime ────────────────────────────────────────────────────────────────── | ||
| FROM debian:bookworm-slim | ||
| RUN apt-get update && apt-get install -y --no-install-recommends \ | ||
| ca-certificates libsqlite3-0 zlib1g \ | ||
| && rm -rf /var/lib/apt/lists/* | ||
|
|
||
| COPY --from=tippecanoe-build /tippecanoe/tippecanoe /usr/local/bin/tippecanoe | ||
| COPY --from=go-build /sync-static /usr/local/bin/sync-static | ||
|
|
||
| WORKDIR /data | ||
| ENTRYPOINT ["sync-static", "--upload"] |
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "log" | ||
|
|
||
| _ "github.com/joho/godotenv/autoload" | ||
|
|
||
| "github.com/RailForLess/tracky/api/providers" | ||
| "github.com/RailForLess/tracky/api/providers/amtrak" | ||
| "github.com/RailForLess/tracky/api/providers/brightline" | ||
| "github.com/RailForLess/tracky/api/providers/metra" | ||
| "github.com/RailForLess/tracky/api/providers/metrotransit" | ||
| "github.com/RailForLess/tracky/api/providers/trirail" | ||
| ) | ||
|
|
||
| func buildRegistry() *providers.Registry { | ||
| registry := providers.NewRegistry() | ||
| registry.Register(amtrak.New()) | ||
| registry.Register(brightline.New()) | ||
| registry.Register(metra.New()) | ||
| registry.Register(metrotransit.New()) | ||
| registry.Register(trirail.New()) | ||
| return registry | ||
| } | ||
|
|
||
| // main is a stub — the DO Functions runtime provides its own entry point and | ||
| // invokes Main directly. This exists only to satisfy the Go compiler. | ||
| func main() {} | ||
|
|
||
| // Main is the Digital Ocean Functions entry point. | ||
| // Optional arg: "provider" (string) — if set, only that provider is synced. | ||
| func Main(args map[string]interface{}) map[string]interface{} { | ||
| registry := buildRegistry() | ||
| ctx := context.Background() | ||
|
|
||
| targets := registry.All() | ||
| if id, ok := args["provider"].(string); ok && id != "" { | ||
| p, ok := registry.Get(id) | ||
| if !ok { | ||
| return map[string]interface{}{ | ||
| "error": fmt.Sprintf("unknown provider: %s", id), | ||
| } | ||
| } | ||
| targets = []providers.Provider{p} | ||
| } | ||
|
|
||
| results := make(map[string]interface{}, len(targets)) | ||
| for _, p := range targets { | ||
| feed, err := p.FetchRealtime(ctx) | ||
| if err != nil { | ||
| log.Printf("sync-realtime: %s: %v", p.ID(), err) | ||
| results[p.ID()] = map[string]interface{}{"error": err.Error()} | ||
| continue | ||
| } | ||
| results[p.ID()] = map[string]interface{}{ | ||
| "positions": len(feed.Positions), | ||
| "stop_times": len(feed.StopTimes), | ||
| } | ||
| } | ||
|
|
||
| return map[string]interface{}{"body": results} | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "log" | ||
| "net/http" | ||
| "os" | ||
|
|
||
| _ "github.com/joho/godotenv/autoload" | ||
|
|
||
| "github.com/RailForLess/tracky/api/db" | ||
| "github.com/RailForLess/tracky/api/providers" | ||
| "github.com/RailForLess/tracky/api/providers/amtrak" | ||
| "github.com/RailForLess/tracky/api/providers/brightline" | ||
| "github.com/RailForLess/tracky/api/providers/cta" | ||
| "github.com/RailForLess/tracky/api/providers/metra" | ||
| "github.com/RailForLess/tracky/api/providers/metrotransit" | ||
| "github.com/RailForLess/tracky/api/providers/trirail" | ||
| "github.com/RailForLess/tracky/api/routes" | ||
| ) | ||
|
|
||
| func main() { | ||
| port := os.Getenv("PORT") | ||
| if port == "" { | ||
| port = "8080" | ||
| } | ||
|
|
||
| dbPath := os.Getenv("DATABASE_PATH") | ||
| if dbPath == "" { | ||
| dbPath = "tracky.db" | ||
| } | ||
|
|
||
| database, err := db.Open(dbPath) | ||
| if err != nil { | ||
| log.Fatal(err) | ||
| } | ||
| defer database.Close() | ||
|
|
||
| registry := providers.NewRegistry() | ||
| registry.Register(amtrak.New()) | ||
| registry.Register(brightline.New()) | ||
| registry.Register(cta.New()) | ||
| registry.Register(metra.New()) | ||
| registry.Register(metrotransit.New()) | ||
| registry.Register(trirail.New()) | ||
|
|
||
| mux := http.NewServeMux() | ||
| mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) { | ||
| w.WriteHeader(http.StatusOK) | ||
| w.Write([]byte("ok")) | ||
| }) | ||
|
|
||
| routes.Setup(mux, registry, database) | ||
|
|
||
| log.Printf("starting server on :%s", port) | ||
| if err := http.ListenAndServe(":"+port, mux); err != nil { | ||
| log.Fatal(err) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
| "flag" | ||
| "fmt" | ||
| "log" | ||
| "os" | ||
| "path/filepath" | ||
|
|
||
| _ "github.com/joho/godotenv/autoload" | ||
|
|
||
| "github.com/RailForLess/tracky/api/db" | ||
| "github.com/RailForLess/tracky/api/providers" | ||
| "github.com/RailForLess/tracky/api/providers/amtrak" | ||
| "github.com/RailForLess/tracky/api/providers/brightline" | ||
| "github.com/RailForLess/tracky/api/providers/cta" | ||
| "github.com/RailForLess/tracky/api/providers/metra" | ||
| "github.com/RailForLess/tracky/api/providers/metrotransit" | ||
| "github.com/RailForLess/tracky/api/providers/trirail" | ||
| "github.com/RailForLess/tracky/api/tiles" | ||
| ) | ||
|
|
||
| func main() { | ||
| dbPath := flag.String("db", "tracky.db", "SQLite database path") | ||
| skipDB := flag.Bool("skip-db", false, "skip writing to the database") | ||
| skipTiles := flag.Bool("skip-tiles", false, "skip tile generation") | ||
| tilesDir := flag.String("tiles-dir", ".", "output directory for PMTiles files") | ||
| upload := flag.Bool("upload", false, "upload artifacts to S3 after generation") | ||
| providerFilter := flag.String("provider", "", "restrict to a single provider ID") | ||
| flag.Parse() | ||
|
|
||
| ctx := context.Background() | ||
|
|
||
| // ── Provider registry ─────────────────────────────────────────────── | ||
|
|
||
| registry := providers.NewRegistry() | ||
| registry.Register(amtrak.New()) | ||
| registry.Register(brightline.New()) | ||
| registry.Register(cta.New()) | ||
| registry.Register(metra.New()) | ||
| registry.Register(metrotransit.New()) | ||
| registry.Register(trirail.New()) | ||
|
|
||
| var selected []providers.Provider | ||
| if *providerFilter != "" { | ||
| p, ok := registry.Get(*providerFilter) | ||
| if !ok { | ||
| log.Fatalf("unknown provider: %s", *providerFilter) | ||
| } | ||
| selected = []providers.Provider{p} | ||
| } else { | ||
| selected = registry.All() | ||
| } | ||
|
|
||
| // ── Open database ─────────────────────────────────────────────────── | ||
|
|
||
| var database *db.DB | ||
| if !*skipDB { | ||
| var err error | ||
| database, err = db.Open(*dbPath) | ||
| if err != nil { | ||
| log.Fatalf("open database: %v", err) | ||
| } | ||
| defer database.Close() | ||
| log.Printf("opened database: %s", *dbPath) | ||
| } | ||
|
|
||
| // ── Process each provider sequentially ────────────────────────────── | ||
|
|
||
| for _, p := range selected { | ||
| log.Printf("processing %s...", p.ID()) | ||
|
|
||
| feed, err := p.FetchStatic(ctx) | ||
| if err != nil { | ||
| log.Printf("warning: %s fetch failed: %v", p.ID(), err) | ||
| continue | ||
| } | ||
| log.Printf("fetched %s: %d routes, %d stops, %d trips, %d stop_times, %d shapes", | ||
| p.ID(), len(feed.Routes), len(feed.Stops), len(feed.Trips), | ||
| len(feed.StopTimes), len(feed.Shapes)) | ||
|
|
||
| // Write to database. | ||
| if database != nil { | ||
| counts, err := database.SaveStaticFeed(ctx, p.ID(), feed) | ||
| if err != nil { | ||
| log.Printf("warning: %s db write failed: %v", p.ID(), err) | ||
| } else { | ||
| log.Printf("saved %s to db: %+v", p.ID(), counts) | ||
| } | ||
| } | ||
|
|
||
| // Generate tiles. | ||
| if !*skipTiles { | ||
| buildTiles(ctx, p.ID(), feed, *tilesDir, *upload) | ||
| } | ||
| } | ||
|
|
||
| log.Printf("done") | ||
| } | ||
|
|
||
| // buildTiles generates a PMTiles file for a single provider and optionally uploads it. | ||
| func buildTiles(ctx context.Context, providerID string, feed *providers.StaticFeed, dir string, doUpload bool) { | ||
| if len(feed.Shapes) == 0 { | ||
| log.Printf("skipping tiles for %s: no shapes", providerID) | ||
| return | ||
| } | ||
|
|
||
| geojson, err := tiles.BuildGeoJSON(feed.Shapes, feed.Trips, feed.Routes) | ||
| if err != nil { | ||
| log.Fatalf("%s: building GeoJSON: %v", providerID, err) | ||
| } | ||
|
Comment on lines
+109
to
+112
|
||
|
|
||
| debugPath := filepath.Join(dir, providerID+".geojson") | ||
| if err := os.WriteFile(debugPath, geojson, 0644); err != nil { | ||
| log.Printf("warning: failed to write debug GeoJSON: %v", err) | ||
| } else { | ||
| log.Printf("wrote debug GeoJSON: %s (%.1f MB)", debugPath, float64(len(geojson))/(1024*1024)) | ||
| } | ||
|
|
||
| tmpFile, err := os.CreateTemp("", fmt.Sprintf("tracky-%s-*.geojson", providerID)) | ||
| if err != nil { | ||
| log.Fatalf("%s: creating temp file: %v", providerID, err) | ||
| } | ||
| tmpPath := tmpFile.Name() | ||
| defer os.Remove(tmpPath) | ||
|
|
||
| if _, err := tmpFile.Write(geojson); err != nil { | ||
| tmpFile.Close() | ||
| log.Fatalf("%s: writing GeoJSON: %v", providerID, err) | ||
| } | ||
| tmpFile.Close() | ||
|
|
||
| output := filepath.Join(dir, providerID+".pmtiles") | ||
| log.Printf("running tippecanoe → %s", output) | ||
| if err := tiles.GenerateTiles(ctx, tmpPath, output); err != nil { | ||
| log.Fatalf("%s: tippecanoe: %v", providerID, err) | ||
| } | ||
|
|
||
| stat, _ := os.Stat(output) | ||
| log.Printf("generated %s (%.1f MB)", output, float64(stat.Size())/(1024*1024)) | ||
|
|
||
| if doUpload { | ||
| objectKey := fmt.Sprintf("%s.pmtiles", providerID) | ||
| log.Printf("uploading %s to S3...", objectKey) | ||
| if err := tiles.Upload(ctx, output, objectKey); err != nil { | ||
| log.Fatalf("%s: upload: %v", providerID, err) | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Server uses
http.ListenAndServewithout setting timeouts. This leaves the process more vulnerable to slowloris-style attacks and can tie up resources under bad clients. Prefer configuring anhttp.Serverwith at leastReadHeaderTimeout(and usuallyReadTimeout/WriteTimeout/IdleTimeout) and callingserver.ListenAndServe().