src/server/internal/config/dotenv.go
2,955 bytes · 118 lines · capsule://quake0day/[email protected]
raw on github
package config
import (
"bufio"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
)
var envMu sync.Mutex
// LoadDotenv reads a .env file and sets each KEY=VALUE pair into the process
// environment via os.Setenv. Lines starting with # and blank lines are skipped.
// Returns nil if the file does not exist.
func LoadDotenv(path string) error {
f, err := os.Open(path)
if os.IsNotExist(err) {
return nil
}
if err != nil {
return err
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
key, value, ok := strings.Cut(line, "=")
if !ok {
continue
}
key = strings.TrimSpace(key)
value = strings.TrimSpace(value)
// Strip surrounding quotes if present
if len(value) >= 2 &&
((value[0] == '"' && value[len(value)-1] == '"') ||
(value[0] == '\'' && value[len(value)-1] == '\'')) {
value = value[1 : len(value)-1]
}
os.Setenv(key, value)
}
return scanner.Err()
}
// SaveDotenv merges updates into an existing .env file. Existing keys are
// updated in place; new keys are appended. The write is atomic (temp file +
// rename) and protected by a package-level mutex.
func SaveDotenv(path string, updates map[string]string) error {
envMu.Lock()
defer envMu.Unlock()
// Read existing lines (may not exist yet).
var lines []string
if data, err := os.ReadFile(path); err == nil {
lines = strings.Split(string(data), "\n")
}
// Track which keys we've already updated in existing lines.
updated := make(map[string]bool, len(updates))
for i, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
continue
}
key, _, ok := strings.Cut(trimmed, "=")
if !ok {
continue
}
key = strings.TrimSpace(key)
if newVal, exists := updates[key]; exists {
lines[i] = fmt.Sprintf("%s=%s", key, newVal)
updated[key] = true
}
}
// Append keys that weren't already present.
for key, val := range updates {
if !updated[key] {
lines = append(lines, fmt.Sprintf("%s=%s", key, val))
}
}
// Remove trailing empty lines, then ensure a final newline.
for len(lines) > 0 && strings.TrimSpace(lines[len(lines)-1]) == "" {
lines = lines[:len(lines)-1]
}
content := strings.Join(lines, "\n") + "\n"
// Atomic write: temp file in same dir, then rename.
dir := filepath.Dir(path)
tmp, err := os.CreateTemp(dir, ".env.tmp.*")
if err != nil {
return fmt.Errorf("create temp file: %w", err)
}
tmpName := tmp.Name()
if _, err := tmp.WriteString(content); err != nil {
tmp.Close()
os.Remove(tmpName)
return fmt.Errorf("write temp file: %w", err)
}
if err := tmp.Close(); err != nil {
os.Remove(tmpName)
return fmt.Errorf("close temp file: %w", err)
}
if err := os.Rename(tmpName, path); err != nil {
os.Remove(tmpName)
return fmt.Errorf("rename temp file: %w", err)
}
return nil
}