Skip to content

Commit

Permalink
feat(cache): add session persistence for memory cache (#20)
Browse files Browse the repository at this point in the history
* fix(cache): add session persistence for memory cache

* fix(cli): make -db flag consistent with -config flag

* feat(config): look in multiple places
  • Loading branch information
s0up4200 authored Nov 11, 2024
1 parent 3cc6290 commit 189e44b
Show file tree
Hide file tree
Showing 13 changed files with 382 additions and 67 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,17 @@ type = "sqlite"
path = "./data/dashbrr.db"
```

By default, the database file will be created in the same directory as your configuration file. For example:

- If your config is at `/home/user/.config/dashbrr/config.toml`, the database will be at `/home/user/.config/dashbrr/data/dashbrr.db`
- If your config is at `/etc/dashbrr/config.toml`, the database will be at `/etc/dashbrr/data/dashbrr.db`

You can override this behavior by using the `-db` flag to specify a different database location:

```bash
dashbrr -config=/etc/dashbrr/config.toml -db=/var/lib/dashbrr/dashbrr.db
```

### Environment Variables

For a complete list of available environment variables and their configurations, see our [Environment Variables Documentation](docs/env_vars.md).
Expand Down
42 changes: 39 additions & 3 deletions cmd/dashbrr/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"net/http"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"
Expand Down Expand Up @@ -56,11 +57,46 @@ func startServer() {
Str("build_date", date).
Msg("Starting dashbrr")

configPath := flag.String("config", "config.toml", "path to config file")
dbPath := flag.String("db", "./data/dashbrr.db", "path to database file")
// Check environment variable first, then fall back to flag
defaultConfigPath := "config.toml"
if envPath := os.Getenv(config.EnvConfigPath); envPath != "" {
defaultConfigPath = envPath
} else {
// Check user config directory
userConfigDir, err := os.UserConfigDir()
if err != nil {
log.Error().Err(err).Msg("failed to get user config directory")
}

base := []string{filepath.Join(userConfigDir, "dashbrr"), "/config"}
configs := []string{"config.toml", "config.yaml", "config.yml"}

for _, b := range base {
for _, c := range configs {
p := filepath.Join(b, c)
if _, err := os.Stat(p); err == nil {
defaultConfigPath = p
break
}
}
if defaultConfigPath != "config.toml" {
break
}
}
}
configPath := flag.String("config", defaultConfigPath, "path to config file")

var dbPath string
flag.StringVar(&dbPath, "db", "", "path to database file")
listenAddr := flag.String("listen", ":8080", "address to listen on")
flag.Parse()

// If dbPath wasn't set via flag, use config directory
if dbPath == "" {
configDir := filepath.Dir(*configPath)
dbPath = filepath.Join(configDir, "data", "dashbrr.db")
}

var cfg *config.Config
var err error

Expand All @@ -77,7 +113,7 @@ func startServer() {
ListenAddr: *listenAddr,
},
Database: config.DatabaseConfig{
Path: *dbPath,
Path: dbPath,
},
}
log.Warn().Err(err).Msg("Failed to load configuration file, using defaults")
Expand Down
35 changes: 35 additions & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,41 @@

This document outlines all available CLI commands in Dashbrr.

## Startup Flags

When starting Dashbrr, you can use the following flags to control its configuration:

```bash
# Start Dashbrr with default settings
dashbrr

# Specify a custom config file location
dashbrr -config=/path/to/config.toml

# Specify a custom database location
dashbrr -db=/path/to/database.db

# Specify a custom listen address
dashbrr -listen=:8081
```

By default:

- The config file is loaded from `./config.toml`
- The database file is created in the same directory as the config file at `<config_dir>/data/dashbrr.db`
- The server listens on port 8080

For example:

```bash
# Using config in /etc/dashbrr
dashbrr -config=/etc/dashbrr/config.toml
# Database will be created at /etc/dashbrr/data/dashbrr.db

# Override default database location
dashbrr -config=/etc/dashbrr/config.toml -db=/var/lib/dashbrr/dashbrr.db
```

## Core Commands

### User Management
Expand Down
17 changes: 17 additions & 0 deletions docs/env_vars.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@
- Format: `<host>:<port>`
- Default: `0.0.0.0:8080`

## Configuration Path

- `DASHBRR__CONFIG_PATH`
- Purpose: Path to the configuration file
- Default: `config.toml`
- Priority: Environment variable > User config directory > Command line flag > Default value
- Note: The application will check the following locations for the configuration file:
1. The path specified by the `DASHBRR__CONFIG_PATH` environment variable.
2. The user config directory (e.g., `~/.config/dashbrr`).
3. The current working directory for `config.toml`, `config.yaml`, or `config.yml`.
4. The `-config` command line flag can also be used to specify a different path.

## Cache Configuration

- `CACHE_TYPE`
Expand Down Expand Up @@ -38,6 +50,11 @@
- `DASHBRR__DB_PATH`
- Purpose: Path to SQLite database file
- Example: `/data/dashbrr.db`
- Note: If not set, the database will be created in a 'data' subdirectory of the config file's location. This can be overridden by:
1. Using the `-db` flag when starting dashbrr
2. Setting this environment variable
3. Specifying the path in the config file
- Priority: Command line flag > Environment variable > Config file > Default location

### PostgreSQL Configuration

Expand Down
2 changes: 1 addition & 1 deletion internal/api/middleware/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func (m *AuthMiddleware) RequireAuth() gin.HandlerFunc {
sessionKey = fmt.Sprintf("session:%s", sessionToken)
err = m.cache.Get(c, sessionKey, &sessionData)
if err != nil {
log.Error().Err(err).Msg("session not found")
log.Debug().Err(err).Msg("session not found")
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired session"})
c.Abort()
return
Expand Down
28 changes: 24 additions & 4 deletions internal/api/routes/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package routes

import (
"os"
"path/filepath"
"time"

"github.com/gin-gonic/gin"
Expand All @@ -26,15 +27,34 @@ func SetupRoutes(r *gin.Engine, db *database.DB, health *services.HealthService)
r.Use(middleware.SetupCORS())
r.Use(middleware.Secure(nil)) // Add secure middleware with default config

// Initialize cache
store, err := cache.InitCache()
// Initialize cache with database directory for session storage
cacheConfig := cache.Config{
DataDir: filepath.Dir(os.Getenv("DASHBRR__DB_PATH")), // Use same directory as database
}

// Configure Redis if enabled
if os.Getenv("REDIS_HOST") != "" {
host := os.Getenv("REDIS_HOST")
port := os.Getenv("REDIS_PORT")
if port == "" {
port = "6379"
}
cacheConfig.RedisAddr = host + ":" + port
}

store, err := cache.InitCache(cacheConfig)
if err != nil {
// This should never happen as InitCache always returns a valid store
log.Debug().Err(err).Msg("Using memory cache")
store = cache.NewMemoryStore()
store = cache.NewMemoryStore(cacheConfig.DataDir)
}

log.Debug().Str("type", os.Getenv("CACHE_TYPE")).Msg("Cache initialized")
// Determine cache type based on environment and Redis configuration
cacheType := "memory"
if os.Getenv("CACHE_TYPE") == "redis" && os.Getenv("REDIS_HOST") != "" {
cacheType = "redis"
}
log.Debug().Str("type", cacheType).Msg("Cache initialized")

// Create rate limiters with different configurations
apiRateLimiter := middleware.NewRateLimiter(store, time.Minute, 60, "api:") // 60 requests per minute for API
Expand Down
4 changes: 4 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import (
"github.com/pelletier/go-toml/v2"
)

const (
EnvConfigPath = "DASHBRR__CONFIG_PATH"
)

// Config represents the main configuration structure
type Config struct {
Server ServerConfig `toml:"server"`
Expand Down
12 changes: 11 additions & 1 deletion internal/database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
type DB struct {
*sql.DB
driver string
path string
}

// Config holds database configuration
Expand Down Expand Up @@ -134,7 +135,11 @@ func InitDBWithConfig(config *Config) (*DB, error) {
Str("driver", config.Driver).
Msg("Successfully connected to database")

db := &DB{database, config.Driver}
db := &DB{
DB: database,
driver: config.Driver,
path: config.Path,
}

// Initialize schema
if err := db.initSchema(); err != nil {
Expand All @@ -144,6 +149,11 @@ func InitDBWithConfig(config *Config) (*DB, error) {
return db, nil
}

// Path returns the database file path (for SQLite)
func (db *DB) Path() string {
return db.path
}

// initSchema creates the necessary database tables
func (db *DB) initSchema() error {
var autoIncrement string
Expand Down
31 changes: 15 additions & 16 deletions internal/services/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ package cache
import (
"context"
"encoding/json"
"os"
"errors"
"strconv"
"strings"
"sync"
Expand All @@ -17,7 +17,8 @@ import (
)

var (
ErrKeyNotFound = redis.Nil
ErrKeyNotFound = errors.New("cache: key not found")
ErrClosed = errors.New("cache: store is closed")
)

const (
Expand Down Expand Up @@ -62,11 +63,6 @@ type localCacheItem struct {

// NewCache creates a new Redis cache instance with optimized configuration
func NewCache(addr string) (Store, error) {
// Check if memory cache is explicitly requested
if os.Getenv("CACHE_TYPE") == "memory" {
return NewMemoryStore(), nil
}

ctx, cancel := context.WithCancel(context.Background())

// Development-optimized Redis configuration
Expand Down Expand Up @@ -134,7 +130,7 @@ func (s *RedisStore) Get(ctx context.Context, key string, value interface{}) err
s.mu.RLock()
if s.closed {
s.mu.RUnlock()
return redis.ErrClosed
return ErrClosed
}
s.mu.RUnlock()

Expand Down Expand Up @@ -186,6 +182,9 @@ func (s *RedisStore) Get(ctx context.Context, key string, value interface{}) err
}
}

if lastErr == redis.Nil {
return ErrKeyNotFound
}
return lastErr
}

Expand All @@ -194,7 +193,7 @@ func (s *RedisStore) Set(ctx context.Context, key string, value interface{}, exp
s.mu.RLock()
if s.closed {
s.mu.RUnlock()
return redis.ErrClosed
return ErrClosed
}
s.mu.RUnlock()

Expand Down Expand Up @@ -294,7 +293,7 @@ func (s *RedisStore) Delete(ctx context.Context, key string) error {
s.mu.RLock()
if s.closed {
s.mu.RUnlock()
return redis.ErrClosed
return ErrClosed
}
s.mu.RUnlock()

Expand Down Expand Up @@ -332,7 +331,7 @@ func (s *RedisStore) Increment(ctx context.Context, key string, timestamp int64)
s.mu.RLock()
if s.closed {
s.mu.RUnlock()
return redis.ErrClosed
return ErrClosed
}
s.mu.RUnlock()

Expand Down Expand Up @@ -367,7 +366,7 @@ func (s *RedisStore) CleanAndCount(ctx context.Context, key string, windowStart
s.mu.RLock()
if s.closed {
s.mu.RUnlock()
return redis.ErrClosed
return ErrClosed
}
s.mu.RUnlock()

Expand Down Expand Up @@ -399,7 +398,7 @@ func (s *RedisStore) GetCount(ctx context.Context, key string) (int64, error) {
s.mu.RLock()
if s.closed {
s.mu.RUnlock()
return 0, redis.ErrClosed
return 0, ErrClosed
}
s.mu.RUnlock()

Expand Down Expand Up @@ -430,7 +429,7 @@ func (s *RedisStore) Expire(ctx context.Context, key string, expiration time.Dur
s.mu.RLock()
if s.closed {
s.mu.RUnlock()
return redis.ErrClosed
return ErrClosed
}
s.mu.RUnlock()

Expand Down Expand Up @@ -466,7 +465,7 @@ func (s *RedisStore) Close() error {
s.mu.Lock()
if s.closed {
s.mu.Unlock()
return redis.ErrClosed
return ErrClosed
}
s.closed = true
s.mu.Unlock()
Expand All @@ -484,6 +483,6 @@ func (s *RedisStore) Close() error {
s.local.items = make(map[string]*localCacheItem)
}()

// Close Redis client
// Close client
return s.client.Close()
}
Loading

0 comments on commit 189e44b

Please sign in to comment.