Compare commits

..

No commits in common. "db776d34ddafb8388ead270a2214b28879baecef" and "9c78413bb944e01771bb6c466cc48fa8166d9e5c" have entirely different histories.

7 changed files with 42 additions and 179 deletions

View File

@ -1,22 +1,3 @@
# 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,34 +5,19 @@ 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. Configure environment variables (copy `.env.example` to `.env` and customize):
2. Set environment variables:
```bash
cp .env.example .env
# Edit .env with your settings
# .env.example
DATABASE_URL="postgres://user:password@localhost/termcloud"
PORT="8080"
```
### 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,7 +8,6 @@ import (
"log"
"os"
"git.keircn.com/keiran/termcloud/internal/config"
"git.keircn.com/keiran/termcloud/internal/db"
)
@ -30,15 +29,13 @@ func main() {
}
ctx := context.Background()
cfg := config.Load()
pool, err := db.NewPool(ctx, cfg)
pool, err := db.NewPool(ctx)
if err != nil {
log.Fatalf("Failed to create database pool: %v", err)
}
defer db.ClosePool(pool)
bucketService := db.NewBucketService(pool, cfg)
bucketService := db.NewBucketService(pool)
switch os.Args[1] {
case "create-user":
@ -49,7 +46,7 @@ func main() {
username := os.Args[2]
email := os.Args[3]
storageLimit := cfg.DefaultStorageLimit
storageLimit := int64(1) * 1024 * 1024 * 1024
if len(os.Args) >= 5 {
var limitGB int64

View File

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

View File

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

View File

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