Compare commits
5 Commits
9c78413bb9
...
db776d34dd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db776d34dd | ||
|
|
3145c2c0aa | ||
|
|
9a1e6122fa | ||
|
|
1af1c4720e | ||
|
|
1aadd25640 |
19
.env.example
19
.env.example
@ -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
|
||||
|
||||
27
README.md
27
README.md
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
100
internal/config/config.go
Normal 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
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user