big juicy update

This commit is contained in:
pavel 2026-05-12 23:31:09 +02:00
commit 1b6651c8e0
5 changed files with 101 additions and 11 deletions

View file

@ -1,4 +1,8 @@
PORT=8080
LISTEN_NETWORK=tcp
LISTEN_ADDRESS=:8080
# Only used when LISTEN_NETWORK=unix
UNIX_SOCKET_PERM=0660
DATABASE_URL=postgres://postgres:postgres@localhost:5432/skills_market?sslmode=disable
STORAGE_DIR=uploads
DEV_MODE=true

View file

@ -22,6 +22,22 @@ Go + Postgres marketplace where authenticated users can upload LLM skill files,
```
5. Open `http://localhost:8080`.
## Listener configuration
- `LISTEN_NETWORK`: `tcp` (default) or `unix`
- `LISTEN_ADDRESS`:
- for `tcp`: address like `:8080` or `127.0.0.1:8080`
- for `unix`: socket path like `/tmp/skills2.sock`
- `UNIX_SOCKET_PERM`: octal file mode for unix socket path (default `0660`)
Examples:
- TCP (default):
- `LISTEN_NETWORK=tcp`
- `LISTEN_ADDRESS=:8080`
- Unix socket:
- `LISTEN_NETWORK=unix`
- `LISTEN_ADDRESS=/tmp/skills2.sock`
- `UNIX_SOCKET_PERM=0660`
## Local dev mode (no auth setup)
Set `DEV_MODE=true` in `.env`.
In this mode, protected routes always authenticate as:

View file

@ -3,31 +3,53 @@ package main
import (
"fmt"
"os"
"strconv"
"strings"
"github.com/joho/godotenv"
)
type Config struct {
Port string
DatabaseURL string
StorageDir string
DevMode bool
Port string
ListenNetwork string
ListenAddress string
UnixSocketPerm os.FileMode
DatabaseURL string
StorageDir string
DevMode bool
}
func LoadConfig() (Config, error) {
_ = godotenv.Load()
cfg := Config{
Port: envOrDefault("PORT", "8080"),
DatabaseURL: os.Getenv("DATABASE_URL"),
StorageDir: envOrDefault("STORAGE_DIR", "uploads"),
DevMode: strings.EqualFold(envOrDefault("DEV_MODE", "false"), "true"),
Port: envOrDefault("PORT", "8080"),
ListenNetwork: strings.ToLower(envOrDefault("LISTEN_NETWORK", "tcp")),
ListenAddress: strings.TrimSpace(os.Getenv("LISTEN_ADDRESS")),
DatabaseURL: os.Getenv("DATABASE_URL"),
StorageDir: envOrDefault("STORAGE_DIR", "uploads"),
DevMode: strings.EqualFold(envOrDefault("DEV_MODE", "false"), "true"),
}
if cfg.ListenAddress == "" {
cfg.ListenAddress = ":" + cfg.Port
}
socketPerm, err := parseFileMode(envOrDefault("UNIX_SOCKET_PERM", "0660"))
if err != nil {
return Config{}, fmt.Errorf("invalid UNIX_SOCKET_PERM: %w", err)
}
cfg.UnixSocketPerm = socketPerm
if cfg.DatabaseURL == "" {
return Config{}, fmt.Errorf("DATABASE_URL is required")
}
switch cfg.ListenNetwork {
case "tcp", "unix":
default:
return Config{}, fmt.Errorf("LISTEN_NETWORK must be tcp or unix")
}
if cfg.ListenNetwork == "unix" && cfg.ListenAddress == "" {
return Config{}, fmt.Errorf("LISTEN_ADDRESS is required when LISTEN_NETWORK=unix")
}
return cfg, nil
}
@ -38,3 +60,12 @@ func envOrDefault(key, fallback string) string {
}
return value
}
func parseFileMode(value string) (os.FileMode, error) {
trimmed := strings.TrimSpace(value)
perm, err := strconv.ParseUint(trimmed, 8, 32)
if err != nil {
return 0, err
}
return os.FileMode(perm), nil
}

45
main.go
View file

@ -103,13 +103,52 @@ func main() {
mux.Handle("/api/purchase", app.auth.RequireAuth(http.HandlerFunc(app.handlePurchaseAPI)))
mux.Handle("/api/my-skills", app.auth.RequireAuth(http.HandlerFunc(app.handleMySkillsAPI)))
addr := ":" + cfg.Port
log.Printf("server listening on %s", addr)
if err := http.ListenAndServe(addr, loggingMiddleware(mux)); err != nil {
listener, cleanup, err := createListener(cfg)
if err != nil {
log.Fatalf("create listener: %v", err)
}
defer cleanup()
log.Printf("server listening network=%s address=%s", cfg.ListenNetwork, cfg.ListenAddress)
if err := http.Serve(listener, loggingMiddleware(mux)); err != nil {
log.Fatalf("server failed: %v", err)
}
}
func createListener(cfg Config) (net.Listener, func(), error) {
if cfg.ListenNetwork == "unix" {
if err := os.MkdirAll(filepath.Dir(cfg.ListenAddress), 0o755); err != nil {
return nil, nil, fmt.Errorf("create socket dir: %w", err)
}
if _, err := os.Stat(cfg.ListenAddress); err == nil {
if err := os.Remove(cfg.ListenAddress); err != nil {
return nil, nil, fmt.Errorf("remove stale socket: %w", err)
}
} else if !os.IsNotExist(err) {
return nil, nil, fmt.Errorf("check socket path: %w", err)
}
l, err := net.Listen("unix", cfg.ListenAddress)
if err != nil {
return nil, nil, err
}
if err := os.Chmod(cfg.ListenAddress, cfg.UnixSocketPerm); err != nil {
_ = l.Close()
return nil, nil, fmt.Errorf("chmod socket: %w", err)
}
cleanup := func() {
_ = l.Close()
_ = os.Remove(cfg.ListenAddress)
}
return l, cleanup, nil
}
l, err := net.Listen("tcp", cfg.ListenAddress)
if err != nil {
return nil, nil, err
}
return l, func() { _ = l.Close() }, nil
}
func runMigrations(db *sql.DB) error {
schema, err := os.ReadFile("db/schema.sql")
if err != nil {

BIN
skills2

Binary file not shown.