- Introduced .editorconfig for consistent coding styles across the project. - Added .golangci.yml for Go linting configuration. - Updated AGENTS.md to clarify project structure and components. - Enhanced CONTRIBUTING.md with Makefile usage for common tasks. - Updated Dockerfiles to use Go 1.24 and improved build instructions. - Refined README.md and deployment documentation for clarity. - Added testing documentation in testing.md for backend and frontend tests. - Introduced Makefile for streamlined development commands and tasks.
459 lines
11 KiB
Go
459 lines
11 KiB
Go
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. Checks context before starting.
|
|
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. Checks context before starting.
|
|
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))
|
|
}
|
|
|
|
// 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)
|
|
}
|