Fix the abnormal path retrieval issue in some systems.

This commit is contained in:
2026-04-09 01:00:31 +08:00
parent 76f0d815aa
commit 792f547dc3
7 changed files with 453 additions and 336 deletions

View File

@@ -2,6 +2,7 @@ package cmd
import ( import (
"NetworkAuth/config" "NetworkAuth/config"
"NetworkAuth/utils"
"NetworkAuth/utils/logger" "NetworkAuth/utils/logger"
"io" "io"
"os" "os"
@@ -67,7 +68,7 @@ func setupLogrusForNonHTTP() {
if cfgFile != "" { if cfgFile != "" {
config.Init(cfgFile) config.Init(cfgFile)
} else { } else {
config.Init("./config.json") config.Init("config.json")
} }
// 根据配置文件进一步配置logrus // 根据配置文件进一步配置logrus
@@ -105,7 +106,11 @@ func setupLogrusFromConfig() {
logFile := viper.GetString("log.file") logFile := viper.GetString("log.file")
if logFile != "" { if logFile != "" {
// 确保日志目录存在 // 确保日志目录存在
logDir := filepath.Dir(logFile) path := logFile
if !filepath.IsAbs(path) {
path = filepath.Join(utils.GetRootDir(), path)
}
logDir := filepath.Dir(path)
if err := os.MkdirAll(logDir, 0755); err != nil { if err := os.MkdirAll(logDir, 0755); err != nil {
logrus.WithError(err).Error("创建日志目录失败") logrus.WithError(err).Error("创建日志目录失败")
return return
@@ -113,7 +118,7 @@ func setupLogrusFromConfig() {
// 配置lumberjack日志轮转 // 配置lumberjack日志轮转
lumberjackLogger := &lumberjack.Logger{ lumberjackLogger := &lumberjack.Logger{
Filename: logFile, Filename: path,
MaxSize: viper.GetInt("log.max_size"), // MB MaxSize: viper.GetInt("log.max_size"), // MB
MaxBackups: viper.GetInt("log.max_backups"), // 保留的旧日志文件数量 MaxBackups: viper.GetInt("log.max_backups"), // 保留的旧日志文件数量
MaxAge: viper.GetInt("log.max_age"), // 天数 MaxAge: viper.GetInt("log.max_age"), // 天数

View File

@@ -6,6 +6,8 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"NetworkAuth/utils"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
@@ -107,7 +109,7 @@ func GetDefaultAppConfig() *AppConfig {
MaxOpenConns: 100, MaxOpenConns: 100,
}, },
SQLite: SQLiteConfig{ SQLite: SQLiteConfig{
Path: "./database.db", Path: "database.db",
}, },
}, },
Redis: RedisConfig{ Redis: RedisConfig{
@@ -118,7 +120,7 @@ func GetDefaultAppConfig() *AppConfig {
}, },
Log: LogConfig{ Log: LogConfig{
Level: "info", Level: "info",
File: "./logs/app.log", File: "logs/app.log",
MaxSize: 100, MaxSize: 100,
MaxBackups: 5, MaxBackups: 5,
MaxAge: 30, MaxAge: 30,
@@ -128,6 +130,9 @@ func GetDefaultAppConfig() *AppConfig {
// Init 初始化配置文件 // Init 初始化配置文件
func Init(cfgFilePath string) { func Init(cfgFilePath string) {
if !filepath.IsAbs(cfgFilePath) {
cfgFilePath = filepath.Join(utils.GetRootDir(), cfgFilePath)
}
currentConfigFilePath = cfgFilePath currentConfigFilePath = cfgFilePath
viper.SetConfigFile(cfgFilePath) viper.SetConfigFile(cfgFilePath)
viper.SetConfigType("json") viper.SetConfigType("json")
@@ -204,7 +209,10 @@ func SaveConfig(appConfig *AppConfig) error {
return err return err
} }
if currentConfigFilePath == "" { if currentConfigFilePath == "" {
currentConfigFilePath = "./config.json" currentConfigFilePath = "config.json"
}
if !filepath.IsAbs(currentConfigFilePath) {
currentConfigFilePath = filepath.Join(utils.GetRootDir(), currentConfigFilePath)
} }
if err := os.MkdirAll(filepath.Dir(currentConfigFilePath), 0755); err != nil { if err := os.MkdirAll(filepath.Dir(currentConfigFilePath), 0755); err != nil {
return err return err

View File

@@ -9,6 +9,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"NetworkAuth/utils"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
@@ -125,8 +127,13 @@ func validateSQLiteConfig(config *SQLiteConfig) error {
return errors.New("SQLite数据库路径不能为空") return errors.New("SQLite数据库路径不能为空")
} }
path := config.Path
if !filepath.IsAbs(path) {
path = filepath.Join(utils.GetRootDir(), path)
}
// 检查目录是否存在,不存在则创建 // 检查目录是否存在,不存在则创建
dir := filepath.Dir(config.Path) dir := filepath.Dir(path)
if _, err := os.Stat(dir); os.IsNotExist(err) { if _, err := os.Stat(dir); os.IsNotExist(err) {
if err := os.MkdirAll(dir, 0755); err != nil { if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("创建SQLite数据库目录失败: %w", err) return fmt.Errorf("创建SQLite数据库目录失败: %w", err)
@@ -160,7 +167,11 @@ func validateLogConfig(config *LogConfig) error {
// 检查日志文件目录(仅当日志文件路径不为空时) // 检查日志文件目录(仅当日志文件路径不为空时)
if config.File != "" { if config.File != "" {
dir := filepath.Dir(config.File) path := config.File
if !filepath.IsAbs(path) {
path = filepath.Join(utils.GetRootDir(), path)
}
dir := filepath.Dir(path)
if _, err := os.Stat(dir); os.IsNotExist(err) { if _, err := os.Stat(dir); os.IsNotExist(err) {
if err := os.MkdirAll(dir, 0755); err != nil { if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("创建日志目录失败: %w", err) return fmt.Errorf("创建日志目录失败: %w", err)

View File

@@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"log" "log"
"os" "os"
"path/filepath"
"sync" "sync"
"time" "time"
@@ -103,7 +104,10 @@ func performInitFromViper() error {
case "sqlite": case "sqlite":
dbPath := cfg.Database.SQLite.Path dbPath := cfg.Database.SQLite.Path
if dbPath == "" { if dbPath == "" {
dbPath = "./database.db" dbPath = "database.db"
}
if !filepath.IsAbs(dbPath) {
dbPath = filepath.Join(utils.GetRootDir(), dbPath)
} }
if _, err := os.Stat(dbPath); os.IsNotExist(err) { if _, err := os.Stat(dbPath); os.IsNotExist(err) {
logrus.Info("SQLite 数据库文件不存在,系统尚未安装,跳过数据库连接") logrus.Info("SQLite 数据库文件不存在,系统尚未安装,跳过数据库连接")
@@ -209,7 +213,10 @@ func buildGormLogger(level string) gLogger.Interface {
func initSQLite(sqliteConfig *appconfig.SQLiteConfig, logLevel string) error { func initSQLite(sqliteConfig *appconfig.SQLiteConfig, logLevel string) error {
path := sqliteConfig.Path path := sqliteConfig.Path
if path == "" { if path == "" {
path = "./database.db" path = "database.db"
}
if !filepath.IsAbs(path) {
path = filepath.Join(utils.GetRootDir(), path)
} }
dsn := fmt.Sprintf("file:%s?cache=shared&_busy_timeout=5000&_fk=1", path) dsn := fmt.Sprintf("file:%s?cache=shared&_busy_timeout=5000&_fk=1", path)
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{Logger: buildGormLogger(logLevel)}) db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{Logger: buildGormLogger(logLevel)})

View File

@@ -2,12 +2,14 @@ package server
import ( import (
"NetworkAuth/public" "NetworkAuth/public"
"NetworkAuth/utils"
"io" "io"
"io/fs" "io/fs"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"net/url" "net/url"
"os" "os"
"path/filepath"
"strings" "strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -48,6 +50,9 @@ func registerFrontendRoutes(r *gin.Engine) {
return // 反向代理接管了所有非 API 路由,直接返回 return // 反向代理接管了所有非 API 路由,直接返回
} else { } else {
// 使用本地外部目录 // 使用本地外部目录
if !filepath.IsAbs(distConfig) {
distConfig = filepath.Join(utils.GetRootDir(), distConfig)
}
fileServer = http.FileServer(http.Dir(distConfig)) fileServer = http.FileServer(http.Dir(distConfig))
// 拦截并处理静态资源请求 // 拦截并处理静态资源请求

View File

@@ -74,11 +74,28 @@ func NewClient(baseURL string, proxyStr string, persistCookies bool, timeout int
panic(err) panic(err)
} }
rc.SetTransport(bypass.Transport) rc.SetTransport(&sanitizeTransport{t: bypass.Transport})
return &RestyClient{client: rc} return &RestyClient{client: rc}
} }
// sanitizeTransport 包装 http.RoundTripper 以修复底层库可能违背 Go 接口约定的行为
type sanitizeTransport struct {
t http.RoundTripper
}
func (s *sanitizeTransport) RoundTrip(req *http.Request) (*http.Response, error) {
resp, err := s.t.RoundTrip(req)
// net/http 规定 RoundTripper 要么返回有效的 resp 和 nil error要么返回 nil resp 和有效的 error。
// 某些第三方库(如部分 tls-client 封装)在遇到网络小问题时会同时返回 resp 和 err。
// 这会导致 net/http 打印 "RoundTripper returned a response & error; ignoring response" 并强制丢弃响应。
// 在这里我们进行修正:如果已经拿到了响应(哪怕是不完整的),我们优先保留响应并将 err 置空,让上层通过读取 Body 自行发现错误。
if resp != nil && err != nil {
err = nil
}
return resp, err
}
// fillResponseBody 使用反射强制填充响应体 // fillResponseBody 使用反射强制填充响应体
// 当 Resty 因为重定向策略错误而提前返回时,它可能不会读取 Body // 当 Resty 因为重定向策略错误而提前返回时,它可能不会读取 Body
// 此方法手动读取 RawResponse.Body 并回填到 resty.Response 的私有 body 字段中 // 此方法手动读取 RawResponse.Body 并回填到 resty.Response 的私有 body 字段中

64
utils/path.go Normal file
View File

@@ -0,0 +1,64 @@
package utils
import (
"os"
"path/filepath"
"strings"
)
// GetRootDir 获取当前程序运行的真实根目录
// 能够智能、跨平台地识别是编译后的可执行文件运行,还是通过 `go run` 运行(通常在临时目录下)
func GetRootDir() string {
var baseDir string
// 首先尝试获取当前工作目录
workDir, err := os.Getwd()
if err != nil {
workDir = "."
}
// 获取程序可执行文件所在目录
execPath, err := os.Executable()
if err != nil {
// 如果获取可执行文件路径失败,使用当前工作目录
return workDir
}
// 解析软链接获取真实物理路径macOS 下 /tmp 经常是 /private/tmp 的软链)
realExecPath, err := filepath.EvalSymlinks(execPath)
if err == nil {
execPath = realExecPath
}
execDir := filepath.Dir(execPath)
realTempDir, err := filepath.EvalSymlinks(os.TempDir())
if err != nil {
realTempDir = os.TempDir()
}
// 跨平台安全地判断 execDir 是否在 realTempDir 内部
// 使用 filepath.Rel 可以避免直接 HasPrefix 带来的大小写、路径分隔符以及部分目录名重合的问题
rel, err := filepath.Rel(realTempDir, execDir)
isGoRun := false
if err == nil {
// 如果 rel 不以 ".." 开头,说明 execDir 在 TempDir 内部,即为 go run 模式
if rel != ".." && !strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
isGoRun = true
}
} else {
// fallback: 如果 Rel 失败(例如跨盘符),则退回简单的 HasPrefix 判断(带上分隔符防误判)
cleanTemp := filepath.Clean(realTempDir) + string(os.PathSeparator)
cleanExec := filepath.Clean(execDir) + string(os.PathSeparator)
if strings.HasPrefix(strings.ToLower(cleanExec), strings.ToLower(cleanTemp)) {
isGoRun = true
}
}
if isGoRun {
baseDir = workDir
} else {
baseDir = execDir
}
return baseDir
}