From a6836455dcf901eda0670d0bf11950ad4151df3c Mon Sep 17 00:00:00 2001 From: CJACK Date: Sat, 4 Apr 2026 22:30:57 +0800 Subject: [PATCH] feat: add support for stripping inline comments in .env files and make Docker host port configurable via DS2API_HOST_PORT --- .env.example | 3 ++ README.MD | 4 +-- README.en.md | 4 +-- docker-compose.dev.yml | 3 +- docker-compose.yml | 12 +++---- docs/DEPLOY.en.md | 4 ++- docs/DEPLOY.md | 4 ++- internal/config/dotenv.go | 58 +++++++++++++++++++++++++++++++++- internal/config/dotenv_test.go | 52 ++++++++++++++++++++++++++++++ 9 files changed, 130 insertions(+), 14 deletions(-) diff --git a/.env.example b/.env.example index a19bde5..840a81e 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,8 @@ # DS2API runtime +# Runtime listen port inside the app/container PORT=5001 +# Docker Compose host port (compose only; container still listens on PORT) +DS2API_HOST_PORT=6011 LOG_LEVEL=INFO # Admin authentication diff --git a/README.MD b/README.MD index 7696bfc..0e1a0dc 100644 --- a/README.MD +++ b/README.MD @@ -191,7 +191,7 @@ go run ./cmd/ds2api cp .env.example .env cp config.example.json config.json -# 2. 编辑 .env(至少设置 DS2API_ADMIN_KEY) +# 2. 编辑 .env(至少设置 DS2API_ADMIN_KEY;如需修改宿主机端口,可额外设置 DS2API_HOST_PORT) # DS2API_ADMIN_KEY=请替换为强密码 # 3. 启动 @@ -201,7 +201,7 @@ docker-compose up -d docker-compose logs -f ``` -默认 `docker-compose.yml` 会把宿主机 `6011` 映射到容器内的 `5001`。如果你希望直接对外暴露 `5001`,请调整 `ports` 配置。 +默认 `docker-compose.yml` 会把宿主机 `6011` 映射到容器内的 `5001`。如果你希望直接对外暴露 `5001`,请设置 `DS2API_HOST_PORT=5001`(或者手动调整 `ports` 配置)。 更新镜像:`docker-compose up -d --build` diff --git a/README.en.md b/README.en.md index 65096ef..7ccc826 100644 --- a/README.en.md +++ b/README.en.md @@ -191,7 +191,7 @@ Default URL: `http://localhost:5001` cp .env.example .env cp config.example.json config.json -# 2. Edit .env (at least set DS2API_ADMIN_KEY) +# 2. Edit .env (at least set DS2API_ADMIN_KEY; optionally set DS2API_HOST_PORT to change the host port) # DS2API_ADMIN_KEY=replace-with-a-strong-secret # 3. Start @@ -201,7 +201,7 @@ docker-compose up -d docker-compose logs -f ``` -The default `docker-compose.yml` maps host port `6011` to container port `5001`. If you want `5001` exposed directly, adjust the `ports` mapping. +The default `docker-compose.yml` maps host port `6011` to container port `5001`. If you want `5001` exposed directly, set `DS2API_HOST_PORT=5001` (or adjust the `ports` mapping). Rebuild after updates: `docker-compose up -d --build` diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 39cb6b5..c147349 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -16,7 +16,8 @@ services: container_name: ds2api-dev command: ["go", "run", "./cmd/ds2api"] ports: - - "${PORT:-5001}:${PORT:-5001}" + # Host port is configurable via DS2API_HOST_PORT; container port stays fixed at 5001. + - "${DS2API_HOST_PORT:-6011}:5001" env_file: - .env environment: diff --git a/docker-compose.yml b/docker-compose.yml index ff95e07..9398fdc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,11 +6,11 @@ services: env_file: - .env ports: - - "${DS2API_HOST_PORT:-6011}:${PORT:-5001}" + # Host port is configurable via DS2API_HOST_PORT; container port stays fixed at 5001. + - "${DS2API_HOST_PORT:-6011}:5001" volumes: - ./config.json:/app/config.json # 配置文件 - - ./.env:/app/.env # 环境变量 - environment: - - TZ=Asia/Shanghai - - LOG_LEVEL=INFO - - DS2API_ADMIN_KEY=${DS2API_ADMIN_KEY:-ds2api} + environment: + - TZ=Asia/Shanghai + - LOG_LEVEL=INFO + - DS2API_ADMIN_KEY=${DS2API_ADMIN_KEY:-ds2api} diff --git a/docs/DEPLOY.en.md b/docs/DEPLOY.en.md index 48ffba1..d41f17a 100644 --- a/docs/DEPLOY.en.md +++ b/docs/DEPLOY.en.md @@ -117,6 +117,8 @@ cp config.example.json config.json # Edit .env and set at least: # DS2API_ADMIN_KEY=your-admin-key +# Optionally set the host port: +# DS2API_HOST_PORT=6011 # Start docker-compose up -d @@ -125,7 +127,7 @@ docker-compose up -d docker-compose logs -f ``` -The default `docker-compose.yml` maps host port `6011` to container port `5001`. If you want `5001` exposed directly, adjust the `ports` mapping. +The default `docker-compose.yml` maps host port `6011` to container port `5001`. If you want `5001` exposed directly, set `DS2API_HOST_PORT=5001` (or adjust the `ports` mapping). ### 2.2 Update diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index c4e57bd..c4cbeab 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -117,6 +117,8 @@ cp config.example.json config.json # 编辑 .env(请改成你的强密码),至少设置: # DS2API_ADMIN_KEY=your-admin-key +# 如需修改宿主机端口,可额外设置: +# DS2API_HOST_PORT=6011 # 启动 docker-compose up -d @@ -125,7 +127,7 @@ docker-compose up -d docker-compose logs -f ``` -默认 `docker-compose.yml` 会把宿主机 `6011` 映射到容器内的 `5001`。如果你希望直接对外暴露 `5001`,请调整 `ports` 配置。 +默认 `docker-compose.yml` 会把宿主机 `6011` 映射到容器内的 `5001`。如果你希望直接对外暴露 `5001`,请设置 `DS2API_HOST_PORT=5001`(或者手动调整 `ports` 配置)。 ### 2.2 更新 diff --git a/internal/config/dotenv.go b/internal/config/dotenv.go index ef44ad5..c33d2b0 100644 --- a/internal/config/dotenv.go +++ b/internal/config/dotenv.go @@ -47,7 +47,7 @@ func loadDotEnvFromPath(path string) error { if _, exists := os.LookupEnv(key); exists { continue } - if err := os.Setenv(key, normalizeDotEnvValue(strings.TrimSpace(value))); err != nil { + if err := os.Setenv(key, normalizeDotEnvValue(trimDotEnvValue(strings.TrimSpace(value)))); err != nil { return fmt.Errorf("%s:%d set env %q: %w", path, i+1, key, err) } } @@ -55,6 +55,62 @@ func loadDotEnvFromPath(path string) error { return nil } +// Preserve quoted values, but drop Compose-style inline comments from unquoted values. +func trimDotEnvValue(raw string) string { + if raw == "" { + return raw + } + + switch raw[0] { + case '"': + if trimmed, ok := trimQuotedDotEnvValue(raw, '"'); ok { + return trimmed + } + case '\'': + if trimmed, ok := trimQuotedDotEnvValue(raw, '\''); ok { + return trimmed + } + default: + if idx := inlineDotEnvCommentStart(raw); idx >= 0 { + return strings.TrimSpace(raw[:idx]) + } + } + + return raw +} + +func trimQuotedDotEnvValue(raw string, quote byte) (string, bool) { + escaped := false + for i := 1; i < len(raw); i++ { + ch := raw[i] + if quote == '"' && escaped { + escaped = false + continue + } + if quote == '"' && ch == '\\' { + escaped = true + continue + } + if ch == quote { + return strings.TrimSpace(raw[:i+1]), true + } + } + return raw, false +} + +func inlineDotEnvCommentStart(raw string) int { + for i := 1; i < len(raw); i++ { + if raw[i] == '#' && isDotEnvCommentSpacer(raw[i-1]) { + return i + } + } + return -1 +} + +func isDotEnvCommentSpacer(b byte) bool { + return b == ' ' || b == '\t' +} + func normalizeDotEnvValue(raw string) string { if len(raw) < 2 { return raw diff --git a/internal/config/dotenv_test.go b/internal/config/dotenv_test.go index 11a271d..2e8a3a8 100644 --- a/internal/config/dotenv_test.go +++ b/internal/config/dotenv_test.go @@ -3,6 +3,7 @@ package config import ( "os" "path/filepath" + "strings" "testing" ) @@ -67,6 +68,57 @@ func TestLoadDotEnvIgnoresMissingFile(t *testing.T) { } } +func TestLoadDotEnvStripsInlineCommentsFromUnquotedValues(t *testing.T) { + dir := t.TempDir() + oldWD, err := os.Getwd() + if err != nil { + t.Fatalf("getwd: %v", err) + } + if err := os.Chdir(dir); err != nil { + t.Fatalf("chdir temp dir: %v", err) + } + t.Cleanup(func() { + _ = os.Chdir(oldWD) + }) + + const plainKey = "DS2API_TEST_DOTENV_PLAIN" + const hashKey = "DS2API_TEST_DOTENV_HASH" + const quotedKey = "DS2API_TEST_DOTENV_QUOTED_COMMENT" + const exportKey = "DS2API_TEST_DOTENV_EXPORT" + + unsetEnv(t, plainKey) + unsetEnv(t, hashKey) + unsetEnv(t, quotedKey) + unsetEnv(t, exportKey) + + content := strings.Join([]string{ + plainKey + "=5001 # local", + hashKey + "=5001#local", + quotedKey + `="5001 # local" # keep the inner hash`, + "export " + exportKey + "=enabled # exported", + }, "\n") + "\n" + if err := os.WriteFile(filepath.Join(dir, ".env"), []byte(content), 0o644); err != nil { + t.Fatalf("write .env: %v", err) + } + + if err := LoadDotEnv(); err != nil { + t.Fatalf("LoadDotEnv() error: %v", err) + } + + if got := os.Getenv(plainKey); got != "5001" { + t.Fatalf("expected inline comment to be stripped, got %q", got) + } + if got := os.Getenv(hashKey); got != "5001#local" { + t.Fatalf("expected hash without preceding whitespace to remain, got %q", got) + } + if got := os.Getenv(quotedKey); got != "5001 # local" { + t.Fatalf("expected quoted value to preserve hash text, got %q", got) + } + if got := os.Getenv(exportKey); got != "enabled" { + t.Fatalf("expected export syntax to load, got %q", got) + } +} + func unsetEnv(t *testing.T, key string) { t.Helper() old, had := os.LookupEnv(key)