Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 31 additions & 11 deletions backend/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"log/slog"
"net/url"
"os"
"path/filepath"
"runtime"
"strings"
"time"

Expand Down Expand Up @@ -944,6 +946,30 @@ func Load() (*Config, error) {
return load(false)
}

// getDataDir returns the data directory used by setup, mirroring setup.GetDataDir().
// This keeps config search paths consistent with where setup writes files,
// preventing path drift on Windows where /app/data maps to the drive root.
func getDataDir() string {
if dir := os.Getenv("DATA_DIR"); dir != "" {
return dir
}
if runtime.GOOS == "linux" {
dockerDataDir := "/app/data"
if info, err := os.Stat(dockerDataDir); err == nil && info.IsDir() {
testFile := dockerDataDir + "/.write_test"
if f, err := os.Create(testFile); err == nil {
_ = f.Close()
_ = os.Remove(testFile)
return dockerDataDir
}
}
}
if exePath, err := os.Executable(); err == nil {
return filepath.Dir(exePath)
}
return "."
}

// LoadForBootstrap 读取启动阶段配置。
//
// 启动阶段允许 jwt.secret 先留空,后续由数据库初始化流程补齐并再次完整校验。
Expand All @@ -955,18 +981,12 @@ func load(allowMissingJWTSecret bool) (*Config, error) {
viper.SetConfigName("config")
viper.SetConfigType("yaml")

// Add config paths in priority order
// 1. DATA_DIR environment variable (highest priority)
if dataDir := os.Getenv("DATA_DIR"); dataDir != "" {
viper.AddConfigPath(dataDir)
}
// 2. Docker data directory
viper.AddConfigPath("/app/data")
// 3. Current directory
viper.AddConfigPath(".")
// 4. Config subdirectory
// Add config paths in the same priority order used by setup.GetDataDir(),
// so runtime and setup always agree on where the config file lives.
viper.AddConfigPath(getDataDir())
// Fallback: Config subdirectory (legacy layout)
viper.AddConfigPath("./config")
// 5. System config directory
// Fallback: System config directory (Linux package installations)
viper.AddConfigPath("/etc/sub2api")

// 环境变量支持
Expand Down
2 changes: 1 addition & 1 deletion backend/internal/pkg/sysutil/restart.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import (
// - Service configured with Restart=always in systemd unit file
func RestartService() error {
if runtime.GOOS != "linux" {
log.Println("Service restart via exit only works on Linux with systemd")
log.Println("Automatic restart is not supported on this platform. Please restart the application manually.")
return nil
}

Expand Down
2 changes: 1 addition & 1 deletion backend/internal/pkg/timezone/timezone.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ var (
// Example timezone values: "Asia/Shanghai", "America/New_York", "UTC"
func Init(tz string) error {
if tz == "" {
tz = "Asia/Shanghai" // Default timezone
tz = "UTC" // Default to UTC for cross-platform compatibility
}

loc, err := time.LoadLocation(tz)
Expand Down
9 changes: 7 additions & 2 deletions backend/internal/setup/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"net/http"
"net/mail"
"regexp"
"runtime"
"strings"
"sync"
"time"
Expand Down Expand Up @@ -348,8 +349,12 @@ func install(c *gin.Context) {
sysutil.RestartServiceAsync()
}()

restartMsg := "Installation completed successfully. Service will restart automatically."
if runtime.GOOS != "linux" {
restartMsg = "Installation completed successfully. Please restart the application manually."
}
response.Success(c, gin.H{
"message": "Installation completed successfully. Service will restart automatically.",
"restart": true,
"message": restartMsg,
"restart": runtime.GOOS == "linux",
})
}
37 changes: 25 additions & 12 deletions backend/internal/setup/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"encoding/hex"
"fmt"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -38,26 +40,37 @@ func setupDefaultAdminConcurrency() int {
}

// GetDataDir returns the data directory for storing config and lock files.
// Priority: DATA_DIR env > /app/data (if exists and writable) > current directory
// Priority: DATA_DIR env > /app/data (Linux/Docker only, if exists and writable) > executable directory > current directory
func GetDataDir() string {
// Check DATA_DIR environment variable first
if dir := os.Getenv("DATA_DIR"); dir != "" {
return dir
}

// Check if /app/data exists and is writable (Docker environment)
dockerDataDir := "/app/data"
if info, err := os.Stat(dockerDataDir); err == nil && info.IsDir() {
// Try to check if writable by creating a temp file
testFile := dockerDataDir + "/.write_test"
if f, err := os.Create(testFile); err == nil {
_ = f.Close()
_ = os.Remove(testFile)
return dockerDataDir
// Check if /app/data exists and is writable — Docker/Linux environments only.
// Skipped on Windows because the path /app/data resolves to the current drive root
// (e.g., C:\app\data), causing a mismatch between setup write path and runtime read path.
if runtime.GOOS == "linux" {
dockerDataDir := "/app/data"
if info, err := os.Stat(dockerDataDir); err == nil && info.IsDir() {
// Try to check if writable by creating a temp file
testFile := dockerDataDir + "/.write_test"
if f, err := os.Create(testFile); err == nil {
_ = f.Close()
_ = os.Remove(testFile)
return dockerDataDir
}
}
}

// Default to current directory
// Use the executable's directory so setup and runtime always agree on the path,
// even when the current working directory differs (common on Windows when launching
// via double-click or as a service).
if exePath, err := os.Executable(); err == nil {
return filepath.Dir(exePath)
}

// Final fallback: current directory
return "."
}

Expand Down Expand Up @@ -432,7 +445,7 @@ func writeConfigFile(cfg *SetupConfig) error {
// Ensure timezone has a default value
tz := cfg.Timezone
if tz == "" {
tz = "Asia/Shanghai"
tz = "UTC"
}

// Prepare config for YAML (exclude sensitive data and admin config)
Expand Down
Loading