clone

an offline map explorer where each world, from a game map to a city's open data, ships as one .atlas file

archive/golden-reference
atlas / cmd / cartograph / library.go
library.go 3.3K
1package main23import (4 "os"5 "path/filepath"6 "sort"7 "sync"8 "time"910 "github.com/FelineStateMachine/atlas/internal/measure"11)1213// library reads the bundles directory whole on every ask and answers with14// games grouped from what it found. Measuring a bundle costs a pass over its15// table of contents and payload JSON, so measurements are cached by the16// file's size and modification time -- the same test the reader's registry17// uses to tell an untouched file from one rewritten under its name -- and a18// page load after the first is a stat per file.19type library struct {20 dir string2122 mu sync.Mutex23 cache map[string]cachedBuild24}2526type cachedBuild struct {27 size int6428 modTime time.Time29 build *build30}3132// game is every build of one slug, newest first: the same order the reader's33// registry folds by, so the first build of every game is the one the reader34// serves.35type volume struct {36 Slug string37 Title string38 Builds []*build39}4041// Serving is the build the reader would pick.42func (g *volume) Serving() *build { return g.Builds[0] }4344// Build finds one build by file name, which is how diff URLs name them.45func (g *volume) Build(file string) *build {46 for _, b := range g.Builds {47 if b.File == file {48 return b49 }50 }51 return nil52}5354// games measures every bundle in the directory and groups the builds by55// game. A bundle that fails to measure is reported in skipped rather than56// failing the scan: one bad download should not take the dashboard down.57func (l *library) volumes() (volumes []*volume, skipped []string, err error) {58 paths, err := filepath.Glob(filepath.Join(l.dir, "*.atlas"))59 if err != nil {60 return nil, nil, err61 }62 sort.Strings(paths)6364 byVolume := make(map[string]*volume)65 for _, path := range paths {66 measured, err := l.measure(path)67 if err != nil {68 skipped = append(skipped, filepath.Base(path)+": "+err.Error())69 continue70 }71 held, ok := byVolume[measured.VolumeSlug]72 if !ok {73 held = &volume{Slug: measured.VolumeSlug}74 byVolume[measured.VolumeSlug] = held75 volumes = append(volumes, held)76 }77 held.Builds = append(held.Builds, measured)78 }7980 for _, held := range byVolume {81 // The registry's own order, spelled once in internal/measure.82 sort.Slice(held.Builds, func(a, b int) bool {83 return measure.Newer(held.Builds[a], held.Builds[b])84 })85 held.Title = held.Serving().VolumeTitle86 }87 sort.Slice(volumes, func(a, b int) bool { return volumes[a].Title < volumes[b].Title })88 return volumes, skipped, nil89}9091// volumeBySlug scans and returns one game, or nil if the slug has no builds.92func (l *library) volumeBySlug(slug string) (*volume, error) {93 volumes, _, err := l.volumes()94 if err != nil {95 return nil, err96 }97 for _, held := range volumes {98 if held.Slug == slug {99 return held, nil100 }101 }102 return nil, nil103}104105func (l *library) measure(path string) (*build, error) {106 info, err := os.Stat(path)107 if err != nil {108 return nil, err109 }110 l.mu.Lock()111 held, ok := l.cache[path]112 l.mu.Unlock()113 if ok && held.size == info.Size() && held.modTime.Equal(info.ModTime()) {114 return held.build, nil115 }116 measured, err := measureBundle(path)117 if err != nil {118 return nil, err119 }120 l.mu.Lock()121 if l.cache == nil {122 l.cache = make(map[string]cachedBuild)123 }124 l.cache[path] = cachedBuild{size: info.Size(), modTime: info.ModTime(), build: measured}125 l.mu.Unlock()126 return measured, nil127}