package store import ( "context" "strconv" "go.etcd.io/bbolt" ) // Store provides access to bbolt database (bucket helpers, CRUD). type Store struct { db *bbolt.DB } // New creates a Store for the given database. func New(db *bbolt.DB) *Store { return &Store{db: db} } // View runs fn in a read-only transaction. It checks context before starting. // Long-running callbacks (e.g. large ForEach or ForEachTile) should check ctx.Done() // periodically and return ctx.Err() to abort early when the context is cancelled. func (s *Store) View(ctx context.Context, fn func(tx *bbolt.Tx) error) error { select { case <-ctx.Done(): return ctx.Err() default: return s.db.View(fn) } } // Update runs fn in a read-write transaction. It checks context before starting. // Long-running callbacks should check ctx.Done() periodically and return ctx.Err() to abort. func (s *Store) Update(ctx context.Context, fn func(tx *bbolt.Tx) error) error { select { case <-ctx.Done(): return ctx.Err() default: return s.db.Update(fn) } } // --- Users --- // GetUser returns the raw JSON for a user, or nil if not found. func (s *Store) GetUser(tx *bbolt.Tx, username string) []byte { b := tx.Bucket(BucketUsers) if b == nil { return nil } return b.Get([]byte(username)) } // PutUser stores a user (creates the bucket if needed). func (s *Store) PutUser(tx *bbolt.Tx, username string, raw []byte) error { b, err := tx.CreateBucketIfNotExists(BucketUsers) if err != nil { return err } return b.Put([]byte(username), raw) } // DeleteUser removes a user by username. func (s *Store) DeleteUser(tx *bbolt.Tx, username string) error { b := tx.Bucket(BucketUsers) if b == nil { return nil } return b.Delete([]byte(username)) } // ForEachUser iterates over all users. func (s *Store) ForEachUser(tx *bbolt.Tx, fn func(k, v []byte) error) error { b := tx.Bucket(BucketUsers) if b == nil { return nil } return b.ForEach(fn) } // UserCount returns the number of users in the database. func (s *Store) UserCount(tx *bbolt.Tx) int { b := tx.Bucket(BucketUsers) if b == nil { return 0 } return b.Stats().KeyN } // --- Sessions --- // GetSession returns the raw JSON for a session, or nil if not found. func (s *Store) GetSession(tx *bbolt.Tx, id string) []byte { b := tx.Bucket(BucketSessions) if b == nil { return nil } return b.Get([]byte(id)) } // PutSession stores a session. func (s *Store) PutSession(tx *bbolt.Tx, id string, raw []byte) error { b, err := tx.CreateBucketIfNotExists(BucketSessions) if err != nil { return err } return b.Put([]byte(id), raw) } // DeleteSession removes a session by ID. func (s *Store) DeleteSession(tx *bbolt.Tx, id string) error { b := tx.Bucket(BucketSessions) if b == nil { return nil } return b.Delete([]byte(id)) } // --- Tokens --- // GetTokenUser returns the username associated with a token, or nil. func (s *Store) GetTokenUser(tx *bbolt.Tx, token string) []byte { b := tx.Bucket(BucketTokens) if b == nil { return nil } return b.Get([]byte(token)) } // PutToken associates a token with a username. func (s *Store) PutToken(tx *bbolt.Tx, token, username string) error { b, err := tx.CreateBucketIfNotExists(BucketTokens) if err != nil { return err } return b.Put([]byte(token), []byte(username)) } // DeleteToken removes a token. func (s *Store) DeleteToken(tx *bbolt.Tx, token string) error { b := tx.Bucket(BucketTokens) if b == nil { return nil } return b.Delete([]byte(token)) } // --- Config --- // GetConfig returns a config value by key, or nil. func (s *Store) GetConfig(tx *bbolt.Tx, key string) []byte { b := tx.Bucket(BucketConfig) if b == nil { return nil } return b.Get([]byte(key)) } // PutConfig stores a config key-value pair. func (s *Store) PutConfig(tx *bbolt.Tx, key string, value []byte) error { b, err := tx.CreateBucketIfNotExists(BucketConfig) if err != nil { return err } return b.Put([]byte(key), value) } // DeleteConfig removes a config key. func (s *Store) DeleteConfig(tx *bbolt.Tx, key string) error { b := tx.Bucket(BucketConfig) if b == nil { return nil } return b.Delete([]byte(key)) } // --- Maps --- // GetMap returns the raw JSON for a map, or nil if not found. func (s *Store) GetMap(tx *bbolt.Tx, id int) []byte { b := tx.Bucket(BucketMaps) if b == nil { return nil } return b.Get([]byte(strconv.Itoa(id))) } // PutMap stores a map entry. func (s *Store) PutMap(tx *bbolt.Tx, id int, raw []byte) error { b, err := tx.CreateBucketIfNotExists(BucketMaps) if err != nil { return err } return b.Put([]byte(strconv.Itoa(id)), raw) } // DeleteMap removes a map by ID. func (s *Store) DeleteMap(tx *bbolt.Tx, id int) error { b := tx.Bucket(BucketMaps) if b == nil { return nil } return b.Delete([]byte(strconv.Itoa(id))) } // MapsNextSequence returns the next auto-increment ID for maps. func (s *Store) MapsNextSequence(tx *bbolt.Tx) (uint64, error) { b, err := tx.CreateBucketIfNotExists(BucketMaps) if err != nil { return 0, err } return b.NextSequence() } // MapsSetSequence sets the maps bucket sequence counter. func (s *Store) MapsSetSequence(tx *bbolt.Tx, v uint64) error { b := tx.Bucket(BucketMaps) if b == nil { return nil } return b.SetSequence(v) } // ForEachMap iterates over all maps. func (s *Store) ForEachMap(tx *bbolt.Tx, fn func(k, v []byte) error) error { b := tx.Bucket(BucketMaps) if b == nil { return nil } return b.ForEach(fn) } // --- Grids --- // GetGrid returns the raw JSON for a grid, or nil if not found. func (s *Store) GetGrid(tx *bbolt.Tx, id string) []byte { b := tx.Bucket(BucketGrids) if b == nil { return nil } return b.Get([]byte(id)) } // PutGrid stores a grid entry. func (s *Store) PutGrid(tx *bbolt.Tx, id string, raw []byte) error { b, err := tx.CreateBucketIfNotExists(BucketGrids) if err != nil { return err } return b.Put([]byte(id), raw) } // DeleteGrid removes a grid by ID. func (s *Store) DeleteGrid(tx *bbolt.Tx, id string) error { b := tx.Bucket(BucketGrids) if b == nil { return nil } return b.Delete([]byte(id)) } // ForEachGrid iterates over all grids. func (s *Store) ForEachGrid(tx *bbolt.Tx, fn func(k, v []byte) error) error { b := tx.Bucket(BucketGrids) if b == nil { return nil } return b.ForEach(fn) } // --- Tiles (nested: mapid -> zoom -> coord) --- // GetTile returns the raw JSON for a tile at the given map/zoom/coord, or nil. func (s *Store) GetTile(tx *bbolt.Tx, mapID, zoom int, coordKey string) []byte { tiles := tx.Bucket(BucketTiles) if tiles == nil { return nil } mapB := tiles.Bucket([]byte(strconv.Itoa(mapID))) if mapB == nil { return nil } zoomB := mapB.Bucket([]byte(strconv.Itoa(zoom))) if zoomB == nil { return nil } return zoomB.Get([]byte(coordKey)) } // GetTiles returns raw JSON for multiple tiles in the same map/zoom in one transaction. // Keys that are not found are omitted from the result. Coord keys are in the form "x_y". func (s *Store) GetTiles(tx *bbolt.Tx, mapID, zoom int, coordKeys []string) map[string][]byte { out := make(map[string][]byte, len(coordKeys)) tiles := tx.Bucket(BucketTiles) if tiles == nil { return out } mapB := tiles.Bucket([]byte(strconv.Itoa(mapID))) if mapB == nil { return out } zoomB := mapB.Bucket([]byte(strconv.Itoa(zoom))) if zoomB == nil { return out } for _, k := range coordKeys { if v := zoomB.Get([]byte(k)); v != nil { out[k] = v } } return out } // PutTile stores a tile entry (creates nested buckets as needed). func (s *Store) PutTile(tx *bbolt.Tx, mapID, zoom int, coordKey string, raw []byte) error { tiles, err := tx.CreateBucketIfNotExists(BucketTiles) if err != nil { return err } mapB, err := tiles.CreateBucketIfNotExists([]byte(strconv.Itoa(mapID))) if err != nil { return err } zoomB, err := mapB.CreateBucketIfNotExists([]byte(strconv.Itoa(zoom))) if err != nil { return err } return zoomB.Put([]byte(coordKey), raw) } // DeleteTilesBucket removes the entire tiles bucket. func (s *Store) DeleteTilesBucket(tx *bbolt.Tx) error { if tx.Bucket(BucketTiles) == nil { return nil } return tx.DeleteBucket(BucketTiles) } // ForEachTile iterates over all tiles across all maps and zoom levels. func (s *Store) ForEachTile(tx *bbolt.Tx, fn func(mapK, zoomK, coordK, v []byte) error) error { tiles := tx.Bucket(BucketTiles) if tiles == nil { return nil } return tiles.ForEach(func(mapK, _ []byte) error { mapB := tiles.Bucket(mapK) if mapB == nil { return nil } return mapB.ForEach(func(zoomK, _ []byte) error { zoomB := mapB.Bucket(zoomK) if zoomB == nil { return nil } return zoomB.ForEach(func(coordK, v []byte) error { return fn(mapK, zoomK, coordK, v) }) }) }) } // GetTilesMapBucket returns the tiles sub-bucket for a specific map, or nil. func (s *Store) GetTilesMapBucket(tx *bbolt.Tx, mapID int) *bbolt.Bucket { tiles := tx.Bucket(BucketTiles) if tiles == nil { return nil } return tiles.Bucket([]byte(strconv.Itoa(mapID))) } // CreateTilesMapBucket returns or creates the tiles sub-bucket for a specific map. func (s *Store) CreateTilesMapBucket(tx *bbolt.Tx, mapID int) (*bbolt.Bucket, error) { tiles, err := tx.CreateBucketIfNotExists(BucketTiles) if err != nil { return nil, err } return tiles.CreateBucketIfNotExists([]byte(strconv.Itoa(mapID))) } // DeleteTilesMapBucket removes the tiles sub-bucket for a specific map. func (s *Store) DeleteTilesMapBucket(tx *bbolt.Tx, mapID int) error { tiles := tx.Bucket(BucketTiles) if tiles == nil { return nil } key := []byte(strconv.Itoa(mapID)) if tiles.Bucket(key) == nil { return nil } return tiles.DeleteBucket(key) } // --- Markers (nested: grid bucket, id bucket) --- // GetMarkersGridBucket returns the markers grid sub-bucket, or nil. func (s *Store) GetMarkersGridBucket(tx *bbolt.Tx) *bbolt.Bucket { mb := tx.Bucket(BucketMarkers) if mb == nil { return nil } return mb.Bucket(BucketMarkersGrid) } // GetMarkersIDBucket returns the markers ID sub-bucket, or nil. func (s *Store) GetMarkersIDBucket(tx *bbolt.Tx) *bbolt.Bucket { mb := tx.Bucket(BucketMarkers) if mb == nil { return nil } return mb.Bucket(BucketMarkersID) } // CreateMarkersBuckets returns or creates both markers sub-buckets (grid and id). func (s *Store) CreateMarkersBuckets(tx *bbolt.Tx) (*bbolt.Bucket, *bbolt.Bucket, error) { mb, err := tx.CreateBucketIfNotExists(BucketMarkers) if err != nil { return nil, nil, err } grid, err := mb.CreateBucketIfNotExists(BucketMarkersGrid) if err != nil { return nil, nil, err } idB, err := mb.CreateBucketIfNotExists(BucketMarkersID) if err != nil { return nil, nil, err } return grid, idB, nil } // MarkersNextSequence returns the next auto-increment ID for markers. func (s *Store) MarkersNextSequence(tx *bbolt.Tx) (uint64, error) { mb := tx.Bucket(BucketMarkers) if mb == nil { return 0, nil } idB := mb.Bucket(BucketMarkersID) if idB == nil { return 0, nil } return idB.NextSequence() } // --- OAuth states --- // GetOAuthState returns the raw JSON for an OAuth state, or nil. func (s *Store) GetOAuthState(tx *bbolt.Tx, state string) []byte { b := tx.Bucket(BucketOAuthStates) if b == nil { return nil } return b.Get([]byte(state)) } // PutOAuthState stores an OAuth state entry. func (s *Store) PutOAuthState(tx *bbolt.Tx, state string, raw []byte) error { b, err := tx.CreateBucketIfNotExists(BucketOAuthStates) if err != nil { return err } return b.Put([]byte(state), raw) } // DeleteOAuthState removes an OAuth state entry. func (s *Store) DeleteOAuthState(tx *bbolt.Tx, state string) error { b := tx.Bucket(BucketOAuthStates) if b == nil { return nil } return b.Delete([]byte(state)) } // --- Bucket existence (for wipe) --- // BucketExists returns true if a top-level bucket with the given name exists. func (s *Store) BucketExists(tx *bbolt.Tx, name []byte) bool { return tx.Bucket(name) != nil } // DeleteBucket removes a top-level bucket (no-op if it doesn't exist). func (s *Store) DeleteBucket(tx *bbolt.Tx, name []byte) error { if tx.Bucket(name) == nil { return nil } return tx.DeleteBucket(name) }