Compare commits

...

5 Commits

Author SHA1 Message Date
Keiran
db776d34dd update database to use new config package 2025-08-07 18:35:16 +01:00
Keiran
3145c2c0aa use config package in cli 2025-08-07 18:35:00 +01:00
Keiran
9a1e6122fa add config package 2025-08-07 18:34:41 +01:00
Keiran
1af1c4720e update env example with proposed config changes 2025-08-07 18:34:23 +01:00
Keiran
1aadd25640 update readme with proposed config changes 2025-08-07 18:34:16 +01:00
7 changed files with 179 additions and 42 deletions

View File

@ -1,3 +1,22 @@
# Do not use quotes, make will scream at you
# Database Configuration
DATABASE_URL=postgres://user:password@localhost/termcloud
# Server Configuration
PORT=8080
STORAGE_DIR=storage
MAX_FILE_SIZE_MB=100
RATE_LIMIT=20.0
CORS_ORIGINS=*
GZIP_LEVEL=5
# Database Pool Configuration
DB_MAX_CONNECTIONS=100
DB_MIN_CONNECTIONS=10
DB_CONN_LIFETIME=1h
DB_CONN_IDLE_TIME=1m
DB_HEALTH_CHECK_PERIOD=5s
# User Defaults
DEFAULT_STORAGE_LIMIT_GB=1

View File

@ -5,19 +5,34 @@ A simple file storage service with user buckets and usage limits.
## Setup
1. Set up PostgreSQL database and run the schema:
```bash
psql -d termcloud -f internal/db/schema.sql
```
2. Set environment variables:
2. Configure environment variables (copy `.env.example` to `.env` and customize):
```bash
# .env.example
DATABASE_URL="postgres://user:password@localhost/termcloud"
PORT="8080"
cp .env.example .env
# Edit .env with your settings
```
### Configuration Options
| Variable | Default | Description |
|----------|---------|-------------|
| `DATABASE_URL` | - | PostgreSQL connection string |
| `PORT` | 8080 | Server port |
| `STORAGE_DIR` | storage | Directory for file storage |
| `MAX_FILE_SIZE_MB` | 100 | Maximum file size in MB |
| `RATE_LIMIT` | 20.0 | Requests per second limit |
| `CORS_ORIGINS` | * | Allowed CORS origins |
| `GZIP_LEVEL` | 5 | Gzip compression level (1-9) |
| `DB_MAX_CONNECTIONS` | 100 | Maximum database connections |
| `DB_MIN_CONNECTIONS` | 10 | Minimum database connections |
| `DB_CONN_LIFETIME` | 1h | Connection lifetime |
| `DB_CONN_IDLE_TIME` | 1m | Connection idle timeout |
| `DB_HEALTH_CHECK_PERIOD` | 5s | Health check interval |
| `DEFAULT_STORAGE_LIMIT_GB` | 1 | Default user storage limit |
3. Build and run:
```bash

View File

@ -8,6 +8,7 @@ import (
"log"
"os"
"git.keircn.com/keiran/termcloud/internal/config"
"git.keircn.com/keiran/termcloud/internal/db"
)
@ -29,13 +30,15 @@ func main() {
}
ctx := context.Background()
pool, err := db.NewPool(ctx)
cfg := config.Load()
pool, err := db.NewPool(ctx, cfg)
if err != nil {
log.Fatalf("Failed to create database pool: %v", err)
}
defer db.ClosePool(pool)
bucketService := db.NewBucketService(pool)
bucketService := db.NewBucketService(pool, cfg)
switch os.Args[1] {
case "create-user":
@ -46,7 +49,7 @@ func main() {
username := os.Args[2]
email := os.Args[3]
storageLimit := int64(1) * 1024 * 1024 * 1024
storageLimit := cfg.DefaultStorageLimit
if len(os.Args) >= 5 {
var limitGB int64

View File

@ -2,9 +2,10 @@ package main
import (
"context"
"fmt"
"log"
"os"
"git.keircn.com/keiran/termcloud/internal/config"
"git.keircn.com/keiran/termcloud/internal/db"
"git.keircn.com/keiran/termcloud/internal/handlers"
"github.com/labstack/echo/v4"
@ -14,30 +15,31 @@ import (
func main() {
ctx := context.Background()
cfg := config.Load()
pool, err := db.NewPool(ctx)
pool, err := db.NewPool(ctx, cfg)
if err != nil {
log.Fatalf("Failed to create database pool: %v", err)
}
defer db.ClosePool(pool)
bucketService := db.NewBucketService(pool)
bucketService := db.NewBucketService(pool, cfg)
h := handlers.NewHandlers(bucketService)
e := echo.New()
e.Use(middleware.BodyLimit("100M"))
e.Use(middleware.BodyLimit(fmt.Sprintf("%dM", cfg.MaxFileSize/1024/1024)))
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"*"},
AllowOrigins: cfg.CORSOrigins,
AllowMethods: []string{echo.GET, echo.POST, echo.PUT, echo.DELETE},
AllowHeaders: []string{"*"},
}))
e.Use(middleware.GzipWithConfig(middleware.GzipConfig{
Level: 5,
Level: cfg.GzipLevel,
}))
e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(rate.Limit(20))))
e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(rate.Limit(cfg.RateLimit))))
e.Static("/storage", "storage")
e.Static("/storage", cfg.StorageDir)
e.GET("/", h.RootHandler)
@ -53,10 +55,5 @@ func main() {
api.GET("/buckets/:bucket/objects/*", h.GetObjectHandler)
api.DELETE("/buckets/:bucket/objects/*", h.DeleteObjectHandler)
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
e.Logger.Fatal(e.Start(":" + port))
e.Logger.Fatal(e.Start(":" + cfg.Port))
}

100
internal/config/config.go Normal file
View File

@ -0,0 +1,100 @@
package config
import (
"os"
"strconv"
"time"
)
type Config struct {
DatabaseURL string
Port string
StorageDir string
MaxFileSize int64
RateLimit float64
CORSOrigins []string
GzipLevel int
MaxConnections int32
MinConnections int32
ConnLifetime time.Duration
ConnIdleTime time.Duration
HealthCheckPeriod time.Duration
DefaultStorageLimit int64
}
func Load() *Config {
return &Config{
DatabaseURL: getEnv("DATABASE_URL", ""),
Port: getEnv("PORT", "8080"),
StorageDir: getEnv("STORAGE_DIR", "storage"),
MaxFileSize: getEnvInt64("MAX_FILE_SIZE_MB", 100) * 1024 * 1024,
RateLimit: getEnvFloat64("RATE_LIMIT", 20.0),
CORSOrigins: getEnvSlice("CORS_ORIGINS", []string{"*"}),
GzipLevel: getEnvInt("GZIP_LEVEL", 5),
MaxConnections: getEnvInt32("DB_MAX_CONNECTIONS", 100),
MinConnections: getEnvInt32("DB_MIN_CONNECTIONS", 10),
ConnLifetime: getEnvDuration("DB_CONN_LIFETIME", time.Hour),
ConnIdleTime: getEnvDuration("DB_CONN_IDLE_TIME", time.Minute),
HealthCheckPeriod: getEnvDuration("DB_HEALTH_CHECK_PERIOD", 5*time.Second),
DefaultStorageLimit: getEnvInt64("DEFAULT_STORAGE_LIMIT_GB", 1) * 1024 * 1024 * 1024,
}
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
func getEnvInt(key string, defaultValue int) int {
if value := os.Getenv(key); value != "" {
if intValue, err := strconv.Atoi(value); err == nil {
return intValue
}
}
return defaultValue
}
func getEnvInt32(key string, defaultValue int32) int32 {
if value := os.Getenv(key); value != "" {
if intValue, err := strconv.ParseInt(value, 10, 32); err == nil {
return int32(intValue)
}
}
return defaultValue
}
func getEnvInt64(key string, defaultValue int64) int64 {
if value := os.Getenv(key); value != "" {
if intValue, err := strconv.ParseInt(value, 10, 64); err == nil {
return intValue
}
}
return defaultValue
}
func getEnvFloat64(key string, defaultValue float64) float64 {
if value := os.Getenv(key); value != "" {
if floatValue, err := strconv.ParseFloat(value, 64); err == nil {
return floatValue
}
}
return defaultValue
}
func getEnvDuration(key string, defaultValue time.Duration) time.Duration {
if value := os.Getenv(key); value != "" {
if duration, err := time.ParseDuration(value); err == nil {
return duration
}
}
return defaultValue
}
func getEnvSlice(key string, defaultValue []string) []string {
if value := os.Getenv(key); value != "" {
return []string{value}
}
return defaultValue
}

View File

@ -9,6 +9,7 @@ import (
"path/filepath"
"time"
"git.keircn.com/keiran/termcloud/internal/config"
"github.com/jackc/pgx/v5/pgxpool"
)
@ -42,11 +43,15 @@ type Object struct {
}
type BucketService struct {
pool *pgxpool.Pool
pool *pgxpool.Pool
storageDir string
}
func NewBucketService(pool *pgxpool.Pool) *BucketService {
return &BucketService{pool: pool}
func NewBucketService(pool *pgxpool.Pool, cfg *config.Config) *BucketService {
return &BucketService{
pool: pool,
storageDir: cfg.StorageDir,
}
}
func (s *BucketService) CreateUser(ctx context.Context, username, email, apiKey string, storageLimit int64) (*User, error) {
@ -136,12 +141,12 @@ func (s *BucketService) DeleteBucket(ctx context.Context, bucketName string, own
}
for _, obj := range objects {
if err := os.Remove(filepath.Join("storage", bucketName, obj.Key)); err != nil {
if err := os.Remove(filepath.Join(s.storageDir, bucketName, obj.Key)); err != nil {
continue
}
}
os.RemoveAll(filepath.Join("storage", bucketName))
os.RemoveAll(filepath.Join(s.storageDir, bucketName))
_, err = s.pool.Exec(ctx, "DELETE FROM buckets WHERE name = $1 AND owner_id = $2", bucketName, ownerID)
if err != nil {
@ -165,7 +170,7 @@ func (s *BucketService) UploadObject(ctx context.Context, bucketID int64, key st
return nil, fmt.Errorf("storage limit exceeded")
}
bucketDir := filepath.Join("storage", bucket.Name)
bucketDir := filepath.Join(s.storageDir, bucket.Name)
if err := os.MkdirAll(bucketDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create bucket directory: %w", err)
}
@ -249,7 +254,7 @@ func (s *BucketService) DeleteObject(ctx context.Context, bucketID int64, key st
return err
}
filePath := filepath.Join("storage", bucket.Name, key)
filePath := filepath.Join(s.storageDir, bucket.Name, key)
if err := os.Remove(filePath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove file: %w", err)
}
@ -272,7 +277,7 @@ func (s *BucketService) GetObjectFile(ctx context.Context, bucketName, key strin
return nil, err
}
filePath := filepath.Join("storage", bucketName, key)
filePath := filepath.Join(s.storageDir, bucketName, key)
file, err := os.Open(filePath)
if err != nil {
return nil, fmt.Errorf("failed to open file: %w", err)

View File

@ -4,28 +4,26 @@ import (
"context"
"fmt"
"log"
"os"
"time"
"git.keircn.com/keiran/termcloud/internal/config"
"github.com/jackc/pgx/v5/pgxpool"
)
func NewPool(ctx context.Context) (*pgxpool.Pool, error) {
connStr := os.Getenv("DATABASE_URL")
if connStr == "" {
func NewPool(ctx context.Context, cfg *config.Config) (*pgxpool.Pool, error) {
if cfg.DatabaseURL == "" {
return nil, fmt.Errorf("DATABASE_URL environment variable is not set")
}
config, err := pgxpool.ParseConfig(connStr)
config, err := pgxpool.ParseConfig(cfg.DatabaseURL)
if err != nil {
return nil, fmt.Errorf("unable to parse DATABASE_URL: %w", err)
}
config.MaxConns = 100
config.MinConns = 10
config.MaxConnLifetime = time.Hour
config.MaxConnIdleTime = time.Minute
config.HealthCheckPeriod = 5 * time.Second
config.MaxConns = cfg.MaxConnections
config.MinConns = cfg.MinConnections
config.MaxConnLifetime = cfg.ConnLifetime
config.MaxConnIdleTime = cfg.ConnIdleTime
config.HealthCheckPeriod = cfg.HealthCheckPeriod
pool, err := pgxpool.NewWithConfig(ctx, config)
if err != nil {